Files
Sprinter-SDCC/testkit/README.md
T
Александр Петров 61d4255091 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>
2026-08-03 22:36:16 +03:00

111 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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-сборку.