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

114 lines
8.0 KiB
JavaScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { execFileSync } from "node:child_process";
import { loadWasmProbe } from "./wasm-probe-runtime.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const capabilityId = "Material:material.createRecompute";
const persistenceId = "Material:material.saveReopen";
const selected = process.env.FREECAD_BUSINESS_IDS;
const ids = selected ? (selected.trim().startsWith("[") ? JSON.parse(selected) : selected.split(/[,\s]+/)) : [capabilityId, persistenceId];
const results = [];
let temporary;
let kernel;
let exitCode = 0;
let cases = [];
let activeId;
try {
if (ids.includes(capabilityId) || ids.includes(persistenceId)) {
temporary = fs.mkdtempSync(path.join(os.tmpdir(), "freecad-material-business-"));
execFileSync(path.join(root, "web/node_modules/.bin/tsc"), ["--target", "ES2022", "--module", "ESNext", "--moduleResolution", "Bundler", "--strict", "--skipLibCheck", "--outDir", temporary,
path.join(root, "web/src/material-facade.ts"), path.join(root, "web/src/material-persistence-facade.ts"), path.join(root, "web/src/facade-envelope.ts")], {stdio: ["ignore", "pipe", "pipe"]});
const facade = await import(pathToFileURL(path.join(temporary, "material-facade.js")));
({kernel} = await loadWasmProbe(path.join(root, "freecad-source/build-web/configure/bin"), "FreeCADWebMaterialKernelProbe"));
if (ids.includes(capabilityId)) {
activeId = capabilityId;
const base = {...facade.MATERIAL_FIXTURE, document: "MaterialBusiness", object: "Housing", uuid: "housing-steel", name: "Housing Steel", objectType: "App::DocumentObjectGroup"};
async function run(input, count, rejected = false) {
let actual;
let error;
try { actual = await facade.handleMaterialCreateRecompute(input, new AbortController().signal, kernel); }
catch (caught) { error = {code: caught.code, message: caught.message}; }
const assertions = rejected
? [{name: "Native rejects invalid material or conflicting object type", passed: error?.code === "material_operation_failed"}]
: [
{name: "Native document and object identity", passed: actual?.document === input.document && actual?.object === input.object && actual?.objectType === input.objectType},
{name: "Material property readback matches user assignment", passed: actual?.uuid === input.uuid && actual?.name === input.name && actual?.propertyType === input.propertyType},
{name: "Retained document object count", passed: actual?.objectCount === count},
{name: "Native recompute succeeds", passed: actual?.hasError === 0 && Number.isInteger(actual?.recompute) && actual.recompute >= 0}
];
cases.push({input, actual, error, assertions});
}
await run(base, 1);
const second = {...base, object: "Bracket", objectType: "App::FeaturePython", uuid: "bracket-aluminium", name: "Bracket Aluminium"};
await run(second, 2);
await run({...base, uuid: "housing-titanium", name: "Housing Titanium"}, 2);
await run({...base, uuid: ""}, 2, true);
await run({...base, objectType: "App::FeaturePython"}, 2, true);
await run({...second, name: "Updated Bracket"}, 2);
await run({...base, document: "MaterialOtherDocument"}, 1);
await run(base, 2);
const passed = cases.every(item => item.assertions.every(assertion => assertion.passed));
results.push({capabilityId, status: passed ? "passed" : "failed", cases});
if (!passed) exitCode = 1;
}
if (ids.includes(persistenceId)) {
activeId = persistenceId;
cases = [];
const persistence = await import(pathToFileURL(path.join(temporary, "material-persistence-facade.js")));
async function assign(input, count) {
const actual = await facade.handleMaterialCreateRecompute(input, new AbortController().signal, kernel);
cases.push({input, actual, assertions: [
{name: "Current document assignment readback", passed: actual.document === input.document && actual.object === input.object && actual.objectType === input.objectType && actual.propertyType === input.propertyType && actual.uuid === input.uuid && actual.name === input.name && actual.objectCount === count}
]});
}
async function save(document, file, expected, rejected = false) {
const input = {...persistence.MATERIAL_PERSISTENCE_FIXTURE, document, path: file};
let actual;
let error;
try { actual = await persistence.handleMaterialPersistence(input, new AbortController().signal, kernel); }
catch (caught) { error = {code: caught.code, message: caught.message}; }
const assertions = rejected
? [{name: "Native rejects invalid path or missing source document", passed: error?.code === "save_reopen_failed"}]
: [
{name: "Requested document saved to requested FCStd path", passed: actual?.document === document && actual?.path === file && actual?.format === ".fcstd" && actual?.size > 0},
{name: "Reopened document contains every current object and material", passed: actual?.objectCount === expected.length && actual?.objects?.length === expected.length && expected.every(item => actual.objects.some(object => object.object === item.object && object.objectType === item.objectType && object.label === item.object && object.propertyType === item.propertyType && object.uuid === item.uuid && object.name === item.name))},
{name: "Native restore and recompute succeed", passed: actual?.roundTrip === true && actual?.materialPropertyRestore === true && actual?.hasError === 0 && Number.isInteger(actual?.recompute) && actual.recompute >= 0 && actual.resourcesReleased === true}
];
cases.push({input: {...input, expectedObjects: expected}, actual, error, assertions});
}
const housing = {...facade.MATERIAL_FIXTURE, document: "MaterialSaveBusiness", object: "Housing", objectType: "App::DocumentObjectGroup", uuid: "save-housing-steel", name: "Housing Steel"};
const bracket = {...housing, object: "Bracket", objectType: "App::FeaturePython", uuid: "save-bracket-aluminium", name: "Bracket Aluminium"};
await assign(housing, 1);
await assign(bracket, 2);
await save(housing.document, "/tmp/MaterialBusinessAssembly.FCStd", [housing, bracket]);
const updated = {...housing, uuid: "save-housing-titanium", name: "Updated Housing Titanium & \"Alloy\" <A>\n\u949b"};
await assign(updated, 2);
await save(housing.document, "/tmp/MaterialBusinessAssembly.FCStd", [updated, bracket]);
const other = {...bracket, document: "MaterialSaveOther", uuid: "other-bracket-copper", name: "Other Copper"};
await assign(other, 1);
await save(other.document, "/tmp/MaterialBusinessOther.FCStd", [other]);
await save(housing.document, "/tmp/MaterialBusinessInvalid.txt", [], true);
await save(housing.document, "/tmp/../MaterialBusinessInvalid.FCStd", [], true);
await save("MaterialSaveMissing", "/tmp/MaterialBusinessMissing.FCStd", [], true);
await save(housing.document, "/tmp/MaterialBusinessRecovery.FCStd", [updated, bracket]);
const passed = cases.every(item => item.assertions.every(assertion => assertion.passed));
results.push({capabilityId: persistenceId, status: passed ? "passed" : "failed", cases});
if (!passed) exitCode = 1;
}
}
} catch (error) {
exitCode = 1;
cases.push({input: {runtime: "FreeCADWebMaterialKernelProbe"}, assertions: [{name: "Business execution completes", passed: false}], error: String(error.stack || error)});
for (const id of [capabilityId, persistenceId].filter(id => ids.includes(id) && !results.some(result => result.capabilityId === id))) {
results.push({capabilityId: id, status: "failed", cases: activeId === id ? cases : [cases[cases.length - 1]]});
}
} finally {
kernel?.PThread?.terminateAllThreads?.();
if (temporary) fs.rmSync(temporary, {recursive: true, force: true});
fs.writeSync(1, JSON.stringify({schema: "freecad-web-business-cases/v1", results}) + "\n");
process.exit(exitCode);
}