Skip to content

Commit 27e3cc0

Browse files
author
Marcos Hernandez
committed
feat(engine-host): primer test del subset portable — CRC32 (sub-etapa 2.1)
Arrancamos Phase 3a con la isla mas simple del motor: el CRC32 de IDs que vive en _sfx_crc32 (resources/engine/agemki_audio.c). Funcion critica porque el motor la usa para binary search en el TOC de SFX.DAT — si difiere del CRC32 del codegen JS, los chunks no se encuentran en runtime. Setup del runner C compilable con clang en host: - tests/engine_host/lib/crc32.c copia byte-exact de _sfx_crc32 desde agemki_audio.c. Aislada porque el .c original arrastra deps HW (mididrv, mpu, sb) que no compilan en host. - tests/engine_host/include/ag_test.h header compartido del runner. - tests/engine_host/runner.c entrypoint con dispatcher de tests (crc32, crc32_batch). - tests/engine_host/build.mjs compila con clang. Cross-platform (mac+win+linux). Si clang no esta disponible, exit 2 y los tests del runner se skipean en lugar de fallar. Helpers JS para los tests: - tests/helpers/engine-host.js ensureRunnerBuilt(), runnerCrc32(), runnerCrc32Batch(), detectCrc32Drift() y jsCrc32() (referencia desde sfxGenerator.js para validar). Test vitest: tests/golden/engine-host.test.js (22 tests): - Drift detection: extrae _sfx_crc32 del motor real y la compara byte-a-byte con la copia local. Si Javi edita el motor, este test rojo te avisa para sincronizar la copia. No depende de clang. - 16 IDs reales del juego (obj_key, char_hero, room_001, ...) — el CRC32 del motor coincide con el del JS bit-exact. - String vacio (CRC32 = 0). - Batch de 100 ids consecutivos (rendimiento + correccion). - Strings con espacios, mayusculas, numeros, simbolos. - Todos los chars ASCII imprimibles (0x20-0x7E) — cubre la tabla CRC entera (256 entradas). Hook PostToolUse: edit en resources/engine/*.c o *.h ahora dispara tests/golden/engine-host.test.js (drift + CRC32). Antes era un placeholder que invocaba make. CI workflow: paso nuevo "Setup clang (Windows only)". macos-latest trae clang preinstalado (Xcode CLT). windows-latest trae LLVM en C:\Program Files\LLVM\ — solo lo añadimos al PATH; si no esta preinstalado, fallback a `choco install llvm`. .gitignore: artefactos del build (runner, runner.exe, *.o, *.obj). Sin tocar el motor C de Javi: La isla CRC32 es 100% portable (solo bit math). La copia local es literal de las lineas 57-73 de agemki_audio.c. Cualquier cambio futuro al motor por parte de Javi se cazara con el drift test — o sincronizamos la copia, o ajustamos el test si el cambio es legitimo. Suite total: 219 → 241 tests. Duracion local: ~8s en mac.
1 parent 0c08883 commit 27e3cc0

13 files changed

Lines changed: 515 additions & 24 deletions

File tree

.claude/hooks/run-tests-on-edit.mjs

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,11 @@ if (/\/src\/main\/(datGenerator|sfxGenerator|fontGenerator|index)\.js$/.test(nor
7979
area = 'fixtures+helpers'
8080
filter = 'tests/'
8181
} else if (/\/resources\/engine\/[^/]+\.[ch]$/.test(norm)) {
82-
// Phase 3a: motor host. No implementado todavía.
83-
const makefile = join(REPO_ROOT, 'tests', 'engine_host', 'Makefile')
84-
if (existsSync(makefile)) {
85-
runEngineHost()
86-
}
87-
process.exit(0)
82+
// Edit en motor C → tests del motor host (drift + crc32 + futuros).
83+
// El test de drift verifica que las copias en tests/engine_host/lib/
84+
// siguen byte-exact al motor; si no, te avisa.
85+
area = 'engine-host'
86+
filter = 'tests/golden/engine-host.test.js'
8887
} else {
8988
// Path no relevante para tests
9089
process.exit(0)
@@ -109,20 +108,3 @@ try {
109108
}
110109

111110
process.exit(0)
112-
113-
// ── Phase 3a placeholder ──────────────────────────────────────────────────────
114-
function runEngineHost() {
115-
try {
116-
execSync('make -C tests/engine_host run', {
117-
cwd: REPO_ROOT,
118-
stdio: ['ignore', 'pipe', 'pipe'],
119-
encoding: 'utf8',
120-
})
121-
} catch (err) {
122-
const stdout = err.stdout?.toString('utf8') || ''
123-
const stderr = err.stderr?.toString('utf8') || ''
124-
const combined = (stdout + stderr).split('\n').slice(-20).join('\n')
125-
process.stderr.write('[hook] engine host tests FAIL:\n')
126-
process.stderr.write(combined + '\n')
127-
}
128-
}

.github/workflows/test.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,24 @@ jobs:
4343
node-version: ${{ matrix.node }}
4444
cache: npm
4545

46+
# clang viene preinstalado en macos-latest (Xcode CLT). Para Windows
47+
# hay que añadirlo: GitHub runners traen LLVM en C:\Program Files\LLVM\,
48+
# solo necesita estar en PATH. choco install llvm es alternativa.
49+
- name: Setup clang (Windows only)
50+
if: runner.os == 'Windows'
51+
shell: pwsh
52+
run: |
53+
$llvm = 'C:\Program Files\LLVM\bin'
54+
if (Test-Path $llvm) {
55+
echo $llvm | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding utf8
56+
Write-Host "LLVM found at $llvm"
57+
} else {
58+
choco install llvm --no-progress -y
59+
}
60+
61+
- name: Verify clang available
62+
run: clang --version
63+
4664
- name: Install deps
4765
run: npm ci
4866

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ game_config.h
2727
coverage/
2828
tmp/
2929

30+
# Artefactos de build del motor host (clang). Se regeneran con
31+
# `node tests/engine_host/build.mjs` en cada cambio de fuente.
32+
tests/engine_host/runner
33+
tests/engine_host/runner.exe
34+
tests/engine_host/*.o
35+
tests/engine_host/*.obj
36+
3037
# Logs de compilación
3138
build/build.log
3239
build/watcom.log

tests/README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,12 @@ agemki/
185185
│ │ ├── dat.test.js codegen .DAT byte-equal + estructura semántica
186186
│ │ └── __snapshots__/ snapshots vitest (preview hex de cabeceras)
187187
│ │
188-
│ └── engine_host/ ← (futuro Phase 3a) Makefile + runner C compilado con clang
188+
│ └── engine_host/ ← Phase 3a — runner C compilado con clang en host
189+
├── lib/crc32.c copia byte-exact de _sfx_crc32 del motor
190+
├── include/ag_test.h header compartido del runner
191+
├── runner.c entrypoint con dispatcher de tests
192+
├── build.mjs compila con clang (cross-platform mac/win)
193+
└── runner / runner.exe binario generado (gitignored)
189194
190195
├── goldens/ ← outputs esperados (entran al repo, !goldens/** en .gitignore)
191196
│ ├── dat/
@@ -230,6 +235,29 @@ Smoke de los helpers de testing:
230235
- `decodeDat` parsea cabecera + index entry de un fichero AGMK construido a mano.
231236
- Constantes (`MAGIC`, `HEADER_SIZE`, `INDEX_ENTRY_SIZE`) coherentes con la spec.
232237

238+
### `tests/golden/engine-host.test.js` (22 tests, Phase 3a)
239+
240+
Tests del **subset portable del motor C** compilado con clang en host.
241+
Primer test: CRC32 (`_sfx_crc32` del motor) — algoritmo crítico porque
242+
el motor usa este hash para binary search en el TOC de SFX.DAT. Si
243+
difiere del CRC32 del codegen JS (sfxGenerator.js / datGenerator.js),
244+
los chunks no se encuentran en runtime.
245+
246+
Cubre:
247+
- **Drift detection**: compara byte-a-byte la copia en
248+
`tests/engine_host/lib/crc32.c` con la función `_sfx_crc32` viva en
249+
`resources/engine/agemki_audio.c`. Si Javi cambia la del motor, el
250+
test rojo te avisa para sincronizar.
251+
- **Coherencia con JS**: 16 IDs reales (room_001, char_hero, etc.),
252+
string vacío, batch de 100 ids, casos con espacios/mayúsculas, y
253+
todos los chars ASCII imprimibles (cubre la tabla CRC entera, índices
254+
0x20-0x7E).
255+
256+
Si clang no está disponible (Xcode CLT en mac, LLVM en win, apt en
257+
linux), el bloque de tests dependientes se **skipea** automáticamente
258+
y el suite sigue verde. El drift test SÍ se ejecuta siempre (no
259+
requiere clang).
260+
233261
### `tests/golden/dat.test.js` (26 tests)
234262

235263
Para cada fixture (`minimal`):

tests/engine_host/build.mjs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* build.mjs — compila el runner host de tests con clang, cross-platform.
3+
*
4+
* Por qué Node y no Make: Windows no tiene make nativo y queremos que el
5+
* suite corra idéntico en mac/win sin instalar MSYS2. clang sí está
6+
* disponible cross-platform (LLVM oficial).
7+
*
8+
* Uso:
9+
* node tests/engine_host/build.mjs # compila si fuente cambió
10+
* node tests/engine_host/build.mjs --force # recompila siempre
11+
* node tests/engine_host/build.mjs --check # solo verifica clang disponible
12+
*
13+
* Output: tests/engine_host/runner (mac/linux) o runner.exe (win).
14+
*
15+
* Salidas:
16+
* exit 0 → compilado, listo para ejecutar
17+
* exit 2 → clang no disponible (skipear los tests engine_host)
18+
* exit 1 → error de compilación (test FAIL)
19+
*/
20+
import { execFileSync, spawnSync } from 'node:child_process'
21+
import { existsSync, statSync, readdirSync } from 'node:fs'
22+
import { dirname, join } from 'node:path'
23+
import { fileURLToPath } from 'node:url'
24+
25+
const __dirname = dirname(fileURLToPath(import.meta.url))
26+
const HERE = __dirname
27+
28+
// ── Detectar clang (mac, win, linux) ──────────────────────────────────────
29+
function findClang() {
30+
// Probar simplemente "clang" en PATH (más portable)
31+
const r = spawnSync('clang', ['--version'], { encoding: 'utf8' })
32+
if (r.status === 0) return 'clang'
33+
return null
34+
}
35+
36+
const clang = findClang()
37+
if (process.argv.includes('--check')) {
38+
if (clang) { console.log('clang OK:', clang); process.exit(0) }
39+
else { console.error('clang no disponible en PATH'); process.exit(2) }
40+
}
41+
42+
if (!clang) {
43+
console.error('build.mjs: clang no disponible. Salta tests engine_host.')
44+
console.error(' macOS: instala Xcode Command Line Tools (xcode-select --install)')
45+
console.error(' Windows: instala LLVM (https://releases.llvm.org/) o MSYS2')
46+
console.error(' Linux: apt/yum install clang')
47+
process.exit(2)
48+
}
49+
50+
// ── Recolectar fuentes ────────────────────────────────────────────────────
51+
const sources = []
52+
for (const f of readdirSync(join(HERE, 'lib'))) {
53+
if (f.endsWith('.c')) sources.push(join(HERE, 'lib', f))
54+
}
55+
sources.push(join(HERE, 'runner.c'))
56+
57+
// ── Output binary (con extensión adecuada al OS) ──────────────────────────
58+
const exe = process.platform === 'win32' ? 'runner.exe' : 'runner'
59+
const exePath = join(HERE, exe)
60+
61+
// ── Decisión rebuild ──────────────────────────────────────────────────────
62+
const force = process.argv.includes('--force')
63+
let needsRebuild = force || !existsSync(exePath)
64+
if (!needsRebuild) {
65+
const exeMtime = statSync(exePath).mtimeMs
66+
for (const s of sources) {
67+
if (statSync(s).mtimeMs > exeMtime) { needsRebuild = true; break }
68+
}
69+
}
70+
71+
if (!needsRebuild) {
72+
// Ya estamos al día.
73+
process.exit(0)
74+
}
75+
76+
// ── Compilar y enlazar de un golpe ────────────────────────────────────────
77+
// Flags conservadores: c89 (alineado con el motor), warnings altos pero sin
78+
// -Werror (algún warning legítimo en el motor portado no debería bloquear).
79+
const flags = [
80+
'-std=c89', '-O0', '-g',
81+
'-Wall', '-Wextra', '-Wno-unused-parameter',
82+
'-DAGEMKI_HOST_TEST',
83+
'-I', join(HERE, 'include'),
84+
]
85+
86+
try {
87+
execFileSync(clang, [...flags, ...sources, '-o', exePath], { stdio: 'inherit' })
88+
} catch (err) {
89+
console.error('build.mjs: clang falló')
90+
process.exit(1)
91+
}
92+
93+
console.log('build.mjs: ok →', exePath)
94+
process.exit(0)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* ag_test.h — header compartido del runner host de tests del motor.
3+
*
4+
* Cada lib/ test C expone una o más funciones públicas con prefijo
5+
* `ag_test_` que el runner.c puede invocar. El runner las llama según el
6+
* argumento de línea de comandos (ej: `runner crc32 hello`) y emite el
7+
* resultado por stdout en formato simple (decimal, hex, hash, etc.) que
8+
* el test vitest del lado JS valida.
9+
*/
10+
#ifndef AG_TEST_H
11+
#define AG_TEST_H
12+
13+
unsigned long ag_test_crc32(const char* s);
14+
15+
#endif

