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

130 lines
7.9 KiB
JavaScript

import { fileURLToPath } from "node:url";
import { loadWasmProbe } from "./wasm-probe-runtime.mjs";
import { writeBusinessReport } from "./business-report.mjs";
const capabilityId = "Robot:robot.createRecompute";
const saveId = "Robot:robot.saveReopen";
let cases = [];
const results = [];
let kernel, exitCode = 0;
const documents = ["RobotBusinessPrimary", "RobotBusinessSecondary", "RobotSavedFirst", "RobotSavedSecond"];
const paths = ["/tmp/RobotBusinessFirst.FCStd", "/tmp/RobotBusinessSecond.FCStd"];
function run(args, check, cmd = "robot.createRecompute") {
const input = {cmd, args};
const entry = {input, assertions: []};
cases.push(entry);
try {
entry.actual = JSON.parse(String(kernel.ccall("fc_web_dispatch_json", "string", ["string"], [JSON.stringify(input)])));
entry.assertions = Object.entries(check(entry.actual)).map(([name, passed]) => ({name, passed: Boolean(passed)}));
} catch (error) {
entry.error = String(error);
entry.assertions.push({name: "native dispatch returns valid business result", passed: false});
}
}
function robot(documentName, objectName, axis1, axis2, count, operation = "upsert") {
run({documentName, objectName, axis1, axis2, operation}, actual => ({
"native Robot recompute succeeds": actual.ok === true && actual.recomputed === true && actual.objectType === "Robot::RobotObject",
"current document and object retained": actual.documentRetained === true && actual.documentName === documentName && actual.objectName === objectName && actual.objectCount === count,
"native properties and Robot model use both requested angles": [actual.axis1, actual.robotAxis1].every(v => Number.isFinite(v) && Math.abs(v-axis1) < 0.0001)
&& [actual.axis2, actual.robotAxis2].every(v => Number.isFinite(v) && Math.abs(v-axis2) < 0.0001),
}));
}
try {
const ids = JSON.parse(process.env.FREECAD_BUSINESS_IDS ?? JSON.stringify([capabilityId, saveId]));
if (!Array.isArray(ids)) throw new Error("FREECAD_BUSINESS_IDS must be an array");
for (const id of new Set(ids)) {
if (![capabilityId, saveId].includes(id)) { results.push({capabilityId: id, status: "not_implemented", cases: []}); exitCode = 1; }
}
if (ids.some(id => [capabilityId, saveId].includes(id)))
({kernel} = await loadWasmProbe(fileURLToPath(new URL("../freecad-source/build-web/configure/bin", import.meta.url)), "FreeCADWebRobotRuntimeProbe"));
if (ids.includes(capabilityId)) {
results.push({capabilityId, status: "failed", cases});
const [a,b] = documents;
robot(a, "Arm", 15, -10, 1);
robot(a, "Support", -45.5, 80.25, 2);
robot(a, "Arm", 30.25, -25.5, 2);
robot(a, "Support", -45.5, 80.25, 2, "read");
robot(b, "Arm", 0, 90, 1);
robot(a, "Arm", 30.25, -25.5, 2, "read");
for (const patch of [{axis1: null}, {axis2: "bad"}, {axis1: 361}, {axis2: -361}, {objectName: ""}, {documentName: "bad\u0000name"}, {operation: null}, {operation: false}, {operation: 1}, {operation: {}}, {operation: []}, {operation: ""}, {operation: "invalid"}]) {
run({documentName: a, objectName: "Arm", axis1: 5, axis2: 6, ...patch}, actual => ({
"invalid native input rejected": actual.ok === false && actual.code === "robot_create_failed" && typeof actual.error === "string" && actual.error.length > 0,
}));
robot(a, "Arm", 30.25, -25.5, 2, "read");
}
run({documentName: a, objectName: "Missing", operation: "read"}, actual => ({"missing object rejected": actual.ok === false && actual.code === "robot_create_failed"}));
robot(a, "Arm", -120, 60, 2);
robot(b, "Arm", 0, 90, 1, "read");
}
if (ids.includes(saveId)) {
cases = [];
results.push({capabilityId: saveId, status: "failed", cases});
const [a, b, first, second] = documents;
for (const documentName of [a, b]) run({documentName, operation: "close"}, actual => ({"setup cleanup": actual.ok === true && actual.closed === true}));
robot(a, "Arm", 23.5, -76.25, 1);
robot(a, "Support", -110, 42, 2);
robot(b, "Arm", 81, 12, 1);
const save = (target, path) => run({documentName: a, reopenedDocumentName: target, path}, actual => ({
"native current document saved and restored": actual.ok === true && actual.recomputed === true && actual.size > 0,
"requested document and complete object count": actual.documentName === a && actual.reopenedDocumentName === target && actual.objectCount === 2 && actual.path === path,
}), "robot.saveReopen");
save(first, paths[0]);
robot(first, "Arm", 23.5, -76.25, 2, "read");
robot(first, "Support", -110, 42, 2, "read");
robot(a, "Arm", -34, 127.5, 2);
robot(first, "Arm", 23.5, -76.25, 2, "read");
for (const patch of [{path: "bad.txt"}, {path: "/tmp/missing/file.FCStd"}, {documentName: "MissingRobotDocument"}, {reopenedDocumentName: a}, {reopenedDocumentName: first}]) {
run({documentName: a, reopenedDocumentName: second, path: paths[1], ...patch}, actual => ({
"invalid save rejected": actual.ok === false && actual.code === "robot_save_failed" && typeof actual.error === "string" && actual.error.length > 0,
}), "robot.saveReopen");
robot(a, "Arm", -34, 127.5, 2, "read");
}
save(second, paths[1]);
run({documentName: a, operation: "close"}, actual => ({"source closed before independent read": actual.ok === true && actual.closed === true}));
robot(second, "Arm", -34, 127.5, 2, "read");
robot(second, "Support", -110, 42, 2, "read");
robot(b, "Arm", 81, 12, 1, "read");
}
} catch (error) {
exitCode = 1;
cases.push({input: {operation: "execute"}, error: String(error), assertions: [{name: "business execution completes", passed: false}]});
if (!results.length) results.push({capabilityId: saveId, status: "failed", cases});
} finally {
if (kernel) for (const documentName of documents) run({documentName, operation: "close"}, actual => ({"native document cleanup completes": actual.ok === true && actual.closed === true}));
if (kernel) for (const path of paths) {
try {
// WasmFS exposes file operations, but not the legacy analyzePath helper.
const removeFile = kernel.FS?.unlink ?? kernel.FS?.unlinkSync ?? kernel.FS?.rm ?? kernel.FS?.rmSync
?? kernel.FS_unlink;
if (typeof removeFile !== "function") throw new Error("Wasm filesystem does not expose unlink");
try { removeFile.call(kernel.FS, path); } catch (error) {
if (error?.code !== "ENOENT" && error?.errno !== 44) throw error;
}
let missing = false;
try {
// WasmFS builds may omit readFile; a second unlink is the native
// existence check and must report ENOENT after cleanup.
removeFile.call(kernel.FS, path);
} catch (error) {
if (error?.code !== "ENOENT" && error?.errno !== 44) throw error;
missing = true;
}
cases.push({input: {operation: "cleanupFile", path}, assertions: [{name: "saved file released", passed: missing}]});
} catch (error) {
cases.push({input: {operation: "cleanupFile", path}, error: String(error), assertions: [{name: "saved file released", passed: false}]});
}
}
try { kernel?.PThread?.terminateAllThreads?.(); } catch (error) { exitCode = 1; cases.push({input: {operation: "terminateThreads"}, error: String(error), assertions: [{name: "runtime cleanup", passed: false}]}); }
for (const result of results) if (result.status !== "not_implemented") {
result.status = exitCode === 0 && result.cases.length > 0 && result.cases.every(c => c.assertions.length > 0 && c.assertions.every(a => a.passed)) ? "passed" : "failed";
if (result.status !== "passed") {
exitCode = 1;
result.error = result.cases.filter(c => c.assertions.some(a => !a.passed))
.map(c => `${JSON.stringify(c.input)}: ${c.error || c.actual?.error || c.assertions.filter(a => !a.passed).map(a => a.name).join(", ")}`)
.join("\n") || "Business execution or runtime cleanup failed";
}
}
await writeBusinessReport({schema: "freecad-web-business-cases/v1", results});
}
process.exit(exitCode);