110 lines
6.1 KiB
JavaScript
110 lines
6.1 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 { setTimeout as yieldToWorkers } from "node:timers/promises";
|
|
import { loadWasmProbe } from "./wasm-probe-runtime.mjs";
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const capabilityId = "MeshPart:meshpart.createRecompute";
|
|
const cases = [];
|
|
const report = { schema: "freecad-web-business-cases/v1", results: [] };
|
|
const out = fs.mkdtempSync(path.join(os.tmpdir(), "freecad-meshpart-business-"));
|
|
let kernel;
|
|
let facade;
|
|
let queue;
|
|
let exitCode = 0;
|
|
let retainedDocument = false;
|
|
const documentName = "MeshPartBusinessInput";
|
|
const stage = (name) => process.stderr.write(`[MeshPart ${performance.now().toFixed(1)}ms] ${name}\n`);
|
|
function check(entry, name, actual, expected) {
|
|
const passed = typeof expected === "function" ? expected(actual) : actual === expected;
|
|
entry.assertions.push({ name, passed, actual, ...(typeof expected === "function" ? {} : { expected }) });
|
|
assert.ok(passed, `${name}${entry.actual?.error ? `: ${JSON.stringify(entry.actual.error)}` : ""}`);
|
|
}
|
|
async function execute(size, options = {}) {
|
|
const input = { ...facade.MESHPART_FIXTURE, size, documentName, ...options };
|
|
const entry = { input, assertions: [] };
|
|
cases.push(entry);
|
|
stage(`entered case ${cases.length}`);
|
|
await yieldToWorkers(0);
|
|
const response = await facade.dispatchMeshPart(queue, facade.createMeshPartRequest(`meshpart-${cases.length}`, input), kernel);
|
|
entry.actual = response;
|
|
if (response.kind === "result") retainedDocument = !response.result.resourcesReleased;
|
|
stage(`completed case ${cases.length}`);
|
|
return { entry, response };
|
|
}
|
|
function verifyMesh(entry, mesh, size, released) {
|
|
check(entry, "actual facet count", mesh.facetCount, n => Number.isInteger(n) && n >= 12);
|
|
check(entry, "actual point count", mesh.pointCount, n => Number.isInteger(n) && n >= 8);
|
|
check(entry, "two document objects after create/update", mesh.objectCount, 2);
|
|
check(entry, "document ownership", mesh.documentName, documentName);
|
|
check(entry, "document lifecycle", mesh.resourcesReleased, released);
|
|
check(entry, "finite mesh coordinates", mesh.vertices, vs => vs.every(v => v.length === 3 && v.every(Number.isFinite)));
|
|
for (let axis = 0; axis < 3; axis++) {
|
|
const coordinates = mesh.vertices.map(v => v[axis]);
|
|
check(entry, `mesh minimum axis ${axis}`, Math.min(...coordinates), n => Math.abs(n) < 1e-6);
|
|
check(entry, `mesh maximum axis ${axis}`, Math.max(...coordinates), n => Math.abs(n - size[axis]) < 1e-6);
|
|
}
|
|
const expectedArea = 2 * (size[0] * size[1] + size[1] * size[2] + size[0] * size[2]);
|
|
check(entry, "native mesh area", mesh.area, n => Math.abs(n - expectedArea) < 1e-5);
|
|
let area = 0;
|
|
for (const indices of mesh.facets) {
|
|
assert.ok(indices.length === 3 && indices.every(i => Number.isInteger(i) && i >= 0 && i < mesh.vertices.length), "valid triangle indices");
|
|
const [a, b, c] = indices.map(i => mesh.vertices[i]);
|
|
const u = b.map((v, i) => v - a[i]);
|
|
const v = c.map((value, i) => value - a[i]);
|
|
area += Math.hypot(u[1]*v[2]-u[2]*v[1], u[2]*v[0]-u[0]*v[2], u[0]*v[1]-u[1]*v[0]) / 2;
|
|
}
|
|
check(entry, "area independently measured from returned triangles", area, n => Math.abs(n - expectedArea) < 1e-5);
|
|
}
|
|
try {
|
|
const ids = JSON.parse(process.env.FREECAD_BUSINESS_IDS ?? JSON.stringify([capabilityId]));
|
|
assert.deepEqual(ids, [capabilityId], "Only the declared MeshPart business ID is supported");
|
|
stage("compile existing facade");
|
|
execFileSync(path.join(root, "web/node_modules/.bin/tsc"), ["--target", "ES2022", "--module", "ESNext", "--moduleResolution", "Bundler", "--strict", "--skipLibCheck", "--outDir", out, path.join(root, "web/src/meshpart-facade.ts")], { stdio: "pipe" });
|
|
facade = await import(pathToFileURL(path.join(out, "meshpart-facade.js")));
|
|
const { FacadeSerialQueue } = await import(pathToFileURL(path.join(out, "facade-envelope.js")));
|
|
queue = new FacadeSerialQueue();
|
|
stage("entered WASM load");
|
|
({ kernel } = await loadWasmProbe(path.join(root, "web/public/kernel"), "FreeCADWebMeshPartRuntimeProbe", { printErr: text => process.stderr.write(`${text}\n`) }));
|
|
stage("completed WASM load");
|
|
for (const size of [[2, 3, 4], [0.5, 7, 1.25]]) {
|
|
const { entry, response } = await execute(size, { keepDocument: true });
|
|
check(entry, "facade success", response.kind, "result");
|
|
verifyMesh(entry, response.result, size, false);
|
|
}
|
|
for (const size of [[0, 3, 4], [-1, 2, 3], [null, 2, 3]]) {
|
|
const { entry, response } = await execute(size, { keepDocument: true });
|
|
check(entry, "reject invalid dimensions", response.kind, "error");
|
|
check(entry, "native validation code", response.error?.code, "invalid_argument");
|
|
}
|
|
const size = [4, 1.5, 2];
|
|
const { entry, response } = await execute(size, { keepDocument: true, closeAfterRecompute: true });
|
|
check(entry, "recovery after invalid input", response.kind, "result");
|
|
verifyMesh(entry, response.result, size, true);
|
|
retainedDocument = false;
|
|
report.results.push({ capabilityId, status: "passed", cases });
|
|
} catch (error) {
|
|
exitCode = 1;
|
|
report.results.push({ capabilityId, status: "failed", cases, error: String(error.stack ?? error) });
|
|
} finally {
|
|
stage("entered cleanup");
|
|
if (retainedDocument && kernel) {
|
|
try {
|
|
const result = JSON.parse(kernel.ccall("fc_web_dispatch_json", "string", ["string"], [JSON.stringify({ cmd: "meshpart.createRecompute", args: { ...facade.MESHPART_FIXTURE, documentName, keepDocument: true, closeAfterRecompute: true } })]));
|
|
assert.ok(result.ok && result.resourcesReleased, "cleanup closes owned document");
|
|
} catch (error) {
|
|
exitCode = 1;
|
|
report.results[0].status = "failed";
|
|
report.results[0].cleanupError = String(error);
|
|
}
|
|
}
|
|
kernel?.PThread?.terminateAllThreads?.();
|
|
fs.rmSync(out, { recursive: true, force: true });
|
|
stage("completed cleanup");
|
|
process.stdout.write(`${JSON.stringify(report)}\n`, () => process.exit(exitCode));
|
|
}
|