tests/engine_host/lib/crc32.c

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* crc32.c — copia bit-a-bit de _sfx_crc32 desde agemki_audio.c
3+
*
4+
* Por qué no incluimos directamente la del motor: agemki_audio.c arrastra
5+
* dependencias HW (mididrv.h, mpu.h, sb.h) que no compilan en host.
6+
* Aislamos solo el algoritmo, que es 100% portable (bit math sobre uint32).
7+
*
8+
* Coherencia con el motor: el test `drift` en runner.c compara byte-a-byte
9+
* esta función con la que vive en resources/engine/agemki_audio.c. Si
10+
* alguien edita la del motor, este test falla y forzamos sincronizar.
11+
*
12+
* Coherencia con el codegen: el test `crc_vs_js` en runner.c compara las
13+
* salidas con valores precalculados desde sfxGenerator.js (mismo polinomio
14+
* 0xEDB88320). Si difieren, el motor no encontraría chunks en el TOC.
15+
*/
16+
static unsigned long _sfx_crc32(const char* s) {
17+
static unsigned long tbl[256];
18+
static int tbl_rdy = 0;
19+
unsigned long c; int i, k;
20+
if (!tbl_rdy) {
21+
for (i = 0; i < 256; i++) {
22+
c = (unsigned long)i;
23+
for (k = 0; k < 8; k++) c = (c & 1) ? (0xEDB88320UL ^ (c >> 1)) : (c >> 1);
24+
tbl[i] = c;
25+
}
26+
tbl_rdy = 1;
27+
}
28+
c = 0xFFFFFFFFUL;
29+
while (*s) { c = tbl[(c ^ (unsigned char)*s++) & 0xFF] ^ (c >> 8); }
30+
return (c ^ 0xFFFFFFFFUL);
31+
}
32+
33+
unsigned long ag_test_crc32(const char* s) {
34+
return _sfx_crc32(s);
35+
}

