diff --git a/target/ast10x0/defs.bzl b/target/ast10x0/defs.bzl index 6a2690210..dc2078207 100644 --- a/target/ast10x0/defs.bzl +++ b/target/ast10x0/defs.bzl @@ -9,7 +9,7 @@ TARGET_COMPATIBLE_WITH = select({ "//conditions:default": ["@platforms//:incompatible"], }) -def _system_image_test_impl(ctx): +def _system_image_test_impl(ctx, extra_runfiles = []): image_info = ctx.attr.image[SystemImageInfo] executable_symlink = ctx.actions.declare_file(ctx.label.name) ctx.actions.symlink(output = executable_symlink, target_file = image_info.elf) @@ -31,15 +31,15 @@ def _system_image_test_impl(ctx): runfiles.merge(ctx.attr.slave_image[DefaultInfo].default_runfiles), ) + if extra_runfiles: + runfiles = runfiles.merge(ctx.runfiles(files = extra_runfiles)) + return [DefaultInfo( executable = executable_symlink, runfiles = runfiles, )] def _flash_system_image_test_impl(ctx): - default_info = _system_image_test_impl(ctx)[0] - providers = [default_info] - # fmc_model describes the QEMU FMC device uniformly (JEDEC ID + SFDP # geometry), shared by both chip selects. The qemu_runner seeds fresh # images at $TEST_TMPDIR/ and attaches each present CS image as @@ -54,6 +54,19 @@ def _flash_system_image_test_impl(ctx): if ctx.attr.cs1_image: env["AST10X0_CS1_IMAGE"] = ctx.attr.cs1_image env["AST10X0_CS1_FILL"] = str(ctx.attr.cs1_fill) + + # A contents file is copied into the image instead of filling it, so the + # device already holds something when the guest boots. The runner opens + # the path relative to the runfiles tree the test runs in. + contents = [] + if ctx.file.cs0_contents: + contents.append(ctx.file.cs0_contents) + env["AST10X0_CS0_CONTENTS"] = ctx.file.cs0_contents.short_path + if ctx.file.cs1_contents: + contents.append(ctx.file.cs1_contents) + env["AST10X0_CS1_CONTENTS"] = ctx.file.cs1_contents.short_path + + providers = [_system_image_test_impl(ctx, extra_runfiles = contents)[0]] if ctx.attr.cs0_image or ctx.attr.cs1_image: providers.append(RunEnvironmentInfo(environment = env)) return providers @@ -83,6 +96,11 @@ flash_system_image_test = rule( implementation = _flash_system_image_test_impl, test = True, attrs = { + "cs0_contents": attr.label( + doc = "File copied into cs0_image at offset 0 instead of filling " + + "it. The rest stays erased. Longer than flash_size is an error.", + allow_single_file = True, + ), "cs0_fill": attr.int( doc = "Byte value the qemu_runner seeds cs0_image with (default 0xFF, erased).", default = 0xFF, @@ -92,6 +110,11 @@ flash_system_image_test = rule( "attached as FMC CS0 flash (if=mtd, index=0).", default = "", ), + "cs1_contents": attr.label( + doc = "File copied into cs1_image at offset 0 instead of filling " + + "it. The rest stays erased. Longer than flash_size is an error.", + allow_single_file = True, + ), "cs1_fill": attr.int( doc = "Byte value the qemu_runner seeds cs1_image with (default 0xFF, erased).", default = 0xFF, diff --git a/target/ast10x0/harness/BUILD.bazel b/target/ast10x0/harness/BUILD.bazel index 419fab50e..aa53726f4 100644 --- a/target/ast10x0/harness/BUILD.bazel +++ b/target/ast10x0/harness/BUILD.bazel @@ -3,6 +3,7 @@ load("@rules_platform//platform_data:defs.bzl", "platform_data") load("@rules_python//python:py_binary.bzl", "py_binary") +load("@rules_python//python:py_test.bzl", "py_test") py_binary( name = "qemu_runner_bin", @@ -25,6 +26,21 @@ platform_data( visibility = ["//visibility:public"], ) +py_test( + name = "qemu_runner_test", + srcs = [ + "qemu_runner.py", + "qemu_runner_test.py", + ], + imports = ["."], + main = "qemu_runner_test.py", + deps = [ + "@@pigweed++cipd+pigweed.qemu//:qemu-system-arm-runfiles", + "@pigweed//pw_tokenizer/py:detokenize", + "@rules_python//python/runfiles", + ], +) + py_binary( name = "test_runner_bin", srcs = [ diff --git a/target/ast10x0/harness/qemu_runner.py b/target/ast10x0/harness/qemu_runner.py index b6e2e3a28..5fbc5a286 100644 --- a/target/ast10x0/harness/qemu_runner.py +++ b/target/ast10x0/harness/qemu_runner.py @@ -131,14 +131,37 @@ def _sentinel_watcher( print(f"Exception watching sentinel: {e}", file=sys.stderr) -def _seed_flash_image(path: str, size: int, fill: int = 0xFF) -> None: - """Create/overwrite `path` with `size` bytes of `fill` (0xFF = erased).""" +def _seed_flash_image( + path: str, size: int, fill: int = 0xFF, contents: str = "" +) -> None: + """Create/overwrite `path` with `size` bytes. + + Without `contents` the image is `size` bytes of `fill` (0xFF = erased). + With it, the file is copied in at offset 0 and the remainder is left + erased, so a test can hand the device a real image to read. + + A `contents` file longer than `size` is an error rather than a truncation: + a half-written image still parses far enough to fail verification, which + would make a verify-failure scenario pass for the wrong reason. + """ + if not contents: + with open(path, "wb") as f: + f.write(bytes([fill & 0xFF]) * size) + return + + data = Path(contents).read_bytes() + if len(data) > size: + raise ValueError( + f"{contents} is {len(data)} bytes, larger than the {size}-byte " + f"flash it seeds" + ) with open(path, "wb") as f: - f.write(bytes([fill & 0xFF]) * size) + f.write(data) + f.write(b"\xff" * (size - len(data))) def _resolve_flash_drives(args): - """Return a list of (index, path, size, fill) FMC backing images. + """Return a list of (index, path, size, fill, contents) FMC backing images. index 0 -> FMC CS0, index 1 -> FMC CS1. Each image is re-seeded on every run so tests start from a known device state. An explicit --flash-image @@ -146,20 +169,26 @@ def _resolve_flash_drives(args): AST10X0_CS0_IMAGE / AST10X0_CS1_IMAGE (basenames) plus AST10X0_FLASH_SIZE and per-CS AST10X0_CS0_FILL / AST10X0_CS1_FILL, resolved against $TEST_TMPDIR so each run gets private, freshly-seeded images. + + AST10X0_CS0_CONTENTS / AST10X0_CS1_CONTENTS name a file to copy in at + offset 0 instead of filling, for a test that needs the device to already + hold an image. The path is a runfile, resolved by the test rule. """ base = os.environ.get("TEST_TMPDIR", tempfile.gettempdir()) size = int(os.environ.get("AST10X0_FLASH_SIZE", str(args.flash_size))) drives = [] if args.flash_image: - drives.append((1, args.flash_image, size, 0xFF)) + drives.append((1, args.flash_image, size, 0xFF, "")) cs0 = os.environ.get("AST10X0_CS0_IMAGE") if cs0: fill = int(os.environ.get("AST10X0_CS0_FILL", "255")) - drives.append((0, os.path.join(base, cs0), size, fill)) + contents = os.environ.get("AST10X0_CS0_CONTENTS", "") + drives.append((0, os.path.join(base, cs0), size, fill, contents)) cs1 = os.environ.get("AST10X0_CS1_IMAGE") if cs1: fill = int(os.environ.get("AST10X0_CS1_FILL", "255")) - drives.append((1, os.path.join(base, cs1), size, fill)) + contents = os.environ.get("AST10X0_CS1_CONTENTS", "") + drives.append((1, os.path.join(base, cs1), size, fill, contents)) return drives @@ -191,8 +220,8 @@ def _main(args) -> None: args.image, ] - for index, path, size, fill in drives: - _seed_flash_image(path, size, fill) + for index, path, size, fill, contents in drives: + _seed_flash_image(path, size, fill, contents) qemu_args += [ "-drive", f"file={path},format=raw,if=mtd,index={index}", diff --git a/target/ast10x0/harness/qemu_runner_test.py b/target/ast10x0/harness/qemu_runner_test.py new file mode 100644 index 000000000..d2fe10098 --- /dev/null +++ b/target/ast10x0/harness/qemu_runner_test.py @@ -0,0 +1,55 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 +"""Tests for the flash seeding in qemu_runner.""" + +import tempfile +import unittest + +from pathlib import Path + +from qemu_runner import _seed_flash_image + + +class SeedFlashImageTest(unittest.TestCase): + """A seeded image is always exactly flash_size bytes.""" + + def setUp(self) -> None: + self._dir = tempfile.TemporaryDirectory() + self.addCleanup(self._dir.cleanup) + self.image = str(Path(self._dir.name) / "cs1.img") + + def _contents_file(self, data: bytes) -> str: + path = Path(self._dir.name) / "contents.bin" + path.write_bytes(data) + return str(path) + + def test_without_contents_the_image_is_all_fill(self) -> None: + _seed_flash_image(self.image, 32, fill=0xFF) + self.assertEqual(Path(self.image).read_bytes(), b"\xff" * 32) + + def test_fill_is_honoured(self) -> None: + _seed_flash_image(self.image, 8, fill=0x00) + self.assertEqual(Path(self.image).read_bytes(), b"\x00" * 8) + + def test_contents_land_at_offset_zero_and_the_rest_is_erased(self) -> None: + _seed_flash_image(self.image, 16, contents=self._contents_file(b"openprot")) + self.assertEqual(Path(self.image).read_bytes(), b"openprot" + b"\xff" * 8) + + def test_contents_override_fill(self) -> None: + """Fill describes the erased remainder, not the seeded bytes.""" + _seed_flash_image(self.image, 4, fill=0x00, contents=self._contents_file(b"ab")) + self.assertEqual(Path(self.image).read_bytes(), b"ab\xff\xff") + + def test_contents_may_fill_the_whole_image(self) -> None: + _seed_flash_image(self.image, 4, contents=self._contents_file(b"abcd")) + self.assertEqual(Path(self.image).read_bytes(), b"abcd") + + def test_contents_larger_than_the_flash_is_an_error(self) -> None: + """Truncating would hand the guest an image that fails for the + wrong reason.""" + with self.assertRaises(ValueError): + _seed_flash_image(self.image, 4, contents=self._contents_file(b"abcde")) + + +if __name__ == "__main__": + unittest.main()