Files
FreeCADWasmDev/tools/start-business-cases.mjs
wangdequan 9a7c4f0335 Save FreeCAD Web workspace sources and user validation evidence
IFC native binding remains incomplete; preserve accurate test results.
2026-09-15 22:01:58 +08:00

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);