Le prove nel browser a sezioni (idea di Andrea): scripts/verify.mjs rifà solo le prove i cui ingressi sono cambiati dall'ultimo verde (.verify-ok.json fuori da git); make verify le usa, make verify-plan spiega; i minigiochi entrano in make verify; due alla volta, perché con quattro le schermate vanno in timeout. Test del pianificatore (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env node
|
||||
// Le prove nel browser a sezioni (idea di Andrea, 1 novembre 2026): ogni prova ha i suoi ingressi, i file da cui dipende;
|
||||
// si rifà solo se la loro impronta (i blob di git, più i file modificati nella cartella di lavoro) è cambiata dall'ultimo
|
||||
// verde, ricordato in .verify-ok.json (fuori da git). Se cambia la briscola si riprova la briscola; se cambia la storia, il
|
||||
// motore, l'impaginazione o le risorse si riprova la partita intera (e le prove delle scene speciali), non la briscola.
|
||||
// node scripts/verify.mjs le prove da rifare, in parallelo (JOBS=2: con quattro Chromium swiftshader insieme le schermate vanno in timeout), e aggiorna i verdi
|
||||
// node scripts/verify.mjs --plan dice solo cosa rifarebbe e perché
|
||||
// node scripts/verify.mjs --all tutte, senza guardare i verdi
|
||||
// node scripts/verify.mjs briscola solo quelle nominate (anche se verdi)
|
||||
// I test unitari e check-public non stanno qui: costano pochi secondi e girano sempre (make verify).
|
||||
import { execFileSync, spawn } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { cpus } from 'node:os';
|
||||
|
||||
// Il motore comune a tutto: cambia questo e si riprova ogni minigioco.
|
||||
export const ENGINE = ['index.html', 'style.css', 'src/main.js', 'src/engine/game.js', 'src/engine/render.js',
|
||||
'tests/helpers.mjs', 'package-lock.json', 'scripts/verify.mjs'];
|
||||
// Non toccano il gioco nel browser: documenti, strumenti, pubblicazione, versione (make bump la cambia prima di verificare),
|
||||
// test unitari (girano sempre, a parte).
|
||||
export const IGNORED = [/^docs\//, /^[^/]*\.md$/, /^src\/version\.js$/, /^package\.json$/, /^Makefile$/, /^scripts\/(?!verify\.mjs)/,
|
||||
/^tools\//, /^video\//, /^\.gitea\//, /^prompts\//, /^LICENSE$/, /^Dockerfile$/, /^nginx\.conf$/, /^docker-compose/, /^requirements\.txt$/,
|
||||
/^\.(gitignore|dockerignore|verify-ok\.json)$/, /^tests\/[^/]*\.test\.mjs$/, /^tests\/test_[^/]*\.py$/, /^deploy\.local\.mk$/, /(^|\/)__pycache__\//];
|
||||
|
||||
// Le prove. `core: true` = dipende da tutto il gioco tranne i file dei minigiochi a sé (che hanno la loro prova); altrimenti
|
||||
// solo dal motore e dai propri file: quelli che si chiamano come la prova (src/engine/flipper.js, flipper-render.js,
|
||||
// assets/closeups/flipper-*.png, docs/BRISCOLA.md…) più gli `extra`. `run` sono gli argomenti di node.
|
||||
export const SUITES = {
|
||||
principale: { core: true, run: ['tests/browser.mjs', '--parte=principale'], note: 'la partita intera, pausa e salvataggi' },
|
||||
minigiochi: { core: true, run: ['tests/browser.mjs', '--parte=minigiochi'], note: 'libro, bocce, tiri, pesca e biliardo dalla partita' },
|
||||
telefoni: { core: true, run: ['tests/browser.mjs', '--parte=telefoni'], note: 'multimedia, telefoni, la finestra di Andrea, debug' },
|
||||
ambient: { core: true, run: ['tests/ambient.browser.mjs'], note: 'tutte le scene nelle tre fasi e gli effetti' },
|
||||
veicoli: { core: true, run: ['tests/veicoli.browser.mjs'], note: 'i veicoli nelle scene' },
|
||||
ingorgo: { core: true, run: ['tests/ingorgo.browser.mjs'], note: "l'Ingorgo Sonoro" },
|
||||
smarronata: { core: true, run: ['tests/smarronata.browser.mjs'], note: 'la Smarronata del 1993' },
|
||||
briscola: { run: ['tests/briscola.browser.mjs'] },
|
||||
tressette: { run: ['tests/tressette.browser.mjs'] },
|
||||
scopa: { run: ['tests/scopa.browser.mjs'] },
|
||||
biliardo: { run: ['tests/biliardo.browser.mjs'] },
|
||||
flipper: { run: ['tests/flipper.browser.mjs'] },
|
||||
micro: { run: ['tests/micro.browser.mjs'] },
|
||||
cerbottana: { run: ['tests/cerbottana.browser.mjs'] },
|
||||
condor: { run: ['tests/condor.browser.mjs'] },
|
||||
campana: { run: ['tests/campana.browser.mjs'] },
|
||||
bruciate: { run: ['tests/bruciate.browser.mjs'] },
|
||||
gara: { run: ['tests/gara.browser.mjs'], extra: ['src/engine/corsa.js', 'src/story/piloti.js', /^assets\/[^/]*\/(gara|gp|pilot)/] },
|
||||
telefono: { run: ['tests/telefono.browser.mjs'], extra: [/^assets\/muro\//] },
|
||||
risveglio: { run: ['tests/risveglio.browser.mjs'] },
|
||||
};
|
||||
|
||||
const ownRe = name => new RegExp(`(^|/)${name}([-_.][^/]*)?$`, 'i');
|
||||
const matches = (path, rule) => rule instanceof RegExp ? rule.test(path) : rule === path;
|
||||
export const ignored = path => IGNORED.some(r => r.test(path));
|
||||
export function ownFiles(name, files) {
|
||||
const s = SUITES[name], re = ownRe(name);
|
||||
return files.filter(p => re.test(p) || (s.extra || []).some(r => matches(p, r)));
|
||||
}
|
||||
// Gli ingressi di una prova fra i file dati (tutti quelli di git più quelli nuovi nella cartella).
|
||||
export function inputsOf(name, files) {
|
||||
const s = SUITES[name], live = files.filter(p => !ignored(p));
|
||||
if (!s.core) return [...new Set([...ENGINE.filter(p => files.includes(p)), ...ownFiles(name, live)])].sort();
|
||||
const others = Object.keys(SUITES).filter(n => !SUITES[n].core);
|
||||
const foreign = new Set(others.flatMap(n => ownFiles(n, live)));
|
||||
return live.filter(p => !foreign.has(p)).sort();
|
||||
}
|
||||
export const fingerprint = (paths, hashOf) => createHash('sha1').update(paths.map(p => `${p} ${hashOf(p)}`).join('\n')).digest('hex');
|
||||
|
||||
const ROOT = fileURLToPath(new URL('..', import.meta.url));
|
||||
const git = (...a) => execFileSync('git', a, { cwd: ROOT, encoding: 'utf8', maxBuffer: 1 << 26 });
|
||||
|
||||
// I file del progetto con l'impronta di ognuno: il blob dell'indice di git, o il contenuto attuale se modificato o nuovo.
|
||||
export function snapshot() {
|
||||
const hashes = new Map();
|
||||
for (const line of git('ls-files', '-s', '-z').split('\0')) if (line) { const [meta, path] = line.split('\t'); hashes.set(path, meta.split(' ')[1]); }
|
||||
const dirty = [];
|
||||
for (const line of git('status', '--porcelain', '-z', '--untracked-files=all').split('\0')) {
|
||||
if (!line) continue;
|
||||
const code = line.slice(0, 2), path = line.slice(3);
|
||||
if (code.includes('D')) hashes.delete(path); else dirty.push(path);
|
||||
}
|
||||
for (const path of dirty) if (existsSync(`${ROOT}/${path}`)) hashes.set(path, git('hash-object', '--', path).trim());
|
||||
return hashes;
|
||||
}
|
||||
|
||||
const OK_FILE = `${ROOT}/.verify-ok.json`;
|
||||
const readOk = () => { try { return JSON.parse(readFileSync(OK_FILE, 'utf8')); } catch { return {}; } };
|
||||
|
||||
export function plan(hashes, ok, names = Object.keys(SUITES)) {
|
||||
const files = [...hashes.keys()], out = [];
|
||||
for (const name of names) {
|
||||
const inputs = inputsOf(name, files), fp = fingerprint(inputs, p => hashes.get(p));
|
||||
const was = ok[name], changed = was?.fingerprint === fp ? [] : inputs.filter(p => !was?.hashes || was.hashes[p] !== hashes.get(p));
|
||||
out.push({ name, fp, inputs, changed, green: was?.fingerprint === fp, seconds: was?.seconds });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function run(name) {
|
||||
return new Promise(resolve => {
|
||||
const t0 = Date.now(), lines = [];
|
||||
const p = spawn(process.execPath, SUITES[name].run, { cwd: ROOT, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
let rest = '';
|
||||
const onData = chunk => { const parts = (rest + chunk).split('\n'); rest = parts.pop(); for (const l of parts) { lines.push(l); console.log(`[${name}] ${l}`); } };
|
||||
p.stdout.on('data', onData); p.stderr.on('data', onData);
|
||||
p.on('close', code => { if (rest) console.log(`[${name}] ${rest}`); resolve({ name, code, seconds: Math.round((Date.now() - t0) / 1000) }); });
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2), all = args.includes('--all'), onlyPlan = args.includes('--plan');
|
||||
const named = args.filter(a => !a.startsWith('--'));
|
||||
for (const n of named) if (!SUITES[n]) { console.error(`prova sconosciuta: ${n} (${Object.keys(SUITES).join(', ')})`); process.exit(2); }
|
||||
const hashes = snapshot(), ok = readOk();
|
||||
const todo = plan(hashes, ok, named.length ? named : undefined).filter(p => all || named.length || !p.green);
|
||||
const green = Object.keys(SUITES).filter(n => !todo.some(t => t.name === n));
|
||||
if (!todo.length) { console.log(`Prove nel browser: tutte verdi dall'ultimo giro (${green.length}); niente da rifare.`); return; }
|
||||
for (const t of todo) {
|
||||
const why = all ? 'tutte' : named.includes(t.name) ? 'chiesta' : !ok[t.name] ? 'mai provata' : `cambiati ${t.changed.slice(0, 4).join(', ')}${t.changed.length > 4 ? ` e altri ${t.changed.length - 4}` : ''}`;
|
||||
console.log(`da rifare: ${t.name}${t.seconds ? ` (~${t.seconds} s)` : ''}: ${why}`);
|
||||
}
|
||||
if (green.length) console.log(`già verdi: ${green.join(', ')}`);
|
||||
if (onlyPlan) return;
|
||||
const jobs = Math.max(1, Number(process.env.JOBS) || Math.min(2, Math.floor(cpus().length / 4)));
|
||||
const queue = todo.sort((a, b) => (b.seconds || 999) - (a.seconds || 999)), results = [];
|
||||
const worker = async () => { for (let t; (t = queue.shift());) {
|
||||
const r = await run(t.name); results.push(r);
|
||||
if (r.code === 0) {
|
||||
const now = readOk();
|
||||
now[t.name] = { fingerprint: t.fp, seconds: r.seconds, when: new Date().toISOString(), hashes: Object.fromEntries(t.inputs.map(p => [p, hashes.get(p)])) };
|
||||
writeFileSync(OK_FILE, JSON.stringify(now, null, 1) + '\n');
|
||||
}
|
||||
} };
|
||||
await Promise.all(Array.from({ length: Math.min(jobs, queue.length) }, worker));
|
||||
for (const r of results.sort((a, b) => a.name.localeCompare(b.name))) console.log(`${r.code === 0 ? 'OK ' : 'FAIL'} ${r.name} (${r.seconds} s)`);
|
||||
const failed = results.filter(r => r.code !== 0).map(r => r.name);
|
||||
if (failed.length) { console.log(`Prove fallite: ${failed.join(', ')}`); process.exit(1); }
|
||||
console.log(`Prove nel browser OK: ${results.length} rifatte, ${green.length} già verdi.`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) main();
|
||||
Reference in New Issue
Block a user