61d4255091
Обвязка для быстрых тестов 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>
116 lines
4.3 KiB
Python
Executable File
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())
|