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