Обвязка для быстрых тестов 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>
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.
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:
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 подхватит его сам.
#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-сборку.