Skip to content

Commit 9439314

Browse files
committed
fix(pages): derive metrics from current gate
1 parent c78b2da commit 9439314

7 files changed

Lines changed: 299 additions & 17 deletions

File tree

docs/playground.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,12 @@ request, or HTTP errors. Set `QUICKJS_OXIDE_COMMIT` when testing an artifact
8585
built with an explicit commit identity.
8686

8787
The page footer keeps the frozen global Test262 vector visibly marked
88-
pre-parity and links the parity contract and Test262 ledger to the exact commit
89-
reported by the loaded WASM package. A local build labelled `local` falls back
90-
to the repository's `main` documentation links.
88+
pre-parity. Its full-pass, eligible-coverage, runnable-quality, and focused
89+
figures are rendered from the authenticated
90+
[`current.conf`](../dev-support/test262/current.conf) rather than copied into
91+
the page. The footer links the parity contract and Test262 ledger to the exact
92+
commit reported by the loaded WASM package. A local build labelled `local`
93+
falls back to the repository's `main` documentation links.
9194

9295
## Deployment
9396

scripts/build-web-playground.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ QUICKJS_OXIDE_COMMIT="${build_commit}" \
5959
rm -rf "${pages_dir}"
6060
mkdir -p "${pages_dir}/pkg"
6161
cp -R "${site_dir}/." "${pages_dir}/"
62+
node "${repo_root}/scripts/current-test262-metrics.mjs" \
63+
--render "${pages_dir}/index.html"
6264
wasm-bindgen \
6365
"${wasm_file}" \
6466
--out-dir "${pages_dir}/pkg" \
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
#!/usr/bin/env node
2+
3+
import {
4+
readFileSync,
5+
writeFileSync,
6+
} from "node:fs";
7+
import path from "node:path";
8+
import process from "node:process";
9+
import { pathToFileURL } from "node:url";
10+
11+
export const PRIMARY_METRICS_TOKEN = "@@QUICKJS_OXIDE_TEST262_PRIMARY@@";
12+
export const DETAIL_METRICS_TOKEN = "@@QUICKJS_OXIDE_TEST262_DETAIL@@";
13+
14+
const DEFAULT_SPEC_PATH = path.resolve(
15+
import.meta.dirname,
16+
"../dev-support/test262/current.conf",
17+
);
18+
const NUMERIC_KEYS = [
19+
"focused_variants",
20+
"focused_eligible",
21+
"focused_runnable",
22+
"focused_passes",
23+
"full_variants",
24+
"full_eligible",
25+
"full_runnable",
26+
"full_passes",
27+
];
28+
29+
function parseSpec(source) {
30+
const values = new Map();
31+
for (const [index, rawLine] of source.split("\n").entries()) {
32+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
33+
if (line === "" || line.startsWith("#")) {
34+
continue;
35+
}
36+
const match = line.match(/^([a-z][a-z0-9_]*)=(.*)$/u);
37+
if (!match) {
38+
throw new TypeError(`malformed Test262 spec line ${index + 1}`);
39+
}
40+
const [, key, value] = match;
41+
if (
42+
value.length === 0 ||
43+
value.trim() !== value ||
44+
!/^[\x20-\x7e]+$/u.test(value)
45+
) {
46+
throw new TypeError(`invalid Test262 spec value for ${key}`);
47+
}
48+
if (values.has(key)) {
49+
throw new TypeError(`duplicate Test262 spec key ${key}`);
50+
}
51+
values.set(key, value);
52+
}
53+
return values;
54+
}
55+
56+
function required(values, key) {
57+
const value = values.get(key);
58+
if (value === undefined) {
59+
throw new TypeError(`missing Test262 spec key ${key}`);
60+
}
61+
return value;
62+
}
63+
64+
function canonicalInteger(values, key) {
65+
const value = required(values, key);
66+
if (!/^(0|[1-9][0-9]*)$/u.test(value)) {
67+
throw new TypeError(`non-canonical Test262 integer ${key}`);
68+
}
69+
const number = Number(value);
70+
if (!Number.isSafeInteger(number)) {
71+
throw new TypeError(`Test262 integer exceeds the safe range: ${key}`);
72+
}
73+
return number;
74+
}
75+
76+
function parseSummary(value, label) {
77+
const outcomes = new Map();
78+
let previous = "";
79+
for (const field of value.split(" ")) {
80+
const match = field.match(/^([a-z][a-z0-9-]*)=(0|[1-9][0-9]*)$/u);
81+
if (!match || match[1] <= previous || outcomes.has(match[1])) {
82+
throw new TypeError(`non-canonical ${label} Test262 summary`);
83+
}
84+
const count = Number(match[2]);
85+
if (!Number.isSafeInteger(count)) {
86+
throw new TypeError(`${label} Test262 summary exceeds the safe range`);
87+
}
88+
outcomes.set(match[1], count);
89+
previous = match[1];
90+
}
91+
return outcomes;
92+
}
93+
94+
function formatInteger(value) {
95+
return String(value).replace(/\B(?=(?:[0-9]{3})+(?![0-9]))/gu, ",");
96+
}
97+
98+
function displayMilestone(value) {
99+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(value)) {
100+
throw new TypeError("invalid Test262 milestone name");
101+
}
102+
return value
103+
.split("-")
104+
.map((part, index) =>
105+
index === 0
106+
? `${part[0].toUpperCase()}${part.slice(1)}`
107+
: part.toUpperCase(),
108+
)
109+
.join("-");
110+
}
111+
112+
export function parseCurrentTest262Metrics(source) {
113+
const values = parseSpec(source);
114+
if (required(values, "schema") !== "test262-gate-v2") {
115+
throw new TypeError("unsupported Test262 gate schema");
116+
}
117+
const numbers = Object.fromEntries(
118+
NUMERIC_KEYS.map((key) => [key, canonicalInteger(values, key)]),
119+
);
120+
const {
121+
focused_variants: focusedVariants,
122+
focused_eligible: focusedEligible,
123+
focused_runnable: focusedRunnable,
124+
focused_passes: focusedPasses,
125+
full_variants: fullVariants,
126+
full_eligible: fullEligible,
127+
full_runnable: fullRunnable,
128+
full_passes: fullPasses,
129+
} = numbers;
130+
if (
131+
focusedPasses > focusedRunnable ||
132+
focusedRunnable !== focusedEligible ||
133+
focusedEligible > focusedVariants ||
134+
fullRunnable === 0 ||
135+
fullPasses > fullRunnable ||
136+
fullRunnable !== fullEligible ||
137+
fullEligible > fullVariants
138+
) {
139+
throw new TypeError("inconsistent Test262 metric ordering");
140+
}
141+
142+
const focusedSummary = parseSummary(
143+
required(values, "focused_summary"),
144+
"focused",
145+
);
146+
const fullSummary = parseSummary(required(values, "full_summary"), "full");
147+
const focusedTotal = [...focusedSummary.values()].reduce(
148+
(total, count) => total + count,
149+
0,
150+
);
151+
const fullTotal = [...fullSummary.values()].reduce(
152+
(total, count) => total + count,
153+
0,
154+
);
155+
const fullIneligible = [...fullSummary]
156+
.filter(([name]) => /^(?:skipped|unsupported)-/u.test(name))
157+
.reduce((total, [, count]) => total + count, 0);
158+
if (
159+
focusedTotal !== focusedVariants ||
160+
focusedSummary.get("pass") !== focusedPasses ||
161+
fullTotal !== fullVariants ||
162+
fullSummary.get("pass") !== fullPasses ||
163+
fullVariants - fullIneligible !== fullEligible
164+
) {
165+
throw new TypeError("Test262 summaries disagree with the official metrics");
166+
}
167+
168+
const milestone = displayMilestone(required(values, "milestone"));
169+
const runnableQuality = ((fullPasses / fullRunnable) * 100).toFixed(3);
170+
return Object.freeze({
171+
detailText:
172+
`${milestone} authenticated vector · ` +
173+
`${formatInteger(focusedPasses)}/${formatInteger(focusedEligible)} focused · ` +
174+
`runnable quality ${formatInteger(fullPasses)}/${formatInteger(fullRunnable)} ` +
175+
`(${runnableQuality}%, secondary) · pre-parity`,
176+
focusedEligible,
177+
focusedPasses,
178+
fullEligible,
179+
fullPasses,
180+
fullRunnable,
181+
fullVariants,
182+
milestone,
183+
primaryText:
184+
`${formatInteger(fullPasses)} full passes / ` +
185+
`${formatInteger(fullEligible)} eligible / ` +
186+
`${formatInteger(fullVariants)} total`,
187+
runnableQuality,
188+
});
189+
}
190+
191+
export function readCurrentTest262Metrics(specPath = DEFAULT_SPEC_PATH) {
192+
return parseCurrentTest262Metrics(readFileSync(specPath, "utf8"));
193+
}
194+
195+
function htmlText(value) {
196+
return value
197+
.replaceAll("&", "&amp;")
198+
.replaceAll("<", "&lt;")
199+
.replaceAll(">", "&gt;")
200+
.replaceAll('"', "&quot;");
201+
}
202+
203+
function replaceExactlyOnce(source, token, value) {
204+
const count = source.split(token).length - 1;
205+
if (count !== 1) {
206+
throw new TypeError(
207+
`expected exactly one ${token} placeholder, found ${count}`,
208+
);
209+
}
210+
return source.replace(token, htmlText(value));
211+
}
212+
213+
export function renderCurrentTest262Metrics(
214+
htmlPath,
215+
specPath = DEFAULT_SPEC_PATH,
216+
) {
217+
const metrics = readCurrentTest262Metrics(specPath);
218+
let html = readFileSync(htmlPath, "utf8");
219+
html = replaceExactlyOnce(html, PRIMARY_METRICS_TOKEN, metrics.primaryText);
220+
html = replaceExactlyOnce(html, DETAIL_METRICS_TOKEN, metrics.detailText);
221+
writeFileSync(htmlPath, html);
222+
return metrics;
223+
}
224+
225+
const invokedAsScript = process.argv[1]
226+
? pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url
227+
: false;
228+
if (invokedAsScript) {
229+
if (process.argv.length !== 4 || process.argv[2] !== "--render") {
230+
console.error(
231+
"usage: current-test262-metrics.mjs --render PATH_TO_INDEX_HTML",
232+
);
233+
process.exitCode = 2;
234+
} else {
235+
const metrics = renderCurrentTest262Metrics(path.resolve(process.argv[3]));
236+
console.log(`Rendered Pages metrics: ${metrics.primaryText}`);
237+
}
238+
}