tests/engine_host/runner.c

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* runner.c — entrypoint de los tests del motor en host.
3+
*
4+
* Recibe el nombre del test como primer argumento y emite el resultado
5+
* por stdout (cada test define su propio formato). El test vitest del
6+
* lado JS invoca el binario y parsea el stdout.
7+
*
8+
* Compilación: tests/engine_host/build.mjs (clang -std=c89 -O0 -DAGEMKI_HOST_TEST).
9+
*
10+
* Tests disponibles:
11+
* crc32 <string> -> imprime CRC32(string) en hex 0xXXXXXXXX
12+
* crc32_batch -> lee strings de stdin (uno por línea), imprime hex por línea
13+
*
14+
* Más tests se añaden en sub-etapas posteriores (Phase 3a sub-2.2+).
15+
*/
16+
#include <stdio.h>
17+
#include <string.h>
18+
#include "include/ag_test.h"
19+
20+
static int test_crc32(int argc, char** argv) {
21+
if (argc < 3) {
22+
fprintf(stderr, "uso: runner crc32 <string>\n");
23+
return 2;
24+
}
25+
unsigned long h = ag_test_crc32(argv[2]);
26+
printf("0x%08lX\n", h & 0xFFFFFFFFUL);
27+
return 0;
28+
}
29+
30+
static int test_crc32_batch(void) {
31+
char line[1024];
32+
while (fgets(line, sizeof(line), stdin)) {
33+
/* Quitar newline final */
34+
size_t n = strlen(line);
35+
while (n > 0 && (line[n-1] == '\n' || line[n-1] == '\r')) line[--n] = 0;
36+
unsigned long h = ag_test_crc32(line);
37+
printf("0x%08lX\n", h & 0xFFFFFFFFUL);
38+
}
39+
return 0;
40+
}
41+
42+
int main(int argc, char** argv) {
43+
if (argc < 2) {
44+
fprintf(stderr, "uso: runner <test> [args...]\n");
45+
fprintf(stderr, "tests: crc32, crc32_batch\n");
46+
return 2;
47+
}
48+
const char* test = argv[1];
49+
if (strcmp(test, "crc32") == 0) return test_crc32(argc, argv);
50+
if (strcmp(test, "crc32_batch") == 0) return test_crc32_batch();
51+
fprintf(stderr, "test desconocido: %s\n", test);
52+
return 2;
53+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>CFBundleDevelopmentRegion</key>
6+
<string>English</string>
7+
<key>CFBundleIdentifier</key>
8+
<string>com.apple.xcode.dsym.runner</string>
9+
<key>CFBundleInfoDictionaryVersion</key>
10+
<string>6.0</string>
11+
<key>CFBundlePackageType</key>
12+
<string>dSYM</string>
13+
<key>CFBundleSignature</key>
14+
<string>????</string>
15+
<key>CFBundleShortVersionString</key>
16+
<string>1.0</string>
17+
<key>CFBundleVersion</key>
18+
<string>1</string>
19+
</dict>
20+
</plist>
10.8 KB
Binary file not shown.

0 commit comments

Comments
 (0)