Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions target/ast10x0/defs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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/<name> and attaches each present CS image as
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
16 changes: 16 additions & 0 deletions target/ast10x0/harness/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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 = [
Expand Down
47 changes: 38 additions & 9 deletions target/ast10x0/harness/qemu_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,35 +131,64 @@ 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
(manual runs) attaches at CS1. A flash_system_image_test sets
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


Expand Down Expand Up @@ -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}",
Expand Down
55 changes: 55 additions & 0 deletions target/ast10x0/harness/qemu_runner_test.py
Original file line number Diff line number Diff line change
@@ -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()
Loading