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:
@@ -0,0 +1,110 @@
|
||||
# testkit — модульные тесты под ucsim_z80
|
||||
|
||||
Обвязка для быстрых тестов plain-C кода: секунды вместо прогона в MAME, без
|
||||
образа диска и без эмуляции всего Sprinter.
|
||||
|
||||
Сама по себе к подпроектам не привязана — так можно тестировать и модули
|
||||
libc, и логику приложений. Наборы тестов лежат **рядом с кодом, который они
|
||||
проверяют**, а не здесь:
|
||||
|
||||
| каталог | что проверяет |
|
||||
|---------|---------------|
|
||||
| `testkit/` | самопроверка обвязки (`t_selftest.c`) |
|
||||
| `applications/PoP/roomtest/tests-host/` | движок PoP |
|
||||
|
||||
```
|
||||
make host-tests # из корня: все каталоги с наборами
|
||||
make -C testkit # только самопроверка обвязки
|
||||
make -C <каталог> t_geom # один набор
|
||||
```
|
||||
|
||||
## Почему под z80, а не хостовым gcc
|
||||
|
||||
Первым порывом было собирать те же `.c` обычным gcc — но у SDCC z80
|
||||
`int` 16 бит, а у хоста 32, и это расходится **не в объявлениях, а в
|
||||
выражениях**: integer promotion повышает операнды до `int` независимо от
|
||||
того, объявлены они как `uint8_t` или `uint16_t`.
|
||||
|
||||
```c
|
||||
uint16_t a = 60000, b = 10000;
|
||||
if (a + b > 65000) { … } // z80: 4464 > 65000 → false
|
||||
// host: 70000 > 65000 → true
|
||||
```
|
||||
|
||||
Перевод всего кода на `intN_t` эту разницу НЕ убирает. Убирает её только
|
||||
исполнение с z80-семантикой. Отсюда `ucsim_z80` — он идёт в комплекте
|
||||
нашего же SDCC (`third_party/sdcc/bin`), так что новых зависимостей нет.
|
||||
|
||||
Побочные выгоды того же решения:
|
||||
|
||||
- проверяется **кодогенерация SDCC**, а не только исходник (см. memory
|
||||
`sdcc_z80_cmp_store_a_bug`);
|
||||
- модули с inline-asm тестируются как есть — например `pop_geom.c`, где
|
||||
LCG оригинала написан на ассемблере ради обхода `__mullong`.
|
||||
|
||||
`t_selftest.c` начинается с `TC_EQ(sizeof(int), 2)` — если обвязка однажды
|
||||
переедет на хостовый компилятор, это заметят сразу.
|
||||
|
||||
## Как устроено
|
||||
|
||||
| файл | роль |
|
||||
|------|------|
|
||||
| `crt0_ucsim.s` | голый z80-старт: SP, зануление `_DATA`/`_BSS`, `main`, `halt` |
|
||||
| `tcheck.h/.c` | микро-фреймворк: итог кладётся в `tc_result` в ОЗУ |
|
||||
| `run_ucsim.py` | гоняет ucsim, дампит `tc_result`, печатает отчёт |
|
||||
| `host-tests.mk` | общие правила сборки и прогона |
|
||||
|
||||
Вывода через `printf` нет: тестовый бинарь линкуется **без** Sprinter-libc
|
||||
(там ESTEX, BIOS, файлы — в ucsim этого нет). Результат складывается в
|
||||
структуру в ОЗУ, а раннер читает её командой `dump` после остановки по
|
||||
`halt`. Через ucsim-simif сознательно не идём: номера его команд плавают
|
||||
от версии к версии, а `halt` + `dump` работают везде.
|
||||
|
||||
## Завести наборы в новом месте
|
||||
|
||||
Создать каталог рядом с проверяемым кодом и положить туда `Makefile`:
|
||||
|
||||
```make
|
||||
TESTKIT := $(abspath $(CURDIR)/../../testkit)
|
||||
ENGINE_DIR := $(abspath $(CURDIR)/..) # где лежат модули под тестом
|
||||
OBJS_geom := build/eng_pop_geom.rel # что доложить в набор t_geom
|
||||
include $(TESTKIT)/host-tests.mk
|
||||
```
|
||||
|
||||
Добавить каталог в `HOST_TEST_DIRS` корневого `Makefile`. Сам набор —
|
||||
`t_<имя>.c` с `main()`, который зовёт `TC_RUN`; wildcard подхватит его сам.
|
||||
|
||||
```c
|
||||
#include "tcheck.h"
|
||||
|
||||
TC_TEST(что_проверяем)
|
||||
{
|
||||
TC_EQ(получено, ожидание);
|
||||
TC_TRUE(условие);
|
||||
}
|
||||
|
||||
int main(void) { TC_RUN(что_проверяем); return 0; }
|
||||
```
|
||||
|
||||
## Грабли
|
||||
|
||||
- **`.area _HEADER (ABS)` объявлять ровно один раз и первой.** Если
|
||||
продублировать её в списке порядка областей, sdas заведёт `_HEADER0`, и
|
||||
линкер положит стартовый код не по `0x0000` (уезжал за `_DATA` на
|
||||
`0x830A`, и с PC=0 исполнялся мусор).
|
||||
- **Файл грузить командой `file`, а не аргументом командной строки:** при
|
||||
передаче аргументом ucsim успевает выполнить `run` до загрузки.
|
||||
- **Константы SDCC сворачивает.** `TC_EQ` над константным выражением
|
||||
проверит компилятор, а не код (`warning 110`). Входные данные для
|
||||
арифметических тестов держать в `static volatile`.
|
||||
- Если тест зациклился, раннер снимет его по таймауту (60 с по умолчанию)
|
||||
и скажет об этом явно.
|
||||
|
||||
## Чего эти тесты не поймают
|
||||
|
||||
Банки и W-окна, тайминги, корректность блитов, поведение реальной
|
||||
клавиатуры, отрисовку — для этого остаётся MAME (`docs/mame-autotest.md`).
|
||||
|
||||
Отдельный класс — баги **порядка вызовов** между модулями: общее
|
||||
глобальное состояние, затираемое тем, кто отработал последним. Такое ловит
|
||||
не unit-тест, а инвариант, вкомпилированный в safe-сборку.
|
||||
Reference in New Issue
Block a user