127 lines
7.9 KiB
JavaScript
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);
|