23 lines
9.1 KiB
JavaScript
23 lines
9.1 KiB
JavaScript
#!/usr/bin/env node
|
|
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 { spawn } from "node:child_process";
|
|
|
|
const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); const dist = path.join(root, "web/dist"); const docs = path.join(root, "docs/p6"); const chromePath = ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/opt/google/chrome/google-chrome"].find(fs.existsSync); const viewport = { width: 1440, height: 900, deviceScaleFactor: 1 }; const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const hash = (value) => crypto.createHash("sha256").update(value).digest("hex");
|
|
const ioMode = process.argv.includes("--io");
|
|
const inspectMode = process.argv.includes("--inspect");
|
|
const pythonMode = process.argv.includes("--python");
|
|
const saveMode = process.argv.includes("--save");
|
|
if (!chromePath) { process.exitCode = 2; } else {
|
|
const server = http.createServer((req, res) => { const requestPath = decodeURIComponent(new URL(req.url ?? "/", "http://127.0.0.1").pathname); const filePath = path.resolve(dist, requestPath === "/" ? "mesh.html" : requestPath.replace(/^\/+/, "")); if (!fs.existsSync(filePath)) { res.writeHead(404); res.end(); return; } const extension = path.extname(filePath); const contentType = extension === ".html" ? "text/html; charset=utf-8" : extension === ".js" ? "text/javascript; charset=utf-8" : "application/wasm"; res.writeHead(200, { "content-type": contentType, "cache-control": "no-store" }); fs.createReadStream(filePath).pipe(res); });
|
|
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", resolve); }); const origin = `http://127.0.0.1:${server.address().port}`; const profile = fs.mkdtempSync(path.join(os.tmpdir(), "freecad-p6-mesh-chrome-")); const chrome = spawn(chromePath, ["--headless=new", "--no-sandbox", "--disable-gpu", "--disable-dev-shm-usage", "--remote-debugging-port=0", "--remote-allow-origins=*", `--user-data-dir=${profile}`, `--window-size=${viewport.width},${viewport.height}`, `${origin}/mesh.html${ioMode ? "?action=io" : inspectMode ? "?action=inspect" : pythonMode ? "?action=python" : saveMode ? "?action=save" : ""}`], { stdio: ["ignore", "pipe", "pipe"] }); let output = ""; chrome.stdout.on("data", (chunk) => { output += chunk; }); chrome.stderr.on("data", (chunk) => { output += chunk; });
|
|
class Cdp { constructor(url) { this.socket = new WebSocket(url); this.id = 0; this.pending = new Map(); this.listeners = new Map(); this.ready = new Promise((resolve, reject) => { this.socket.addEventListener("open", resolve, { once: true }); this.socket.addEventListener("error", reject, { once: true }); }); this.socket.addEventListener("message", (event) => { const message = JSON.parse(event.data); if (message.id) { const pending = this.pending.get(message.id); if (!pending) return; this.pending.delete(message.id); if (message.error) pending.reject(new Error(message.error.message)); else pending.resolve(message.result); return; } for (const listener of this.listeners.get(message.method) ?? []) listener(message.params); }); } async send(method, params = {}) { await this.ready; return new Promise((resolve, reject) => { const id = ++this.id; this.pending.set(id, { resolve, reject }); this.socket.send(JSON.stringify({ id, method, params })); }); } on(method, listener) { this.listeners.set(method, [...(this.listeners.get(method) ?? []), listener]); } close() { this.socket.close(); } }
|
|
let cdp; try {
|
|
let target; for (let attempt = 0; attempt < 300 && !target; attempt += 1) { const match = output.match(/DevTools listening on ws:\/\/([^\s]+)/); if (match) { const port = match[1].split(":")[1].split("/")[0]; try { const response = await fetch(`http://127.0.0.1:${port}/json/list`); target = (await response.json()).find((item) => item.type === "page"); } catch { target = undefined; } } if (!target) await wait(100); }
|
|
if (!target?.webSocketDebuggerUrl) throw new Error("Chrome CDP target unavailable"); cdp = new Cdp(target.webSocketDebuggerUrl); const requests = []; const exceptions = []; const consoleEntries = []; cdp.on("Network.requestWillBeSent", (event) => requests.push(event.request.url)); cdp.on("Runtime.exceptionThrown", (event) => exceptions.push(event.exceptionDetails)); cdp.on("Runtime.consoleAPICalled", (event) => consoleEntries.push({ type: event.type, args: event.args?.length ?? 0 })); await cdp.send("Runtime.enable"); await cdp.send("Network.enable"); await cdp.send("Page.enable"); await cdp.send("Emulation.setDeviceMetricsOverride", { ...viewport, mobile: false }); const evaluate = async (expression) => (await cdp.send("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true })).result?.value; const state = () => evaluate("window.__freecadMeshState"); const waitStage = async (wanted) => { for (let attempt = 0; attempt < 300; attempt += 1) { const value = await state(); if (value?.stage === wanted) return value; await wait(100); } throw new Error(`${wanted} timed out`); }; const ready = await waitStage("ready"); await evaluate("document.querySelector('#run').click()"); const success = await waitStage("success"); await evaluate("document.querySelector('#error').click()"); const expectedError = await waitStage("expected-error"); await evaluate("document.querySelector('#reset').click()"); const reset = await waitStage("reset"); const screenshot = Buffer.from((await cdp.send("Page.captureScreenshot", { format: "png", fromSurface: true })).data, "base64"); const artifactStem = ioMode ? "mesh-action-02-chrome" : inspectMode ? "mesh-action-03-chrome" : pythonMode ? "mesh-action-04-chrome" : saveMode ? "mesh-action-05-chrome" : "mesh-chrome"; fs.writeFileSync(path.join(docs, `${artifactStem}.png`), screenshot); const externalRequests = requests.filter((url) => !url.startsWith(origin)); const version = await new Promise((resolve) => { const child = spawn(chromePath, ["--version"], { stdio: ["ignore", "pipe", "ignore"] }); let text = ""; child.stdout.on("data", (chunk) => { text += chunk; }); child.on("close", () => resolve(text.trim())); }); const result = (ioMode ? success?.response?.kind === "result" && success.response.result?.action === "import/export" && success.response.result?.importedFacetCount === 1 && success.response.result?.importedPointCount === 3 : inspectMode ? success?.response?.kind === "result" && success.response.result?.action === "inspect/source-inventory" && success.response.result?.registeredTypeCount >= 20 && success.response.result?.pythonBindingCount === 5 : pythonMode ? success?.response?.kind === "result" && success.response.result?.action === "python-api" && success.response.result?.apiCount === 5 && success.response.result?.apiNames?.length === 5 : saveMode ? success?.response?.kind === "result" && success.response.result?.action === "save/reopen" && success.response.result?.roundTrip === true && success.response.result?.facetCount === 1 && success.response.result?.pointCount === 3 : success?.response?.kind === "result" && success.response.result?.facetCount === 1 && success.response.result?.pointCount === 3) && expectedError?.response?.kind === "error" && expectedError.response.error?.code === (inspectMode || pythonMode ? "revision_mismatch" : saveMode ? "invalid_path" : "invalid_fixture") && reset?.stage === "reset" && externalRequests.length === 0 && exceptions.length === 0 && consoleEntries.filter((entry) => entry.type === "error").length === 0 ? "pass" : "fail"; const report = { schema: ioMode ? "freecad-web-p6-mesh-action-02-chrome/v1" : inspectMode ? "freecad-web-p6-mesh-action-03-chrome/v1" : pythonMode ? "freecad-web-p6-mesh-action-04-chrome/v1" : saveMode ? "freecad-web-p6-mesh-action-05-chrome/v1" : "freecad-web-p6-mesh-chrome/v1", taskId: ioMode ? "FW-P6-MSH-ACT-02-F" : inspectMode ? "FW-P6-MSH-ACT-03-F" : pythonMode ? "FW-P6-MSH-ACT-04-F" : saveMode ? "FW-P6-MSH-ACT-05-F" : "FW-P6-MSH-F", module: "Mesh", action: ioMode ? "import/export" : inspectMode ? "inspect/source-inventory" : pythonMode ? "python-api" : saveMode ? "save/reopen" : "create/recompute", result, chrome: { path: chromePath, version }, mode: "headless-new", origin, viewport, cases: { ready, success, expectedError, reset }, network: { requests, externalRequests, externalRequestCount: externalRequests.length }, console: consoleEntries, exceptions, policy: { browser: "Google Chrome only", nonChromeBrowsersAttempted: false }, evidence: { screenshot: `docs/p6/${artifactStem}.png`, screenshotSha256: hash(screenshot), resultHash: hash(JSON.stringify({ ready, success, expectedError, reset })) } }; fs.writeFileSync(path.join(docs, `${artifactStem}.json`), `${JSON.stringify(report, null, 2)}\n`); fs.writeFileSync(path.join(root, `docs/freecad-p6-mesh-${ioMode ? "action-02-f" : inspectMode ? "action-03-f" : pythonMode ? "action-04-f" : saveMode ? "action-05-f" : "f"}-chrome.log`), `result=${result}\nchrome=${version}\nexternalRequests=${externalRequests.length}\nexceptions=${exceptions.length}\n`); console.log(JSON.stringify({ result, chrome: version, externalRequests: externalRequests.length, exceptions: exceptions.length })); if (result !== "pass") process.exitCode = 1;
|
|
} finally { cdp?.close(); chrome.kill("SIGTERM"); server.close(); }
|
|
}
|