130 lines
8.1 KiB
JavaScript
130 lines
8.1 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { execFileSync } from "node:child_process";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import { loadWasmProbe } from "./wasm-probe-runtime.mjs";
|
|
import { writeBusinessReport } from "./business-report.mjs";
|
|
import { runStartBrowserCases } from "./start-browser-business-cases.mjs";
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const supported = [
|
|
"Start:start.moduleImport",
|
|
"Start:start.importSvg",
|
|
"business-format:Start:import:.svg",
|
|
];
|
|
const out = fs.mkdtempSync(path.join(os.tmpdir(), "freecad-start-business-"));
|
|
const results = [];
|
|
const timings = [];
|
|
let failed = false;
|
|
let kernel;
|
|
let browserCases;
|
|
const timed = async (stage, action) => {
|
|
const start = performance.now();
|
|
try { return await action(); }
|
|
finally { timings.push({ stage, seconds: (performance.now() - start) / 1000 }); }
|
|
};
|
|
try {
|
|
const ids = JSON.parse(process.env.FREECAD_BUSINESS_IDS ?? JSON.stringify(supported));
|
|
assert.ok(Array.isArray(ids) && ids.length && ids.every(id => supported.includes(id)));
|
|
results.push(...[...new Set(ids)].map(capabilityId => ({ capabilityId, status: "failed", cases: [] })));
|
|
await timed("transpile", () => execFileSync(path.join(root, "web/node_modules/.bin/tsc"), [
|
|
"--target", "ES2022", "--module", "ESNext", "--moduleResolution", "Bundler", "--strict",
|
|
"--skipLibCheck", "--outDir", out, path.join(root, "web/src/start-facade.ts"),
|
|
], { stdio: "pipe", timeout: 30000 }));
|
|
const facade = await import(pathToFileURL(path.join(out, "start-facade.js")));
|
|
const { FacadeSerialQueue } = await import(pathToFileURL(path.join(out, "facade-envelope.js")));
|
|
({ kernel } = await timed("load", () => loadWasmProbe(
|
|
path.join(root, "freecad-source/build-web/configure/bin"), "FreeCADWebStartRuntimeProbe")));
|
|
const queue = new FacadeSerialQueue();
|
|
let sequence = 0;
|
|
const record = (result, input, actual, checks) => {
|
|
const assertions = checks.map(([name, passed]) => ({ name, passed: Boolean(passed) }));
|
|
result.cases.push({ input, actual, assertions });
|
|
assert.ok(assertions.every(check => check.passed), JSON.stringify(result.cases.at(-1)));
|
|
};
|
|
// Initialize before either selected workflow, sharing one runtime throughout.
|
|
const initial = await timed("initialize", () => facade.dispatchStart(queue,
|
|
facade.createStartRequest(`init-${++sequence}`), kernel));
|
|
assert.equal(initial.kind, "result", JSON.stringify(initial));
|
|
for (const result of results) {
|
|
try {
|
|
if (result.capabilityId === "Start:start.moduleImport") {
|
|
for (const sourceEntry of [facade.START_FIXTURE.sourceEntry, "invalid.cpp", facade.START_FIXTURE.sourceEntry]) {
|
|
const input = { ...facade.START_FIXTURE, sourceEntry };
|
|
const actual = await facade.dispatchStart(queue, facade.createStartRequest(`module-${++sequence}`, input), kernel);
|
|
const valid = sourceEntry === facade.START_FIXTURE.sourceEntry;
|
|
record(result, input, actual, [["module imports or rejects an unlocked source", valid
|
|
? actual.kind === "result" && actual.result.moduleAvailable && actual.result.nativeTypeCount === 0
|
|
&& actual.result.pythonApiCount === 0 && actual.result.resourcesReleased
|
|
: actual.kind === "error" && actual.error.code === "revision_mismatch"],
|
|
["native initialization remains idempotent", kernel.ccall("fc_web_start_initialization_runs", "number") === 1]]);
|
|
}
|
|
} else {
|
|
const invoke = (file, content) => timed("svg-import", () => facade.dispatchStartImportSvg(queue,
|
|
facade.createStartImportSvgRequest(`svg-${++sequence}`, file, content), kernel));
|
|
let previous;
|
|
let firstLength;
|
|
for (const [width, height] of [[5, 3], [8, 4]]) {
|
|
const input = { path: `/tmp/rectangle-${width}.svg`, content:
|
|
`<svg xmlns="http://www.w3.org/2000/svg"><rect width="${width}" height="${height}"/></svg>` };
|
|
const actual = await invoke(input.path, input.content);
|
|
const value = actual.result;
|
|
const length = value?.objects?.reduce((sum, object) => sum + object.length, 0);
|
|
record(result, input, actual, [
|
|
["SVG content creates valid native geometry", actual.kind === "result" && value.recomputed
|
|
&& value.objects?.length > 0 && value.objects.every(object => object.valid && !object.isNull && object.length > 0)],
|
|
["import appends to the active document", !previous || value?.document === previous.document
|
|
&& value.objectCount > previous.objectCount && value.object !== previous.object],
|
|
["native perimeter responds to changed dimensions", firstLength === undefined
|
|
? length > 0 : Math.abs(length / firstLength - 1.5) < 1e-8],
|
|
]);
|
|
previous = value;
|
|
firstLength ??= length;
|
|
}
|
|
for (const input of [{ path: "/tmp/bad.svg", content: "not XML" },
|
|
{ path: "/tmp/partial.svg", content: '<svg xmlns="http://www.w3.org/2000/svg"><rect width="5" height="3"/><path d="M 0 0 L 3 0"' },
|
|
{ path: "/tmp/absent-start.svg", content: "" }, { path: "/tmp/bad.txt", content: "invalid" }]) {
|
|
const actual = await invoke(input.path, input.content);
|
|
record(result, input, actual, [["invalid or missing SVG is rejected", actual.kind === "error"]]);
|
|
}
|
|
const input = { path: "/tmp/recovery.svg", content:
|
|
'<svg xmlns="http://www.w3.org/2000/svg"><rect width="5" height="3"/></svg>' };
|
|
const actual = await invoke(input.path, input.content);
|
|
record(result, input, actual, [["failure recovery preserves existing document and geometry", actual.kind === "result"
|
|
&& actual.result.document === previous.document && actual.result.objectCount === previous.objectCount + 1
|
|
&& Math.abs(actual.result.objects.reduce((sum, object) => sum + object.length, 0) - firstLength) < 1e-8]]);
|
|
for (const [name, shape, expectedLength, count] of [
|
|
["circle", '<circle cx="8" cy="8" r="3"/>', 6 * Math.PI, 1],
|
|
["polyline", '<polyline points="0,0 3,0 3,4" fill="none"/>', 7, 1],
|
|
["polygon", '<polygon points="0,0 3,0 3,4"/>', 12, 1],
|
|
["transform", '<g transform="translate(10,20) scale(2)"><rect width="5" height="3"/></g>', 32, 1],
|
|
["arc", '<path d="M 0 0 A 5 5 0 0 1 10 0" fill="none"/>', 5 * Math.PI, 1],
|
|
["mixed", '<rect width="5" height="3"/><circle cx="15" cy="15" r="2"/>', 16 + 4 * Math.PI, 2],
|
|
]) {
|
|
const input = { path: `/tmp/${name}.svg`, content: `<svg xmlns="http://www.w3.org/2000/svg" width="100mm" height="100mm" viewBox="0 0 100 100">${shape}</svg>` };
|
|
const actual = await invoke(input.path, input.content);
|
|
record(result, input, actual, [["official SVG importer handles distinct entity types and transforms with native OCCT lengths", actual.kind === "result"
|
|
&& actual.result.objects.length === count && Math.abs(actual.result.objects.reduce((sum, object) => sum + object.length, 0) - expectedLength) < 1e-6]]);
|
|
}
|
|
const controller = new AbortController();
|
|
const cancelled = facade.handleStartImportSvg(facade.createStartImportSvgRequest("cancel", "/tmp/cancel.svg", input.content).payload, controller.signal, Promise.resolve(kernel));
|
|
controller.abort();
|
|
await assert.rejects(cancelled, error => error.code === "cancelled");
|
|
if (!browserCases) browserCases = await timed("browser-worker", runStartBrowserCases);
|
|
result.cases.push(...browserCases);
|
|
}
|
|
result.status = "passed";
|
|
} catch (error) { result.error = String(error.stack ?? error); failed = true; }
|
|
}
|
|
} catch (error) {
|
|
failed = true;
|
|
for (const result of results) result.error = String(error.stack ?? error);
|
|
if (!results.length) process.stderr.write(String(error.stack ?? error) + "\n");
|
|
} finally {
|
|
await timed("cleanup", () => fs.rmSync(out, { recursive: true, force: true }));
|
|
}
|
|
await writeBusinessReport({ schema: "freecad-web-business-cases/v1", results, timings });
|
|
process.exit(failed ? 1 : 0);
|