testkit: модульные тесты под ucsim_z80 + планы покрытия
Обвязка для быстрых тестов plain-C логики: секунды вместо прогона в MAME, без образа диска. ucsim_z80 идёт в комплекте нашего SDCC — новых зависимостей нет. ПОЧЕМУ ПОД Z80, А НЕ ХОСТОВЫМ GCC. У SDCC z80 int 16 бит, у хоста 32, и расходится это НЕ в объявлениях, а в выражениях: integer promotion повышает операнды до int независимо от того, объявлены они как uint8_t или uint16_t. Перевод кода на фиксированные типы разницу не убирает — убирает только исполнение с z80-семантикой. Побочно проверяется кодогенерация SDCC и модули с inline-asm, которых хостовая сборка не видит в принципе. Устройство: crt0_ucsim.s (SP, зануление, main, halt), tcheck.* (итог в структуру в ОЗУ), run_ucsim.py (гоняет ucsim, дампит tc_result, печатает отчёт), host-tests.mk (общие правила). Вывода через printf нет: тестовый бинарь линкуется без Sprinter-libc. Через ucsim-simif не идём — номера его команд плавают между версиями, halt + dump работают везде. Наборы лежат РЯДОМ с проверяемым кодом, обвязка общая: testkit/t_selftest.c — самопроверка (sizeof(int)==2) applications/PoP/roomtest/tests-host/ — движок PoP Первый содержательный набор — t_geom: сверяет рукописный asm-LCG из pop_geom.c с наивной 32-битной формулой на 128 шагах. Заявка «бит-в-бит как в SDLPoP» до сих пор держалась на комментарии. Тест проверен мутацией: порча эталонной константы даёт красный. Планы дальнейшего покрытия: docs/host-tests-plan.md — libc и libbgi (не начато) applications/PoP/docs/host_tests_plan.md — движок PoP Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Executable
+115
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
run_ucsim.py — прогнать тестовый .ihx под ucsim_z80 и разобрать результат.
|
||||
|
||||
Как это устроено (см. crt0_ucsim.s и tcheck.h): тестовый бинарь кладёт итог
|
||||
в структуру tc_result в ОЗУ и заканчивается инструкцией halt. Мы находим
|
||||
адрес _tc_result в карте линковки (.noi), гоняем ucsim, дампим структуру и
|
||||
печатаем отчёт.
|
||||
|
||||
Через simif сознательно не идём: номера его команд меняются от версии к
|
||||
версии, а halt + dump работают одинаково везде.
|
||||
|
||||
Код возврата: 0 — все проверки прошли, 1 — есть провалы или сбой прогона.
|
||||
"""
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
TC_MAGIC = 0x5A5A
|
||||
HDR = 8 # magic, total, failed, logn — по 2 байта
|
||||
LOGSZ = 768 # TC_LOGSZ из tcheck.h
|
||||
BYTES_PER_LINE = 16
|
||||
|
||||
# Строка дампа ucsim: "0x8000 5a 5a 07 00 ... ZZ......"
|
||||
# ASCII-хвост тоже может выглядеть как hex, поэтому берём РОВНО столько
|
||||
# токенов, сколько байт положено в строке, а не «сколько нашлось».
|
||||
DUMP_RE = re.compile(r"^0x([0-9A-Fa-f]+)\s+((?:[0-9A-Fa-f]{2}\s+)+)")
|
||||
|
||||
|
||||
def find_symbol(noi_path: Path, name: str) -> int:
|
||||
m = re.search(rf"^DEF {re.escape(name)} (0x[0-9A-Fa-f]+)$",
|
||||
noi_path.read_text(), re.M)
|
||||
if not m:
|
||||
raise SystemExit(f"{noi_path.name}: символ {name} не найден "
|
||||
f"(тест не слинкован с tcheck.c?)")
|
||||
return int(m.group(1), 16)
|
||||
|
||||
|
||||
def run(ucsim: Path, ihx: Path, start: int, length: int, timeout: int):
|
||||
stop = start + length - 1
|
||||
script = (f'file "{ihx}"\n'
|
||||
f'reset\n'
|
||||
f'run\n'
|
||||
f'dump 0x{start:04X} 0x{stop:04X} {BYTES_PER_LINE}\n'
|
||||
f'quit\n')
|
||||
try:
|
||||
p = subprocess.run([str(ucsim), "-t", "z80"], input=script,
|
||||
capture_output=True, text=True, timeout=timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
raise SystemExit(f"ТАЙМАУТ {timeout} с — тест зациклился "
|
||||
f"(нет halt / бесконечный цикл в коде)")
|
||||
return p.stdout
|
||||
|
||||
|
||||
def parse_dump(out: str, start: int, length: int) -> bytes:
|
||||
mem = bytearray(length)
|
||||
seen = bytearray(length)
|
||||
for line in out.splitlines():
|
||||
m = DUMP_RE.match(line.strip())
|
||||
if not m:
|
||||
continue
|
||||
addr = int(m.group(1), 16)
|
||||
off = addr - start
|
||||
if off < 0 or off >= length:
|
||||
continue
|
||||
want = min(BYTES_PER_LINE, length - off)
|
||||
toks = m.group(2).split()[:want]
|
||||
for i, t in enumerate(toks):
|
||||
mem[off + i] = int(t, 16)
|
||||
seen[off + i] = 1
|
||||
if not any(seen):
|
||||
raise SystemExit("не удалось разобрать дамп ucsim:\n" + out)
|
||||
return bytes(mem)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 4:
|
||||
print("usage: run_ucsim.py <ucsim> <test.ihx> <test.noi> [timeout]",
|
||||
file=sys.stderr)
|
||||
return 2
|
||||
ucsim, ihx, noi = (Path(a) for a in sys.argv[1:4])
|
||||
timeout = int(sys.argv[4]) if len(sys.argv) > 4 else 60
|
||||
name = ihx.stem
|
||||
|
||||
addr = find_symbol(noi, "_tc_result")
|
||||
out = run(ucsim, ihx, addr, HDR + LOGSZ, timeout)
|
||||
|
||||
if "Halted" not in out:
|
||||
print(f"[{name}] ПРОГОН НЕ ЗАВЕРШИЛСЯ штатным halt:\n{out}")
|
||||
return 1
|
||||
|
||||
mem = parse_dump(out, addr, HDR + LOGSZ)
|
||||
u16 = lambda o: mem[o] | (mem[o + 1] << 8)
|
||||
magic, total, failed, logn = u16(0), u16(2), u16(4), u16(6)
|
||||
|
||||
if magic != TC_MAGIC:
|
||||
print(f"[{name}] tc_result не заполнен (magic=0x{magic:04X}) — "
|
||||
f"main() не позвал ни одной проверки?")
|
||||
return 1
|
||||
|
||||
if failed:
|
||||
log = mem[HDR:HDR + min(logn, LOGSZ)].decode("utf-8", "replace")
|
||||
print(f"[{name}] ПРОВАЛ: {failed} из {total}")
|
||||
for line in log.rstrip("\n").split("\n"):
|
||||
print(f" {line}")
|
||||
return 1
|
||||
|
||||
print(f"[{name}] ok: {total}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user