126 lines
14 KiB
JavaScript
126 lines
14 KiB
JavaScript
import { isDeepStrictEqual } from 'node:util'
|
|
import { readFile } from 'node:fs/promises'
|
|
import { resolve } from 'node:path'
|
|
|
|
const root = resolve(new URL('..', import.meta.url).pathname)
|
|
const report = JSON.parse(await readFile(resolve(root, 'config/freecad-property-direction-failure.json'), 'utf8'))
|
|
const fail = (message) => { throw new Error(`FreeCAD PropertyDirection failure check failed: ${message}`) }
|
|
|
|
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-direction-failure' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyDirection') fail('baseline is invalid')
|
|
|
|
const setter = report.setterBoundaries
|
|
const mirrorObjects = [{ name: 'MirrorSource', typeId: 'Part::Box' }, { name: 'DirectionMirror', typeId: 'Part::Mirroring' }]
|
|
if (setter?.object?.name !== 'DirectionMirror' || setter.object.typeId !== 'Part::Mirroring' || setter.property?.name !== 'Normal' || setter.property.typeId !== 'App::PropertyDirection' || !isDeepStrictEqual(setter.baseline, [0, 0, 1])) fail('setter host boundary changed')
|
|
const expectedCoercions = new Map([
|
|
['mixed-numeric-tuple', { requested: '(1, 0.5, 2)', after: [1, 0.5, 2] }],
|
|
['boolean-tuple', { requested: '(True, False, True)', after: [1, 0, 1] }],
|
|
])
|
|
if (setter.acceptedCoercions?.length !== expectedCoercions.size) fail('accepted coercion count changed')
|
|
for (const entry of setter.acceptedCoercions) {
|
|
const expected = expectedCoercions.get(entry.id)
|
|
if (!expected || entry.requested !== expected.requested || entry.exception !== null || entry.accepted !== true || !isDeepStrictEqual(entry.before, [0, 0, 1]) || !isDeepStrictEqual(entry.after, expected.after) || entry.objectsPreserved !== true) fail(`${entry.id} accepted coercion changed`)
|
|
}
|
|
|
|
const expectedFailures = new Map([
|
|
['none', { requested: 'None', message: "type must be 'Vector' or tuple of three floats, not NoneType" }],
|
|
['list', { requested: '[0.0, 0.0, 1.0]', message: "type must be 'Vector' or tuple of three floats, not list" }],
|
|
['short-tuple', { requested: '(0.0, 1.0)', message: "type must be 'Vector' or tuple of three floats, not tuple" }],
|
|
['long-tuple', { requested: '(0.0, 0.0, 1.0, 2.0)', message: "type must be 'Vector' or tuple of three floats, not tuple" }],
|
|
['string-component', { requested: "(0.0, 'bad', 1.0)", message: 'Not allowed type used in tuple (float expected)...' }],
|
|
['scalar-float', { requested: '1.0', message: "type must be 'Vector' or tuple of three floats, not float" }],
|
|
['scalar-integer', { requested: '1', message: "type must be 'Vector' or tuple of three floats, not int" }],
|
|
])
|
|
if (setter.failures?.length !== expectedFailures.size) fail('setter failure count changed')
|
|
for (const entry of setter.failures) {
|
|
const expected = expectedFailures.get(entry.id)
|
|
if (!expected || entry.requested !== expected.requested || entry.exception?.type !== 'TypeError' || entry.exception.message !== expected.message || !isDeepStrictEqual(entry.before, [0, 0, 1]) || !isDeepStrictEqual(entry.after, [0, 0, 1]) || entry.valuePreserved !== true || entry.objectsPreserved !== true || entry.polluted !== false || !isDeepStrictEqual(entry.beforeObjects, mirrorObjects) || !isDeepStrictEqual(entry.afterObjects, mirrorObjects)) fail(`${entry.id} setter rejection changed`)
|
|
}
|
|
if (setter.documentIntegrity?.objectsPreserved !== true || !isDeepStrictEqual(setter.documentIntegrity.initialObjects, mirrorObjects) || !isDeepStrictEqual(setter.documentIntegrity.finalObjects, mirrorObjects)) fail('setter cases polluted the document')
|
|
|
|
const hostExpectations = {
|
|
'Mirroring-zero': {
|
|
objectTypeId: 'Part::Mirroring',
|
|
propertyName: 'Normal',
|
|
objects: mirrorObjects,
|
|
beforeState: { state: ['Up-to-date'], statusString: 'Valid', mustExecute: false },
|
|
afterSetState: { state: ['Touched'], statusString: 'Touched', mustExecute: true },
|
|
afterState: { state: ['Touched', 'Invalid'], statusString: 'gp_Dir() - input vector has zero norm', mustExecute: true },
|
|
shape: { shapeType: 'Solid', solids: 1, faces: 6, edges: 12, vertices: 8 },
|
|
},
|
|
'ProjectOnSurface-zero': {
|
|
objectTypeId: 'Part::ProjectOnSurface',
|
|
propertyName: 'Direction',
|
|
objects: [{ name: 'ProjectionSupport', typeId: 'Part::Feature' }, { name: 'ProjectionWire', typeId: 'Part::Feature' }, { name: 'DirectionProjection', typeId: 'Part::ProjectOnSurface' }],
|
|
beforeState: { state: ['Up-to-date'], statusString: 'Valid', mustExecute: false },
|
|
afterSetState: { state: ['Touched'], statusString: 'Touched', mustExecute: false },
|
|
afterState: { state: ['Touched', 'Invalid'], statusString: 'gp_Dir() - input vector has zero norm', mustExecute: false },
|
|
shape: { shapeType: 'Compound', solids: 0, faces: 0, edges: 4, vertices: 4 },
|
|
},
|
|
}
|
|
if (report.consumerFailures?.length !== 2) fail('zero-direction host coverage changed')
|
|
for (const entry of report.consumerFailures) {
|
|
const expected = hostExpectations[entry.id]
|
|
if (!expected || entry.objectTypeId !== expected.objectTypeId || entry.propertyName !== expected.propertyName || !isDeepStrictEqual(entry.requested, [0, 0, 0]) || entry.initialRecomputeResult !== true || entry.setterException !== null || entry.recomputeException !== null || entry.recomputeResult !== true || entry.objectsPreserved !== true) fail(`${entry.id} zero-direction execution boundary changed`)
|
|
if (!isDeepStrictEqual(entry.before.value, [0, 0, 1]) || !isDeepStrictEqual(entry.afterSet.value, [0, 0, 0]) || !isDeepStrictEqual(entry.afterRecompute.value, [0, 0, 0]) || !isDeepStrictEqual(entry.before.state, expected.beforeState) || !isDeepStrictEqual(entry.afterSet.state, expected.afterSetState) || !isDeepStrictEqual(entry.afterRecompute.state, expected.afterState) || !isDeepStrictEqual(entry.before.objects, expected.objects) || !isDeepStrictEqual(entry.afterRecompute.objects, expected.objects)) fail(`${entry.id} zero-direction state changed`)
|
|
for (const snapshot of [entry.before.shape, entry.afterSet.shape, entry.afterRecompute.shape]) {
|
|
if (snapshot?.isNull !== false || snapshot.valid !== true || snapshot.shapeType !== expected.shape.shapeType || snapshot.solids !== expected.shape.solids || snapshot.faces !== expected.shape.faces || snapshot.edges !== expected.shape.edges || snapshot.vertices !== expected.shape.vertices || typeof snapshot.brepSha256 !== 'string' || snapshot.brepSha256.length !== 64) fail(`${entry.id} retained Shape evidence changed`)
|
|
}
|
|
if (entry.before.shape.brepSha256 !== entry.afterSet.shape.brepSha256 || entry.before.shape.brepSha256 !== entry.afterRecompute.shape.brepSha256) fail(`${entry.id} did not retain the last valid Shape`)
|
|
}
|
|
|
|
const nonFiniteIds = new Set([
|
|
'Mirroring-nan',
|
|
'Mirroring-positive-infinity',
|
|
'Mirroring-negative-infinity',
|
|
'ProjectOnSurface-nan',
|
|
'ProjectOnSurface-positive-infinity',
|
|
'ProjectOnSurface-negative-infinity',
|
|
])
|
|
if (report.nonFiniteConsumerBoundaries?.length !== nonFiniteIds.size || new Set(report.nonFiniteConsumerBoundaries.map(({ id }) => id)).size !== nonFiniteIds.size) fail('non-finite isolated coverage changed')
|
|
for (const boundary of report.nonFiniteConsumerBoundaries) {
|
|
if (!nonFiniteIds.has(boundary.id) || boundary.mode !== `non-finite:${boundary.id.replace('-', ':')}` || boundary.attemptCount !== 2 || boundary.stable !== true || boundary.attempts?.length !== 2) fail(`${boundary.id} isolated identity changed`)
|
|
const mirroring = boundary.id.startsWith('Mirroring-')
|
|
const expectedClassification = mirroring ? 'native-synchronous-consumer-timeout' : 'native-process-exit-without-probe-completion'
|
|
if (boundary.classification !== expectedClassification) fail(`${boundary.id} isolated classification changed`)
|
|
for (const [index, attempt] of boundary.attempts.entries()) {
|
|
if (attempt.attempt !== index + 1 || attempt.timeoutMs !== 5000 || !(attempt.elapsedMs >= (mirroring ? 5000 : 0)) || !(attempt.elapsedMs < 10000) || attempt.progressMarkerObserved !== true || attempt.completionMarkerObserved !== false || attempt.completedCase !== null) fail(`${boundary.id} attempt ${index + 1} metadata changed`)
|
|
if (mirroring) {
|
|
if (attempt.timedOut !== true || attempt.exitStatus !== null || attempt.signal !== 'SIGTERM' || attempt.errorCode !== 'ETIMEDOUT' || attempt.outcome !== 'timeout' || !attempt.diagnostic.includes(`FREECAD_PROPERTY_DIRECTION_PROGRESS=${boundary.id}`)) fail(`${boundary.id} timeout evidence changed`)
|
|
} else if (attempt.timedOut !== false || attempt.exitStatus !== 0 || attempt.signal !== null || attempt.errorCode !== null || attempt.outcome !== 'process-exited-without-completion' || !attempt.diagnostic.some((line) => line.includes('Courbes non jointives'))) fail(`${boundary.id} process-exit evidence changed`)
|
|
}
|
|
}
|
|
|
|
const dependencyExpectations = {
|
|
'Mirroring-no-source': { objectTypeId: 'Part::Mirroring', propertyName: 'Normal', removed: { property: 'Source', value: null }, state: { state: ['Touched', 'Invalid'], statusString: 'No object linked', mustExecute: true }, retainsShape: true },
|
|
'ProjectOnSurface-no-support': { objectTypeId: 'Part::ProjectOnSurface', propertyName: 'Direction', removed: { property: 'SupportFace', value: null }, state: { state: ['Touched', 'Invalid'], statusString: 'No support face specified', mustExecute: false }, retainsShape: true },
|
|
'ProjectOnSurface-no-projection': { objectTypeId: 'Part::ProjectOnSurface', propertyName: 'Direction', removed: { property: 'Projection', value: [] }, state: { state: ['Up-to-date'], statusString: 'Valid', mustExecute: false }, retainsShape: false },
|
|
}
|
|
if (report.dependencyFailures?.length !== 3) fail('dependency coverage changed')
|
|
for (const entry of report.dependencyFailures) {
|
|
const expected = dependencyExpectations[entry.id]
|
|
if (!expected || entry.objectTypeId !== expected.objectTypeId || entry.propertyName !== expected.propertyName || !isDeepStrictEqual(entry.removed, expected.removed) || entry.recomputeException !== null || entry.recomputeResult !== true || entry.objectsPreserved !== true || !isDeepStrictEqual(entry.before.value, [0, 0, 1]) || !isDeepStrictEqual(entry.afterRecompute.value, [0, 0, 1]) || !isDeepStrictEqual(entry.before.objects, entry.afterRecompute.objects) || !isDeepStrictEqual(entry.afterRecompute.state, expected.state)) fail(`${entry.id} dependency behavior changed`)
|
|
if (expected.retainsShape) {
|
|
if (entry.before.shape.brepSha256 !== entry.afterRecompute.shape.brepSha256 || entry.afterRecompute.shape.isNull !== false || entry.afterRecompute.shape.valid !== true) fail(`${entry.id} did not retain its last valid Shape`)
|
|
} else if (entry.afterRecompute.shape.isNull !== true || entry.afterRecompute.shape.shapeType !== 'Null' || entry.afterRecompute.shape.edges !== 0) fail(`${entry.id} empty Projection boundary changed`)
|
|
}
|
|
|
|
const disabled = report.disabled
|
|
if (disabled?.classification !== 'editor-readonly-python-mutable-until-immutable' || !isDeepStrictEqual(disabled.before, [0, 0, 1]) || !isDeepStrictEqual(disabled.editorMode, ['ReadOnly']) || !isDeepStrictEqual(disabled.editorRequested, [1, 0, 1]) || disabled.editorException !== null || !isDeepStrictEqual(disabled.editorAfter, [1, 0, 1]) || disabled.pythonBypassesEditorReadOnly !== true) fail('editor ReadOnly boundary changed')
|
|
if (!isDeepStrictEqual(disabled.immutableStatus, ['Immutable']) || !isDeepStrictEqual(disabled.immutableRequested, [0, 1, 1]) || disabled.immutableException?.type !== 'AttributeError' || disabled.immutableException.message !== "Object attribute 'Normal' is read-only" || !isDeepStrictEqual(disabled.immutableAfter, [0, 0, 1]) || disabled.immutableValuePreserved !== true || !isDeepStrictEqual(disabled.restoredStatus, []) || !isDeepStrictEqual(disabled.restoredEditorMode, [])) fail('Immutable boundary changed')
|
|
|
|
const transaction = report.transaction
|
|
if (transaction?.undoMode !== 1 || transaction.pendingAfterEdit !== true || transaction.pendingAfterAbort !== false || transaction.activeAfterEdit?.name !== 'property-direction-cancel' || !(transaction.activeAfterEdit.id > 0) || transaction.activeAfterAbort?.name !== '' || transaction.activeAfterAbort.id !== 0 || transaction.recoveryRecomputeResult !== true || transaction.restored !== true || transaction.objectsRestored !== true) fail('transaction lifecycle changed')
|
|
if (!isDeepStrictEqual(transaction.before, [0, 0, 1]) || !isDeepStrictEqual(transaction.edited, [0.25, -0.5, 1.5]) || !isDeepStrictEqual(transaction.afterAbort, [0, 0, 1]) || !isDeepStrictEqual(transaction.afterRecompute, [0, 0, 1]) || !isDeepStrictEqual(transaction.stateAfterEdit, { state: ['Touched'], statusString: 'Touched', mustExecute: true }) || !isDeepStrictEqual(transaction.stateAfterAbort, { state: ['Touched'], statusString: 'Touched', mustExecute: true }) || !isDeepStrictEqual(transaction.stateAfterRecompute, { state: ['Up-to-date'], statusString: 'Valid', mustExecute: false }) || !isDeepStrictEqual(transaction.objectsBefore, mirrorObjects) || !isDeepStrictEqual(transaction.objectsAfter, mirrorObjects)) fail('transaction abort recovery changed')
|
|
if (report.cancellationBoundary?.supported !== false || report.cancellationBoundary.classification !== 'synchronous-property-setter' || report.cancellationBoundary.reason !== 'no-native-cancel-hook' || report.cancellationBoundary.replacement !== 'abort-active-document-transaction') fail('cancellation boundary changed')
|
|
if (report.documentIntegrity?.allObjectsPreserved !== true || report.documentIntegrity.setter?.objectsPreserved !== true || report.documentIntegrity.disabledAndTransaction?.objectsPreserved !== true) fail('failure cases polluted a document object set')
|
|
|
|
console.log(JSON.stringify({
|
|
status: 'freecad-property-direction-failure-pass',
|
|
acceptedCoercions: [...expectedCoercions.keys()],
|
|
rejectedInputs: [...expectedFailures.keys()],
|
|
zeroDirectionConsumers: report.consumerFailures.map(({ id, afterRecompute }) => ({ id, status: afterRecompute.state.statusString })),
|
|
nonFiniteBoundaries: report.nonFiniteConsumerBoundaries.map(({ id, classification, attemptCount }) => ({ id, classification, attemptCount })),
|
|
dependencyBoundaries: report.dependencyFailures.map(({ id, afterRecompute }) => ({ id, state: afterRecompute.state })),
|
|
transactionRestored: transaction.restored,
|
|
}, null, 2))
|