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