Replace argument recovery with direct build targets. Run host, target, board, coverage, Miri and selected Kani checks while retaining source identities, logs and artifacts, including failed attempts. Keep the PID proof diagnostic exception explicit and match its exact identity alongside passing contract assertions and covers. Validation: final-source verification matrix, assembled workflow tests and documentation build.
322 lines
13 KiB
Python
322 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""Exercise board Makefiles with fake Cargo, image creation, and DFU tools."""
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
|
|
FIRMWARE = Path(__file__).resolve().parents[1]
|
|
BOARDS = ("geprc-f722-v2", "kakute-f7-mini-v3")
|
|
TARGET = "thumbv7em-none-eabihf"
|
|
FAKE_TOOL = """#!/usr/bin/env python3
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
tool = Path(sys.argv[0]).name
|
|
args = sys.argv[1:]
|
|
with open(os.environ["WORKFLOW_LOG"], "a") as log:
|
|
log.write(json.dumps({"tool": tool, "args": args}) + "\\n")
|
|
if os.environ.get("FAIL_TOOL") == tool:
|
|
sys.exit(1)
|
|
if tool == "cargo":
|
|
if args[0] != "build" or not all(
|
|
flag in args for flag in ("--release", "--locked", "--offline")
|
|
):
|
|
raise ValueError("expected locked offline release build")
|
|
directory = Path(args[args.index("--target-dir") + 1])
|
|
directory /= args[args.index("--target") + 1]
|
|
directory /= "release"
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
for index, arg in enumerate(args):
|
|
if arg == "--bin":
|
|
binary = args[index + 1]
|
|
(directory / binary).write_text("fresh " + binary)
|
|
elif tool == "create-dfu-image.sh":
|
|
if len(args) != 1:
|
|
raise ValueError("expected one ELF")
|
|
elf = Path(args[0])
|
|
if elf.read_text() != "fresh " + elf.name:
|
|
raise ValueError("stale ELF")
|
|
Path(str(elf) + ".bin").write_text(elf.read_text())
|
|
elif tool == "dfu-util":
|
|
image = Path(args[args.index("--download") + 1])
|
|
if image.read_text() != "fresh " + image.stem:
|
|
raise ValueError("stale image")
|
|
else:
|
|
raise AssertionError(tool)
|
|
"""
|
|
|
|
|
|
class BoardWorkflowTests(unittest.TestCase):
|
|
def setUp(self):
|
|
temporary = tempfile.TemporaryDirectory(prefix="kaptan-workflow-")
|
|
self.addCleanup(temporary.cleanup)
|
|
self.root = Path(temporary.name)
|
|
self.boards = self.root / "firmware" / "boards"
|
|
self.boards.mkdir(parents=True)
|
|
for relative in ("Makefile", "board.mk", *(f"{b}/Makefile" for b in BOARDS)):
|
|
destination = self.boards / relative
|
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copyfile(FIRMWARE / "boards" / relative, destination)
|
|
for relative in (
|
|
"bin/cargo",
|
|
"bin/dfu-util",
|
|
"firmware/scripts/create-dfu-image.sh",
|
|
):
|
|
tool = self.root / relative
|
|
tool.parent.mkdir(parents=True, exist_ok=True)
|
|
tool.write_text(FAKE_TOOL)
|
|
tool.chmod(0o755)
|
|
self.log = self.root / "events.jsonl"
|
|
self.environment = dict(os.environ)
|
|
for name in ("CARGO_TARGET_DIR", "MAKEFLAGS", "MFLAGS", "MAKEOVERRIDES"):
|
|
self.environment.pop(name, None)
|
|
self.environment.update(
|
|
PATH=f"{self.root / 'bin'}{os.pathsep}{os.environ['PATH']}",
|
|
WORKFLOW_LOG=str(self.log),
|
|
)
|
|
# A wrong packaging path must fail even when old build outputs exist.
|
|
self.seed_stale(self.root / "firmware" / "target")
|
|
|
|
def seed_stale(self, target_dir):
|
|
release = target_dir / TARGET / "release"
|
|
release.mkdir(parents=True, exist_ok=True)
|
|
for board in BOARDS:
|
|
for suffix in ("", ".bin"):
|
|
(release / f"kaptan-{board}{suffix}").write_text("stale")
|
|
|
|
def run_make(self, directory, goal, target_dir=None, fail="", variables=()):
|
|
self.log.unlink(missing_ok=True)
|
|
environment = dict(self.environment, FAIL_TOOL=fail)
|
|
if target_dir is not None:
|
|
environment["CARGO_TARGET_DIR"] = target_dir
|
|
result = subprocess.run(
|
|
[
|
|
"make",
|
|
"--no-print-directory",
|
|
"-j2",
|
|
"-C",
|
|
str(directory),
|
|
goal,
|
|
*variables,
|
|
],
|
|
env=environment,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
)
|
|
events = (
|
|
[json.loads(line) for line in self.log.read_text().splitlines()]
|
|
if self.log.exists()
|
|
else []
|
|
)
|
|
return result, events
|
|
|
|
def test_selected_board_artifact_directory_and_identity(self):
|
|
for board in BOARDS:
|
|
directory = self.boards / board
|
|
for target_dir in (
|
|
None,
|
|
"local artifacts",
|
|
str(self.root / "external artifacts"),
|
|
):
|
|
with self.subTest(board=board, target_dir=target_dir):
|
|
result, events = self.run_make(
|
|
directory,
|
|
"flash",
|
|
target_dir,
|
|
variables=(
|
|
"BOARD=wrong",
|
|
"PACKAGE=wrong",
|
|
"EMBEDDED_TARGET=wrong",
|
|
),
|
|
)
|
|
self.assertEqual(
|
|
result.returncode, 0, result.stdout + result.stderr
|
|
)
|
|
self.assertEqual(
|
|
[event["tool"] for event in events],
|
|
["cargo", "create-dfu-image.sh", "dfu-util"],
|
|
)
|
|
args = events[0]["args"]
|
|
self.assertEqual(args[args.index("-p") + 1], f"kaptan-{board}")
|
|
self.assertEqual(args[args.index("--bin") + 1], f"kaptan-{board}")
|
|
self.assertEqual(args[args.index("--target") + 1], TARGET)
|
|
expected = directory / (target_dir or "../../target")
|
|
self.assertEqual(
|
|
(directory / args[args.index("--target-dir") + 1]).resolve(),
|
|
expected.resolve(),
|
|
)
|
|
elf = expected / TARGET / "release" / f"kaptan-{board}"
|
|
self.assertEqual(
|
|
(directory / events[1]["args"][0]).resolve(), elf.resolve()
|
|
)
|
|
self.assertEqual(
|
|
(directory / events[2]["args"][-1]).resolve(),
|
|
Path(str(elf) + ".bin").resolve(),
|
|
)
|
|
|
|
def test_all_boards_use_one_artifact_directory(self):
|
|
for target_dir in (
|
|
None,
|
|
"local artifacts",
|
|
str(self.root / "external artifacts"),
|
|
):
|
|
with self.subTest(target_dir=target_dir):
|
|
result, events = self.run_make(self.boards, "build", target_dir)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
self.assertEqual(
|
|
[event["tool"] for event in events],
|
|
["cargo", "create-dfu-image.sh", "create-dfu-image.sh"],
|
|
)
|
|
expected = self.boards / (target_dir or "../target")
|
|
args = events[0]["args"]
|
|
self.assertEqual(
|
|
(self.boards / args[args.index("--target-dir") + 1]).resolve(),
|
|
expected.resolve(),
|
|
)
|
|
self.assertEqual(
|
|
[
|
|
(self.boards / event["args"][0]).resolve()
|
|
for event in events[1:]
|
|
],
|
|
[
|
|
(expected / TARGET / "release" / f"kaptan-{b}").resolve()
|
|
for b in BOARDS
|
|
],
|
|
)
|
|
|
|
def test_failures_stop_packaging_and_flashing_even_with_stale_outputs(self):
|
|
target_dir = self.root / "external artifacts"
|
|
for directory, goal in (
|
|
(self.boards, "build"),
|
|
*((self.boards / board, "flash") for board in BOARDS),
|
|
):
|
|
for failed_tool in ("cargo", "create-dfu-image.sh"):
|
|
with self.subTest(directory=directory, failed_tool=failed_tool):
|
|
self.seed_stale(target_dir)
|
|
result, events = self.run_make(
|
|
directory, goal, str(target_dir), failed_tool
|
|
)
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertEqual(
|
|
[event["tool"] for event in events],
|
|
["cargo"] if failed_tool == "cargo" else ["cargo", failed_tool],
|
|
)
|
|
for board in BOARDS:
|
|
image = target_dir / TARGET / "release" / f"kaptan-{board}.bin"
|
|
self.assertEqual(image.read_text(), "stale")
|
|
|
|
|
|
class PackagingValidationTests(unittest.TestCase):
|
|
def test_real_packager_validates_sram_and_bounds_before_objcopy(self):
|
|
fake = """#!/usr/bin/env python3
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
name = Path(sys.argv[0]).name
|
|
if name == "rustc":
|
|
print("host: test-host" if sys.argv[1] == "-vV" else os.environ["FAKE_SYSROOT"])
|
|
elif name == "llvm-readobj":
|
|
print(os.environ["ELF_METADATA"])
|
|
elif name == "llvm-objdump":
|
|
print("20000000: bx lr")
|
|
elif name == "llvm-objcopy":
|
|
Path(os.environ["OBJCOPY_LOG"]).touch()
|
|
Path(sys.argv[-1]).write_bytes(b"validated image")
|
|
else:
|
|
raise AssertionError(name)
|
|
"""
|
|
with tempfile.TemporaryDirectory(prefix="kaptan-package-") as temporary:
|
|
root = Path(temporary)
|
|
tools = root / "lib/rustlib/test-host/bin"
|
|
tools.mkdir(parents=True)
|
|
for path in (
|
|
root / "rustc",
|
|
*(tools / f"llvm-{tool}" for tool in ("readobj", "objdump", "objcopy")),
|
|
):
|
|
path.write_text(fake)
|
|
path.chmod(0o755)
|
|
elf, memory, log = (
|
|
root / "arbitrary-name.elf",
|
|
root / "memory.x",
|
|
root / "objcopy-called",
|
|
)
|
|
elf.touch()
|
|
image = elf.with_suffix(".bin")
|
|
memory.write_text(
|
|
"FLASH : ORIGIN = 0x08000000, LENGTH = 256\nRAM : ORIGIN = 0x20000000, LENGTH = 128\n"
|
|
)
|
|
for executable, size, accepted in (
|
|
(True, 16, True),
|
|
(False, 16, False),
|
|
(True, 129, False),
|
|
):
|
|
with self.subTest(executable=executable, size=size):
|
|
image.write_bytes(b"previous image")
|
|
log.unlink(missing_ok=True)
|
|
flags = [{"Name": "SHF_ALLOC"}]
|
|
if executable:
|
|
flags.append({"Name": "SHF_EXECINSTR"})
|
|
metadata = [
|
|
{
|
|
"Sections": [
|
|
{
|
|
"Section": {
|
|
"Name": {"Name": ".data"},
|
|
"Size": size,
|
|
"Address": 0x20000000,
|
|
"Flags": {"Flags": flags},
|
|
}
|
|
}
|
|
],
|
|
"ProgramHeaders": [
|
|
{
|
|
"ProgramHeader": {
|
|
"Type": {"Name": "PT_LOAD"},
|
|
"VirtualAddress": 0x20000000,
|
|
"PhysicalAddress": 0x08000000,
|
|
"FileSize": size,
|
|
"MemSize": size,
|
|
}
|
|
}
|
|
],
|
|
}
|
|
]
|
|
result = subprocess.run(
|
|
[
|
|
str(FIRMWARE / "scripts/create-dfu-image.sh"),
|
|
str(elf),
|
|
str(memory),
|
|
],
|
|
env=dict(
|
|
os.environ,
|
|
PATH=f"{root}{os.pathsep}{os.environ['PATH']}",
|
|
FAKE_SYSROOT=str(root),
|
|
ELF_METADATA=json.dumps(metadata),
|
|
OBJCOPY_LOG=str(log),
|
|
),
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
)
|
|
self.assertEqual(
|
|
result.returncode == 0, accepted, result.stdout + result.stderr
|
|
)
|
|
self.assertEqual(log.exists(), accepted)
|
|
self.assertEqual(
|
|
image.read_bytes(),
|
|
b"validated image" if accepted else b"previous image",
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|