103 lines
5.0 KiB
JavaScript
103 lines
5.0 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { createHash } from 'node:crypto';
|
|
import { loadWasmProbe } from './wasm-probe-runtime.mjs';
|
|
|
|
// Run in a child process so a synchronous native stall has a hard deadline.
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
const directory = process.argv[2];
|
|
const report = { status: 'failed', stages: [], calls: [], assertions: [] };
|
|
const started = performance.now();
|
|
let kernel;
|
|
const persist = () => fs.writeFileSync(path.join(directory, 'independent-native.json'), JSON.stringify(report, null, 2));
|
|
const stage = name => {
|
|
report.stages.push({ name, enteredMs: performance.now() - started });
|
|
persist();
|
|
};
|
|
const done = () => { report.stages.at(-1).completedMs = performance.now() - started; persist(); };
|
|
const call = (document, action, details = {}, reject = false) => {
|
|
const args = { ...details, document, action, includeMesh: true };
|
|
stage(`${document}:${action}`);
|
|
const response = JSON.parse(kernel.ccall('fc_web_dispatch_json', 'string', ['string'], [JSON.stringify({ cmd: 'partdesign.document', args })]));
|
|
report.calls.push({ args, response });
|
|
done();
|
|
assert.equal(response.ok, !reject, JSON.stringify(response));
|
|
if (reject) { assert.ok(response.error); return response; }
|
|
assert.equal(response.result.document, document);
|
|
return response.result;
|
|
};
|
|
const profile = width => ({ object: 'Sketch1', body: 'Body1', profileType: 'Rectangle', parameters: { Width: width, Height: 8 }, position: [0, 0, 0] });
|
|
const check = (snapshot, width) => {
|
|
assert.equal(snapshot.objectCount, snapshot.objects.length);
|
|
const body = snapshot.objects.find(object => object.name === 'Body1');
|
|
const sketch = snapshot.objects.find(object => object.name === 'Sketch1');
|
|
const pad = snapshot.objects.find(object => object.name === 'Pad1');
|
|
assert.equal(body.type, 'PartDesign::Body');
|
|
assert.equal(body.tip, 'Pad1');
|
|
assert.ok(body.group.includes('Sketch1') && body.group.includes('Pad1'));
|
|
assert.equal(sketch.type, 'Sketcher::SketchObject');
|
|
assert.equal(sketch.body, 'Body1');
|
|
assert.equal(sketch.geometryCount, 4);
|
|
assert.ok(sketch.constraintCount >= 8);
|
|
assert.equal(sketch.dof, 0);
|
|
assert.equal(sketch.fullyConstrained, true);
|
|
assert.deepEqual(sketch.parameters, { Width: width, Height: 8 });
|
|
assert.equal(pad.type, 'PartDesign::Pad');
|
|
assert.equal(pad.body, 'Body1');
|
|
assert.equal(pad.profile, 'Sketch1');
|
|
assert.equal(pad.length, 5);
|
|
assert.ok(Math.abs(pad.volume - width * 8 * 5) < 1e-7);
|
|
assert.ok(pad.mesh.triangleCount > 0 && pad.mesh.positions.length > 0);
|
|
};
|
|
try {
|
|
stage('load-and-initialize-wasm');
|
|
({ kernel } = await loadWasmProbe(path.join(root, 'web/public/kernel'), 'FreeCADWebPartDesignRuntimeProbe'));
|
|
done();
|
|
const primary = 'CrossNative', other = 'CrossOther', restored = 'CrossRestored';
|
|
for (const [document, width] of [[primary, 12], [other, 7]]) {
|
|
call(document, 'create');
|
|
call(document, 'createBody', { object: 'Body1' });
|
|
call(document, 'setSketch', profile(width));
|
|
check(call(document, 'setFeature', { object: 'Pad1', body: 'Body1', profile: 'Sketch1', featureType: 'Pad', length: 5, reversed: false }), width);
|
|
}
|
|
const unrelated = call(other, 'snapshot');
|
|
const baseline = call(primary, 'snapshot');
|
|
const edited = call(primary, 'setSketch', profile(14));
|
|
check(edited, 14);
|
|
assert.equal(edited.undoCount, baseline.undoCount + 1);
|
|
check(call(primary, 'undo'), 12);
|
|
const undone = call(primary, 'snapshot');
|
|
assert.equal(undone.redoCount, 1);
|
|
call(primary, 'setSketch', profile(-1), true);
|
|
assert.deepEqual(call(primary, 'snapshot'), undone, 'Rejected native edit must preserve geometry AND pending redo');
|
|
check(call(primary, 'redo'), 14);
|
|
assert.deepEqual(call(other, 'snapshot'), unrelated, 'Same-name objects in the second document remain untouched');
|
|
report.assertions.push('Constrained sketch and dependent Pad recompute; undo/redo and rejected edit preserve both documents');
|
|
const filename = '/tmp/CrossNative.FCStd';
|
|
call(primary, 'save', { path: filename });
|
|
const bytes = kernel.FS.readFile(filename);
|
|
assert.ok(bytes.length > 1000);
|
|
assert.deepEqual(Array.from(bytes.slice(0, 2)), [80, 75]);
|
|
fs.writeFileSync(path.join(directory, 'independent-native.FCStd'), bytes);
|
|
report.file = { byteLength: bytes.length, sha256: createHash('sha256').update(bytes).digest('hex') };
|
|
call(primary, 'close');
|
|
check(call(restored, 'open', { path: filename }), 14);
|
|
check(call(restored, 'setSketch', profile(15)), 15);
|
|
check(call(restored, 'undo'), 14);
|
|
check(call(restored, 'redo'), 15);
|
|
assert.deepEqual(call(other, 'snapshot'), unrelated);
|
|
report.assertions.push('Actual FCStd restores Body/Tip/profile/constraints and permits fresh edit, undo and redo');
|
|
call(restored, 'close');
|
|
call(other, 'close');
|
|
report.status = 'passed';
|
|
} catch (error) {
|
|
report.error = String(error.stack ?? error);
|
|
process.exitCode = 1;
|
|
} finally {
|
|
stage('cleanup');
|
|
kernel?.PThread?.terminateAllThreads();
|
|
done();
|
|
}
|