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

127 lines
7.9 KiB
JavaScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import ts from "../web/node_modules/typescript/lib/typescript.js";
import { loadWasmProbe } from "./wasm-probe-runtime.mjs";
const root = fileURLToPath(new URL("../", import.meta.url));
const ids = ["Surface:surface.createRecompute", "Surface:surface.objectContractBatch", "Surface:surface.saveReopen"];
const selected = JSON.parse(process.env.FREECAD_BUSINESS_IDS ?? JSON.stringify(ids));
if (!Array.isArray(selected) || selected.some(id => !ids.includes(id))) {
throw new Error("Unsupported Surface business IDs");
}
const results = [];
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "surface-business-"));
let kernel;
async function stage(name, action) {
const started = performance.now();
process.stderr.write(`${name}: entered\n`);
try { return await action(); }
finally { process.stderr.write(`${name}: completed in ${Math.round(performance.now() - started)}ms\n`); }
}
async function runCase(result, input, action) {
const item = { input, assertions: [] };
result.cases.push(item);
const check = (name, passed) => item.assertions.push({ name, passed: passed === true });
try { item.output = await action(check); }
catch (error) {
item.error = String(error.stack ?? error);
check("request completes successfully", false);
}
}
try {
for (const name of ["facade-envelope", "surface-facade", "surface-object-contract-facade", "surface-save-facade"]) {
const source = await fs.readFile(path.join(root, "web/src", `${name}.ts`), "utf8");
const compiled = ts.transpileModule(source, {
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ES2022 },
});
await fs.writeFile(path.join(temporary, `${name}.js`), compiled.outputText);
}
await fs.writeFile(path.join(temporary, "package.json"), '{"type":"module"}\n');
const surface = await import(pathToFileURL(path.join(temporary, "surface-facade.js")));
const contract = await import(pathToFileURL(path.join(temporary, "surface-object-contract-facade.js")));
const save = await import(pathToFileURL(path.join(temporary, "surface-save-facade.js")));
({ kernel } = await stage("load", () => loadWasmProbe(
process.env.FREECAD_SURFACE_BUSINESS_BUILD ?? path.join(root, "freecad-source/build-web/configure/bin"),
"FreeCADWebSurfaceRuntimeProbe", { printErr: text => process.stderr.write(`${text}\n`) },
)));
for (const capabilityId of selected) {
const result = { capabilityId, status: "failed", cases: [] };
results.push(result);
await stage(capabilityId, async () => {
if (capabilityId === ids[0]) {
// A changed operand must reach native code; fixture rejection is a business failure.
for (const second of [surface.SURFACE_FIXTURE.second, { length: 2, width: 3, height: 4 }]) {
const input = { ...surface.SURFACE_FIXTURE, second };
await runCase(result, input, async check => {
const output = await surface.handleSurface(input, new AbortController().signal, kernel);
check("native Surface::Cut created", output.objectType === "Surface::Cut");
check("three document objects recomputed", output.objectCount === 3 && output.recomputeCount === 3);
check("native document released", output.resourcesReleased === true);
return output;
});
}
} else if (capabilityId === ids[2]) {
// The legacy probe is a baseline, not evidence of current-document persistence.
for (const savePath of [save.SURFACE_SAVE_FIXTURE.path, "/tmp/SurfaceBusinessAlternate.FCStd"]) {
const input = { ...save.SURFACE_SAVE_FIXTURE, path: savePath };
await runCase(result, input, async check => {
const output = await save.handleSurfaceSave(input, new AbortController().signal, kernel);
check("saved path matches requested path", output.path === input.path);
check("native FCStd has nonzero size", Number.isInteger(output.size) && output.size > 0);
check("native reopened Surface Cut", output.objectType === "Surface::Cut" && output.objectCount === 3);
check("native round trip completed", output.roundTrip === true);
check("native documents released", output.resourcesReleased === true);
return output;
});
}
for (const kind of ["invalid-path", "cancelled"]) {
const input = { ...save.SURFACE_SAVE_FIXTURE, path: kind === "invalid-path" ? "" : save.SURFACE_SAVE_FIXTURE.path };
await runCase(result, { payload: input, aborted: kind === "cancelled" }, async check => {
const controller = new AbortController();
if (kind === "cancelled") controller.abort();
let error;
try { await save.handleSurfaceSave(input, controller.signal, kernel); }
catch (caught) { error = caught; }
check("request rejected with expected code", error?.code === (kind === "cancelled" ? "cancelled" : "invalid_path"));
return { code: error?.code, message: error?.message };
});
}
} else {
const record = { apiId: "obj:Surface:Surface::Sections", name: "Surface::Sections",
family: "view-provider", sourcePath: "src/Mod/Surface/App/FeatureSections.h" };
for (const records of [[record], [record, { ...record, apiId: "obj:Surface:Unknown" }]]) {
const input = contract.createSurfaceObjectContractRequest("surface-business", records).payload;
await runCase(result, input, async check => {
const output = await contract.handleSurfaceObjectContract(input, new AbortController().signal, kernel);
const actual = output.records[0];
check("native record count matches request", output.records.length === records.length && output.count === records.length);
check("Sections contract executed", actual.apiId === record.apiId && actual.status === "exposed" && actual.code === "native_object_contract");
check("desktop object and view provider match", actual.objectType === "Surface::Sections" && actual.viewProvider === "SurfaceGui::ViewProviderSections");
check("native NSections property exists", actual.hasNSections === true);
check("native recompute executed", Number.isInteger(actual.recomputeCount) && actual.recomputeCount > 0);
check("documents released", output.resourcesReleased === true && output.records.every(entry => entry.resourcesReleased === true));
if (records.length > 1) check("unknown object is not exposed", output.records[1].status === "not_implemented" && output.records[1].code === "unmatched_source_record");
return output;
});
}
}
});
result.status = result.cases.every(item => item.assertions.length && item.assertions.every(check => check.passed)) ? "passed" : "failed";
if (result.status === "failed") result.error = result.cases.filter(item => item.assertions.some(check => !check.passed))
.map(item => item.error ?? item.assertions.filter(check => !check.passed).map(check => check.name).join(", ")).join("\n");
}
} catch (error) {
for (const capabilityId of selected.filter(id => !results.some(result => result.capabilityId === id))) {
results.push({ capabilityId, status: "failed", error: String(error.stack ?? error),
cases: [{ input: { capabilityId }, assertions: [{ name: "runtime and facade load", passed: false }] }] });
}
} finally {
await stage("cleanup", () => fs.rm(temporary, { recursive: true, force: true }));
}
const report = { schema: "freecad-web-business-cases/v1", results };
await new Promise((resolve, reject) => process.stdout.write(`${JSON.stringify(report)}\n`, error => error ? reject(error) : resolve()));
// The standalone process owns the Emscripten pthread pool.
process.exit(results.length === selected.length && results.every(result => result.status === "passed") ? 0 : 1);