scripts/test-live-pages.mjs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ import process from "node:process";
1313
import { spawn } from "node:child_process";
1414
import { setTimeout as wait } from "node:timers/promises";
1515
import { fileURLToPath, pathToFileURL } from "node:url";
16+
import { readCurrentTest262Metrics } from "./current-test262-metrics.mjs";
1617

1718
const DEFAULT_ATTEMPTS = 15;
1819
const DEFAULT_RETRY_BASE_MS = 2_000;
1920
const DEFAULT_RETRY_MAX_MS = 10_000;
2021
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
2122
const DEFAULT_CHILD_TIMEOUT_MS = 15_000;
2223
const CHILD_OUTPUT_LIMIT = 16_384;
24+
const CURRENT_TEST262_METRICS = readCurrentTest262Metrics();
2325
const JAVASCRIPT_MIME_TYPES = new Set([
2426
"application/javascript",
2527
"text/javascript",
@@ -265,11 +267,8 @@ function validatePageAndBuildLabel(page, wasm, expectedCommit) {
265267
"function that returns 42",
266268
"real quickjs-oxide Rust interpreter compiled to WebAssembly",
267269
"This editor currently exposes the Script goal",
268-
"68,295 passes / 68,347 runnable / 102,037 total",
269-
"R3ec-A module declaration-position admission",
270-
"86/86 focused",
271-
"+86 canonical passes",
272-
"zero regressions",
270+
CURRENT_TEST262_METRICS.primaryText,
271+
CURRENT_TEST262_METRICS.detailText,
273272
"pre-parity",
274273
];
275274
const missingPageMarker = requiredPageMarkers.find(

scripts/test-web-playground-browser.mjs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
sha256Bytes,
1818
verifyPagesDeployment,
1919
} from "./test-live-pages.mjs";
20+
import { readCurrentTest262Metrics } from "./current-test262-metrics.mjs";
2021

2122
const repoRoot = path.resolve(import.meta.dirname, "..");
2223
const pagesDir = path.resolve(
@@ -32,6 +33,7 @@ const requiredArtifactFiles = [
3233
"pkg/quickjs_oxide_web_bg.wasm",
3334
];
3435
const contentDigestPattern = "[0-9a-f]{64}";
36+
const currentTest262Metrics = readCurrentTest262Metrics();
3537
const requiredBrowserResources = [
3638
[new RegExp(`^${pagesBasePath}app\\.${contentDigestPattern}\\.js$`, "u"), "app"],
3739
[
@@ -556,9 +558,13 @@ async function runAcceptance(url, serverErrors) {
556558
await test262Progress.getAttribute("data-repository-ref"),
557559
expectedDocumentationRef,
558560
);
559-
assert.match(
560-
(await page.locator("#frozen-global-vector").textContent()) || "",
561-
/68,295 passes\s*\/\s*68,347 runnable\s*\/\s*102,037 total[\s\S]*R3ec-A module declaration-position admission[\s\S]*86\/86 focused[\s\S]*\+86 canonical passes[\s\S]*zero regressions[\s\S]*pre-parity/,
561+
assert.equal(
562+
(await page.locator("#test262-primary-metrics").textContent())?.trim(),
563+
currentTest262Metrics.primaryText,
564+
);
565+
assert.equal(
566+
(await page.locator("#test262-detail-metrics").textContent())?.trim(),
567+
currentTest262Metrics.detailText,
562568
);
563569
assert.match(
564570
(await page.locator(".project-note-copy").textContent()) || "",

scripts/test-web-playground.sh

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,41 @@ import assert from "node:assert/strict";
5757
import { readFile } from "node:fs/promises";
5858
import { createRequire } from "node:module";
5959
import path from "node:path";
60+
import { pathToFileURL } from "node:url";
6061
6162
const require = createRequire(import.meta.url);
63+
const metricsModulePath = path.resolve(
64+
process.cwd(),
65+
"scripts/current-test262-metrics.mjs",
66+
);
67+
const { parseCurrentTest262Metrics } = await import(
68+
pathToFileURL(metricsModulePath)
69+
);
70+
const currentSpec = await readFile(
71+
path.resolve(process.cwd(), "dev-support/test262/current.conf"),
72+
"utf8",
73+
);
74+
const currentMetrics = parseCurrentTest262Metrics(currentSpec);
75+
const renderedIndex = await readFile(
76+
path.resolve(process.cwd(), "target/pages/index.html"),
77+
"utf8",
78+
);
79+
assert.ok(renderedIndex.includes(currentMetrics.primaryText));
80+
assert.ok(renderedIndex.includes(currentMetrics.detailText));
81+
assert.throws(
82+
() => parseCurrentTest262Metrics(`${currentSpec}\nfull_passes=1\n`),
83+
/duplicate Test262 spec key full_passes/u,
84+
);
85+
assert.throws(
86+
() => parseCurrentTest262Metrics(
87+
currentSpec.replace(
88+
`pass=${currentMetrics.fullPasses}`,
89+
`pass=${currentMetrics.fullPasses - 1}`,
90+
),
91+
),
92+
/summaries disagree with the official metrics/u,
93+
);
94+
6295
const wrapperPath = path.resolve(
6396
process.cwd(),
6497
"target/web-playground-node/quickjs_oxide_web.js",
@@ -184,6 +217,6 @@ assert.equal(syntaxError.kind, "exception");
184217
assert.match(syntaxError.text, /^SyntaxError:/);
185218
186219
console.log(
187-
`Node/WASM smoke: ${examplesModule.EXAMPLES.length} playground examples and build metadata passed; direct eval and quickjs-oxide returned 42; deep yield-star overflow stayed catchable`,
220+
`Node/WASM smoke: ${examplesModule.EXAMPLES.length} playground examples, current Test262 metrics, and build metadata passed; direct eval and quickjs-oxide returned 42; deep yield-star overflow stayed catchable`,
188221
);
189222
NODE

web/site/index.html

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -310,11 +310,12 @@ <h2 id="project-note-title">No browser-native shortcut.</h2>
310310
</p>
311311
<p id="frozen-global-vector" class="footer-vector">
312312
Frozen global Test262 vector
313-
<strong>68,295 passes / 68,347 runnable / 102,037 total</strong>
314-
<span>
315-
R3ec-A module declaration-position admission · 86/86 focused ·
316-
+86 canonical passes · zero regressions · pre-parity
317-
</span>
313+
<strong id="test262-primary-metrics"
314+
>@@QUICKJS_OXIDE_TEST262_PRIMARY@@</strong
315+
>
316+
<span id="test262-detail-metrics"
317+
>@@QUICKJS_OXIDE_TEST262_DETAIL@@</span
318+
>
318319
</p>
319320
</div>
320321

0 commit comments

Comments
 (0)