114 lines
8.0 KiB
JavaScript
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);
|
|
}
|