You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
413 lines
12 KiB
413 lines
12 KiB
#!/usr/bin/env python3
|
|
# type: ignore[attr-defined]
|
|
|
|
#
|
|
# Copyright (c) 2024, Arm Limited. All rights reserved.
|
|
#
|
|
# SPDX-License-Identifier: BSD-3-Clause
|
|
#
|
|
|
|
"""Contains unit tests for the CLI functionality."""
|
|
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
from math import log2, ceil
|
|
|
|
import pytest
|
|
import pytest
|
|
import yaml
|
|
from click.testing import CliRunner
|
|
|
|
from tlc.cli import cli
|
|
from tlc.te import TransferEntry
|
|
from tlc.tl import TransferList
|
|
|
|
|
|
def test_create_empty_tl(tmpdir):
|
|
runner = CliRunner()
|
|
test_file = tmpdir.join("tl.bin")
|
|
|
|
result = runner.invoke(cli, ["create", test_file.strpath])
|
|
assert result.exit_code == 0
|
|
assert TransferList.fromfile(test_file) is not None
|
|
|
|
|
|
def test_create_with_fdt(tmpdir):
|
|
runner = CliRunner()
|
|
fdt = tmpdir.join("fdt.dtb")
|
|
fdt.write_binary(b"\x00" * 100)
|
|
|
|
result = runner.invoke(
|
|
cli,
|
|
[
|
|
"create",
|
|
"--fdt",
|
|
fdt.strpath,
|
|
"--size",
|
|
"1000",
|
|
tmpdir.join("tl.bin").strpath,
|
|
],
|
|
)
|
|
assert result.exit_code == 0
|
|
|
|
|
|
def test_add_single_entry(tlcrunner, tmptlstr):
|
|
tlcrunner.invoke(cli, ["add", "--entry", "0", "/dev/null", tmptlstr])
|
|
|
|
tl = TransferList.fromfile(tmptlstr)
|
|
assert tl is not None
|
|
assert len(tl.entries) == 1
|
|
assert tl.entries[0].id == 0
|
|
|
|
|
|
def test_add_multiple_entries(tlcrunner, tlc_entries, tmptlstr):
|
|
for id, path in tlc_entries:
|
|
tlcrunner.invoke(cli, ["add", "--entry", id, path, tmptlstr])
|
|
|
|
tl = TransferList.fromfile(tmptlstr)
|
|
assert tl is not None
|
|
assert len(tl.entries) == len(tlc_entries)
|
|
|
|
|
|
def test_info(tlcrunner, tmptlstr, tmpfdt):
|
|
tlcrunner.invoke(cli, ["add", "--entry", "0", "/dev/null", tmptlstr])
|
|
tlcrunner.invoke(cli, ["add", "--fdt", tmpfdt.strpath, tmptlstr])
|
|
|
|
result = tlcrunner.invoke(cli, ["info", tmptlstr])
|
|
assert result.exit_code == 0
|
|
assert "signature" in result.stdout
|
|
assert "id" in result.stdout
|
|
|
|
result = tlcrunner.invoke(cli, ["info", "--header", tmptlstr])
|
|
assert result.exit_code == 0
|
|
assert "signature" in result.stdout
|
|
assert "id" not in result.stdout
|
|
|
|
result = tlcrunner.invoke(cli, ["info", "--entries", tmptlstr])
|
|
assert result.exit_code == 0
|
|
assert "signature" not in result.stdout
|
|
assert "id" in result.stdout
|
|
|
|
|
|
def test_raises_max_size_error(tmptlstr, tmpfdt):
|
|
tmpfdt.write_binary(bytes(6000))
|
|
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["create", "--fdt", tmpfdt, tmptlstr])
|
|
|
|
assert result.exception
|
|
assert isinstance(result.exception, MemoryError)
|
|
assert "TL max size exceeded, consider increasing with the option -s" in str(
|
|
result.exception
|
|
)
|
|
assert "TL size has exceeded the maximum allocation" in str(
|
|
result.exception.__cause__
|
|
)
|
|
|
|
|
|
def test_info_get_fdt_offset(tmptlstr, tmpfdt):
|
|
runner = CliRunner()
|
|
with runner.isolated_filesystem():
|
|
runner.invoke(cli, ["create", "--size", "1000", tmptlstr])
|
|
runner.invoke(cli, ["add", "--entry", "1", tmpfdt.strpath, tmptlstr])
|
|
result = runner.invoke(cli, ["info", "--fdt-offset", tmptlstr])
|
|
|
|
assert result.exit_code == 0
|
|
assert result.output.strip("\n").isdigit()
|
|
|
|
|
|
def test_remove_tag(tlcrunner, tmptlstr):
|
|
tlcrunner.invoke(cli, ["add", "--entry", "0", "/dev/null", tmptlstr])
|
|
result = tlcrunner.invoke(cli, ["info", tmptlstr])
|
|
|
|
assert result.exit_code == 0
|
|
assert "signature" in result.stdout
|
|
|
|
tlcrunner.invoke(cli, ["remove", "--tags", "0", tmptlstr])
|
|
tl = TransferList.fromfile(tmptlstr)
|
|
|
|
assert result.exit_code == 0
|
|
assert len(tl.entries) == 0
|
|
|
|
|
|
def test_unpack_tl(tlcrunner, tmptlstr, tmpfdt, tmpdir):
|
|
with tlcrunner.isolated_filesystem(temp_dir=tmpdir):
|
|
tlcrunner.invoke(cli, ["add", "--entry", 1, tmpfdt.strpath, tmptlstr])
|
|
tlcrunner.invoke(cli, ["unpack", tmptlstr])
|
|
assert Path("te_0_1.bin").exists()
|
|
|
|
|
|
def test_unpack_multiple_tes(tlcrunner, tlc_entries, tmptlstr, tmpdir):
|
|
with tlcrunner.isolated_filesystem(temp_dir=tmpdir):
|
|
for id, path in tlc_entries:
|
|
tlcrunner.invoke(cli, ["add", "--entry", id, path, tmptlstr])
|
|
|
|
assert all(
|
|
filter(
|
|
lambda te: (Path(tmpdir.strpath) / f"te_{te[0]}.bin").exists(), tlc_entries
|
|
)
|
|
)
|
|
|
|
|
|
def test_unpack_into_dir(tlcrunner, tmpdir, tmptlstr, tmpfdt):
|
|
tlcrunner.invoke(cli, ["add", "--entry", 1, tmpfdt.strpath, tmptlstr])
|
|
tlcrunner.invoke(cli, ["unpack", "-C", tmpdir.strpath, tmptlstr])
|
|
|
|
assert (Path(tmpdir.strpath) / "te_0_1.bin").exists()
|
|
|
|
|
|
def test_unpack_into_dir_with_conflicting_tags(tlcrunner, tmpdir, tmptlstr, tmpfdt):
|
|
tlcrunner.invoke(cli, ["add", "--entry", 1, tmpfdt.strpath, tmptlstr])
|
|
tlcrunner.invoke(cli, ["add", "--entry", 1, tmpfdt.strpath, tmptlstr])
|
|
tlcrunner.invoke(cli, ["unpack", "-C", tmpdir.strpath, tmptlstr])
|
|
|
|
assert (Path(tmpdir.strpath) / "te_0_1.bin").exists()
|
|
assert (Path(tmpdir.strpath) / "te_1_1.bin").exists()
|
|
|
|
|
|
def test_validate_invalid_signature(tmptlstr, tlcrunner, monkeypatch):
|
|
tl = TransferList()
|
|
tl.signature = 0xDEADBEEF
|
|
|
|
mock_open = lambda tmptlstr, mode: mock.mock_open(read_data=tl.header_to_bytes())()
|
|
monkeypatch.setattr("builtins.open", mock_open)
|
|
|
|
result = tlcrunner.invoke(cli, ["validate", tmptlstr])
|
|
assert result.exit_code != 0
|
|
|
|
|
|
def test_validate_misaligned_entries(tmptlstr, tlcrunner, monkeypatch):
|
|
"""Base address of a TE must be 8-byte aligned."""
|
|
mock_open = lambda tmptlstr, mode: mock.mock_open(
|
|
read_data=TransferList().header_to_bytes()
|
|
+ bytes(5)
|
|
+ TransferEntry(0, 0, bytes(0)).header_to_bytes
|
|
)()
|
|
monkeypatch.setattr("builtins.open", mock_open)
|
|
|
|
result = tlcrunner.invoke(cli, ["validate", tmptlstr])
|
|
|
|
assert result.exit_code == 1
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"version", [0, TransferList.version, TransferList.version + 1, 1 << 8]
|
|
)
|
|
def test_validate_unsupported_version(version, tmptlstr, tlcrunner, monkeypatch):
|
|
tl = TransferList()
|
|
tl.version = version
|
|
|
|
mock_open = lambda tmptlstr, mode: mock.mock_open(read_data=tl.header_to_bytes())()
|
|
monkeypatch.setattr("builtins.open", mock_open)
|
|
|
|
result = tlcrunner.invoke(cli, ["validate", tmptlstr])
|
|
|
|
if version >= TransferList.version and version <= 0xFF:
|
|
assert result.exit_code == 0
|
|
else:
|
|
assert result.exit_code == 1
|
|
|
|
|
|
def test_create_entry_from_yaml_and_blob_file(
|
|
tlcrunner, tmpyamlconfig_blob_file, tmptlstr, non_empty_tag_id
|
|
):
|
|
tlcrunner.invoke(
|
|
cli,
|
|
[
|
|
"create",
|
|
"--from-yaml",
|
|
tmpyamlconfig_blob_file.strpath,
|
|
tmptlstr,
|
|
],
|
|
)
|
|
|
|
tl = TransferList.fromfile(tmptlstr)
|
|
assert tl is not None
|
|
assert len(tl.entries) == 1
|
|
assert tl.entries[0].id == non_empty_tag_id
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"entry",
|
|
[
|
|
{"tag_id": 0},
|
|
{
|
|
"tag_id": 0x104,
|
|
"addr": 0x0400100000000010,
|
|
"size": 0x0003300000000000,
|
|
},
|
|
{
|
|
"tag_id": 0x100,
|
|
"pp_addr": 100,
|
|
},
|
|
{
|
|
"tag_id": "optee_pageable_part",
|
|
"pp_addr": 100,
|
|
},
|
|
],
|
|
)
|
|
def test_create_from_yaml_check_sum_bytes(tlcrunner, tmpyamlconfig, tmptlstr, entry):
|
|
"""Test creating a TL from a yaml file, but only check that the sum of the
|
|
data in the yaml file matches the sum of the data in the TL. This means
|
|
you don't have to type the exact sequence of expected bytes. All the data
|
|
in the yaml file must be integers (except for the tag IDs, which can be
|
|
strings).
|
|
"""
|
|
# create yaml config file
|
|
config = {
|
|
"has_checksum": True,
|
|
"max_size": 0x1000,
|
|
"entries": [entry],
|
|
}
|
|
with open(tmpyamlconfig, "w") as f:
|
|
yaml.safe_dump(config, f)
|
|
|
|
# invoke TLC
|
|
tlcrunner.invoke(
|
|
cli,
|
|
[
|
|
"create",
|
|
"--from-yaml",
|
|
tmpyamlconfig,
|
|
tmptlstr,
|
|
],
|
|
)
|
|
|
|
# open created TL, and check
|
|
tl = TransferList.fromfile(tmptlstr)
|
|
assert tl is not None
|
|
assert len(tl.entries) == 1
|
|
|
|
# Check that the sum of all the data in the transfer entry in the yaml file
|
|
# is the same as the sum of all the data in the transfer list. Don't count
|
|
# the tag id or the TE headers.
|
|
|
|
# every item in the entry dict must be an integer
|
|
yaml_total = 0
|
|
for key, data in iter_nested_dict(entry):
|
|
if key != "tag_id":
|
|
num_bytes = ceil(log2(data + 1) / 8)
|
|
yaml_total += sum(data.to_bytes(num_bytes, "little"))
|
|
|
|
tl_total = sum(tl.entries[0].data)
|
|
|
|
assert tl_total == yaml_total
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"entry,expected",
|
|
[
|
|
(
|
|
{
|
|
"tag_id": 0x102,
|
|
"ep_info": {
|
|
"h": {
|
|
"type": 0x01,
|
|
"version": 0x02,
|
|
"attr": 8,
|
|
},
|
|
"pc": 67239936,
|
|
"spsr": 965,
|
|
"args": [67112976, 67112960, 0, 0, 0, 0, 0, 0],
|
|
},
|
|
},
|
|
(
|
|
"0x00580201 0x00000008 0x04020000 0x00000000 "
|
|
"0x000003C5 0x00000000 0x04001010 0x00000000 "
|
|
"0x04001000 0x00000000 0x00000000 0x00000000 "
|
|
"0x00000000 0x00000000 0x00000000 0x00000000 "
|
|
"0x00000000 0x00000000 0x00000000 0x00000000 "
|
|
"0x00000000 0x00000000"
|
|
),
|
|
),
|
|
(
|
|
{
|
|
"tag_id": 0x102,
|
|
"ep_info": {
|
|
"h": {
|
|
"type": 0x01,
|
|
"version": 0x02,
|
|
"attr": "EP_NON_SECURE | EP_ST_ENABLE",
|
|
},
|
|
"pc": 67239936,
|
|
"spsr": 965,
|
|
"args": [67112976, 67112960, 0, 0, 0, 0, 0, 0],
|
|
},
|
|
},
|
|
(
|
|
"0x00580201 0x00000005 0x04020000 0x00000000 "
|
|
"0x000003C5 0x00000000 0x04001010 0x00000000 "
|
|
"0x04001000 0x00000000 0x00000000 0x00000000 "
|
|
"0x00000000 0x00000000 0x00000000 0x00000000 "
|
|
"0x00000000 0x00000000 0x00000000 0x00000000 "
|
|
"0x00000000 0x00000000"
|
|
),
|
|
),
|
|
],
|
|
)
|
|
def test_create_from_yaml_check_exact_data(
|
|
tlcrunner, tmpyamlconfig, tmptlstr, entry, expected
|
|
):
|
|
"""Test creating a TL from a yaml file, checking the exact sequence of
|
|
bytes. This is useful for checking that the alignment is correct. You can
|
|
get the expected sequence of bytes by copying it from the ArmDS debugger.
|
|
"""
|
|
# create yaml config file
|
|
config = {
|
|
"has_checksum": True,
|
|
"max_size": 0x1000,
|
|
"entries": [entry],
|
|
}
|
|
with open(tmpyamlconfig, "w") as f:
|
|
yaml.safe_dump(config, f)
|
|
|
|
# invoke TLC
|
|
tlcrunner.invoke(
|
|
cli,
|
|
[
|
|
"create",
|
|
"--from-yaml",
|
|
tmpyamlconfig,
|
|
tmptlstr,
|
|
],
|
|
)
|
|
|
|
# open TL and check
|
|
tl = TransferList.fromfile(tmptlstr)
|
|
assert tl is not None
|
|
assert len(tl.entries) == 1
|
|
|
|
# check expected and actual data
|
|
actual = tl.entries[0].data
|
|
actual = bytes_to_hex(actual)
|
|
|
|
assert actual == expected
|
|
|
|
|
|
def bytes_to_hex(data: bytes) -> str:
|
|
"""Convert bytes to a hex string in the same format as the debugger in
|
|
ArmDS
|
|
|
|
You can copy data from the debugger in Arm Development Studio and put it
|
|
into a unit test. You can then run this function on the output from tlc,
|
|
and compare it to the data you copied.
|
|
|
|
The format is groups of 4 bytes with 0x prefixes separated by spaces.
|
|
Little endian is used.
|
|
"""
|
|
words_hex = []
|
|
for i in range(0, len(data), 4):
|
|
word = data[i : i + 4]
|
|
word_int = int.from_bytes(word, "little")
|
|
word_hex = "0x" + f"{word_int:0>8x}".upper()
|
|
words_hex.append(word_hex)
|
|
|
|
return " ".join(words_hex)
|
|
|
|
|
|
def iter_nested_dict(dictionary: dict):
|
|
for key, value in dictionary.items():
|
|
if isinstance(value, dict):
|
|
yield from iter_nested_dict(value)
|
|
else:
|
|
yield key, value
|
|
|