380 lines
23 KiB
JavaScript
380 lines
23 KiB
JavaScript
import { loadWasmProbe } from './wasm-probe-runtime.mjs';
|
|
import { readFileSync } from 'node:fs';
|
|
import { createRequire } from 'node:module';
|
|
import { runInNewContext } from 'node:vm';
|
|
import { runProfileCases, profileIds } from './partdesign-profile-business-cases.mjs';
|
|
import { runFinishingCases, finishingCases } from './partdesign-finishing-business-cases.mjs';
|
|
import { runAdditiveCases, additiveIds } from './partdesign-additive-business-cases.mjs';
|
|
import { runSubtractiveCases, subtractiveIds } from './partdesign-subtractive-business-cases.mjs';
|
|
|
|
function loadBusinessWorker(kernel) {
|
|
const require = createRequire(new URL('../web/package.json', import.meta.url));
|
|
const ts = require('typescript');
|
|
const modules = new Map();
|
|
const messages = [];
|
|
const scope = { postMessage: message => messages.push(message) };
|
|
const load = name => {
|
|
if (modules.has(name)) return modules.get(name);
|
|
const exports = {};
|
|
modules.set(name, exports);
|
|
let source = readFileSync(new URL(`../web/src/${name}.ts`, import.meta.url), 'utf8');
|
|
// Inject the public WASM instance only at the loader boundary; execute the real message handler.
|
|
if (name === 'partdesign.worker') source = source.replace('let kernel = createKernel();', 'let kernel = Promise.resolve(injectedKernel);');
|
|
const code = ts.transpileModule(source, { compilerOptions: {
|
|
target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS,
|
|
} }).outputText;
|
|
runInNewContext(code, { exports, require: path => load(path.replace(/^\.\//, '').replace(/\.js$/, '')),
|
|
self: scope, injectedKernel: kernel, AbortController, setTimeout, clearTimeout });
|
|
return exports;
|
|
};
|
|
load('partdesign.worker');
|
|
return {
|
|
create: load('partdesign-facade').createPartDesignRequest,
|
|
request: load('facade-envelope').createRequest,
|
|
desktop: load('partdesign-desktop-adapter').executePartDesignDesktop,
|
|
async send(request) {
|
|
await scope.onmessage({ data: request });
|
|
const reply = messages.find(message => message.requestId === request.requestId);
|
|
if (!reply) throw new Error(`Worker did not reply to ${request.requestId}`);
|
|
return reply;
|
|
},
|
|
};
|
|
}
|
|
|
|
async function loadSaveFacade() {
|
|
const require = createRequire(new URL('../web/package.json', import.meta.url));
|
|
const ts = require('typescript');
|
|
const compile = name => ts.transpileModule(readFileSync(new URL(`../web/src/${name}.ts`, import.meta.url), 'utf8'), {
|
|
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext },
|
|
}).outputText;
|
|
const url = source => `data:text/javascript;base64,${Buffer.from(source).toString('base64')}`;
|
|
const envelope = url(compile('facade-envelope'));
|
|
return import(url(compile('partdesign-save-facade').replace('./facade-envelope.js', envelope)));
|
|
}
|
|
|
|
const capabilityId = 'PartDesign:partdesign.createRecompute';
|
|
const selection = process.env.FREECAD_BUSINESS_IDS;
|
|
const selectedIds = !selection ? null : (selection.trim().startsWith('[')
|
|
? JSON.parse(selection) : selection.split(/[\s,]+/));
|
|
const selected = !selectedIds || selectedIds.includes(capabilityId);
|
|
const results = [];
|
|
let kernel;
|
|
let exitCode = 0;
|
|
if (selected) {
|
|
const cases = [];
|
|
const handles = new Set();
|
|
const dispatch = args => JSON.parse(String(kernel.ccall('fc_web_dispatch_json', 'string',
|
|
['string'], [JSON.stringify({ cmd: 'partdesign.createRecompute', args })])));
|
|
const record = (input, actual, checks) => {
|
|
const assertions = Object.entries(checks).map(([name, passed]) => ({ name, passed: Boolean(passed) }));
|
|
cases.push({ input: { cmd: 'partdesign.createRecompute', args: input }, actual, assertions });
|
|
if (assertions.some(check => !check.passed)) exitCode = 1;
|
|
};
|
|
try {
|
|
({ kernel } = await loadWasmProbe('web/public/kernel', 'FreeCADWebPartDesignRuntimeProbe'));
|
|
const documentHandle = `BusinessPD${process.pid}`;
|
|
handles.add(documentHandle);
|
|
for (const [objectHandle, dimensions, expectedCount] of [
|
|
['BoxA', [2, 3, 4], 1], ['BoxA', [5, 2, 3], 1],
|
|
['BoxB', [1.5, 4, 2], 2], ['BoxA', [3, 4, 5], 2],
|
|
]) {
|
|
const input = { documentHandle, objectHandle, dimensions, createDocument: cases.length === 0 };
|
|
const actual = dispatch(input);
|
|
record(input, actual, {
|
|
'native operation succeeded': actual.ok === true,
|
|
'shape is valid': actual.valid === true,
|
|
'volume follows supplied dimensions': Math.abs(actual.volume - dimensions.reduce((a, b) => a * b, 1)) < 1e-8,
|
|
'native properties match dimensions': JSON.stringify(actual.dimensions) === JSON.stringify(dimensions),
|
|
'recompute preserves current document and other objects': actual.documentHandle === documentHandle && actual.documentRetained === true && actual.objectCount === expectedCount,
|
|
'requested object was recomputed': actual.objectHandle === objectHandle,
|
|
});
|
|
}
|
|
for (const dimensions of [[0, 3, 4], [-1, 2, 3], [2, 3], ['2', 3, 4], [null, 3, 4]]) {
|
|
const input = { documentHandle, objectHandle: 'BoxA', dimensions };
|
|
const actual = dispatch(input);
|
|
record(input, actual, { 'native rejects invalid dimensions': actual.ok === false && typeof actual.error === 'string' && actual.error.length > 0 });
|
|
}
|
|
const missing = { documentHandle: `${documentHandle}Missing`, dimensions: [2, 3, 4] };
|
|
const missingResult = dispatch(missing);
|
|
record(missing, missingResult, { 'missing current document is rejected': missingResult.ok === false && typeof missingResult.error === 'string' });
|
|
const recovery = { documentHandle, objectHandle: 'BoxB', dimensions: [2, 2, 2] };
|
|
const actual = dispatch(recovery);
|
|
record(recovery, actual, {
|
|
'same document remains usable after errors': actual.ok === true && actual.valid === true && actual.objectCount === 2 && actual.documentHandle === documentHandle,
|
|
'recovery produces computed geometry': Math.abs(actual.volume - 8) < 1e-8,
|
|
});
|
|
} catch (error) {
|
|
exitCode = 1;
|
|
cases.push({ input: { capabilityId }, assertions: [{ name: 'WASM business execution completed', passed: false }], error: String(error.stack || error) });
|
|
} finally {
|
|
if (kernel) for (const documentHandle of handles) {
|
|
const input = { documentHandle, operation: 'close' };
|
|
try {
|
|
const actual = dispatch(input);
|
|
record(input, actual, { 'test document released': actual.ok === true && actual.closed === true });
|
|
} catch (error) {
|
|
exitCode = 1;
|
|
cases.push({ input, assertions: [{ name: 'test document released', passed: false }], error: String(error) });
|
|
}
|
|
}
|
|
}
|
|
results.push({ capabilityId, status: exitCode ? 'failed' : 'passed', cases });
|
|
}
|
|
const saveId = 'PartDesign:partdesign.saveReopen';
|
|
if (!selectedIds || selectedIds.includes(saveId)) {
|
|
const cases = [];
|
|
const handles = new Set();
|
|
const paths = new Set();
|
|
const call = (cmd, args, checks, reply) => {
|
|
const actual = reply ?? JSON.parse(String(kernel.ccall('fc_web_dispatch_json', 'string',
|
|
['string'], [JSON.stringify({ cmd, args })])));
|
|
const assertions = Object.entries(checks(actual)).map(([name, passed]) => ({ name, passed: Boolean(passed) }));
|
|
cases.push({ input: { cmd, args }, actual, assertions });
|
|
if (assertions.some(check => !check.passed)) exitCode = 1;
|
|
return actual;
|
|
};
|
|
const create = (documentHandle, objectHandle, dimensions, createDocument, count) => {
|
|
const actual = call('partdesign.createRecompute', { documentHandle, objectHandle, dimensions, createDocument }, actual => ({
|
|
'current document geometry computed': actual.ok === true && actual.valid === true && actual.documentHandle === documentHandle,
|
|
'input dimensions and object count preserved': JSON.stringify(actual.dimensions) === JSON.stringify(dimensions) && actual.objectCount === count,
|
|
'volume computed from input': Math.abs(actual.volume - dimensions.reduce((a, b) => a * b, 1)) < 1e-8,
|
|
}));
|
|
if (actual.ok) handles.add(documentHandle);
|
|
};
|
|
let facade;
|
|
const save = async (documentHandle, reopenedDocumentHandle, path, expected) => {
|
|
paths.add(path);
|
|
const matches = objects => Array.isArray(objects) && objects.length === Object.keys(expected).length &&
|
|
new Set(objects.map(item => item.name)).size === objects.length && objects.every(item =>
|
|
Object.hasOwn(expected, item.name) && item.type === 'PartDesign::AdditiveBox' && item.valid === true &&
|
|
Math.abs(item.volume - expected[item.name]) < 1e-8);
|
|
const payload = { schema: facade.PARTDESIGN_SAVE_SCHEMA, module: 'PartDesign', action: 'save/reopen', documentHandle, reopenedDocumentHandle, path };
|
|
const reply = await facade.handlePartDesignSave(payload, new AbortController().signal, kernel);
|
|
const actual = call('partdesign.saveReopen', payload, actual => ({
|
|
'native FCStd round trip succeeded': actual.ok === true && actual.roundTrip === true && actual.schema === 'freecad-web-partdesign/save-reopen/v1',
|
|
'requested documents and file used': actual.documentHandle === documentHandle && actual.reopenedDocumentHandle === reopenedDocumentHandle && actual.path === path,
|
|
'source retained and file written': actual.documentRetained === true && actual.size > 0,
|
|
'all current objects saved': matches(actual.before),
|
|
'all reopened shapes match input geometry': matches(actual.objects) && actual.objectCount === Object.keys(expected).length,
|
|
'names types and labels survive persistence': Array.isArray(actual.before) && Array.isArray(actual.objects) &&
|
|
actual.before.every(before => actual.objects.some(after => after.name === before.name && after.type === before.type && after.label === before.label)),
|
|
}), reply);
|
|
if (actual.ok) handles.add(actual.reopenedDocumentHandle);
|
|
};
|
|
try {
|
|
facade = await loadSaveFacade();
|
|
if (!kernel) ({ kernel } = await loadWasmProbe('web/public/kernel', 'FreeCADWebPartDesignRuntimeProbe'));
|
|
const worker = loadBusinessWorker(kernel);
|
|
const workerDoc = `BusinessPDWorker${process.pid}`;
|
|
const workerCopy = `${workerDoc}Copy`;
|
|
handles.add(workerDoc);
|
|
handles.add(workerCopy);
|
|
const workerPath = `/tmp/${workerDoc}.FCStd`;
|
|
paths.add(workerPath);
|
|
const registry = readFileSync(new URL('../web/src/module-registry.generated.ts', import.meta.url), 'utf8');
|
|
const routeMatches = /case "PartDesign:partdesign.saveReopen": return "partdesign.worker.ts";/.test(registry);
|
|
const requests = [
|
|
worker.create('worker-create', { schema: 'freecad-web-partdesign/create-recompute/v1', module: 'PartDesign',
|
|
action: 'create/recompute', baseType: 'Part::Box', featureType: 'PartDesign::AdditiveBox',
|
|
documentHandle: workerDoc, objectHandle: 'WorkerBox', dimensions: [3, 5, 7], createDocument: true }),
|
|
worker.request('worker-save', 'partdesign.saveReopen', { schema: facade.PARTDESIGN_SAVE_SCHEMA,
|
|
module: 'PartDesign', action: 'save/reopen', documentHandle: workerDoc, reopenedDocumentHandle: workerCopy, path: workerPath }),
|
|
worker.create('worker-edit-reopened', { schema: 'freecad-web-partdesign/create-recompute/v1', module: 'PartDesign',
|
|
action: 'create/recompute', baseType: 'Part::Box', featureType: 'PartDesign::AdditiveBox',
|
|
documentHandle: workerCopy, objectHandle: 'WorkerBox', dimensions: [2, 5, 7] }),
|
|
];
|
|
for (const request of requests) {
|
|
const actual = await worker.send(request);
|
|
const result = actual.result;
|
|
const saved = request.requestId === 'worker-save';
|
|
const passed = routeMatches && (saved
|
|
? result?.roundTrip === true && result.objects?.length === 1 && Math.abs(result.objects[0].volume - 105) < 1e-8
|
|
: result?.valid === true && Math.abs(result.volume - (request.requestId === 'worker-create' ? 105 : 70)) < 1e-8);
|
|
if (!passed) exitCode = 1;
|
|
cases.push({ input: request, actual, assertions: [
|
|
{ name: 'save route uses the current-document Worker', passed: routeMatches },
|
|
{ name: 'real Worker preserves native document across create save and continued editing', passed },
|
|
] });
|
|
}
|
|
const doc = `BusinessPDSave${process.pid}`;
|
|
const first = `${doc}First`, second = `${doc}Second`, third = `${doc}Third`;
|
|
create(doc, 'BoxA', [2, 3, 4], true, 1);
|
|
create(doc, 'BoxB', [1.5, 4, 2], false, 2);
|
|
await save(doc, first, `/tmp/${doc}.FCStd`, { BoxA: 24, BoxB: 12 });
|
|
create(doc, 'BoxA', [5, 2, 3], false, 2);
|
|
await save(doc, second, `/tmp/${doc}.FCStd`, { BoxA: 30, BoxB: 12 });
|
|
// Saving the first reopened document proves it remains independent of later source edits.
|
|
await save(first, third, `/tmp/${first}.FCStd`, { BoxA: 24, BoxB: 12 });
|
|
create(second, 'BoxB', [3, 4, 5], false, 2);
|
|
for (const args of [
|
|
{ documentHandle: `${doc}Missing`, reopenedDocumentHandle: `${doc}Rejected`, path: `/tmp/${doc}.FCStd` },
|
|
{ documentHandle: doc, reopenedDocumentHandle: doc, path: `/tmp/${doc}.FCStd` },
|
|
{ documentHandle: doc, reopenedDocumentHandle: first, path: `/tmp/${doc}.FCStd` },
|
|
...['/outside.FCStd', '/tmp/../outside.FCStd', `/tmp/${doc}.txt`].map(path =>
|
|
({ documentHandle: doc, reopenedDocumentHandle: `${doc}Rejected`, path })),
|
|
]) {
|
|
call('partdesign.saveReopen', args, actual => ({
|
|
'invalid save request rejected': actual.ok === false && typeof actual.error === 'string' && actual.error.length > 0,
|
|
}));
|
|
let rejected = false;
|
|
let errorMessage;
|
|
const payload = { schema: facade.PARTDESIGN_SAVE_SCHEMA, module: 'PartDesign', action: 'save/reopen', ...args };
|
|
try {
|
|
await facade.handlePartDesignSave(payload, new AbortController().signal, kernel);
|
|
} catch (error) {
|
|
rejected = true;
|
|
errorMessage = error.message;
|
|
}
|
|
if (!rejected) exitCode = 1;
|
|
cases.push({ input: { cmd: 'partdesign.saveReopen', args: payload }, errorMessage,
|
|
assertions: [{ name: 'facade rejects invalid current-document save', passed: rejected }] });
|
|
}
|
|
await save(second, `${doc}Recovery`, `/tmp/${second}.FCStd`, { BoxA: 30, BoxB: 60 });
|
|
} catch (error) {
|
|
exitCode = 1;
|
|
cases.push({ input: { capabilityId: saveId }, assertions: [{ name: 'WASM save/reopen execution completed', passed: false }], error: String(error.stack || error) });
|
|
} finally {
|
|
if (kernel) {
|
|
for (const documentHandle of handles) {
|
|
try {
|
|
call('partdesign.createRecompute', { documentHandle, operation: 'close' }, actual => ({
|
|
'source or reopened document released': actual.ok === true && actual.closed === true,
|
|
}));
|
|
} catch (error) {
|
|
exitCode = 1;
|
|
cases.push({ input: { documentHandle, operation: 'close' }, assertions: [{ name: 'document cleanup completed', passed: false }], error: String(error) });
|
|
}
|
|
}
|
|
for (const path of paths) {
|
|
try {
|
|
try {
|
|
kernel.FS_unlink(path);
|
|
} catch (error) {
|
|
if (error.code !== 'ENOENT') throw error;
|
|
}
|
|
let removed = false;
|
|
try {
|
|
kernel.FS_readFile(path);
|
|
} catch (error) {
|
|
if (error.code !== 'ENOENT') throw error;
|
|
removed = true;
|
|
}
|
|
if (!removed) exitCode = 1;
|
|
cases.push({ input: { operation: 'unlink', path }, assertions: [{ name: 'test FCStd removed', passed: removed }] });
|
|
} catch (error) {
|
|
exitCode = 1;
|
|
cases.push({ input: { operation: 'unlink', path }, assertions: [{ name: 'test FCStd removed', passed: false }], error: String(error) });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
results.push({ capabilityId: saveId, status: cases.some(item => item.assertions.some(check => !check.passed)) ? 'failed' : 'passed', cases });
|
|
}
|
|
const holeId = 'PartDesign:desktop:PartDesign_Hole';
|
|
if (!selectedIds || selectedIds.includes(holeId)) {
|
|
const cases = [];
|
|
const document = `BusinessHole${process.pid}`;
|
|
const copy = `${document}Copy`;
|
|
const path = `/tmp/${document}.FCStd`;
|
|
let worker;
|
|
let sequence = 0;
|
|
const check = (input, actual, assertions) => {
|
|
cases.push({ input, actual, assertions: Object.entries(assertions).map(([name, passed]) => ({ name, passed: Boolean(passed) })) });
|
|
};
|
|
const send = (action, args = {}, doc = document) => worker.send(worker.request(`hole-${++sequence}`, 'partdesign.document', {
|
|
schema: 'freecad-web-partdesign/document/v1', module: 'PartDesign', document: doc, action, ...args,
|
|
}, doc));
|
|
const object = (reply, name) => reply.result?.objects?.find(item => item.name === name);
|
|
const holeArgs = { body: 'Body', object: 'Hole', profile: 'HoleProfile', diameter: 4, depth: 5 };
|
|
const desktop = (args, doc = document) => worker.desktop({ request: (_module, command, payload, documentId) =>
|
|
worker.send(worker.request(`hole-desktop-${++sequence}`, command, payload, documentId)) },
|
|
'PartDesign_Hole', { ...args, document: doc }, doc);
|
|
try {
|
|
if (!kernel) ({ kernel } = await loadWasmProbe('web/public/kernel', 'FreeCADWebPartDesignRuntimeProbe'));
|
|
worker = loadBusinessWorker(kernel);
|
|
for (const [action, args] of [
|
|
['create', {}], ['createBody', { object: 'Body' }],
|
|
['setSketch', { body: 'Body', object: 'BaseProfile', profileType: 'Rectangle', parameters: { Width: 20, Height: 20 }, position: [0, 0, 0] }],
|
|
['setFeature', { body: 'Body', object: 'Pad', featureType: 'Pad', profile: 'BaseProfile', length: 10 }],
|
|
['setSketch', { body: 'Body', object: 'HoleProfile', profileType: 'Circle', parameters: { Radius: 2 }, position: [10, 10, 10] }],
|
|
]) {
|
|
const reply = await send(action, args);
|
|
check({ action, ...args }, reply, { 'native fixture created': reply.kind === 'result' });
|
|
if (reply.kind !== 'result') throw new Error(`Hole setup failed: ${JSON.stringify(reply)}`);
|
|
}
|
|
const created = await desktop({ ...holeArgs, selection: [{ documentId: document, object: 'HoleProfile' }], includeMesh: true });
|
|
const first = object(created, 'Hole');
|
|
check(holeArgs, created, {
|
|
'native Hole owns profile and body': first?.type === 'PartDesign::Hole' && first.body === 'Body' && first.profile === 'HoleProfile',
|
|
'native Hole is body tip': object(created, 'Body')?.tip === 'Hole',
|
|
'native Hole removes material and provides mesh': first?.volume > 0 && first.volume < 4000 && first.mesh?.triangleCount > 0,
|
|
'native Hole parameters persist': first?.diameter === 4 && first.depth === 5 && first.depthType === 'Dimension',
|
|
});
|
|
const edited = await desktop({ ...holeArgs, diameter: 6, selection: [{ documentId: document, object: 'Hole' }] });
|
|
const second = object(edited, 'Hole');
|
|
check({ diameter: 6, selectedObject: 'Hole' }, edited, {
|
|
'selected Hole retains its native profile on edit': second?.profile === 'HoleProfile' && second?.body === 'Body',
|
|
'larger diameter removes more native material': second?.volume > 0 && second.volume < first?.volume,
|
|
});
|
|
const undone = await send('undo');
|
|
check({ action: 'undo' }, undone, { 'undo restores Hole geometry and diameter': object(undone, 'Hole')?.diameter === 4 && Math.abs(object(undone, 'Hole')?.volume - first?.volume) < 1e-7 });
|
|
const redone = await send('redo');
|
|
check({ action: 'redo' }, redone, { 'redo restores Hole edit': object(redone, 'Hole')?.diameter === 6 && Math.abs(object(redone, 'Hole')?.volume - second?.volume) < 1e-7 });
|
|
// Native validation occurs inside the edit transaction; a missing profile must roll it back.
|
|
const rejected = await desktop({ ...holeArgs, profile: 'MissingProfile' });
|
|
const unchanged = await send('snapshot');
|
|
check({ profile: 'MissingProfile' }, rejected, {
|
|
'invalid native edit fails': rejected.kind !== 'result',
|
|
'failed edit preserves geometry and transaction history': object(unchanged, 'Hole')?.diameter === 6
|
|
&& Math.abs(object(unchanged, 'Hole')?.volume - second?.volume) < 1e-7
|
|
&& unchanged.result?.undoCount === redone.result?.undoCount,
|
|
});
|
|
const foreign = await desktop({ ...holeArgs, selection: [{ documentId: copy, object: 'HoleProfile' }] });
|
|
check({ selectionDocument: copy }, foreign, { 'foreign selection rejected': foreign.kind !== 'result' });
|
|
for (const selection of [
|
|
[{ documentId: document, object: 'Body' }],
|
|
[{ documentId: document, object: 'Hole', subelements: ['Face1'] }],
|
|
[{ documentId: copy, object: 'Hole' }],
|
|
]) {
|
|
const invalidSelection = await desktop({ ...holeArgs, selection });
|
|
check({ selection }, invalidSelection, { 'invalid feature edit selection rejected': invalidSelection.kind !== 'result' });
|
|
}
|
|
const saved = await send('save', { path });
|
|
check({ action: 'save' }, saved, { 'native FCStd bytes produced': saved.result?.bytes?.byteLength > 0 });
|
|
const opened = await send('open', { path, bytes: saved.result?.bytes }, copy);
|
|
check({ action: 'open' }, opened, {
|
|
'reopen preserves native Hole links and geometry': object(opened, 'Hole')?.profile === 'HoleProfile'
|
|
&& object(opened, 'Hole')?.body === 'Body' && object(opened, 'Body')?.tip === 'Hole'
|
|
&& Math.abs(object(opened, 'Hole')?.volume - second?.volume) < 1e-7,
|
|
});
|
|
const continued = await desktop({ ...holeArgs, depth: 3, selection: [{ documentId: copy, object: 'Hole' }] }, copy);
|
|
check({ document: copy, depth: 3 }, continued, { 'reopened Hole remains editable': object(continued, 'Hole')?.depth === 3 && object(continued, 'Hole')?.volume > second?.volume });
|
|
const restored = await send('undo', {}, copy);
|
|
check({ document: copy, action: 'undo' }, restored, { 'reopened edit supports undo': object(restored, 'Hole')?.diameter === 6 && object(restored, 'Hole')?.depth === 5 });
|
|
} catch (error) {
|
|
check({ capabilityId: holeId }, { error: String(error.stack || error) }, { 'Hole workflow completed': false });
|
|
} finally {
|
|
if (worker) for (const doc of [document, copy]) await send('close', {}, doc);
|
|
}
|
|
const failed = cases.some(item => item.assertions.some(assertion => !assertion.passed));
|
|
results.push({ capabilityId: holeId, status: failed ? 'failed' : 'passed', cases });
|
|
if (failed) exitCode = 1;
|
|
}
|
|
const { datumCloneIds, runDatumCloneCases } = await import('./partdesign-datum-clone-business-cases.mjs');
|
|
const datumCloneResults = await runDatumCloneCases((selectedIds ?? datumCloneIds).filter(id => datumCloneIds.includes(id)), loadBusinessWorker);
|
|
results.push(...datumCloneResults);
|
|
if (datumCloneResults.some(result => result.status !== 'passed')) exitCode = 1;
|
|
const profileResults = await runProfileCases((selectedIds ?? []).filter(id => profileIds.includes(id)));
|
|
results.push(...profileResults);
|
|
if (profileResults.some(result => result.status !== 'passed')) exitCode = 1;
|
|
const finishingResults = await runFinishingCases((selectedIds ?? []).filter(id => Object.hasOwn(finishingCases, id)), loadBusinessWorker);
|
|
results.push(...finishingResults);
|
|
if (finishingResults.some(result => result.status !== 'passed')) exitCode = 1;
|
|
const additiveResults = await runAdditiveCases((selectedIds ?? []).filter(id => additiveIds.includes(id)), loadBusinessWorker);
|
|
results.push(...additiveResults);
|
|
if (additiveResults.some(result => result.status !== 'passed')) exitCode = 1;
|
|
const subtractiveResults = await runSubtractiveCases((selectedIds ?? []).filter(id => subtractiveIds.includes(id)), loadBusinessWorker);
|
|
results.push(...subtractiveResults);
|
|
if (subtractiveResults.some(result => result.status !== 'passed')) exitCode = 1;
|
|
process.stdout.write(`${JSON.stringify({ schema: 'freecad-web-business-cases/v1', results })}\n`, () => process.exit(exitCode));
|