# 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-сборку.