新增:模拟一致性检查页(/dev/sim-check.html),任意设备的浏览器重放黄金结果的整套战斗,逐帧与 Node 比较
This commit is contained in:
1 parent
bc50cac01f
commit
ce52512559
11 files changed
+826
No files matched your search
@@ -0,0 +1,102 @@
|
||||
// simcheck's Node side: the reference (every scenario traced in Node, core.js), headless runs of the check page in
|
||||
// several engines, and the field-by-field comparison of a dump a browser downloaded with Node's run of the same ticks.
|
||||
// Engines: Playwright's chromium / firefox / webkit when the `playwright` package is installed (its browsers fetched
|
||||
// with `npx playwright install firefox webkit`), else a local Chrome through puppeteer-core ($CHROME_PATH or the usual
|
||||
// install paths).
|
||||
//
|
||||
// Written for Stronghold Protocol (GPL-3.0-or-later) and Duel Channel (AGPL-3.0-or-later) by the same author, and kept
|
||||
// the same in both.
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { tracer, diffDumps, VERSION } from './core.js';
|
||||
|
||||
/**
|
||||
* Trace every scenario in Node: the reference a page compares with.
|
||||
* @param {object} adapter @param {{ id: string }[]} scenarios
|
||||
* @param {{ every?: number, project: string, made?: object, onProgress?: (i: number, n: number, id: string) => void }} o
|
||||
*/
|
||||
export function buildReference(adapter, scenarios, { every = 30, deepEvery = every * 5, project, made = {}, onProgress } = {}) {
|
||||
const out = {};
|
||||
scenarios.forEach((sc, i) => {
|
||||
if (onProgress) onProgress(i, scenarios.length, sc.id);
|
||||
const { ticks, blocks, views, deeps, result } = tracer(adapter, sc, { every, deepEvery }).finish();
|
||||
out[sc.id] = { ticks, blocks, views, deeps, result };
|
||||
});
|
||||
return { simcheck: VERSION, project, every, deepEvery, made: { engine: `Node ${process.version}`, date: new Date().toISOString().slice(0, 10), ...made }, scenarios: out };
|
||||
}
|
||||
|
||||
const CHROMES = [process.env.CHROME_PATH, 'C:/Program Files/Google/Chrome/Application/chrome.exe', 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe',
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium', '/usr/bin/chromium-browser'];
|
||||
async function tryImport(name) { try { return await import(name); } catch { return null; } }
|
||||
|
||||
/**
|
||||
* Open the check page (url, with ?auto=1 and the subset) in each engine and wait for its results.
|
||||
* @param {string} url @param {string[]} engines 'chromium' | 'firefox' | 'webkit' | 'chrome' (a local Chrome)
|
||||
* @returns {Promise<Record<string, { version?: string, status: string, summary?: object, results?: object[], report?: string, error?: string }>>}
|
||||
*/
|
||||
export async function runEngines(url, engines, { timeoutMs = 30 * 60000, log = () => {} } = {}) {
|
||||
const pw = await tryImport('playwright'), pp = pw ? null : await tryImport('puppeteer-core');
|
||||
const out = {};
|
||||
for (const name of engines) {
|
||||
let browser = null;
|
||||
try {
|
||||
if (pw && pw[name === 'chrome' ? 'chromium' : name]) {
|
||||
const type = pw[name === 'chrome' ? 'chromium' : name], exe = CHROMES.find((p) => p && existsSync(p));
|
||||
// chromium / chrome: Playwright's own build when it is installed, else the local Chrome
|
||||
try { browser = await type.launch(name === 'chrome' && exe ? { executablePath: exe } : {}); } catch (e) {
|
||||
if (type !== pw.chromium || !exe) throw e;
|
||||
browser = await type.launch({ executablePath: exe });
|
||||
}
|
||||
} else if (pp && (name === 'chrome' || name === 'chromium')) {
|
||||
const exe = CHROMES.find((p) => p && existsSync(p));
|
||||
if (!exe) throw new Error('no Chrome found: set CHROME_PATH');
|
||||
browser = await (pp.default || pp).launch({ executablePath: exe, headless: true });
|
||||
} else throw new Error(pw ? `playwright has no ${name}` : `${name} needs the playwright package (npm i -D playwright && npx playwright install ${name})`);
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', (e) => errors.push(String(e.message || e)));
|
||||
await page.goto(url, { timeout: 120000 });
|
||||
const t0 = Date.now();
|
||||
let last = -1;
|
||||
for (;;) {
|
||||
const st = await page.evaluate(() => { const s = globalThis.__simcheck; return s ? { status: s.status, n: s.results.length } : null; });
|
||||
if (st && (st.status === 'done' || st.status === 'stopped')) break;
|
||||
if (errors.length && !st) throw new Error(errors.join('; '));
|
||||
if (st && st.n !== last && st.n % 25 === 0) { log(` ${name}: ${st.n}`); last = st.n; }
|
||||
if (Date.now() - t0 > timeoutMs) throw new Error('timed out');
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
}
|
||||
const res = await page.evaluate(() => { const s = globalThis.__simcheck; return { status: s.status, summary: s.summary, results: s.results, report: s.report }; });
|
||||
out[name] = { version: browser.version ? await browser.version() : '', ...res, errors };
|
||||
} catch (e) {
|
||||
out[name] = { status: 'error', error: String(e && e.message || e) };
|
||||
} finally { if (browser) await browser.close().catch(() => {}); }
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* A dump a browser downloaded (the page's 下载这一段的状态) against Node's run of the same ticks: the differing entries,
|
||||
* first ones first, as printable lines.
|
||||
* @param {string} file @param {object} adapter @param {{ id: string }[]} scenarios
|
||||
*/
|
||||
export function diffDumpFile(file, adapter, scenarios, { every = 30, deepEvery = every * 5, limit = 20 } = {}) {
|
||||
const d = JSON.parse(readFileSync(file, 'utf8'));
|
||||
const sc = scenarios.find((s) => s.id === d.id);
|
||||
if (!sc) return [`no scenario ${d.id} here`];
|
||||
const ref = tracer(adapter, sc, { every, deepEvery, dump: d.window }).finish();
|
||||
const diffs = diffDumps(ref.dumps, d.dumps, limit);
|
||||
const lines = [`${d.id}, ticks ${d.window[0]}–${d.window[1]}: Node ${process.version} vs ${d.engine}`];
|
||||
if (!diffs.length) lines.push(' the same in every entry of these ticks (the difference is in what the state vector does not hold: view / deep)');
|
||||
for (const x of diffs) lines.push(x.index < 0 ? ` tick ${x.tick}: ${x.label}` : ` tick ${x.tick} ${x.label}: ${x.ref} (${x.refBits}) vs ${x.got} (${x.gotBits})`);
|
||||
return lines;
|
||||
}
|
||||
|
||||
/** One line per engine for a console. */
|
||||
export function formatEngines(res) {
|
||||
return Object.entries(res).map(([name, r]) => {
|
||||
if (r.status === 'error') return `${name}: error — ${r.error}`;
|
||||
const s = r.summary || {};
|
||||
const bad = (r.results || []).filter((x) => !x.c.ok).map((x) => x.id);
|
||||
return `${name} ${r.version || ''}: ${s.ok}/${s.n} the same${bad.length ? ` — different: ${bad.slice(0, 12).join(', ')}${bad.length > 12 ? ' …' : ''}` : ''}${r.errors && r.errors.length ? ` (page errors: ${r.errors.length})` : ''}`;
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user