140 lines
11 KiB
JavaScript
140 lines
11 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import vm from "node:vm";
|
|
import test from "node:test";
|
|
import ts from "typescript";
|
|
|
|
const modules = new Map();
|
|
function load(name) {
|
|
if (modules.has(name)) return modules.get(name);
|
|
const source = fs.readFileSync(new URL(`../src/${name.replace(/^\.\//, "").replace(/\.js$/, ".ts")}`, import.meta.url), "utf8");
|
|
const code = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText;
|
|
const module = { exports: {} };
|
|
vm.runInNewContext("(function(exports,require,module){" + code + "})", { AbortController, ArrayBuffer, Uint8Array })(module.exports, load, module);
|
|
modules.set(name, module.exports); return module.exports;
|
|
}
|
|
const facade = load("./partdesign-document-facade.js");
|
|
const input = { schema: facade.PARTDESIGN_DOCUMENT_SCHEMA, module: "PartDesign", document: "Current", action: "setSketch", body: "Body", object: "Sketch", profileType: "Rectangle", parameters: { Width: 10, Height: 8 }, position: [2, 3, 0] };
|
|
const sketch = { name: "Sketch", type: "Sketcher::SketchObject", body: "Body", profileType: "Rectangle", parameters: input.parameters, position: input.position, dof: 0, fullyConstrained: true };
|
|
const response = object => ({ ok: true, result: { document: "Current", objectCount: 1, objects: [object] } });
|
|
const call = (payload, kernel) => facade.handleDesignDocument(payload, new AbortController().signal, kernel);
|
|
|
|
test("native sketch requests preserve document ownership, dimensions and placement", async () => {
|
|
let request;
|
|
await call(input, { ccall(_name, _type, _types, args) { request = JSON.parse(args[0]); return JSON.stringify(response(sketch)); } });
|
|
assert.deepEqual(request, { cmd: "partdesign.document", args: input });
|
|
});
|
|
|
|
test("invalid profile parameters fail before calling the native module", async () => {
|
|
for (const override of [{ document: "other/doc" }, { body: "" }, { parameters: { Width: 10 } }, { parameters: { Width: 0, Height: 4 } },
|
|
{ parameters: { Width: Infinity, Height: 4 } }, { profileType: "constructor" }, { position: [1, 2] }, { position: [NaN, 0, 0] }])
|
|
await assert.rejects(call({ ...input, ...override }, { ccall() { assert.fail("invalid request reached native code"); } }));
|
|
});
|
|
|
|
test("underconstrained, wrong-body and incomplete native replies cannot pass", async () => {
|
|
for (const override of [{ body: "Other" }, { dof: 2 }, { fullyConstrained: false }, { parameters: { Width: 10 } },
|
|
{ parameters: { Width: null, Height: 8 } }, { position: [0, 0, 0] }])
|
|
await assert.rejects(call(input, { ccall: () => JSON.stringify(response({ ...sketch, ...override })) }), /does not match/);
|
|
});
|
|
|
|
test("Pad and Pocket require matching native Profile links, length and direction", async () => {
|
|
const payload = { ...input, action: "setFeature", object: "Pocket", featureType: "Pocket", profile: "Sketch", length: 3, reversed: false };
|
|
const feature = { name: "Pocket", type: "PartDesign::Pocket", body: "Body", profile: "Sketch", length: 3, reversed: false, volume: 442.3 };
|
|
await call(payload, { ccall: () => JSON.stringify(response(feature)) });
|
|
for (const override of [{ profile: "Other" }, { length: 5 }, { reversed: true }, { volume: null }])
|
|
await assert.rejects(call(payload, { ccall: () => JSON.stringify(response({ ...feature, ...override })) }), /links or dimensions/);
|
|
});
|
|
|
|
test("native failures and invalid FCStd paths are not converted into success", async () => {
|
|
await assert.rejects(call(input, { ccall: () => JSON.stringify({ ok: false, code: "partdesign_document_failed", stage: "recompute", error: "profile invalid" }) }), /recompute: profile invalid/);
|
|
await assert.rejects(call({ ...input, action: "save", path: "/other/document.FCStd" }, { ccall() { assert.fail("invalid path reached native code"); } }), /document-local/);
|
|
});
|
|
|
|
test("subtractive features require matching native types and persistent section/spine links", async () => {
|
|
for (const type of ["SubtractiveLoft", "SubtractivePipe"]) {
|
|
const links = type.endsWith("Loft") ? { sections: ["Section"], ruled: true, closed: false } : { spine: "Base", spineEdges: ["Edge1"] };
|
|
const payload = { ...input, action: "setFeature", featureType: type, object: "Result", profile: "Sketch", ...links };
|
|
const feature = { name: "Result", type: `PartDesign::${type}`, body: "Body", profile: "Sketch", reversed: false, volume: 970, ...links };
|
|
await call(payload, { ccall: () => JSON.stringify(response(feature)) });
|
|
for (const override of [{ type: feature.type.replace("Subtractive", "Additive") }, { body: "Other" }, { volume: null },
|
|
...(type.endsWith("Loft") ? [{ sections: ["Missing"] }, { ruled: false }] : [{ spine: "Other" }, { spineEdges: [] }])])
|
|
await assert.rejects(call(payload, { ccall: () => JSON.stringify(response({ ...feature, ...override })) }));
|
|
for (const override of type.endsWith("Loft") ? [{ sections: [] }, { sections: ["Section", "Section"] }, { closed: "yes" }]
|
|
: [{ spine: "" }, { spineEdges: ["bad"] }])
|
|
await assert.rejects(call({ ...payload, ...override }, { ccall() { assert.fail("invalid subtraction reached native code"); } }));
|
|
}
|
|
});
|
|
|
|
test("subtractive desktop routes cannot turn a subtraction into an additive request", async () => {
|
|
const { executePartDesignDesktop } = load("./partdesign-desktop-adapter.js");
|
|
for (const type of ["SubtractiveLoft", "SubtractivePipe"]) {
|
|
const requests = [];
|
|
const router = { request: async (...args) => { requests.push(args); return { kind: "result", result: { document: "Current", objects: [{ name: "Result" }] } }; } };
|
|
const args = { body: "Body", object: "Result", profile: "Sketch", featureType: "Loft", sections: ["Section"], spine: "Base", spineEdges: ["Edge1"] };
|
|
assert.equal((await executePartDesignDesktop(router, `PartDesign_${type}`, args, "Current")).kind, "result");
|
|
assert.equal(requests[0][2].featureType, type); assert.equal(requests[0][1], "partdesign.document");
|
|
assert.equal((await executePartDesignDesktop(router, `PartDesign_${type}`, { ...args, activeTask: true }, "Current")).kind, "error");
|
|
assert.equal(requests.length, 1);
|
|
}
|
|
});
|
|
|
|
test("rotary feature validates angle, native axis ownership result and direction", async () => {
|
|
const payload = { ...input, action: "setFeature", object: "Turn", featureType: "Revolution", profile: "Sketch", angle: 180, axis: "Y_Axis", reversed: true };
|
|
const feature = { name: "Turn", type: "PartDesign::Revolution", body: "Body", profile: "Sketch", angle: 180, axis: "Y_Axis", reversed: true, volume: 200 };
|
|
await call(payload, { ccall: () => JSON.stringify(response(feature)) });
|
|
for (const override of [{ angle: 0 }, { angle: 361 }, { axis: "" }, { axis: "other/axis" }])
|
|
await assert.rejects(call({ ...payload, ...override }, { ccall() { assert.fail("invalid rotary request reached native"); } }));
|
|
for (const override of [{ angle: 360 }, { axis: "X_Axis" }, { reversed: false }])
|
|
await assert.rejects(call(payload, { ccall: () => JSON.stringify(response({ ...feature, ...override })) }));
|
|
});
|
|
|
|
test("dress-up validates explicit edge selection and matching native feature result", async () => {
|
|
const payload = { ...input, action: "setDressUp", object: "Round", dressUpType: "Fillet", base: "Pad", size: 0.8, edges: ["Edge1", "Edge3"] };
|
|
const feature = { name: "Round", type: "PartDesign::Fillet", dressUpType: "Fillet", body: "Body", base: "Pad", size: 0.8, edges: payload.edges, volume: 190 };
|
|
await call(payload, { ccall: () => JSON.stringify(response(feature)) });
|
|
for (const override of [{ size: 0 }, { base: "Round" }, { edges: [] }, { edges: ["Edge0"] }, { edges: ["Edge1", "Edge1"] }, { dressUpType: "Other" }])
|
|
await assert.rejects(call({ ...payload, ...override }, { ccall() { assert.fail("invalid dress-up request reached native"); } }));
|
|
for (const override of [{ base: "Other" }, { edges: ["Edge1"] }, { size: 1 }])
|
|
await assert.rejects(call(payload, { ccall: () => JSON.stringify(response({ ...feature, ...override })) }));
|
|
});
|
|
|
|
test("geometry export returns isolated native bytes only for the requested object and path", async () => {
|
|
const payload = { ...input, action: "export", object: "Pad", path: "/tmp/Selected.step" };
|
|
const file = new Uint8Array([1, 2, 3, 4]);
|
|
const native = { ...response({ name: "Pad", type: "PartDesign::Pad", volume: 192 }).result, path: payload.path, size: file.length, exportedObject: "Pad" };
|
|
const kernel = result => ({ ccall: () => JSON.stringify({ ok: true, result }), FS: { readFile: path => { assert.equal(path, payload.path); return file; } } });
|
|
const actual = await call(payload, kernel(native));
|
|
assert.deepEqual(new Uint8Array(actual.bytes), file); file[0] = 9; assert.equal(new Uint8Array(actual.bytes)[0], 1);
|
|
for (const override of [{ exportedObject: "Body" }, { path: "/tmp/Other.step" }, { size: 0 }, { size: 3 }])
|
|
await assert.rejects(call(payload, kernel({ ...native, ...override })));
|
|
});
|
|
|
|
test("geometry input bytes are staged once and released on success and native failure", async () => {
|
|
const payload = { ...input, action: "importShape", object: "Imported", path: "/tmp/Selected.STP", bytes: new Uint8Array([2, 4, 8]).buffer };
|
|
for (const succeeded of [true, false]) {
|
|
const operations = [];
|
|
const kernel = {
|
|
FS: {
|
|
writeFile: (path, bytes) => { assert.equal(path, payload.path); assert.deepEqual([...bytes], [2, 4, 8]); operations.push("write"); },
|
|
unlink: path => { assert.equal(path, payload.path); operations.push("unlink"); },
|
|
},
|
|
ccall(_name, _type, _types, args) {
|
|
const native = JSON.parse(args[0]); assert.equal(native.args.bytes, undefined); assert.equal(native.args.object, "Imported"); operations.push("native");
|
|
return JSON.stringify(succeeded ? response({ name: "Imported", type: "Part::Feature", volume: 192 }) : { ok: false, error: "invalid file" });
|
|
},
|
|
};
|
|
if (succeeded) await call(payload, kernel); else await assert.rejects(call(payload, kernel), /invalid file/);
|
|
assert.deepEqual(operations, ["write", "native", "unlink"]);
|
|
}
|
|
});
|
|
|
|
test("invalid geometry requests and cancellation never write files or call native code", async () => {
|
|
const payload = { ...input, action: "importShape", object: "Imported", path: "/tmp/Selected.brep", bytes: new Uint8Array([1]).buffer };
|
|
const kernel = { ccall() { assert.fail("unexpected native call"); }, FS: { writeFile() { assert.fail("unexpected file write"); } } };
|
|
for (const override of [{ path: "/outside.step" }, { path: "/tmp/../Selected.step" }, { path: "/tmp/Selected.exe" }, { object: "../Imported" }, { bytes: new ArrayBuffer(0) }, { bytes: [1] }])
|
|
await assert.rejects(call({ ...payload, ...override }, kernel));
|
|
const controller = new AbortController();
|
|
const result = facade.handleDesignDocument(payload, controller.signal, Promise.resolve(kernel)); controller.abort();
|
|
await assert.rejects(result, /cancelled/i);
|
|
});
|