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