test_cli.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. #!/usr/bin/env python3
  2. # type: ignore[attr-defined]
  3. #
  4. # Copyright (c) 2024, Arm Limited. All rights reserved.
  5. #
  6. # SPDX-License-Identifier: BSD-3-Clause
  7. #
  8. """Contains unit tests for the CLI functionality."""
  9. from math import ceil, log2
  10. from pathlib import Path
  11. from re import findall, search
  12. from unittest import mock
  13. import pytest
  14. import yaml
  15. from click.testing import CliRunner
  16. from conftest import generate_random_bytes
  17. from tlc.cli import cli
  18. from tlc.te import TransferEntry
  19. from tlc.tl import TransferList
  20. def test_create_empty_tl(tmpdir):
  21. runner = CliRunner()
  22. test_file = tmpdir.join("tl.bin")
  23. result = runner.invoke(cli, ["create", test_file.strpath])
  24. assert result.exit_code == 0
  25. assert TransferList.fromfile(test_file) is not None
  26. @pytest.mark.parametrize("align", [4, 6, 12, 13])
  27. def test_create_with_align(align, tlcrunner, tmpdir):
  28. tl_file = tmpdir.join("tl.bin").strpath
  29. tlcrunner.invoke(cli, ["create", "-s", "10000", "-a", align, tl_file])
  30. blob = tmpdir.join("blob.bin")
  31. blob.write_binary(generate_random_bytes(0x200))
  32. tlcrunner.invoke(cli, ["add", "--entry", 1, blob.strpath, tl_file])
  33. tl = TransferList.fromfile(tl_file)
  34. te = tl.entries[-1]
  35. assert tl.alignment == align
  36. assert (te.offset + te.hdr_size) % (1 << align) == 0
  37. def test_create_with_fdt(tmpdir):
  38. runner = CliRunner()
  39. fdt = tmpdir.join("fdt.dtb")
  40. fdt.write_binary(b"\x00" * 100)
  41. result = runner.invoke(
  42. cli,
  43. [
  44. "create",
  45. "--fdt",
  46. fdt.strpath,
  47. "--size",
  48. "1000",
  49. tmpdir.join("tl.bin").strpath,
  50. ],
  51. )
  52. assert result.exit_code == 0
  53. def test_add_single_entry(tlcrunner, tmptlstr):
  54. tlcrunner.invoke(cli, ["add", "--entry", "0", "/dev/null", tmptlstr])
  55. tl = TransferList.fromfile(tmptlstr)
  56. assert tl is not None
  57. assert len(tl.entries) == 1
  58. assert tl.entries[0].id == 0
  59. def test_add_multiple_entries(tlcrunner, tlc_entries, tmptlstr):
  60. for id, path in tlc_entries:
  61. tlcrunner.invoke(cli, ["add", "--entry", id, path, tmptlstr])
  62. tl = TransferList.fromfile(tmptlstr)
  63. assert tl is not None
  64. assert len(tl.entries) == len(tlc_entries)
  65. @pytest.mark.parametrize("align", [4, 6, 12, 13])
  66. def test_cli_add_entry_with_align(align, tlcrunner, tmpdir, tmptlstr):
  67. blob = tmpdir.join("blob.bin")
  68. blob.write_binary(bytes(0x100))
  69. tlcrunner.invoke(cli, ["add", "--align", align, "--entry", 1, blob, tmptlstr])
  70. tl = TransferList.fromfile(tmptlstr)
  71. te = tl.entries[-1]
  72. print(tl, *(te for te in tl.entries), sep="\n---------------\n")
  73. assert (te.offset + te.hdr_size) % (1 << align) == 0
  74. assert tl.alignment == align
  75. def test_info(tlcrunner, tmptlstr, tmpfdt):
  76. tlcrunner.invoke(cli, ["add", "--entry", "0", "/dev/null", tmptlstr])
  77. tlcrunner.invoke(cli, ["add", "--fdt", tmpfdt.strpath, tmptlstr])
  78. result = tlcrunner.invoke(cli, ["info", tmptlstr])
  79. assert result.exit_code == 0
  80. assert "signature" in result.stdout
  81. assert "id" in result.stdout
  82. result = tlcrunner.invoke(cli, ["info", "--header", tmptlstr])
  83. assert result.exit_code == 0
  84. assert "signature" in result.stdout
  85. assert "id" not in result.stdout
  86. result = tlcrunner.invoke(cli, ["info", "--entries", tmptlstr])
  87. assert result.exit_code == 0
  88. assert "signature" not in result.stdout
  89. assert "id" in result.stdout
  90. def test_raises_max_size_error(tmptlstr, tmpfdt):
  91. tmpfdt.write_binary(bytes(6000))
  92. runner = CliRunner()
  93. result = runner.invoke(cli, ["create", "--fdt", tmpfdt, tmptlstr])
  94. assert result.exception
  95. assert isinstance(result.exception, MemoryError)
  96. assert "TL max size exceeded, consider increasing with the option -s" in str(
  97. result.exception
  98. )
  99. assert "TL size has exceeded the maximum allocation" in str(
  100. result.exception.__cause__
  101. )
  102. def test_info_get_fdt_offset(tmptlstr, tmpfdt):
  103. runner = CliRunner()
  104. with runner.isolated_filesystem():
  105. runner.invoke(cli, ["create", "--size", "1000", tmptlstr])
  106. runner.invoke(cli, ["add", "--entry", "1", tmpfdt.strpath, tmptlstr])
  107. result = runner.invoke(cli, ["info", "--fdt-offset", tmptlstr])
  108. assert result.exit_code == 0
  109. assert result.output.strip("\n").isdigit()
  110. def test_remove_tag(tlcrunner, tmptlstr):
  111. tlcrunner.invoke(cli, ["add", "--entry", "0", "/dev/null", tmptlstr])
  112. result = tlcrunner.invoke(cli, ["info", tmptlstr])
  113. assert result.exit_code == 0
  114. assert "signature" in result.stdout
  115. tlcrunner.invoke(cli, ["remove", "--tags", "0", tmptlstr])
  116. tl = TransferList.fromfile(tmptlstr)
  117. assert result.exit_code == 0
  118. assert len(tl.entries) == 0
  119. def test_unpack_tl(tlcrunner, tmptlstr, tmpfdt, tmpdir):
  120. with tlcrunner.isolated_filesystem(temp_dir=tmpdir):
  121. tlcrunner.invoke(cli, ["add", "--entry", 1, tmpfdt.strpath, tmptlstr])
  122. tlcrunner.invoke(cli, ["unpack", tmptlstr])
  123. assert Path("te_0_1.bin").exists()
  124. def test_unpack_multiple_tes(tlcrunner, tlc_entries, tmptlstr, tmpdir):
  125. with tlcrunner.isolated_filesystem(temp_dir=tmpdir):
  126. for id, path in tlc_entries:
  127. tlcrunner.invoke(cli, ["add", "--entry", id, path, tmptlstr])
  128. assert all(
  129. filter(
  130. lambda te: (Path(tmpdir.strpath) / f"te_{te[0]}.bin").exists(), tlc_entries
  131. )
  132. )
  133. def test_unpack_into_dir(tlcrunner, tmpdir, tmptlstr, tmpfdt):
  134. tlcrunner.invoke(cli, ["add", "--entry", 1, tmpfdt.strpath, tmptlstr])
  135. tlcrunner.invoke(cli, ["unpack", "-C", tmpdir.strpath, tmptlstr])
  136. assert (Path(tmpdir.strpath) / "te_0_1.bin").exists()
  137. def test_unpack_into_dir_with_conflicting_tags(tlcrunner, tmpdir, tmptlstr, tmpfdt):
  138. tlcrunner.invoke(cli, ["add", "--entry", 1, tmpfdt.strpath, tmptlstr])
  139. tlcrunner.invoke(cli, ["add", "--entry", 1, tmpfdt.strpath, tmptlstr])
  140. tlcrunner.invoke(cli, ["unpack", "-C", tmpdir.strpath, tmptlstr])
  141. assert (Path(tmpdir.strpath) / "te_0_1.bin").exists()
  142. assert (Path(tmpdir.strpath) / "te_1_1.bin").exists()
  143. def test_validate_invalid_signature(tmptlstr, tlcrunner, monkeypatch):
  144. tl = TransferList()
  145. tl.signature = 0xDEADBEEF
  146. mock_open = lambda tmptlstr, mode: mock.mock_open(read_data=tl.header_to_bytes())()
  147. monkeypatch.setattr("builtins.open", mock_open)
  148. result = tlcrunner.invoke(cli, ["validate", tmptlstr])
  149. assert result.exit_code != 0
  150. def test_validate_misaligned_entries(tmptlstr, tlcrunner, monkeypatch):
  151. """Base address of a TE must be 8-byte aligned."""
  152. mock_open = lambda tmptlstr, mode: mock.mock_open(
  153. read_data=TransferList().header_to_bytes()
  154. + bytes(5)
  155. + TransferEntry(0, 0, bytes(0)).header_to_bytes
  156. )()
  157. monkeypatch.setattr("builtins.open", mock_open)
  158. result = tlcrunner.invoke(cli, ["validate", tmptlstr])
  159. assert result.exit_code == 1
  160. @pytest.mark.parametrize(
  161. "version", [0, TransferList.version, TransferList.version + 1, 1 << 8]
  162. )
  163. def test_validate_unsupported_version(version, tmptlstr, tlcrunner, monkeypatch):
  164. tl = TransferList()
  165. tl.version = version
  166. mock_open = lambda tmptlstr, mode: mock.mock_open(read_data=tl.header_to_bytes())()
  167. monkeypatch.setattr("builtins.open", mock_open)
  168. result = tlcrunner.invoke(cli, ["validate", tmptlstr])
  169. if version >= TransferList.version and version <= 0xFF:
  170. assert result.exit_code == 0
  171. else:
  172. assert result.exit_code == 1
  173. def test_create_entry_from_yaml_and_blob_file(
  174. tlcrunner, tmpyamlconfig_blob_file, tmptlstr, non_empty_tag_id
  175. ):
  176. tlcrunner.invoke(
  177. cli,
  178. [
  179. "create",
  180. "--from-yaml",
  181. tmpyamlconfig_blob_file.strpath,
  182. tmptlstr,
  183. ],
  184. )
  185. tl = TransferList.fromfile(tmptlstr)
  186. assert tl is not None
  187. assert len(tl.entries) == 1
  188. assert tl.entries[0].id == non_empty_tag_id
  189. @pytest.mark.parametrize(
  190. "entry",
  191. [
  192. {"tag_id": 0},
  193. {
  194. "tag_id": 0x104,
  195. "addr": 0x0400100000000010,
  196. "size": 0x0003300000000000,
  197. },
  198. {
  199. "tag_id": 0x100,
  200. "pp_addr": 100,
  201. },
  202. {
  203. "tag_id": "optee_pageable_part",
  204. "pp_addr": 100,
  205. },
  206. ],
  207. )
  208. def test_create_from_yaml_check_sum_bytes(tlcrunner, tmpyamlconfig, tmptlstr, entry):
  209. """Test creating a TL from a yaml file, but only check that the sum of the
  210. data in the yaml file matches the sum of the data in the TL. This means
  211. you don't have to type the exact sequence of expected bytes. All the data
  212. in the yaml file must be integers (except for the tag IDs, which can be
  213. strings).
  214. """
  215. # create yaml config file
  216. config = {
  217. "has_checksum": True,
  218. "max_size": 0x1000,
  219. "entries": [entry],
  220. }
  221. with open(tmpyamlconfig, "w") as f:
  222. yaml.safe_dump(config, f)
  223. # invoke TLC
  224. tlcrunner.invoke(
  225. cli,
  226. [
  227. "create",
  228. "--from-yaml",
  229. tmpyamlconfig,
  230. tmptlstr,
  231. ],
  232. )
  233. # open created TL, and check
  234. tl = TransferList.fromfile(tmptlstr)
  235. assert tl is not None
  236. assert len(tl.entries) == 1
  237. # Check that the sum of all the data in the transfer entry in the yaml file
  238. # is the same as the sum of all the data in the transfer list. Don't count
  239. # the tag id or the TE headers.
  240. # every item in the entry dict must be an integer
  241. yaml_total = 0
  242. for key, data in iter_nested_dict(entry):
  243. if key != "tag_id":
  244. num_bytes = ceil(log2(data + 1) / 8)
  245. yaml_total += sum(data.to_bytes(num_bytes, "little"))
  246. tl_total = sum(tl.entries[0].data)
  247. assert tl_total == yaml_total
  248. @pytest.mark.parametrize(
  249. "entry,expected",
  250. [
  251. (
  252. {
  253. "tag_id": 0x102,
  254. "ep_info": {
  255. "h": {
  256. "type": 0x01,
  257. "version": 0x02,
  258. "attr": 8,
  259. },
  260. "pc": 67239936,
  261. "spsr": 965,
  262. "args": [67112976, 67112960, 0, 0, 0, 0, 0, 0],
  263. },
  264. },
  265. (
  266. "0x00580201 0x00000008 0x04020000 0x00000000 "
  267. "0x000003C5 0x00000000 0x04001010 0x00000000 "
  268. "0x04001000 0x00000000 0x00000000 0x00000000 "
  269. "0x00000000 0x00000000 0x00000000 0x00000000 "
  270. "0x00000000 0x00000000 0x00000000 0x00000000 "
  271. "0x00000000 0x00000000"
  272. ),
  273. ),
  274. (
  275. {
  276. "tag_id": 0x102,
  277. "ep_info": {
  278. "h": {
  279. "type": 0x01,
  280. "version": 0x02,
  281. "attr": "EP_NON_SECURE | EP_ST_ENABLE",
  282. },
  283. "pc": 67239936,
  284. "spsr": 965,
  285. "args": [67112976, 67112960, 0, 0, 0, 0, 0, 0],
  286. },
  287. },
  288. (
  289. "0x00580201 0x00000005 0x04020000 0x00000000 "
  290. "0x000003C5 0x00000000 0x04001010 0x00000000 "
  291. "0x04001000 0x00000000 0x00000000 0x00000000 "
  292. "0x00000000 0x00000000 0x00000000 0x00000000 "
  293. "0x00000000 0x00000000 0x00000000 0x00000000 "
  294. "0x00000000 0x00000000"
  295. ),
  296. ),
  297. ],
  298. )
  299. def test_create_from_yaml_check_exact_data(
  300. tlcrunner, tmpyamlconfig, tmptlstr, entry, expected
  301. ):
  302. """Test creating a TL from a yaml file, checking the exact sequence of
  303. bytes. This is useful for checking that the alignment is correct. You can
  304. get the expected sequence of bytes by copying it from the ArmDS debugger.
  305. """
  306. # create yaml config file
  307. config = {
  308. "has_checksum": True,
  309. "max_size": 0x1000,
  310. "entries": [entry],
  311. }
  312. with open(tmpyamlconfig, "w") as f:
  313. yaml.safe_dump(config, f)
  314. # invoke TLC
  315. tlcrunner.invoke(
  316. cli,
  317. [
  318. "create",
  319. "--from-yaml",
  320. tmpyamlconfig,
  321. tmptlstr,
  322. ],
  323. )
  324. # open TL and check
  325. tl = TransferList.fromfile(tmptlstr)
  326. assert tl is not None
  327. assert len(tl.entries) == 1
  328. # check expected and actual data
  329. actual = tl.entries[0].data
  330. actual = bytes_to_hex(actual)
  331. assert actual == expected
  332. @pytest.mark.parametrize("option", ["-O", "--output"])
  333. def test_gen_tl_header_with_output_name(tlcrunner, tmptlstr, option, filename="test.h"):
  334. with tlcrunner.isolated_filesystem():
  335. result = tlcrunner.invoke(
  336. cli,
  337. [
  338. "gen-header",
  339. option,
  340. filename,
  341. tmptlstr,
  342. ],
  343. )
  344. assert result.exit_code == 0
  345. assert Path(filename).exists()
  346. def test_gen_tl_with_fdt_header(tmptlstr, tmpfdt):
  347. tlcrunner = CliRunner()
  348. with tlcrunner.isolated_filesystem():
  349. tlcrunner.invoke(cli, ["create", "--size", 1000, "--fdt", tmpfdt, tmptlstr])
  350. result = tlcrunner.invoke(
  351. cli,
  352. [
  353. "gen-header",
  354. tmptlstr,
  355. ],
  356. )
  357. assert result.exit_code == 0
  358. assert Path("header.h").exists()
  359. with open("header.h", "r") as f:
  360. dtb_match = search(r"DTB_OFFSET\s+(\d+)", "".join(f.readlines()))
  361. assert dtb_match and dtb_match[1].isnumeric()
  362. def test_gen_empty_tl_c_header(tlcrunner, tmptlstr):
  363. with tlcrunner.isolated_filesystem():
  364. result = tlcrunner.invoke(
  365. cli,
  366. [
  367. "gen-header",
  368. tmptlstr,
  369. ],
  370. )
  371. assert result.exit_code == 0
  372. assert Path("header.h").exists()
  373. with open("header.h", "r") as f:
  374. lines = "".join(f.readlines())
  375. assert TransferList.hdr_size == int(
  376. findall(r"SIZE\s+(0x[0-9a-fA-F]+|\d+)", lines)[0], 16
  377. )
  378. assert TransferList.version == int(
  379. findall(r"VERSION.+(0x[0-9a-fA-F]+|\d+)", lines)[0]
  380. )
  381. def bytes_to_hex(data: bytes) -> str:
  382. """Convert bytes to a hex string in the same format as the debugger in
  383. ArmDS
  384. You can copy data from the debugger in Arm Development Studio and put it
  385. into a unit test. You can then run this function on the output from tlc,
  386. and compare it to the data you copied.
  387. The format is groups of 4 bytes with 0x prefixes separated by spaces.
  388. Little endian is used.
  389. """
  390. words_hex = []
  391. for i in range(0, len(data), 4):
  392. word = data[i : i + 4]
  393. word_int = int.from_bytes(word, "little")
  394. word_hex = "0x" + f"{word_int:0>8x}".upper()
  395. words_hex.append(word_hex)
  396. return " ".join(words_hex)
  397. def iter_nested_dict(dictionary: dict):
  398. for key, value in dictionary.items():
  399. if isinstance(value, dict):
  400. yield from iter_nested_dict(value)
  401. else:
  402. yield key, value