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-color-failure.json'), 'utf8')) const fail = (message) => { throw new Error(`FreeCAD PropertyColor failure check failed: ${message}`) } if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-color-failure' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyColor') fail('baseline is invalid') if (report.object?.name !== 'ColorProbe' || report.object.typeId !== 'App::FeatureTest' || report.property?.name !== 'Colour' || report.property.typeId !== 'App::PropertyColor' || !isDeepStrictEqual(report.initial, [0, 0, 0, 1])) fail('native fixture identity changed') const expectedCoercions = new Map([ ['float-out-of-range', { requested: '(2.0, -1.0, 0.5, 1.5)', after: [2, -1, 0.5, 1.5] }], ['integer-out-of-range', { requested: '(-1, 256, 511, -255)', after: [-0.003921569, 1.003921628, 2.003921509, -1] }], ['boolean-tuple', { requested: '(True, False, True, False)', after: [0.003921569, 0, 0.003921569, 0] }], ['packed-max', { requested: '4294967295', after: [1, 1, 1, 1] }], ['packed-wrap', { requested: '4294967296', after: [0, 0, 0, 0] }], ['packed-negative', { requested: '-1', after: [1, 1, 1, 1] }], ]) if (report.coercions?.length !== expectedCoercions.size) fail('coercion case count changed') for (const entry of report.coercions) { const expected = expectedCoercions.get(entry.id) if (!expected || entry.requested !== expected.requested || entry.accepted !== true || entry.exception !== null || entry.valuePreserved !== false || !isDeepStrictEqual(entry.before, [0, 0, 0, 1]) || !isDeepStrictEqual(entry.after, expected.after) || entry.objectsPreserved !== true || !isDeepStrictEqual(entry.beforeObjects, entry.afterObjects)) fail(`${entry.id} accepted coercion changed`) } const expectedFailures = new Map([ ['none', { requested: 'None', message: 'type must be integer or tuple of float or tuple integer, not NoneType' }], ['list', { requested: '[0.1, 0.2, 0.3]', message: 'type must be integer or tuple of float or tuple integer, not list' }], ['short-tuple', { requested: '(0.1, 0.2)', message: 'type must be integer or tuple of float or tuple integer, not tuple' }], ['long-tuple', { requested: '(0.1, 0.2, 0.3, 0.4, 0.5)', message: 'type must be integer or tuple of float or tuple integer, not tuple' }], ['string-first', { requested: "('bad', 0.2, 0.3)", message: 'Type in tuple must be float or integer' }], ['mixed-after-float', { requested: '(0.1, 2, 0.3)', message: 'Type in tuple must be consistent (float)' }], ['mixed-after-integer', { requested: '(1, 0.2, 3)', message: 'Type in tuple must be consistent (integer)' }], ['scalar-float', { requested: '0.5', message: 'type must be integer or tuple of float or tuple integer, not float' }], ]) if (report.failures?.length !== expectedFailures.size) fail('failure case count changed') for (const entry of report.failures) { const expected = expectedFailures.get(entry.id) if (!expected || entry.requested !== expected.requested || entry.exception?.type !== 'TypeError' || entry.exception.message !== expected.message || entry.valuePreserved !== true || entry.objectsPreserved !== true || entry.polluted !== false || !isDeepStrictEqual(entry.before, [0, 0, 0, 1]) || !isDeepStrictEqual(entry.after, [0, 0, 0, 1]) || !isDeepStrictEqual(entry.beforeObjects, entry.afterObjects)) fail(`${entry.id} did not reject without pollution`) } const disabled = report.disabled if (disabled?.classification !== 'editor-readonly-python-mutable-until-immutable' || !isDeepStrictEqual(disabled.before, [0, 0, 0, 1]) || !isDeepStrictEqual(disabled.editorMode, ['ReadOnly']) || !isDeepStrictEqual(disabled.editorReadOnlyRequested, [0.2, 0.3, 0.4, 0.5]) || disabled.editorReadOnlyException !== null || disabled.pythonBypassesEditorReadOnly !== true || !isDeepStrictEqual(disabled.editorReadOnlyAfter, [0.200000003, 0.300000012, 0.400000006, 0.5])) fail('editor ReadOnly boundary changed') if (!isDeepStrictEqual(disabled.immutableStatus, ['Immutable', 'ReadOnly']) || !isDeepStrictEqual(disabled.immutableRequested, [0.6, 0.7, 0.8, 0.9]) || disabled.immutableException?.type !== 'AttributeError' || disabled.immutableException.message !== "Object attribute 'Colour' is read-only" || !isDeepStrictEqual(disabled.immutableAfter, [0, 0, 0, 1]) || disabled.immutableValuePreserved !== true || disabled.objectsPreserved !== true || !isDeepStrictEqual(disabled.restoredStatus, []) || !isDeepStrictEqual(disabled.restoredEditorMode, [])) fail('Immutable rejection or status restoration changed') const transaction = report.transaction if (transaction?.undoMode !== 1 || transaction.pendingAfterEdit !== true || transaction.pendingAfterAbort !== false || transaction.activeAfterEdit?.name !== 'property-color-cancel' || !(transaction.activeAfterEdit.id > 0) || transaction.activeAfterAbort?.name !== '' || transaction.activeAfterAbort.id !== 0 || transaction.restored !== true || transaction.objectsRestored !== true || transaction.recoveryRecomputeResult !== true) fail('transaction lifecycle changed') if (!isDeepStrictEqual(transaction.before, [0, 0, 0, 1]) || !isDeepStrictEqual(transaction.edited, [0.75, 0.5, 0.25, 0.125]) || !isDeepStrictEqual(transaction.afterAbort, [0, 0, 0, 1]) || !isDeepStrictEqual(transaction.afterRecompute, [0, 0, 0, 1]) || !isDeepStrictEqual(transaction.stateAfterEdit, { state: ['Touched'], statusString: 'Touched' }) || !isDeepStrictEqual(transaction.stateAfterAbort, { state: ['Touched'], statusString: 'Touched' }) || !isDeepStrictEqual(transaction.stateAfterRecompute, { state: ['Up-to-date'], statusString: 'Valid' }) || !isDeepStrictEqual(transaction.objectsBefore, transaction.objectsAfter)) fail('transaction abort did not restore value and state') 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 is not explicit') if (report.documentIntegrity?.objectsPreserved !== true || !isDeepStrictEqual(report.documentIntegrity.initialObjects, report.documentIntegrity.finalObjects)) fail('failure cases polluted the document') console.log(JSON.stringify({ status: 'freecad-property-color-failure-pass', acceptedCoercions: report.coercions.map(({ id, after }) => ({ id, after })), rejectedCases: report.failures.map(({ id, exception }) => ({ id, exception })), disabled: { pythonBypassesEditorReadOnly: disabled.pythonBypassesEditorReadOnly, immutableException: disabled.immutableException }, transactionRestored: transaction.restored, cancellationBoundary: report.cancellationBoundary }, null, 2))