Checkpoint web parity through Chromium input tasks
This commit is contained in:
188
tools/web/check-probe-identity.mjs
Normal file
188
tools/web/check-probe-identity.mjs
Normal file
@@ -0,0 +1,188 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import http from "node:http";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const distRoot = path.join(root, "web/dist");
|
||||
const reportPath = path.join(root, "tests/golden/M14-01D/probe-identity-report.json");
|
||||
const manifestPath = path.join(root, "tests/golden/M14-01D/manifest.json");
|
||||
const digest = (value) => crypto.createHash("sha256").update(value).digest("hex");
|
||||
const fileDigest = (file) => digest(fs.readFileSync(file));
|
||||
const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/");
|
||||
const assetsRoot = path.join(distRoot, "assets");
|
||||
assert.ok(fs.existsSync(assetsRoot), "production assets are missing; run npm --prefix web run build first");
|
||||
const workerName = fs.readdirSync(assetsRoot).find((name) => /^storage\.worker-[\w-]+\.js$/u.test(name));
|
||||
const wasmName = fs.readdirSync(assetsRoot).find((name) => /^web_engine-[\w-]+\.wasm$/u.test(name));
|
||||
assert.ok(workerName && wasmName, "production worker/WASM assets are missing");
|
||||
|
||||
const mime = new Map([
|
||||
[".html", "text/html; charset=utf-8"],
|
||||
[".js", "text/javascript; charset=utf-8"],
|
||||
[".css", "text/css; charset=utf-8"],
|
||||
[".json", "application/json"],
|
||||
[".wasm", "application/wasm"],
|
||||
]);
|
||||
const server = http.createServer((request, response) => {
|
||||
const pathname = decodeURIComponent(new URL(request.url ?? "/", "http://127.0.0.1").pathname);
|
||||
const relativePath = pathname === "/" ? "index.html" : pathname.replace(/^\//u, "");
|
||||
const file = path.resolve(distRoot, relativePath);
|
||||
if (!file.startsWith(`${distRoot}${path.sep}`) || !fs.existsSync(file) || !fs.statSync(file).isFile()) {
|
||||
response.writeHead(404);
|
||||
response.end("not found");
|
||||
return;
|
||||
}
|
||||
response.statusCode = 200;
|
||||
response.setHeader("Content-Type", mime.get(path.extname(file)) ?? "application/octet-stream");
|
||||
response.setHeader("Cross-Origin-Opener-Policy", "same-origin");
|
||||
response.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
|
||||
response.setHeader("Cross-Origin-Resource-Policy", "same-origin");
|
||||
response.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self'; font-src 'self'; img-src 'self'; media-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'");
|
||||
fs.createReadStream(file).pipe(response);
|
||||
});
|
||||
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const address = server.address();
|
||||
assert.ok(address && typeof address === "object");
|
||||
const origin = `http://127.0.0.1:${address.port}`;
|
||||
let browser;
|
||||
try {
|
||||
const playwright = await import(pathToFileURL(path.join(root, "web/node_modules/playwright/index.mjs")).href);
|
||||
browser = await playwright.chromium.launch({ headless: true });
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
await page.goto(`${origin}/`, { waitUntil: "load" });
|
||||
const probe = await page.evaluate(async ({ workerPath, wasmPath }) => {
|
||||
const run = async (name, operation) => {
|
||||
try {
|
||||
return { name, ...await operation() };
|
||||
} catch (error) {
|
||||
return {
|
||||
name,
|
||||
status: "BLOCKED",
|
||||
code: error instanceof Error ? error.name : "PROBE_FAILED",
|
||||
detail: error instanceof Error ? error.message.slice(0, 200) : String(error),
|
||||
};
|
||||
}
|
||||
};
|
||||
const wasm = await run("wasm", async () => {
|
||||
const response = await fetch(wasmPath);
|
||||
if (!response.ok) throw new Error(`HTTP_${response.status}`);
|
||||
const bytes = await response.arrayBuffer();
|
||||
await WebAssembly.compile(bytes);
|
||||
return { status: "PASS", code: "WASM_READY", bytes: bytes.byteLength };
|
||||
});
|
||||
const worker = await run("worker", async () => {
|
||||
await new Promise((resolve, reject) => {
|
||||
const value = new Worker(workerPath, { type: "module" });
|
||||
const timer = setTimeout(() => { value.terminate(); resolve(); }, 500);
|
||||
value.onerror = (event) => { clearTimeout(timer); value.terminate(); reject(new Error(event.message || "WORKER_LOAD_FAILED")); };
|
||||
});
|
||||
return { status: "PASS", code: "WORKER_READY" };
|
||||
});
|
||||
const webgl2 = await run("webgl2", async () => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const gl = canvas.getContext("webgl2");
|
||||
if (!gl) return { status: "BLOCKED", code: "WEBGL2_UNAVAILABLE" };
|
||||
const debug = gl.getExtension("WEBGL_debug_renderer_info");
|
||||
return {
|
||||
status: "PASS",
|
||||
code: "WEBGL2_READY",
|
||||
vendor: debug ? gl.getParameter(debug.UNMASKED_VENDOR_WEBGL) : "REDACTED",
|
||||
renderer: debug ? gl.getParameter(debug.UNMASKED_RENDERER_WEBGL) : "REDACTED",
|
||||
version: gl.getParameter(gl.VERSION),
|
||||
};
|
||||
});
|
||||
const webgpu = await run("webgpu", async () => {
|
||||
if (!("gpu" in navigator)) return { status: "BLOCKED", code: "WEBGPU_UNAVAILABLE" };
|
||||
const adapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" });
|
||||
if (!adapter) return { status: "BLOCKED", code: "WEBGPU_ADAPTER_UNAVAILABLE" };
|
||||
const info = adapter.info ?? {};
|
||||
return {
|
||||
status: "PASS",
|
||||
code: "WEBGPU_READY",
|
||||
vendor: info.vendor ?? "REDACTED",
|
||||
architecture: info.architecture ?? "REDACTED",
|
||||
device: info.device ?? "REDACTED",
|
||||
description: info.description ?? "REDACTED",
|
||||
isFallbackAdapter: Boolean(adapter.isFallbackAdapter),
|
||||
};
|
||||
});
|
||||
return {
|
||||
userAgent: navigator.userAgent,
|
||||
platform: navigator.platform,
|
||||
language: navigator.language,
|
||||
hardwareConcurrency: navigator.hardwareConcurrency,
|
||||
deviceMemory: typeof navigator.deviceMemory === "number" ? navigator.deviceMemory : null,
|
||||
crossOriginIsolated,
|
||||
capabilities: [wasm, worker, webgl2, webgpu],
|
||||
};
|
||||
}, { workerPath: `/assets/${workerName}`, wasmPath: `/assets/${wasmName}` });
|
||||
|
||||
const capabilities = Object.fromEntries(probe.capabilities.map((item) => [item.name, item]));
|
||||
const reportWithoutIdentity = {
|
||||
schemaVersion: 1,
|
||||
task: "M14-01D",
|
||||
operation: "PROBE_IDENTITY_GPU_OS_ADAPTER",
|
||||
runtime: "PLAYWRIGHT_CHROMIUM",
|
||||
browser: {
|
||||
version: await browser.version(),
|
||||
executablePath: playwright.chromium.executablePath(),
|
||||
userAgent: probe.userAgent,
|
||||
platform: probe.platform,
|
||||
language: probe.language,
|
||||
hardwareConcurrency: probe.hardwareConcurrency,
|
||||
deviceMemory: probe.deviceMemory,
|
||||
},
|
||||
os: {
|
||||
platform: os.platform(),
|
||||
release: os.release(),
|
||||
version: os.version(),
|
||||
arch: os.arch(),
|
||||
machine: os.machine(),
|
||||
cpus: os.cpus().length,
|
||||
},
|
||||
adapter: {
|
||||
webgl2: capabilities.webgl2,
|
||||
webgpu: capabilities.webgpu,
|
||||
},
|
||||
isolation: { crossOriginIsolated: probe.crossOriginIsolated },
|
||||
assets: {
|
||||
worker: { path: relative(path.join(assetsRoot, workerName)), sha256: fileDigest(path.join(assetsRoot, workerName)) },
|
||||
wasm: { path: relative(path.join(assetsRoot, wasmName)), sha256: fileDigest(path.join(assetsRoot, wasmName)) },
|
||||
},
|
||||
supportRule: "IDENTITY_IS_OBSERVED_ONLY; PASS_ONLY_WHEN_PROBED; BLOCKED_OR_UNAVAILABLE_DOES_NOT_CLAIM_SUPPORT",
|
||||
execution: "DISABLED",
|
||||
nextTask: "M14-01E",
|
||||
};
|
||||
const identityInput = JSON.stringify(reportWithoutIdentity);
|
||||
const report = { ...reportWithoutIdentity, identitySha256: digest(identityInput) };
|
||||
if (process.env.UPDATE_M14_01D_REPORT === "1") {
|
||||
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
|
||||
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
|
||||
if (process.env.UPDATE_M14_01D_REPORT === "1") {
|
||||
const artifactPaths = {
|
||||
parentManifest: path.join(root, "tests/golden/M14-01C/manifest.json"),
|
||||
checker: path.join(root, "tools/web/check-probe-identity.mjs"),
|
||||
package: path.join(root, "web/package.json"),
|
||||
engineManifest: path.join(root, "web/app/public/engine-manifest.json"),
|
||||
report: reportPath,
|
||||
};
|
||||
const artifacts = Object.fromEntries(Object.entries(artifactPaths).map(([name, file]) => [name, { path: relative(file), sha256: fileDigest(file) }]));
|
||||
fs.writeFileSync(manifestPath, `${JSON.stringify({ schemaVersion: 1, task: "M14-01D", parentTask: "M14-01C", enablingTask: false, parityStateChange: false, runtime: "PLAYWRIGHT_CHROMIUM", operation: "PROBE_IDENTITY_GPU_OS_ADAPTER", artifacts, nextTask: "M14-01E" }, null, 2)}\n`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report);
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
||||
assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M14-01D", parentTask: "M14-01C", nextTask: "M14-01E" });
|
||||
for (const artifact of Object.values(manifest.artifacts)) assert.equal(fileDigest(path.join(root, artifact.path)), artifact.sha256, artifact.path);
|
||||
assert.equal(report.identitySha256, digest(JSON.stringify(reportWithoutIdentity)));
|
||||
const summary = [capabilities.webgl2, capabilities.webgpu].map((item) => `${item.name}=${item.status}`).join(",");
|
||||
process.stdout.write(`probe-identity-ok version=${report.browser.version} os=${report.os.platform}/${report.os.arch} ${summary} identity=${report.identitySha256} execution=DISABLED next=${manifest.nextTask}\n`);
|
||||
} finally {
|
||||
await browser?.close();
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
}
|
||||
Reference in New Issue
Block a user