155 lines
9.2 KiB
JavaScript
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);
|
|
}
|