Files
FreeCADWasmDev/tools/reverseengineering-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

155 lines
9.2 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";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const id = "ReverseEngineering:reverseengineering.createRecompute";
const saveId = "ReverseEngineering:reverseengineering.saveReopen";
let cases = [];
const results = [], timings = [];
let currentResult;
const out = fs.mkdtempSync(path.join(os.tmpdir(), "freecad-reen-business-"));
let kernel, failure, stage = "setup", started = performance.now();
const next = name => {
timings.push({ stage, milliseconds: performance.now() - started });
stage = name; started = performance.now();
process.stderr.write(`ReverseEngineering stage: ${name}\n`);
};
try {
const ids = JSON.parse(process.env.FREECAD_BUSINESS_IDS ?? JSON.stringify([id, saveId]));
assert.ok(Array.isArray(ids) && ids.length > 0);
assert.equal(new Set(ids).size, ids.length);
assert.ok(ids.every(value => [id, saveId].includes(value)));
for (const capabilityId of ids) results.push({ capabilityId, status: "failed", cases: [] });
execFileSync(path.join(root, "web/node_modules/.bin/tsc"), ["--target", "ES2022", "--module", "ESNext", "--moduleResolution", "Bundler", "--strict", "--skipLibCheck", "--outDir", out, path.join(root, "web/src/reverseengineering-create-facade.ts")], { stdio: "pipe", timeout: 30000 });
const facade = await import(pathToFileURL(path.join(out, "reverseengineering-create-facade.js")));
next("load-wasm");
({ kernel } = await loadWasmProbe(path.join(root, "freecad-source/build-web/configure"), "FreeCADWebReverseEngineeringRuntimeProbe"));
next("initialize");
assert.equal(kernel.ccall("fc_web_reverseengineering_initialize", "number"), 1);
const base = [[0, 0, 0], [1, 1, 0], [2, 0, 0], [3, 1, 0]];
const payload = (document, object, points) => ({ schema: facade.REVERSEENGINEERING_CREATE_SCHEMA, module: "ReverseEngineering", action: "create/recompute", document, object, points });
const run = async (input, count, expectedLength) => {
const record = { input, assertions: [] }; cases.push(record);
const actual = await facade.handleReverseEngineeringCreate(input, new AbortController().signal, kernel);
record.actual = actual;
assert.equal(actual.objectCount, count);
assert.equal(actual.shapeValid, true);
assert.equal(actual.recomputed, true);
assert.equal(actual.documentRetained, true);
if (expectedLength !== undefined) assert.ok(Math.abs(actual.length - expectedLength) < 1e-5 * Math.max(1, expectedLength), JSON.stringify(actual));
record.assertions.push({ name: "native retained document object count, valid recompute and fitted length", passed: true });
return actual;
};
for (const result of results) {
currentResult = result;
cases = result.cases;
if (result.capabilityId === id) {
next("create-update-current-document");
const first = await run(payload("ReenBusinessA", "Curve", base), 1);
assert.ok(first.length >= 3 && first.length < 10);
const scaled = base.map(p => p.map(x => x * 2));
await run(payload("ReenBusinessA", "Curve", scaled), 1, first.length * 2);
await run(payload("ReenBusinessA", "Other", base.map(p => p.map(x => x + 7))), 2, first.length);
await run(payload("ReenBusinessB", "Curve", base), 1, first.length);
const active = await run(payload(undefined, "Other", scaled), 2, first.length * 2);
assert.equal(active.document, "ReenBusinessB");
next("invalid-input-and-recovery");
for (const change of [{ points: [] }, { points: [[0, 0], [1, 1], [2, 2]] }, { points: [[0, 0, 0], [1, null, 0], [2, 0, 0]] }, { document: "bad/name" }, { object: "" }]) {
const input = { ...payload("ReenBusinessA", "Curve", base), ...change };
const actual = JSON.parse(kernel.ccall("fc_web_dispatch_json", "string", ["string"], [JSON.stringify({ cmd: facade.REVERSEENGINEERING_CREATE_COMMAND, args: input })]));
assert.equal(actual.ok, false);
assert.equal(actual.code, "create_failed");
assert.ok(actual.error);
cases.push({ input, actual, assertions: [{ name: "native rejects invalid request", passed: true }] });
}
await run(payload("ReenBusinessA", "Curve", scaled), 2, first.length * 2);
} else {
const native = (name, type = "number", argTypes = [], args = []) => kernel.ccall(`fc_web_reverseengineering_${name}`, type, argTypes, args);
const save = (file, count, length) => {
const record = { input: { action: "save/reopen", path: file, document: "current active document", expectedObjectCount: count, expectedLength: length }, assertions: [] };
cases.push(record);
assert.equal(native("persistence_set_path", "number", ["string"], [file]), 1);
const success = native("save_reopen");
const actual = {
success, error: native("persistence_error", "string"),
path: native("persistence_path", "string"), size: native("persistence_size"),
objectCount: native("persistence_object_count"), objectType: native("persistence_object_type", "string"),
beforeLength: native("persistence_before_length"), afterLength: native("persistence_after_length"),
};
record.actual = actual;
assert.equal(success, 1, actual.error);
assert.equal(actual.path, file);
assert.ok(actual.size > 0);
assert.equal(actual.objectCount, count);
assert.equal(actual.objectType, "Part::Feature");
// Shape.Length and the persistence probe use different OCCT length evaluations.
// The create facade reports Shape.Length while persistence measures the
// saved BRep with LinearProperties; these valid evaluations differ
// slightly for fitted curves. Keep this cross-path check meaningful
// while leaving the native roundtrip invariant strict below.
const creationTolerance = 1e-2 * Math.max(1, length);
for (const measured of [actual.beforeLength, actual.afterLength]) {
assert.ok(Number.isFinite(measured) && measured > 0 && Math.abs(measured - length) < creationTolerance,
JSON.stringify({ ...actual, expectedLength: length, difference: measured - length, tolerance: creationTolerance }));
}
const roundtripTolerance = 1e-7 * Math.max(1, actual.beforeLength);
assert.ok(Math.abs(actual.afterLength - actual.beforeLength) < roundtripTolerance, JSON.stringify(actual));
record.assertions.push({ name: "persisted lengths match current create results", passed: true, expectedLength: length, tolerance: creationTolerance });
record.assertions.push({ name: "FCStd roundtrip preserves length at strict persistence tolerance", passed: true, tolerance: roundtripTolerance });
record.assertions.push({ name: "native FCStd restore preserves current document object count and fitted lengths", passed: true });
};
next("save-current-document");
const first = await run(payload("ReenSaveA", "Curve", base), 1);
save("/tmp/ReenSaveA.FCStd", 1, first.length);
const scaled = base.map(p => p.map(x => x * 3));
await run(payload(undefined, "Curve", scaled), 1, first.length * 3);
const other = await run(payload(undefined, "Other", base.map(p => p.map(x => x + 11))), 2, first.length);
assert.equal(other.document, "ReenSaveA");
save("/tmp/ReenSaveAUpdated.FCStd", 2, first.length * 4);
const second = await run(payload("ReenSaveB", "Different", base.map(p => p.map(x => x * 2))), 1, first.length * 2);
save("/tmp/ReenSaveB.FCStd", 1, second.length);
next("save-invalid-path-and-recovery");
for (const file of ["", "/tmp/ReenSaveB.txt"]) {
const record = { input: { action: "save/reopen", path: file }, assertions: [] };
cases.push(record);
const accepted = native("persistence_set_path", "number", ["string"], [file]);
const success = native("save_reopen");
record.actual = { accepted, success, error: native("persistence_error", "string") };
assert.equal(accepted, 0);
assert.equal(success, 0);
assert.match(record.actual.error, /invalid_path/);
record.assertions.push({ name: "native rejects invalid save paths", passed: true });
}
save("/tmp/ReenSaveBRecovered.FCStd", 1, second.length);
const retained = await run(payload(undefined, "Different", scaled), 1, first.length * 3);
assert.equal(retained.document, "ReenSaveB");
save("/tmp/ReenSaveBRecovered.FCStd", 1, retained.length);
}
result.status = "passed";
}
} catch (error) {
failure = String(error.stack ?? error);
if (!results.length) results.push({ capabilityId: id, status: "failed", cases });
const failedResult = currentResult ?? results[0];
failedResult.error = failure;
failedResult.cases.push({ input: { stage }, assertions: [{ name: failure, passed: false }] });
} finally {
next("cleanup");
try {
kernel?.PThread?.terminateAllThreads();
fs.rmSync(out, { recursive: true, force: true });
} catch (error) {
failure ??= String(error);
for (const result of results) { result.status = "failed"; result.error = failure; }
}
next("report");
await writeBusinessReport({ schema: "freecad-web-business-cases/v1", results, timings });
process.exit(failure ? 1 : 0);
}