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
@@ -0,0 +1,146 @@
/*
* t_geom.c — тесты pop_geom.c (геометрия комнаты + PRNG оригинала).
*
* Модуль выбран первым, потому что не тянет за собой ничего: ни графики,
* ни данных уровня, ни клавиатуры — только свой заголовок.
*
* Главный тест здесь — geom_lcg_matches_reference. В pop_geom.c LCG
* оригинала (s = s*214013 + 2531011) написан на ассемблере по схеме
* Горнера ради обхода __mullong, и заявка «бит-в-бит как в SDLPoP» до сих
* пор держалась только на комментарии. Тест сверяет рукописный asm с
* наивной 32-битной формулой на каждом шаге — и по возвращаемому значению,
* и по обеим половинам сида.
*/
#include "tcheck.h"
#include "pop_geom.h"
/* ---- геометрия ------------------------------------------------------ */
TC_TEST(geom_xbump_layout)
{
/* Шаг колонок равномерный и равен TILE_SIZEX — на этом стоит вся
* арифметика колонок (x_bump[col + FIRST_ONSCREEN_COLUMN]). */
uint8_t i;
for (i = 1; i < 20; i++)
TC_EQ(pop_x_bump[i] - pop_x_bump[i - 1], TILE_SIZEX);
/* Центр тайла колонки 0 — та самая величина, которую кладут в Char.x
* do_startpos и pos_guards (seg003). */
TC_EQ(pop_x_bump[0 + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX, 72);
TC_EQ(pop_x_bump[9 + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX, 198);
}
TC_TEST(geom_yland_rows)
{
/* y_land[row + 1]; [0] — ряд «над комнатой». */
TC_EQ(pop_y_land[0], -8);
TC_EQ(pop_y_land[1], 55);
TC_EQ(pop_y_land[2], 118);
TC_EQ(pop_y_land[3], 181);
TC_EQ(pop_y_land[4], 244);
/* Шаг между рядами — TILE_SIZEY. */
TC_EQ(pop_y_land[2] - pop_y_land[1], TILE_SIZEY);
TC_EQ(pop_y_land[3] - pop_y_land[2], TILE_SIZEY);
}
TC_TEST(geom_y_to_row)
{
/* Пол каждого ряда должен давать номер этого ряда. */
TC_EQ(pop_y_to_row(pop_y_land[1]), 0);
TC_EQ(pop_y_to_row(pop_y_land[2]), 1);
TC_EQ(pop_y_to_row(pop_y_land[3]), 2);
/* Ряд над комнатой. */
TC_EQ(pop_y_to_row(pop_y_land[0]), -1);
/* Оборот mod 4: ряд «под комнатой» сворачивается обратно в -1 —
* ровно на это опирается спавн падающего куска у нижней кромки. */
TC_EQ(pop_y_to_row(pop_y_land[4]), -1);
}
/* ---- PRNG: сверка asm-LCG с эталонной формулой ---------------------- */
#if POP_PRANDOM_EXACT
#define LCG_ITERS 128
TC_TEST(geom_lcg_matches_reference)
{
pop_rnd_t s;
unsigned long ref = 12345UL; /* произвольный ненулевой старт */
uint16_t i, got, want;
pop_prandom_set(s, 12345u);
for (i = 0; i < LCG_ITERS; i++) {
got = pop_prandom(&s, 255u);
ref = ref * 214013UL + 2531011UL;
want = (uint16_t)((uint16_t)(ref >> 16) % 256u);
/* Обрываемся на первом расхождении: иначе одна ошибка в умножении
* забила бы весь буфер отчёта однотипными строками. */
if (got != want ||
s.lo != (uint16_t)ref ||
s.hi != (uint16_t)(ref >> 16)) {
TC_EQ(got, want);
TC_EQ(s.lo, (uint16_t)ref);
TC_EQ(s.hi, (uint16_t)(ref >> 16));
break;
}
}
TC_EQ(i, LCG_ITERS); /* прошли всю дистанцию без расхождений */
}
TC_TEST(geom_lcg_seed_zero)
{
/* Ноль — законный сид (кладку сеют номером комнаты + ряд + колонка,
* что вполне даёт 0). У LCG ноль не является неподвижной точкой —
* убеждаемся, что asm это воспроизводит. */
pop_rnd_t s;
unsigned long ref = 0UL;
pop_prandom_set(s, 0u);
pop_prandom(&s, 255u);
ref = ref * 214013UL + 2531011UL;
TC_EQ(s.lo, (uint16_t)ref);
TC_EQ(s.hi, (uint16_t)(ref >> 16));
}
#endif /* POP_PRANDOM_EXACT */
TC_TEST(geom_prandom_range)
{
/* Оба генератора обязаны укладываться в 0..maxv, в том числе когда
* maxv+1 НЕ степень двойки (там другая ветка pop_rnd_fit). */
pop_rnd_t s;
uint16_t i, v, seen_hi = 0;
uint16_t out_of_range = 0;
pop_prandom_set(s, 1u);
for (i = 0; i < 200; i++) {
v = pop_prandom(&s, 4u); /* n = 5, не степень двойки */
if (v > 4) out_of_range++;
if (v == 4) seen_hi = 1;
}
TC_EQ(out_of_range, 0);
TC_TRUE(seen_hi); /* верхняя граница достижима */
pop_prandom_set(s, 7u);
out_of_range = 0;
for (i = 0; i < 64; i++) {
v = pop_prandom(&s, 1u); /* n = 2, маска */
if (v > 1) out_of_range++;
}
TC_EQ(out_of_range, 0);
}
int main(void)
{
TC_RUN(geom_xbump_layout);
TC_RUN(geom_yland_rows);
TC_RUN(geom_y_to_row);
#if POP_PRANDOM_EXACT
TC_RUN(geom_lcg_matches_reference);
TC_RUN(geom_lcg_seed_zero);
#endif
TC_RUN(geom_prandom_range);
return 0;
}