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