Files
digital-design-onboarding-F…/scripts/build_test.py
T
2026-09-17 16:52:07 -04:00

106 lines
3.5 KiB
Python

#!/usr/bin/env python3
"""Assemble one tests/<name>/program.asm and generate its golden (Expected outputs)."""
from __future__ import annotations
import argparse
import os
from pathlib import Path
import shutil
import struct
import subprocess
from rv32_model import RV32Model, read_words, write_words
REPO_ROOT = Path(__file__).resolve().parents[1]
def find_assembler(explicit: str | None) -> str:
candidates = [
explicit,
os.environ.get("RISCV_AS"),
REPO_ROOT / "scripts" / "riscv-none-elf-as",
"riscv-none-elf-as",
"riscv32-unknown-elf-as",
"riscv64-unknown-elf-as",
]
for candidate in candidates:
if not candidate:
continue
candidate = str(candidate)
resolved = shutil.which(candidate)
if resolved:
return resolved
path = Path(candidate)
try:
if path.is_file() and os.access(path, os.X_OK):
return str(path)
except OSError:
continue
raise RuntimeError(
"RISC-V GNU assembler not found; "
"or set RISCV_AS=/path/to/riscv-*-as"
)
def elf_text(path: Path) -> bytes:
image = path.read_bytes()
if image[:4] != b"\x7fELF" or image[4] != 1 or image[5] != 1:
raise RuntimeError(f"{path} is not a little-endian ELF32 object")
section_offset = struct.unpack_from("<I", image, 32)[0]
section_size = struct.unpack_from("<H", image, 46)[0]
section_count = struct.unpack_from("<H", image, 48)[0]
names_index = struct.unpack_from("<H", image, 50)[0]
names_header = section_offset + names_index * section_size
names_offset, names_size = struct.unpack_from("<II", image, names_header + 16)
names = image[names_offset:names_offset + names_size]
for index in range(section_count):
header = section_offset + index * section_size
name_index = struct.unpack_from("<I", image, header)[0]
end = names.find(b"\0", name_index)
name = names[name_index:end].decode()
if name == ".text":
offset, size = struct.unpack_from("<II", image, header + 16)
return image[offset:offset + size]
raise RuntimeError(f"{path} has no .text section")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("test_dir", type=Path)
parser.add_argument("--assembler")
parser.add_argument("--max-instructions", type=int, default=10000)
args = parser.parse_args()
test_dir = args.test_dir.resolve()
source = test_dir / "program.asm"
if not source.is_file():
raise RuntimeError(f"missing required {source}")
assembler = find_assembler(args.assembler)
obj = test_dir / "program.o"
subprocess.run(
[assembler, "-march=rv32i", "-mabi=ilp32", "-mno-relax", str(source), "-o", str(obj)],
check=True,
)
text = elf_text(obj)
if len(text) % 4:
raise RuntimeError(".text size is not a multiple of four bytes")
program = [int.from_bytes(text[i:i + 4], "little") for i in range(0, len(text), 4)]
program_path = test_dir / "program.hex"
write_words(program_path, program)
data_path = test_dir / "data.hex"
initial_data = read_words(data_path if data_path.exists() else None)
model = RV32Model(program, initial_data)
count = model.run(args.max_instructions)
write_words(test_dir / "expected_regs.hex", model.regs)
write_words(test_dir / "expected_data.hex", model.data_words())
print(f"Built {test_dir.name}: {len(program)} words, halted after {count} instructions")
if __name__ == "__main__":
main()