Files
Sprinter-SDCC/testkit/run_ucsim.py
T
Александр Петров 61d4255091 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>
2026-08-03 22:36:16 +03:00

116 lines
4.3 KiB
Python
Executable File

#!/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())