129 lines
8.2 KiB
JavaScript
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);
|