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

129 lines
8.2 KiB
JavaScript

import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { fileURLToPath, pathToFileURL } from "node:url";
import { loadWasmProbe } from "./wasm-probe-runtime.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const out = fs.mkdtempSync(path.join(os.tmpdir(), "freecad-measure-business-"));
const id = "Measure:measure.createRecompute";
const saveId = "Measure:measure.saveReopen";
const ids = JSON.parse(process.env.FREECAD_BUSINESS_IDS ?? JSON.stringify([id, saveId]));
const results = [];
let cases = [];
let exitCode = 0;
let stage = "compile-facade";
let failure;
try {
assert.ok(Array.isArray(ids) && ids.length > 0 && ids.every(value => [id, saveId].includes(value)), "Unsupported Measure business selection");
execFileSync(path.join(root, "web/node_modules/.bin/tsc"), ["--target", "ES2022", "--module", "ESNext", "--moduleResolution", "Bundler", "--strict", "--skipLibCheck", "--outDir", out, path.join(root, "web/src/facade-envelope.ts"), path.join(root, "web/src/measure-facade.ts")], { cwd: root, stdio: "pipe" });
const facade = await import(pathToFileURL(path.join(out, "measure-facade.js")));
const { FacadeSerialQueue } = await import(pathToFileURL(path.join(out, "facade-envelope.js")));
stage = "load-wasm";
const { kernel } = await loadWasmProbe(process.env.FREECAD_MEASURE_BUSINESS_BUILD ?? path.join(root, "freecad-source/build-web/configure/bin"), "FreeCADWebMeasureKernelProbe");
const queue = new FacadeSerialQueue(); let n = 0;
for (const capabilityId of ids) {
cases = [];
const result = { capabilityId, status: "failed", cases };
results.push(result);
if (capabilityId === id) {
for (const [p1, p2] of [[[0, 0, 0], [3, 4, 0]], [[-1.5, 2, 3], [2.5, -1, 15]]]) {
stage = "create-recompute";
const payload = { ...facade.MEASURE_FIXTURE, document: `MeasureBusiness${++n}`, object: "Distance", position1: p1, position2: p2 };
const response = await facade.dispatchMeasure(queue, facade.createMeasureRequest(`measure-${n}`, payload), kernel);
assert.equal(response.kind, "result", JSON.stringify(response));
assert.ok(Math.abs(response.result.distance - Math.hypot(p2[0]-p1[0], p2[1]-p1[1], p2[2]-p1[2])) < 1e-8);
assert.equal(response.result.objectCount, 1); assert.equal(response.result.hasError, 0); assert.ok(response.result.recompute >= 0);
cases.push({ input: payload, actual: response.result, assertions: [{ name: "native distance and recompute match current points", passed: true }, { name: "document owns one measured object without recompute error", passed: true }] });
}
for (const [requestId, position1, expectedCode] of [
["invalid-shape", [0, 0], "invalid_request"],
["invalid-native-coordinate", [1e101, 0, 0], "measure_operation_failed"],
]) {
stage = requestId;
const payload = { ...facade.MEASURE_FIXTURE, document: "MeasureBusiness2", object: "Distance", position1 };
const response = await facade.dispatchMeasure(queue, facade.createMeasureRequest(requestId, payload), kernel);
assert.equal(response.kind, "error", JSON.stringify(response));
assert.equal(response.error.code, expectedCode, JSON.stringify(response));
cases.push({ input: payload, actual: response, assertions: [{ name: "invalid point returns the expected typed error", passed: true }] });
}
stage = "recover-existing-object";
const recoveryPayload = { ...facade.MEASURE_FIXTURE, document: "MeasureBusiness2", object: "Distance", position1: [2, -3, 4], position2: [-4, 5, 4] };
const recovery = await facade.dispatchMeasure(queue, facade.createMeasureRequest("recovery", recoveryPayload), kernel);
assert.equal(recovery.kind, "result", JSON.stringify(recovery));
assert.equal(recovery.result.document, recoveryPayload.document);
assert.equal(recovery.result.object, recoveryPayload.object);
assert.equal(recovery.result.objectCount, 1);
assert.equal(recovery.result.hasError, 0);
assert.ok(recovery.result.recompute >= 0);
assert.ok(Math.abs(recovery.result.distance - 10) < 1e-8);
cases.push({ input: recoveryPayload, actual: recovery.result, assertions: [{ name: "existing native object recomputes new points after rejected inputs without duplication", passed: true }] });
} else {
const document = "MeasureSaveBusiness";
const expected = new Map();
const create = async (object, position1, position2) => {
stage = `save-input-${object}`;
const payload = { ...facade.MEASURE_FIXTURE, document, object, position1, position2 };
const response = await facade.dispatchMeasure(queue, facade.createMeasureRequest(`save-create-${++n}`, payload), kernel);
assert.equal(response.kind, "result", JSON.stringify(response));
expected.set(object, { position1, position2 });
assert.equal(response.result.objectCount, expected.size);
assert.ok(Math.abs(response.result.distance - Math.hypot(...position2.map((v, i) => v - position1[i]))) < 1e-8);
cases.push({ input: payload, actual: response.result, assertions: [{ name: "current document contains the requested measured object", passed: true }] });
};
const dispatch = args => JSON.parse(String(kernel.ccall("fc_web_dispatch_json", "string", ["string"], [JSON.stringify({ cmd: "measure.saveReopen", args })])));
const reopen = file => {
stage = "save-reopen-current-document";
const input = { document, path: file };
const actual = dispatch(input);
assert.equal(actual.ok, true, JSON.stringify(actual));
assert.equal(actual.schema, "freecad-web-measure/save-reopen/v1");
assert.equal(actual.document, document);
assert.equal(actual.path, file);
assert.equal(actual.format, "FCStd");
assert.ok(actual.size > 0);
assert.equal(actual.roundTrip, true);
assert.equal(actual.hasError, 0);
assert.ok(actual.recompute >= 0);
assert.equal(actual.objectCount, expected.size);
assert.equal(actual.objects.length, expected.size);
assert.deepEqual(actual.objects.map(entry => entry.object).sort(), [...expected.keys()].sort());
for (const entry of actual.objects) {
const points = expected.get(entry.object);
assert.equal(entry.objectType, "Measure::MeasureDistanceDetached");
assert.deepEqual(entry.position1, points.position1);
assert.deepEqual(entry.position2, points.position2);
assert.ok(Math.abs(entry.distance - Math.hypot(...points.position2.map((v, i) => v - points.position1[i]))) < 1e-8);
}
cases.push({ input, actual, assertions: [{ name: "FCStd restores all current objects, names, coordinates and recomputed distances", passed: true }] });
};
await create("Diagonal", [-1.5, 2, 3], [2.5, -1, 15]);
await create("Coincident", [7, -2, 0.5], [7, -2, 0.5]);
reopen("/tmp/MeasureBusinessFirst.FCStd");
await create("Diagonal", [2, -3, 4], [-4, 5, 4]);
reopen("/tmp/MeasureBusinessEdited.FCStd");
for (const input of [
{ document, path: "/tmp/../Rejected.FCStd" },
{ document, path: "/tmp/Rejected.txt" },
{ document: "MissingMeasureBusiness", path: "/tmp/MeasureMissing.FCStd" },
]) {
stage = "save-reopen-invalid-input";
const actual = dispatch(input);
assert.equal(actual.ok, false, JSON.stringify(actual));
assert.equal(actual.code, "save_reopen_failed");
assert.ok(actual.error.length > 0);
cases.push({ input, actual, assertions: [{ name: "native persistence rejects invalid path or absent document", passed: true }] });
}
reopen("/tmp/MeasureBusinessRecovery.FCStd");
}
result.status = "passed";
}
} catch (error) { exitCode = 1; failure = `${stage}: ${String(error.stack ?? error)}`; cases.push({ stage, error: failure }); }
finally { fs.rmSync(out, { recursive: true, force: true }); }
fs.writeSync(1, JSON.stringify({ schema: "freecad-web-business-cases/v1", results: ids.map(capabilityId => {
const result = results.find(entry => entry.capabilityId === capabilityId);
return result?.status === "passed" ? result : { capabilityId, status: "failed", cases: result?.cases ?? [], error: failure };
}) }) + "\n");
process.exit(exitCode);