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:
Александр Петров
2026-08-03 22:36:16 +03:00
parent b5d2a81ee3
commit 61d4255091
19 changed files with 1103 additions and 1 deletions
+103
View File
@@ -0,0 +1,103 @@
;; ----------------------------------------------------------------------
;; crt0_ucsim.s — стартовый код для тестовых бинарей, исполняемых в ucsim.
;;
;; Зачем отдельный crt0, а не runtime/crt0*.s: те завязаны на Sprinter —
;; префикс ESTEX, argv из командной строки, выход через ESTEX EXIT
;; (rst #0x10). В ucsim нет ни ESTEX, ни DSS; нужен голый z80: поставить
;; SP, занулить данные, позвать main, встать.
;;
;; Контракт с раннером (tests-host/run_ucsim.sh):
;; - выполнение заканчивается инструкцией `halt` — по ней ucsim
;; останавливает симуляцию, и раннер получает управление;
;; - результат тестов раннер читает из ОЗУ командой `dump` (адрес берётся
;; из карты линковки, символ _tc_result — см. tcheck.c).
;; Никакого simif не используем: номера его команд от версии к версии
;; плавают, а halt + dump работают везде.
;; ----------------------------------------------------------------------
.module crt0_ucsim
.globl _main
;; Символы, которые расставляет линкер.
.globl s__INITIALIZER
.globl l__INITIALIZER
.globl s__INITIALIZED
.globl s__DATA
.globl l__DATA
.globl s__BSS
.globl l__BSS
;; =========================================================================
;; Точка входа: ucsim стартует с PC = 0x0000.
;;
;; ВАЖНО: `.area _HEADER (ABS)` должна встречаться РОВНО ОДИН РАЗ и первой.
;; Если продублировать её в списке порядка областей ниже, sdas заведёт
;; вторую область _HEADER0, и линкер положит стартовый код не по 0x0000, а
;; куда придётся (проверено: уезжал за _DATA на 0x830A, и с PC=0
;; исполнялся мусор).
;; =========================================================================
.area _HEADER (ABS)
.org 0x0000
di
ld sp, #0xFEF0 ; стек под верхней границей ОЗУ ucsim
call gsinit
call _main
halt ; ucsim остановит симуляцию здесь
1$: jr 1$ ; страховка, если halt проигнорирован
;; =========================================================================
;; Порядок остальных областей — объявляем заранее, чтобы линкер разложил их
;; так (без _HEADER, см. комментарий выше).
;; =========================================================================
.area _HOME
.area _CODE
.area _INITIALIZER
.area _GSINIT
.area _GSFINAL
.area _DATA
.area _INITIALIZED
.area _BSEG
.area _BSS
.area _HEAP
;; =========================================================================
;; gsinit — занулить _DATA/_BSS и скопировать _INITIALIZER -> _INITIALIZED.
;; SDCC вставляет между gsinit и ret в _GSFINAL код инициализации модулей.
;; =========================================================================
.area _CODE
;; zero_area — заполнить BC байт по адресу HL нулями (BC может быть 0).
zero_area:
ld a, b
or a, c
ret Z
ld (hl), #0
dec bc
ld a, b
or a, c
ret Z
ld d, h
ld e, l
inc de
ldir
ret
.area _GSINIT
gsinit::
ld hl, #s__DATA
ld bc, #l__DATA
call zero_area
ld hl, #s__BSS
ld bc, #l__BSS
call zero_area
ld bc, #l__INITIALIZER
ld a, b
or a, c
jr Z, gsinit_done
ld de, #s__INITIALIZED
ld hl, #s__INITIALIZER
ldir
gsinit_done:
.area _GSFINAL
ret