feat: close ordered pairs and property codec batches
This commit is contained in:
13
scripts/check-chrome-property-acceleration.mjs
Normal file
13
scripts/check-chrome-property-acceleration.mjs
Normal file
@@ -0,0 +1,13 @@
|
||||
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/chrome-property-acceleration-verification.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`Chrome PropertyAcceleration check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.browserId !== 'chrome' || !/Chrome\//.test(report.userAgent || '') || report.crossOriginIsolated !== true) fail('browser boundary is invalid')
|
||||
const requiredWorkerRequests = ['initialize', 'save-document', 'load-document', 'load-checkpoint', 'recovery-report', 'put-resource', 'get-resource', 'release-resource', 'dispose']
|
||||
if (!report.worker?.constructed?.some(({ name }) => name === 'bitbybit-persistence') || !requiredWorkerRequests.every((type) => report.worker.requestTypes?.includes(type)) || report.worker.terminated?.includes('bitbybit-persistence') !== true) fail('persistence Worker lifecycle is incomplete')
|
||||
if (report.ui?.inputType !== 'number' || report.ui.label !== 'Acceleration' || report.ui.unit !== 'mm/s^2' || report.ui.beforeValue !== 1000 || report.ui.afterValue !== 500 || report.ui.documentVersion !== 2 || report.ui.dirty !== true || report.ui.objectState !== 'touched' || report.ui.errors?.length !== 0) fail('production UI edit evidence is incomplete')
|
||||
if (report.persistence?.mode !== 'sqlite-opfs' || report.persistence.sqliteWasm !== true || report.persistence.opfs !== true || report.persistence.savedMode !== 'sqlite-opfs' || report.persistence.loadedValue !== 500 || report.persistence.loadedType !== 'App::PropertyAcceleration' || report.persistence.checkpointVersion !== 2 || report.persistence.recoveryIntegrity !== 'ok' || report.persistence.fcstdElement !== 'Float' || report.persistence.fcstdValue !== '500') fail('OPFS/FCStd persistence evidence is incomplete')
|
||||
if (!(report.resource?.byteLength > 0) || report.resource.roundTrip !== true || report.resource.released !== true || report.resource.markerRemoved !== true || report.release?.shapeCount !== 0 || report.release.kernelReferenceCount !== 0 || report.release.workerTerminated !== true) fail('resource release evidence is incomplete')
|
||||
console.log(JSON.stringify({ status: 'chrome-property-acceleration-pass', ui: report.ui, persistence: report.persistence, workerRequests: report.worker.requestTypes, resource: report.resource, release: report.release }, null, 2))
|
||||
13
scripts/check-chrome-property-area.mjs
Normal file
13
scripts/check-chrome-property-area.mjs
Normal file
@@ -0,0 +1,13 @@
|
||||
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/chrome-property-area-verification.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`Chrome PropertyArea check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.browserId !== 'chrome' || !/Chrome\//.test(report.userAgent || '') || report.crossOriginIsolated !== true) fail('browser boundary is invalid')
|
||||
const requiredWorkerRequests = ['initialize', 'save-document', 'load-document', 'load-checkpoint', 'recovery-report', 'put-resource', 'get-resource', 'release-resource', 'dispose']
|
||||
if (!report.worker?.constructed?.some(({ name }) => name === 'bitbybit-persistence') || !requiredWorkerRequests.every((type) => report.worker.requestTypes?.includes(type)) || report.worker.terminated?.includes('bitbybit-persistence') !== true) fail('persistence Worker lifecycle is incomplete')
|
||||
if (report.ui?.areaReadOnly !== true || report.ui.beforeText !== '20 mm^2' || report.ui.afterText !== '0 mm^2' || report.ui.beforeArea !== 20 || report.ui.afterArea !== 0 || report.ui.beforeElementCount !== 2 || report.ui.afterElementCount !== 0 || report.ui.triggerLabel !== 'Clear subshape links' || report.ui.documentVersion !== 2 || report.ui.dirty !== true || report.ui.objectState !== 'touched' || report.ui.errors?.length !== 0) fail('production UI derived-area evidence is incomplete')
|
||||
if (report.persistence?.mode !== 'sqlite-opfs' || report.persistence.sqliteWasm !== true || report.persistence.opfs !== true || report.persistence.savedMode !== 'sqlite-opfs' || report.persistence.loadedArea !== 0 || report.persistence.loadedAreaType !== 'App::PropertyArea' || report.persistence.loadedElementCount !== 0 || report.persistence.loadedElementsType !== 'App::PropertyLinkSubList' || report.persistence.checkpointVersion !== 2 || report.persistence.recoveryIntegrity !== 'ok' || report.persistence.fcstdAreaElement !== 'Float' || Number(report.persistence.fcstdAreaValue) !== 0 || report.persistence.fcstdElementsElement !== 'LinkSubList' || report.persistence.fcstdElementCount !== 0) fail('OPFS/FCStd persistence evidence is incomplete')
|
||||
if (!(report.resource?.byteLength > 0) || report.resource.roundTrip !== true || report.resource.released !== true || report.resource.markerRemoved !== true || report.release?.shapeCount !== 0 || report.release.kernelReferenceCount !== 0 || report.release.workerTerminated !== true) fail('resource release evidence is incomplete')
|
||||
console.log(JSON.stringify({ status: 'chrome-property-area-pass', ui: report.ui, persistence: report.persistence, workerRequests: report.worker.requestTypes, resource: report.resource, release: report.release }, null, 2))
|
||||
15
scripts/check-chrome-property-boollist.mjs
Normal file
15
scripts/check-chrome-property-boollist.mjs
Normal file
@@ -0,0 +1,15 @@
|
||||
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/chrome-property-boollist-verification.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`Chrome PropertyBoolList check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.browserId !== 'chrome' || !/Chrome\//.test(report.userAgent || '') || report.crossOriginIsolated !== true) fail('browser boundary is invalid')
|
||||
const requiredWorkerRequests = ['initialize', 'save-document', 'load-document', 'load-checkpoint', 'recovery-report', 'put-resource', 'get-resource', 'release-resource', 'dispose']
|
||||
if (!report.worker?.constructed?.some(({ name }) => name === 'bitbybit-persistence') || !requiredWorkerRequests.every((type) => report.worker.requestTypes?.includes(type)) || report.worker.terminated?.includes('bitbybit-persistence') !== true) fail('persistence Worker lifecycle is incomplete')
|
||||
const target = [true, false, true, false]
|
||||
if (report.ui?.inputType !== 'text' || report.ui.label !== 'Bool list' || report.ui.beforeText !== 'false' || report.ui.afterText !== 'true, false, true, false' || !isDeepStrictEqual(report.ui.beforeValue, [false]) || !isDeepStrictEqual(report.ui.afterValue, target) || report.ui.documentVersion !== 2 || report.ui.dirty !== true || report.ui.objectState !== 'touched' || report.ui.errors?.length !== 0) fail('production UI edit evidence is incomplete')
|
||||
if (report.persistence?.mode !== 'sqlite-opfs' || report.persistence.sqliteWasm !== true || report.persistence.opfs !== true || report.persistence.savedMode !== 'sqlite-opfs' || !isDeepStrictEqual(report.persistence.loadedValue, target) || report.persistence.loadedType !== 'App::PropertyBoolList' || report.persistence.checkpointVersion !== 2 || report.persistence.recoveryIntegrity !== 'ok' || report.persistence.fcstdElement !== 'BoolList' || report.persistence.fcstdValue !== '[true,false,true,false]') fail('OPFS/FCStd persistence evidence is incomplete')
|
||||
if (!(report.resource?.byteLength > 0) || report.resource.roundTrip !== true || report.resource.released !== true || report.resource.markerRemoved !== true || report.release?.shapeCount !== 0 || report.release.kernelReferenceCount !== 0 || report.release.workerTerminated !== true) fail('resource release evidence is incomplete')
|
||||
console.log(JSON.stringify({ status: 'chrome-property-boollist-pass', ui: report.ui, persistence: report.persistence, workerRequests: report.worker.requestTypes, resource: report.resource, release: report.release }, null, 2))
|
||||
15
scripts/check-chrome-property-color.mjs
Normal file
15
scripts/check-chrome-property-color.mjs
Normal file
@@ -0,0 +1,15 @@
|
||||
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/chrome-property-color-verification.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`Chrome PropertyColor check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.browserId !== 'chrome' || !/Chrome\//.test(report.userAgent || '') || report.crossOriginIsolated !== true) fail('browser boundary is invalid')
|
||||
const requiredWorkerRequests = ['initialize', 'save-document', 'load-document', 'load-checkpoint', 'recovery-report', 'put-resource', 'get-resource', 'release-resource', 'dispose']
|
||||
if (!report.worker?.constructed?.some(({ name }) => name === 'bitbybit-persistence') || !requiredWorkerRequests.every((type) => report.worker.requestTypes?.includes(type)) || report.worker.terminated?.includes('bitbybit-persistence') !== true) fail('persistence Worker lifecycle is incomplete')
|
||||
const target = [0.2, 0.4, 0.6, 0.8]
|
||||
if (report.ui?.rgbInputType !== 'color' || report.ui.alphaInputType !== 'number' || report.ui.rgbLabel !== 'Colour RGB' || report.ui.alphaLabel !== 'Colour alpha' || report.ui.beforeHex !== '#000000' || report.ui.afterHex !== '#336699' || report.ui.beforeAlpha !== '1' || report.ui.afterAlpha !== 0.8 || !isDeepStrictEqual(report.ui.beforeValue, [0, 0, 0, 1]) || !isDeepStrictEqual(report.ui.afterValue, target) || report.ui.documentVersion !== 3 || report.ui.dirty !== true || report.ui.objectState !== 'touched' || report.ui.errors?.length !== 0) fail('production UI edit evidence is incomplete')
|
||||
if (report.persistence?.mode !== 'sqlite-opfs' || report.persistence.sqliteWasm !== true || report.persistence.opfs !== true || report.persistence.savedMode !== 'sqlite-opfs' || !isDeepStrictEqual(report.persistence.loadedValue, target) || report.persistence.loadedType !== 'App::PropertyColor' || report.persistence.checkpointVersion !== 3 || report.persistence.recoveryIntegrity !== 'ok' || report.persistence.fcstdElement !== 'PropertyColor' || report.persistence.fcstdValue !== '862362060') fail('OPFS/FCStd persistence evidence is incomplete')
|
||||
if (!(report.resource?.byteLength > 0) || report.resource.roundTrip !== true || report.resource.released !== true || report.resource.markerRemoved !== true || report.release?.shapeCount !== 0 || report.release.kernelReferenceCount !== 0 || report.release.workerTerminated !== true) fail('resource release evidence is incomplete')
|
||||
console.log(JSON.stringify({ status: 'chrome-property-color-pass', ui: report.ui, persistence: report.persistence, workerRequests: report.worker.requestTypes, resource: report.resource, release: report.release }, null, 2))
|
||||
15
scripts/check-chrome-property-colorlist.mjs
Normal file
15
scripts/check-chrome-property-colorlist.mjs
Normal file
@@ -0,0 +1,15 @@
|
||||
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/chrome-property-colorlist-verification.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`Chrome PropertyColorList check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.browserId !== 'chrome' || !/Chrome\//.test(report.userAgent || '') || report.crossOriginIsolated !== true) fail('browser boundary is invalid')
|
||||
const requiredWorkerRequests = ['initialize', 'save-document', 'load-document', 'load-checkpoint', 'recovery-report', 'put-resource', 'get-resource', 'release-resource', 'dispose']
|
||||
if (!report.worker?.constructed?.some(({ name }) => name === 'bitbybit-persistence') || !requiredWorkerRequests.every((type) => report.worker.requestTypes?.includes(type)) || report.worker.terminated?.includes('bitbybit-persistence') !== true) fail('persistence Worker lifecycle is incomplete')
|
||||
const target = [[0.2, 0.4, 0.6, 0.8], [17 / 255, 34 / 255, 51 / 255, 0.4]]
|
||||
if (!isDeepStrictEqual(report.ui?.rgbInputTypes, ['color', 'color']) || !isDeepStrictEqual(report.ui?.alphaInputTypes, ['number', 'number']) || !isDeepStrictEqual(report.ui?.rgbLabels, ['Colour list 1 RGB', 'Colour list 2 RGB']) || !isDeepStrictEqual(report.ui?.alphaLabels, ['Colour list 1 alpha', 'Colour list 2 alpha']) || report.ui?.addLabel !== 'Add Colour list color' || report.ui?.removeLabel !== 'Remove Colour list 3' || !isDeepStrictEqual(report.ui?.beforeHex, ['#000000']) || !isDeepStrictEqual(report.ui?.afterHex, ['#336699', '#112233']) || !isDeepStrictEqual(report.ui?.beforeAlpha, ['1']) || !isDeepStrictEqual(report.ui?.afterAlpha, [0.8, 0.4]) || !isDeepStrictEqual(report.ui?.beforeValue, [[0, 0, 0, 1]]) || !isDeepStrictEqual(report.ui?.afterValue, target) || report.ui?.beforeItemCount !== 1 || report.ui?.peakItemCount !== 3 || report.ui?.afterItemCount !== 2 || report.ui?.documentVersion !== 8 || report.ui?.dirty !== true || report.ui?.objectState !== 'touched' || report.ui?.errors?.length !== 0) fail('production UI edit evidence is incomplete')
|
||||
if (report.persistence?.mode !== 'sqlite-opfs' || report.persistence.sqliteWasm !== true || report.persistence.opfs !== true || report.persistence.savedMode !== 'sqlite-opfs' || !isDeepStrictEqual(report.persistence.loadedValue, target) || report.persistence.loadedType !== 'App::PropertyColorList' || report.persistence.checkpointVersion !== 8 || report.persistence.recoveryIntegrity !== 'ok' || report.persistence.fcstdElement !== 'ColorList' || report.persistence.fcstdValue !== JSON.stringify(target) || report.persistence.fcstdResourcePath !== 'ColourList' || report.persistence.fcstdResourceBytes !== 12) fail('OPFS/FCStd persistence evidence is incomplete')
|
||||
if (!(report.resource?.byteLength > 0) || report.resource.roundTrip !== true || report.resource.released !== true || report.resource.markerRemoved !== true || report.release?.shapeCount !== 0 || report.release.kernelReferenceCount !== 0 || report.release.workerTerminated !== true) fail('resource release evidence is incomplete')
|
||||
console.log(JSON.stringify({ status: 'chrome-property-colorlist-pass', ui: report.ui, persistence: report.persistence, workerRequests: report.worker.requestTypes, resource: report.resource, release: report.release }, null, 2))
|
||||
16
scripts/check-chrome-property-direction.mjs
Normal file
16
scripts/check-chrome-property-direction.mjs
Normal file
@@ -0,0 +1,16 @@
|
||||
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/chrome-property-direction-verification.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`Chrome PropertyDirection check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.browserId !== 'chrome' || !/Chrome\//.test(report.userAgent || '') || report.crossOriginIsolated !== true) fail('browser boundary is invalid')
|
||||
const requiredWorkerRequests = ['initialize', 'save-document', 'load-document', 'load-checkpoint', 'recovery-report', 'put-resource', 'get-resource', 'release-resource', 'dispose']
|
||||
if (!report.worker?.constructed?.some(({ name }) => name === 'bitbybit-persistence') || !requiredWorkerRequests.every((type) => report.worker.requestTypes?.includes(type)) || report.worker.terminated?.includes('bitbybit-persistence') !== true) fail('persistence Worker lifecycle is incomplete')
|
||||
const initial = { x: 0, y: 0, z: 1 }
|
||||
const target = { x: 0.25, y: -0.5, z: 1.5 }
|
||||
if (report.ui?.inputTypes?.some((type) => type !== 'number') || !isDeepStrictEqual(report.ui?.labels, ['Normal x', 'Normal y', 'Normal z']) || !isDeepStrictEqual(report.ui?.beforeValue, initial) || !isDeepStrictEqual(report.ui?.afterValue, target) || report.ui?.documentVersion !== 4 || report.ui?.dirty !== true || report.ui?.objectState !== 'touched' || report.ui?.errors?.length !== 0) fail('production UI direction edit evidence is incomplete')
|
||||
if (report.persistence?.mode !== 'sqlite-opfs' || report.persistence.sqliteWasm !== true || report.persistence.opfs !== true || report.persistence.savedMode !== 'sqlite-opfs' || !isDeepStrictEqual(report.persistence.loadedValue, target) || report.persistence.loadedType !== 'App::PropertyDirection' || report.persistence.checkpointVersion !== 4 || report.persistence.recoveryIntegrity !== 'ok' || report.persistence.fcstdElement !== 'PropertyVector' || report.persistence.fcstdValue !== JSON.stringify(target)) fail('OPFS/FCStd persistence evidence is incomplete')
|
||||
if (!(report.resource?.byteLength > 0) || report.resource.roundTrip !== true || report.resource.released !== true || report.resource.markerRemoved !== true || report.release?.shapeCount !== 0 || report.release?.kernelReferenceCount !== 0 || report.release?.workerTerminated !== true) fail('resource release evidence is incomplete')
|
||||
console.log(JSON.stringify({ status: 'chrome-property-direction-pass', ui: report.ui, persistence: report.persistence, workerRequests: report.worker.requestTypes, resource: report.resource, release: report.release }, null, 2))
|
||||
15
scripts/check-chrome-property-file.mjs
Normal file
15
scripts/check-chrome-property-file.mjs
Normal file
@@ -0,0 +1,15 @@
|
||||
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/chrome-property-file-verification.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`Chrome PropertyFile check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.browserId !== 'chrome' || !/Chrome\//.test(report.userAgent || '') || report.crossOriginIsolated !== true) fail('browser boundary is invalid')
|
||||
const requiredWorkerRequests = ['initialize', 'save-document', 'load-document', 'load-checkpoint', 'recovery-report', 'put-resource', 'get-resource', 'release-resource', 'dispose']
|
||||
if (!report.worker?.constructed?.some(({ name }) => name === 'bitbybit-persistence') || !requiredWorkerRequests.every((type) => report.worker.requestTypes?.includes(type)) || report.worker.terminated?.includes('bitbybit-persistence') !== true) fail('persistence Worker lifecycle is incomplete')
|
||||
const initial = 'project-files/initial.obj'
|
||||
const target = 'project-files/chrome-updated.obj'
|
||||
if (report.ui?.inputType !== 'text' || report.ui.label !== 'File name' || report.ui.beforeValue !== initial || report.ui.afterValue !== target || report.ui.documentVersion !== 2 || report.ui.dirty !== true || report.ui.objectState !== 'touched' || report.ui.errors?.length !== 0) fail('production UI file edit evidence is incomplete')
|
||||
if (report.persistence?.mode !== 'sqlite-opfs' || report.persistence.sqliteWasm !== true || report.persistence.opfs !== true || report.persistence.savedMode !== 'sqlite-opfs' || report.persistence.loadedValue !== target || report.persistence.loadedType !== 'App::PropertyFile' || report.persistence.checkpointVersion !== 2 || report.persistence.recoveryIntegrity !== 'ok' || report.persistence.fcstdElement !== 'String' || report.persistence.fcstdValue !== target) fail('OPFS/FCStd persistence evidence is incomplete')
|
||||
if (!(report.resource?.byteLength > 0) || report.resource.roundTrip !== true || report.resource.released !== true || report.resource.markerRemoved !== true || report.release?.shapeCount !== 0 || report.release.kernelReferenceCount !== 0 || report.release.workerTerminated !== true) fail('resource release evidence is incomplete')
|
||||
console.log(JSON.stringify({ status: 'chrome-property-file-pass', ui: report.ui, persistence: report.persistence, workerRequests: report.worker.requestTypes, resource: report.resource, release: report.release }, null, 2))
|
||||
15
scripts/check-chrome-property-fileincluded.mjs
Normal file
15
scripts/check-chrome-property-fileincluded.mjs
Normal file
@@ -0,0 +1,15 @@
|
||||
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/chrome-property-fileincluded-verification.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`Chrome PropertyFileIncluded check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.browserId !== 'chrome' || !/Chrome\//.test(report.userAgent || '') || report.crossOriginIsolated !== true) fail('browser boundary is invalid')
|
||||
const requiredWorkerRequests = ['initialize', 'save-document', 'load-document', 'load-checkpoint', 'recovery-report', 'put-resource', 'get-resource', 'release-resource', 'dispose']
|
||||
if (!report.worker?.constructed?.some(({ name }) => name === 'bitbybit-persistence') || !requiredWorkerRequests.every((type) => report.worker.requestTypes?.includes(type)) || report.worker.terminated?.includes('bitbybit-persistence') !== true) fail('persistence Worker lifecycle is incomplete')
|
||||
const value = report.ui?.afterValue
|
||||
if (report.ui?.inputType !== 'file' || report.ui.label !== 'Embedded file' || report.ui.beforeName !== 'Empty' || report.ui.afterName !== 'chrome-resource.bin' || value?.schemaVersion !== 1 || value.archiveName !== 'chrome-resource.bin' || !/^[a-f0-9]{64}$/.test(value.resourceHash || '') || value.byteLength !== 16 || value.mediaType !== 'application/octet-stream' || report.ui.documentVersion !== 2 || report.ui.dirty !== true || report.ui.objectState !== 'touched' || !isDeepStrictEqual(report.ui.notices, ['chrome-resource.bin embedded'])) fail('production UI included-file edit evidence is incomplete')
|
||||
if (report.persistence?.mode !== 'sqlite-opfs' || report.persistence.sqliteWasm !== true || report.persistence.opfs !== true || report.persistence.savedMode !== 'sqlite-opfs' || !isDeepStrictEqual(report.persistence.loadedValue, value) || report.persistence.loadedType !== 'App::PropertyFileIncluded' || report.persistence.checkpointVersion !== 2 || report.persistence.recoveryIntegrity !== 'ok' || report.persistence.fcstdElement !== 'FileIncluded' || report.persistence.fcstdResourcePath !== 'chrome-resource.bin' || report.persistence.fcstdResourceBytes !== 16 || report.persistence.fcstdResourceMatches !== true) fail('OPFS/FCStd persistence evidence is incomplete')
|
||||
if (report.resource?.documentByteLength !== 16 || report.resource.documentRoundTrip !== true || !(report.resource.independentByteLength > 0) || report.resource.independentRoundTrip !== true || report.resource.independentReleased !== true || report.resource.markerRemoved !== true || report.release?.shapeCount !== 0 || report.release.kernelReferenceCount !== 0 || report.release.workerTerminated !== true) fail('resource release evidence is incomplete')
|
||||
console.log(JSON.stringify({ status: 'chrome-property-fileincluded-pass', ui: report.ui, persistence: report.persistence, workerRequests: report.worker.requestTypes, resource: report.resource, release: report.release }, null, 2))
|
||||
@@ -6,7 +6,21 @@ const forbidden = [
|
||||
/from\s+["']three(?:\/|["'])/, /from\s+["']@types\/three/, /from\s+["']sqlite3?/, /from\s+["']@sqlite/, /from\s+["']opfs/, /SharedArrayBuffer/, /\bpostMessage\s*\(/,
|
||||
]
|
||||
const allowDirectRuntime = new Set(['facade/threeViewport.ts', 'facade/threeViewport.tsx', 'facade/persistenceWorker.ts', 'facade/projectStore.ts', 'facade/projectMigrationSeedWorker.ts', 'facade/geometryWorker.ts', 'facade/nativeHistoryWorkerClient.ts', 'facade/nativeHistoryWorkerEntry.ts', 'facade/planegcsWorkerClient.ts', 'facade/planegcsWorkerEntry.ts'])
|
||||
const allowWorkerMessaging = new Set(['assemblySolverWorker.ts', 'meshLodWorker.ts', 'chromeAssemblyHarness.ts', 'chromeMeshHarness.ts', 'facade/camPipeline.ts'])
|
||||
const allowWorkerMessaging = new Set([
|
||||
'assemblySolverWorker.ts',
|
||||
'meshLodWorker.ts',
|
||||
'chromeAssemblyHarness.ts',
|
||||
'chromeMeshHarness.ts',
|
||||
'chromePropertyAccelerationHarness.tsx',
|
||||
'chromePropertyAreaHarness.tsx',
|
||||
'chromePropertyBoolListHarness.tsx',
|
||||
'chromePropertyColorHarness.tsx',
|
||||
'chromePropertyColorListHarness.tsx',
|
||||
'chromePropertyDirectionHarness.tsx',
|
||||
'chromePropertyFileHarness.tsx',
|
||||
'chromePropertyFileIncludedHarness.tsx',
|
||||
'facade/camPipeline.ts',
|
||||
])
|
||||
|
||||
async function walk(relative = '') {
|
||||
const directory = new URL(relative, sourceRoot)
|
||||
|
||||
@@ -5,11 +5,27 @@ import { resolve } from 'node:path'
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const report = JSON.parse(await readFile(resolve(root, 'config/freecad-native-property-semantics.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD native property semantics check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.exactPromotionReady !== false || report.exactBlocker !== '58 runtime property types and 523 records remain opaque-only; complete native document and property semantics') fail('report boundary is invalid')
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.exactPromotionReady !== false || report.exactBlocker !== '50 runtime property types and 466 records remain opaque-only; complete native document and property semantics') fail('report boundary is invalid')
|
||||
if (report.runtime?.registeredObjectTypes !== 352 || report.runtime.instantiableObjectTypes !== 348 || report.runtime.unavailableObjectTypes !== 4 || report.runtime.propertyRecords !== 5510 || report.runtime.propertyTypes !== 85 || report.runtime.unavailableObjects?.length !== 4) fail('runtime inventory counts changed')
|
||||
const support = report.supportSummary
|
||||
if (support?.['native-editable-codec']?.typeCount !== 22 || support['native-editable-codec'].recordCount !== 4304 || support?.['native-specialized-codec']?.typeCount !== 5 || support['native-specialized-codec'].recordCount !== 683 || support?.['opaque-fcstd-proxy']?.typeCount !== 58 || support['opaque-fcstd-proxy'].recordCount !== 523) fail('property support partition is stale')
|
||||
if (support?.['native-editable-codec']?.typeCount !== 30 || support['native-editable-codec'].recordCount !== 4361 || support?.['native-specialized-codec']?.typeCount !== 5 || support['native-specialized-codec'].recordCount !== 683 || support?.['opaque-fcstd-proxy']?.typeCount !== 50 || support['opaque-fcstd-proxy'].recordCount !== 466) fail('property support partition is stale')
|
||||
if (report.types?.length !== 85 || new Set(report.types.map(({ typeId }) => typeId)).size !== 85 || report.types.reduce((total, entry) => total + entry.recordCount, 0) !== 5510) fail('per-TypeId inventory is invalid')
|
||||
const acceleration = report.types.find(({ typeId }) => typeId === 'App::PropertyAcceleration')
|
||||
if (acceleration?.support !== 'native-editable-codec' || acceleration.recordCount !== 1 || acceleration.objectTypeCount !== 1) fail('App::PropertyAcceleration promotion is missing')
|
||||
const area = report.types.find(({ typeId }) => typeId === 'App::PropertyArea')
|
||||
if (area?.support !== 'native-editable-codec' || area.recordCount !== 1 || area.objectTypeCount !== 1 || area.statusNames?.join(',') !== 'PropOutput,PropReadOnly') fail('App::PropertyArea promotion is missing')
|
||||
const boolList = report.types.find(({ typeId }) => typeId === 'App::PropertyBoolList')
|
||||
if (boolList?.support !== 'native-editable-codec' || boolList.recordCount !== 7 || boolList.objectTypeCount !== 7 || boolList.statusNames?.join(',') !== 'Hidden,Immutable,LockDynamic') fail('App::PropertyBoolList promotion is missing')
|
||||
const color = report.types.find(({ typeId }) => typeId === 'App::PropertyColor')
|
||||
if (color?.support !== 'native-editable-codec' || color.recordCount !== 8 || color.objectTypeCount !== 8 || color.statusNames?.length !== 0) fail('App::PropertyColor promotion is missing')
|
||||
const colorList = report.types.find(({ typeId }) => typeId === 'App::PropertyColorList')
|
||||
if (colorList?.support !== 'native-editable-codec' || colorList.recordCount !== 2 || colorList.objectTypeCount !== 2 || colorList.statusNames?.length !== 0) fail('App::PropertyColorList promotion is missing')
|
||||
const direction = report.types.find(({ typeId }) => typeId === 'App::PropertyDirection')
|
||||
if (direction?.support !== 'native-editable-codec' || direction.recordCount !== 2 || direction.objectTypeCount !== 2 || direction.statusNames?.length !== 0) fail('App::PropertyDirection promotion is missing')
|
||||
const file = report.types.find(({ typeId }) => typeId === 'App::PropertyFile')
|
||||
if (file?.support !== 'native-editable-codec' || file.recordCount !== 16 || file.objectTypeCount !== 12 || file.statusNames?.length !== 0) fail('App::PropertyFile promotion is missing')
|
||||
const fileIncluded = report.types.find(({ typeId }) => typeId === 'App::PropertyFileIncluded')
|
||||
if (fileIncluded?.support !== 'native-editable-codec' || fileIncluded.recordCount !== 20 || fileIncluded.objectTypeCount !== 15 || fileIncluded.statusNames?.join(',') !== 'PropOutput,ReadOnly') fail('App::PropertyFileIncluded promotion is missing')
|
||||
if (report.propertyStatus?.unknownNumericBits?.length !== 0 || report.propertyStatus.proxyOnlyRecordCount !== 0 || report.propertyStatus.proxyOnly?.length !== 0 || report.propertyStatus.facadeNative?.join(',') !== report.propertyStatus.observed?.join(',')) fail('property status representation coverage is stale')
|
||||
if (report.propertyStatus.behaviorNative?.join(',') !== 'Hidden,Immutable,LockDynamic,NoModify,Ordered,Output,PartialTrigger,PropHidden,PropNoPersist,PropNoRecompute,PropOutput,PropReadOnly,PropTransient,ReadOnly,Transient' || report.propertyStatus.preservedOnly?.length !== 0 || report.propertyStatus.preservedOnlyRecordCount !== 0) fail('property status behavior boundary is stale')
|
||||
for (const locked of [report.source, report.harness]) {
|
||||
@@ -17,4 +33,4 @@ for (const locked of [report.source, report.harness]) {
|
||||
const [content, bytes] = await Promise.all([readFile(path), stat(path).then(({ size }) => size)])
|
||||
if (bytes !== locked.bytes || createHash('sha256').update(content).digest('hex') !== locked.sha256) fail(`report is stale for ${locked.path}`)
|
||||
}
|
||||
console.log(JSON.stringify({ status: 'freecad-native-property-semantics-pass', propertyTypes: 85, propertyRecords: 5510, nativeCodecRecords: 4987, opaqueProxyRecords: 523, representedStatusRecords: 5510, preservedOnlyStatusRecords: 0, exactPromotionReady: false }, null, 2))
|
||||
console.log(JSON.stringify({ status: 'freecad-native-property-semantics-pass', propertyTypes: 85, propertyRecords: 5510, nativeCodecRecords: 5044, opaqueProxyRecords: 466, promotedTypes: [acceleration.typeId, area.typeId, boolList.typeId, color.typeId, colorList.typeId, direction.typeId, file.typeId, fileIncluded.typeId], representedStatusRecords: 5510, preservedOnlyStatusRecords: 0, exactPromotionReady: false, exactBlocker: report.exactBlocker }, null, 2))
|
||||
|
||||
@@ -6,6 +6,13 @@ const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const report = JSON.parse(await readFile(resolve(root, 'config/freecad-ordered-operation-pair-classification.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD ordered operation pair classification: ${message}`) }
|
||||
const sha256 = (value) => createHash('sha256').update(value).digest('hex')
|
||||
const summariesMatchWithinTolerance = (left, right, tolerance = 1e-6) => (
|
||||
left?.isValid === right?.isValid
|
||||
&& ['solids', 'faces', 'edges', 'vertices'].every((key) => left?.[key] === right?.[key])
|
||||
&& ['volume', 'area'].every((key) => Math.abs(left?.[key] - right?.[key]) <= tolerance)
|
||||
&& left?.bounds?.length === right?.bounds?.length
|
||||
&& left.bounds.every((value, index) => Math.abs(value - right.bounds[index]) <= tolerance)
|
||||
)
|
||||
if (report.schemaVersion !== 1 || report.baseline?.freecadVersion !== '1.1.1' || report.baseline.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || !/^8\./.test(report.baseline.occtVersion)) fail('baseline is invalid.')
|
||||
for (const harness of [report.nativeProbe?.executor, report.nativeProbe?.matrix, report.nativeProbe?.resave]) {
|
||||
const path = resolve(root, harness?.path ?? '')
|
||||
@@ -227,6 +234,154 @@ const expectedPairs = [
|
||||
{ pair: 'chamfer->fuse', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepFilletAPI_MakeChamfer', firstInputCount: 1, secondBuilder: 'BRepAlgoAPI_Fuse', secondInputCount: 2, mutationParameter: 'toolOffsetX', mutationTrajectory: [8, 7, 8] },
|
||||
{ pair: 'chamfer->cut', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepFilletAPI_MakeChamfer', firstInputCount: 1, secondBuilder: 'BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'toolSize', mutationTrajectory: [4, 3, 4] },
|
||||
{ pair: 'chamfer->common', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepFilletAPI_MakeChamfer', firstInputCount: 1, secondBuilder: 'BRepAlgoAPI_Common', secondInputCount: 2, mutationParameter: 'toolOffsetX', mutationTrajectory: [7, 6, 7] },
|
||||
{ pair: 'chamfer->rotate', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepFilletAPI_MakeChamfer', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [15, 22.5, 15] },
|
||||
{ pair: 'chamfer->pad', decision: 'rejected', reasonCode: 'native-builder-and-freecad-profile-reject-with-clean-resave', firstBuilder: 'BRepFilletAPI_MakeChamfer', firstInputCount: 1, rejectionAuthority: 'occt-builder', secondBuilder: 'BRepPrimAPI_MakePrism', secondInputCount: 1, mutationParameter: 'length', mutationTrajectory: [5, 7.5, 5], nativeTypeId: 'PartDesign::Pad', freeCadDiagnostic: 'FeatureExtrusion: Length: Could not extrude the sketch!' },
|
||||
{ pair: 'chamfer->pocket', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepFilletAPI_MakeChamfer', firstInputCount: 1, secondBuilder: 'BRepPrimAPI_MakePrism+BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'length', mutationTrajectory: [5, 4, 5] },
|
||||
{ pair: 'chamfer->loft', decision: 'rejected', reasonCode: 'freecad-input-precondition-reject-with-kernel-diagnostic-and-clean-resave', firstBuilder: 'BRepFilletAPI_MakeChamfer', firstInputCount: 1, rejectionAuthority: 'freecad-document', kernelOutcome: 'accepted', secondBuilder: 'BRepOffsetAPI_ThruSections', secondInputCount: 2, mutationParameter: 'ruled', mutationTrajectory: [false, true, false], nativeTypeId: 'Part::Loft', freeCadDiagnostic: 'Profile shape is not a single vertex, edge, wire nor face.' },
|
||||
{ pair: 'chamfer->pipe', decision: 'rejected', reasonCode: 'freecad-input-precondition-reject-with-kernel-diagnostic-and-clean-resave', firstBuilder: 'BRepFilletAPI_MakeChamfer', firstInputCount: 1, rejectionAuthority: 'freecad-document', kernelOutcome: 'invalid-result', secondBuilder: 'BRepOffsetAPI_MakePipe', secondInputCount: 2, mutationParameter: 'spineLength', mutationTrajectory: [15, 12, 15], nativeTypeId: 'Part::Sweep', freeCadDiagnostic: 'A fatal error occurred when making the sweep' },
|
||||
{ pair: 'chamfer->revolution', decision: 'rejected', reasonCode: 'native-builder-and-freecad-profile-reject-with-clean-resave', firstBuilder: 'BRepFilletAPI_MakeChamfer', firstInputCount: 1, rejectionAuthority: 'occt-builder', secondBuilder: 'BRepPrimAPI_MakeRevol', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [360, 270, 360], nativeTypeId: 'Part::Revolution', freeCadDiagnostic: 'Solids are not Processed' },
|
||||
{ pair: 'chamfer->groove', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepFilletAPI_MakeChamfer', firstInputCount: 1, secondBuilder: 'BRepPrimAPI_MakeRevol+BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'angle', mutationTrajectory: [360, 180, 360] },
|
||||
{ pair: 'chamfer->fillet', decision: 'rejected', reasonCode: 'native-builder-wasm-abort-with-freecad-profile-acceptance-and-clean-resave', firstBuilder: 'BRepFilletAPI_MakeChamfer', firstInputCount: 1, rejectionAuthority: 'occt-builder', secondBuilder: 'BRepFilletAPI_MakeFillet', secondInputCount: 1, mutationParameter: 'radius', mutationTrajectory: [0.4, 0.6, 0.4], nativeTypeId: 'Part::Fillet', freecadProfileAccepted: true, wasmCrashSignature: 'memory access out of bounds' },
|
||||
{ pair: 'chamfer->chamfer', decision: 'rejected', reasonCode: 'native-builder-and-freecad-profile-reject-with-clean-resave', firstBuilder: 'BRepFilletAPI_MakeChamfer', firstInputCount: 1, rejectionAuthority: 'occt-builder', secondBuilder: 'BRepFilletAPI_MakeChamfer', secondInputCount: 1, mutationParameter: 'distance', mutationTrajectory: [0.4, 0.6, 0.4], nativeTypeId: 'Part::Chamfer', freeCadDiagnostic: 'BRep_API: command not done', wasmCrashSignature: 'memory access out of bounds' },
|
||||
{ pair: 'chamfer->hole', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepFilletAPI_MakeChamfer', firstInputCount: 1, secondBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', secondInputCount: 1, mutationParameter: 'radius', mutationTrajectory: [1, 1.5, 1] },
|
||||
{ pair: 'chamfer->draft', decision: 'rejected', reasonCode: 'native-builder-and-freecad-profile-reject-with-clean-resave', firstBuilder: 'BRepFilletAPI_MakeChamfer', firstInputCount: 1, rejectionAuthority: 'occt-builder', secondBuilder: 'BRepOffsetAPI_DraftAngle', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [5, 8, 5], nativeTypeId: 'PartDesign::Draft', freeCadDiagnostic: '', inputShapeTransfer: 'partdesign-feature-summary-exact-with-explicit-brep-drift' },
|
||||
{ pair: 'chamfer->thickness', decision: 'rejected', reasonCode: 'native-and-freecad-profile-no-op-with-clean-resave', firstBuilder: 'BRepFilletAPI_MakeChamfer', firstInputCount: 1, rejectionAuthority: 'occt-no-op', kernelOutcome: 'no-op', secondBuilder: 'BRepOffsetAPI_MakeThickSolid', secondInputCount: 1, mutationParameter: 'offset', mutationTrajectory: [-0.4, -0.6, -0.4], nativeTypeId: 'Part::Thickness', freeCadDiagnostic: 'Valid', freecadProfileNoOp: true, rollbackShape: 'geometry-and-summary-exact-with-explicit-brep-reserialization' },
|
||||
{ pair: 'chamfer->linear-pattern', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepFilletAPI_MakeChamfer', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', secondInputCount: 1, mutationParameter: 'translationX', mutationTrajectory: [8, 9, 8] },
|
||||
{ pair: 'chamfer->polar-pattern', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepFilletAPI_MakeChamfer', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [30, 45, 30] },
|
||||
{ pair: 'chamfer->mirrored', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepFilletAPI_MakeChamfer', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', secondInputCount: 1, mutationParameter: 'planeOriginX', mutationTrajectory: [6, 7, 6] },
|
||||
{ pair: 'chamfer->multi-transform', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepFilletAPI_MakeChamfer', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', secondInputCount: 1, mutationParameter: 'multiTranslationX', mutationTrajectory: [8, 9, 8] },
|
||||
{ pair: 'hole->fuse', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', firstInputCount: 1, secondBuilder: 'BRepAlgoAPI_Fuse', secondInputCount: 2, mutationParameter: 'toolOffsetX', mutationTrajectory: [8, 7, 8] },
|
||||
{ pair: 'hole->cut', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', firstInputCount: 1, secondBuilder: 'BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'toolSize', mutationTrajectory: [4, 3, 4] },
|
||||
{ pair: 'hole->common', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', firstInputCount: 1, secondBuilder: 'BRepAlgoAPI_Common', secondInputCount: 2, mutationParameter: 'toolOffsetX', mutationTrajectory: [7, 6, 7] },
|
||||
{ pair: 'hole->rotate', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [15, 22.5, 15] },
|
||||
{ pair: 'hole->pad', decision: 'rejected', reasonCode: 'native-builder-and-freecad-profile-reject-with-clean-resave', firstBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', firstInputCount: 1, rejectionAuthority: 'occt-builder', secondBuilder: 'BRepPrimAPI_MakePrism', secondInputCount: 1, mutationParameter: 'length', mutationTrajectory: [5, 7.5, 5], nativeTypeId: 'PartDesign::Pad', freeCadDiagnostic: 'FeatureExtrusion: Length: Could not extrude the sketch!' },
|
||||
{ pair: 'hole->pocket', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', firstInputCount: 1, secondBuilder: 'BRepPrimAPI_MakePrism+BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'length', mutationTrajectory: [5, 4, 5] },
|
||||
{ pair: 'hole->loft', decision: 'rejected', reasonCode: 'freecad-input-precondition-reject-with-kernel-diagnostic-and-clean-resave', firstBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', firstInputCount: 1, rejectionAuthority: 'freecad-document', kernelOutcome: 'accepted', secondBuilder: 'BRepOffsetAPI_ThruSections', secondInputCount: 2, mutationParameter: 'ruled', mutationTrajectory: [false, true, false], nativeTypeId: 'Part::Loft', freeCadDiagnostic: 'Profile shape is not a single vertex, edge, wire nor face.' },
|
||||
{ pair: 'hole->pipe', decision: 'rejected', reasonCode: 'freecad-input-precondition-reject-with-kernel-diagnostic-and-clean-resave', firstBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', firstInputCount: 1, rejectionAuthority: 'freecad-document', kernelOutcome: 'invalid-result', secondBuilder: 'BRepOffsetAPI_MakePipe', secondInputCount: 2, mutationParameter: 'spineLength', mutationTrajectory: [15, 12, 15], nativeTypeId: 'Part::Sweep', freeCadDiagnostic: 'A fatal error occurred when making the sweep' },
|
||||
{ pair: 'hole->revolution', decision: 'rejected', reasonCode: 'native-builder-and-freecad-profile-reject-with-clean-resave', firstBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', firstInputCount: 1, rejectionAuthority: 'occt-builder', secondBuilder: 'BRepPrimAPI_MakeRevol', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [360, 270, 360], nativeTypeId: 'Part::Revolution', freeCadDiagnostic: 'Solids are not Processed' },
|
||||
{ pair: 'hole->groove', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', firstInputCount: 1, secondBuilder: 'BRepPrimAPI_MakeRevol+BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'angle', mutationTrajectory: [360, 180, 360] },
|
||||
{ pair: 'hole->fillet', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', firstInputCount: 1, secondBuilder: 'BRepFilletAPI_MakeFillet', secondInputCount: 1, mutationParameter: 'radius', mutationTrajectory: [0.4, 0.6, 0.4] },
|
||||
{ pair: 'hole->chamfer', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', firstInputCount: 1, secondBuilder: 'BRepFilletAPI_MakeChamfer', secondInputCount: 1, mutationParameter: 'distance', mutationTrajectory: [0.4, 0.6, 0.4] },
|
||||
{ pair: 'hole->hole', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', firstInputCount: 1, secondBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', secondInputCount: 1, mutationParameter: 'radius', mutationTrajectory: [1, 1.5, 1] },
|
||||
{ pair: 'hole->draft', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', firstInputCount: 1, secondBuilder: 'BRepOffsetAPI_DraftAngle', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [5, 8, 5] },
|
||||
{ pair: 'hole->thickness', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', firstInputCount: 1, secondBuilder: 'BRepOffsetAPI_MakeThickSolid', secondInputCount: 1, mutationParameter: 'offset', mutationTrajectory: [-0.4, -0.6, -0.4] },
|
||||
{ pair: 'hole->linear-pattern', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', secondInputCount: 1, mutationParameter: 'translationX', mutationTrajectory: [8, 9, 8] },
|
||||
{ pair: 'hole->polar-pattern', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [30, 45, 30] },
|
||||
{ pair: 'hole->mirrored', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', secondInputCount: 1, mutationParameter: 'planeOriginX', mutationTrajectory: [6, 7, 6] },
|
||||
{ pair: 'hole->multi-transform', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', secondInputCount: 1, mutationParameter: 'multiTranslationX', mutationTrajectory: [8, 9, 8] },
|
||||
{ pair: 'draft->fuse', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_DraftAngle', firstInputCount: 1, secondBuilder: 'BRepAlgoAPI_Fuse', secondInputCount: 2, mutationParameter: 'toolOffsetX', mutationTrajectory: [8, 7, 8] },
|
||||
{ pair: 'draft->cut', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_DraftAngle', firstInputCount: 1, secondBuilder: 'BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'toolSize', mutationTrajectory: [4, 3, 4] },
|
||||
{ pair: 'draft->common', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_DraftAngle', firstInputCount: 1, secondBuilder: 'BRepAlgoAPI_Common', secondInputCount: 2, mutationParameter: 'toolOffsetX', mutationTrajectory: [7, 6, 7] },
|
||||
{ pair: 'draft->rotate', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_DraftAngle', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [15, 22.5, 15] },
|
||||
{ pair: 'draft->pad', decision: 'rejected', reasonCode: 'native-builder-and-freecad-profile-reject-with-clean-resave', firstBuilder: 'BRepOffsetAPI_DraftAngle', firstInputCount: 1, rejectionAuthority: 'occt-builder', secondBuilder: 'BRepPrimAPI_MakePrism', secondInputCount: 1, mutationParameter: 'length', mutationTrajectory: [5, 7.5, 5], nativeTypeId: 'PartDesign::Pad', freeCadDiagnostic: 'FeatureExtrusion: Length: Could not extrude the sketch!' },
|
||||
{ pair: 'draft->pocket', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_DraftAngle', firstInputCount: 1, secondBuilder: 'BRepPrimAPI_MakePrism+BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'length', mutationTrajectory: [5, 4, 5] },
|
||||
{ pair: 'draft->loft', decision: 'rejected', reasonCode: 'freecad-input-precondition-reject-with-kernel-diagnostic-and-clean-resave', firstBuilder: 'BRepOffsetAPI_DraftAngle', firstInputCount: 1, rejectionAuthority: 'freecad-document', kernelOutcome: 'accepted', secondBuilder: 'BRepOffsetAPI_ThruSections', secondInputCount: 2, mutationParameter: 'ruled', mutationTrajectory: [false, true, false], nativeTypeId: 'Part::Loft', freeCadDiagnostic: 'Profile shape is not a single vertex, edge, wire nor face.' },
|
||||
{ pair: 'draft->pipe', decision: 'rejected', reasonCode: 'freecad-input-precondition-reject-with-kernel-diagnostic-and-clean-resave', firstBuilder: 'BRepOffsetAPI_DraftAngle', firstInputCount: 1, rejectionAuthority: 'freecad-document', kernelOutcome: 'accepted', secondBuilder: 'BRepOffsetAPI_MakePipe', secondInputCount: 2, mutationParameter: 'spineLength', mutationTrajectory: [15, 12, 15], nativeTypeId: 'Part::Sweep', freeCadDiagnostic: 'A fatal error occurred when making the sweep' },
|
||||
{ pair: 'draft->revolution', decision: 'rejected', reasonCode: 'native-builder-and-freecad-profile-reject-with-clean-resave', firstBuilder: 'BRepOffsetAPI_DraftAngle', firstInputCount: 1, rejectionAuthority: 'occt-builder', secondBuilder: 'BRepPrimAPI_MakeRevol', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [360, 270, 360], nativeTypeId: 'Part::Revolution', freeCadDiagnostic: 'Solids are not Processed' },
|
||||
{ pair: 'draft->groove', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_DraftAngle', firstInputCount: 1, secondBuilder: 'BRepPrimAPI_MakeRevol+BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'angle', mutationTrajectory: [360, 180, 360] },
|
||||
{ pair: 'draft->fillet', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_DraftAngle', firstInputCount: 1, secondBuilder: 'BRepFilletAPI_MakeFillet', secondInputCount: 1, mutationParameter: 'radius', mutationTrajectory: [0.4, 0.6, 0.4] },
|
||||
{ pair: 'draft->chamfer', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_DraftAngle', firstInputCount: 1, secondBuilder: 'BRepFilletAPI_MakeChamfer', secondInputCount: 1, mutationParameter: 'distance', mutationTrajectory: [0.4, 0.6, 0.4] },
|
||||
{ pair: 'draft->hole', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_DraftAngle', firstInputCount: 1, secondBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', secondInputCount: 1, mutationParameter: 'radius', mutationTrajectory: [1, 1.5, 1] },
|
||||
{ pair: 'draft->draft', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_DraftAngle', firstInputCount: 1, secondBuilder: 'BRepOffsetAPI_DraftAngle', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [5, 8, 5] },
|
||||
{ pair: 'draft->thickness', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_DraftAngle', firstInputCount: 1, secondBuilder: 'BRepOffsetAPI_MakeThickSolid', secondInputCount: 1, mutationParameter: 'offset', mutationTrajectory: [-0.4, -0.6, -0.4] },
|
||||
{ pair: 'draft->linear-pattern', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_DraftAngle', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', secondInputCount: 1, mutationParameter: 'translationX', mutationTrajectory: [8, 9, 8] },
|
||||
{ pair: 'draft->polar-pattern', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_DraftAngle', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [30, 45, 30] },
|
||||
{ pair: 'draft->mirrored', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_DraftAngle', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', secondInputCount: 1, mutationParameter: 'planeOriginX', mutationTrajectory: [5, 6, 5] },
|
||||
{ pair: 'draft->multi-transform', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_DraftAngle', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', secondInputCount: 1, mutationParameter: 'multiTranslationX', mutationTrajectory: [8, 9, 8] },
|
||||
{ pair: 'thickness->fuse', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_MakeThickSolid', firstInputCount: 1, secondBuilder: 'BRepAlgoAPI_Fuse', secondInputCount: 2, mutationParameter: 'toolOffsetX', mutationTrajectory: [8, 7, 8] },
|
||||
{ pair: 'thickness->cut', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_MakeThickSolid', firstInputCount: 1, secondBuilder: 'BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'toolSize', mutationTrajectory: [4, 3, 4] },
|
||||
{ pair: 'thickness->common', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_MakeThickSolid', firstInputCount: 1, secondBuilder: 'BRepAlgoAPI_Common', secondInputCount: 2, mutationParameter: 'toolOffsetX', mutationTrajectory: [7, 6, 7] },
|
||||
{ pair: 'thickness->rotate', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_MakeThickSolid', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [15, 22.5, 15] },
|
||||
{ pair: 'thickness->pad', decision: 'rejected', reasonCode: 'native-builder-and-freecad-profile-reject-with-clean-resave', firstBuilder: 'BRepOffsetAPI_MakeThickSolid', firstInputCount: 1, rejectionAuthority: 'occt-builder', secondBuilder: 'BRepPrimAPI_MakePrism', secondInputCount: 1, mutationParameter: 'length', mutationTrajectory: [5, 7.5, 5], nativeTypeId: 'PartDesign::Pad', freeCadDiagnostic: 'FeatureExtrusion: Length: Could not extrude the sketch!' },
|
||||
{ pair: 'thickness->pocket', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_MakeThickSolid', firstInputCount: 1, secondBuilder: 'BRepPrimAPI_MakePrism+BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'length', mutationTrajectory: [5, 4, 5] },
|
||||
{ pair: 'thickness->loft', decision: 'rejected', reasonCode: 'freecad-input-precondition-reject-with-kernel-diagnostic-and-clean-resave', firstBuilder: 'BRepOffsetAPI_MakeThickSolid', firstInputCount: 1, rejectionAuthority: 'freecad-document', kernelOutcome: 'accepted', secondBuilder: 'BRepOffsetAPI_ThruSections', secondInputCount: 2, mutationParameter: 'ruled', mutationTrajectory: [false, true, false], nativeTypeId: 'Part::Loft', freeCadDiagnostic: 'Profile shape is not a single vertex, edge, wire nor face.' },
|
||||
{ pair: 'thickness->pipe', decision: 'rejected', reasonCode: 'freecad-input-precondition-reject-with-kernel-diagnostic-and-clean-resave', firstBuilder: 'BRepOffsetAPI_MakeThickSolid', firstInputCount: 1, rejectionAuthority: 'freecad-document', kernelOutcome: 'invalid-result', secondBuilder: 'BRepOffsetAPI_MakePipe', secondInputCount: 2, mutationParameter: 'spineLength', mutationTrajectory: [15, 12, 15], nativeTypeId: 'Part::Sweep', freeCadDiagnostic: 'A fatal error occurred when making the sweep' },
|
||||
{ pair: 'thickness->revolution', decision: 'rejected', reasonCode: 'native-builder-and-freecad-profile-reject-with-clean-resave', firstBuilder: 'BRepOffsetAPI_MakeThickSolid', firstInputCount: 1, rejectionAuthority: 'occt-builder', secondBuilder: 'BRepPrimAPI_MakeRevol', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [360, 270, 360], nativeTypeId: 'Part::Revolution', freeCadDiagnostic: 'Solids are not Processed' },
|
||||
{ pair: 'thickness->groove', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_MakeThickSolid', firstInputCount: 1, secondBuilder: 'BRepPrimAPI_MakeRevol+BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'angle', mutationTrajectory: [360, 180, 360] },
|
||||
{ pair: 'thickness->fillet', decision: 'rejected', reasonCode: 'native-builder-wasm-abort-with-freecad-profile-acceptance-and-clean-resave', firstBuilder: 'BRepOffsetAPI_MakeThickSolid', firstInputCount: 1, rejectionAuthority: 'occt-builder', secondBuilder: 'BRepFilletAPI_MakeFillet', secondInputCount: 1, mutationParameter: 'radius', mutationTrajectory: [0.4, 0.6, 0.4], nativeTypeId: 'Part::Fillet', freecadProfileAccepted: true, freecadSelectedEdges: [1], wasmCrashSignature: 'null function or function signature mismatch' },
|
||||
{ pair: 'thickness->chamfer', decision: 'rejected', reasonCode: 'native-builder-cpp-exception-with-freecad-profile-acceptance-and-clean-resave', firstBuilder: 'BRepOffsetAPI_MakeThickSolid', firstInputCount: 1, rejectionAuthority: 'occt-builder', secondBuilder: 'BRepFilletAPI_MakeChamfer', secondInputCount: 1, mutationParameter: 'distance', mutationTrajectory: [0.4, 0.6, 0.4], nativeTypeId: 'Part::Chamfer', freecadProfileAccepted: true, freecadSelectedEdges: [1] },
|
||||
{ pair: 'thickness->hole', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_MakeThickSolid', firstInputCount: 1, secondBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', secondInputCount: 1, mutationParameter: 'radius', mutationTrajectory: [1, 1.5, 1] },
|
||||
{ pair: 'thickness->draft', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_MakeThickSolid', firstInputCount: 1, secondBuilder: 'BRepOffsetAPI_DraftAngle', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [5, 8, 5] },
|
||||
{ pair: 'thickness->thickness', decision: 'rejected', reasonCode: 'native-and-freecad-profile-no-op-with-clean-resave', firstBuilder: 'BRepOffsetAPI_MakeThickSolid', firstInputCount: 1, rejectionAuthority: 'occt-no-op', kernelOutcome: 'no-op', secondBuilder: 'BRepOffsetAPI_MakeThickSolid', secondInputCount: 1, mutationParameter: 'offset', mutationTrajectory: [-0.4, -0.6, -0.4], nativeTypeId: 'Part::Thickness', freeCadDiagnostic: 'Valid', freecadProfileNoOp: true },
|
||||
{ pair: 'thickness->linear-pattern', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_MakeThickSolid', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', secondInputCount: 1, mutationParameter: 'translationX', mutationTrajectory: [8, 9, 8] },
|
||||
{ pair: 'thickness->polar-pattern', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_MakeThickSolid', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [30, 45, 30] },
|
||||
{ pair: 'thickness->mirrored', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_MakeThickSolid', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', secondInputCount: 1, mutationParameter: 'planeOriginX', mutationTrajectory: [5, 6, 5] },
|
||||
{ pair: 'thickness->multi-transform', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepOffsetAPI_MakeThickSolid', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', secondInputCount: 1, mutationParameter: 'multiTranslationX', mutationTrajectory: [8, 9, 8] },
|
||||
{ pair: 'linear-pattern->fuse', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepAlgoAPI_Fuse', secondInputCount: 2, mutationParameter: 'toolOffsetX', mutationTrajectory: [16, 15, 16] },
|
||||
{ pair: 'linear-pattern->cut', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'toolSize', mutationTrajectory: [4, 3, 4] },
|
||||
{ pair: 'linear-pattern->common', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepAlgoAPI_Common', secondInputCount: 2, mutationParameter: 'toolOffsetX', mutationTrajectory: [10, 9, 10] },
|
||||
{ pair: 'linear-pattern->rotate', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [15, 22.5, 15] },
|
||||
{ pair: 'linear-pattern->pad', decision: 'rejected', reasonCode: 'native-builder-and-freecad-profile-reject-with-clean-resave', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, rejectionAuthority: 'occt-builder', secondBuilder: 'BRepPrimAPI_MakePrism', secondInputCount: 1, mutationParameter: 'length', mutationTrajectory: [5, 7.5, 5], nativeTypeId: 'PartDesign::Pad', freeCadDiagnostic: 'FeatureExtrusion: Length: Could not extrude the sketch!' },
|
||||
{ pair: 'linear-pattern->pocket', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepPrimAPI_MakePrism+BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'length', mutationTrajectory: [5, 4, 5] },
|
||||
{ pair: 'linear-pattern->loft', decision: 'rejected', reasonCode: 'freecad-input-precondition-reject-with-kernel-diagnostic-and-clean-resave', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, rejectionAuthority: 'freecad-document', kernelOutcome: 'accepted', secondBuilder: 'BRepOffsetAPI_ThruSections', secondInputCount: 2, mutationParameter: 'ruled', mutationTrajectory: [false, true, false], nativeTypeId: 'Part::Loft', freeCadDiagnostic: 'Profile shape is not a single vertex, edge, wire nor face.' },
|
||||
{ pair: 'linear-pattern->pipe', decision: 'rejected', reasonCode: 'freecad-input-precondition-reject-with-kernel-diagnostic-and-clean-resave', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, rejectionAuthority: 'freecad-document', kernelOutcome: 'invalid-result', secondBuilder: 'BRepOffsetAPI_MakePipe', secondInputCount: 2, mutationParameter: 'spineLength', mutationTrajectory: [15, 12, 15], nativeTypeId: 'Part::Sweep', freeCadDiagnostic: 'A fatal error occurred when making the sweep' },
|
||||
{ pair: 'linear-pattern->revolution', decision: 'rejected', reasonCode: 'native-builder-and-freecad-profile-reject-with-clean-resave', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, rejectionAuthority: 'occt-builder', secondBuilder: 'BRepPrimAPI_MakeRevol', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [360, 270, 360], nativeTypeId: 'Part::Revolution', freeCadDiagnostic: 'Solids are not Processed' },
|
||||
{ pair: 'linear-pattern->groove', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepPrimAPI_MakeRevol+BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'angle', mutationTrajectory: [360, 180, 360] },
|
||||
{ pair: 'linear-pattern->fillet', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepFilletAPI_MakeFillet', secondInputCount: 1, mutationParameter: 'radius', mutationTrajectory: [0.4, 0.6, 0.4] },
|
||||
{ pair: 'linear-pattern->chamfer', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepFilletAPI_MakeChamfer', secondInputCount: 1, mutationParameter: 'distance', mutationTrajectory: [0.4, 0.6, 0.4] },
|
||||
{ pair: 'linear-pattern->hole', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', secondInputCount: 1, mutationParameter: 'radius', mutationTrajectory: [1, 1.5, 1] },
|
||||
{ pair: 'linear-pattern->draft', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepOffsetAPI_DraftAngle', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [5, 8, 5] },
|
||||
{ pair: 'linear-pattern->thickness', decision: 'rejected', reasonCode: 'native-and-freecad-profile-no-op-with-clean-resave', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, rejectionAuthority: 'occt-no-op', kernelOutcome: 'no-op', secondBuilder: 'BRepOffsetAPI_MakeThickSolid', secondInputCount: 1, mutationParameter: 'offset', mutationTrajectory: [-0.4, -0.6, -0.4], nativeTypeId: 'Part::Thickness', freeCadDiagnostic: 'Valid', freecadProfileNoOp: true, rollbackShape: 'geometry-and-summary-exact-with-explicit-brep-reserialization' },
|
||||
{ pair: 'linear-pattern->linear-pattern', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', secondInputCount: 1, mutationParameter: 'translationX', mutationTrajectory: [8, 9, 8] },
|
||||
{ pair: 'linear-pattern->polar-pattern', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [30, 45, 30] },
|
||||
{ pair: 'linear-pattern->mirrored', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', secondInputCount: 1, mutationParameter: 'planeOriginX', mutationTrajectory: [5, 6, 5] },
|
||||
{ pair: 'linear-pattern->multi-transform', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', secondInputCount: 1, mutationParameter: 'multiTranslationX', mutationTrajectory: [8, 9, 8] },
|
||||
{ pair: 'polar-pattern->fuse', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepAlgoAPI_Fuse', secondInputCount: 2, mutationParameter: 'toolOffsetX', mutationTrajectory: [8, 7, 8] },
|
||||
{ pair: 'polar-pattern->cut', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'toolSize', mutationTrajectory: [4, 3, 4] },
|
||||
{ pair: 'polar-pattern->common', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepAlgoAPI_Common', secondInputCount: 2, mutationParameter: 'toolOffsetX', mutationTrajectory: [7, 6, 7] },
|
||||
{ pair: 'polar-pattern->rotate', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [15, 22.5, 15] },
|
||||
{ pair: 'polar-pattern->pad', decision: 'rejected', reasonCode: 'native-builder-and-freecad-profile-reject-with-clean-resave', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, rejectionAuthority: 'occt-builder', secondBuilder: 'BRepPrimAPI_MakePrism', secondInputCount: 1, mutationParameter: 'length', mutationTrajectory: [5, 7.5, 5], nativeTypeId: 'PartDesign::Pad', freeCadDiagnostic: 'FeatureExtrusion: Length: Could not extrude the sketch!' },
|
||||
{ pair: 'polar-pattern->pocket', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepPrimAPI_MakePrism+BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'length', mutationTrajectory: [5, 4, 5] },
|
||||
{ pair: 'polar-pattern->loft', decision: 'rejected', reasonCode: 'freecad-input-precondition-reject-with-kernel-diagnostic-and-clean-resave', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, rejectionAuthority: 'freecad-document', kernelOutcome: 'accepted', secondBuilder: 'BRepOffsetAPI_ThruSections', secondInputCount: 2, mutationParameter: 'ruled', mutationTrajectory: [false, true, false], nativeTypeId: 'Part::Loft', freeCadDiagnostic: 'Profile shape is not a single vertex, edge, wire nor face.' },
|
||||
{ pair: 'polar-pattern->pipe', decision: 'rejected', reasonCode: 'freecad-input-precondition-reject-with-kernel-diagnostic-and-clean-resave', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, rejectionAuthority: 'freecad-document', kernelOutcome: 'invalid-result', secondBuilder: 'BRepOffsetAPI_MakePipe', secondInputCount: 2, mutationParameter: 'spineLength', mutationTrajectory: [15, 12, 15], nativeTypeId: 'Part::Sweep', freeCadDiagnostic: 'A fatal error occurred when making the sweep' },
|
||||
{ pair: 'polar-pattern->revolution', decision: 'rejected', reasonCode: 'native-builder-and-freecad-profile-reject-with-clean-resave', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, rejectionAuthority: 'occt-builder', secondBuilder: 'BRepPrimAPI_MakeRevol', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [360, 270, 360], nativeTypeId: 'Part::Revolution', freeCadDiagnostic: 'BRep_API: command not done' },
|
||||
{ pair: 'polar-pattern->groove', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepPrimAPI_MakeRevol+BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'angle', mutationTrajectory: [360, 180, 360] },
|
||||
{ pair: 'polar-pattern->fillet', decision: 'rejected', reasonCode: 'native-builder-and-freecad-profile-reject-with-clean-resave', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, rejectionAuthority: 'occt-builder', secondBuilder: 'BRepFilletAPI_MakeFillet', secondInputCount: 1, mutationParameter: 'radius', mutationTrajectory: [0.4, 0.6, 0.4], nativeTypeId: 'Part::Fillet', freeCadDiagnostic: 'BRep_API: command not done', freecadSelectedEdges: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26], wasmCrashSignature: 'null function or function signature mismatch' },
|
||||
{ pair: 'polar-pattern->chamfer', decision: 'rejected', reasonCode: 'native-builder-and-freecad-profile-reject-with-clean-resave', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, rejectionAuthority: 'occt-builder', secondBuilder: 'BRepFilletAPI_MakeChamfer', secondInputCount: 1, mutationParameter: 'distance', mutationTrajectory: [0.4, 0.6, 0.4], nativeTypeId: 'Part::Chamfer', freeCadDiagnostic: 'BRep_API: command not done', freecadSelectedEdges: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26] },
|
||||
{ pair: 'polar-pattern->hole', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', secondInputCount: 1, mutationParameter: 'radius', mutationTrajectory: [1, 1.5, 1] },
|
||||
{ pair: 'polar-pattern->draft', decision: 'rejected', reasonCode: 'native-builder-and-freecad-profile-reject-with-clean-resave', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, rejectionAuthority: 'occt-builder', secondBuilder: 'BRepOffsetAPI_DraftAngle', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [5, 8, 5], nativeTypeId: 'PartDesign::Draft', freeCadDiagnostic: '', inputShapeTransfer: 'partdesign-feature-summary-exact-with-explicit-brep-drift' },
|
||||
{ pair: 'polar-pattern->thickness', decision: 'rejected', reasonCode: 'native-and-freecad-profile-no-op-with-clean-resave', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, rejectionAuthority: 'occt-no-op', kernelOutcome: 'no-op', secondBuilder: 'BRepOffsetAPI_MakeThickSolid', secondInputCount: 1, mutationParameter: 'offset', mutationTrajectory: [-0.4, -0.6, -0.4], nativeTypeId: 'Part::Thickness', freeCadDiagnostic: 'Valid', freecadProfileNoOp: true, rollbackShape: 'geometry-and-summary-exact-with-explicit-brep-reserialization', kernelNoOpComparison: 'topology-and-metrics-within-1e-6' },
|
||||
{ pair: 'polar-pattern->linear-pattern', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', secondInputCount: 1, mutationParameter: 'translationX', mutationTrajectory: [8, 9, 8] },
|
||||
{ pair: 'polar-pattern->polar-pattern', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [30, 45, 30] },
|
||||
{ pair: 'polar-pattern->mirrored', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', secondInputCount: 1, mutationParameter: 'planeOriginX', mutationTrajectory: [5, 6, 5] },
|
||||
{ pair: 'polar-pattern->multi-transform', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', secondInputCount: 1, mutationParameter: 'multiTranslationX', mutationTrajectory: [8, 9, 8] },
|
||||
{ pair: 'mirrored->fuse', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepAlgoAPI_Fuse', secondInputCount: 2, mutationParameter: 'toolOffsetX', mutationTrajectory: [8, 7, 8] },
|
||||
{ pair: 'mirrored->cut', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'toolSize', mutationTrajectory: [4, 3, 4] },
|
||||
{ pair: 'mirrored->common', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepAlgoAPI_Common', secondInputCount: 2, mutationParameter: 'toolOffsetX', mutationTrajectory: [7, 6, 7] },
|
||||
{ pair: 'mirrored->rotate', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [15, 22.5, 15] },
|
||||
{ pair: 'mirrored->pad', decision: 'rejected', reasonCode: 'native-builder-and-freecad-profile-reject-with-clean-resave', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, rejectionAuthority: 'occt-builder', secondBuilder: 'BRepPrimAPI_MakePrism', secondInputCount: 1, mutationParameter: 'length', mutationTrajectory: [5, 7.5, 5], nativeTypeId: 'PartDesign::Pad', freeCadDiagnostic: 'FeatureExtrusion: Length: Could not extrude the sketch!' },
|
||||
{ pair: 'mirrored->pocket', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepPrimAPI_MakePrism+BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'length', mutationTrajectory: [5, 4, 5] },
|
||||
{ pair: 'mirrored->loft', decision: 'rejected', reasonCode: 'freecad-input-precondition-reject-with-kernel-diagnostic-and-clean-resave', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, rejectionAuthority: 'freecad-document', kernelOutcome: 'accepted', secondBuilder: 'BRepOffsetAPI_ThruSections', secondInputCount: 2, mutationParameter: 'ruled', mutationTrajectory: [false, true, false], nativeTypeId: 'Part::Loft', freeCadDiagnostic: 'Profile shape is not a single vertex, edge, wire nor face.' },
|
||||
{ pair: 'mirrored->pipe', decision: 'rejected', reasonCode: 'freecad-input-precondition-reject-with-kernel-diagnostic-and-clean-resave', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, rejectionAuthority: 'freecad-document', kernelOutcome: 'invalid-result', secondBuilder: 'BRepOffsetAPI_MakePipe', secondInputCount: 2, mutationParameter: 'spineLength', mutationTrajectory: [15, 12, 15], nativeTypeId: 'Part::Sweep', freeCadDiagnostic: 'A fatal error occurred when making the sweep' },
|
||||
{ pair: 'mirrored->revolution', decision: 'rejected', reasonCode: 'native-builder-and-freecad-profile-reject-with-clean-resave', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, rejectionAuthority: 'occt-builder', secondBuilder: 'BRepPrimAPI_MakeRevol', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [360, 270, 360], nativeTypeId: 'Part::Revolution', freeCadDiagnostic: 'Solids are not Processed' },
|
||||
{ pair: 'mirrored->groove', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepPrimAPI_MakeRevol+BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'angle', mutationTrajectory: [360, 180, 360] },
|
||||
{ pair: 'mirrored->fillet', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepFilletAPI_MakeFillet', secondInputCount: 1, mutationParameter: 'radius', mutationTrajectory: [0.4, 0.6, 0.4] },
|
||||
{ pair: 'mirrored->chamfer', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepFilletAPI_MakeChamfer', secondInputCount: 1, mutationParameter: 'distance', mutationTrajectory: [0.4, 0.6, 0.4] },
|
||||
{ pair: 'mirrored->hole', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', secondInputCount: 1, mutationParameter: 'radius', mutationTrajectory: [1, 1.5, 1] },
|
||||
{ pair: 'mirrored->draft', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepOffsetAPI_DraftAngle', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [5, 8, 5] },
|
||||
{ pair: 'mirrored->thickness', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepOffsetAPI_MakeThickSolid', secondInputCount: 1, mutationParameter: 'offset', mutationTrajectory: [-0.4, -0.6, -0.4] },
|
||||
{ pair: 'mirrored->polar-pattern', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [30, 45, 30] },
|
||||
{ pair: 'mirrored->mirrored', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', secondInputCount: 1, mutationParameter: 'planeOriginX', mutationTrajectory: [5, 6, 5] },
|
||||
{ pair: 'mirrored->multi-transform', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', secondInputCount: 1, mutationParameter: 'multiTranslationX', mutationTrajectory: [8, 9, 8] },
|
||||
{ pair: 'multi-transform->fuse', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', firstInputCount: 1, secondBuilder: 'BRepAlgoAPI_Fuse', secondInputCount: 2, mutationParameter: 'toolOffsetX', mutationTrajectory: [16, 15, 16] },
|
||||
{ pair: 'multi-transform->cut', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', firstInputCount: 1, secondBuilder: 'BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'toolSize', mutationTrajectory: [4, 3, 4] },
|
||||
{ pair: 'multi-transform->common', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', firstInputCount: 1, secondBuilder: 'BRepAlgoAPI_Common', secondInputCount: 2, mutationParameter: 'toolOffsetX', mutationTrajectory: [10, 9, 10] },
|
||||
{ pair: 'multi-transform->rotate', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [15, 22.5, 15] },
|
||||
{ pair: 'multi-transform->pad', decision: 'rejected', reasonCode: 'native-builder-and-freecad-profile-reject-with-clean-resave', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', firstInputCount: 1, rejectionAuthority: 'occt-builder', secondBuilder: 'BRepPrimAPI_MakePrism', secondInputCount: 1, mutationParameter: 'length', mutationTrajectory: [5, 7.5, 5], nativeTypeId: 'PartDesign::Pad', freeCadDiagnostic: 'FeatureExtrusion: Length: Could not extrude the sketch!' },
|
||||
{ pair: 'multi-transform->pocket', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', firstInputCount: 1, secondBuilder: 'BRepPrimAPI_MakePrism+BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'length', mutationTrajectory: [5, 4, 5] },
|
||||
{ pair: 'multi-transform->loft', decision: 'rejected', reasonCode: 'freecad-input-precondition-reject-with-kernel-diagnostic-and-clean-resave', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', firstInputCount: 1, rejectionAuthority: 'freecad-document', kernelOutcome: 'accepted', secondBuilder: 'BRepOffsetAPI_ThruSections', secondInputCount: 2, mutationParameter: 'ruled', mutationTrajectory: [false, true, false], nativeTypeId: 'Part::Loft', freeCadDiagnostic: 'Profile shape is not a single vertex, edge, wire nor face.' },
|
||||
{ pair: 'multi-transform->pipe', decision: 'rejected', reasonCode: 'freecad-input-precondition-reject-with-kernel-diagnostic-and-clean-resave', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', firstInputCount: 1, rejectionAuthority: 'freecad-document', kernelOutcome: 'invalid-result', secondBuilder: 'BRepOffsetAPI_MakePipe', secondInputCount: 2, mutationParameter: 'spineLength', mutationTrajectory: [15, 12, 15], nativeTypeId: 'Part::Sweep', freeCadDiagnostic: 'A fatal error occurred when making the sweep' },
|
||||
{ pair: 'multi-transform->revolution', decision: 'rejected', reasonCode: 'native-builder-and-freecad-profile-reject-with-clean-resave', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', firstInputCount: 1, rejectionAuthority: 'occt-builder', secondBuilder: 'BRepPrimAPI_MakeRevol', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [360, 270, 360], nativeTypeId: 'Part::Revolution', freeCadDiagnostic: 'BRep_API: command not done' },
|
||||
{ pair: 'multi-transform->groove', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', firstInputCount: 1, secondBuilder: 'BRepPrimAPI_MakeRevol+BRepAlgoAPI_Cut', secondInputCount: 2, mutationParameter: 'angle', mutationTrajectory: [360, 180, 360] },
|
||||
{ pair: 'multi-transform->fillet', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', firstInputCount: 1, secondBuilder: 'BRepFilletAPI_MakeFillet', secondInputCount: 1, mutationParameter: 'radius', mutationTrajectory: [0.4, 0.6, 0.4] },
|
||||
{ pair: 'multi-transform->chamfer', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', firstInputCount: 1, secondBuilder: 'BRepFilletAPI_MakeChamfer', secondInputCount: 1, mutationParameter: 'distance', mutationTrajectory: [0.4, 0.6, 0.4] },
|
||||
{ pair: 'multi-transform->hole', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', firstInputCount: 1, secondBuilder: 'BRepPrimAPI_MakeCylinder+BRepAlgoAPI_Cut', secondInputCount: 1, mutationParameter: 'radius', mutationTrajectory: [1, 1.5, 1] },
|
||||
{ pair: 'multi-transform->draft', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', firstInputCount: 1, secondBuilder: 'BRepOffsetAPI_DraftAngle', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [5, 8, 5] },
|
||||
{ pair: 'multi-transform->thickness', decision: 'rejected', reasonCode: 'native-and-freecad-profile-no-op-with-clean-resave', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', firstInputCount: 1, rejectionAuthority: 'occt-no-op', kernelOutcome: 'no-op', secondBuilder: 'BRepOffsetAPI_MakeThickSolid', secondInputCount: 1, mutationParameter: 'offset', mutationTrajectory: [-0.4, -0.6, -0.4], nativeTypeId: 'Part::Thickness', freeCadDiagnostic: 'Valid', freecadProfileNoOp: true, rollbackShape: 'geometry-and-summary-exact-with-explicit-brep-reserialization' },
|
||||
{ pair: 'multi-transform->linear-pattern', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', secondInputCount: 1, mutationParameter: 'translationX', mutationTrajectory: [8, 9, 8] },
|
||||
{ pair: 'multi-transform->polar-pattern', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', secondInputCount: 1, mutationParameter: 'angle', mutationTrajectory: [30, 45, 30] },
|
||||
{ pair: 'multi-transform->mirrored', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse', secondInputCount: 1, mutationParameter: 'planeOriginX', mutationTrajectory: [5, 6, 5] },
|
||||
{ pair: 'multi-transform->multi-transform', decision: 'accepted', reasonCode: 'native-builders-mutation-naming-and-fcstd-resave-pass', firstBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', firstInputCount: 1, secondBuilder: 'BRepBuilderAPI_Transform+BRepAlgoAPI_Fuse(ordered)', secondInputCount: 1, mutationParameter: 'multiTranslationX', mutationTrajectory: [8, 9, 8] },
|
||||
]
|
||||
if (report.nativeProbe?.artifacts?.length !== 3 || report.classifications?.length !== expectedPairs.length) fail('native artifacts or classification prefix is incomplete.')
|
||||
for (const [index, entry] of report.classifications.entries()) {
|
||||
@@ -251,16 +406,22 @@ for (const [index, entry] of report.classifications.entries()) {
|
||||
for (const [attemptIndex, attempt] of rejection.kernelAttempts.entries()) {
|
||||
const validKernelShape = expected.kernelOutcome !== 'invalid-result'
|
||||
const expectedAuthority = expected.kernelOutcome === 'no-op' ? 'kernel-no-op' : expected.kernelOutcome === 'accepted' ? 'kernel-superset-only' : 'kernel-invalid-result'
|
||||
if (attempt.operation !== secondOperation || attempt.builder !== expected.secondBuilder || attempt.inputCount !== expected.secondInputCount || attempt.parameter !== expected.mutationParameter || attempt.parameterValue !== expected.mutationTrajectory[attemptIndex] || attempt.runtime !== 'occt-native' || attempt.outcome !== expected.kernelOutcome || attempt.resultProduced !== true || attempt.shapeProduced !== true || attempt.validShapeProduced !== validKernelShape || attempt.historyProduced !== true || attempt.historyProvider !== 'occt-native' || attempt.summary?.isValid !== validKernelShape || attempt.historyRecords < 1 || !attempt.historySha256 || attempt.authority !== expectedAuthority || (expected.kernelOutcome === 'no-op' && JSON.stringify(attempt.summary) !== JSON.stringify(entry.first.summary))) fail(`${expected.pair} OCCT compatibility attempt ${attemptIndex} is invalid.`)
|
||||
const tolerantNoOp = expected.kernelNoOpComparison === 'topology-and-metrics-within-1e-6'
|
||||
const noOpMatchesInput = tolerantNoOp
|
||||
? attempt.noOpComparison === expected.kernelNoOpComparison && summariesMatchWithinTolerance(attempt.summary, entry.first.summary)
|
||||
: JSON.stringify(attempt.summary) === JSON.stringify(entry.first.summary)
|
||||
if (attempt.operation !== secondOperation || attempt.builder !== expected.secondBuilder || attempt.inputCount !== expected.secondInputCount || attempt.parameter !== expected.mutationParameter || attempt.parameterValue !== expected.mutationTrajectory[attemptIndex] || attempt.runtime !== 'occt-native' || attempt.outcome !== expected.kernelOutcome || attempt.resultProduced !== true || attempt.shapeProduced !== true || attempt.validShapeProduced !== validKernelShape || attempt.historyProduced !== true || attempt.historyProvider !== 'occt-native' || attempt.summary?.isValid !== validKernelShape || attempt.historyRecords < 1 || !attempt.historySha256 || attempt.authority !== expectedAuthority || (expected.kernelOutcome === 'no-op' && !noOpMatchesInput)) fail(`${expected.pair} OCCT compatibility attempt ${attemptIndex} is invalid.`)
|
||||
}
|
||||
}
|
||||
const persistence = rejection.persistence
|
||||
const diagnostic = persistence?.diagnostic
|
||||
const freecadProfileAccepted = expected.freecadProfileAccepted === true
|
||||
const freecadProfileNoOp = expected.freecadProfileNoOp === true
|
||||
const persistenceChecksPass = Object.values(persistence?.checks ?? {}).every(Boolean) || (freecadProfileAccepted && persistence?.checks?.nativeFeatureRejected === false && Object.entries(persistence?.checks ?? {}).filter(([key]) => key !== 'nativeFeatureRejected').every(([, value]) => value === true)) || (freecadProfileNoOp && persistence?.checks?.freecadProfileNoOp === true && persistence?.checks?.freecadProfileAccepted === false && persistence?.checks?.nativeFeatureRejected === false && Object.entries(persistence?.checks ?? {}).filter(([key]) => !['nativeFeatureRejected', 'freecadProfileAccepted'].includes(key)).every(([, value]) => value === true)) || (!freecadProfileAccepted && !freecadProfileNoOp && persistence?.checks?.nativeFeatureRejected === true && Object.entries(persistence?.checks ?? {}).filter(([key]) => key !== 'freecadProfileAccepted').every(([, value]) => value === true))
|
||||
const noOpSourceBrepReserialized = expected.rollbackShape === 'geometry-and-summary-exact-with-explicit-brep-reserialization' && persistence?.checks?.sourceShapeRestored === false && persistence?.checks?.sourceShapeSummaryRestored === true && persistence?.checks?.sourceShapeGeometricallyRestored === true && persistence?.rollback?.sourceBrepReserialized === true
|
||||
const persistenceChecksPass = Object.values(persistence?.checks ?? {}).every(Boolean) || (freecadProfileAccepted && persistence?.checks?.nativeFeatureRejected === false && Object.entries(persistence?.checks ?? {}).filter(([key]) => key !== 'nativeFeatureRejected').every(([, value]) => value === true)) || (freecadProfileNoOp && persistence?.checks?.freecadProfileNoOp === true && persistence?.checks?.freecadProfileAccepted === false && persistence?.checks?.nativeFeatureRejected === false && Object.entries(persistence?.checks ?? {}).filter(([key]) => !['nativeFeatureRejected', 'freecadProfileAccepted', ...(noOpSourceBrepReserialized ? ['sourceShapeRestored'] : [])].includes(key)).every(([, value]) => value === true)) || (!freecadProfileAccepted && !freecadProfileNoOp && persistence?.checks?.nativeFeatureRejected === true && Object.entries(persistence?.checks ?? {}).filter(([key]) => key !== 'freecadProfileAccepted').every(([, value]) => value === true))
|
||||
if (persistence?.status !== 'pass' || persistence.decision !== 'rejected' || persistence.pair !== entry.pair || persistence.operation !== secondOperation || persistence.nativeTypeId !== expected.nativeTypeId || persistence.freecadVersion !== '1.1.1' || !persistenceChecksPass) fail(`${expected.pair} FCStd rejection evidence is incomplete.`)
|
||||
if (diagnostic?.nativeTypeId !== expected.nativeTypeId || diagnostic.parameter?.name !== expected.mutationParameter || diagnostic.parameter.value !== expected.mutationTrajectory[0] || diagnostic.inputShape?.solids !== 1 || diagnostic.recomputeResult !== true || !diagnostic.objects?.some(({ name, typeId }) => name === 'PairResult' && typeId === expected.nativeTypeId)) fail(`${expected.pair} did not capture a real FreeCAD profile evaluation.`)
|
||||
if (expected.freecadSelectedEdges && JSON.stringify(diagnostic.input?.selectedEdges) !== JSON.stringify(expected.freecadSelectedEdges)) fail(`${expected.pair} FreeCAD selected-edge fixture changed.`)
|
||||
if (freecadProfileNoOp) {
|
||||
if (diagnostic.shapeNull !== false || diagnostic.shapeValid !== true || diagnostic.statusString !== 'Valid' || !diagnostic.state?.includes('Up-to-date') || JSON.stringify(diagnostic.resultShape) !== JSON.stringify(diagnostic.inputShape)) fail(`${expected.pair} FreeCAD no-op evidence is incomplete.`)
|
||||
} else if (freecadProfileAccepted) {
|
||||
@@ -273,7 +434,14 @@ for (const [index, entry] of report.classifications.entries()) {
|
||||
const input = diagnostic.input
|
||||
if (input?.name !== 'DraftBase' || input.typeId !== 'PartDesign::Feature' || JSON.stringify(input.subelements) !== JSON.stringify(['Face1']) || input.sourceName !== persistence.source?.name || input.shapeSummaryTransferredExactly !== true || typeof input.sourceBrepSha256 !== 'string' || typeof input.draftBaseBrepSha256 !== 'string' || input.sourceBrepSha256 === input.draftBaseBrepSha256) fail(`${expected.pair} PartDesign input Shape transfer evidence is incomplete.`)
|
||||
}
|
||||
if (persistence.source?.shape?.solids !== 1 || !persistence.source.shape.brepSha256 || JSON.stringify(persistence.rollback?.before) !== JSON.stringify(persistence.rollback?.after)) fail(`${expected.pair} abort did not restore the source object set and Shape.`)
|
||||
if (persistence.source?.shape?.solids !== 1 || !persistence.source.shape.brepSha256) fail(`${expected.pair} source Shape evidence is incomplete.`)
|
||||
if (expected.rollbackShape === 'geometry-and-summary-exact-with-explicit-brep-reserialization') {
|
||||
const before = persistence.rollback?.before
|
||||
const after = persistence.rollback?.after
|
||||
const symmetricDifference = persistence.rollback?.sourceShapeSymmetricDifference
|
||||
const withoutBrep = (shape) => Object.fromEntries(Object.entries(shape ?? {}).filter(([key]) => key !== 'brepSha256'))
|
||||
if (JSON.stringify(before?.objects) !== JSON.stringify(after?.objects) || JSON.stringify(withoutBrep(before?.sourceShape)) !== JSON.stringify(withoutBrep(after?.sourceShape)) || before?.sourceShape?.brepSha256 === after?.sourceShape?.brepSha256 || persistence.rollback?.sourceBrepReserialized !== true || persistence.rollback?.sourceShapeTopologicallyEqual !== false || symmetricDifference?.beforeMinusAfterVolume !== 0 || symmetricDifference?.afterMinusBeforeVolume !== 0 || persistence.checks.sourceShapeSummaryRestored !== true || persistence.checks.sourceShapeGeometricallyRestored !== true || persistence.checks.sourceShapeRestored !== false) fail(`${expected.pair} did not classify the source BREP reserialization precisely.`)
|
||||
} else if (JSON.stringify(persistence.rollback?.before) !== JSON.stringify(persistence.rollback?.after)) fail(`${expected.pair} abort did not restore the source object set and Shape.`)
|
||||
const phases = persistence.phases
|
||||
if (JSON.stringify(phases?.initial) !== JSON.stringify(phases?.reopened) || JSON.stringify(phases?.initial) !== JSON.stringify(phases?.resaved) || phases.initial.objects?.length !== persistence.rollback.before.objects.length || phases.initial.objects?.some(({ typeId }) => typeId === 'PartDesign::Body' || typeId === 'PartDesign::Pad' || typeId === 'Part::Loft' || typeId === 'Part::Revolution')) fail(`${expected.pair} polluted the clean FCStd save/reopen/resave chain.`)
|
||||
continue
|
||||
@@ -288,6 +456,6 @@ for (const [index, entry] of report.classifications.entries()) {
|
||||
const phases = entry.persistence.phases
|
||||
if (JSON.stringify(phases?.initial) !== JSON.stringify(phases?.reopened) || JSON.stringify(phases?.initial) !== JSON.stringify(phases?.resaved) || phases.initial.namingEvidenceSha256 !== entry.second.namingEvidenceSha256) fail(`${expected.pair} FCStd phases changed Shape or naming evidence.`)
|
||||
}
|
||||
const expectedSummary = { registeredOperations: 19, orderedPairs: 361, classifiedPairs: 209, accepted: 157, rejected: 52, unknown: 152 }
|
||||
const expectedSummary = { registeredOperations: 19, orderedPairs: 361, classifiedPairs: 357, accepted: 260, rejected: 97, unknown: 4 }
|
||||
if (JSON.stringify(report.summary) !== JSON.stringify(expectedSummary)) fail('summary is inconsistent.')
|
||||
console.log(JSON.stringify({ status: 'freecad-ordered-operation-pair-classification-pass', completedTasks: report.classifications.map(({ taskId }) => taskId), pairs: report.classifications.map(({ pair, classification }) => ({ pair, classification })), nativeBuilderRuns: report.classifications.length * 4, fcstdPhases: report.classifications.length * 3, remainingPairs: report.summary.unknown }, null, 2))
|
||||
|
||||
24
scripts/check-freecad-property-acceleration-failure.mjs
Normal file
24
scripts/check-freecad-property-acceleration-failure.mjs
Normal file
@@ -0,0 +1,24 @@
|
||||
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-acceleration-failure.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyAcceleration failure check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-acceleration-failure' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is invalid')
|
||||
if (report.object?.typeId !== 'Robot::TrajectoryDressUpObject' || report.property?.typeId !== 'App::PropertyAcceleration' || report.initial?.numeric !== 1000 || report.failures?.length !== 2) fail('failure fixture identity or cases changed')
|
||||
const expectedFailures = new Map([
|
||||
['wrong-dimension', { requested: '5 mm', type: 'ArithmeticError', message: 'Not matching Unit!' }],
|
||||
['malformed-text', { requested: 'not-a-quantity', type: 'ParserError', message: 'syntax error' }],
|
||||
])
|
||||
for (const entry of report.failures) {
|
||||
const expected = expectedFailures.get(entry.id)
|
||||
if (!expected || entry.requested !== expected.requested || entry.exception?.type !== expected.type || entry.exception?.message !== expected.message || entry.valuePreserved !== true || entry.objectsPreserved !== true || entry.polluted !== false || JSON.stringify(entry.before) !== JSON.stringify(entry.after) || JSON.stringify(entry.beforeObjects) !== JSON.stringify(entry.afterObjects)) fail(`${entry.id} did not reject without document pollution`)
|
||||
}
|
||||
if (expectedFailures.size !== report.failures.length) fail('failure cases are not the locked set')
|
||||
const disabled = report.disabled
|
||||
if (disabled?.classification !== 'editor-read-only-and-python-immutable' || disabled.requested !== '750 mm/s^2' || disabled.status?.includes('Immutable') !== true || disabled.status.includes('ReadOnly') !== true || JSON.stringify(disabled.editorMode) !== '["ReadOnly"]' || disabled.exception?.type !== 'AttributeError' || disabled.exception.message !== "Object attribute 'Acceleration' is read-only" || disabled.valuePreserved !== true || disabled.objectsPreserved !== true || JSON.stringify(disabled.before) !== JSON.stringify(disabled.after) || JSON.stringify(disabled.beforeObjects) !== JSON.stringify(disabled.afterObjects) || JSON.stringify(disabled.restoredStatus) !== '[]' || JSON.stringify(disabled.restoredEditorMode) !== '[]') fail('disabled property state did not reject and restore cleanly')
|
||||
const transaction = report.transaction
|
||||
if (transaction?.undoMode !== 1 || transaction.pendingAfterEdit !== true || transaction.pendingAfterAbort !== false || transaction.activeAfterEdit?.name !== 'property-acceleration-cancel' || !(transaction.activeAfterEdit.id > 0) || transaction.activeAfterAbort?.name !== '' || transaction.activeAfterAbort?.id !== 0 || transaction.restored !== true || transaction.objectsRestored !== true || transaction.before?.numeric !== 1000 || transaction.edited?.numeric !== 250 || transaction.afterAbort?.numeric !== 1000 || JSON.stringify(transaction.objectsBefore) !== JSON.stringify(transaction.objectsAfter)) fail('transaction abort did not restore the property')
|
||||
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 || report.documentIntegrity.objectCount !== 1 || JSON.stringify(report.documentIntegrity.initialObjects) !== JSON.stringify(report.documentIntegrity.finalObjects)) fail('failure, disabled or cancellation cases polluted the document')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-acceleration-failure-pass', failures: report.failures.map(({ id, exception }) => ({ id, exception })), disabled: { status: disabled.status, editorMode: disabled.editorMode, exception: disabled.exception }, transactionRestored: transaction.restored, documentObjectsPreserved: report.documentIntegrity.objectsPreserved, cancellationBoundary: report.cancellationBoundary }, null, 2))
|
||||
24
scripts/check-freecad-property-acceleration-inventory.mjs
Normal file
24
scripts/check-freecad-property-acceleration-inventory.mjs
Normal file
@@ -0,0 +1,24 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFile, stat } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const sourcePath = resolve(root, '.cache/freecad/reference-desktop.json')
|
||||
const reportPath = resolve(root, 'config/freecad-property-acceleration-inventory.json')
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyAcceleration inventory check failed: ${message}`) }
|
||||
const [sourceContent, reportContent] = await Promise.all([readFile(sourcePath), readFile(reportPath)])
|
||||
const source = JSON.parse(sourceContent)
|
||||
const report = JSON.parse(reportContent)
|
||||
const expectedSourceHash = createHash('sha256').update(sourceContent).digest('hex')
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.propertyType !== 'App::PropertyAcceleration' || report.classification !== 'opaque-fcstd-proxy') fail('report boundary is invalid')
|
||||
if (report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is not locked to FreeCAD 1.1.1')
|
||||
if (report.source?.path !== '.cache/freecad/reference-desktop.json' || report.source.bytes !== sourceContent.length || report.source.sha256 !== expectedSourceHash) fail('source provenance is stale')
|
||||
if (report.recordCount !== 1 || !Array.isArray(report.records) || report.records.length !== 1) fail('record count is not exactly one')
|
||||
const [record] = report.records
|
||||
if (record.objectTypeId !== 'Robot::TrajectoryDressUpObject' || record.objectAvailable !== true || record.probeStatus !== 'available' || record.propertyName !== 'Acceleration' || record.group !== 'TrajectoryDressUp' || JSON.stringify(record.status) !== '[]' || record.defaultRaw !== '1000.0 mm/s^2') fail('native property inventory changed')
|
||||
if (record.valueModel?.kind !== 'quantity' || record.valueModel.dimension !== 'length/time^2' || record.valueModel.defaultValue !== 1000 || record.valueModel.defaultUnit !== 'mm/s^2') fail('quantity value model is invalid')
|
||||
if (!Array.isArray(record.dependencies) || record.dependencies.length !== 0 || record.applicability?.requiredObjectTypeId !== record.objectTypeId) fail('dependencies or applicability are incomplete')
|
||||
const nativeMatches = []
|
||||
for (const objectType of source.runtimeObjects?.types ?? []) for (const property of objectType.properties ?? []) if (property.typeId === 'App::PropertyAcceleration') nativeMatches.push({ objectTypeId: objectType.typeId, propertyName: property.name, group: property.group, status: property.status, defaultRaw: property.default })
|
||||
if (nativeMatches.length !== 1 || JSON.stringify(nativeMatches[0]) !== JSON.stringify({ objectTypeId: record.objectTypeId, propertyName: record.propertyName, group: record.group, status: record.status, defaultRaw: record.defaultRaw })) fail('report does not match the locked runtime oracle')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-acceleration-inventory-pass', propertyType: report.propertyType, recordCount: report.recordCount, objectTypeId: record.objectTypeId, defaultRaw: record.defaultRaw, classification: report.classification }, null, 2))
|
||||
21
scripts/check-freecad-property-acceleration-mutation.mjs
Normal file
21
scripts/check-freecad-property-acceleration-mutation.mjs
Normal file
@@ -0,0 +1,21 @@
|
||||
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-acceleration-mutation.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyAcceleration mutation check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-acceleration-mutation' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is invalid')
|
||||
if (report.object?.name !== 'AccelerationProbe' || report.object.typeId !== 'Robot::TrajectoryDressUpObject' || report.property?.name !== 'Acceleration' || report.property.typeId !== 'App::PropertyAcceleration') fail('native object/property identity changed')
|
||||
if (!(report.recompute?.initial > 0) || !(report.recompute.edit > 0) || !(report.recompute.restore > 0)) fail('initial, edit and restore were not recomputed')
|
||||
if (report.touchedAfterEdit?.value?.numeric !== 500 || JSON.stringify(report.touchedAfterEdit.objectState) !== '["Touched"]' || report.touchedAfterEdit.mustExecute !== false) fail('edit did not preserve the native touched/MustExecute state before recompute')
|
||||
const expectedValues = [['before', 1000], ['edited', 500], ['restored', 1000]]
|
||||
for (const [phase, expected] of expectedValues) {
|
||||
const snapshot = report[phase]
|
||||
if (snapshot?.value?.numeric !== expected || typeof snapshot.value.unit !== 'string' || !snapshot.value.unit.includes('mm/s^2') || JSON.stringify(snapshot.propertyStatus) !== '[]' || JSON.stringify(snapshot.editorMode) !== '[]' || snapshot.source?.name !== 'SourceTrajectory' || snapshot.source.typeId !== 'Robot::TrajectoryObject' || snapshot.useAcceleration !== true || snapshot.shape?.applicable !== false || JSON.stringify(snapshot.objectState) !== '["Up-to-date"]' || snapshot.statusString !== 'Valid' || snapshot.mustExecute !== false) fail(`${phase} native semantic state is invalid`)
|
||||
if (snapshot.trajectory?.waypointCount !== 2 || snapshot.trajectory.waypoints?.length !== 2 || snapshot.trajectory.waypoints.some((waypoint) => waypoint.acceleration !== expected)) fail(`${phase} waypoint acceleration semantics are invalid`)
|
||||
}
|
||||
if (report.before.trajectory.contentSha256 === report.edited.trajectory.contentSha256 || report.before.trajectory.contentSha256 !== report.restored.trajectory.contentSha256) fail('trajectory content did not change and restore exactly')
|
||||
if (report.source?.preserved !== true || JSON.stringify(report.source.before) !== JSON.stringify(report.source.edited) || JSON.stringify(report.source.before) !== JSON.stringify(report.source.restored)) fail('source trajectory changed during dress-up mutation')
|
||||
if (report.document?.objectsPreserved !== true || JSON.stringify(report.document.objectsBefore) !== JSON.stringify(report.document.objectsRestored) || report.document.objectsBefore?.length !== 2) fail('mutation polluted the document object set')
|
||||
if (report.classification?.propertyChanged !== true || report.classification.trajectoryChanged !== true || report.classification.propertyRestored !== true || report.classification.trajectoryRestored !== true || report.classification.semanticStateRestored !== true || report.classification.geometry !== 'not-applicable-no-shape-property') fail('mutation/restore classification is incomplete')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-acceleration-mutation-pass', recompute: report.recompute, values: Object.fromEntries(expectedValues.map(([phase]) => [phase, report[phase].value.numeric])), waypointAccelerations: Object.fromEntries(expectedValues.map(([phase]) => [phase, report[phase].trajectory.waypoints.map((waypoint) => waypoint.acceleration)])), sourcePreserved: report.source.preserved, semanticStateRestored: report.classification.semanticStateRestored, geometry: report.classification.geometry }, null, 2))
|
||||
27
scripts/check-freecad-property-acceleration-promotion.mjs
Normal file
27
scripts/check-freecad-property-acceleration-promotion.mjs
Normal file
@@ -0,0 +1,27 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
|
||||
const [report, semantics, progress, roundTrip, chrome] = await Promise.all([
|
||||
load('config/freecad-property-acceleration-promotion.json'),
|
||||
load('config/freecad-native-property-semantics.json'),
|
||||
load('config/freecad-follow-up-task-progress.json'),
|
||||
load('config/freecad-property-acceleration-roundtrip.json'),
|
||||
load('config/chrome-property-acceleration-verification.json'),
|
||||
])
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyAcceleration promotion check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.taskId !== 'PROP-app-propertyacceleration-I' || report.propertyType !== 'App::PropertyAcceleration' || report.recordCount !== 1 || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.systemExact !== false) fail('promotion report boundary is invalid')
|
||||
const requiredPhases = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
|
||||
const completed = new Map(progress.completedTasks?.map((entry) => [entry.id, entry]) ?? [])
|
||||
for (const phase of requiredPhases) {
|
||||
const taskId = `PROP-app-propertyacceleration-${phase}`
|
||||
if (!completed.has(taskId) || !Array.isArray(report.phaseEvidence?.[phase]) || report.phaseEvidence[phase].length === 0 || JSON.stringify(report.phaseEvidence[phase]) !== JSON.stringify(completed.get(taskId).evidence)) fail(`phase ${phase} evidence is incomplete`)
|
||||
}
|
||||
const propertyType = semantics.types?.find(({ typeId }) => typeId === report.propertyType)
|
||||
if (report.promotion?.from !== 'opaque-fcstd-proxy' || report.promotion.to !== 'native-editable-codec' || propertyType?.support !== report.promotion.to || report.promotion.facadeValueModel !== 'finite-number-mm/s^2' || report.promotion.fcstdElement !== 'Float' || report.promotion.nativeRoundTripValue !== 500 || report.promotion.browserRoundTripValue !== 500 || report.promotion.zeroUnknownDrift !== true) fail('capability promotion is incomplete')
|
||||
const sync = report.exactBlockerSync
|
||||
if (sync?.nativeEditableTypes !== 30 || sync.nativeEditableRecords !== 4361 || sync.opaqueTypes !== 50 || sync.opaqueRecords !== 466 || sync.exactPromotionReady !== false || sync.exactBlocker !== '50 runtime property types and 466 records remain opaque-only; complete native document and property semantics') fail('exact blocker synchronization is stale')
|
||||
if (JSON.stringify(sync) !== JSON.stringify({ nativeEditableTypes: semantics.supportSummary['native-editable-codec'].typeCount, nativeEditableRecords: semantics.supportSummary['native-editable-codec'].recordCount, opaqueTypes: semantics.supportSummary['opaque-fcstd-proxy'].typeCount, opaqueRecords: semantics.supportSummary['opaque-fcstd-proxy'].recordCount, exactPromotionReady: semantics.exactPromotionReady, exactBlocker: semantics.exactBlocker })) fail('promotion report diverges from global property semantics')
|
||||
if (roundTrip.classification?.zeroUnknownDrift !== true || chrome.status !== 'pass' || chrome.persistence?.fcstdElement !== 'Float' || chrome.persistence.loadedValue !== 500 || chrome.resource?.released !== true || chrome.release?.workerTerminated !== true) fail('G/H closure evidence regressed')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-acceleration-promotion-pass', propertyType: report.propertyType, promotion: report.promotion, completedPhases: requiredPhases, exactBlockerSync: sync, systemExact: report.systemExact }, null, 2))
|
||||
16
scripts/check-freecad-property-acceleration-roundtrip.mjs
Normal file
16
scripts/check-freecad-property-acceleration-roundtrip.mjs
Normal file
@@ -0,0 +1,16 @@
|
||||
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-acceleration-roundtrip.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyAcceleration roundtrip check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-acceleration-roundtrip' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is invalid')
|
||||
for (const phase of ['nativeInitial', 'nativeAfter']) {
|
||||
const snapshot = phase === 'nativeInitial' ? report.nativeInitial : report.nativeAfter?.reopened
|
||||
if (snapshot?.object?.typeId !== 'Robot::TrajectoryDressUpObject' || snapshot.object.propertyTypeId !== 'App::PropertyAcceleration' || snapshot.object.source?.typeId !== 'Robot::TrajectoryObject' || snapshot.object.useAcceleration !== true || JSON.stringify(snapshot.object.propertyStatus) !== '[]' || JSON.stringify(snapshot.object.editorMode) !== '[]' || snapshot.object.trajectory?.waypointCount !== 2 || snapshot.sourceTrajectory?.waypointCount !== 2) fail(`${phase} native object semantics are incomplete`)
|
||||
}
|
||||
if (report.nativeInitial.object.value.numeric !== 1000 || report.nativeInitial.object.trajectory.waypoints.some((waypoint) => waypoint.acceleration !== 1000) || report.nativeAfter.reopened.object.value.numeric !== 500 || report.nativeAfter.resaved.object.value.numeric !== 500 || report.nativeAfter.reopened.object.trajectory.waypoints.some((waypoint) => waypoint.acceleration !== 500) || report.nativeAfter.resaved.object.trajectory.waypoints.some((waypoint) => waypoint.acceleration !== 500)) fail('native initial/reopen/resave values drifted')
|
||||
if (report.nativeAfter.reopened.sourceTrajectory.contentSha256 !== report.nativeAfter.resaved.sourceTrajectory.contentSha256 || report.nativeAfter.reopened.object.trajectory.contentSha256 !== report.nativeAfter.resaved.object.trajectory.contentSha256) fail('FreeCAD resave did not preserve native trajectory semantics')
|
||||
if (report.web.before?.typeId !== 'App::PropertyAcceleration' || report.web.before?.element !== 'Float' || Number(report.web.before?.value) !== 1000 || report.web.after?.typeId !== 'App::PropertyAcceleration' || report.web.after?.element !== 'Float' || report.web.after?.value !== '500' || report.web.objectSetPreserved !== true || report.web.semanticObjectsPreserved !== true || report.web.opaqueEntriesPreserved !== true || !Number.isSafeInteger(report.web.opaquePathsPreserved) || report.web.opaquePathsPreserved < 0) fail('Web edit did not preserve native archive semantics')
|
||||
if (report.classification?.webValue !== '500' || report.classification.reopenedValue !== 500 || report.classification.resavedValue !== 500 || report.classification.nativeOutputMatches !== true || report.classification.unknownSemanticDrift !== false || report.classification.zeroUnknownDrift !== true) fail('round-trip classification is not exact for PropertyAcceleration')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-acceleration-roundtrip-pass', values: { nativeInitial: report.nativeInitial.object.value.numeric, webEdited: Number(report.classification.webValue), nativeReopened: report.classification.reopenedValue, nativeResaved: report.classification.resavedValue }, opaquePathsPreserved: report.web.opaquePathsPreserved, semanticObjectsPreserved: report.web.semanticObjectsPreserved, zeroUnknownDrift: report.classification.zeroUnknownDrift }, null, 2))
|
||||
17
scripts/check-freecad-property-acceleration-success.mjs
Normal file
17
scripts/check-freecad-property-acceleration-success.mjs
Normal file
@@ -0,0 +1,17 @@
|
||||
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-acceleration-success.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyAcceleration success check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-acceleration-success' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is invalid')
|
||||
if (report.object?.typeId !== 'Robot::TrajectoryDressUpObject' || report.property?.name !== 'Acceleration' || report.property?.typeId !== 'App::PropertyAcceleration' || report.property?.group !== 'TrajectoryDressUp') fail('native object/property identity changed')
|
||||
if (report.diagnostics?.exception !== null || report.diagnostics?.documentObjectCount !== 1 || report.cases?.length !== 4) fail('native success cases are incomplete')
|
||||
const byId = new Map(report.cases.map((entry) => [entry.id, entry]))
|
||||
for (const id of ['default', 'nominal', 'lower-bound', 'negative-boundary']) if (!byId.has(id)) fail(`missing case ${id}`)
|
||||
for (const entry of report.cases) {
|
||||
if (entry.typeId !== 'App::PropertyAcceleration' || entry.group !== 'TrajectoryDressUp' || JSON.stringify(entry.status) !== '[]' || JSON.stringify(entry.editorMode) !== '[]' || entry.shape?.applicable !== false || !Array.isArray(entry.objectState)) fail(`${entry.id} native metadata is invalid`)
|
||||
if (typeof entry.value?.unit !== 'string' || !entry.value.unit.includes('mm/s^2') || typeof entry.value.numeric !== 'number' || !Number.isFinite(entry.value.numeric)) fail(`${entry.id} quantity result is invalid`)
|
||||
}
|
||||
if (byId.get('default').value.numeric !== 1000 || byId.get('nominal').value.numeric !== 500 || byId.get('lower-bound').value.numeric !== 0 || byId.get('negative-boundary').value.numeric !== -500) fail('nominal or boundary values changed')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-acceleration-success-pass', cases: report.cases.length, nominal: byId.get('nominal').value, boundary: [byId.get('lower-bound').value, byId.get('negative-boundary').value] }, null, 2))
|
||||
44
scripts/check-freecad-property-area-failure.mjs
Normal file
44
scripts/check-freecad-property-area-failure.mjs
Normal file
@@ -0,0 +1,44 @@
|
||||
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-area-failure.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyArea failure check failed: ${message}`) }
|
||||
const same = (left, right) => JSON.stringify(left) === JSON.stringify(right)
|
||||
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-area-failure' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is invalid')
|
||||
if (report.object?.name !== 'AreaProbe' || report.object.typeId !== 'Measure::MeasureArea' || report.property?.name !== 'Area' || report.property.typeId !== 'App::PropertyArea' || report.input?.name !== 'Elements' || report.input.typeId !== 'App::PropertyLinkSubList') fail('native object/property identity changed')
|
||||
if (report.initial?.numeric !== 10 || report.initial.statusString !== 'Valid' || !same(report.initial.state, ['Up-to-date']) || report.failures?.length !== 2) fail('initial state or failure case set changed')
|
||||
|
||||
const expectedFailures = new Map([
|
||||
['unsupported-point', { name: 'AreaPoint', subElement: 'Vertex1', shapeType: 'Vertex' }],
|
||||
['unsupported-line', { name: 'AreaLine', subElement: 'Edge1', shapeType: 'Edge' }],
|
||||
])
|
||||
for (const entry of report.failures) {
|
||||
const expected = expectedFailures.get(entry.id)
|
||||
if (!expected || entry.source?.name !== expected.name || entry.source.typeId !== 'Part::Feature' || entry.source.subElement !== expected.subElement || entry.source.shapeType !== expected.shapeType) fail(`${entry.id} source fixture changed`)
|
||||
if (entry.expectedDiagnostic !== 'Cannot calculate area' || entry.assignmentException !== null || entry.recomputeException !== null || entry.recomputeResult !== true) fail(`${entry.id} native failure phase changed`)
|
||||
if (entry.after?.numeric !== 10 || entry.after.statusString !== 'Cannot calculate area' || !same(entry.after.state, ['Touched', 'Invalid']) || !same(entry.after.elements, [{ object: expected.name, subElements: [expected.subElement] }])) fail(`${entry.id} did not enter the locked invalid state`)
|
||||
if (entry.valuePreserved !== true || entry.objectsPreserved !== true || entry.recoveredExactly !== true || entry.recovered?.numeric !== 10 || entry.recovered.statusString !== 'Valid' || !same(entry.recovered.state, ['Up-to-date']) || !same(entry.recovered.elements, entry.before.elements) || !same(entry.beforeObjects, entry.afterObjects)) fail(`${entry.id} did not recover without document pollution`)
|
||||
}
|
||||
|
||||
const disabled = report.disabled
|
||||
if (disabled?.classification !== 'editor-readonly-python-mutable-until-immutable' || disabled.editorReadOnlyRequested !== '20 mm^2' || disabled.immutableRequested !== '30 mm^2' || !same(disabled.propertyStatus, ['24', '27']) || !same(disabled.editorMode, ['ReadOnly'])) fail('read-only property boundary changed')
|
||||
if (disabled.editorReadOnlyException !== null || disabled.pythonBypassesEditorReadOnly !== true || disabled.editorReadOnlyAfter?.numeric !== 20 || !disabled.immutableStatus?.includes('Immutable') || disabled.immutableException?.type !== 'AttributeError' || disabled.immutableException.message !== "Object attribute 'Area' is read-only") fail('editor read-only and Immutable behavior changed')
|
||||
if (disabled.immutableAfter?.numeric !== 20 || disabled.immutableValuePreserved !== true || disabled.valueRestored !== true || disabled.restored?.numeric !== 10 || disabled.restored.statusString !== 'Valid' || disabled.objectsPreserved !== true || !same(disabled.restoredStatus, ['24', '27']) || !same(disabled.restoredEditorMode, ['ReadOnly'])) fail('disabled state did not restore cleanly')
|
||||
|
||||
const transaction = report.transaction
|
||||
if (transaction?.undoMode !== 1 || transaction.pendingAfterEdit !== true || transaction.pendingAfterAbort !== false || transaction.activeAfterEdit?.name !== 'property-area-cancel' || !(transaction.activeAfterEdit.id > 0) || transaction.activeAfterAbort?.name !== '' || transaction.activeAfterAbort?.id !== 0) fail('transaction lifecycle changed')
|
||||
if (transaction.before?.numeric !== 10 || transaction.edited?.numeric !== 75.398223877 || transaction.afterAbort?.numeric !== 10 || transaction.afterRecompute?.numeric !== 10 || transaction.afterAbort.statusString !== 'Touched' || transaction.afterRecompute.statusString !== 'Valid' || transaction.restored !== true || transaction.objectsRestored !== true || !same(transaction.before.elements, transaction.afterRecompute.elements) || !same(transaction.objectsBefore, transaction.objectsAfter)) fail('transaction abort did not restore Elements and derived Area')
|
||||
|
||||
if (report.cancellationBoundary?.supported !== false || report.cancellationBoundary.classification !== 'synchronous-measure-recompute' || 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 || report.documentIntegrity.objectCount !== 5 || !same(report.documentIntegrity.initialObjects, report.documentIntegrity.finalObjects)) fail('failure, disabled or cancellation cases polluted the document')
|
||||
|
||||
console.log(JSON.stringify({
|
||||
status: 'freecad-property-area-failure-pass',
|
||||
failures: report.failures.map(({ id, after }) => ({ id, state: after.state, diagnostic: after.statusString })),
|
||||
disabled: { editorMode: disabled.editorMode, pythonBypassesEditorReadOnly: disabled.pythonBypassesEditorReadOnly, immutableException: disabled.immutableException },
|
||||
transactionRestored: transaction.restored,
|
||||
documentObjectsPreserved: report.documentIntegrity.objectsPreserved,
|
||||
cancellationBoundary: report.cancellationBoundary,
|
||||
}, null, 2))
|
||||
25
scripts/check-freecad-property-area-inventory.mjs
Normal file
25
scripts/check-freecad-property-area-inventory.mjs
Normal file
@@ -0,0 +1,25 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
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-area-inventory.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyArea inventory check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyArea' || report.recordCount !== 1 || report.classification !== 'opaque-fcstd-proxy') fail('report boundary is invalid')
|
||||
const [record] = report.records ?? []
|
||||
if (record?.objectTypeId !== 'Measure::MeasureArea' || record.objectAvailable !== true || record.probeStatus !== 'available' || record.propertyName !== 'Area' || record.group !== 'Measurement' || JSON.stringify(record.status) !== JSON.stringify([24, 27]) || JSON.stringify(record.normalizedStatus) !== JSON.stringify(['PropReadOnly', 'PropOutput']) || record.defaultRaw !== '0.0 mm^2') fail('native property identity or status changed')
|
||||
if (record.valueModel?.kind !== 'quantity' || record.valueModel.dimension !== 'length^2' || record.valueModel.defaultValue !== 0 || record.valueModel.defaultUnit !== 'mm^2' || record.valueModel.derived !== true || record.valueModel.writable !== false) fail('area value model is invalid')
|
||||
const [input] = record.inputs ?? []
|
||||
if (input?.propertyName !== 'Elements' || input.typeId !== 'App::PropertyLinkSubList' || input.group !== 'Measurement' || JSON.stringify(input.defaultValue) !== '[]' || input.scope !== 'Global' || input.allowExternal !== true || input.cardinality !== 'one-or-more') fail('native input contract is incomplete')
|
||||
const [dependency] = record.dependencies ?? []
|
||||
if (dependency?.sourceProperty !== 'Elements' || dependency.targetProperty !== 'Area' || dependency.relation !== 'link-sub-list' || dependency.effect !== 'immediate-recompute-on-change') fail('dependency contract is incomplete')
|
||||
const applicability = record.applicability
|
||||
if (applicability?.selectionMustBeNonEmpty !== true || applicability.everyElementMustBeValid !== true || JSON.stringify(applicability.supportedMeasureElementTypes) !== JSON.stringify(['PLANE', 'CYLINDER', 'SURFACE', 'VOLUME']) || JSON.stringify(applicability.unsupportedMeasureElementTypes) !== JSON.stringify(['INVALID', 'POINT', 'LINE', 'CURVE']) || applicability.externalDocumentElementsAllowed !== true) fail('selection applicability is incomplete')
|
||||
if (record.execution?.aggregation !== 'sum-area' || record.execution.resultProperty !== 'Area' || record.execution.invalidGeometryDiagnostic !== 'Cannot calculate area' || record.execution.shape !== 'not-applicable-derived-measurement') fail('execution contract is incomplete')
|
||||
for (const locked of [report.provenance?.runtime, ...(report.provenance?.sources ?? [])]) {
|
||||
const content = await readFile(resolve(root, locked?.path ?? ''))
|
||||
if (content.length !== locked.bytes || createHash('sha256').update(content).digest('hex') !== locked.sha256) fail(`provenance is stale for ${locked.path}`)
|
||||
}
|
||||
const sourceText = await readFile(resolve(root, '.cache/freecad/FreeCAD/src/Mod/Measure/App/MeasureArea.cpp'), 'utf8')
|
||||
if (!sourceText.includes('Elements.setScope(App::LinkScope::Global)') || !sourceText.includes('Elements.setAllowExternal(true)') || !sourceText.includes('App::Prop_ReadOnly | App::Prop_Output') || !sourceText.includes('Cannot calculate area')) fail('locked native source no longer exposes the recorded contract')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-area-inventory-pass', propertyType: report.propertyType, objectTypeId: record.objectTypeId, defaultRaw: record.defaultRaw, statusNames: record.normalizedStatus, input: record.inputs[0], applicability: record.applicability, execution: record.execution }, null, 2))
|
||||
49
scripts/check-freecad-property-area-mutation.mjs
Normal file
49
scripts/check-freecad-property-area-mutation.mjs
Normal file
@@ -0,0 +1,49 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
import { isDeepStrictEqual } from 'node:util'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const report = JSON.parse(await readFile(resolve(root, 'config/freecad-property-area-mutation.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyArea mutation check failed: ${message}`) }
|
||||
const same = isDeepStrictEqual
|
||||
const near = (left, right, tolerance = 2e-6) => Math.abs(left - right) <= tolerance
|
||||
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-area-mutation' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is invalid')
|
||||
if (report.object?.name !== 'AreaProbe' || report.object.typeId !== 'Measure::MeasureArea' || report.property?.name !== 'Area' || report.property.typeId !== 'App::PropertyArea' || report.input?.name !== 'Elements' || report.input.typeId !== 'App::PropertyLinkSubList') fail('native object/property identity changed')
|
||||
if (!(report.recompute?.initial > 0) || !(report.recompute.edit > 0) || !(report.recompute.restore > 0)) fail('initial, edit and restore were not recomputed')
|
||||
|
||||
const nominalElements = [{ object: 'AreaBox', typeId: 'Part::Box', subElements: ['Face1'] }]
|
||||
const editedElements = [
|
||||
{ object: 'AreaBox', typeId: 'Part::Box', subElements: ['Face1', 'Face2'] },
|
||||
{ object: 'AreaCylinder', typeId: 'Part::Cylinder', subElements: ['Face1'] },
|
||||
]
|
||||
for (const [phase, expectedValue, expectedElements] of [
|
||||
['before', 10, nominalElements],
|
||||
['edited', 95.398223877, editedElements],
|
||||
['restored', 10, nominalElements],
|
||||
]) {
|
||||
const snapshot = report[phase]
|
||||
if (!near(snapshot?.value?.numeric, expectedValue) || typeof snapshot.value.unit !== 'string' || !snapshot.value.unit.includes('mm^2') || !same(snapshot.elements, expectedElements)) fail(`${phase} value or Elements changed`)
|
||||
if (!same(snapshot.propertyStatus, ['24', '27']) || !same(snapshot.editorMode, ['ReadOnly']) || snapshot.shape?.applicable !== false || !same(snapshot.objectState, ['Up-to-date']) || snapshot.statusString !== 'Valid' || snapshot.mustExecute !== false) fail(`${phase} native semantic state is invalid`)
|
||||
}
|
||||
|
||||
for (const [phase, expectedElements] of [['touchedAfterEdit', editedElements], ['touchedAfterRestore', nominalElements]]) {
|
||||
const snapshot = report[phase]
|
||||
if (!same(snapshot?.elements, expectedElements) || !same(snapshot.objectState, ['Touched']) || snapshot.mustExecute !== false) fail(`${phase} did not preserve the native dirty state before recompute`)
|
||||
}
|
||||
if (!near(report.touchedAfterEdit.value?.numeric, 95.398223877) || report.touchedAfterRestore.value?.numeric !== 10) fail('derived Area was not updated synchronously with Elements')
|
||||
|
||||
if (report.sources?.preserved !== true || !same(report.sources.before, report.sources.edited) || !same(report.sources.before, report.sources.restored) || report.sources.before?.length !== 2) fail('source geometry changed during Elements mutation')
|
||||
for (const source of report.sources.before) if (source.valid !== true || source.shapeType !== 'Solid' || source.solids !== 1 || !(source.faces > 0) || !(source.area > 0) || !(source.volume > 0)) fail(`${source.name} source Shape evidence is invalid`)
|
||||
if (report.document?.objectsPreserved !== true || !same(report.document.objectsBefore, report.document.objectsRestored) || report.document.objectsBefore?.length !== 3) fail('mutation polluted the document object set')
|
||||
if (report.classification?.inputChanged !== true || report.classification.derivedValueChanged !== true || report.classification.inputRestored !== true || report.classification.derivedValueRestored !== true || report.classification.semanticStateRestored !== true || report.classification.sourceGeometryPreserved !== true || report.classification.geometry !== 'source-shapes-preserved-derived-measurement-has-no-shape') fail('mutation/restore classification is incomplete')
|
||||
|
||||
console.log(JSON.stringify({
|
||||
status: 'freecad-property-area-mutation-pass',
|
||||
recompute: report.recompute,
|
||||
values: { before: report.before.value.numeric, edited: report.edited.value.numeric, restored: report.restored.value.numeric },
|
||||
elements: { before: report.before.elements, edited: report.edited.elements, restored: report.restored.elements },
|
||||
sourceGeometryPreserved: report.sources.preserved,
|
||||
semanticStateRestored: report.classification.semanticStateRestored,
|
||||
geometry: report.classification.geometry,
|
||||
}, null, 2))
|
||||
27
scripts/check-freecad-property-area-promotion.mjs
Normal file
27
scripts/check-freecad-property-area-promotion.mjs
Normal file
@@ -0,0 +1,27 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
|
||||
const [report, semantics, progress, roundTrip, chrome] = await Promise.all([
|
||||
load('config/freecad-property-area-promotion.json'),
|
||||
load('config/freecad-native-property-semantics.json'),
|
||||
load('config/freecad-follow-up-task-progress.json'),
|
||||
load('config/freecad-property-area-roundtrip.json'),
|
||||
load('config/chrome-property-area-verification.json'),
|
||||
])
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyArea promotion check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.taskId !== 'PROP-app-propertyarea-I' || report.propertyType !== 'App::PropertyArea' || report.recordCount !== 1 || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.systemExact !== false) fail('promotion report boundary is invalid')
|
||||
const requiredPhases = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
|
||||
const completed = new Map(progress.completedTasks?.map((entry) => [entry.id, entry]) ?? [])
|
||||
for (const phase of requiredPhases) {
|
||||
const taskId = `PROP-app-propertyarea-${phase}`
|
||||
if (!completed.has(taskId) || !Array.isArray(report.phaseEvidence?.[phase]) || report.phaseEvidence[phase].length === 0 || JSON.stringify(report.phaseEvidence[phase]) !== JSON.stringify(completed.get(taskId).evidence)) fail(`phase ${phase} evidence is incomplete`)
|
||||
}
|
||||
const propertyType = semantics.types?.find(({ typeId }) => typeId === report.propertyType)
|
||||
if (report.promotion?.from !== 'opaque-fcstd-proxy' || report.promotion.to !== 'native-editable-codec' || propertyType?.support !== report.promotion.to || propertyType.statusNames?.join(',') !== 'PropOutput,PropReadOnly' || report.promotion.facadeValueModel !== 'read-only-derived-mm^2-with-link-sub-list-input' || report.promotion.inputType !== 'App::PropertyLinkSubList' || report.promotion.fcstdAreaElement !== 'Float' || report.promotion.fcstdElementsElement !== 'LinkSubList' || report.promotion.nativeRoundTripArea !== 20 || report.promotion.browserRoundTripArea !== 0 || report.promotion.browserRoundTripElementCount !== 0 || report.promotion.sourceGeometryPreserved !== true || report.promotion.zeroUnknownDrift !== true) fail('capability promotion is incomplete')
|
||||
const sync = report.exactBlockerSync
|
||||
if (sync?.nativeEditableTypes !== 30 || sync.nativeEditableRecords !== 4361 || sync.opaqueTypes !== 50 || sync.opaqueRecords !== 466 || sync.exactPromotionReady !== false || sync.exactBlocker !== '50 runtime property types and 466 records remain opaque-only; complete native document and property semantics') fail('exact blocker synchronization is stale')
|
||||
if (JSON.stringify(sync) !== JSON.stringify({ nativeEditableTypes: semantics.supportSummary['native-editable-codec'].typeCount, nativeEditableRecords: semantics.supportSummary['native-editable-codec'].recordCount, opaqueTypes: semantics.supportSummary['opaque-fcstd-proxy'].typeCount, opaqueRecords: semantics.supportSummary['opaque-fcstd-proxy'].recordCount, exactPromotionReady: semantics.exactPromotionReady, exactBlocker: semantics.exactBlocker })) fail('promotion report diverges from global property semantics')
|
||||
if (roundTrip.classification?.zeroUnknownDrift !== true || roundTrip.classification.sourceGeometryPreserved !== true || chrome.status !== 'pass' || chrome.ui?.areaReadOnly !== true || chrome.persistence?.fcstdAreaElement !== 'Float' || chrome.persistence.fcstdElementsElement !== 'LinkSubList' || chrome.persistence.loadedArea !== 0 || chrome.persistence.loadedElementCount !== 0 || chrome.resource?.released !== true || chrome.release?.workerTerminated !== true) fail('G/H closure evidence regressed')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-area-promotion-pass', propertyType: report.propertyType, promotion: report.promotion, completedPhases: requiredPhases, exactBlockerSync: sync, systemExact: report.systemExact }, null, 2))
|
||||
30
scripts/check-freecad-property-area-roundtrip.mjs
Normal file
30
scripts/check-freecad-property-area-roundtrip.mjs
Normal file
@@ -0,0 +1,30 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFile, stat } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
import { isDeepStrictEqual } from 'node:util'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const report = JSON.parse(await readFile(resolve(root, 'config/freecad-property-area-roundtrip.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyArea roundtrip check failed: ${message}`) }
|
||||
const same = isDeepStrictEqual
|
||||
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-area-roundtrip' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is invalid')
|
||||
for (const phase of ['nativeInitial', 'nativeReopened', 'nativeResaved']) {
|
||||
const snapshot = phase === 'nativeInitial' ? report.nativeInitial : phase === 'nativeReopened' ? report.nativeAfter?.reopened : report.nativeAfter?.resaved
|
||||
if (!same(snapshot?.objectSet, [{ name: 'AreaBox', typeId: 'Part::Box' }, { name: 'AreaProbe', typeId: 'Measure::MeasureArea' }]) || snapshot.source?.valid !== true || snapshot.source.shapeType !== 'Solid' || snapshot.source.solids !== 1 || snapshot.source.faces !== 6 || snapshot.source.edges !== 12 || snapshot.source.vertices !== 8 || snapshot.source.volume !== 100 || snapshot.source.area !== 160 || !same(snapshot.source.bounds, [0, 0, 0, 10, 5, 2])) fail(`${phase} source geometry is invalid`)
|
||||
if (snapshot.measure?.typeId !== 'Measure::MeasureArea' || snapshot.measure.areaTypeId !== 'App::PropertyArea' || snapshot.measure.elementsTypeId !== 'App::PropertyLinkSubList' || !same(snapshot.measure.propertyStatus, ['24', '27']) || !same(snapshot.measure.editorMode, ['ReadOnly']) || snapshot.measure.shape?.applicable !== false || !same(snapshot.measure.state, ['Up-to-date']) || snapshot.measure.statusString !== 'Valid') fail(`${phase} native MeasureArea semantics are incomplete`)
|
||||
}
|
||||
if (report.nativeInitial.measure.area.numeric !== 10 || !same(report.nativeInitial.measure.elements, [{ object: 'AreaBox', typeId: 'Part::Box', subElements: ['Face1'] }])) fail('native initial measurement changed')
|
||||
for (const snapshot of [report.nativeAfter.reopened, report.nativeAfter.resaved]) if (snapshot.measure.area.numeric !== 20 || !same(snapshot.measure.elements, [{ object: 'AreaBox', typeId: 'Part::Box', subElements: ['Face1', 'Face2'] }])) fail('FreeCAD did not recompute and preserve the Web-edited measurement')
|
||||
if (!same(report.nativeInitial.source, report.nativeAfter.reopened.source) || !same(report.nativeInitial.source, report.nativeAfter.resaved.source)) fail('source Shape changed during measurement round-trip')
|
||||
|
||||
if (report.web.before?.area?.typeId !== 'App::PropertyArea' || report.web.before.area.element !== 'Float' || Number(report.web.before.area.value) !== 10 || report.web.before.elements?.typeId !== 'App::PropertyLinkSubList' || report.web.before.elements.element !== 'LinkSubList' || report.web.before.elements.linkSubs?.length !== 1) fail('Web could not decode the native initial properties')
|
||||
if (report.web.after?.area?.typeId !== 'App::PropertyArea' || report.web.after.area.element !== 'Float' || Number(report.web.after.area.value) !== 20 || report.web.after.elements?.typeId !== 'App::PropertyLinkSubList' || report.web.after.elements.element !== 'LinkSubList' || !same(report.web.after.elements.linkSubs, [{ objectId: 'AreaBox', subElement: 'Face1' }, { objectId: 'AreaBox', subElement: 'Face2' }])) fail('Web edit did not encode the target properties')
|
||||
if (report.web.objectSetPreserved !== true || report.web.semanticObjectsPreserved !== true || report.web.opaqueEntriesPreserved !== true || !Number.isSafeInteger(report.web.opaquePathsPreserved) || report.web.opaquePathsPreserved < 1) fail('Web edit did not preserve native archive semantics and resources')
|
||||
if (report.classification?.webArea !== 20 || report.classification.webElementCount !== 2 || report.classification.reopenedArea !== 20 || report.classification.resavedArea !== 20 || report.classification.nativeOutputMatches !== true || report.classification.sourceGeometryPreserved !== true || report.classification.unknownSemanticDrift !== false || report.classification.zeroUnknownDrift !== true) fail('round-trip classification is not exact for PropertyArea')
|
||||
for (const archive of Object.values(report.archives ?? {})) {
|
||||
const path = resolve(root, archive.path ?? '')
|
||||
const content = await readFile(path)
|
||||
if ((await stat(path)).size !== archive.bytes || createHash('sha256').update(content).digest('hex') !== archive.sha256) fail(`archive is stale: ${archive.path}`)
|
||||
}
|
||||
console.log(JSON.stringify({ status: 'freecad-property-area-roundtrip-pass', values: { nativeInitial: report.nativeInitial.measure.area.numeric, webEdited: report.classification.webArea, nativeReopened: report.classification.reopenedArea, nativeResaved: report.classification.resavedArea }, elements: { initial: 1, edited: report.classification.webElementCount }, opaquePathsPreserved: report.web.opaquePathsPreserved, sourceGeometryPreserved: report.classification.sourceGeometryPreserved, zeroUnknownDrift: report.classification.zeroUnknownDrift }, null, 2))
|
||||
25
scripts/check-freecad-property-area-success.mjs
Normal file
25
scripts/check-freecad-property-area-success.mjs
Normal file
@@ -0,0 +1,25 @@
|
||||
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-area-success.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyArea success check failed: ${message}`) }
|
||||
const near = (left, right, tolerance = 2e-6) => Math.abs(left - right) <= tolerance
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-area-success' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is invalid')
|
||||
if (report.object?.typeId !== 'Measure::MeasureArea' || report.property?.name !== 'Area' || report.property.typeId !== 'App::PropertyArea' || report.property.group !== 'Measurement' || report.input?.name !== 'Elements' || report.input.typeId !== 'App::PropertyLinkSubList' || report.input.group !== 'Measurement') fail('native object/property identity changed')
|
||||
if (report.diagnostics?.exception !== null || report.diagnostics.documentObjectCount !== 4 || report.sources?.length !== 3 || report.cases?.length !== 6) fail('native success cases are incomplete')
|
||||
const expectedIds = ['empty-default', 'planar-face', 'cylindrical-face', 'surface-face', 'solid-volume', 'multi-element-sum']
|
||||
const byId = new Map(report.cases.map((entry) => [entry.id, entry]))
|
||||
for (const id of expectedIds) if (!byId.has(id)) fail(`missing case ${id}`)
|
||||
for (const entry of report.cases) {
|
||||
if (entry.areaTypeId !== 'App::PropertyArea' || entry.elementsTypeId !== 'App::PropertyLinkSubList' || JSON.stringify(entry.propertyStatus) !== JSON.stringify(['24', '27']) || JSON.stringify(entry.editorMode) !== JSON.stringify(['ReadOnly']) || entry.shape?.applicable !== false || entry.object?.typeId !== 'Measure::MeasureArea' || JSON.stringify(entry.object.state) !== JSON.stringify(['Up-to-date']) || entry.object.status !== 'Valid') fail(`${entry.id} native metadata is invalid`)
|
||||
if (typeof entry.value?.numeric !== 'number' || !Number.isFinite(entry.value.numeric) || !String(entry.value.unit).includes('mm^2') || !near(entry.value.numeric, entry.expectedArea)) fail(`${entry.id} area did not match source Shape evidence`)
|
||||
for (const element of entry.elements ?? []) if (element.shapeNull !== false || element.shapeValid !== true || !(element.area > 0) || !['Face', 'Solid'].includes(element.shapeType)) fail(`${entry.id} contains invalid source geometry evidence`)
|
||||
}
|
||||
if (byId.get('empty-default').value.numeric !== 0 || byId.get('empty-default').elements.length !== 0) fail('empty default boundary changed')
|
||||
if (byId.get('planar-face').elements[0]?.shapeType !== 'Face' || !near(byId.get('planar-face').value.numeric, 10)) fail('planar face evidence changed')
|
||||
if (byId.get('cylindrical-face').elements[0]?.shapeType !== 'Face' || !(byId.get('cylindrical-face').value.numeric > 0)) fail('cylindrical face evidence changed')
|
||||
if (byId.get('surface-face').elements[0]?.shapeType !== 'Face' || !(byId.get('surface-face').value.numeric > 0)) fail('surface face evidence changed')
|
||||
if (byId.get('solid-volume').elements[0]?.shapeType !== 'Solid' || !near(byId.get('solid-volume').value.numeric, 160)) fail('solid volume surface-area evidence changed')
|
||||
if (byId.get('multi-element-sum').elements.length !== 3 || !near(byId.get('multi-element-sum').value.numeric, byId.get('multi-element-sum').elements.reduce((total, element) => total + element.area, 0))) fail('multi-element sum evidence changed')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-area-success-pass', cases: expectedIds, values: Object.fromEntries(expectedIds.map((id) => [id, byId.get(id).value.numeric])), propertyStatus: byId.get('planar-face').propertyStatus, shapeApplicable: false }, null, 2))
|
||||
39
scripts/check-freecad-property-boollist-failure.mjs
Normal file
39
scripts/check-freecad-property-boollist-failure.mjs
Normal file
@@ -0,0 +1,39 @@
|
||||
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-boollist-failure.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyBoolList failure check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-boollist-failure' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyBoolList') fail('baseline is invalid')
|
||||
const writable = report.writable
|
||||
if (writable?.object?.typeId !== 'App::FeatureTest' || writable.property?.typeId !== 'App::PropertyBoolList' || !isDeepStrictEqual(writable.initial, [false])) fail('writable fixture identity changed')
|
||||
const accepted = new Map(writable.acceptedCoercions?.map((entry) => [entry.id, entry]) ?? [])
|
||||
if (accepted.size !== 2 || accepted.get('scalar-integer')?.requested !== '7' || accepted.get('scalar-integer').exception !== null || !isDeepStrictEqual(accepted.get('scalar-integer').after, [true]) || accepted.get('bitstring')?.requested !== "'10201'" || accepted.get('bitstring').exception !== null || !isDeepStrictEqual(accepted.get('bitstring').after, [true, false, false, false, true]) || ![...accepted.values()].every((entry) => entry.objectsPreserved === true && isDeepStrictEqual(entry.beforeObjects, entry.afterObjects))) fail('native accepted coercion partition changed')
|
||||
const expectedFailures = new Map([
|
||||
['none', { requested: 'None', message: 'type in list must be bool or int, not NoneType' }],
|
||||
['float-item', { requested: '[True, 1.5]', message: 'type in list must be bool or int, not float' }],
|
||||
['string-item', { requested: "[False, 'bad']", message: 'type in list must be bool or int, not str' }],
|
||||
])
|
||||
if (writable.failures?.length !== expectedFailures.size) fail('failure case count changed')
|
||||
for (const entry of writable.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, [false]) || !isDeepStrictEqual(entry.after, [false]) || !isDeepStrictEqual(entry.beforeObjects, entry.afterObjects)) fail(`${entry.id} did not reject without pollution`)
|
||||
}
|
||||
const disabled = writable.disabled
|
||||
if (disabled?.classification !== 'editor-read-only-and-python-immutable' || !isDeepStrictEqual(disabled.requested, [true, false]) || !isDeepStrictEqual(disabled.status, ['Immutable', 'ReadOnly']) || !isDeepStrictEqual(disabled.editorMode, ['ReadOnly']) || disabled.exception?.type !== 'AttributeError' || disabled.exception.message !== "Object attribute 'BoolList' is read-only" || disabled.valuePreserved !== true || disabled.objectsPreserved !== true || !isDeepStrictEqual(disabled.before, [false]) || !isDeepStrictEqual(disabled.after, [false]) || !isDeepStrictEqual(disabled.restoredStatus, []) || !isDeepStrictEqual(disabled.restoredEditorMode, [])) fail('disabled state did not reject and restore cleanly')
|
||||
const transaction = writable.transaction
|
||||
if (transaction?.undoMode !== 1 || transaction.pendingAfterEdit !== true || transaction.pendingAfterAbort !== false || transaction.activeAfterEdit?.name !== 'property-boollist-cancel' || !(transaction.activeAfterEdit.id > 0) || transaction.activeAfterAbort?.name !== '' || transaction.activeAfterAbort.id !== 0 || transaction.restored !== true || transaction.objectsRestored !== true || !isDeepStrictEqual(transaction.before, [false]) || !isDeepStrictEqual(transaction.edited, [true, false, true]) || !isDeepStrictEqual(transaction.afterAbort, [false]) || !isDeepStrictEqual(transaction.objectsBefore, transaction.objectsAfter)) fail('writable transaction abort did not restore')
|
||||
if (writable.documentIntegrity?.objectsPreserved !== true || !isDeepStrictEqual(writable.documentIntegrity.initialObjects, writable.documentIntegrity.finalObjects)) fail('writable failures polluted the document')
|
||||
|
||||
const linkTypes = ['App::Link', 'App::LinkGroup', 'App::LinkGroupPython', 'App::LinkPython']
|
||||
if (report.links?.length !== linkTypes.length || new Set(report.links.map(({ objectTypeId }) => objectTypeId)).size !== linkTypes.length) fail('Link failure inventory changed')
|
||||
for (const typeId of linkTypes) {
|
||||
const entry = report.links.find((candidate) => candidate.objectTypeId === typeId)
|
||||
if (entry?.propertyTypeId !== 'App::PropertyBoolList' || !isDeepStrictEqual(entry.initial, [true, true]) || !isDeepStrictEqual(entry.status, ['Immutable', 'Hidden', 'LockDynamic']) || !isDeepStrictEqual(entry.editorMode, ['Hidden']) || entry.directWrite?.exception?.type !== 'AttributeError' || entry.directWrite.exception.message !== "Object attribute 'VisibilityList' is read-only" || entry.directWrite.preserved !== true || !isDeepStrictEqual(entry.directWrite.after, [true, true])) fail(`${typeId} immutable rejection changed`)
|
||||
if (entry.invalidElement?.name !== 'MissingElement' || entry.invalidElement.result !== -1 || entry.invalidElement.preserved !== true || !isDeepStrictEqual(entry.invalidElement.after, [true, true])) fail(`${typeId} invalid element rejection changed`)
|
||||
const linkTransaction = entry.transaction
|
||||
if (linkTransaction?.hideResult !== 1 || !isDeepStrictEqual(linkTransaction.before, [true, true]) || !isDeepStrictEqual(linkTransaction.edited, [false, true]) || !isDeepStrictEqual(linkTransaction.afterAbort, [true, true]) || linkTransaction.pendingAfterEdit !== true || linkTransaction.pendingAfterAbort !== false || linkTransaction.activeAfterEdit?.name !== 'property-boollist-link-cancel' || !(linkTransaction.activeAfterEdit.id > 0) || linkTransaction.activeAfterAbort?.name !== '' || linkTransaction.activeAfterAbort.id !== 0 || linkTransaction.restored !== true || !isDeepStrictEqual(linkTransaction.stateAfterAbort, ['Touched']) || linkTransaction.statusAfterAbort !== 'Touched' || linkTransaction.recoveryRecomputeResult !== true || !isDeepStrictEqual(linkTransaction.stateAfterRecovery, ['Up-to-date']) || linkTransaction.statusAfterRecovery !== 'Valid' || !isDeepStrictEqual(entry.objectState, ['Up-to-date']) || entry.statusString !== 'Valid' || entry.objectsPreserved !== true || !isDeepStrictEqual(entry.objectsBefore, entry.objectsAfter)) fail(`${typeId} transaction rollback changed`)
|
||||
}
|
||||
if (report.cancellationBoundary?.supported !== false || report.cancellationBoundary.classification !== 'synchronous-property-setter-and-link-extension' || report.cancellationBoundary.reason !== 'no-native-cancel-hook' || report.cancellationBoundary.replacement !== 'abort-active-document-transaction') fail('cancellation boundary is not explicit')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-boollist-failure-pass', acceptedCoercions: [...accepted.values()].map(({ id, requested, after }) => ({ id, requested, after })), failures: writable.failures.map(({ id, exception }) => ({ id, exception })), disabled: { status: disabled.status, editorMode: disabled.editorMode, exception: disabled.exception }, writableTransactionRestored: transaction.restored, linkTransactionsRestored: report.links.every(({ transaction }) => transaction.restored), cancellationBoundary: report.cancellationBoundary }, null, 2))
|
||||
32
scripts/check-freecad-property-boollist-inventory.mjs
Normal file
32
scripts/check-freecad-property-boollist-inventory.mjs
Normal file
@@ -0,0 +1,32 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const sourcePath = resolve(root, '.cache/freecad/reference-desktop.json')
|
||||
const reportPath = resolve(root, 'config/freecad-property-boollist-inventory.json')
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyBoolList inventory check failed: ${message}`) }
|
||||
const [sourceContent, reportContent] = await Promise.all([readFile(sourcePath), readFile(reportPath)])
|
||||
const source = JSON.parse(sourceContent)
|
||||
const report = JSON.parse(reportContent)
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.propertyType !== 'App::PropertyBoolList' || report.classification !== 'opaque-fcstd-proxy') fail('report boundary is invalid')
|
||||
if (report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is not locked to FreeCAD 1.1.1')
|
||||
if (report.source?.path !== '.cache/freecad/reference-desktop.json' || report.source.bytes !== sourceContent.length || report.source.sha256 !== createHash('sha256').update(sourceContent).digest('hex')) fail('source provenance is stale')
|
||||
if (report.recordCount !== 7 || !Array.isArray(report.records) || report.records.length !== 7) fail('record count is not exactly seven')
|
||||
const expected = [
|
||||
['App::FeatureTest', 'BoolList', '', [], [false]],
|
||||
['App::FeatureTestException', 'BoolList', '', [], [false]],
|
||||
['App::Link', 'VisibilityList', ' Link', ['Immutable', 'Hidden', 'LockDynamic'], []],
|
||||
['App::LinkGroup', 'VisibilityList', ' Link', ['Immutable', 'Hidden', 'LockDynamic'], []],
|
||||
['App::LinkGroupPython', 'VisibilityList', ' Link', ['Immutable', 'Hidden', 'LockDynamic'], []],
|
||||
['App::LinkPython', 'VisibilityList', ' Link', ['Immutable', 'Hidden', 'LockDynamic'], []],
|
||||
['Surface::GeomFillSurface', 'ReversedList', '', [], [false]],
|
||||
]
|
||||
for (const [objectTypeId, propertyName, group, status, defaultValue] of expected) {
|
||||
const record = report.records.find((candidate) => candidate.objectTypeId === objectTypeId && candidate.propertyName === propertyName)
|
||||
if (!record || record.objectAvailable !== true || record.probeStatus !== 'available' || record.group !== group || JSON.stringify(record.status) !== JSON.stringify(status) || JSON.stringify(record.defaultValue) !== JSON.stringify(defaultValue) || record.valueModel?.kind !== 'boolean-list' || record.valueModel.elementType !== 'bool' || record.valueModel.cardinality !== 'variable' || !Array.isArray(record.dependencies) || record.dependencies.length !== 0 || record.applicability?.requiredObjectTypeId !== record.objectTypeId) fail(`native inventory changed for ${objectTypeId}.${propertyName}`)
|
||||
}
|
||||
const nativeMatches = []
|
||||
for (const objectType of source.runtimeObjects?.types ?? []) for (const property of objectType.properties ?? []) if (property.typeId === 'App::PropertyBoolList') nativeMatches.push({ objectTypeId: objectType.typeId, propertyName: property.name, group: property.group, status: property.status, defaultValue: property.default })
|
||||
if (nativeMatches.length !== 7) fail('locked runtime oracle no longer contains seven BoolList records')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-boollist-inventory-pass', propertyType: report.propertyType, recordCount: report.recordCount, writableRecords: report.records.filter(({ valueModel }) => valueModel.writable).length, immutableRecords: report.records.filter(({ valueModel }) => !valueModel.writable).length, classification: report.classification }, null, 2))
|
||||
42
scripts/check-freecad-property-boollist-mutation.mjs
Normal file
42
scripts/check-freecad-property-boollist-mutation.mjs
Normal file
@@ -0,0 +1,42 @@
|
||||
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-boollist-mutation.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyBoolList mutation check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-boollist-mutation' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyBoolList') fail('baseline is invalid')
|
||||
if (report.caseCount !== 7 || report.cases?.length !== 7 || new Set(report.cases.map(({ objectTypeId }) => objectTypeId)).size !== 7) fail('mutation case inventory is incomplete')
|
||||
const byType = new Map(report.cases.map((entry) => [entry.objectTypeId, entry]))
|
||||
for (const entry of report.cases) {
|
||||
if (entry.valueRestored !== true || entry.shapeRestored !== true || entry.objectsRestored !== true || entry.editRecompute !== true || entry.restoreRecompute !== true || !isDeepStrictEqual(entry.before.objectSet, entry.restored.objectSet)) fail(`${entry.objectTypeId} did not restore value, shape and objects`)
|
||||
for (const snapshot of [entry.before, entry.touchedAfterEdit, entry.edited, entry.touchedAfterRestore, entry.restored]) if (snapshot.propertyTypeId !== 'App::PropertyBoolList') fail(`${entry.objectTypeId} property TypeId changed`)
|
||||
}
|
||||
|
||||
for (const typeId of ['App::FeatureTest', 'App::FeatureTestException']) {
|
||||
const entry = byType.get(typeId)
|
||||
if (entry?.mode !== 'direct-native-property-setter' || entry.propertyName !== 'BoolList' || !isDeepStrictEqual(entry.before.value, [false]) || !isDeepStrictEqual(entry.touchedAfterEdit.value, [true, false, true]) || !isDeepStrictEqual(entry.edited.value, [true, false, true]) || !isDeepStrictEqual(entry.touchedAfterRestore.value, [false]) || !isDeepStrictEqual(entry.restored.value, [false])) fail(`${typeId} mutation values changed`)
|
||||
for (const snapshot of [entry.before, entry.touchedAfterEdit, entry.edited, entry.touchedAfterRestore, entry.restored]) if (!isDeepStrictEqual(snapshot.propertyStatus, []) || !isDeepStrictEqual(snapshot.editorMode, []) || snapshot.shape?.applicable !== false || snapshot.mustExecute !== false || snapshot.objectSet?.length !== 1) fail(`${typeId} mutation metadata changed`)
|
||||
}
|
||||
const feature = byType.get('App::FeatureTest')
|
||||
if (feature.hostExecution !== 'normal' || !isDeepStrictEqual(feature.before.objectState, ['Up-to-date']) || !isDeepStrictEqual(feature.touchedAfterEdit.objectState, ['Touched']) || !isDeepStrictEqual(feature.edited.objectState, ['Up-to-date']) || !isDeepStrictEqual(feature.touchedAfterRestore.objectState, ['Touched']) || !isDeepStrictEqual(feature.restored.objectState, ['Up-to-date']) || feature.before.statusString !== 'Valid' || feature.edited.statusString !== 'Valid' || feature.restored.statusString !== 'Valid') fail('FeatureTest state trajectory changed')
|
||||
const exception = byType.get('App::FeatureTestException')
|
||||
if (exception.hostExecution !== 'intrinsic-test-exception' || !isDeepStrictEqual(exception.before.objectState, ['Up-to-date']) || !isDeepStrictEqual(exception.touchedAfterEdit.objectState, ['Touched']) || !isDeepStrictEqual(exception.edited.objectState, ['Touched', 'Invalid']) || !isDeepStrictEqual(exception.touchedAfterRestore.objectState, ['Touched', 'Invalid']) || !isDeepStrictEqual(exception.restored.objectState, ['Touched', 'Invalid']) || exception.edited.statusString !== 'FeatureTestException::execute(): Testexception ;-)' || exception.restored.statusString !== exception.edited.statusString) fail('FeatureTestException intrinsic trajectory changed')
|
||||
|
||||
const surface = byType.get('Surface::GeomFillSurface')
|
||||
if (surface?.mode !== 'direct-native-property-setter' || surface.propertyName !== 'ReversedList' || !isDeepStrictEqual(surface.before.value, [false]) || !isDeepStrictEqual(surface.edited.value, [true, false, false, false]) || !isDeepStrictEqual(surface.restored.value, [false])) fail('Surface mutation values changed')
|
||||
for (const snapshot of [surface.before, surface.touchedAfterEdit, surface.edited, surface.touchedAfterRestore, surface.restored]) {
|
||||
const shape = snapshot.shape
|
||||
if (!isDeepStrictEqual(snapshot.propertyStatus, []) || !isDeepStrictEqual(snapshot.editorMode, []) || shape?.applicable !== true || shape.isNull !== false || shape.valid !== true || shape.shapeType !== 'Face' || shape.faces !== 1 || shape.edges !== 4 || shape.vertices !== 4 || shape.area !== 100 || shape.volume !== 0 || !isDeepStrictEqual(shape.bounds, [0, 0, 0, 10, 10, 0]) || snapshot.objectSet?.length !== 5) fail('Surface geometry evidence changed')
|
||||
}
|
||||
if (surface.before.shape.brepSha256 === surface.edited.shape.brepSha256 || surface.before.shape.brepSha256 !== surface.restored.shape.brepSha256 || !isDeepStrictEqual(surface.before.objectState, ['Up-to-date']) || !isDeepStrictEqual(surface.touchedAfterEdit.objectState, ['Touched']) || surface.touchedAfterEdit.mustExecute !== true || !isDeepStrictEqual(surface.edited.objectState, ['Up-to-date']) || !isDeepStrictEqual(surface.touchedAfterRestore.objectState, ['Touched']) || surface.touchedAfterRestore.mustExecute !== true || !isDeepStrictEqual(surface.restored.objectState, ['Up-to-date'])) fail('Surface reversal did not mutate and restore exactly')
|
||||
|
||||
for (const typeId of ['App::Link', 'App::LinkGroup', 'App::LinkGroupPython', 'App::LinkPython']) {
|
||||
const entry = byType.get(typeId)
|
||||
const pythonLink = typeId.endsWith('Python')
|
||||
const expectedSetup = typeId === 'App::Link' || typeId === 'App::LinkPython' ? 'LinkedObject' : 'ElementList'
|
||||
if (entry?.mode !== 'link-base-extension-element-visibility' || entry.propertyName !== 'VisibilityList' || entry.setupProperty !== expectedSetup || entry.hideResult !== 1 || entry.showResult !== 1 || !isDeepStrictEqual(entry.before.value, [true, true]) || !isDeepStrictEqual(entry.touchedAfterEdit.value, [false, true]) || !isDeepStrictEqual(entry.edited.value, [false, true]) || !isDeepStrictEqual(entry.touchedAfterRestore.value, [true, true]) || !isDeepStrictEqual(entry.restored.value, [true, true])) fail(`${typeId} mutation values changed`)
|
||||
for (const snapshot of [entry.before, entry.touchedAfterEdit, entry.edited, entry.touchedAfterRestore, entry.restored]) if (!isDeepStrictEqual(snapshot.propertyStatus, ['Immutable', 'Hidden', 'LockDynamic']) || !isDeepStrictEqual(snapshot.editorMode, ['Hidden']) || snapshot.shape?.applicable !== false || snapshot.objectSet?.length !== 4) fail(`${typeId} mutation metadata changed`)
|
||||
if (!isDeepStrictEqual(entry.before.objectState, ['Up-to-date']) || !isDeepStrictEqual(entry.touchedAfterEdit.objectState, ['Touched']) || entry.touchedAfterEdit.mustExecute !== pythonLink || !isDeepStrictEqual(entry.edited.objectState, ['Up-to-date']) || !isDeepStrictEqual(entry.touchedAfterRestore.objectState, ['Touched']) || entry.touchedAfterRestore.mustExecute !== pythonLink || !isDeepStrictEqual(entry.restored.objectState, ['Up-to-date']) || entry.before.statusString !== 'Valid' || entry.edited.statusString !== 'Valid' || entry.restored.statusString !== 'Valid') fail(`${typeId} state trajectory changed`)
|
||||
}
|
||||
console.log(JSON.stringify({ status: 'freecad-property-boollist-mutation-pass', cases: report.caseCount, surface: { beforeBrep: surface.before.shape.brepSha256, editedBrep: surface.edited.shape.brepSha256, restoredBrep: surface.restored.shape.brepSha256, restoredExactly: surface.shapeRestored }, linkVisibilityRestored: ['App::Link', 'App::LinkGroup', 'App::LinkGroupPython', 'App::LinkPython'].every((typeId) => byType.get(typeId).valueRestored), intrinsicHostDiagnostic: exception.hostExecution }, null, 2))
|
||||
29
scripts/check-freecad-property-boollist-promotion.mjs
Normal file
29
scripts/check-freecad-property-boollist-promotion.mjs
Normal file
@@ -0,0 +1,29 @@
|
||||
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 load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
|
||||
const [report, semantics, progress, roundTrip, chrome] = await Promise.all([
|
||||
load('config/freecad-property-boollist-promotion.json'),
|
||||
load('config/freecad-native-property-semantics.json'),
|
||||
load('config/freecad-follow-up-task-progress.json'),
|
||||
load('config/freecad-property-boollist-roundtrip.json'),
|
||||
load('config/chrome-property-boollist-verification.json'),
|
||||
])
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyBoolList promotion check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.taskId !== 'PROP-app-propertyboollist-I' || report.propertyType !== 'App::PropertyBoolList' || report.recordCount !== 7 || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.systemExact !== false) fail('promotion report boundary is invalid')
|
||||
const requiredPhases = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
|
||||
const completed = new Map(progress.completedTasks?.map((entry) => [entry.id, entry]) ?? [])
|
||||
for (const phase of requiredPhases) {
|
||||
const taskId = `PROP-app-propertyboollist-${phase}`
|
||||
if (!completed.has(taskId) || !Array.isArray(report.phaseEvidence?.[phase]) || report.phaseEvidence[phase].length === 0 || !isDeepStrictEqual(report.phaseEvidence[phase], completed.get(taskId).evidence)) fail(`phase ${phase} evidence is incomplete`)
|
||||
}
|
||||
const target = [true, false, true, false]
|
||||
const propertyType = semantics.types?.find(({ typeId }) => typeId === report.propertyType)
|
||||
if (report.promotion?.from !== 'opaque-fcstd-proxy' || report.promotion.to !== 'native-editable-codec' || propertyType?.support !== report.promotion.to || propertyType.statusNames?.join(',') !== 'Hidden,Immutable,LockDynamic' || report.promotion.facadeValueModel !== 'variable-boolean-array' || report.promotion.writableRecords !== 3 || report.promotion.immutableHiddenRecords !== 4 || report.promotion.fcstdElement !== 'BoolList' || !isDeepStrictEqual(report.promotion.nativeRoundTripValue, target) || !isDeepStrictEqual(report.promotion.browserRoundTripValue, target) || report.promotion.zeroUnknownDrift !== true) fail('capability promotion is incomplete')
|
||||
const sync = report.exactBlockerSync
|
||||
if (sync?.nativeEditableTypes !== 30 || sync.nativeEditableRecords !== 4361 || sync.opaqueTypes !== 50 || sync.opaqueRecords !== 466 || sync.exactPromotionReady !== false || sync.exactBlocker !== '50 runtime property types and 466 records remain opaque-only; complete native document and property semantics') fail('exact blocker synchronization is stale')
|
||||
if (!isDeepStrictEqual(sync, { nativeEditableTypes: semantics.supportSummary['native-editable-codec'].typeCount, nativeEditableRecords: semantics.supportSummary['native-editable-codec'].recordCount, opaqueTypes: semantics.supportSummary['opaque-fcstd-proxy'].typeCount, opaqueRecords: semantics.supportSummary['opaque-fcstd-proxy'].recordCount, exactPromotionReady: semantics.exactPromotionReady, exactBlocker: semantics.exactBlocker })) fail('promotion report diverges from global property semantics')
|
||||
if (roundTrip.classification?.zeroUnknownDrift !== true || !isDeepStrictEqual(roundTrip.classification.resavedValue, target) || chrome.status !== 'pass' || chrome.persistence?.fcstdElement !== 'BoolList' || !isDeepStrictEqual(chrome.persistence.loadedValue, target) || chrome.resource?.released !== true || chrome.release?.workerTerminated !== true) fail('G/H closure evidence regressed')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-boollist-promotion-pass', propertyType: report.propertyType, promotion: report.promotion, completedPhases: requiredPhases, exactBlockerSync: sync, systemExact: report.systemExact }, null, 2))
|
||||
17
scripts/check-freecad-property-boollist-roundtrip.mjs
Normal file
17
scripts/check-freecad-property-boollist-roundtrip.mjs
Normal file
@@ -0,0 +1,17 @@
|
||||
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-boollist-roundtrip.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyBoolList roundtrip check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-boollist-roundtrip' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is invalid')
|
||||
const target = [true, false, true, false]
|
||||
for (const [phase, snapshot] of [['nativeInitial', report.nativeInitial], ['nativeReopened', report.nativeAfter?.reopened], ['nativeResaved', report.nativeAfter?.resaved]]) {
|
||||
if (snapshot?.object?.typeId !== 'App::FeatureTest' || snapshot.object.propertyTypeId !== 'App::PropertyBoolList' || !isDeepStrictEqual(snapshot.object.propertyStatus, []) || !isDeepStrictEqual(snapshot.object.editorMode, []) || snapshot.object.hasShapeProperty !== false || !isDeepStrictEqual(snapshot.objectSet, [{ name: 'BoolListProbe', typeId: 'App::FeatureTest' }])) fail(`${phase} native object semantics are incomplete`)
|
||||
}
|
||||
if (!isDeepStrictEqual(report.nativeInitial.object.value, [false]) || !isDeepStrictEqual(report.nativeAfter.reopened.object.value, target) || !isDeepStrictEqual(report.nativeAfter.resaved.object.value, target)) fail('native initial/reopen/resave values drifted')
|
||||
if (report.web.before?.typeId !== 'App::PropertyBoolList' || report.web.before?.element !== 'BoolList' || report.web.before?.value !== '[false]' || report.web.after?.typeId !== 'App::PropertyBoolList' || report.web.after?.element !== 'BoolList' || report.web.after?.value !== '[true,false,true,false]' || report.web.objectSetPreserved !== true || report.web.semanticObjectsPreserved !== true || report.web.opaqueEntriesPreserved !== true || !Number.isSafeInteger(report.web.opaquePathsPreserved) || report.web.opaquePathsPreserved < 0) fail('Web edit did not preserve native archive semantics')
|
||||
if (report.classification?.webValue !== '[true,false,true,false]' || !isDeepStrictEqual(report.classification.reopenedValue, target) || !isDeepStrictEqual(report.classification.resavedValue, target) || report.classification.nativeOutputMatches !== true || report.classification.unknownSemanticDrift !== false || report.classification.zeroUnknownDrift !== true) fail('round-trip classification is not exact for PropertyBoolList')
|
||||
for (const archive of Object.values(report.archives ?? {})) if (!archive?.path || !Number.isSafeInteger(archive.bytes) || archive.bytes <= 0 || !/^[0-9a-f]{64}$/.test(archive.sha256)) fail('archive evidence is incomplete')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-boollist-roundtrip-pass', values: { nativeInitial: report.nativeInitial.object.value, webEdited: JSON.parse(report.classification.webValue), nativeReopened: report.classification.reopenedValue, nativeResaved: report.classification.resavedValue }, opaquePathsPreserved: report.web.opaquePathsPreserved, semanticObjectsPreserved: report.web.semanticObjectsPreserved, zeroUnknownDrift: report.classification.zeroUnknownDrift }, null, 2))
|
||||
41
scripts/check-freecad-property-boollist-success.mjs
Normal file
41
scripts/check-freecad-property-boollist-success.mjs
Normal file
@@ -0,0 +1,41 @@
|
||||
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-boollist-success.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyBoolList success check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-boollist-success' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyBoolList') fail('baseline is invalid')
|
||||
if (report.caseCount !== 7 || !Array.isArray(report.cases) || report.cases.length !== 7 || new Set(report.cases.map(({ objectTypeId }) => objectTypeId)).size !== 7) fail('native success case inventory is incomplete')
|
||||
const byType = new Map(report.cases.map((entry) => [entry.objectTypeId, entry]))
|
||||
const directTypes = ['App::FeatureTest', 'App::FeatureTestException']
|
||||
for (const typeId of directTypes) {
|
||||
const entry = byType.get(typeId)
|
||||
if (entry?.mode !== 'direct-native-property-setter' || entry.propertyName !== 'BoolList' || entry.propertyAssignmentsAccepted !== true || !isDeepStrictEqual(entry.defaultValue, [false])) fail(`${typeId} setter contract is invalid`)
|
||||
const expected = { default: [false], empty: [], singleton: [true], mixed: [true, false, true, false], restored: [false] }
|
||||
for (const [phase, value] of Object.entries(expected)) if (!isDeepStrictEqual(entry.phases?.[phase]?.value, value)) fail(`${typeId} ${phase} value changed`)
|
||||
const large = entry.phases?.large
|
||||
if (large?.value?.length !== 257 || !large.value.every((value, index) => value === (index % 3 === 0))) fail(`${typeId} large boundary changed`)
|
||||
for (const snapshot of Object.values(entry.phases)) if (snapshot.propertyTypeId !== 'App::PropertyBoolList' || !isDeepStrictEqual(snapshot.propertyStatus, []) || !isDeepStrictEqual(snapshot.editorMode, []) || snapshot.shape?.applicable !== false || snapshot.objectSet?.length !== 1 || snapshot.objectSet[0].typeId !== typeId) fail(`${typeId} native metadata is invalid`)
|
||||
if (typeId === 'App::FeatureTestException') {
|
||||
if (entry.hostExecution !== 'intrinsic-test-exception' || !Object.values(entry.phases).slice(1).every((snapshot) => isDeepStrictEqual(snapshot.objectState, ['Touched', 'Invalid']) && snapshot.statusString === 'FeatureTestException::execute(): Testexception ;-)')) fail('FeatureTestException intrinsic host diagnostic changed')
|
||||
} else if (entry.hostExecution !== 'normal' || !Object.values(entry.phases).every((snapshot) => isDeepStrictEqual(snapshot.objectState, ['Up-to-date']) && snapshot.statusString === 'Valid')) fail('FeatureTest host state changed')
|
||||
}
|
||||
|
||||
const surface = byType.get('Surface::GeomFillSurface')
|
||||
if (surface?.mode !== 'direct-native-property-setter' || surface.propertyName !== 'ReversedList' || surface.propertyAssignmentsAccepted !== true || surface.hostExecution !== 'normal') fail('Surface BoolList contract is invalid')
|
||||
const surfaceValues = { default: [false], allFalse: [false, false, false, false], oneReversed: [true, false, false, false], alternating: [true, false, true, false], allReversed: [true, true, true, true], restored: [false] }
|
||||
for (const [phase, value] of Object.entries(surfaceValues)) {
|
||||
const snapshot = surface.phases?.[phase]
|
||||
if (!isDeepStrictEqual(snapshot?.value, value) || snapshot.propertyTypeId !== 'App::PropertyBoolList' || !isDeepStrictEqual(snapshot.propertyStatus, []) || !isDeepStrictEqual(snapshot.editorMode, []) || !isDeepStrictEqual(snapshot.objectState, ['Up-to-date']) || snapshot.statusString !== 'Valid' || snapshot.shape?.applicable !== true || snapshot.shape.isNull !== false || snapshot.shape.valid !== true || snapshot.shape.shapeType !== 'Face' || snapshot.shape.faces !== 1 || snapshot.shape.edges !== 4 || snapshot.shape.vertices !== 4 || snapshot.objectSet?.length !== 5) fail(`Surface ${phase} success evidence changed`)
|
||||
}
|
||||
|
||||
const linkTypes = ['App::Link', 'App::LinkGroup', 'App::LinkGroupPython', 'App::LinkPython']
|
||||
for (const typeId of linkTypes) {
|
||||
const entry = byType.get(typeId)
|
||||
const expectedSetup = typeId === 'App::Link' || typeId === 'App::LinkPython' ? 'LinkedObject' : 'ElementList'
|
||||
if (entry?.mode !== 'link-base-extension-element-visibility' || entry.propertyName !== 'VisibilityList' || entry.propertyAssignmentsAccepted !== true || entry.hostExecution !== 'normal' || entry.setupProperty !== expectedSetup || entry.hideResult !== 1 || entry.showResult !== 1) fail(`${typeId} LinkBaseExtension contract changed`)
|
||||
if (!isDeepStrictEqual(entry.phases?.linked?.value, [true, true]) || !isDeepStrictEqual(entry.phases?.hidden?.value, [false, true]) || !isDeepStrictEqual(entry.phases?.restored?.value, [true, true])) fail(`${typeId} visibility values changed`)
|
||||
for (const snapshot of Object.values(entry.phases)) if (snapshot.propertyTypeId !== 'App::PropertyBoolList' || !isDeepStrictEqual(snapshot.propertyStatus, ['Immutable', 'Hidden', 'LockDynamic']) || !isDeepStrictEqual(snapshot.editorMode, ['Hidden']) || !isDeepStrictEqual(snapshot.objectState, ['Up-to-date']) || snapshot.statusString !== 'Valid' || snapshot.shape?.applicable !== false || snapshot.objectSet?.length !== 4 || snapshot.objectSet[3].typeId !== typeId) fail(`${typeId} native metadata is invalid`)
|
||||
}
|
||||
console.log(JSON.stringify({ status: 'freecad-property-boollist-success-pass', cases: report.caseCount, directBoundaries: { empty: 0, singleton: 1, mixed: 4, large: 257 }, surface: { valid: true, shapeType: 'Face', faces: 1 }, linkVisibility: { linked: [true, true], hidden: [false, true], restored: [true, true] }, intrinsicHostDiagnostic: byType.get('App::FeatureTestException').hostExecution }, null, 2))
|
||||
50
scripts/check-freecad-property-color-failure.mjs
Normal file
50
scripts/check-freecad-property-color-failure.mjs
Normal file
@@ -0,0 +1,50 @@
|
||||
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))
|
||||
50
scripts/check-freecad-property-color-inventory.mjs
Normal file
50
scripts/check-freecad-property-color-inventory.mjs
Normal file
@@ -0,0 +1,50 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const runtimePath = resolve(root, '.cache/freecad/reference-desktop.json')
|
||||
const reportPath = resolve(root, 'config/freecad-property-color-inventory.json')
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyColor inventory check failed: ${message}`) }
|
||||
const [runtimeContent, reportContent] = await Promise.all([readFile(runtimePath), readFile(reportPath)])
|
||||
const runtime = JSON.parse(runtimeContent)
|
||||
const report = JSON.parse(reportContent)
|
||||
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.propertyType !== 'App::PropertyColor' || report.classification !== 'opaque-fcstd-proxy') fail('report boundary is invalid')
|
||||
if (report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is not locked to FreeCAD 1.1.1')
|
||||
if (report.recordCount !== 8 || !Array.isArray(report.records) || report.records.length !== 8) fail('record count is not exactly eight')
|
||||
if (report.provenance?.runtime?.path !== '.cache/freecad/reference-desktop.json' || report.provenance.runtime.bytes !== runtimeContent.length || report.provenance.runtime.sha256 !== createHash('sha256').update(runtimeContent).digest('hex')) fail('runtime provenance is stale')
|
||||
if (report.storage?.xmlElement !== 'PropertyColor' || report.storage.attribute !== 'value' || report.storage.encoding !== 'unsigned-decimal-packed-0xRRGGBBAA' || report.storage.legacyBeforeVersion !== '1.1' || report.storage.legacyConversion !== 'invert-alpha-byte') fail('native storage contract is incomplete')
|
||||
|
||||
const expected = [
|
||||
['App::FeatureTest', 'Colour', '', [0, 0, 0, 1]],
|
||||
['App::FeatureTestException', 'Colour', '', [0, 0, 0, 1]],
|
||||
['App::Part', 'Color', '', [1, 1, 1, 0]],
|
||||
['Assembly::AssemblyLink', 'Color', '', [1, 1, 1, 0]],
|
||||
['Assembly::AssemblyObject', 'Color', '', [1, 1, 1, 0]],
|
||||
['TechDraw::DrawViewAnnotation', 'TextColor', 'Annotation', [0, 0, 0, 1]],
|
||||
['TechDraw::DrawViewDraft', 'Color', 'Draft view', [0, 0, 0, 1]],
|
||||
['TechDraw::DrawViewSpreadsheet', 'TextColor', 'Spreadsheet', [0, 0, 0, 1]],
|
||||
]
|
||||
for (const [objectTypeId, propertyName, group, defaultValue] of expected) {
|
||||
const record = report.records.find((candidate) => candidate.objectTypeId === objectTypeId && candidate.propertyName === propertyName)
|
||||
if (!record || record.objectAvailable !== true || record.probeStatus !== 'available' || record.group !== group || record.status?.length !== 0 || JSON.stringify(record.defaultValue) !== JSON.stringify(defaultValue)) fail(`native inventory changed for ${objectTypeId}.${propertyName}`)
|
||||
const model = record.valueModel
|
||||
if (model?.kind !== 'rgba-color' || model.channels?.join(',') !== 'red,green,blue,alpha' || model.canonicalRepresentation !== 'four-channel-float-tuple' || model.packedRepresentation !== '0xRRGGBBAA' || model.alphaSemantics !== '1-opaque-0-transparent' || model.writable !== true) fail(`value model changed for ${objectTypeId}.${propertyName}`)
|
||||
const [floats, integers, packed] = record.inputs ?? []
|
||||
if (floats?.kind !== 'float-tuple' || JSON.stringify(floats.cardinality) !== '[3,4]' || floats.componentsMustHaveSameType !== true || floats.omittedAlpha !== 1 || integers?.kind !== 'integer-tuple' || JSON.stringify(integers.cardinality) !== '[3,4]' || integers.componentsMustHaveSameType !== true || integers.normalizationDivisor !== 255 || integers.omittedAlpha !== 1 || packed?.kind !== 'packed-integer' || packed.encoding !== '0xRRGGBBAA') fail(`setter inputs changed for ${objectTypeId}.${propertyName}`)
|
||||
if (!Array.isArray(record.dependencies) || record.dependencies.length !== 0 || record.applicability?.requiredObjectTypeId !== objectTypeId || record.applicability.source !== 'Document.supportedTypes runtime inventory' || record.applicability.shapeRequired !== false) fail(`dependencies or applicability changed for ${objectTypeId}.${propertyName}`)
|
||||
}
|
||||
|
||||
const nativeMatches = []
|
||||
for (const objectType of runtime.runtimeObjects?.types ?? []) for (const property of objectType.properties ?? []) if (property.typeId === 'App::PropertyColor') nativeMatches.push({ objectTypeId: objectType.typeId, propertyName: property.name, group: property.group, status: property.status, defaultValue: property.default })
|
||||
if (nativeMatches.length !== 8) fail('locked runtime oracle no longer contains eight PropertyColor records')
|
||||
for (const locked of report.provenance?.sources ?? []) {
|
||||
const content = await readFile(resolve(root, locked?.path ?? ''))
|
||||
if (content.length !== locked.bytes || createHash('sha256').update(content).digest('hex') !== locked.sha256) fail(`source provenance is stale for ${locked.path}`)
|
||||
}
|
||||
const propertySource = await readFile(resolve(root, '.cache/freecad/FreeCAD/src/App/PropertyStandard.cpp'), 'utf8')
|
||||
const colorHeader = await readFile(resolve(root, '.cache/freecad/FreeCAD/src/Base/Color.h'), 'utf8')
|
||||
if (!propertySource.includes('PyTuple_Size(value) == 3 || PyTuple_Size(value) == 4') || !propertySource.includes('cCol.r = PyLong_AsLong(item) / 255.0') || !propertySource.includes('cCol.setPackedValue(PyLong_AsUnsignedLong(value))') || !propertySource.includes('<PropertyColor value=') || !propertySource.includes('alphaMax - (rgba & alphaMax)')) fail('locked PropertyColor source no longer exposes the recorded setter/storage contract')
|
||||
if (!colorHeader.includes('0xRRGGBBAA') || !colorHeader.includes('A defines the alpha value, 1 means fully opaque and 0 transparent')) fail('locked Base::Color source no longer exposes the recorded channel contract')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-color-inventory-pass', propertyType: report.propertyType, recordCount: report.recordCount, writableRecords: report.records.filter(({ valueModel }) => valueModel.writable).length, defaults: [...new Set(report.records.map(({ defaultValue }) => JSON.stringify(defaultValue)))], classification: report.classification }, null, 2))
|
||||
42
scripts/check-freecad-property-color-mutation.mjs
Normal file
42
scripts/check-freecad-property-color-mutation.mjs
Normal file
@@ -0,0 +1,42 @@
|
||||
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-mutation.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyColor mutation check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-color-mutation' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyColor') fail('baseline is invalid')
|
||||
if (report.caseCount !== 8 || !Array.isArray(report.cases) || report.cases.length !== 8 || new Set(report.cases.map(({ objectTypeId }) => objectTypeId)).size !== 8) fail('mutation case inventory is incomplete')
|
||||
const target = [0.125, 0.375, 0.625, 0.875]
|
||||
const expectedHosts = [
|
||||
['App::FeatureTest', 'Colour', [0, 0, 0, 1], 1],
|
||||
['App::FeatureTestException', 'Colour', [0, 0, 0, 1], 1],
|
||||
['App::Part', 'Color', [1, 1, 1, 0], 9],
|
||||
['Assembly::AssemblyLink', 'Color', [1, 1, 1, 0], 9],
|
||||
['Assembly::AssemblyObject', 'Color', [1, 1, 1, 0], 9],
|
||||
['TechDraw::DrawViewAnnotation', 'TextColor', [0, 0, 0, 1], 1],
|
||||
['TechDraw::DrawViewDraft', 'Color', [0, 0, 0, 1], 1],
|
||||
['TechDraw::DrawViewSpreadsheet', 'TextColor', [0, 0, 0, 1], 2],
|
||||
]
|
||||
const byType = new Map(report.cases.map((entry) => [entry.objectTypeId, entry]))
|
||||
for (const [typeId, propertyName, original, objectCount] of expectedHosts) {
|
||||
const entry = byType.get(typeId)
|
||||
if (entry?.propertyName !== propertyName || !isDeepStrictEqual(entry.target, target) || entry.editRecomputeResult !== true || entry.restoreRecomputeResult !== true || entry.mutationDetected !== true || entry.restoredExactly !== true) fail(`${typeId} mutation boundary changed`)
|
||||
if (typeId === 'TechDraw::DrawViewSpreadsheet') {
|
||||
if (entry.setup?.kind !== 'spreadsheet-source' || entry.setup.objectTypeId !== 'Spreadsheet::Sheet' || entry.setup.objectName !== 'MutationSource') fail('Spreadsheet mutation setup changed')
|
||||
} else if (entry.setup?.kind !== 'none') fail(`${typeId} has unexpected mutation setup`)
|
||||
if (!isDeepStrictEqual(entry.before?.value, original) || !isDeepStrictEqual(entry.touchedAfterEdit?.value, target) || !isDeepStrictEqual(entry.edited?.value, target) || !isDeepStrictEqual(entry.touchedAfterRestore?.value, original) || !isDeepStrictEqual(entry.restored?.value, original)) fail(`${typeId} mutation values changed`)
|
||||
const objectSet = entry.before.objectSet
|
||||
if (objectSet?.length !== objectCount || !objectSet.some(({ typeId: candidateType }) => candidateType === typeId)) fail(`${typeId} mutation object set changed`)
|
||||
for (const phase of ['before', 'touchedAfterEdit', 'edited', 'touchedAfterRestore', 'restored']) {
|
||||
const snapshot = entry[phase]
|
||||
if (snapshot.propertyTypeId !== 'App::PropertyColor' || !isDeepStrictEqual(snapshot.propertyStatus, []) || !isDeepStrictEqual(snapshot.editorMode, []) || snapshot.shape?.applicable !== false || !isDeepStrictEqual(snapshot.objectSet, objectSet)) fail(`${typeId} ${phase} native metadata changed`)
|
||||
}
|
||||
if (typeId === 'App::FeatureTestException') {
|
||||
if (entry.hostExecution !== 'intrinsic-test-exception' || !isDeepStrictEqual(entry.before.objectState, ['Up-to-date']) || entry.before.statusString !== 'Valid' || !isDeepStrictEqual(entry.touchedAfterEdit.objectState, ['Touched']) || entry.touchedAfterEdit.statusString !== 'Touched') fail('FeatureTestException initial mutation sequence changed')
|
||||
for (const phase of ['edited', 'touchedAfterRestore', 'restored']) if (!isDeepStrictEqual(entry[phase].objectState, ['Touched', 'Invalid']) || entry[phase].statusString !== 'FeatureTestException::execute(): Testexception ;-)') fail(`FeatureTestException ${phase} intrinsic diagnostic changed`)
|
||||
} else {
|
||||
if (entry.hostExecution !== 'normal' || !isDeepStrictEqual(entry.before.objectState, ['Up-to-date']) || entry.before.statusString !== 'Valid' || !isDeepStrictEqual(entry.touchedAfterEdit.objectState, ['Touched']) || entry.touchedAfterEdit.statusString !== 'Touched' || !isDeepStrictEqual(entry.edited.objectState, ['Up-to-date']) || entry.edited.statusString !== 'Valid' || !isDeepStrictEqual(entry.touchedAfterRestore.objectState, ['Touched']) || entry.touchedAfterRestore.statusString !== 'Touched' || !isDeepStrictEqual(entry.restored.objectState, ['Up-to-date']) || entry.restored.statusString !== 'Valid') fail(`${typeId} mutation state sequence changed`)
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify({ status: 'freecad-property-color-mutation-pass', cases: report.caseCount, target, mutated: report.cases.filter(({ mutationDetected }) => mutationDetected).length, restoredExactly: report.cases.filter(({ restoredExactly }) => restoredExactly).length, normalStateSequence: ['Valid', 'Touched', 'Valid', 'Touched', 'Valid'], intrinsicHostDiagnostic: 'App::FeatureTestException' }, null, 2))
|
||||
30
scripts/check-freecad-property-color-promotion.mjs
Normal file
30
scripts/check-freecad-property-color-promotion.mjs
Normal file
@@ -0,0 +1,30 @@
|
||||
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 load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
|
||||
const [report, semantics, progress, roundTrip, chrome] = await Promise.all([
|
||||
load('config/freecad-property-color-promotion.json'),
|
||||
load('config/freecad-native-property-semantics.json'),
|
||||
load('config/freecad-follow-up-task-progress.json'),
|
||||
load('config/freecad-property-color-roundtrip.json'),
|
||||
load('config/chrome-property-color-verification.json'),
|
||||
])
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyColor promotion check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.taskId !== 'PROP-app-propertycolor-I' || report.propertyType !== 'App::PropertyColor' || report.recordCount !== 8 || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.systemExact !== false) fail('promotion report boundary is invalid')
|
||||
const requiredPhases = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
|
||||
const completed = new Map(progress.completedTasks?.map((entry) => [entry.id, entry]) ?? [])
|
||||
for (const phase of requiredPhases) {
|
||||
const taskId = `PROP-app-propertycolor-${phase}`
|
||||
if (!completed.has(taskId) || !Array.isArray(report.phaseEvidence?.[phase]) || report.phaseEvidence[phase].length === 0 || !isDeepStrictEqual(report.phaseEvidence[phase], completed.get(taskId).evidence)) fail(`phase ${phase} evidence is incomplete`)
|
||||
}
|
||||
const target = [0.2, 0.4, 0.6, 0.8]
|
||||
const approximatelyTarget = (value) => Array.isArray(value) && value.length === target.length && value.every((channel, index) => typeof channel === 'number' && Math.abs(channel - target[index]) <= 1e-7)
|
||||
const propertyType = semantics.types?.find(({ typeId }) => typeId === report.propertyType)
|
||||
if (report.promotion?.from !== 'opaque-fcstd-proxy' || report.promotion.to !== 'native-editable-codec' || propertyType?.support !== report.promotion.to || propertyType.statusNames?.length !== 0 || report.promotion.facadeValueModel !== 'four-finite-rgba-channels-with-8-bit-fcstd-packing' || report.promotion.writableRecords !== 8 || report.promotion.fcstdElement !== 'PropertyColor' || report.promotion.fcstdPackedValue !== '862362060' || !approximatelyTarget(report.promotion.nativeRoundTripValue) || !isDeepStrictEqual(report.promotion.browserRoundTripValue, target) || report.promotion.zeroUnknownDrift !== true) fail('capability promotion is incomplete')
|
||||
const sync = report.exactBlockerSync
|
||||
if (sync?.nativeEditableTypes !== 30 || sync.nativeEditableRecords !== 4361 || sync.opaqueTypes !== 50 || sync.opaqueRecords !== 466 || sync.exactPromotionReady !== false || sync.exactBlocker !== '50 runtime property types and 466 records remain opaque-only; complete native document and property semantics') fail('exact blocker synchronization is stale')
|
||||
if (!isDeepStrictEqual(sync, { nativeEditableTypes: semantics.supportSummary['native-editable-codec'].typeCount, nativeEditableRecords: semantics.supportSummary['native-editable-codec'].recordCount, opaqueTypes: semantics.supportSummary['opaque-fcstd-proxy'].typeCount, opaqueRecords: semantics.supportSummary['opaque-fcstd-proxy'].recordCount, exactPromotionReady: semantics.exactPromotionReady, exactBlocker: semantics.exactBlocker })) fail('promotion report diverges from global property semantics')
|
||||
if (roundTrip.classification?.zeroUnknownDrift !== true || !approximatelyTarget(roundTrip.classification.resavedValue) || chrome.status !== 'pass' || chrome.persistence?.fcstdElement !== 'PropertyColor' || chrome.persistence.fcstdValue !== '862362060' || !isDeepStrictEqual(chrome.persistence.loadedValue, target) || chrome.resource?.released !== true || chrome.release?.workerTerminated !== true) fail('G/H closure evidence regressed')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-color-promotion-pass', propertyType: report.propertyType, promotion: report.promotion, completedPhases: requiredPhases, exactBlockerSync: sync, systemExact: report.systemExact }, null, 2))
|
||||
18
scripts/check-freecad-property-color-roundtrip.mjs
Normal file
18
scripts/check-freecad-property-color-roundtrip.mjs
Normal file
@@ -0,0 +1,18 @@
|
||||
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-roundtrip.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyColor roundtrip check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-color-roundtrip' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is invalid')
|
||||
const target = [0.2, 0.4, 0.6, 0.8]
|
||||
const approximatelyTarget = (value) => Array.isArray(value) && value.length === target.length && value.every((channel, index) => typeof channel === 'number' && Math.abs(channel - target[index]) <= 1e-7)
|
||||
for (const [phase, snapshot] of [['nativeInitial', report.nativeInitial], ['nativeReopened', report.nativeAfter?.reopened], ['nativeResaved', report.nativeAfter?.resaved]]) {
|
||||
if (snapshot?.object?.typeId !== 'App::FeatureTest' || snapshot.object.propertyTypeId !== 'App::PropertyColor' || !isDeepStrictEqual(snapshot.object.propertyStatus, []) || !isDeepStrictEqual(snapshot.object.editorMode, []) || snapshot.object.hasShapeProperty !== false || !isDeepStrictEqual(snapshot.objectSet, [{ name: 'ColorProbe', typeId: 'App::FeatureTest' }])) fail(`${phase} native object semantics are incomplete`)
|
||||
}
|
||||
if (!approximatelyTarget(report.nativeAfter.reopened.object.value) || !approximatelyTarget(report.nativeAfter.resaved.object.value) || !isDeepStrictEqual(report.nativeAfter.reopened.object.value, report.nativeAfter.resaved.object.value)) fail('native reopen/resave values drifted')
|
||||
if (report.web.before?.typeId !== 'App::PropertyColor' || report.web.before?.element !== 'PropertyColor' || report.web.before?.value !== '287454020' || report.web.after?.typeId !== 'App::PropertyColor' || report.web.after?.element !== 'PropertyColor' || report.web.after?.value !== '862362060' || report.web.objectSetPreserved !== true || report.web.semanticObjectsPreserved !== true || report.web.opaqueEntriesPreserved !== true || !Number.isSafeInteger(report.web.opaquePathsPreserved) || report.web.opaquePathsPreserved < 0) fail('Web edit did not preserve native archive semantics')
|
||||
if (!isDeepStrictEqual(report.classification?.requestedValue, target) || report.classification.webValue !== '862362060' || !approximatelyTarget(report.classification.reopenedValue) || !approximatelyTarget(report.classification.resavedValue) || report.classification.nativeOutputMatches !== true || report.classification.unknownSemanticDrift !== false || report.classification.zeroUnknownDrift !== true) fail('round-trip classification is not exact for PropertyColor')
|
||||
for (const archive of Object.values(report.archives ?? {})) if (!archive?.path || !Number.isSafeInteger(archive.bytes) || archive.bytes <= 0 || !/^[0-9a-f]{64}$/.test(archive.sha256)) fail('archive evidence is incomplete')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-color-roundtrip-pass', values: { nativeInitial: report.nativeInitial.object.value, webEditedPacked: report.classification.webValue, nativeReopened: report.classification.reopenedValue, nativeResaved: report.classification.resavedValue }, opaquePathsPreserved: report.web.opaquePathsPreserved, semanticObjectsPreserved: report.web.semanticObjectsPreserved, zeroUnknownDrift: report.classification.zeroUnknownDrift }, null, 2))
|
||||
59
scripts/check-freecad-property-color-success.mjs
Normal file
59
scripts/check-freecad-property-color-success.mjs
Normal file
@@ -0,0 +1,59 @@
|
||||
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-success.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyColor success check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-color-success' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyColor') fail('baseline is invalid')
|
||||
if (report.caseCount !== 8 || !Array.isArray(report.cases) || report.cases.length !== 8 || new Set(report.cases.map(({ objectTypeId }) => objectTypeId)).size !== 8) fail('native success case inventory is incomplete')
|
||||
|
||||
const expectedHosts = [
|
||||
['App::FeatureTest', 'Colour', [0, 0, 0, 1], 1],
|
||||
['App::FeatureTestException', 'Colour', [0, 0, 0, 1], 1],
|
||||
['App::Part', 'Color', [1, 1, 1, 0], 9],
|
||||
['Assembly::AssemblyLink', 'Color', [1, 1, 1, 0], 9],
|
||||
['Assembly::AssemblyObject', 'Color', [1, 1, 1, 0], 9],
|
||||
['TechDraw::DrawViewAnnotation', 'TextColor', [0, 0, 0, 1], 1],
|
||||
['TechDraw::DrawViewDraft', 'Color', [0, 0, 0, 1], 1],
|
||||
['TechDraw::DrawViewSpreadsheet', 'TextColor', [0, 0, 0, 1], 2],
|
||||
]
|
||||
const normalized = {
|
||||
floatRgb: [0.25, 0.5, 0.75, 1],
|
||||
floatRgba: [0.100000001, 0.200000003, 0.300000012, 0.400000006],
|
||||
byteRgb: [0.003921569, 0.498039216, 1, 1],
|
||||
byteRgba: [1, 0.501960814, 0, 0.250980407],
|
||||
packedRgba: [0.06666667, 0.13333334, 0.200000003, 0.266666681],
|
||||
transparentBoundary: [0, 0, 0, 0],
|
||||
}
|
||||
const requested = {
|
||||
floatRgb: [0.25, 0.5, 0.75],
|
||||
floatRgba: [0.1, 0.2, 0.3, 0.4],
|
||||
byteRgb: [1, 127, 255],
|
||||
byteRgba: [255, 128, 0, 64],
|
||||
packedRgba: 0x11223344,
|
||||
transparentBoundary: [0, 0, 0, 0],
|
||||
}
|
||||
const byType = new Map(report.cases.map((entry) => [entry.objectTypeId, entry]))
|
||||
for (const [typeId, propertyName, defaultValue, objectCount] of expectedHosts) {
|
||||
const entry = byType.get(typeId)
|
||||
if (entry?.propertyName !== propertyName || entry.mode !== 'direct-native-property-setter' || entry.propertyAssignmentsAccepted !== true || !isDeepStrictEqual(entry.defaultValue, defaultValue) || !isDeepStrictEqual(entry.requested, requested)) fail(`${typeId} setter contract is invalid`)
|
||||
if (typeId === 'TechDraw::DrawViewSpreadsheet') {
|
||||
if (entry.setup?.kind !== 'spreadsheet-source' || entry.setup.objectTypeId !== 'Spreadsheet::Sheet' || entry.setup.objectName !== 'ColorSource') fail('Spreadsheet host setup is incomplete')
|
||||
} else if (entry.setup?.kind !== 'none') fail(`${typeId} has unexpected host setup`)
|
||||
if (!isDeepStrictEqual(entry.phases?.default?.value, defaultValue) || !isDeepStrictEqual(entry.phases?.restored?.value, defaultValue)) fail(`${typeId} default or restored value changed`)
|
||||
for (const [phase, value] of Object.entries(normalized)) if (!isDeepStrictEqual(entry.phases?.[phase]?.value, value)) fail(`${typeId} ${phase} normalization changed`)
|
||||
const objectSet = entry.phases?.default?.objectSet
|
||||
if (!Array.isArray(objectSet) || objectSet.length !== objectCount || !objectSet.some(({ typeId: candidateType }) => candidateType === typeId)) fail(`${typeId} host object set is invalid`)
|
||||
for (const snapshot of Object.values(entry.phases ?? {})) {
|
||||
if (snapshot.propertyTypeId !== 'App::PropertyColor' || !isDeepStrictEqual(snapshot.propertyStatus, []) || !isDeepStrictEqual(snapshot.editorMode, []) || snapshot.shape?.applicable !== false || typeof snapshot.recomputeResult !== 'boolean' || !isDeepStrictEqual(snapshot.objectSet, objectSet)) fail(`${typeId} native metadata changed`)
|
||||
}
|
||||
if (typeId === 'App::FeatureTestException') {
|
||||
if (entry.hostExecution !== 'intrinsic-test-exception' || !isDeepStrictEqual(entry.phases.default.objectState, ['Up-to-date']) || entry.phases.default.statusString !== 'Valid') fail('FeatureTestException initial state changed')
|
||||
for (const [phase, snapshot] of Object.entries(entry.phases)) if (phase !== 'default' && (!isDeepStrictEqual(snapshot.objectState, ['Touched', 'Invalid']) || snapshot.statusString !== 'FeatureTestException::execute(): Testexception ;-)')) fail(`FeatureTestException ${phase} intrinsic diagnostic changed`)
|
||||
} else {
|
||||
if (entry.hostExecution !== 'normal') fail(`${typeId} host execution classification changed`)
|
||||
for (const snapshot of Object.values(entry.phases)) if (!isDeepStrictEqual(snapshot.objectState, ['Up-to-date']) || snapshot.statusString !== 'Valid') fail(`${typeId} success state changed`)
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify({ status: 'freecad-property-color-success-pass', cases: report.caseCount, acceptedInputs: Object.keys(requested), normalized, normalHosts: 7, intrinsicHostDiagnostic: 'App::FeatureTestException' }, null, 2))
|
||||
58
scripts/check-freecad-property-colorlist-failure.mjs
Normal file
58
scripts/check-freecad-property-colorlist-failure.mjs
Normal file
@@ -0,0 +1,58 @@
|
||||
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-colorlist-failure.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyColorList failure check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-colorlist-failure' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyColorList') fail('baseline is invalid')
|
||||
const baseline = [[0.06666667, 0.13333334, 0.200000003, 0.266666681], [1, 1, 1, 1]]
|
||||
if (report.object?.name !== 'ColorListProbe' || report.object.typeId !== 'App::FeatureTest' || report.property?.name !== 'ColourList' || report.property.typeId !== 'App::PropertyColorList' || !isDeepStrictEqual(report.initial, baseline)) fail('native fixture identity changed')
|
||||
|
||||
const expectedCoercions = new Map([
|
||||
['boolean-scalar', { requested: 'True', after: [[0, 0, 0, 0.003921569]] }],
|
||||
['bytes-sequence', { requested: "b'\\x01\\xff'", after: [[0, 0, 0, 0.003921569], [0, 0, 0, 1]] }],
|
||||
['bare-float-tuple', { requested: '(0.1, 0.2, 0.3, 0.4)', after: [[0.100000001, 0.200000003, 0.300000012, 0.400000006]] }],
|
||||
['bare-integer-tuple', { requested: '(255, 128, 0, 64)', after: [[1, 0.501960814, 0, 0.250980407]] }],
|
||||
['tuple-packed-sequence', { requested: '(287454020, 4294967295)', after: baseline }],
|
||||
['empty-dictionary', { requested: '{}', after: [] }],
|
||||
])
|
||||
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 || !isDeepStrictEqual(entry.before, baseline) || !isDeepStrictEqual(entry.after, expected.after) || entry.valuePreserved !== isDeepStrictEqual(baseline, expected.after) || entry.objectsPreserved !== true || !isDeepStrictEqual(entry.beforeObjects, entry.afterObjects)) fail(`${entry.id} accepted coercion changed`)
|
||||
}
|
||||
|
||||
const expectedFailures = new Map([
|
||||
['none', { requested: 'None', type: 'TypeError', message: 'type must be integer or tuple of float or tuple integer, not NoneType' }],
|
||||
['bare-float-list', { requested: '[0.1, 0.2, 0.3, 0.4]', type: 'TypeError', message: 'type must be integer or tuple of float or tuple integer, not float' }],
|
||||
['bare-mixed-tuple', { requested: '(0.1, 2, 0.3)', type: 'TypeError', message: 'type must be integer or tuple of float or tuple integer, not float' }],
|
||||
['nested-short-tuple', { requested: '[(0.1, 0.2)]', type: 'TypeError', message: 'type must be integer or tuple of float or tuple integer, not tuple' }],
|
||||
['mixed-valid-none', { requested: '[(0.1, 0.2, 0.3, 0.4), None]', type: 'TypeError', message: 'type must be integer or tuple of float or tuple integer, not NoneType' }],
|
||||
['scalar-float', { requested: '0.5', type: 'TypeError', message: 'type must be integer or tuple of float or tuple integer, not float' }],
|
||||
['string-sequence', { requested: "'rgb'", type: 'TypeError', message: 'type must be integer or tuple of float or tuple integer, not str' }],
|
||||
['nested-list', { requested: '[[0.1, 0.2, 0.3, 0.4]]', type: 'TypeError', message: 'type must be integer or tuple of float or tuple integer, not list' }],
|
||||
['dictionary-string-key', { requested: "{'0': 287454020}", type: 'TypeError', message: 'expect key type to be integer' }],
|
||||
['dictionary-high-index', { requested: '{3: 287454020}', type: 'ValueError', message: 'index out of bound' }],
|
||||
['dictionary-negative-index', { requested: '{-2: 287454020}', type: 'ValueError', message: 'index out of bound' }],
|
||||
['dictionary-partial-invalid', { requested: '{0: 16909060, 1: None}', type: 'TypeError', message: 'type must be integer or tuple of float or tuple integer, not NoneType', after: [[0.003921569, 0.007843138, 0.011764706, 0.015686275], [1, 1, 1, 1]] }],
|
||||
])
|
||||
if (report.failures?.length !== expectedFailures.size) fail('failure case count changed')
|
||||
for (const entry of report.failures) {
|
||||
const expected = expectedFailures.get(entry.id)
|
||||
const expectedAfter = expected?.after ?? baseline
|
||||
if (!expected || entry.requested !== expected.requested || entry.exception?.type !== expected.type || entry.exception.message !== expected.message || !isDeepStrictEqual(entry.before, baseline) || !isDeepStrictEqual(entry.after, expectedAfter) || entry.valuePreserved !== isDeepStrictEqual(baseline, expectedAfter) || entry.objectsPreserved !== true || entry.polluted !== false || !isDeepStrictEqual(entry.beforeObjects, entry.afterObjects)) fail(`${entry.id} failure behavior changed`)
|
||||
}
|
||||
if (report.failures.filter(({ valuePreserved }) => !valuePreserved).map(({ id }) => id).join(',') !== 'dictionary-partial-invalid') fail('native partial-mutation boundary changed')
|
||||
|
||||
const disabled = report.disabled
|
||||
if (disabled?.classification !== 'editor-readonly-python-mutable-until-immutable' || !isDeepStrictEqual(disabled.before, baseline) || !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 'ColourList' is read-only" || !isDeepStrictEqual(disabled.immutableAfter, baseline) || disabled.immutableValuePreserved !== true || disabled.objectsPreserved !== true || !isDeepStrictEqual(disabled.restoredStatus, []) || !isDeepStrictEqual(disabled.restoredEditorMode, [])) fail('Immutable rejection or status restoration changed')
|
||||
|
||||
const transaction = report.transaction
|
||||
const edited = [[0.75, 0.5, 0.25, 0.125], baseline[0], [0, 1, 0, 0.501960814]]
|
||||
if (transaction?.undoMode !== 1 || transaction.pendingAfterEdit !== true || transaction.pendingAfterAbort !== false || transaction.activeAfterEdit?.name !== 'property-colorlist-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, baseline) || !isDeepStrictEqual(transaction.edited, edited) || !isDeepStrictEqual(transaction.afterAbort, baseline) || !isDeepStrictEqual(transaction.afterRecompute, baseline) || !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 object set')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-colorlist-failure-pass', acceptedCoercions: report.coercions.map(({ id, after }) => ({ id, after })), rejectedCases: report.failures.map(({ id, exception, valuePreserved }) => ({ id, exception, valuePreserved })), partialMutation: report.failures.find(({ id }) => id === 'dictionary-partial-invalid'), disabled: { pythonBypassesEditorReadOnly: disabled.pythonBypassesEditorReadOnly, immutableException: disabled.immutableException }, transactionRestored: transaction.restored, cancellationBoundary: report.cancellationBoundary }, null, 2))
|
||||
46
scripts/check-freecad-property-colorlist-inventory.mjs
Normal file
46
scripts/check-freecad-property-colorlist-inventory.mjs
Normal file
@@ -0,0 +1,46 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const runtimePath = resolve(root, '.cache/freecad/reference-desktop.json')
|
||||
const reportPath = resolve(root, 'config/freecad-property-colorlist-inventory.json')
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyColorList inventory check failed: ${message}`) }
|
||||
const [runtimeContent, reportContent] = await Promise.all([readFile(runtimePath), readFile(reportPath)])
|
||||
const runtime = JSON.parse(runtimeContent)
|
||||
const report = JSON.parse(reportContent)
|
||||
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.propertyType !== 'App::PropertyColorList' || report.classification !== 'opaque-fcstd-proxy') fail('report boundary is invalid')
|
||||
if (report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is not locked to FreeCAD 1.1.1')
|
||||
if (report.recordCount !== 2 || !Array.isArray(report.records) || report.records.length !== 2) fail('record count is not exactly two')
|
||||
if (report.provenance?.runtime?.path !== '.cache/freecad/reference-desktop.json' || report.provenance.runtime.bytes !== runtimeContent.length || report.provenance.runtime.sha256 !== createHash('sha256').update(runtimeContent).digest('hex')) fail('runtime provenance is stale')
|
||||
const storage = report.storage
|
||||
if (storage?.xmlElement !== 'ColorList' || storage.fileAttribute !== 'file' || storage.nonEmptyExternalResourceName !== 'property-name' || storage.emptyResourceName !== '' || storage.externalBinary?.byteOrder !== 'little-endian' || storage.externalBinary.count !== 'uint32' || storage.externalBinary.element !== 'uint32-packed-0xRRGGBBAA' || storage.legacyBeforeVersion !== '1.1' || storage.legacyConversion !== 'invert-alpha-channel') fail('native storage contract is incomplete')
|
||||
|
||||
for (const objectTypeId of ['App::FeatureTest', 'App::FeatureTestException']) {
|
||||
const record = report.records.find((candidate) => candidate.objectTypeId === objectTypeId && candidate.propertyName === 'ColourList')
|
||||
if (!record || record.objectAvailable !== true || record.probeStatus !== 'available' || record.group !== '' || record.status?.length !== 0 || JSON.stringify(record.defaultValue) !== '[[0,0,0,1]]') fail(`native inventory changed for ${objectTypeId}.ColourList`)
|
||||
const model = record.valueModel
|
||||
if (model?.kind !== 'ordered-rgba-color-list' || model.elementRepresentation !== 'four-channel-float-tuple' || model.packedElementRepresentation !== '0xRRGGBBAA' || model.alphaSemantics !== '1-opaque-0-transparent' || model.writable !== true) fail(`value model changed for ${objectTypeId}.ColourList`)
|
||||
if (record.inputs?.collection?.join(',') !== 'sequence,iterable,single-tuple-color-value,single-non-sequence-color-value,integer-index-dictionary' || JSON.stringify(record.inputs.dictionaryAppendIndexes) !== '[-1,"current-size"]' || record.inputs.elementSetters?.join(',') !== 'float-tuple-3-or-4,integer-tuple-3-or-4,packed-integer-0xRRGGBBAA') fail(`setter inputs changed for ${objectTypeId}.ColourList`)
|
||||
if (!Array.isArray(record.dependencies) || record.dependencies.length !== 0 || record.applicability?.requiredObjectTypeId !== objectTypeId || record.applicability.source !== 'Document.supportedTypes runtime inventory' || record.applicability.shapeRequired !== false) fail(`dependencies or applicability changed for ${objectTypeId}.ColourList`)
|
||||
}
|
||||
|
||||
const nativeMatches = []
|
||||
for (const objectType of runtime.runtimeObjects?.types ?? []) for (const property of objectType.properties ?? []) if (property.typeId === 'App::PropertyColorList') nativeMatches.push({ objectTypeId: objectType.typeId, propertyName: property.name, group: property.group, status: property.status, defaultValue: property.default })
|
||||
if (nativeMatches.length !== 2) fail('locked runtime oracle no longer contains two PropertyColorList records')
|
||||
for (const locked of report.provenance?.sources ?? []) {
|
||||
const content = await readFile(resolve(root, locked?.path ?? ''))
|
||||
if (content.length !== locked.bytes || createHash('sha256').update(content).digest('hex') !== locked.sha256) fail(`source provenance is stale for ${locked.path}`)
|
||||
}
|
||||
const [propertySource, propertyBase, streamSource, colorHeader] = await Promise.all([
|
||||
readFile(resolve(root, '.cache/freecad/FreeCAD/src/App/PropertyStandard.cpp'), 'utf8'),
|
||||
readFile(resolve(root, '.cache/freecad/FreeCAD/src/App/Property.cpp'), 'utf8'),
|
||||
readFile(resolve(root, '.cache/freecad/FreeCAD/src/Base/Stream.cpp'), 'utf8'),
|
||||
readFile(resolve(root, '.cache/freecad/FreeCAD/src/Base/Color.h'), 'utf8'),
|
||||
])
|
||||
if (!propertySource.includes('PropertyColorList::getPyValue') || !propertySource.includes('PropertyColor col;') || !propertySource.includes('<ColorList file=') || !propertySource.includes('uint32_t uCt = (uint32_t)getSize()') || !propertySource.includes('str << it.getPackedValue()') || !propertySource.includes('it.a = 1.0F - it.a')) fail('locked PropertyColorList source no longer exposes the recorded setter/storage contract')
|
||||
if (!propertyBase.includes('if (PyDict_Check(value))') || !propertyBase.includes('if (PySequence_Check(value))') || !propertyBase.includes('PyObject_GetIter(value)') || !propertyBase.includes('vals.push_back(value)')) fail('locked PropertyLists source no longer exposes the recorded collection inputs')
|
||||
if (!streamSource.includes('Stream::Stream() = default') || !streamSource.includes('return _swap ? BigEndian : LittleEndian') || !streamSource.includes('OutputStream::operator<<(uint32_t ui)')) fail('locked Base::Stream source no longer exposes the recorded binary encoding')
|
||||
if (!colorHeader.includes('0xRRGGBBAA')) fail('locked Base::Color source no longer exposes the packed element contract')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-colorlist-inventory-pass', propertyType: report.propertyType, recordCount: report.recordCount, writableRecords: report.records.filter(({ valueModel }) => valueModel.writable).length, defaultValue: report.records[0].defaultValue, storage: report.storage, classification: report.classification }, null, 2))
|
||||
23
scripts/check-freecad-property-colorlist-mutation.mjs
Normal file
23
scripts/check-freecad-property-colorlist-mutation.mjs
Normal file
@@ -0,0 +1,23 @@
|
||||
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-colorlist-mutation.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyColorList mutation check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-colorlist-mutation' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyColorList' || report.caseCount !== 2 || report.cases?.length !== 2) fail('baseline is invalid')
|
||||
const before = [[0, 0, 0, 1]]
|
||||
const target = [[0.125, 0.375, 0.625, 0.875], [0.06666667, 0.13333334, 0.200000003, 0.266666681], [1, 0, 0.501960814, 0.250980407], [0, 0, 0, 0]]
|
||||
for (const typeId of ['App::FeatureTest', 'App::FeatureTestException']) {
|
||||
const entry = report.cases.find(({ objectTypeId }) => objectTypeId === typeId)
|
||||
if (!entry || entry.propertyName !== 'ColourList' || entry.mutationDetected !== true || entry.restoredExactly !== true || !isDeepStrictEqual(entry.before.value, before) || !isDeepStrictEqual(entry.touchedAfterEdit.value, target) || !isDeepStrictEqual(entry.edited.value, target) || !isDeepStrictEqual(entry.touchedAfterRestore.value, before) || !isDeepStrictEqual(entry.restored.value, before)) fail(`${typeId} mutation values changed`)
|
||||
for (const snapshot of [entry.before, entry.touchedAfterEdit, entry.edited, entry.touchedAfterRestore, entry.restored]) if (snapshot.propertyTypeId !== 'App::PropertyColorList' || !isDeepStrictEqual(snapshot.propertyStatus, []) || !isDeepStrictEqual(snapshot.editorMode, []) || snapshot.shape?.applicable !== false || snapshot.objectSet?.length !== 1 || snapshot.objectSet[0].typeId !== typeId) fail(`${typeId} native metadata changed`)
|
||||
if (!isDeepStrictEqual(entry.before.objectSet, entry.edited.objectSet) || !isDeepStrictEqual(entry.before.objectSet, entry.restored.objectSet) || !isDeepStrictEqual(entry.before.shape, entry.edited.shape) || !isDeepStrictEqual(entry.before.shape, entry.restored.shape)) fail(`${typeId} object or Shape boundary drifted`)
|
||||
if (typeId === 'App::FeatureTest') {
|
||||
const states = [entry.before, entry.touchedAfterEdit, entry.edited, entry.touchedAfterRestore, entry.restored].map(({ objectState }) => objectState)
|
||||
if (!isDeepStrictEqual(states, [['Up-to-date'], ['Touched'], ['Up-to-date'], ['Touched'], ['Up-to-date']]) || entry.editRecomputeResult !== true || entry.restoreRecomputeResult !== true || entry.edited.statusString !== 'Valid' || entry.restored.statusString !== 'Valid') fail('normal host state sequence changed')
|
||||
} else {
|
||||
if (entry.hostExecution !== 'intrinsic-test-exception' || !entry.edited.objectState.includes('Invalid') || !entry.restored.objectState.includes('Invalid') || !entry.edited.statusString.includes('Testexception') || !entry.restored.statusString.includes('Testexception')) fail('FeatureTestException intrinsic diagnostic changed')
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify({ status: 'freecad-property-colorlist-mutation-pass', cases: report.caseCount, target, mutated: report.cases.filter(({ mutationDetected }) => mutationDetected).length, restoredExactly: report.cases.filter(({ restoredExactly }) => restoredExactly).length, normalStateSequence: ['Valid', 'Touched', 'Valid', 'Touched', 'Valid'], intrinsicHostDiagnostic: 'App::FeatureTestException' }, null, 2))
|
||||
31
scripts/check-freecad-property-colorlist-promotion.mjs
Normal file
31
scripts/check-freecad-property-colorlist-promotion.mjs
Normal file
@@ -0,0 +1,31 @@
|
||||
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 load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
|
||||
const [report, semantics, progress, roundTrip, chrome] = await Promise.all([
|
||||
load('config/freecad-property-colorlist-promotion.json'),
|
||||
load('config/freecad-native-property-semantics.json'),
|
||||
load('config/freecad-follow-up-task-progress.json'),
|
||||
load('config/freecad-property-colorlist-roundtrip.json'),
|
||||
load('config/chrome-property-colorlist-verification.json'),
|
||||
])
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyColorList promotion check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.taskId !== 'PROP-app-propertycolorlist-I' || report.propertyType !== 'App::PropertyColorList' || report.recordCount !== 2 || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.systemExact !== false) fail('promotion report boundary is invalid')
|
||||
const requiredPhases = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
|
||||
const completed = new Map(progress.completedTasks?.map((entry) => [entry.id, entry]) ?? [])
|
||||
for (const phase of requiredPhases) {
|
||||
const taskId = `PROP-app-propertycolorlist-${phase}`
|
||||
if (!completed.has(taskId) || !Array.isArray(report.phaseEvidence?.[phase]) || report.phaseEvidence[phase].length === 0 || !isDeepStrictEqual(report.phaseEvidence[phase], completed.get(taskId).evidence)) fail(`phase ${phase} evidence is incomplete`)
|
||||
}
|
||||
const nativeTarget = [[0.200000003, 0.400000006, 0.600000024, 0.800000012], [0.06666667, 0.13333334, 0.200000003, 0.266666681]]
|
||||
const browserTarget = [[0.2, 0.4, 0.6, 0.8], [17 / 255, 34 / 255, 51 / 255, 0.4]]
|
||||
const approximately = (value, target, tolerance = 1e-7) => Array.isArray(value) && value.length === target.length && value.every((color, index) => Array.isArray(color) && color.length === target[index].length && color.every((channel, channelIndex) => typeof channel === 'number' && Math.abs(channel - target[index][channelIndex]) <= tolerance))
|
||||
const propertyType = semantics.types?.find(({ typeId }) => typeId === report.propertyType)
|
||||
if (report.promotion?.from !== 'opaque-fcstd-proxy' || report.promotion.to !== 'native-editable-codec' || propertyType?.support !== report.promotion.to || propertyType.statusNames?.length !== 0 || report.promotion.facadeValueModel !== 'ordered-variable-rgba-array-with-external-8-bit-fcstd-packing' || report.promotion.writableRecords !== 2 || report.promotion.fcstdElement !== 'ColorList' || report.promotion.fcstdResourcePath !== 'ColourList' || report.promotion.fcstdResourceBytes !== 12 || !approximately(report.promotion.nativeRoundTripValue, nativeTarget) || !isDeepStrictEqual(report.promotion.browserRoundTripValue, browserTarget) || report.promotion.zeroUnknownDrift !== true) fail('capability promotion is incomplete')
|
||||
const sync = report.exactBlockerSync
|
||||
if (sync?.nativeEditableTypes !== 30 || sync.nativeEditableRecords !== 4361 || sync.opaqueTypes !== 50 || sync.opaqueRecords !== 466 || sync.exactPromotionReady !== false || sync.exactBlocker !== '50 runtime property types and 466 records remain opaque-only; complete native document and property semantics') fail('exact blocker synchronization is stale')
|
||||
if (!isDeepStrictEqual(sync, { nativeEditableTypes: semantics.supportSummary['native-editable-codec'].typeCount, nativeEditableRecords: semantics.supportSummary['native-editable-codec'].recordCount, opaqueTypes: semantics.supportSummary['opaque-fcstd-proxy'].typeCount, opaqueRecords: semantics.supportSummary['opaque-fcstd-proxy'].recordCount, exactPromotionReady: semantics.exactPromotionReady, exactBlocker: semantics.exactBlocker })) fail('promotion report diverges from global property semantics')
|
||||
if (roundTrip.classification?.zeroUnknownDrift !== true || !approximately(roundTrip.classification.resavedValue, nativeTarget) || chrome.status !== 'pass' || chrome.persistence?.fcstdElement !== 'ColorList' || chrome.persistence.fcstdResourcePath !== 'ColourList' || chrome.persistence.fcstdResourceBytes !== 12 || !isDeepStrictEqual(chrome.persistence.loadedValue, browserTarget) || chrome.resource?.released !== true || chrome.release?.workerTerminated !== true) fail('G/H closure evidence regressed')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-colorlist-promotion-pass', propertyType: report.propertyType, promotion: report.promotion, completedPhases: requiredPhases, exactBlockerSync: sync, systemExact: report.systemExact }, null, 2))
|
||||
28
scripts/check-freecad-property-colorlist-roundtrip.mjs
Normal file
28
scripts/check-freecad-property-colorlist-roundtrip.mjs
Normal file
@@ -0,0 +1,28 @@
|
||||
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-colorlist-roundtrip.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyColorList roundtrip check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-colorlist-roundtrip' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyColorList') fail('baseline is invalid')
|
||||
const initial = [[17 / 255, 34 / 255, 51 / 255, 68 / 255]]
|
||||
const target = [
|
||||
[51 / 255, 102 / 255, 153 / 255, 204 / 255],
|
||||
[17 / 255, 34 / 255, 51 / 255, 68 / 255],
|
||||
]
|
||||
const approximately = (value, expected) => Array.isArray(value) && value.length === expected.length && value.every((color, colorIndex) => Array.isArray(color) && color.length === 4 && color.every((channel, channelIndex) => typeof channel === 'number' && Math.abs(channel - expected[colorIndex][channelIndex]) <= 1e-7))
|
||||
for (const [phase, snapshot] of [['nativeInitial', report.nativeInitial], ['nativeReopened', report.nativeAfter?.reopened], ['nativeResaved', report.nativeAfter?.resaved]]) {
|
||||
if (snapshot?.object?.typeId !== 'App::FeatureTest' || snapshot.object.propertyTypeId !== 'App::PropertyColorList' || !isDeepStrictEqual(snapshot.object.propertyStatus, []) || !isDeepStrictEqual(snapshot.object.editorMode, []) || snapshot.object.hasShapeProperty !== false || !isDeepStrictEqual(snapshot.objectSet, [{ name: 'ColorListProbe', typeId: 'App::FeatureTest' }])) fail(`${phase} native object semantics are incomplete`)
|
||||
}
|
||||
if (!approximately(report.nativeInitial.object.value, initial) || !approximately(report.nativeAfter.reopened.object.value, target) || !approximately(report.nativeAfter.resaved.object.value, target) || !isDeepStrictEqual(report.nativeAfter.reopened.object.value, report.nativeAfter.resaved.object.value)) fail('native initial/reopen/resave values drifted')
|
||||
let webBefore
|
||||
let webAfter
|
||||
try {
|
||||
webBefore = JSON.parse(report.web.before?.value)
|
||||
webAfter = JSON.parse(report.web.after?.value)
|
||||
} catch { fail('Web values are not structured ColorList JSON') }
|
||||
if (report.web.before?.typeId !== 'App::PropertyColorList' || report.web.before?.element !== 'ColorList' || !approximately(webBefore, initial) || report.web.after?.typeId !== 'App::PropertyColorList' || report.web.after?.element !== 'ColorList' || !approximately(webAfter, target) || report.web.resource?.path !== 'ColourList' || report.web.resource.pathPreserved !== true || report.web.resource.beforeBytes !== 8 || report.web.resource.afterBytes !== 12 || report.web.resource.changed !== true || report.web.objectSetPreserved !== true || report.web.semanticObjectsPreserved !== true || report.web.opaqueEntriesPreserved !== true || !Number.isSafeInteger(report.web.opaquePathsPreserved) || report.web.opaquePathsPreserved < 1) fail('Web edit did not preserve native archive semantics')
|
||||
if (!approximately(report.classification?.requestedValue, target) || !approximately(JSON.parse(report.classification.webValue), target) || !approximately(report.classification.reopenedValue, target) || !approximately(report.classification.resavedValue, target) || report.classification.nativeOutputMatches !== true || report.classification.unknownSemanticDrift !== false || report.classification.zeroUnknownDrift !== true) fail('round-trip classification is not exact for PropertyColorList')
|
||||
for (const archive of Object.values(report.archives ?? {})) if (!archive?.path || !Number.isSafeInteger(archive.bytes) || archive.bytes <= 0 || !/^[0-9a-f]{64}$/.test(archive.sha256)) fail('archive evidence is incomplete')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-colorlist-roundtrip-pass', values: { nativeInitial: report.nativeInitial.object.value, webEdited: JSON.parse(report.classification.webValue), nativeReopened: report.classification.reopenedValue, nativeResaved: report.classification.resavedValue }, resource: report.web.resource, opaquePathsPreserved: report.web.opaquePathsPreserved, semanticObjectsPreserved: report.web.semanticObjectsPreserved, zeroUnknownDrift: report.classification.zeroUnknownDrift }, null, 2))
|
||||
34
scripts/check-freecad-property-colorlist-success.mjs
Normal file
34
scripts/check-freecad-property-colorlist-success.mjs
Normal file
@@ -0,0 +1,34 @@
|
||||
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-colorlist-success.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyColorList success check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-colorlist-success' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyColorList' || report.caseCount !== 2 || report.cases?.length !== 2) fail('baseline is invalid')
|
||||
const expected = {
|
||||
default: [[0, 0, 0, 1]],
|
||||
empty: [],
|
||||
singletonFloat: [[0.25, 0.5, 0.75, 1]],
|
||||
mixedElements: [[0.100000001, 0.200000003, 0.300000012, 0.400000006], [1, 0.501960814, 0, 0.250980407], [0.06666667, 0.13333334, 0.200000003, 0.266666681]],
|
||||
singlePacked: [[0.06666667, 0.13333334, 0.200000003, 0.266666681]],
|
||||
iterable: [[0, 0.25, 0.5, 0.75], [1, 0.75, 0.5, 0.25]],
|
||||
indexedDictionary: [[0, 0, 0, 1], [0.06666667, 0.13333334, 0.200000003, 0.266666681], [0, 1, 0, 0.501960814]],
|
||||
restored: [[0, 0, 0, 1]],
|
||||
}
|
||||
for (const typeId of ['App::FeatureTest', 'App::FeatureTestException']) {
|
||||
const entry = report.cases.find(({ objectTypeId }) => objectTypeId === typeId)
|
||||
if (entry?.mode !== 'native-property-list-setter' || entry.propertyName !== 'ColourList' || entry.propertyAssignmentsAccepted !== true || !isDeepStrictEqual(entry.defaultValue, expected.default)) fail(`${typeId} setter contract is invalid`)
|
||||
for (const [phase, value] of Object.entries(expected)) if (!isDeepStrictEqual(entry.phases?.[phase]?.value, value)) fail(`${typeId} ${phase} value changed`)
|
||||
const large = entry.phases?.largeList?.value
|
||||
const largeColor = [0.003921569, 0.007843138, 0.011764706, 0.015686275]
|
||||
if (!Array.isArray(large) || large.length !== 257 || !large.every((color) => isDeepStrictEqual(color, largeColor))) fail(`${typeId} large-list boundary changed`)
|
||||
for (const snapshot of Object.values(entry.phases)) if (snapshot.propertyTypeId !== 'App::PropertyColorList' || !isDeepStrictEqual(snapshot.propertyStatus, []) || !isDeepStrictEqual(snapshot.editorMode, []) || snapshot.shape?.applicable !== false || snapshot.objectSet?.length !== 1 || snapshot.objectSet[0].typeId !== typeId) fail(`${typeId} native metadata is invalid`)
|
||||
if (typeId === 'App::FeatureTest') {
|
||||
for (const snapshot of Object.values(entry.phases)) if (!isDeepStrictEqual(snapshot.objectState, ['Up-to-date']) || snapshot.statusString !== 'Valid') fail(`${typeId} normal execution state changed`)
|
||||
} else {
|
||||
if (!isDeepStrictEqual(entry.phases.default.objectState, ['Up-to-date']) || entry.phases.default.statusString !== 'Valid' || entry.phases.default.recomputeResult !== false) fail('FeatureTestException default state changed')
|
||||
for (const [phase, snapshot] of Object.entries(entry.phases)) if (phase !== 'default' && (!snapshot.objectState.includes('Invalid') || !snapshot.statusString.includes('Testexception'))) fail(`FeatureTestException ${phase} intrinsic diagnostic changed`)
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify({ status: 'freecad-property-colorlist-success-pass', cases: report.caseCount, phases: Object.keys(report.cases[0].phases), largeListLength: report.cases[0].phases.largeList.value.length, normalHost: 'App::FeatureTest', intrinsicHostDiagnostic: 'App::FeatureTestException' }, null, 2))
|
||||
125
scripts/check-freecad-property-direction-failure.mjs
Normal file
125
scripts/check-freecad-property-direction-failure.mjs
Normal file
@@ -0,0 +1,125 @@
|
||||
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))
|
||||
59
scripts/check-freecad-property-direction-inventory.mjs
Normal file
59
scripts/check-freecad-property-direction-inventory.mjs
Normal file
@@ -0,0 +1,59 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const runtimePath = resolve(root, '.cache/freecad/reference-desktop.json')
|
||||
const reportPath = resolve(root, 'config/freecad-property-direction-inventory.json')
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyDirection inventory check failed: ${message}`) }
|
||||
const [runtimeContent, reportContent] = await Promise.all([readFile(runtimePath), readFile(reportPath)])
|
||||
const runtime = JSON.parse(runtimeContent)
|
||||
const report = JSON.parse(reportContent)
|
||||
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.propertyType !== 'App::PropertyDirection' || report.classification !== 'opaque-fcstd-proxy') fail('report boundary is invalid')
|
||||
if (report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is not locked to FreeCAD 1.1.1')
|
||||
if (report.recordCount !== 2 || !Array.isArray(report.records) || report.records.length !== 2) fail('record count is not exactly two')
|
||||
if (report.provenance?.runtime?.path !== '.cache/freecad/reference-desktop.json' || report.provenance.runtime.bytes !== runtimeContent.length || report.provenance.runtime.sha256 !== createHash('sha256').update(runtimeContent).digest('hex')) fail('runtime provenance is stale')
|
||||
const storage = report.storage
|
||||
if (storage?.inheritedFrom !== 'App::PropertyVector' || storage.xmlElement !== 'PropertyVector' || storage.attributes?.join(',') !== 'valueX,valueY,valueZ' || storage.scalarEncoding !== 'decimal-double' || storage.externalResource !== false) fail('native storage contract is incomplete')
|
||||
|
||||
const expected = {
|
||||
'Part::Mirroring.Normal': { group: 'Plane', dependencyNames: 'Source,Base,MirrorPlane', consumer: 'gp_Ax2(gp_Pnt(Base), gp_Dir(Normal))' },
|
||||
'Part::ProjectOnSurface.Direction': { group: 'Projection', dependencyNames: 'SupportFace,Projection', consumer: 'gp_Dir(Direction)' },
|
||||
}
|
||||
for (const [identity, contract] of Object.entries(expected)) {
|
||||
const [objectTypeId, propertyName] = identity.split(/\.(?=[^.]+$)/)
|
||||
const record = report.records.find((candidate) => candidate.objectTypeId === objectTypeId && candidate.propertyName === propertyName)
|
||||
if (!record || record.objectAvailable !== true || record.probeStatus !== 'available' || record.group !== contract.group || record.status?.length !== 0 || record.defaultDisplayValue !== 'Vector (0.0, 0.0, 1.0)' || JSON.stringify(record.defaultValue) !== '[0,0,1]') fail(`native inventory changed for ${identity}`)
|
||||
const model = record.valueModel
|
||||
if (model?.kind !== 'three-component-direction-vector' || model.representation !== 'Base::Vector3d' || model.normalizationAtPropertyBoundary !== 'none' || model.unit !== 'Length' || model.writable !== true) fail(`value model changed for ${identity}`)
|
||||
if (record.inputs?.accepted?.join(',') !== 'Base.Vector,tuple-of-three-float-or-integer-components' || record.inputs.listAccepted !== false || record.inputs.componentPaths?.join(',') !== 'x,y,z' || record.editor !== 'Gui::PropertyEditor::PropertyDirectionItem') fail(`setter or editor contract changed for ${identity}`)
|
||||
if (record.dependencies?.map(({ propertyName: name }) => name).join(',') !== contract.dependencyNames || record.applicability?.requiredObjectTypeId !== objectTypeId || record.applicability.source !== 'Document.supportedTypes runtime inventory' || record.applicability.propertyWriteRequiresShape !== false || record.applicability.hostExecutionRequiresShapeInputs !== true || record.applicability.nonZeroRequiredByConsumer !== true || record.applicability.consumer !== contract.consumer) fail(`dependencies or applicability changed for ${identity}`)
|
||||
}
|
||||
const mirror = report.records.find(({ objectTypeId }) => objectTypeId === 'Part::Mirroring')
|
||||
const mirrorPlane = mirror?.dependencies?.find(({ propertyName }) => propertyName === 'MirrorPlane')
|
||||
if (mirrorPlane?.role !== 'optional-plane-override' || mirrorPlane.requiredForExecution !== false || mirrorPlane.overrides?.join(',') !== 'Base,Normal' || mirrorPlane.overriddenStatus !== 'ReadOnly') fail('Mirroring plane override contract is incomplete')
|
||||
const projection = report.records.find(({ objectTypeId }) => objectTypeId === 'Part::ProjectOnSurface')
|
||||
if (projection?.dependencies?.some(({ requiredForExecution }) => requiredForExecution !== true)) fail('ProjectOnSurface dependencies are not locked')
|
||||
|
||||
const nativeMatches = []
|
||||
for (const objectType of runtime.runtimeObjects?.types ?? []) for (const property of objectType.properties ?? []) if (property.typeId === 'App::PropertyDirection') nativeMatches.push({ objectTypeId: objectType.typeId, propertyName: property.name, group: property.group, status: property.status, defaultValue: property.default })
|
||||
if (nativeMatches.length !== 2) fail('locked runtime oracle no longer contains two PropertyDirection records')
|
||||
for (const locked of report.provenance?.sources ?? []) {
|
||||
const content = await readFile(resolve(root, locked?.path ?? ''))
|
||||
if (content.length !== locked.bytes || createHash('sha256').update(content).digest('hex') !== locked.sha256) fail(`source provenance is stale for ${locked.path}`)
|
||||
}
|
||||
const [propertyHeader, propertySource, editorHeader, editorSource, mirrorSource, projectionSource] = await Promise.all([
|
||||
readFile(resolve(root, '.cache/freecad/FreeCAD/src/App/PropertyGeo.h'), 'utf8'),
|
||||
readFile(resolve(root, '.cache/freecad/FreeCAD/src/App/PropertyGeo.cpp'), 'utf8'),
|
||||
readFile(resolve(root, '.cache/freecad/FreeCAD/src/Gui/propertyeditor/PropertyItem.h'), 'utf8'),
|
||||
readFile(resolve(root, '.cache/freecad/FreeCAD/src/Gui/propertyeditor/PropertyItem.cpp'), 'utf8'),
|
||||
readFile(resolve(root, '.cache/freecad/FreeCAD/src/Mod/Part/App/FeatureMirroring.cpp'), 'utf8'),
|
||||
readFile(resolve(root, '.cache/freecad/FreeCAD/src/Mod/Part/App/FeatureProjectOnSurface.cpp'), 'utf8'),
|
||||
])
|
||||
if (!propertyHeader.includes('class AppExport PropertyDirection: public PropertyVector') || !propertyHeader.includes('return Base::Unit::Length;') || !propertyHeader.includes('Gui::PropertyEditor::PropertyDirectionItem')) fail('locked PropertyDirection declaration changed')
|
||||
if (!propertySource.includes('TYPESYSTEM_SOURCE(App::PropertyDirection, App::PropertyVector)') || !propertySource.includes('PyObject_TypeCheck(value, &(Base::VectorPy::Type))') || !propertySource.includes('PyTuple_Check(value) && PyTuple_Size(value) == 3') || !propertySource.includes('PyFloat_Check(item)') || !propertySource.includes('PyLong_Check(item)') || !propertySource.includes('<PropertyVector') || !propertySource.includes('valueX=') || !propertySource.includes('valueY=') || !propertySource.includes('valueZ=')) fail('locked PropertyDirection setter/storage inheritance changed')
|
||||
if (!editorHeader.includes('class GuiExport PropertyDirectionItem: public PropertyVectorDistanceItem') || !editorSource.includes('PROPERTYITEM_SOURCE(Gui::PropertyEditor::PropertyDirectionItem)')) fail('locked PropertyDirection editor changed')
|
||||
if (!mirrorSource.includes('ADD_PROPERTY_TYPE(Normal, (Base::Vector3d(0, 0, 1))') || !mirrorSource.includes('Base::Vector3d norm = Normal.getValue()') || !mirrorSource.includes('gp_Ax2 ax2(gp_Pnt(base.x, base.y, base.z), gp_Dir(norm.x, norm.y, norm.z))') || !mirrorSource.includes('Normal.setStatus(App::Property::ReadOnly, true)')) fail('locked Mirroring consumer contract changed')
|
||||
if (!projectionSource.includes('ADD_PROPERTY_TYPE(\n Direction,') || !projectionSource.includes('const auto& vec = Direction.getValue()') || !projectionSource.includes('gp_Dir dir(vec.x, vec.y, vec.z)') || !projectionSource.includes('No support face specified') || !projectionSource.includes('getProjectionShapes()')) fail('locked ProjectOnSurface consumer contract changed')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-direction-inventory-pass', propertyType: report.propertyType, recordCount: report.recordCount, writableRecords: report.records.filter(({ valueModel }) => valueModel.writable).length, defaultValue: report.records[0].defaultValue, storage: report.storage, classification: report.classification }, null, 2))
|
||||
56
scripts/check-freecad-property-direction-mutation.mjs
Normal file
56
scripts/check-freecad-property-direction-mutation.mjs
Normal file
@@ -0,0 +1,56 @@
|
||||
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-mutation.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyDirection mutation check failed: ${message}`) }
|
||||
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-direction-mutation' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyDirection') fail('baseline is invalid')
|
||||
if (report.caseCount !== 2 || report.cases?.length !== 2) fail('mutation host coverage changed')
|
||||
|
||||
const expectedHosts = {
|
||||
'Part::Mirroring': {
|
||||
caseId: 'Mirroring',
|
||||
objectName: 'DirectionMirror',
|
||||
propertyName: 'Normal',
|
||||
objectSet: [{ name: 'MirrorSource', typeId: 'Part::Box' }, { name: 'DirectionMirror', typeId: 'Part::Mirroring' }],
|
||||
afterSetMustExecute: true,
|
||||
shape: { shapeType: 'Solid', solids: 1, faces: 6, edges: 12, vertices: 8, area: 52, volume: 24 },
|
||||
},
|
||||
'Part::ProjectOnSurface': {
|
||||
caseId: 'ProjectOnSurface',
|
||||
objectName: 'DirectionProjection',
|
||||
propertyName: 'Direction',
|
||||
objectSet: [{ name: 'ProjectionSupport', typeId: 'Part::Feature' }, { name: 'ProjectionWire', typeId: 'Part::Feature' }, { name: 'DirectionProjection', typeId: 'Part::ProjectOnSurface' }],
|
||||
afterSetMustExecute: false,
|
||||
shape: { shapeType: 'Compound', solids: 0, faces: 0, edges: 4, vertices: 4, area: 0, volume: 0 },
|
||||
},
|
||||
}
|
||||
for (const entry of report.cases) {
|
||||
const expected = expectedHosts[entry.objectTypeId]
|
||||
if (!expected || entry.caseId !== expected.caseId || entry.objectName !== expected.objectName || entry.propertyName !== expected.propertyName || !isDeepStrictEqual(entry.editedValue, [0.25, -0.5, 1.5]) || entry.valueChanged !== true || entry.valueRestored !== true || entry.geometryChanged !== true || entry.geometryRestored !== true || entry.objectsPreserved !== true) fail(`${entry.objectTypeId} mutation summary changed`)
|
||||
const phases = entry.phases
|
||||
const phaseExpectations = {
|
||||
before: { value: [0, 0, 1], state: ['Up-to-date'], status: 'Valid', mustExecute: false, recomputeResult: true },
|
||||
afterSet: { value: [0.25, -0.5, 1.5], state: ['Touched'], status: 'Touched', mustExecute: expected.afterSetMustExecute, recomputeResult: null },
|
||||
afterRecompute: { value: [0.25, -0.5, 1.5], state: ['Up-to-date'], status: 'Valid', mustExecute: false, recomputeResult: true },
|
||||
afterRestoreSet: { value: [0, 0, 1], state: ['Touched'], status: 'Touched', mustExecute: expected.afterSetMustExecute, recomputeResult: null },
|
||||
afterRestoreRecompute: { value: [0, 0, 1], state: ['Up-to-date'], status: 'Valid', mustExecute: false, recomputeResult: true },
|
||||
}
|
||||
for (const [phase, phaseExpected] of Object.entries(phaseExpectations)) {
|
||||
const snapshot = phases?.[phase]
|
||||
if (!snapshot || !isDeepStrictEqual(snapshot.value, phaseExpected.value) || snapshot.propertyTypeId !== 'App::PropertyDirection' || !isDeepStrictEqual(snapshot.propertyStatus, []) || !isDeepStrictEqual(snapshot.editorMode, []) || !isDeepStrictEqual(snapshot.objectState, phaseExpected.state) || snapshot.statusString !== phaseExpected.status || snapshot.mustExecute !== phaseExpected.mustExecute || snapshot.recomputeResult !== phaseExpected.recomputeResult || !isDeepStrictEqual(snapshot.objectSet, expected.objectSet)) fail(`${entry.objectTypeId} ${phase} property or state changed`)
|
||||
const shape = snapshot.shape
|
||||
if (shape?.isNull !== false || shape.valid !== true || shape.shapeType !== expected.shape.shapeType || shape.solids !== expected.shape.solids || shape.faces !== expected.shape.faces || shape.edges !== expected.shape.edges || shape.vertices !== expected.shape.vertices || shape.area !== expected.shape.area || shape.volume !== expected.shape.volume || !Array.isArray(shape.bounds) || shape.bounds.length !== 6 || typeof shape.brepSha256 !== 'string' || shape.brepSha256.length !== 64) fail(`${entry.objectTypeId} ${phase} Shape evidence changed`)
|
||||
}
|
||||
if (phases.before.shape.brepSha256 === phases.afterRecompute.shape.brepSha256 || isDeepStrictEqual(phases.before.shape.bounds, phases.afterRecompute.shape.bounds)) fail(`${entry.objectTypeId} edit did not mutate geometry`)
|
||||
if (!isDeepStrictEqual(phases.before.shape, phases.afterRestoreRecompute.shape)) fail(`${entry.objectTypeId} restore did not recover exact geometry`)
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
status: 'freecad-property-direction-mutation-pass',
|
||||
cases: report.caseCount,
|
||||
editedValue: [0.25, -0.5, 1.5],
|
||||
restored: report.cases.map(({ objectTypeId, valueRestored, geometryRestored, objectsPreserved }) => ({ objectTypeId, valueRestored, geometryRestored, objectsPreserved })),
|
||||
}, null, 2))
|
||||
30
scripts/check-freecad-property-direction-promotion.mjs
Normal file
30
scripts/check-freecad-property-direction-promotion.mjs
Normal file
@@ -0,0 +1,30 @@
|
||||
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 load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
|
||||
const [report, semantics, progress, roundTrip, chrome] = await Promise.all([
|
||||
load('config/freecad-property-direction-promotion.json'),
|
||||
load('config/freecad-native-property-semantics.json'),
|
||||
load('config/freecad-follow-up-task-progress.json'),
|
||||
load('config/freecad-property-direction-roundtrip.json'),
|
||||
load('config/chrome-property-direction-verification.json'),
|
||||
])
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyDirection promotion check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.taskId !== 'PROP-app-propertydirection-I' || report.propertyType !== 'App::PropertyDirection' || report.recordCount !== 2 || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.systemExact !== false) fail('promotion report boundary is invalid')
|
||||
const requiredPhases = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
|
||||
const completed = new Map(progress.completedTasks?.map((entry) => [entry.id, entry]) ?? [])
|
||||
for (const phase of requiredPhases) {
|
||||
const taskId = `PROP-app-propertydirection-${phase}`
|
||||
if (!completed.has(taskId) || !Array.isArray(report.phaseEvidence?.[phase]) || report.phaseEvidence[phase].length === 0 || !isDeepStrictEqual(report.phaseEvidence[phase], completed.get(taskId).evidence)) fail(`phase ${phase} evidence is incomplete`)
|
||||
}
|
||||
const target = { x: 0.25, y: -0.5, z: 1.5 }
|
||||
const vectorEquals = (left, right) => !!left && (['x', 'y', 'z'].every((axis) => left[axis] === right[axis]))
|
||||
const propertyType = semantics.types?.find(({ typeId }) => typeId === report.propertyType)
|
||||
if (report.promotion?.from !== 'opaque-fcstd-proxy' || report.promotion.to !== 'native-editable-codec' || propertyType?.support !== report.promotion.to || propertyType.recordCount !== 2 || propertyType.objectTypeCount !== 2 || report.promotion.facadeValueModel !== 'three-finite-vector-components-with-native-propertyvector-fcstd-payload' || report.promotion.writableRecords !== 2 || report.promotion.fcstdElement !== 'PropertyVector' || !vectorEquals(report.promotion.nativeRoundTripValue, target) || !vectorEquals(report.promotion.browserRoundTripValue, target) || report.promotion.zeroUnknownDrift !== true) fail('capability promotion is incomplete')
|
||||
const sync = report.exactBlockerSync
|
||||
if (sync?.nativeEditableTypes !== 30 || sync.nativeEditableRecords !== 4361 || sync.opaqueTypes !== 50 || sync.opaqueRecords !== 466 || sync.exactPromotionReady !== false || sync.exactBlocker !== '50 runtime property types and 466 records remain opaque-only; complete native document and property semantics') fail('exact blocker synchronization is stale')
|
||||
if (!isDeepStrictEqual(sync, { nativeEditableTypes: semantics.supportSummary['native-editable-codec'].typeCount, nativeEditableRecords: semantics.supportSummary['native-editable-codec'].recordCount, opaqueTypes: semantics.supportSummary['opaque-fcstd-proxy'].typeCount, opaqueRecords: semantics.supportSummary['opaque-fcstd-proxy'].recordCount, exactPromotionReady: semantics.exactPromotionReady, exactBlocker: semantics.exactBlocker })) fail('promotion report diverges from global property semantics')
|
||||
if (roundTrip.classification?.zeroUnknownDrift !== true || !vectorEquals(roundTrip.classification.resavedValue, target) || chrome.status !== 'pass' || chrome.persistence?.fcstdElement !== 'PropertyVector' || chrome.persistence.fcstdValue !== JSON.stringify(target) || !vectorEquals(chrome.persistence.loadedValue, target) || chrome.resource?.released !== true || chrome.release?.workerTerminated !== true) fail('G/H closure evidence regressed')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-direction-promotion-pass', propertyType: report.propertyType, promotion: report.promotion, completedPhases: requiredPhases, exactBlockerSync: sync, systemExact: report.systemExact }, null, 2))
|
||||
33
scripts/check-freecad-property-direction-roundtrip.mjs
Normal file
33
scripts/check-freecad-property-direction-roundtrip.mjs
Normal file
@@ -0,0 +1,33 @@
|
||||
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-roundtrip.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyDirection roundtrip check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-direction-roundtrip' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyDirection') fail('baseline is invalid')
|
||||
|
||||
const initial = { x: 0, y: 0, z: 1 }
|
||||
const target = { x: 0.25, y: -0.5, z: 1.5 }
|
||||
const objectSet = [{ name: 'MirrorSource', typeId: 'Part::Box' }, { name: 'DirectionMirror', typeId: 'Part::Mirroring' }]
|
||||
for (const [phase, snapshot] of [['nativeInitial', report.nativeInitial], ['nativeReopened', report.nativeAfter?.reopened], ['nativeResaved', report.nativeAfter?.resaved], ['nativeRepeatReopened', report.nativeRepeat?.reopened], ['nativeRepeatResaved', report.nativeRepeat?.resaved]]) {
|
||||
if (snapshot?.object?.name !== 'DirectionMirror' || snapshot.object.typeId !== 'Part::Mirroring' || snapshot.object.propertyTypeId !== 'App::PropertyDirection' || !isDeepStrictEqual(snapshot.object.propertyStatus, []) || !isDeepStrictEqual(snapshot.object.editorMode, []) || !isDeepStrictEqual(snapshot.object.state, ['Up-to-date']) || snapshot.object.statusString !== 'Valid' || !isDeepStrictEqual(snapshot.objectSet, objectSet)) fail(`${phase} native object semantics are incomplete`)
|
||||
const shape = snapshot.object.shape
|
||||
if (shape?.isNull !== false || shape.valid !== true || shape.shapeType !== 'Solid' || shape.solids !== 1 || shape.faces !== 6 || shape.edges !== 12 || shape.vertices !== 8 || shape.area !== 52 || shape.volume !== 24 || !Array.isArray(shape.bounds) || shape.bounds.length !== 6 || !/^[0-9a-f]{64}$/.test(shape.brepSha256)) fail(`${phase} native Shape evidence is incomplete`)
|
||||
}
|
||||
if (!isDeepStrictEqual(report.nativeInitial.object.value, initial) || !isDeepStrictEqual(report.nativeAfter.reopened.object.value, target) || !isDeepStrictEqual(report.nativeAfter.resaved.object.value, target) || !isDeepStrictEqual(report.nativeRepeat.reopened.object.value, target) || !isDeepStrictEqual(report.nativeRepeat.resaved.object.value, target)) fail('native direction values drifted')
|
||||
let webBefore
|
||||
let webAfter
|
||||
try {
|
||||
webBefore = JSON.parse(report.web.before?.value)
|
||||
webAfter = JSON.parse(report.web.after?.value)
|
||||
} catch { fail('Web Direction values are not structured JSON') }
|
||||
if (report.web.before?.typeId !== 'App::PropertyDirection' || report.web.before?.element !== 'PropertyVector' || !isDeepStrictEqual(webBefore, initial) || report.web.after?.typeId !== 'App::PropertyDirection' || report.web.after?.element !== 'PropertyVector' || !isDeepStrictEqual(webAfter, target) || report.web.targetMarkedTouched !== true || report.web.webOutputMatches !== true || report.web.objectSetPreserved !== true || report.web.semanticObjectsPreserved !== true || report.web.opaqueEntriesPreserved !== true || !Number.isSafeInteger(report.web.opaquePathsPreserved) || report.web.opaquePathsPreserved < 1) fail('Web edit did not preserve native archive semantics')
|
||||
if (!isDeepStrictEqual(report.classification?.requestedValue, target) || !isDeepStrictEqual(report.classification.webValue, target) || !isDeepStrictEqual(report.classification.reopenedValue, target) || !isDeepStrictEqual(report.classification.resavedValue, target) || report.classification.nativeOutputMatches !== true || report.classification.nativeShapeChanged !== true || report.classification.nativeShapeStructureStable !== true || report.classification.nativeBrepReserialized !== true || report.classification.nativeBrepEvolutionReproduced !== true || report.classification.nativeAllowedEvolution?.classification !== 'native-allowed-evolution' || report.classification.nativeAllowedEvolution.dimension !== 'brep-serialization' || !report.classification.nativeAllowedEvolution.reason.includes('reserializes') || report.classification.unknownSemanticDrift !== false || report.classification.zeroUnknownDrift !== true) fail('round-trip classification is not exact for PropertyDirection')
|
||||
const shapeStructure = ({ brepSha256: _brepSha256, ...shape }) => shape
|
||||
if (report.nativeInitial.object.shape.brepSha256 === report.nativeAfter.reopened.object.shape.brepSha256 || isDeepStrictEqual(report.nativeInitial.object.shape.bounds, report.nativeAfter.reopened.object.shape.bounds)) fail('native Direction did not change Shape geometry')
|
||||
if (!isDeepStrictEqual(shapeStructure(report.nativeAfter.reopened.object.shape), shapeStructure(report.nativeAfter.resaved.object.shape)) || !isDeepStrictEqual(shapeStructure(report.nativeAfter.reopened.object.shape), shapeStructure(report.nativeRepeat.reopened.object.shape)) || !isDeepStrictEqual(shapeStructure(report.nativeAfter.reopened.object.shape), shapeStructure(report.nativeRepeat.resaved.object.shape))) fail('native Shape structure drifted across resave repetitions')
|
||||
if (report.nativeAfter.reopened.object.shape.brepSha256 === report.nativeAfter.resaved.object.shape.brepSha256 || report.nativeAfter.reopened.object.shape.brepSha256 !== report.nativeRepeat.reopened.object.shape.brepSha256 || report.nativeAfter.resaved.object.shape.brepSha256 !== report.nativeRepeat.resaved.object.shape.brepSha256) fail('native BREP serialization evolution was not reproduced')
|
||||
for (const archive of Object.values(report.archives ?? {})) if (!archive?.path || !Number.isSafeInteger(archive.bytes) || archive.bytes <= 0 || !/^[0-9a-f]{64}$/.test(archive.sha256)) fail('archive evidence is incomplete')
|
||||
|
||||
console.log(JSON.stringify({ status: 'freecad-property-direction-roundtrip-pass', values: { nativeInitial: report.nativeInitial.object.value, webEdited: report.classification.webValue, nativeReopened: report.classification.reopenedValue, nativeResaved: report.classification.resavedValue }, shapeChanged: report.classification.nativeShapeChanged, shapeStructureStable: report.classification.nativeShapeStructureStable, nativeAllowedEvolution: report.classification.nativeAllowedEvolution, opaquePathsPreserved: report.web.opaquePathsPreserved, zeroUnknownDrift: report.classification.zeroUnknownDrift }, null, 2))
|
||||
59
scripts/check-freecad-property-direction-success.mjs
Normal file
59
scripts/check-freecad-property-direction-success.mjs
Normal file
@@ -0,0 +1,59 @@
|
||||
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-success.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyDirection success check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-direction-success' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyDirection') fail('baseline is invalid')
|
||||
if (report.caseCount !== 2 || !Array.isArray(report.cases) || report.cases.length !== 2 || new Set(report.cases.map(({ objectTypeId }) => objectTypeId)).size !== 2) fail('native success case inventory is incomplete')
|
||||
|
||||
const values = {
|
||||
axisZTuple: [0, 0, 1],
|
||||
integerTuple: [1, 1, 2],
|
||||
nonUnitFloatTuple: [0.25, -0.5, 1.5],
|
||||
vectorObject: [-0.125, 0.25, 1],
|
||||
nearAxisTuple: [1e-9, -1e-9, 1],
|
||||
largeFiniteTuple: [1e6, -2e6, 3e6],
|
||||
}
|
||||
const requested = Object.fromEntries(Object.entries(values).map(([phase, value]) => [phase, { inputKind: phase === 'vectorObject' ? 'Base.Vector' : 'tuple', value }]))
|
||||
const expectedHosts = {
|
||||
'Part::Mirroring': {
|
||||
objectName: 'DirectionMirror',
|
||||
propertyName: 'Normal',
|
||||
objectSet: [{ name: 'MirrorSource', typeId: 'Part::Box' }, { name: 'DirectionMirror', typeId: 'Part::Mirroring' }],
|
||||
shape: { shapeType: 'Solid', solids: 1, faces: 6, edges: 12, vertices: 8, area: 52, volume: 24 },
|
||||
afterSetMustExecute: true,
|
||||
},
|
||||
'Part::ProjectOnSurface': {
|
||||
objectName: 'DirectionProjection',
|
||||
propertyName: 'Direction',
|
||||
objectSet: [{ name: 'ProjectionSupport', typeId: 'Part::Feature' }, { name: 'ProjectionWire', typeId: 'Part::Feature' }, { name: 'DirectionProjection', typeId: 'Part::ProjectOnSurface' }],
|
||||
shape: { shapeType: 'Compound', solids: 0, faces: 0, edges: 4, vertices: 4, area: 0, volume: 0 },
|
||||
afterSetMustExecute: false,
|
||||
},
|
||||
}
|
||||
for (const [typeId, expected] of Object.entries(expectedHosts)) {
|
||||
const entry = report.cases.find(({ objectTypeId }) => objectTypeId === typeId)
|
||||
if (entry?.objectName !== expected.objectName || entry.propertyName !== expected.propertyName || entry.mode !== 'direct-native-property-setter-with-valid-host-geometry' || !isDeepStrictEqual(entry.defaultValue, [0, 0, 1]) || !isDeepStrictEqual(entry.requested, requested)) fail(`${typeId} setter contract is invalid`)
|
||||
if (typeId === 'Part::Mirroring') {
|
||||
if (entry.setup?.kind !== 'box-mirror' || entry.setup.source?.typeId !== 'Part::Box' || !isDeepStrictEqual(entry.setup.source.dimensions, [2, 3, 4]) || !isDeepStrictEqual(entry.setup.base, [0, 0, 0]) || entry.setup.mirrorPlane !== null) fail('Mirroring setup is incomplete')
|
||||
} else if (entry.setup?.kind !== 'wire-to-planar-face' || entry.setup.support?.typeId !== 'Part::Feature' || !isDeepStrictEqual(entry.setup.support.subElements, ['Face1']) || entry.setup.projection?.typeId !== 'Part::Feature' || !isDeepStrictEqual(entry.setup.projection.subElements, ['Wire1']) || entry.setup.projection.z !== -5 || entry.setup.mode !== 'Edges') fail('ProjectOnSurface setup is incomplete')
|
||||
|
||||
const defaultSnapshot = entry.phases?.default?.afterRecompute
|
||||
const restoredSnapshot = entry.phases?.restored?.afterRecompute
|
||||
if (!isDeepStrictEqual(defaultSnapshot?.value, [0, 0, 1]) || !isDeepStrictEqual(restoredSnapshot?.value, [0, 0, 1]) || defaultSnapshot.shape?.brepSha256 !== restoredSnapshot.shape?.brepSha256 || !isDeepStrictEqual(defaultSnapshot.shape?.bounds, restoredSnapshot.shape?.bounds)) fail(`${typeId} default restore is not exact`)
|
||||
for (const [phase, value] of Object.entries({ default: [0, 0, 1], ...values, restored: [0, 0, 1] })) {
|
||||
const snapshots = entry.phases?.[phase]
|
||||
const snapshot = snapshots?.afterRecompute
|
||||
if (!snapshot || !isDeepStrictEqual(snapshot.value, value) || snapshot.propertyTypeId !== 'App::PropertyDirection' || !isDeepStrictEqual(snapshot.propertyStatus, []) || !isDeepStrictEqual(snapshot.editorMode, []) || !isDeepStrictEqual(snapshot.objectState, ['Up-to-date']) || snapshot.statusString !== 'Valid' || snapshot.mustExecute !== false || snapshot.recomputeResult !== true || !isDeepStrictEqual(snapshot.objectSet, expected.objectSet)) fail(`${typeId} ${phase} native semantics changed`)
|
||||
const shape = snapshot.shape
|
||||
if (shape?.applicable !== true || shape.isNull !== false || shape.valid !== true || shape.shapeType !== expected.shape.shapeType || shape.solids !== expected.shape.solids || shape.faces !== expected.shape.faces || shape.edges !== expected.shape.edges || shape.vertices !== expected.shape.vertices || shape.area !== expected.shape.area || shape.volume !== expected.shape.volume || typeof shape.brepSha256 !== 'string' || shape.brepSha256.length !== 64 || !Array.isArray(shape.bounds) || shape.bounds.length !== 6) fail(`${typeId} ${phase} Shape evidence changed`)
|
||||
if (phase !== 'default') {
|
||||
const afterSet = snapshots.afterSet
|
||||
if (!afterSet || !isDeepStrictEqual(afterSet.value, value) || !isDeepStrictEqual(afterSet.objectState, ['Touched']) || afterSet.statusString !== 'Touched' || afterSet.mustExecute !== expected.afterSetMustExecute) fail(`${typeId} ${phase} setter touch evidence changed`)
|
||||
}
|
||||
}
|
||||
for (const phase of ['integerTuple', 'nonUnitFloatTuple', 'vectorObject', 'nearAxisTuple', 'largeFiniteTuple']) if (entry.phases[phase].afterRecompute.shape.brepSha256 === defaultSnapshot.shape.brepSha256) fail(`${typeId} ${phase} did not produce a distinct Shape`)
|
||||
}
|
||||
console.log(JSON.stringify({ status: 'freecad-property-direction-success-pass', cases: report.caseCount, acceptedInputs: Object.keys(values), noPropertyNormalization: true, shapes: Object.fromEntries(Object.entries(expectedHosts).map(([typeId, value]) => [typeId, value.shape])) }, null, 2))
|
||||
22
scripts/check-freecad-property-file-failure.mjs
Normal file
22
scripts/check-freecad-property-file-failure.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
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-file-failure.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyFile failure check failed: ${message}`) }
|
||||
const same = (left, right) => JSON.stringify(left) === JSON.stringify(right)
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-file-failure' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is invalid')
|
||||
if (report.object?.typeId !== 'Mesh::Import' || report.property?.name !== 'FileName' || report.property?.typeId !== 'App::PropertyFile' || report.initial?.value !== '' || report.failures?.length !== 2) fail('failure fixture identity or cases changed')
|
||||
const failures = new Map(report.failures.map((entry) => [entry.id, entry]))
|
||||
for (const id of ['missing-path', 'wrong-type']) if (!failures.has(id)) fail(`missing failure case ${id}`)
|
||||
const missing = failures.get('missing-path')
|
||||
if (missing.assignmentException !== null || missing.recomputeException !== null || missing.after?.value !== '/definitely/missing/bitbybit-file.stl' || missing.after?.statusString !== 'File does not exist' || !same(missing.after?.objectState, ['Touched', 'Invalid']) || missing.objectsPreserved !== true || !same(missing.beforeObjects, missing.afterObjects)) fail('missing path did not enter the native invalid state without pollution')
|
||||
const wrong = failures.get('wrong-type')
|
||||
if (wrong.assignmentException?.type !== 'TypeError' || wrong.objectsPreserved !== true || !same(wrong.beforeObjects, wrong.afterObjects)) fail('wrong type did not reject without document pollution')
|
||||
const disabled = report.disabled
|
||||
if (disabled?.classification !== 'editor-read-only-and-python-immutable' || !disabled.status?.includes('Immutable') || disabled.exception?.type !== 'AttributeError' || disabled.exception.message !== "Object attribute 'FileName' is read-only" || !same(disabled.beforeObjects, disabled.afterObjects) || !same(disabled.restoredStatus, []) || !same(disabled.restoredEditorMode, [])) fail('disabled property state did not reject and restore cleanly')
|
||||
const transaction = report.transaction
|
||||
if (transaction?.undoMode !== 1 || transaction.pendingAfterEdit !== true || transaction.pendingAfterAbort !== false || transaction.activeAfterEdit?.name !== 'property-file-cancel' || !(transaction.activeAfterEdit.id > 0) || transaction.activeAfterAbort?.name !== '' || transaction.activeAfterAbort?.id !== 0 || transaction.before?.value !== '' || transaction.edited?.value !== '/tmp/transaction-file.stl' || transaction.afterAbort?.value !== '' || transaction.restored !== true || transaction.objectsRestored !== true) fail('transaction abort did not restore PropertyFile')
|
||||
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 || report.documentIntegrity.objectCount !== 1 || !same(report.documentIntegrity.initialObjects, report.documentIntegrity.finalObjects)) fail('failure, disabled or cancellation cases polluted the document')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-file-failure-pass', failures: report.failures.map(({ id, after }) => ({ id, state: after.objectState, diagnostic: after.statusString })), disabled: { status: disabled.status, editorMode: disabled.editorMode, exception: disabled.exception }, transactionRestored: transaction.restored, documentObjectsPreserved: report.documentIntegrity.objectsPreserved, cancellationBoundary: report.cancellationBoundary }, null, 2))
|
||||
30
scripts/check-freecad-property-file-inventory.mjs
Normal file
30
scripts/check-freecad-property-file-inventory.mjs
Normal file
@@ -0,0 +1,30 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const runtimePath = resolve(root, '.cache/freecad/reference-desktop.json')
|
||||
const reportPath = resolve(root, 'config/freecad-property-file-inventory.json')
|
||||
const sourcePaths = ['.cache/freecad/FreeCAD/src/App/PropertyFile.h', '.cache/freecad/FreeCAD/src/App/PropertyFile.cpp', '.cache/freecad/FreeCAD/src/Gui/propertyeditor/PropertyItem.h', '.cache/freecad/FreeCAD/src/Gui/propertyeditor/PropertyItem.cpp']
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyFile inventory check failed: ${message}`) }
|
||||
const [runtimeContent, reportContent, ...sourceContents] = await Promise.all([readFile(runtimePath), readFile(reportPath), ...sourcePaths.map((path) => readFile(resolve(root, path)))])
|
||||
const runtime = JSON.parse(runtimeContent)
|
||||
const report = JSON.parse(reportContent)
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.propertyType !== 'App::PropertyFile' || report.classification !== 'opaque-fcstd-proxy' || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('report boundary is invalid')
|
||||
if (report.recordCount !== 16 || !Array.isArray(report.records) || report.records.length !== 16) fail('record count is not exactly sixteen')
|
||||
if (report.storage?.inheritedFrom !== 'App::PropertyString' || report.storage.xmlElement !== 'String' || report.storage.externalResource !== true || report.storage.absoluteHostPathPolicy !== 'preserve-or-reject; never rewrite into browser project') fail('storage boundary is invalid')
|
||||
if (report.provenance?.runtime?.path !== '.cache/freecad/reference-desktop.json' || report.provenance.runtime.bytes !== runtimeContent.length || report.provenance.runtime.sha256 !== createHash('sha256').update(runtimeContent).digest('hex')) fail('runtime provenance is stale')
|
||||
for (const [index, path] of sourcePaths.entries()) if (report.provenance.sources?.[index]?.path !== path || report.provenance.sources[index].bytes !== sourceContents[index].length || report.provenance.sources[index].sha256 !== createHash('sha256').update(sourceContents[index]).digest('hex')) fail(`source provenance is stale for ${path}`)
|
||||
const nativeMatches = []
|
||||
for (const objectType of runtime.runtimeObjects?.types ?? []) for (const property of objectType.properties ?? []) if (property.typeId === 'App::PropertyFile') nativeMatches.push({ objectTypeId: objectType.typeId, objectAvailable: objectType.available === true, probeStatus: objectType.probeStatus, propertyName: property.name, group: property.group, status: property.status, defaultRaw: property.default })
|
||||
const normalize = (record) => ({ objectTypeId: record.objectTypeId, objectAvailable: record.objectAvailable, probeStatus: record.probeStatus, propertyName: record.propertyName, group: record.group, status: record.status, defaultRaw: record.defaultRaw })
|
||||
if (nativeMatches.length !== 16 || JSON.stringify(nativeMatches.map(normalize).sort((a, b) => `${a.objectTypeId}.${a.propertyName}`.localeCompare(`${b.objectTypeId}.${b.propertyName}`))) !== JSON.stringify(report.records.map(normalize).sort((a, b) => `${a.objectTypeId}.${a.propertyName}`.localeCompare(`${b.objectTypeId}.${b.propertyName}`)))) fail('report does not match the locked runtime oracle')
|
||||
for (const record of report.records) {
|
||||
if (record.objectAvailable !== true || record.probeStatus !== 'available' || JSON.stringify(record.status) !== '[]' || record.editor !== 'Gui::PropertyEditor::PropertyFileItem' || record.valueModel?.kind !== 'path-string' || record.valueModel.representation !== 'App::PropertyString-derived UTF-8 path' || record.valueModel.externalResource !== true || record.inputs?.accepted?.join('|') !== 'string|{filename:string,filter?:string}' || record.inputs?.emptyStringAccepted !== true || record.dependencies?.length !== 0 || record.applicability?.requiredObjectTypeId !== record.objectTypeId) fail(`native contract changed for ${record.objectTypeId}.${record.propertyName}`)
|
||||
}
|
||||
const propertyHeader = sourceContents[0].toString('utf8')
|
||||
const propertySource = sourceContents[1].toString('utf8')
|
||||
const editorHeader = sourceContents[2].toString('utf8')
|
||||
const editorSource = sourceContents[3].toString('utf8')
|
||||
if (!propertyHeader.includes('class AppExport PropertyFile: public PropertyString') || !propertyHeader.includes('Gui::PropertyEditor::PropertyFileItem') || !propertySource.includes('TYPESYSTEM_SOURCE(App::PropertyFile, App::PropertyString)') || !propertySource.includes('PyDict_Check(value)') || !propertySource.includes('dict.hasKey("filter")') || !propertySource.includes('dict.hasKey("filename")') || !editorHeader.includes('class GuiExport PropertyFileItem: public PropertyItem') || !editorSource.includes('PROPERTYITEM_SOURCE(Gui::PropertyEditor::PropertyFileItem)') || !editorSource.includes('Gui::FileChooser')) fail('locked PropertyFile setter/editor contract changed')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-file-inventory-pass', propertyType: report.propertyType, recordCount: report.recordCount, objectTypes: report.records.map(({ objectTypeId }) => objectTypeId), classification: report.classification }, null, 2))
|
||||
13
scripts/check-freecad-property-file-mutation.mjs
Normal file
13
scripts/check-freecad-property-file-mutation.mjs
Normal file
@@ -0,0 +1,13 @@
|
||||
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-file-mutation.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyFile mutation check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-file-mutation' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyFile' || report.object?.typeId !== 'Mesh::Import' || report.property?.name !== 'FileName' || report.property?.typeId !== 'App::PropertyFile') fail('baseline or identity is invalid')
|
||||
const phases = report.phases
|
||||
if (!phases?.before || !phases.edited || !phases.restored || report.requested?.before !== phases.before.value || report.requested.edited !== phases.edited.value || report.requested.restored !== phases.restored.value) fail('mutation phases are incomplete')
|
||||
for (const [id, phase] of Object.entries(phases)) if (phase.propertyTypeId !== 'App::PropertyFile' || JSON.stringify(phase.propertyStatus) !== '[]' || JSON.stringify(phase.editorMode) !== '[]' || !Array.isArray(phase.objectState) || !Array.isArray(phase.objectSet) || typeof phase.recomputeResult !== 'boolean') fail(`${id} phase metadata is invalid`)
|
||||
if (phases.before.statusString !== 'Valid' || phases.before.value !== report.requested.before || phases.edited.value !== report.requested.edited || phases.edited.statusString !== 'File does not exist' || JSON.stringify(phases.edited.objectState) !== '["Touched","Invalid"]' || phases.restored.value !== report.requested.restored || phases.restored.statusString !== 'Valid' || JSON.stringify(phases.restored.objectState) !== '["Up-to-date"]') fail('native mutation or restore semantics changed')
|
||||
if (report.classification?.valueChanged !== true || report.classification.invalidEditedPath !== true || report.classification.restoreExact !== true || report.classification.objectSetStable !== true || report.classification.unknownSemanticDrift !== false) fail('mutation classification is not exact')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-file-mutation-pass', before: phases.before.value, edited: phases.edited.value, restored: phases.restored.value, editedState: phases.edited.objectState, restoreExact: report.classification.restoreExact, unknownSemanticDrift: report.classification.unknownSemanticDrift }, null, 2))
|
||||
30
scripts/check-freecad-property-file-promotion.mjs
Normal file
30
scripts/check-freecad-property-file-promotion.mjs
Normal file
@@ -0,0 +1,30 @@
|
||||
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 load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
|
||||
const [report, semantics, progress, roundTrip, chrome] = await Promise.all([
|
||||
load('config/freecad-property-file-promotion.json'),
|
||||
load('config/freecad-native-property-semantics.json'),
|
||||
load('config/freecad-follow-up-task-progress.json'),
|
||||
load('config/freecad-property-file-roundtrip.json'),
|
||||
load('config/chrome-property-file-verification.json'),
|
||||
])
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyFile promotion check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.taskId !== 'PROP-app-propertyfile-I' || report.propertyType !== 'App::PropertyFile' || report.recordCount !== 16 || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.systemExact !== false) fail('promotion report boundary is invalid')
|
||||
const requiredPhases = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
|
||||
const completed = new Map(progress.completedTasks?.map((entry) => [entry.id, entry]) ?? [])
|
||||
for (const phase of requiredPhases) {
|
||||
const taskId = `PROP-app-propertyfile-${phase}`
|
||||
if (!completed.has(taskId) || !Array.isArray(report.phaseEvidence?.[phase]) || report.phaseEvidence[phase].length === 0 || !isDeepStrictEqual(report.phaseEvidence[phase], completed.get(taskId).evidence)) fail(`phase ${phase} evidence is incomplete`)
|
||||
}
|
||||
const nativeTarget = '.cache/freecad/property-file-roundtrip/alternate.obj'
|
||||
const browserTarget = 'project-files/chrome-updated.obj'
|
||||
const propertyType = semantics.types?.find(({ typeId }) => typeId === report.propertyType)
|
||||
if (report.promotion?.from !== 'opaque-fcstd-proxy' || report.promotion.to !== 'native-editable-codec' || propertyType?.support !== report.promotion.to || propertyType.recordCount !== 16 || propertyType.objectTypeCount !== 12 || propertyType.statusNames?.length !== 0 || report.promotion.facadeValueModel !== 'safe-project-relative-path-string-with-native-string-fcstd-payload' || report.promotion.writableRecords !== 16 || report.promotion.objectTypeCount !== 12 || report.promotion.fcstdElement !== 'String' || report.promotion.nativeRoundTripValue !== nativeTarget || report.promotion.browserRoundTripValue !== browserTarget || report.promotion.externalResourceBoundary !== 'project-relative-path; asset bytes remain external to FCStd' || report.promotion.zeroUnknownDrift !== true) fail('capability promotion is incomplete')
|
||||
const sync = report.exactBlockerSync
|
||||
if (sync?.nativeEditableTypes !== 30 || sync.nativeEditableRecords !== 4361 || sync.opaqueTypes !== 50 || sync.opaqueRecords !== 466 || sync.exactPromotionReady !== false || sync.exactBlocker !== '50 runtime property types and 466 records remain opaque-only; complete native document and property semantics') fail('exact blocker synchronization is stale')
|
||||
if (!isDeepStrictEqual(sync, { nativeEditableTypes: semantics.supportSummary['native-editable-codec'].typeCount, nativeEditableRecords: semantics.supportSummary['native-editable-codec'].recordCount, opaqueTypes: semantics.supportSummary['opaque-fcstd-proxy'].typeCount, opaqueRecords: semantics.supportSummary['opaque-fcstd-proxy'].recordCount, exactPromotionReady: semantics.exactPromotionReady, exactBlocker: semantics.exactBlocker })) fail('promotion report diverges from global property semantics')
|
||||
if (roundTrip.classification?.zeroUnknownDrift !== true || roundTrip.classification.resavedValue !== nativeTarget || roundTrip.classification.externalResourceBoundary !== 'project-relative-path; asset bytes remain external to FCStd' || chrome.status !== 'pass' || chrome.persistence?.fcstdElement !== 'String' || chrome.persistence.fcstdValue !== browserTarget || chrome.persistence.loadedValue !== browserTarget || chrome.resource?.released !== true || chrome.resource.markerRemoved !== true || chrome.release?.workerTerminated !== true) fail('G/H closure evidence regressed')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-file-promotion-pass', propertyType: report.propertyType, promotion: report.promotion, completedPhases: requiredPhases, exactBlockerSync: sync, systemExact: report.systemExact }, null, 2))
|
||||
22
scripts/check-freecad-property-file-roundtrip.mjs
Normal file
22
scripts/check-freecad-property-file-roundtrip.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
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-file-roundtrip.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyFile roundtrip check failed: ${message}`) }
|
||||
const initial = '.cache/freecad/FreeCAD/data/tests/mesh.obj'
|
||||
const target = '.cache/freecad/property-file-roundtrip/alternate.obj'
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-file-roundtrip' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyFile') fail('baseline is invalid')
|
||||
const objectSet = [{ name: 'FileProbe', typeId: 'Mesh::Import' }]
|
||||
for (const [phase, snapshot] of [['nativeInitial', report.nativeInitial], ['nativeReopened', report.nativeAfter?.reopened], ['nativeResaved', report.nativeAfter?.resaved]]) {
|
||||
if (snapshot?.object?.name !== 'FileProbe' || snapshot.object.typeId !== 'Mesh::Import' || snapshot.object.propertyTypeId !== 'App::PropertyFile' || !isDeepStrictEqual(snapshot.object.propertyStatus, []) || !isDeepStrictEqual(snapshot.object.editorMode, []) || !isDeepStrictEqual(snapshot.object.state, ['Up-to-date']) || snapshot.object.statusString !== 'Valid' || snapshot.object.hasShapeProperty !== false || snapshot.object.hasMeshProperty !== true || !isDeepStrictEqual(snapshot.objectSet, objectSet)) fail(`${phase} native object semantics are incomplete`)
|
||||
if (snapshot.object.asset?.exists !== true || !Number.isSafeInteger(snapshot.object.asset.bytes) || snapshot.object.asset.bytes <= 0 || !/^[0-9a-f]{64}$/.test(snapshot.object.asset.sha256) || snapshot.object.mesh?.isEmpty !== false || !Number.isSafeInteger(snapshot.object.mesh.facets) || snapshot.object.mesh.facets <= 0 || !Number.isSafeInteger(snapshot.object.mesh.points) || snapshot.object.mesh.points <= 0 || !Array.isArray(snapshot.object.mesh.bounds) || snapshot.object.mesh.bounds.length !== 6) fail(`${phase} native external asset or Mesh evidence is incomplete`)
|
||||
}
|
||||
if (report.nativeInitial.object.value !== initial || report.nativeAfter.reopened.object.value !== target || report.nativeAfter.resaved.object.value !== target) fail('native PropertyFile values drifted')
|
||||
if (report.nativeInitial.object.asset.sha256 !== report.assets?.initial?.sha256 || report.nativeAfter.reopened.object.asset.sha256 !== report.assets?.target?.sha256 || report.nativeAfter.resaved.object.asset.sha256 !== report.assets?.target?.sha256) fail('native object did not consume the locked external assets')
|
||||
if (isDeepStrictEqual(report.nativeInitial.object.mesh, report.nativeAfter.reopened.object.mesh) || !isDeepStrictEqual(report.nativeAfter.reopened.object.mesh, report.nativeAfter.resaved.object.mesh)) fail('native Mesh did not change and stabilize with the PropertyFile reference')
|
||||
if (report.web.before?.typeId !== 'App::PropertyFile' || report.web.before?.element !== 'String' || report.web.before?.value !== initial || report.web.after?.typeId !== 'App::PropertyFile' || report.web.after?.element !== 'String' || report.web.after?.value !== target || report.web.targetMarkedTouched !== true || report.web.webOutputMatches !== true || report.web.semanticObjectsPreserved !== true || report.web.opaqueEntriesPreserved !== true || !Number.isSafeInteger(report.web.opaquePathsPreserved) || report.web.opaquePathsPreserved < 1) fail('Web edit did not preserve native archive semantics')
|
||||
if (report.classification?.requestedValue !== target || report.classification.webValue !== target || report.classification.reopenedValue !== target || report.classification.resavedValue !== target || report.classification.nativeOutputMatches !== true || report.classification.objectSetPreserved !== true || report.classification.nativeMeshChanged !== true || report.classification.nativeMeshStable !== true || report.classification.externalResourceBoundary !== 'project-relative-path; asset bytes remain external to FCStd' || report.classification.unknownSemanticDrift !== false || report.classification.zeroUnknownDrift !== true) fail('round-trip classification is not exact for PropertyFile')
|
||||
for (const artifact of [...Object.values(report.assets ?? {}), ...Object.values(report.archives ?? {})]) if (!artifact?.path || !Number.isSafeInteger(artifact.bytes) || artifact.bytes <= 0 || !/^[0-9a-f]{64}$/.test(artifact.sha256)) fail('asset or archive evidence is incomplete')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-file-roundtrip-pass', values: { nativeInitial: report.nativeInitial.object.value, webEdited: report.classification.webValue, nativeReopened: report.classification.reopenedValue, nativeResaved: report.classification.resavedValue }, mesh: { initial: report.nativeInitial.object.mesh, reopened: report.nativeAfter.reopened.object.mesh }, opaquePathsPreserved: report.web.opaquePathsPreserved, externalResourceBoundary: report.classification.externalResourceBoundary, zeroUnknownDrift: report.classification.zeroUnknownDrift }, null, 2))
|
||||
19
scripts/check-freecad-property-file-success.mjs
Normal file
19
scripts/check-freecad-property-file-success.mjs
Normal file
@@ -0,0 +1,19 @@
|
||||
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-file-success.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyFile success check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-file-success' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyFile') fail('baseline is invalid')
|
||||
if (report.object?.typeId !== 'Mesh::Import' || report.property?.name !== 'FileName' || report.property?.typeId !== 'App::PropertyFile' || report.property?.group !== '') fail('native object/property identity changed')
|
||||
if (report.asset?.exists !== true || report.asset.bytes <= 0 || !Array.isArray(report.asset.path) && typeof report.asset.path !== 'string') fail('valid mesh asset evidence is missing')
|
||||
if (report.diagnostics?.exception !== null || report.diagnostics?.documentObjectCount !== 1 || !report.phases || !['default', 'valid-mesh', 'relative-path', 'restored'].every((id) => report.phases[id])) fail('native success phases are incomplete')
|
||||
const objectSet = [{ name: 'FileProbe', typeId: 'Mesh::Import' }]
|
||||
for (const [id, phase] of Object.entries(report.phases)) {
|
||||
if (phase.propertyTypeId !== 'App::PropertyFile' || !isDeepStrictEqual(phase.propertyStatus, []) || !isDeepStrictEqual(phase.editorMode, []) || !isDeepStrictEqual(phase.objectSet, objectSet) || typeof phase.recomputeResult !== 'boolean' || !Array.isArray(phase.objectState)) fail(`${id} native metadata is invalid`)
|
||||
}
|
||||
if (report.phases.default.value !== '' || report.phases.default.requested !== '' || report.phases['valid-mesh'].value !== report.asset.path || report.phases['valid-mesh'].requested !== report.asset.path || report.phases['relative-path'].value !== 'mesh-data/Cube.stl' || report.phases.restored.value !== report.asset.path) fail('PropertyFile values did not preserve native paths')
|
||||
if (report.phases['valid-mesh'].statusString !== 'Valid' || !isDeepStrictEqual(report.phases['valid-mesh'].objectState, ['Up-to-date']) || report.phases['valid-mesh'].mesh?.applicable !== true || report.phases['valid-mesh'].mesh?.error) fail('valid path success evidence is incomplete')
|
||||
if (report.phases.restored.statusString !== 'Valid' || !isDeepStrictEqual(report.phases.restored.objectState, ['Up-to-date']) || report.phases.restored.mesh?.error) fail('restored path state drifted')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-file-success-pass', propertyType: report.propertyType, phases: Object.keys(report.phases), validPath: report.phases['valid-mesh'].value, restored: report.phases.restored.value, hostMesh: report.phases['valid-mesh'].mesh }, null, 2))
|
||||
40
scripts/check-freecad-property-fileincluded-failure.mjs
Normal file
40
scripts/check-freecad-property-fileincluded-failure.mjs
Normal file
@@ -0,0 +1,40 @@
|
||||
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-fileincluded-failure.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyFileIncluded failure check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-fileincluded-failure' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyFileIncluded' || report.object?.typeId !== 'App::DocumentObjectFileIncluded' || report.property?.name !== 'File' || report.property?.typeId !== 'App::PropertyFileIncluded') fail('baseline or identity is invalid')
|
||||
if (report.initial?.exists !== true || report.initial.isFile !== true || report.initial.isDirectory !== false || report.initial.writeBits !== 0 || report.initial.propertyTypeId !== 'App::PropertyFileIncluded' || !isDeepStrictEqual(report.initial.propertyStatus, []) || !isDeepStrictEqual(report.initial.editorMode, []) || !isDeepStrictEqual(report.initial.objectState, ['Up-to-date']) || report.initial.statusString !== 'Valid') fail('initial included-file state is invalid')
|
||||
|
||||
const expectedFailures = new Map([
|
||||
['missing-path', ['OSError', 'does not exist']],
|
||||
['wrong-type', ['TypeError', 'Type must be string or file']],
|
||||
['tuple-wrong-arity', ['TypeError', 'Tuple needs size of (filePath,newFileName)']],
|
||||
['tuple-wrong-name-type', ['TypeError', 'Second item in tuple must be a string']],
|
||||
['dictionary-wrong-filename-type', ['TypeError', 'PyCXX: Error creating object']],
|
||||
['same-current-transient', ['OSError', 'Not possible to set the same file!']],
|
||||
])
|
||||
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.assignmentException?.type !== expected[0] || !entry.assignmentException.message.includes(expected[1]) || entry.valuePreserved !== true || entry.objectsPreserved !== true || !isDeepStrictEqual(entry.before.objectSet, entry.after.objectSet) || entry.before.value !== entry.after.value || entry.before.sha256 !== entry.after.sha256 || entry.after.writeBits !== 0 || entry.after.exists !== true) fail(`${entry.id} rejection polluted the included resource or document`)
|
||||
}
|
||||
|
||||
const [directory] = report.acceptedRisks ?? []
|
||||
if (report.acceptedRisks?.length !== 1 || directory?.id !== 'directory-path' || directory.assignmentException !== null || directory.oldFileRemoved !== true || directory.directoryAccepted !== true || directory.objectsPreserved !== true || directory.before?.isFile !== true || directory.after?.isFile !== false || directory.after?.isDirectory !== true || directory.after.pathExists !== true || directory.recovery?.isFile !== true || directory.recovery.isDirectory !== false || directory.recovery.writeBits !== 0 || directory.recovery.statusString !== 'Valid') fail('directory-path non-atomic acceptance or recovery changed')
|
||||
|
||||
const filterOnly = report.filterOnly
|
||||
if (filterOnly?.exception !== null || filterOnly.valuePreserved !== true || filterOnly.objectsPreserved !== true || filterOnly.before.value !== filterOnly.after.value || filterOnly.before.sha256 !== filterOnly.after.sha256) fail('filter-only dictionary boundary changed')
|
||||
const editor = report.editorReadOnly
|
||||
if (!isDeepStrictEqual(editor?.editorMode, ['ReadOnly']) || editor.exception !== null || editor.pythonBypassesEditorReadOnly !== true || editor.after.exists !== true || editor.after.writeBits !== 0 || editor.after.baseName !== 'editor-write.bin') fail('editor ReadOnly did not preserve Python setter behavior')
|
||||
const immutable = report.immutable
|
||||
if (!isDeepStrictEqual(immutable?.status, ['Immutable']) || immutable.exception?.type !== 'AttributeError' || immutable.exception.message !== "Object attribute 'File' is read-only" || immutable.before.value !== immutable.after.value || immutable.before.sha256 !== immutable.after.sha256 || !isDeepStrictEqual(immutable.restoredStatus, [])) 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-fileincluded-cancel' || !(transaction.activeAfterEdit.id > 0) || transaction.activeAfterAbort?.name !== '' || transaction.activeAfterAbort.id !== 0 || transaction.restored !== true || transaction.editedPathExistsAfterAbort !== false || !isDeepStrictEqual(transaction.objectsBefore, transaction.objectsAfter) || transaction.edited?.baseName !== 'transaction-write.bin' || transaction.edited.writeBits !== 0 || transaction.afterAbort.writeBits !== 0 || transaction.before.sha256 === transaction.edited.sha256 || transaction.before.sha256 !== transaction.afterAbort.sha256) fail('transaction abort did not restore ownership and remove the edited transient copy')
|
||||
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 || report.documentIntegrity.objectCount !== 1 || !isDeepStrictEqual(report.documentIntegrity.initialObjects, report.documentIntegrity.finalObjects)) fail('failure cases polluted the document object set')
|
||||
|
||||
console.log(JSON.stringify({ status: 'freecad-property-fileincluded-failure-pass', failures: report.failures.map(({ id, assignmentException, valuePreserved }) => ({ id, type: assignmentException.type, message: assignmentException.message, valuePreserved })), acceptedNonAtomicRisk: { id: directory.id, oldFileRemoved: directory.oldFileRemoved, directoryAccepted: directory.directoryAccepted, recovered: directory.recovery.isFile }, filterOnlyNoOp: filterOnly.valuePreserved, editorReadOnlyPythonBypass: editor.pythonBypassesEditorReadOnly, immutable: immutable.exception, transactionRestored: transaction.restored, editedTransientRemoved: !transaction.editedPathExistsAfterAbort, cancellationBoundary: report.cancellationBoundary }, null, 2))
|
||||
109
scripts/check-freecad-property-fileincluded-inventory.mjs
Normal file
109
scripts/check-freecad-property-fileincluded-inventory.mjs
Normal file
@@ -0,0 +1,109 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
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 runtimePath = '.cache/freecad/reference-desktop.json'
|
||||
const reportPath = 'config/freecad-property-fileincluded-inventory.json'
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyFileIncluded inventory check failed: ${message}`) }
|
||||
const [runtimeContent, reportContent] = await Promise.all([
|
||||
readFile(resolve(root, runtimePath)),
|
||||
readFile(resolve(root, reportPath)),
|
||||
])
|
||||
const runtime = JSON.parse(runtimeContent)
|
||||
const report = JSON.parse(reportContent)
|
||||
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.propertyType !== 'App::PropertyFileIncluded' || report.classification !== 'opaque-fcstd-proxy' || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('report boundary is invalid')
|
||||
if (report.recordCount !== 20 || report.objectTypeCount !== 15 || report.records?.length !== 20) fail('runtime record or host count changed')
|
||||
if (report.provenance?.runtime?.path !== runtimePath || report.provenance.runtime.bytes !== runtimeContent.length || report.provenance.runtime.sha256 !== createHash('sha256').update(runtimeContent).digest('hex')) fail('runtime provenance is stale')
|
||||
for (const locked of report.provenance?.sources ?? []) {
|
||||
const content = await readFile(resolve(root, locked.path))
|
||||
if (content.length !== locked.bytes || createHash('sha256').update(content).digest('hex') !== locked.sha256) fail(`source provenance is stale for ${locked.path}`)
|
||||
}
|
||||
|
||||
const normalizeStatus = (status) => status.map((entry) => entry === 27 ? 'PropOutput' : String(entry))
|
||||
const nativeMatches = []
|
||||
for (const objectType of runtime.runtimeObjects?.types ?? []) for (const property of objectType.properties ?? []) {
|
||||
if (property.typeId !== 'App::PropertyFileIncluded') continue
|
||||
nativeMatches.push({
|
||||
objectTypeId: objectType.typeId,
|
||||
objectAvailable: objectType.available === true,
|
||||
probeStatus: objectType.probeStatus,
|
||||
propertyName: property.name,
|
||||
group: property.group,
|
||||
status: property.status,
|
||||
normalizedStatus: normalizeStatus(property.status),
|
||||
defaultRaw: property.default,
|
||||
})
|
||||
}
|
||||
const identity = ({ objectTypeId, objectAvailable, probeStatus, propertyName, group, status, normalizedStatus, defaultRaw }) => ({ objectTypeId, objectAvailable, probeStatus, propertyName, group, status, normalizedStatus, defaultRaw })
|
||||
const sortRecords = (records) => records.map(identity).sort((left, right) => `${left.objectTypeId}.${left.propertyName}`.localeCompare(`${right.objectTypeId}.${right.propertyName}`))
|
||||
if (nativeMatches.length !== 20 || !isDeepStrictEqual(sortRecords(nativeMatches), sortRecords(report.records))) fail('report diverges from the locked runtime oracle')
|
||||
|
||||
const expectedKeys = [
|
||||
'App::DocumentObjectFileIncluded.File',
|
||||
'App::VRMLObject.VrmlFile',
|
||||
'Image::ImagePlane.ImageFile',
|
||||
'Robot::RobotObject.RobotKinematicFile',
|
||||
'Robot::RobotObject.RobotVrmlFile',
|
||||
'Sketcher::SketchObjectSF.SketchFlatFile',
|
||||
'TechDraw::DrawComplexSection.PatIncluded',
|
||||
'TechDraw::DrawComplexSection.SvgIncluded',
|
||||
'TechDraw::DrawComplexSectionPython.PatIncluded',
|
||||
'TechDraw::DrawComplexSectionPython.SvgIncluded',
|
||||
'TechDraw::DrawGeomHatch.PatIncluded',
|
||||
'TechDraw::DrawHatch.SvgIncluded',
|
||||
'TechDraw::DrawSVGTemplate.PageResult',
|
||||
'TechDraw::DrawTileWeld.SymbolIncluded',
|
||||
'TechDraw::DrawTileWeldPython.SymbolIncluded',
|
||||
'TechDraw::DrawViewImage.ImageIncluded',
|
||||
'TechDraw::DrawViewSection.PatIncluded',
|
||||
'TechDraw::DrawViewSection.SvgIncluded',
|
||||
'TechDraw::DrawViewSectionPython.PatIncluded',
|
||||
'TechDraw::DrawViewSectionPython.SvgIncluded',
|
||||
]
|
||||
if (report.records.map(({ objectTypeId, propertyName }) => `${objectTypeId}.${propertyName}`).join('|') !== expectedKeys.join('|')) fail('host/property inventory changed')
|
||||
if (report.records.some((record) => record.objectAvailable !== true || record.probeStatus !== 'available' || record.valueModel?.kind !== 'included-file-resource' || record.valueModel.representation !== 'read-only document-transient file backed by embedded FCStd bytes' || record.valueModel.pathIsRuntimeOnly !== true || record.valueModel.bytesAreAuthoritative !== true || record.inputs?.sourceFileMustExist !== true || record.inputs.emptyStringBehavior !== 'accepted-no-op; does not clear an existing value' || record.inputs.sameTransientPathBehavior !== 'rejected' || record.editor !== 'Gui::PropertyEditor::PropertyTransientFileItem' || record.applicability?.requiredObjectTypeId !== record.objectTypeId || record.applicability.propertyWriteRequiresShape !== false)) fail('per-record value, input, editor or applicability contract is incomplete')
|
||||
if (report.records.filter(({ valueModel }) => valueModel.writableByStatus).length !== 11 || report.records.filter(({ status }) => isDeepStrictEqual(status, ['ReadOnly'])).length !== 8 || report.records.filter(({ status }) => isDeepStrictEqual(status, [27])).length !== 1 || report.records.filter(({ defaultKind }) => defaultKind === 'empty').length !== 8 || report.records.filter(({ defaultKind }) => defaultKind === 'document-transient-copy').length !== 12) fail('status/default partition changed')
|
||||
|
||||
const dependencyPairs = report.records.filter(({ dependency }) => dependency !== null).map(({ objectTypeId, propertyName, dependency }) => `${objectTypeId}.${propertyName}<-${dependency.sourceProperty}`)
|
||||
const expectedDependencyPairs = [
|
||||
'TechDraw::DrawComplexSection.PatIncluded<-FileGeomPattern',
|
||||
'TechDraw::DrawComplexSection.SvgIncluded<-FileHatchPattern',
|
||||
'TechDraw::DrawComplexSectionPython.PatIncluded<-FileGeomPattern',
|
||||
'TechDraw::DrawComplexSectionPython.SvgIncluded<-FileHatchPattern',
|
||||
'TechDraw::DrawGeomHatch.PatIncluded<-FilePattern',
|
||||
'TechDraw::DrawHatch.SvgIncluded<-HatchPattern',
|
||||
'TechDraw::DrawSVGTemplate.PageResult<-Template',
|
||||
'TechDraw::DrawTileWeld.SymbolIncluded<-SymbolFile',
|
||||
'TechDraw::DrawTileWeldPython.SymbolIncluded<-SymbolFile',
|
||||
'TechDraw::DrawViewImage.ImageIncluded<-ImageFile',
|
||||
'TechDraw::DrawViewSection.PatIncluded<-FileGeomPattern',
|
||||
'TechDraw::DrawViewSection.SvgIncluded<-FileHatchPattern',
|
||||
'TechDraw::DrawViewSectionPython.PatIncluded<-FileGeomPattern',
|
||||
'TechDraw::DrawViewSectionPython.SvgIncluded<-FileHatchPattern',
|
||||
]
|
||||
if (!isDeepStrictEqual(dependencyPairs, expectedDependencyPairs) || report.records.filter(({ dependency }) => dependency === null).length !== 6 || report.records.some(({ dependency }) => dependency && dependency.relation !== 'host-onChanged-copies-source-bytes-into-included-property')) fail('derived-source dependency inventory changed')
|
||||
|
||||
const setter = report.setterSemantics
|
||||
if (setter?.externalSource !== 'copy bytes into the document transient directory' || setter.writableTransientSource !== 'rename into the selected transient destination' || setter.replacement !== 'delete the previous transient copy unless undo owns it' || setter.destinationPermissions !== 'read-only' || setter.archiveNameCollision !== 'append a positive integer before the extension' || setter.undoRedo !== 'Copy/Paste move or copy transient files while preserving per-property ownership') fail('setter and ownership semantics are incomplete')
|
||||
const storage = report.storage
|
||||
if (storage?.xmlElement !== 'FileIncluded' || storage.archiveAttribute !== 'file' || storage.archiveResource !== 'separate FCStd ZIP entry containing byte-identical file data' || storage.forceXmlAttribute !== 'data' || storage.forceXmlResource !== 'binary data embedded in the XML stream' || storage.emptyEncoding !== '<FileIncluded file=""/>' || storage.restoredValue !== 'document transient path; not a portable semantic path' || storage.browserBoundary !== 'project resource identity plus bytes; never expose or persist an ambient host path') fail('FCStd or browser storage boundary is incomplete')
|
||||
|
||||
const sources = new Map(await Promise.all((report.provenance?.sources ?? []).map(async ({ path }) => [path, await readFile(resolve(root, path), 'utf8')])))
|
||||
const propertyHeader = sources.get('.cache/freecad/FreeCAD/src/App/PropertyFile.h') ?? ''
|
||||
const propertySource = sources.get('.cache/freecad/FreeCAD/src/App/PropertyFile.cpp') ?? ''
|
||||
const editorHeader = sources.get('.cache/freecad/FreeCAD/src/Gui/propertyeditor/PropertyItem.h') ?? ''
|
||||
const editorSource = sources.get('.cache/freecad/FreeCAD/src/Gui/propertyeditor/PropertyItem.cpp') ?? ''
|
||||
if (!propertyHeader.includes('class AppExport PropertyFileIncluded: public Property') || !propertyHeader.includes('return "Gui::PropertyEditor::PropertyTransientFileItem"') || !propertyHeader.includes("it's not allowed to write the file") || !propertySource.includes('TYPESYSTEM_SOURCE(App::PropertyFileIncluded, App::Property)') || !propertySource.includes('Not possible to set the same file!') || !propertySource.includes('str << "File " << file.filePath() << " does not exist."') || !propertySource.includes('PyTuple_Check(value)') || !propertySource.includes('PyDict_Check(value)') || !propertySource.includes('writer.isForceXML()') || !propertySource.includes('<FileIncluded data=') || !propertySource.includes('<FileIncluded file=') || !propertySource.includes('writer.addFile(file.fileName().c_str(), this)') || !propertySource.includes('reader.addFile(file.c_str(), this)') || !propertySource.includes('reader.readBinFile(_cValue.c_str())') || !propertySource.includes('PropertyFileIncluded::SaveDocFile()') || !propertySource.includes('PropertyFileIncluded::Copy()') || !propertySource.includes('PropertyFileIncluded::Paste(') || !editorHeader.includes('class GuiExport PropertyTransientFileItem: public PropertyItem') || !editorSource.includes('PROPERTYITEM_SOURCE(Gui::PropertyEditor::PropertyTransientFileItem)') || !editorSource.includes('Gui::FileChooser')) fail('locked native setter, FCStd, ownership or editor source changed')
|
||||
for (const [path, snippets] of [
|
||||
['.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawViewSection.cpp', ['if (prop == &FileHatchPattern)', 'replaceSvgIncluded(FileHatchPattern.getValue())', 'if (prop == &FileGeomPattern)', 'replacePatIncluded(FileGeomPattern.getValue())']],
|
||||
['.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawGeomHatch.cpp', ['if (prop == &FilePattern)', 'PatIncluded.setValue(newHatchFileName.c_str())']],
|
||||
['.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawHatch.cpp', ['if (prop == &HatchPattern)', 'SvgIncluded.setValue(newHatchFileName.c_str())']],
|
||||
['.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawSVGTemplate.cpp', ['if (prop == &Template && !isRestoring())', 'PageResult.setValue(newTemplateFileName.c_str())']],
|
||||
['.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawTileWeld.cpp', ['if (prop == &SymbolFile)', 'SymbolIncluded.setValue(newSymbolFile.c_str())']],
|
||||
['.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawViewImage.cpp', ['if (prop == &ImageFile)', 'ImageIncluded.setValue(newImageFile.c_str())']],
|
||||
]) for (const snippet of snippets) if (!(sources.get(path) ?? '').includes(snippet)) fail(`locked derived-source contract changed in ${path}`)
|
||||
|
||||
console.log(JSON.stringify({ status: 'freecad-property-fileincluded-inventory-pass', propertyType: report.propertyType, recordCount: report.recordCount, objectTypeCount: report.objectTypeCount, writableByStatus: report.records.filter(({ valueModel }) => valueModel.writableByStatus).length, readOnlyRecords: 8, outputRecords: 1, directRecords: 6, derivedRecords: 14, storage: report.storage, classification: report.classification }, null, 2))
|
||||
32
scripts/check-freecad-property-fileincluded-mutation.mjs
Normal file
32
scripts/check-freecad-property-fileincluded-mutation.mjs
Normal file
@@ -0,0 +1,32 @@
|
||||
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, inventory] = await Promise.all([
|
||||
readFile(resolve(root, 'config/freecad-property-fileincluded-mutation.json'), 'utf8').then(JSON.parse),
|
||||
readFile(resolve(root, 'config/freecad-property-fileincluded-inventory.json'), 'utf8').then(JSON.parse),
|
||||
])
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyFileIncluded mutation check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-fileincluded-mutation' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyFileIncluded' || report.caseCount !== 20 || report.cases?.length !== 20) fail('baseline or case count is invalid')
|
||||
const keys = report.cases.map(({ objectTypeId, propertyName }) => `${objectTypeId}.${propertyName}`)
|
||||
if (!isDeepStrictEqual(keys, inventory.records.map(({ objectTypeId, propertyName }) => `${objectTypeId}.${propertyName}`))) fail('mutation cases do not cover the locked inventory')
|
||||
for (const [index, entry] of report.cases.entries()) {
|
||||
const record = inventory.records[index]
|
||||
const expectedStatus = record.status.map(String)
|
||||
const expectedEditor = isDeepStrictEqual(record.status, ['ReadOnly']) ? ['ReadOnly'] : []
|
||||
const snapshots = [entry.before, entry.edited, entry.editedRecompute?.snapshot, entry.restored, entry.restoredRecompute?.snapshot]
|
||||
if (!entry.pathStable || !entry.objectSetStable || entry.beforeSource.sha256 === entry.editedSource.sha256 || entry.beforeSource.bytes === entry.editedSource.bytes && entry.beforeSource.sha256 === entry.editedSource.sha256) fail(`${keys[index]} did not produce a distinct edited resource`)
|
||||
if (entry.beforeSource.sha256 !== entry.before.sha256 || entry.editedSource.sha256 !== entry.edited.sha256 || entry.beforeSource.sha256 !== entry.restored.sha256 || entry.beforeSource.bytes !== entry.before.bytes || entry.editedSource.bytes !== entry.edited.bytes || entry.restored.bytes !== entry.beforeSource.bytes) fail(`${keys[index]} byte/hash mutation or restoration changed`)
|
||||
if (entry.before.baseName !== entry.archiveName || entry.edited.baseName !== entry.archiveName || entry.restored.baseName !== entry.archiveName || snapshots.some((snapshot) => snapshot.propertyTypeId !== 'App::PropertyFileIncluded' || snapshot.writeBits !== 0 || snapshot.underTransientDir !== true || !isDeepStrictEqual(snapshot.propertyStatus, expectedStatus) || !isDeepStrictEqual(snapshot.editorMode, expectedEditor) || snapshot.exists !== true)) fail(`${keys[index]} path/permission/status metadata changed`)
|
||||
if (entry.editedRecompute.result !== !isDeepStrictEqual(expectedStatus, ['27']) || entry.restoredRecompute.result !== !isDeepStrictEqual(expectedStatus, ['27'])) fail(`${keys[index]} recompute result changed`)
|
||||
if (isDeepStrictEqual(expectedStatus, ['27'])) {
|
||||
if (!isDeepStrictEqual(entry.before.objectState, ['Up-to-date']) || !isDeepStrictEqual(entry.edited.objectState, ['Up-to-date']) || !isDeepStrictEqual(entry.restored.objectState, ['Up-to-date']) || entry.edited.statusString !== 'Valid' || entry.restored.statusString !== 'Valid') fail(`${keys[index]} PropOutput touch suppression changed`)
|
||||
} else {
|
||||
if (!isDeepStrictEqual(entry.before.objectState, ['Up-to-date']) || entry.before.statusString !== 'Valid' || !isDeepStrictEqual(entry.edited.objectState, ['Touched']) || !isDeepStrictEqual(entry.restored.objectState, ['Touched']) || entry.edited.statusString !== 'Touched' || entry.restored.statusString !== 'Touched') fail(`${keys[index]} setter touch sequence changed`)
|
||||
if (!isDeepStrictEqual(entry.editedRecompute.snapshot.objectState, ['Up-to-date']) || entry.editedRecompute.snapshot.statusString !== 'Valid' || !isDeepStrictEqual(entry.restoredRecompute.snapshot.objectState, ['Up-to-date']) || entry.restoredRecompute.snapshot.statusString !== 'Valid') fail(`${keys[index]} recompute recovery changed`)
|
||||
}
|
||||
const shapeExpected = entry.objectTypeId === 'Sketcher::SketchObjectSF'
|
||||
if (entry.restored.shape?.applicable !== shapeExpected || shapeExpected && (entry.before.shape.isNull !== true || entry.edited.shape.isNull !== true || entry.restored.shape.isNull !== true || entry.restored.shape.edges !== 0)) fail(`${keys[index]} Shape boundary changed`)
|
||||
}
|
||||
console.log(JSON.stringify({ status: 'freecad-property-fileincluded-mutation-pass', propertyType: report.propertyType, caseCount: report.caseCount, editedCases: report.cases.filter(({ before, edited }) => before.sha256 !== edited.sha256).length, restoredCases: report.cases.filter(({ before, restored }) => before.sha256 === restored.sha256).length, stablePaths: report.cases.filter(({ pathStable }) => pathStable).length, stableObjects: report.cases.filter(({ objectSetStable }) => objectSetStable).length, outputTouchSuppressed: report.cases.find(({ propertyName, objectTypeId }) => objectTypeId === 'TechDraw::DrawSVGTemplate' && propertyName === 'PageResult')?.editedRecompute.result === false }, null, 2))
|
||||
29
scripts/check-freecad-property-fileincluded-promotion.mjs
Normal file
29
scripts/check-freecad-property-fileincluded-promotion.mjs
Normal file
@@ -0,0 +1,29 @@
|
||||
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 load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
|
||||
const [report, semantics, progress, roundTrip, chrome] = await Promise.all([
|
||||
load('config/freecad-property-fileincluded-promotion.json'),
|
||||
load('config/freecad-native-property-semantics.json'),
|
||||
load('config/freecad-follow-up-task-progress.json'),
|
||||
load('config/freecad-property-fileincluded-roundtrip.json'),
|
||||
load('config/chrome-property-fileincluded-verification.json'),
|
||||
])
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyFileIncluded promotion check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.taskId !== 'PROP-app-propertyfileincluded-I' || report.propertyType !== 'App::PropertyFileIncluded' || report.recordCount !== 20 || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.systemExact !== false) fail('promotion report boundary is invalid')
|
||||
const requiredPhases = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
|
||||
const completed = new Map(progress.completedTasks?.map((entry) => [entry.id, entry]) ?? [])
|
||||
for (const phase of requiredPhases) {
|
||||
const taskId = `PROP-app-propertyfileincluded-${phase}`
|
||||
if (!completed.has(taskId) || !Array.isArray(report.phaseEvidence?.[phase]) || report.phaseEvidence[phase].length === 0 || !isDeepStrictEqual(report.phaseEvidence[phase], completed.get(taskId).evidence)) fail(`phase ${phase} evidence is incomplete`)
|
||||
}
|
||||
const nativeTarget = '09f4e4853de4277e25871d26f2e953c6ea9c13a73c7a01bdfcb5209d17f9a583'
|
||||
const propertyType = semantics.types?.find(({ typeId }) => typeId === report.propertyType)
|
||||
if (report.promotion?.from !== 'opaque-fcstd-proxy' || report.promotion.to !== 'native-editable-codec' || propertyType?.support !== report.promotion.to || propertyType.recordCount !== 20 || propertyType.objectTypeCount !== 15 || propertyType.statusNames?.join(',') !== 'PropOutput,ReadOnly' || report.promotion.facadeValueModel !== 'project-resource-identity-and-authoritative-bytes-with-native-fileincluded-fcstd-resource' || report.promotion.writableRecords !== 11 || report.promotion.readOnlyRecords !== 8 || report.promotion.outputRecords !== 1 || report.promotion.objectTypeCount !== 15 || report.promotion.fcstdElement !== 'FileIncluded' || report.promotion.nativeArchiveName !== 'payload.bin' || report.promotion.nativeRoundTripSha256 !== nativeTarget || report.promotion.browserArchiveName !== 'chrome-resource.bin' || report.promotion.browserRoundTripBytes !== 16 || report.promotion.externalResourceBoundary !== 'embedded FCStd ZIP entry restored to a read-only document transient file' || report.promotion.zeroUnknownDrift !== true) fail('capability promotion is incomplete')
|
||||
const sync = report.exactBlockerSync
|
||||
if (sync?.nativeEditableTypes !== 30 || sync.nativeEditableRecords !== 4361 || sync.opaqueTypes !== 50 || sync.opaqueRecords !== 466 || sync.exactPromotionReady !== false || sync.exactBlocker !== '50 runtime property types and 466 records remain opaque-only; complete native document and property semantics') fail('exact blocker synchronization is stale')
|
||||
if (!isDeepStrictEqual(sync, { nativeEditableTypes: semantics.supportSummary['native-editable-codec'].typeCount, nativeEditableRecords: semantics.supportSummary['native-editable-codec'].recordCount, opaqueTypes: semantics.supportSummary['opaque-fcstd-proxy'].typeCount, opaqueRecords: semantics.supportSummary['opaque-fcstd-proxy'].recordCount, exactPromotionReady: semantics.exactPromotionReady, exactBlocker: semantics.exactBlocker })) fail('promotion report diverges from global property semantics')
|
||||
if (roundTrip.classification?.zeroUnknownDrift !== true || roundTrip.classification.resavedSha256 !== nativeTarget || roundTrip.classification.resavedResourceMatches !== true || roundTrip.classification.externalResourceBoundary !== 'embedded FCStd ZIP entry restored to a read-only document transient file' || chrome.status !== 'pass' || chrome.persistence?.fcstdElement !== 'FileIncluded' || chrome.persistence.fcstdResourcePath !== 'chrome-resource.bin' || chrome.persistence.fcstdResourceBytes !== 16 || chrome.persistence.fcstdResourceMatches !== true || chrome.resource?.documentRoundTrip !== true || chrome.resource.independentReleased !== true || chrome.resource.markerRemoved !== true || chrome.release?.workerTerminated !== true) fail('G/H closure evidence regressed')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-fileincluded-promotion-pass', propertyType: report.propertyType, promotion: report.promotion, completedPhases: requiredPhases, exactBlockerSync: sync, systemExact: report.systemExact }, null, 2))
|
||||
24
scripts/check-freecad-property-fileincluded-roundtrip.mjs
Normal file
24
scripts/check-freecad-property-fileincluded-roundtrip.mjs
Normal file
@@ -0,0 +1,24 @@
|
||||
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-fileincluded-roundtrip.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyFileIncluded roundtrip check failed: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-fileincluded-roundtrip' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyFileIncluded') fail('baseline is invalid')
|
||||
const objectSet = [{ name: 'IncludedProbe', typeId: 'App::DocumentObjectFileIncluded' }]
|
||||
for (const [phase, snapshot] of [['nativeInitial', report.nativeInitial], ['nativeReopened', report.nativeAfter?.reopened], ['nativeResaved', report.nativeAfter?.resaved]]) {
|
||||
const object = snapshot?.object
|
||||
if (object?.name !== 'IncludedProbe' || object.typeId !== 'App::DocumentObjectFileIncluded' || object.propertyTypeId !== 'App::PropertyFileIncluded' || object.baseName !== 'payload.bin' || object.exists !== true || !Number.isSafeInteger(object.bytes) || object.bytes <= 0 || !/^[0-9a-f]{64}$/.test(object.sha256) || object.writeBits !== 0 || object.underTransientDir !== true || !isDeepStrictEqual(object.propertyStatus, []) || !isDeepStrictEqual(object.editorMode, []) || !isDeepStrictEqual(object.state, ['Up-to-date']) || object.statusString !== 'Valid' || object.hasShapeProperty !== false || !isDeepStrictEqual(snapshot.objectSet, objectSet)) fail(`${phase} native object semantics are incomplete`)
|
||||
}
|
||||
if (report.nativeInitial.object.sha256 !== report.assets?.initial?.sha256 || report.nativeAfter.reopened.object.sha256 !== report.assets?.target?.sha256 || report.nativeAfter.resaved.object.sha256 !== report.assets?.target?.sha256) fail('native transient bytes do not match the locked assets')
|
||||
const before = report.web?.before
|
||||
const after = report.web?.after
|
||||
if (before?.typeId !== 'App::PropertyFileIncluded' || before.element !== 'FileIncluded' || before.resourcePath !== 'payload.bin' || before.includedFileResource?.byteLength !== report.assets.initial.bytes || after?.typeId !== 'App::PropertyFileIncluded' || after.element !== 'FileIncluded' || after.resourcePath !== 'payload.bin' || after.includedFileResource?.byteLength !== report.assets.target.bytes) fail('Web property summaries are incomplete')
|
||||
const resource = report.web.resource
|
||||
if (resource?.path !== 'payload.bin' || resource.pathPreserved !== true || resource.beforeBytes !== report.assets.initial.bytes || resource.afterBytes !== report.assets.target.bytes || resource.beforeSha256 !== report.assets.initial.sha256 || resource.afterSha256 !== report.assets.target.sha256 || resource.changed !== true || report.web.targetMarkedTouched !== true || report.web.webOutputMatches !== true || report.web.semanticObjectsPreserved !== true || report.web.opaqueEntriesPreserved !== true || report.web.opaquePathsPreserved !== 0) fail('Web edit did not preserve native archive semantics')
|
||||
if (report.nativeResavedResource?.path !== 'payload.bin' || report.nativeResavedResource.byteLength !== report.assets.target.bytes) fail('native resave did not retain the embedded resource')
|
||||
const classification = report.classification
|
||||
if (classification?.archiveName !== 'payload.bin' || classification.initialSha256 !== report.assets.initial.sha256 || classification.targetSha256 !== report.assets.target.sha256 || classification.reopenedSha256 !== report.assets.target.sha256 || classification.resavedSha256 !== report.assets.target.sha256 || classification.nativeOutputMatches !== true || classification.objectSetPreserved !== true || classification.resavedResourceMatches !== true || classification.externalResourceBoundary !== 'embedded FCStd ZIP entry restored to a read-only document transient file' || classification.unknownSemanticDrift !== false || classification.zeroUnknownDrift !== true) fail('round-trip classification is not exact for PropertyFileIncluded')
|
||||
for (const artifact of [...Object.values(report.assets ?? {}), ...Object.values(report.archives ?? {})]) if (!artifact?.path || !Number.isSafeInteger(artifact.bytes) || artifact.bytes <= 0 || !/^[0-9a-f]{64}$/.test(artifact.sha256)) fail('asset or archive evidence is incomplete')
|
||||
console.log(JSON.stringify({ status: 'freecad-property-fileincluded-roundtrip-pass', hashes: { initial: classification.initialSha256, reopened: classification.reopenedSha256, resaved: classification.resavedSha256 }, resource, opaquePathsPreserved: report.web.opaquePathsPreserved, zeroUnknownDrift: classification.zeroUnknownDrift }, null, 2))
|
||||
56
scripts/check-freecad-property-fileincluded-success.mjs
Normal file
56
scripts/check-freecad-property-fileincluded-success.mjs
Normal file
@@ -0,0 +1,56 @@
|
||||
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, inventory] = await Promise.all([
|
||||
readFile(resolve(root, 'config/freecad-property-fileincluded-success.json'), 'utf8').then(JSON.parse),
|
||||
readFile(resolve(root, 'config/freecad-property-fileincluded-inventory.json'), 'utf8').then(JSON.parse),
|
||||
])
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyFileIncluded success check failed: ${message}`) }
|
||||
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-fileincluded-success' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyFileIncluded' || report.hostCaseCount !== 20 || report.hostCases?.length !== 20 || report.diagnostics?.exception !== null) fail('baseline or report boundary is invalid')
|
||||
|
||||
const setter = report.setterVariants
|
||||
const phaseIds = ['StringPath', 'BytesPath', 'TupleRename', 'OpenIoFile', 'Dictionary', 'CollisionA', 'CollisionB']
|
||||
const phaseBaseNames = ['payload.bin', 'payload-two.bin', 'renamed.dat', 'payload-two1.bin', 'payload1.bin', 'collision.bin', 'collision1.bin']
|
||||
if (setter?.phases?.map(({ id }) => id).join('|') !== phaseIds.join('|') || setter.phases.map(({ baseName }) => baseName).join('|') !== phaseBaseNames.join('|') || setter.objectCount !== 8 || setter.recomputeResult !== true) fail('setter success phases changed')
|
||||
for (const phase of setter.phases) {
|
||||
if (phase.propertyTypeId !== 'App::PropertyFileIncluded' || !isDeepStrictEqual(phase.propertyStatus, []) || !isDeepStrictEqual(phase.editorMode, []) || phase.exists !== true || phase.bytes <= 0 || phase.sha256 !== phase.sourceSha256 || phase.writeBits !== 0 || phase.underTransientDir !== true || !isDeepStrictEqual(phase.objectState, ['Touched']) || phase.statusString !== 'Touched' || phase.shape?.applicable !== false) fail(`${phase.id} did not preserve native bytes, ownership, state or metadata`)
|
||||
}
|
||||
const empty = setter.emptyString
|
||||
if (empty?.preserved !== true || empty.before?.value !== empty.after?.value || empty.before?.sha256 !== empty.after?.sha256 || empty.before?.bytes !== 38 || empty.after?.writeBits !== 0 || empty.after?.underTransientDir !== true) fail('empty-string no-op semantics changed')
|
||||
const collision = setter.collision
|
||||
if (collision?.distinctPaths !== true || collision.distinctBytes !== true || collision.first?.baseName !== 'collision.bin' || collision.second?.baseName !== 'collision1.bin' || collision.first.sha256 === collision.second.sha256 || collision.first.writeBits !== 0 || collision.second.writeBits !== 0) fail('same-name resource collision semantics changed')
|
||||
|
||||
const inventoryKeys = inventory.records.map(({ objectTypeId, propertyName }) => `${objectTypeId}.${propertyName}`)
|
||||
const caseKeys = report.hostCases.map(({ objectTypeId, propertyName }) => `${objectTypeId}.${propertyName}`)
|
||||
if (!isDeepStrictEqual(caseKeys, inventoryKeys)) fail('host cases do not cover the complete locked inventory in order')
|
||||
const expectedAssets = {
|
||||
bin: { bytes: 38, sha256: '537d3bf00d6bbc268e5dc4bd9f436ee937b8e481ad6aac5b7fcc6edb29bb3246' },
|
||||
wrl: { bytes: 54, sha256: 'b06eb69af003e2a1cc0e70e8833473a5dff8f0a4487d65fb96a5e79cc6e1b179' },
|
||||
png: { bytes: 68, sha256: '431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460' },
|
||||
csv: { bytes: 214, sha256: '75a789684c89a28de4dc9e8c40fe836e0cdbeeb2f58de251dd0734adb98bda3a' },
|
||||
skf: { bytes: 42, sha256: 'b32d9e3958b275212f00ab37b40e8cfb8f8fca5be7209713233cd55158031739' },
|
||||
pat: { bytes: 39, sha256: '2409d42fbc1592ac7e7aa6ebb7daf60cf4e1ed670c29da62deccc795e870ce98' },
|
||||
svg: { bytes: 88, sha256: '35b293e12fead414a67d8b703bb8cd7523e2361b52140430c7017b2e4780f723' },
|
||||
}
|
||||
for (const [index, entry] of report.hostCases.entries()) {
|
||||
const inventoryRecord = inventory.records[index]
|
||||
const expectedStatus = inventoryRecord.status.map(String)
|
||||
const expectedEditor = isDeepStrictEqual(inventoryRecord.status, ['ReadOnly']) ? ['ReadOnly'] : []
|
||||
const expectedAsset = expectedAssets[entry.source?.kind]
|
||||
const expectedBaseName = `host-${String(index).padStart(2, '0')}.${entry.source?.kind}`
|
||||
if (!expectedAsset || entry.source.bytes !== expectedAsset.bytes || entry.source.sha256 !== expectedAsset.sha256 || entry.archiveName !== expectedBaseName) fail(`${caseKeys[index]} source fixture changed`)
|
||||
for (const [phaseName, snapshot] of [['afterSet', entry.afterSet], ['afterRecompute', entry.afterRecompute]]) {
|
||||
if (snapshot?.propertyTypeId !== 'App::PropertyFileIncluded' || !isDeepStrictEqual(snapshot.propertyStatus, expectedStatus) || !isDeepStrictEqual(snapshot.editorMode, expectedEditor) || snapshot.baseName !== expectedBaseName || snapshot.exists !== true || snapshot.bytes !== expectedAsset.bytes || snapshot.sha256 !== expectedAsset.sha256 || snapshot.writeBits !== 0 || snapshot.underTransientDir !== true) fail(`${caseKeys[index]} ${phaseName} resource or metadata changed`)
|
||||
}
|
||||
if (entry.objectSetStable !== true || !isDeepStrictEqual(entry.afterRecompute.objectState, ['Up-to-date']) || entry.afterRecompute.statusString !== 'Valid') fail(`${caseKeys[index]} object set or final state changed`)
|
||||
const isOutput = isDeepStrictEqual(expectedStatus, ['27'])
|
||||
if (isOutput ? (entry.recomputeResult !== false || !isDeepStrictEqual(entry.afterSet.objectState, ['Up-to-date']) || entry.afterSet.statusString !== 'Valid') : (entry.recomputeResult !== true || !isDeepStrictEqual(entry.afterSet.objectState, ['Touched']) || entry.afterSet.statusString !== 'Touched')) fail(`${caseKeys[index]} touch/recompute behavior changed`)
|
||||
const shapeExpected = entry.objectTypeId === 'Sketcher::SketchObjectSF'
|
||||
if (entry.afterSet.shape?.applicable !== shapeExpected || shapeExpected && (entry.afterSet.shape.isNull !== true || entry.afterRecompute.shape.isNull !== true || entry.afterRecompute.shape.edges !== 0)) fail(`${caseKeys[index]} Shape applicability changed`)
|
||||
}
|
||||
if (report.hostCases.filter(({ default: value }) => value.exists).length !== 12 || report.hostCases.filter(({ afterSet }) => afterSet.exists).length !== 20 || report.hostCases.filter(({ afterSet, source }) => afterSet.sha256 === source.sha256).length !== 20 || report.hostCases.filter(({ afterSet }) => isDeepStrictEqual(afterSet.propertyStatus, ['ReadOnly'])).length !== 8 || report.hostCases.filter(({ afterSet }) => isDeepStrictEqual(afterSet.propertyStatus, ['27'])).length !== 1) fail('host success partition changed')
|
||||
|
||||
console.log(JSON.stringify({ status: 'freecad-property-fileincluded-success-pass', propertyType: report.propertyType, setterInputs: ['string', 'bytes', 'tuple', 'io-file', 'dictionary'], emptyStringNoOp: empty.preserved, collisionNames: [collision.first.baseName, collision.second.baseName], hostCases: report.hostCaseCount, byteExactCases: report.hostCases.filter(({ afterSet, source }) => afterSet.sha256 === source.sha256).length, readOnlyCases: 8, outputCases: 1, shapeApplicableCases: 1, finalValidCases: report.hostCases.filter(({ afterRecompute }) => afterRecompute.statusString === 'Valid').length }, null, 2))
|
||||
@@ -168,6 +168,7 @@ def collect_rejected():
|
||||
document.UndoMode = 1
|
||||
baseline = document_snapshot(document)
|
||||
baseline_objects = object_set_snapshot(document)
|
||||
baseline_source_shape_object = source.Shape.copy()
|
||||
baseline_source_shape = shape_snapshot_from_shape(source.Shape, include_brep=True)
|
||||
diagnostic = {
|
||||
"exceptionType": None,
|
||||
@@ -237,15 +238,19 @@ def collect_rejected():
|
||||
elif operation == "fillet":
|
||||
feature = document.addObject("Part::Fillet", "PairResult")
|
||||
feature.Base = source
|
||||
feature.Edges = [(index, 0.4, 0.4) for index in range(1, len(source.Shape.Edges) + 1)]
|
||||
edge_indices = [1] if pair == "thickness->fillet" else range(1, len(source.Shape.Edges) + 1)
|
||||
feature.Edges = [(index, 0.4, 0.4) for index in edge_indices]
|
||||
diagnostic["parameter"] = {"name": "radius", "value": 0.4}
|
||||
diagnostic["input"] = link_snapshot(feature.Base)
|
||||
diagnostic["input"]["selectedEdges"] = [entry[0] for entry in feature.Edges]
|
||||
elif operation == "chamfer":
|
||||
feature = document.addObject("Part::Chamfer", "PairResult")
|
||||
feature.Base = source
|
||||
feature.Edges = [(index, 0.4, 0.4) for index in range(1, len(source.Shape.Edges) + 1)]
|
||||
edge_indices = [1] if pair == "thickness->chamfer" else range(1, len(source.Shape.Edges) + 1)
|
||||
feature.Edges = [(index, 0.4, 0.4) for index in edge_indices]
|
||||
diagnostic["parameter"] = {"name": "distance", "value": 0.4}
|
||||
diagnostic["input"] = link_snapshot(feature.Base)
|
||||
diagnostic["input"]["selectedEdges"] = [entry[0] for entry in feature.Edges]
|
||||
elif operation == "draft":
|
||||
body = document.addObject("PartDesign::Body", "Body")
|
||||
draft_base = body.newObject("PartDesign::Feature", "DraftBase")
|
||||
@@ -302,6 +307,20 @@ def collect_rejected():
|
||||
if restored_source is None or not hasattr(restored_source, "Shape") or restored_source.Shape.isNull():
|
||||
raise RuntimeError("Fuse source Shape is absent after transaction abort")
|
||||
restored_source_shape = shape_snapshot_from_shape(restored_source.Shape, include_brep=True)
|
||||
source_shape_summary_restored = {
|
||||
key: value for key, value in baseline_source_shape.items() if key != "brepSha256"
|
||||
} == {
|
||||
key: value for key, value in restored_source_shape.items() if key != "brepSha256"
|
||||
}
|
||||
source_shape_topologically_equal = bool(restored_source.Shape.isEqual(baseline_source_shape_object))
|
||||
source_shape_symmetric_difference = {
|
||||
"beforeMinusAfterVolume": round(float(baseline_source_shape_object.cut(restored_source.Shape).Volume), 7),
|
||||
"afterMinusBeforeVolume": round(float(restored_source.Shape.cut(baseline_source_shape_object).Volume), 7),
|
||||
}
|
||||
source_shape_geometrically_restored = source_shape_summary_restored and all(
|
||||
value == 0 for value in source_shape_symmetric_difference.values()
|
||||
)
|
||||
source_brep_reserialized = baseline_source_shape["brepSha256"] != restored_source_shape["brepSha256"]
|
||||
initial_path = os.path.join(output_directory, "ordered-pair-rejection.FCStd")
|
||||
resaved_path = os.path.join(output_directory, "ordered-pair-rejection-resaved.FCStd")
|
||||
document.saveAs(initial_path)
|
||||
@@ -334,6 +353,8 @@ def collect_rejected():
|
||||
"freecadProfileAccepted": freecad_profile_accepted,
|
||||
"objectSetRestored": baseline_objects == restored_objects,
|
||||
"sourceShapeRestored": baseline_source_shape == restored_source_shape,
|
||||
"sourceShapeGeometricallyRestored": source_shape_geometrically_restored,
|
||||
"sourceShapeSummaryRestored": source_shape_summary_restored,
|
||||
"documentUnpolluted": baseline == initial,
|
||||
"documentStable": initial == reopened_snapshot and initial == resaved_snapshot,
|
||||
}
|
||||
@@ -357,10 +378,13 @@ def collect_rejected():
|
||||
"rollback": {
|
||||
"before": {"objects": baseline_objects, "sourceShape": baseline_source_shape},
|
||||
"after": {"objects": restored_objects, "sourceShape": restored_source_shape},
|
||||
"sourceBrepReserialized": source_brep_reserialized,
|
||||
"sourceShapeSymmetricDifference": source_shape_symmetric_difference,
|
||||
"sourceShapeTopologicallyEqual": source_shape_topologically_equal,
|
||||
},
|
||||
"phases": {"initial": initial, "reopened": reopened_snapshot, "resaved": resaved_snapshot},
|
||||
"checks": checks,
|
||||
"status": "pass" if all(checks.values()) or (operation in ("fillet", "chamfer", "thickness") and freecad_profile_accepted and all(value for key, value in checks.items() if key != "nativeFeatureRejected")) or (freecad_profile_no_op and all(value for key, value in checks.items() if key not in ("nativeFeatureRejected", "freecadProfileAccepted"))) or (not freecad_profile_accepted and feature_rejected and all(value for key, value in checks.items() if key != "freecadProfileAccepted")) else "failed",
|
||||
"status": "pass" if all(checks.values()) or (operation in ("fillet", "chamfer", "thickness") and freecad_profile_accepted and all(value for key, value in checks.items() if key != "nativeFeatureRejected")) or (freecad_profile_no_op and source_shape_geometrically_restored and all(value for key, value in checks.items() if key not in ("nativeFeatureRejected", "freecadProfileAccepted", "sourceShapeRestored"))) or (not freecad_profile_accepted and feature_rejected and all(value for key, value in checks.items() if key != "freecadProfileAccepted")) else "failed",
|
||||
}
|
||||
finally:
|
||||
for name in list(App.listDocuments().keys()):
|
||||
|
||||
152
scripts/freecad-property-acceleration-failure.py
Normal file
152
scripts/freecad-property-acceleration-failure.py
Normal file
@@ -0,0 +1,152 @@
|
||||
import json
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
INVALID_INPUTS = [
|
||||
("wrong-dimension", "5 mm"),
|
||||
("malformed-text", "not-a-quantity"),
|
||||
]
|
||||
|
||||
|
||||
def value_snapshot(obj):
|
||||
value = obj.Acceleration
|
||||
return {
|
||||
"raw": str(value),
|
||||
"numeric": round(float(value.Value), 9),
|
||||
"unit": str(value.Unit),
|
||||
}
|
||||
|
||||
|
||||
def object_snapshot(document):
|
||||
return [
|
||||
{"name": obj.Name, "typeId": obj.TypeId}
|
||||
for obj in document.Objects
|
||||
]
|
||||
|
||||
|
||||
def exception_snapshot(callback):
|
||||
try:
|
||||
callback()
|
||||
except Exception as error:
|
||||
return {"type": type(error).__name__, "message": str(error)}
|
||||
return None
|
||||
|
||||
|
||||
def active_transaction_snapshot():
|
||||
active = App.getActiveTransaction()
|
||||
if not active:
|
||||
return {"name": "", "id": 0}
|
||||
return {"name": str(active[0]), "id": int(active[1])}
|
||||
|
||||
|
||||
def run():
|
||||
document = App.newDocument("PropertyAccelerationFailure")
|
||||
try:
|
||||
obj = document.addObject("Robot::TrajectoryDressUpObject", "AccelerationProbe")
|
||||
document.recompute()
|
||||
document.UndoMode = 1
|
||||
initial = value_snapshot(obj)
|
||||
initial_objects = object_snapshot(document)
|
||||
failures = []
|
||||
for case_id, requested in INVALID_INPUTS:
|
||||
before = value_snapshot(obj)
|
||||
before_objects = object_snapshot(document)
|
||||
exception = exception_snapshot(lambda: setattr(obj, "Acceleration", requested))
|
||||
after = value_snapshot(obj)
|
||||
after_objects = object_snapshot(document)
|
||||
failures.append({
|
||||
"id": case_id,
|
||||
"requested": requested,
|
||||
"exception": exception,
|
||||
"before": before,
|
||||
"after": after,
|
||||
"beforeObjects": before_objects,
|
||||
"afterObjects": after_objects,
|
||||
"valuePreserved": before == after,
|
||||
"objectsPreserved": before_objects == after_objects,
|
||||
"polluted": before_objects != after_objects,
|
||||
})
|
||||
|
||||
disabled_before = value_snapshot(obj)
|
||||
disabled_objects_before = object_snapshot(document)
|
||||
obj.setEditorMode("Acceleration", ["ReadOnly"])
|
||||
obj.setPropertyStatus("Acceleration", "Immutable")
|
||||
disabled_status = [str(item) for item in obj.getPropertyStatus("Acceleration")]
|
||||
disabled_editor_mode = [str(item) for item in obj.getEditorMode("Acceleration")]
|
||||
disabled_exception = exception_snapshot(lambda: setattr(obj, "Acceleration", "750 mm/s^2"))
|
||||
disabled_after = value_snapshot(obj)
|
||||
disabled_objects_after = object_snapshot(document)
|
||||
obj.setPropertyStatus("Acceleration", "-Immutable")
|
||||
obj.setEditorMode("Acceleration", 0)
|
||||
disabled = {
|
||||
"classification": "editor-read-only-and-python-immutable",
|
||||
"requested": "750 mm/s^2",
|
||||
"status": disabled_status,
|
||||
"editorMode": disabled_editor_mode,
|
||||
"exception": disabled_exception,
|
||||
"before": disabled_before,
|
||||
"after": disabled_after,
|
||||
"beforeObjects": disabled_objects_before,
|
||||
"afterObjects": disabled_objects_after,
|
||||
"valuePreserved": disabled_before == disabled_after,
|
||||
"objectsPreserved": disabled_objects_before == disabled_objects_after,
|
||||
"restoredStatus": [str(item) for item in obj.getPropertyStatus("Acceleration")],
|
||||
"restoredEditorMode": [str(item) for item in obj.getEditorMode("Acceleration")],
|
||||
}
|
||||
|
||||
document.openTransaction("property-acceleration-cancel")
|
||||
transaction_before = value_snapshot(obj)
|
||||
transaction_objects_before = object_snapshot(document)
|
||||
obj.Acceleration = "250 mm/s^2"
|
||||
transaction_edited = value_snapshot(obj)
|
||||
transaction_pending_after_edit = bool(document.HasPendingTransaction)
|
||||
transaction_active_after_edit = active_transaction_snapshot()
|
||||
document.abortTransaction()
|
||||
transaction_after = value_snapshot(obj)
|
||||
transaction_objects_after = object_snapshot(document)
|
||||
final_objects = object_snapshot(document)
|
||||
return {
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-acceleration-failure",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"object": {"name": obj.Name, "typeId": obj.TypeId},
|
||||
"property": {"name": "Acceleration", "typeId": obj.getTypeIdOfProperty("Acceleration")},
|
||||
"initial": initial,
|
||||
"failures": failures,
|
||||
"disabled": disabled,
|
||||
"transaction": {
|
||||
"before": transaction_before,
|
||||
"edited": transaction_edited,
|
||||
"afterAbort": transaction_after,
|
||||
"objectsBefore": transaction_objects_before,
|
||||
"objectsAfter": transaction_objects_after,
|
||||
"undoMode": int(document.UndoMode),
|
||||
"pendingAfterEdit": transaction_pending_after_edit,
|
||||
"pendingAfterAbort": bool(document.HasPendingTransaction),
|
||||
"activeAfterEdit": transaction_active_after_edit,
|
||||
"activeAfterAbort": active_transaction_snapshot(),
|
||||
"restored": transaction_before == transaction_after,
|
||||
"objectsRestored": transaction_objects_before == transaction_objects_after,
|
||||
},
|
||||
"cancellationBoundary": {
|
||||
"supported": False,
|
||||
"classification": "synchronous-property-setter",
|
||||
"reason": "no-native-cancel-hook",
|
||||
"replacement": "abort-active-document-transaction",
|
||||
},
|
||||
"documentIntegrity": {
|
||||
"initialObjects": initial_objects,
|
||||
"finalObjects": final_objects,
|
||||
"objectsPreserved": initial_objects == final_objects,
|
||||
"objectCount": len(final_objects),
|
||||
},
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
print("FREECAD_PROPERTY_ACCELERATION_FAILURE_RESULT=" + json.dumps(run(), sort_keys=True))
|
||||
137
scripts/freecad-property-acceleration-mutation.py
Normal file
137
scripts/freecad-property-acceleration-mutation.py
Normal file
@@ -0,0 +1,137 @@
|
||||
import hashlib
|
||||
import json
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import FreeCAD as App
|
||||
import Robot
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
|
||||
|
||||
def quantity_snapshot(value):
|
||||
return {
|
||||
"raw": str(value),
|
||||
"numeric": round(float(value.Value), 9),
|
||||
"unit": str(value.Unit),
|
||||
}
|
||||
|
||||
|
||||
def trajectory_snapshot(obj):
|
||||
content = obj.Trajectory.Content
|
||||
root = ET.fromstring(content)
|
||||
waypoints = []
|
||||
for waypoint in root.findall("Waypoint"):
|
||||
waypoints.append({
|
||||
"name": waypoint.attrib["name"],
|
||||
"type": waypoint.attrib["type"],
|
||||
"position": [round(float(waypoint.attrib[key]), 9) for key in ("Px", "Py", "Pz")],
|
||||
"velocity": round(float(waypoint.attrib["vel"]), 9),
|
||||
"acceleration": round(float(waypoint.attrib["acc"]), 9),
|
||||
"continuous": waypoint.attrib["cont"] == "1",
|
||||
"tool": int(waypoint.attrib["tool"]),
|
||||
"base": int(waypoint.attrib["base"]),
|
||||
})
|
||||
return {
|
||||
"contentSha256": hashlib.sha256(content.encode("utf-8")).hexdigest(),
|
||||
"waypointCount": len(waypoints),
|
||||
"length": round(float(obj.Trajectory.Length), 9),
|
||||
"duration": round(float(obj.Trajectory.Duration), 9),
|
||||
"waypoints": waypoints,
|
||||
}
|
||||
|
||||
|
||||
def semantic_snapshot(obj):
|
||||
return {
|
||||
"value": quantity_snapshot(obj.Acceleration),
|
||||
"propertyStatus": [str(item) for item in obj.getPropertyStatus("Acceleration")],
|
||||
"editorMode": [str(item) for item in obj.getEditorMode("Acceleration")],
|
||||
"source": {"name": obj.Source.Name, "typeId": obj.Source.TypeId},
|
||||
"useAcceleration": bool(obj.UseAcceleration),
|
||||
"trajectory": trajectory_snapshot(obj),
|
||||
"shape": {"applicable": hasattr(obj, "Shape")},
|
||||
"objectState": [str(item) for item in obj.State],
|
||||
"statusString": str(obj.getStatusString()),
|
||||
"mustExecute": bool(obj.MustExecute),
|
||||
}
|
||||
|
||||
|
||||
def object_snapshot(document):
|
||||
return [{"name": obj.Name, "typeId": obj.TypeId} for obj in document.Objects]
|
||||
|
||||
|
||||
def run():
|
||||
document = App.newDocument("PropertyAccelerationMutation")
|
||||
try:
|
||||
source = document.addObject("Robot::TrajectoryObject", "SourceTrajectory")
|
||||
trajectory = source.Trajectory
|
||||
trajectory = trajectory.insertWaypoints(App.Placement(App.Vector(1.0, 2.0, 3.0), App.Rotation()))
|
||||
trajectory = trajectory.insertWaypoints(App.Placement(App.Vector(4.0, 6.0, 3.0), App.Rotation()))
|
||||
source.Trajectory = trajectory
|
||||
|
||||
obj = document.addObject("Robot::TrajectoryDressUpObject", "AccelerationProbe")
|
||||
obj.Source = source
|
||||
obj.UseAcceleration = True
|
||||
initial_recompute = int(document.recompute())
|
||||
before = semantic_snapshot(obj)
|
||||
source_before = trajectory_snapshot(source)
|
||||
objects_before = object_snapshot(document)
|
||||
|
||||
obj.Acceleration = "500 mm/s^2"
|
||||
touched_after_edit = {
|
||||
"value": quantity_snapshot(obj.Acceleration),
|
||||
"objectState": [str(item) for item in obj.State],
|
||||
"mustExecute": bool(obj.MustExecute),
|
||||
}
|
||||
edit_recompute = int(document.recompute())
|
||||
edited = semantic_snapshot(obj)
|
||||
source_edited = trajectory_snapshot(source)
|
||||
|
||||
obj.Acceleration = "1000 mm/s^2"
|
||||
restore_recompute = int(document.recompute())
|
||||
restored = semantic_snapshot(obj)
|
||||
source_restored = trajectory_snapshot(source)
|
||||
objects_restored = object_snapshot(document)
|
||||
|
||||
return {
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-acceleration-mutation",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"object": {"name": obj.Name, "typeId": obj.TypeId},
|
||||
"property": {"name": "Acceleration", "typeId": obj.getTypeIdOfProperty("Acceleration")},
|
||||
"recompute": {
|
||||
"initial": initial_recompute,
|
||||
"edit": edit_recompute,
|
||||
"restore": restore_recompute,
|
||||
},
|
||||
"touchedAfterEdit": touched_after_edit,
|
||||
"before": before,
|
||||
"edited": edited,
|
||||
"restored": restored,
|
||||
"source": {
|
||||
"before": source_before,
|
||||
"edited": source_edited,
|
||||
"restored": source_restored,
|
||||
"preserved": source_before == source_edited == source_restored,
|
||||
},
|
||||
"document": {
|
||||
"objectsBefore": objects_before,
|
||||
"objectsRestored": objects_restored,
|
||||
"objectsPreserved": objects_before == objects_restored,
|
||||
},
|
||||
"classification": {
|
||||
"propertyChanged": before["value"] != edited["value"],
|
||||
"trajectoryChanged": before["trajectory"] != edited["trajectory"],
|
||||
"propertyRestored": before["value"] == restored["value"],
|
||||
"trajectoryRestored": before["trajectory"] == restored["trajectory"],
|
||||
"semanticStateRestored": before == restored,
|
||||
"geometry": "not-applicable-no-shape-property",
|
||||
},
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
print("FREECAD_PROPERTY_ACCELERATION_MUTATION_RESULT=" + json.dumps(run(), sort_keys=True))
|
||||
116
scripts/freecad-property-acceleration-roundtrip.py
Normal file
116
scripts/freecad-property-acceleration-roundtrip.py
Normal file
@@ -0,0 +1,116 @@
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import FreeCAD as App
|
||||
import Robot
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
|
||||
|
||||
def trajectory_snapshot(obj):
|
||||
content = obj.Trajectory.Content
|
||||
root = ET.fromstring(content)
|
||||
waypoints = [
|
||||
{
|
||||
"name": node.attrib["name"],
|
||||
"type": node.attrib["type"],
|
||||
"position": [round(float(node.attrib[key]), 9) for key in ("Px", "Py", "Pz")],
|
||||
"velocity": round(float(node.attrib["vel"]), 9),
|
||||
"acceleration": round(float(node.attrib["acc"]), 9),
|
||||
}
|
||||
for node in root.findall("Waypoint")
|
||||
]
|
||||
return {
|
||||
"contentSha256": hashlib.sha256(content.encode("utf-8")).hexdigest(),
|
||||
"waypointCount": len(waypoints),
|
||||
"length": round(float(obj.Trajectory.Length), 9),
|
||||
"duration": round(float(obj.Trajectory.Duration), 9),
|
||||
"waypoints": waypoints,
|
||||
}
|
||||
|
||||
|
||||
def object_snapshot(document):
|
||||
obj = document.getObject("AccelerationProbe")
|
||||
source = document.getObject("SourceTrajectory")
|
||||
return {
|
||||
"objectSet": [{"name": item.Name, "typeId": item.TypeId} for item in document.Objects],
|
||||
"object": {
|
||||
"name": obj.Name,
|
||||
"typeId": obj.TypeId,
|
||||
"propertyTypeId": obj.getTypeIdOfProperty("Acceleration"),
|
||||
"value": {"raw": str(obj.Acceleration), "numeric": round(float(obj.Acceleration.Value), 9), "unit": str(obj.Acceleration.Unit)},
|
||||
"propertyStatus": [str(item) for item in obj.getPropertyStatus("Acceleration")],
|
||||
"editorMode": [str(item) for item in obj.getEditorMode("Acceleration")],
|
||||
"source": {"name": obj.Source.Name, "typeId": obj.Source.TypeId},
|
||||
"useAcceleration": bool(obj.UseAcceleration),
|
||||
"state": [str(item) for item in obj.State],
|
||||
"statusString": str(obj.getStatusString()),
|
||||
"trajectory": trajectory_snapshot(obj),
|
||||
},
|
||||
"sourceTrajectory": trajectory_snapshot(source),
|
||||
}
|
||||
|
||||
|
||||
def create_native(path):
|
||||
document = App.newDocument("PropertyAccelerationRoundtrip")
|
||||
try:
|
||||
source = document.addObject("Robot::TrajectoryObject", "SourceTrajectory")
|
||||
trajectory = source.Trajectory
|
||||
trajectory = trajectory.insertWaypoints(App.Placement(App.Vector(1.0, 2.0, 3.0), App.Rotation()))
|
||||
trajectory = trajectory.insertWaypoints(App.Placement(App.Vector(4.0, 6.0, 3.0), App.Rotation()))
|
||||
source.Trajectory = trajectory
|
||||
obj = document.addObject("Robot::TrajectoryDressUpObject", "AccelerationProbe")
|
||||
obj.Source = source
|
||||
obj.UseAcceleration = True
|
||||
obj.Acceleration = "1000 mm/s^2"
|
||||
document.recompute()
|
||||
document.saveAs(path)
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
reopened = App.openDocument(path)
|
||||
try:
|
||||
reopened.recompute()
|
||||
return object_snapshot(reopened)
|
||||
finally:
|
||||
App.closeDocument(reopened.Name)
|
||||
|
||||
|
||||
def verify_native(path, resaved_path):
|
||||
document = App.openDocument(path)
|
||||
try:
|
||||
document.recompute()
|
||||
reopened = object_snapshot(document)
|
||||
document.saveAs(resaved_path)
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
resaved_document = App.openDocument(resaved_path)
|
||||
try:
|
||||
resaved_document.recompute()
|
||||
resaved = object_snapshot(resaved_document)
|
||||
finally:
|
||||
App.closeDocument(resaved_document.Name)
|
||||
return {"reopened": reopened, "resaved": resaved}
|
||||
|
||||
|
||||
mode = os.environ.get("FREECAD_PROPERTY_ACCELERATION_ROUNDTRIP_MODE", "")
|
||||
path = os.environ.get("FREECAD_PROPERTY_ACCELERATION_ROUNDTRIP_PATH", "")
|
||||
resaved_path = os.environ.get("FREECAD_PROPERTY_ACCELERATION_ROUNDTRIP_RESAVED_PATH", "")
|
||||
if mode == "create":
|
||||
result = create_native(path)
|
||||
elif mode == "verify":
|
||||
result = verify_native(path, resaved_path)
|
||||
else:
|
||||
raise RuntimeError("FREECAD_PROPERTY_ACCELERATION_ROUNDTRIP_MODE must be create or verify")
|
||||
|
||||
print("FREECAD_PROPERTY_ACCELERATION_ROUNDTRIP_RESULT=" + json.dumps({
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-acceleration-roundtrip",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"mode": mode,
|
||||
"result": result,
|
||||
}, sort_keys=True))
|
||||
77
scripts/freecad-property-acceleration-success.py
Normal file
77
scripts/freecad-property-acceleration-success.py
Normal file
@@ -0,0 +1,77 @@
|
||||
import json
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
CASES = [
|
||||
("default", None),
|
||||
("nominal", "500 mm/s^2"),
|
||||
("lower-bound", "0 mm/s^2"),
|
||||
("negative-boundary", "-500 mm/s^2"),
|
||||
]
|
||||
|
||||
|
||||
def value_snapshot(obj):
|
||||
value = getattr(obj, "Acceleration")
|
||||
return {
|
||||
"raw": str(value),
|
||||
"numeric": round(float(value.Value), 9),
|
||||
"unit": str(value.Unit),
|
||||
}
|
||||
|
||||
|
||||
def shape_snapshot(obj):
|
||||
if not hasattr(obj, "Shape"):
|
||||
return {"applicable": False}
|
||||
shape = obj.Shape
|
||||
if shape.isNull():
|
||||
return {"applicable": True, "shapeNull": True, "shapeValid": None, "solids": 0, "faces": 0}
|
||||
return {
|
||||
"applicable": True,
|
||||
"shapeNull": False,
|
||||
"shapeValid": bool(shape.isValid()),
|
||||
"solids": len(shape.Solids),
|
||||
"faces": len(shape.Faces),
|
||||
}
|
||||
|
||||
|
||||
def run():
|
||||
document = App.newDocument("PropertyAccelerationSuccess")
|
||||
try:
|
||||
obj = document.addObject("Robot::TrajectoryDressUpObject", "AccelerationProbe")
|
||||
document.recompute()
|
||||
cases = []
|
||||
for case_id, requested in CASES:
|
||||
if requested is not None:
|
||||
obj.Acceleration = requested
|
||||
document.recompute()
|
||||
cases.append({
|
||||
"id": case_id,
|
||||
"requested": requested,
|
||||
"value": value_snapshot(obj),
|
||||
"status": [str(item) for item in obj.getPropertyStatus("Acceleration")],
|
||||
"typeId": obj.getTypeIdOfProperty("Acceleration"),
|
||||
"group": obj.getGroupOfProperty("Acceleration"),
|
||||
"editorMode": [str(item) for item in obj.getEditorMode("Acceleration")],
|
||||
"shape": shape_snapshot(obj),
|
||||
"objectState": [str(item) for item in obj.State],
|
||||
})
|
||||
return {
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-acceleration-success",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"object": {"name": obj.Name, "typeId": obj.TypeId},
|
||||
"property": {"name": "Acceleration", "typeId": obj.getTypeIdOfProperty("Acceleration"), "group": obj.getGroupOfProperty("Acceleration")},
|
||||
"cases": cases,
|
||||
"diagnostics": {"exception": None, "documentObjectCount": len(document.Objects)},
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
print("FREECAD_PROPERTY_ACCELERATION_SUCCESS_RESULT=" + json.dumps(run(), sort_keys=True))
|
||||
190
scripts/freecad-property-area-failure.py
Normal file
190
scripts/freecad-property-area-failure.py
Normal file
@@ -0,0 +1,190 @@
|
||||
import json
|
||||
|
||||
import FreeCAD as App
|
||||
import Part
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
|
||||
|
||||
def object_set(document):
|
||||
return [{"name": obj.Name, "typeId": obj.TypeId} for obj in document.Objects]
|
||||
|
||||
|
||||
def area_snapshot(measure):
|
||||
value = measure.Area
|
||||
return {
|
||||
"raw": str(value),
|
||||
"numeric": round(float(value.Value), 9),
|
||||
"unit": str(value.Unit),
|
||||
"elements": [
|
||||
{"object": obj.Name, "subElements": list(sub_elements)}
|
||||
for obj, sub_elements in measure.Elements
|
||||
],
|
||||
"state": [str(item) for item in measure.State],
|
||||
"statusString": str(measure.getStatusString()),
|
||||
}
|
||||
|
||||
|
||||
def exception_snapshot(callback):
|
||||
try:
|
||||
callback()
|
||||
except Exception as error:
|
||||
return {"type": type(error).__name__, "message": str(error)}
|
||||
return None
|
||||
|
||||
|
||||
def active_transaction_snapshot():
|
||||
active = App.getActiveTransaction()
|
||||
if not active:
|
||||
return {"name": "", "id": 0}
|
||||
return {"name": str(active[0]), "id": int(active[1])}
|
||||
|
||||
|
||||
def run():
|
||||
document = App.newDocument("PropertyAreaFailure")
|
||||
try:
|
||||
box = document.addObject("Part::Box", "AreaBox")
|
||||
box.Length = 10
|
||||
box.Width = 5
|
||||
box.Height = 2
|
||||
cylinder = document.addObject("Part::Cylinder", "AreaCylinder")
|
||||
cylinder.Radius = 3
|
||||
cylinder.Height = 4
|
||||
point = document.addObject("Part::Feature", "AreaPoint")
|
||||
point.Shape = Part.Vertex(App.Vector(0, 0, 0))
|
||||
line = document.addObject("Part::Feature", "AreaLine")
|
||||
line.Shape = Part.makeLine(App.Vector(0, 0, 0), App.Vector(4, 0, 0))
|
||||
measure = document.addObject("Measure::MeasureArea", "AreaProbe")
|
||||
measure.Elements = [(box, ["Face1"])]
|
||||
document.recompute()
|
||||
document.UndoMode = 1
|
||||
initial = area_snapshot(measure)
|
||||
initial_objects = object_set(document)
|
||||
|
||||
failures = []
|
||||
for case_id, source, sub_element, shape_type in [
|
||||
("unsupported-point", point, "Vertex1", "Vertex"),
|
||||
("unsupported-line", line, "Edge1", "Edge"),
|
||||
]:
|
||||
measure.Elements = [(box, ["Face1"])]
|
||||
document.recompute()
|
||||
before = area_snapshot(measure)
|
||||
before_objects = object_set(document)
|
||||
assignment_exception = exception_snapshot(lambda: setattr(measure, "Elements", [(source, [sub_element])]))
|
||||
recompute_exception = None
|
||||
recompute_result = None
|
||||
try:
|
||||
recompute_result = bool(document.recompute())
|
||||
except Exception as error:
|
||||
recompute_exception = {"type": type(error).__name__, "message": str(error)}
|
||||
after = area_snapshot(measure)
|
||||
after_objects = object_set(document)
|
||||
measure.Elements = [(box, ["Face1"])]
|
||||
document.recompute()
|
||||
recovered = area_snapshot(measure)
|
||||
failures.append({
|
||||
"id": case_id,
|
||||
"source": {"name": source.Name, "typeId": source.TypeId, "subElement": sub_element, "shapeType": shape_type},
|
||||
"expectedDiagnostic": "Cannot calculate area",
|
||||
"assignmentException": assignment_exception,
|
||||
"recomputeException": recompute_exception,
|
||||
"recomputeResult": recompute_result,
|
||||
"before": before,
|
||||
"after": after,
|
||||
"recovered": recovered,
|
||||
"beforeObjects": before_objects,
|
||||
"afterObjects": after_objects,
|
||||
"objectsPreserved": before_objects == after_objects,
|
||||
"valuePreserved": before["numeric"] == after["numeric"],
|
||||
"recoveredExactly": before["numeric"] == recovered["numeric"] and before["elements"] == recovered["elements"],
|
||||
})
|
||||
|
||||
measure.Elements = [(box, ["Face1"])]
|
||||
document.recompute()
|
||||
disabled_before = area_snapshot(measure)
|
||||
disabled_objects_before = object_set(document)
|
||||
editor_read_only_exception = exception_snapshot(lambda: setattr(measure, "Area", "20 mm^2"))
|
||||
editor_read_only_after = area_snapshot(measure)
|
||||
measure.setPropertyStatus("Area", "Immutable")
|
||||
immutable_status = [str(item) for item in measure.getPropertyStatus("Area")]
|
||||
immutable_exception = exception_snapshot(lambda: setattr(measure, "Area", "30 mm^2"))
|
||||
immutable_after = area_snapshot(measure)
|
||||
measure.setPropertyStatus("Area", "-Immutable")
|
||||
restored_status = [str(item) for item in measure.getPropertyStatus("Area")]
|
||||
restored_editor_mode = [str(item) for item in measure.getEditorMode("Area")]
|
||||
measure.Elements = []
|
||||
measure.Elements = [(box, ["Face1"])]
|
||||
document.recompute()
|
||||
disabled_restored = area_snapshot(measure)
|
||||
disabled_objects_after = object_set(document)
|
||||
disabled = {
|
||||
"classification": "editor-readonly-python-mutable-until-immutable",
|
||||
"editorReadOnlyRequested": "20 mm^2",
|
||||
"immutableRequested": "30 mm^2",
|
||||
"propertyStatus": ["24", "27"],
|
||||
"editorMode": [str(item) for item in measure.getEditorMode("Area")],
|
||||
"editorReadOnlyException": editor_read_only_exception,
|
||||
"editorReadOnlyAfter": editor_read_only_after,
|
||||
"pythonBypassesEditorReadOnly": editor_read_only_exception is None and editor_read_only_after["numeric"] == 20,
|
||||
"immutableStatus": immutable_status,
|
||||
"immutableException": immutable_exception,
|
||||
"immutableAfter": immutable_after,
|
||||
"before": disabled_before,
|
||||
"restored": disabled_restored,
|
||||
"restoredStatus": restored_status,
|
||||
"restoredEditorMode": restored_editor_mode,
|
||||
"immutableValuePreserved": editor_read_only_after == immutable_after,
|
||||
"valueRestored": disabled_before["numeric"] == disabled_restored["numeric"] and disabled_before["elements"] == disabled_restored["elements"],
|
||||
"objectsPreserved": disabled_objects_before == disabled_objects_after,
|
||||
}
|
||||
|
||||
document.openTransaction("property-area-cancel")
|
||||
transaction_before = area_snapshot(measure)
|
||||
transaction_objects_before = object_set(document)
|
||||
measure.Elements = [(cylinder, ["Face1"])]
|
||||
document.recompute()
|
||||
transaction_edited = area_snapshot(measure)
|
||||
transaction_pending_after_edit = bool(document.HasPendingTransaction)
|
||||
transaction_active_after_edit = active_transaction_snapshot()
|
||||
document.abortTransaction()
|
||||
transaction_after_abort = area_snapshot(measure)
|
||||
document.recompute()
|
||||
transaction_after_recompute = area_snapshot(measure)
|
||||
transaction_objects_after = object_set(document)
|
||||
|
||||
return {
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-area-failure",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"object": {"name": measure.Name, "typeId": measure.TypeId},
|
||||
"property": {"name": "Area", "typeId": measure.getTypeIdOfProperty("Area")},
|
||||
"input": {"name": "Elements", "typeId": measure.getTypeIdOfProperty("Elements")},
|
||||
"initial": initial,
|
||||
"failures": failures,
|
||||
"disabled": disabled,
|
||||
"transaction": {
|
||||
"before": transaction_before,
|
||||
"edited": transaction_edited,
|
||||
"afterAbort": transaction_after_abort,
|
||||
"afterRecompute": transaction_after_recompute,
|
||||
"objectsBefore": transaction_objects_before,
|
||||
"objectsAfter": transaction_objects_after,
|
||||
"undoMode": int(document.UndoMode),
|
||||
"pendingAfterEdit": transaction_pending_after_edit,
|
||||
"pendingAfterAbort": bool(document.HasPendingTransaction),
|
||||
"activeAfterEdit": transaction_active_after_edit,
|
||||
"activeAfterAbort": active_transaction_snapshot(),
|
||||
"restored": transaction_before["numeric"] == transaction_after_recompute["numeric"] and transaction_before["elements"] == transaction_after_recompute["elements"],
|
||||
"objectsRestored": transaction_objects_before == transaction_objects_after,
|
||||
},
|
||||
"cancellationBoundary": {"supported": False, "classification": "synchronous-measure-recompute", "reason": "no-native-cancel-hook", "replacement": "abort-active-document-transaction"},
|
||||
"documentIntegrity": {"initialObjects": initial_objects, "finalObjects": object_set(document), "objectsPreserved": initial_objects == object_set(document), "objectCount": len(document.Objects)},
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
print("FREECAD_PROPERTY_AREA_FAILURE_RESULT=" + json.dumps(run(), sort_keys=True))
|
||||
143
scripts/freecad-property-area-mutation.py
Normal file
143
scripts/freecad-property-area-mutation.py
Normal file
@@ -0,0 +1,143 @@
|
||||
import json
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
|
||||
|
||||
def quantity_snapshot(value):
|
||||
return {
|
||||
"raw": str(value),
|
||||
"numeric": round(float(value.Value), 9),
|
||||
"unit": str(value.Unit),
|
||||
}
|
||||
|
||||
|
||||
def elements_snapshot(measure):
|
||||
return [
|
||||
{"object": obj.Name, "typeId": obj.TypeId, "subElements": list(sub_elements)}
|
||||
for obj, sub_elements in measure.Elements
|
||||
]
|
||||
|
||||
|
||||
def semantic_snapshot(measure):
|
||||
return {
|
||||
"value": quantity_snapshot(measure.Area),
|
||||
"elements": elements_snapshot(measure),
|
||||
"propertyStatus": [str(item) for item in measure.getPropertyStatus("Area")],
|
||||
"editorMode": [str(item) for item in measure.getEditorMode("Area")],
|
||||
"shape": {"applicable": hasattr(measure, "Shape")},
|
||||
"objectState": [str(item) for item in measure.State],
|
||||
"statusString": str(measure.getStatusString()),
|
||||
"mustExecute": bool(measure.MustExecute),
|
||||
}
|
||||
|
||||
|
||||
def shape_snapshot(obj):
|
||||
shape = obj.Shape
|
||||
bounds = shape.BoundBox
|
||||
return {
|
||||
"name": obj.Name,
|
||||
"typeId": obj.TypeId,
|
||||
"shapeType": shape.ShapeType,
|
||||
"valid": bool(shape.isValid()),
|
||||
"solids": len(shape.Solids),
|
||||
"faces": len(shape.Faces),
|
||||
"edges": len(shape.Edges),
|
||||
"vertices": len(shape.Vertexes),
|
||||
"volume": round(float(shape.Volume), 9),
|
||||
"area": round(float(shape.Area), 9),
|
||||
"bounds": [
|
||||
round(float(value), 9)
|
||||
for value in (bounds.XMin, bounds.YMin, bounds.ZMin, bounds.XMax, bounds.YMax, bounds.ZMax)
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def object_snapshot(document):
|
||||
return [{"name": obj.Name, "typeId": obj.TypeId} for obj in document.Objects]
|
||||
|
||||
|
||||
def run():
|
||||
document = App.newDocument("PropertyAreaMutation")
|
||||
try:
|
||||
box = document.addObject("Part::Box", "AreaBox")
|
||||
box.Length = 10
|
||||
box.Width = 5
|
||||
box.Height = 2
|
||||
cylinder = document.addObject("Part::Cylinder", "AreaCylinder")
|
||||
cylinder.Radius = 3
|
||||
cylinder.Height = 4
|
||||
measure = document.addObject("Measure::MeasureArea", "AreaProbe")
|
||||
measure.Elements = [(box, ["Face1"])]
|
||||
initial_recompute = int(document.recompute())
|
||||
|
||||
before = semantic_snapshot(measure)
|
||||
sources_before = [shape_snapshot(source) for source in (box, cylinder)]
|
||||
objects_before = object_snapshot(document)
|
||||
|
||||
measure.Elements = [(box, ["Face1", "Face2"]), (cylinder, ["Face1"])]
|
||||
touched_after_edit = {
|
||||
"value": quantity_snapshot(measure.Area),
|
||||
"elements": elements_snapshot(measure),
|
||||
"objectState": [str(item) for item in measure.State],
|
||||
"mustExecute": bool(measure.MustExecute),
|
||||
}
|
||||
edit_recompute = int(document.recompute())
|
||||
edited = semantic_snapshot(measure)
|
||||
sources_edited = [shape_snapshot(source) for source in (box, cylinder)]
|
||||
|
||||
measure.Elements = [(box, ["Face1"])]
|
||||
touched_after_restore = {
|
||||
"value": quantity_snapshot(measure.Area),
|
||||
"elements": elements_snapshot(measure),
|
||||
"objectState": [str(item) for item in measure.State],
|
||||
"mustExecute": bool(measure.MustExecute),
|
||||
}
|
||||
restore_recompute = int(document.recompute())
|
||||
restored = semantic_snapshot(measure)
|
||||
sources_restored = [shape_snapshot(source) for source in (box, cylinder)]
|
||||
objects_restored = object_snapshot(document)
|
||||
|
||||
return {
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-area-mutation",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"object": {"name": measure.Name, "typeId": measure.TypeId},
|
||||
"property": {"name": "Area", "typeId": measure.getTypeIdOfProperty("Area")},
|
||||
"input": {"name": "Elements", "typeId": measure.getTypeIdOfProperty("Elements")},
|
||||
"recompute": {"initial": initial_recompute, "edit": edit_recompute, "restore": restore_recompute},
|
||||
"touchedAfterEdit": touched_after_edit,
|
||||
"touchedAfterRestore": touched_after_restore,
|
||||
"before": before,
|
||||
"edited": edited,
|
||||
"restored": restored,
|
||||
"sources": {
|
||||
"before": sources_before,
|
||||
"edited": sources_edited,
|
||||
"restored": sources_restored,
|
||||
"preserved": sources_before == sources_edited == sources_restored,
|
||||
},
|
||||
"document": {
|
||||
"objectsBefore": objects_before,
|
||||
"objectsRestored": objects_restored,
|
||||
"objectsPreserved": objects_before == objects_restored,
|
||||
},
|
||||
"classification": {
|
||||
"inputChanged": before["elements"] != edited["elements"],
|
||||
"derivedValueChanged": before["value"] != edited["value"],
|
||||
"inputRestored": before["elements"] == restored["elements"],
|
||||
"derivedValueRestored": before["value"] == restored["value"],
|
||||
"semanticStateRestored": before == restored,
|
||||
"sourceGeometryPreserved": sources_before == sources_edited == sources_restored,
|
||||
"geometry": "source-shapes-preserved-derived-measurement-has-no-shape",
|
||||
},
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
print("FREECAD_PROPERTY_AREA_MUTATION_RESULT=" + json.dumps(run(), sort_keys=True))
|
||||
117
scripts/freecad-property-area-roundtrip.py
Normal file
117
scripts/freecad-property-area-roundtrip.py
Normal file
@@ -0,0 +1,117 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
|
||||
|
||||
def shape_snapshot(obj):
|
||||
shape = obj.Shape
|
||||
bounds = shape.BoundBox
|
||||
return {
|
||||
"name": obj.Name,
|
||||
"typeId": obj.TypeId,
|
||||
"valid": bool(shape.isValid()),
|
||||
"shapeType": shape.ShapeType,
|
||||
"solids": len(shape.Solids),
|
||||
"faces": len(shape.Faces),
|
||||
"edges": len(shape.Edges),
|
||||
"vertices": len(shape.Vertexes),
|
||||
"volume": round(float(shape.Volume), 9),
|
||||
"area": round(float(shape.Area), 9),
|
||||
"bounds": [
|
||||
round(float(value), 9)
|
||||
for value in (bounds.XMin, bounds.YMin, bounds.ZMin, bounds.XMax, bounds.YMax, bounds.ZMax)
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def measure_snapshot(document):
|
||||
measure = document.getObject("AreaProbe")
|
||||
box = document.getObject("AreaBox")
|
||||
return {
|
||||
"objectSet": [{"name": obj.Name, "typeId": obj.TypeId} for obj in document.Objects],
|
||||
"source": {
|
||||
**shape_snapshot(box),
|
||||
"length": round(float(box.Length.Value), 9),
|
||||
"width": round(float(box.Width.Value), 9),
|
||||
"height": round(float(box.Height.Value), 9),
|
||||
},
|
||||
"measure": {
|
||||
"name": measure.Name,
|
||||
"typeId": measure.TypeId,
|
||||
"areaTypeId": measure.getTypeIdOfProperty("Area"),
|
||||
"elementsTypeId": measure.getTypeIdOfProperty("Elements"),
|
||||
"area": {"raw": str(measure.Area), "numeric": round(float(measure.Area.Value), 9), "unit": str(measure.Area.Unit)},
|
||||
"elements": [
|
||||
{"object": obj.Name, "typeId": obj.TypeId, "subElements": list(sub_elements)}
|
||||
for obj, sub_elements in measure.Elements
|
||||
],
|
||||
"propertyStatus": [str(item) for item in measure.getPropertyStatus("Area")],
|
||||
"editorMode": [str(item) for item in measure.getEditorMode("Area")],
|
||||
"state": [str(item) for item in measure.State],
|
||||
"statusString": str(measure.getStatusString()),
|
||||
"shape": {"applicable": hasattr(measure, "Shape")},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def create_native(path):
|
||||
document = App.newDocument("PropertyAreaRoundtrip")
|
||||
try:
|
||||
box = document.addObject("Part::Box", "AreaBox")
|
||||
box.Length = 10
|
||||
box.Width = 5
|
||||
box.Height = 2
|
||||
measure = document.addObject("Measure::MeasureArea", "AreaProbe")
|
||||
measure.Elements = [(box, ["Face1"])]
|
||||
document.recompute()
|
||||
document.saveAs(path)
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
reopened = App.openDocument(path)
|
||||
try:
|
||||
reopened.recompute()
|
||||
return measure_snapshot(reopened)
|
||||
finally:
|
||||
App.closeDocument(reopened.Name)
|
||||
|
||||
|
||||
def verify_native(path, resaved_path):
|
||||
document = App.openDocument(path)
|
||||
try:
|
||||
document.recompute()
|
||||
reopened = measure_snapshot(document)
|
||||
document.saveAs(resaved_path)
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
resaved_document = App.openDocument(resaved_path)
|
||||
try:
|
||||
resaved_document.recompute()
|
||||
resaved = measure_snapshot(resaved_document)
|
||||
finally:
|
||||
App.closeDocument(resaved_document.Name)
|
||||
return {"reopened": reopened, "resaved": resaved}
|
||||
|
||||
|
||||
mode = os.environ.get("FREECAD_PROPERTY_AREA_ROUNDTRIP_MODE", "")
|
||||
path = os.environ.get("FREECAD_PROPERTY_AREA_ROUNDTRIP_PATH", "")
|
||||
resaved_path = os.environ.get("FREECAD_PROPERTY_AREA_ROUNDTRIP_RESAVED_PATH", "")
|
||||
if mode == "create":
|
||||
result = create_native(path)
|
||||
elif mode == "verify":
|
||||
result = verify_native(path, resaved_path)
|
||||
else:
|
||||
raise RuntimeError("FREECAD_PROPERTY_AREA_ROUNDTRIP_MODE must be create or verify")
|
||||
|
||||
print("FREECAD_PROPERTY_AREA_ROUNDTRIP_RESULT=" + json.dumps({
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-area-roundtrip",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"mode": mode,
|
||||
"result": result,
|
||||
}, sort_keys=True))
|
||||
111
scripts/freecad-property-area-success.py
Normal file
111
scripts/freecad-property-area-success.py
Normal file
@@ -0,0 +1,111 @@
|
||||
import json
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
|
||||
|
||||
def quantity_snapshot(value):
|
||||
return {
|
||||
"raw": str(value),
|
||||
"numeric": round(float(value.Value), 9),
|
||||
"unit": str(value.Unit),
|
||||
}
|
||||
|
||||
|
||||
def object_snapshot(obj):
|
||||
return {
|
||||
"name": obj.Name,
|
||||
"typeId": obj.TypeId,
|
||||
"state": [str(item) for item in obj.State],
|
||||
"status": obj.getStatusString(),
|
||||
}
|
||||
|
||||
|
||||
def element_snapshot(obj, sub_elements):
|
||||
entries = []
|
||||
for sub_element in sub_elements:
|
||||
shape = obj.Shape if not sub_element else obj.getSubObject(sub_element)
|
||||
entries.append({
|
||||
"object": obj.Name,
|
||||
"subElement": sub_element,
|
||||
"shapeType": shape.ShapeType,
|
||||
"shapeNull": bool(shape.isNull()),
|
||||
"shapeValid": bool(shape.isValid()),
|
||||
"area": round(float(shape.Area), 9),
|
||||
"faces": len(shape.Faces),
|
||||
"edges": len(shape.Edges),
|
||||
"vertices": len(shape.Vertexes),
|
||||
})
|
||||
return entries
|
||||
|
||||
|
||||
def run():
|
||||
document = App.newDocument("PropertyAreaSuccess")
|
||||
try:
|
||||
box = document.addObject("Part::Box", "AreaBox")
|
||||
box.Length = 10
|
||||
box.Width = 5
|
||||
box.Height = 2
|
||||
cylinder = document.addObject("Part::Cylinder", "AreaCylinder")
|
||||
cylinder.Radius = 3
|
||||
cylinder.Height = 4
|
||||
sphere = document.addObject("Part::Sphere", "AreaSphere")
|
||||
sphere.Radius = 2
|
||||
measure = document.addObject("Measure::MeasureArea", "AreaProbe")
|
||||
document.recompute()
|
||||
|
||||
cases = []
|
||||
|
||||
def capture(case_id, requested):
|
||||
if requested is not None:
|
||||
measure.Elements = requested
|
||||
document.recompute()
|
||||
references = [] if requested is None else [
|
||||
(obj, list(sub_elements)) for obj, sub_elements in requested
|
||||
]
|
||||
elements = [
|
||||
entry
|
||||
for obj, sub_elements in references
|
||||
for entry in element_snapshot(obj, sub_elements)
|
||||
]
|
||||
expected_area = round(sum(entry["area"] for entry in elements), 9)
|
||||
cases.append({
|
||||
"id": case_id,
|
||||
"elements": elements,
|
||||
"expectedArea": expected_area,
|
||||
"value": quantity_snapshot(measure.Area),
|
||||
"propertyStatus": [str(item) for item in measure.getPropertyStatus("Area")],
|
||||
"editorMode": [str(item) for item in measure.getEditorMode("Area")],
|
||||
"elementsTypeId": measure.getTypeIdOfProperty("Elements"),
|
||||
"areaTypeId": measure.getTypeIdOfProperty("Area"),
|
||||
"object": object_snapshot(measure),
|
||||
"shape": {"applicable": hasattr(measure, "Shape")},
|
||||
})
|
||||
|
||||
capture("empty-default", None)
|
||||
capture("planar-face", [(box, ["Face1"])])
|
||||
capture("cylindrical-face", [(cylinder, ["Face1"])])
|
||||
capture("surface-face", [(sphere, ["Face1"])])
|
||||
capture("solid-volume", [(box, [""])])
|
||||
capture("multi-element-sum", [(box, ["Face1", "Face2"]), (cylinder, ["Face1"])])
|
||||
|
||||
return {
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-area-success",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"object": {"name": measure.Name, "typeId": measure.TypeId},
|
||||
"property": {"name": "Area", "typeId": measure.getTypeIdOfProperty("Area"), "group": measure.getGroupOfProperty("Area")},
|
||||
"input": {"name": "Elements", "typeId": measure.getTypeIdOfProperty("Elements"), "group": measure.getGroupOfProperty("Elements")},
|
||||
"sources": [object_snapshot(source) for source in [box, cylinder, sphere]],
|
||||
"cases": cases,
|
||||
"diagnostics": {"exception": None, "documentObjectCount": len(document.Objects)},
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
print("FREECAD_PROPERTY_AREA_SUCCESS_RESULT=" + json.dumps(run(), sort_keys=True))
|
||||
246
scripts/freecad-property-boollist-failure.py
Normal file
246
scripts/freecad-property-boollist-failure.py
Normal file
@@ -0,0 +1,246 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_BOOLLIST_FAILURE_OUTPUT", "")
|
||||
|
||||
|
||||
def object_snapshot(document):
|
||||
return [{"name": obj.Name, "typeId": obj.TypeId} for obj in document.Objects]
|
||||
|
||||
|
||||
def active_transaction_snapshot():
|
||||
active = App.getActiveTransaction()
|
||||
if not active:
|
||||
return {"name": "", "id": 0}
|
||||
return {"name": str(active[0]), "id": int(active[1])}
|
||||
|
||||
|
||||
def exception_snapshot(callback):
|
||||
try:
|
||||
callback()
|
||||
except Exception as error:
|
||||
return {"type": type(error).__name__, "message": str(error)}
|
||||
return None
|
||||
|
||||
|
||||
def writable_failures():
|
||||
document = App.newDocument("PropertyBoolListFailure")
|
||||
try:
|
||||
obj = document.addObject("App::FeatureTest", "BoolListProbe")
|
||||
document.recompute()
|
||||
document.UndoMode = 1
|
||||
initial = list(obj.BoolList)
|
||||
initial_objects = object_snapshot(document)
|
||||
accepted_coercions = []
|
||||
for case_id, requested in [("scalar-integer", 7), ("bitstring", "10201")]:
|
||||
before = list(obj.BoolList)
|
||||
before_objects = object_snapshot(document)
|
||||
exception = exception_snapshot(lambda value=requested: setattr(obj, "BoolList", value))
|
||||
after = list(obj.BoolList)
|
||||
after_objects = object_snapshot(document)
|
||||
accepted_coercions.append({
|
||||
"id": case_id,
|
||||
"requested": repr(requested),
|
||||
"exception": exception,
|
||||
"before": before,
|
||||
"after": after,
|
||||
"beforeObjects": before_objects,
|
||||
"afterObjects": after_objects,
|
||||
"objectsPreserved": before_objects == after_objects,
|
||||
})
|
||||
obj.BoolList = initial
|
||||
requested_cases = [
|
||||
("none", None),
|
||||
("float-item", [True, 1.5]),
|
||||
("string-item", [False, "bad"]),
|
||||
]
|
||||
failures = []
|
||||
for case_id, requested in requested_cases:
|
||||
before = list(obj.BoolList)
|
||||
before_objects = object_snapshot(document)
|
||||
exception = exception_snapshot(lambda value=requested: setattr(obj, "BoolList", value))
|
||||
after = list(obj.BoolList)
|
||||
after_objects = object_snapshot(document)
|
||||
if exception is None:
|
||||
obj.BoolList = initial
|
||||
failures.append({
|
||||
"id": case_id,
|
||||
"requested": repr(requested),
|
||||
"exception": exception,
|
||||
"before": before,
|
||||
"after": after,
|
||||
"beforeObjects": before_objects,
|
||||
"afterObjects": after_objects,
|
||||
"valuePreserved": before == after,
|
||||
"objectsPreserved": before_objects == after_objects,
|
||||
"polluted": before_objects != after_objects,
|
||||
})
|
||||
|
||||
disabled_before = list(obj.BoolList)
|
||||
disabled_objects_before = object_snapshot(document)
|
||||
obj.setEditorMode("BoolList", ["ReadOnly"])
|
||||
obj.setPropertyStatus("BoolList", "Immutable")
|
||||
disabled_status = [str(item) for item in obj.getPropertyStatus("BoolList")]
|
||||
disabled_editor_mode = [str(item) for item in obj.getEditorMode("BoolList")]
|
||||
disabled_exception = exception_snapshot(lambda: setattr(obj, "BoolList", [True, False]))
|
||||
disabled_after = list(obj.BoolList)
|
||||
disabled_objects_after = object_snapshot(document)
|
||||
obj.setPropertyStatus("BoolList", "-Immutable")
|
||||
obj.setEditorMode("BoolList", 0)
|
||||
disabled = {
|
||||
"classification": "editor-read-only-and-python-immutable",
|
||||
"requested": [True, False],
|
||||
"status": disabled_status,
|
||||
"editorMode": disabled_editor_mode,
|
||||
"exception": disabled_exception,
|
||||
"before": disabled_before,
|
||||
"after": disabled_after,
|
||||
"beforeObjects": disabled_objects_before,
|
||||
"afterObjects": disabled_objects_after,
|
||||
"valuePreserved": disabled_before == disabled_after,
|
||||
"objectsPreserved": disabled_objects_before == disabled_objects_after,
|
||||
"restoredStatus": [str(item) for item in obj.getPropertyStatus("BoolList")],
|
||||
"restoredEditorMode": [str(item) for item in obj.getEditorMode("BoolList")],
|
||||
}
|
||||
|
||||
document.openTransaction("property-boollist-cancel")
|
||||
transaction_before = list(obj.BoolList)
|
||||
transaction_objects_before = object_snapshot(document)
|
||||
obj.BoolList = [True, False, True]
|
||||
transaction_edited = list(obj.BoolList)
|
||||
pending_after_edit = bool(document.HasPendingTransaction)
|
||||
active_after_edit = active_transaction_snapshot()
|
||||
document.abortTransaction()
|
||||
transaction_after = list(obj.BoolList)
|
||||
transaction_objects_after = object_snapshot(document)
|
||||
return {
|
||||
"object": {"name": obj.Name, "typeId": obj.TypeId},
|
||||
"property": {"name": "BoolList", "typeId": obj.getTypeIdOfProperty("BoolList")},
|
||||
"initial": initial,
|
||||
"acceptedCoercions": accepted_coercions,
|
||||
"failures": failures,
|
||||
"disabled": disabled,
|
||||
"transaction": {
|
||||
"before": transaction_before,
|
||||
"edited": transaction_edited,
|
||||
"afterAbort": transaction_after,
|
||||
"objectsBefore": transaction_objects_before,
|
||||
"objectsAfter": transaction_objects_after,
|
||||
"undoMode": int(document.UndoMode),
|
||||
"pendingAfterEdit": pending_after_edit,
|
||||
"pendingAfterAbort": bool(document.HasPendingTransaction),
|
||||
"activeAfterEdit": active_after_edit,
|
||||
"activeAfterAbort": active_transaction_snapshot(),
|
||||
"restored": transaction_before == transaction_after,
|
||||
"objectsRestored": transaction_objects_before == transaction_objects_after,
|
||||
},
|
||||
"documentIntegrity": {
|
||||
"initialObjects": initial_objects,
|
||||
"finalObjects": object_snapshot(document),
|
||||
"objectsPreserved": initial_objects == object_snapshot(document),
|
||||
},
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
def link_failure(object_type_id, object_name):
|
||||
document = App.newDocument("PropertyBoolList" + object_name + "Failure")
|
||||
try:
|
||||
first = document.addObject("Part::Feature", "TargetA")
|
||||
second = document.addObject("Part::Feature", "TargetB")
|
||||
group = document.addObject("App::DocumentObjectGroup", "TargetGroup")
|
||||
group.addObjects([first, second])
|
||||
obj = document.addObject(object_type_id, object_name)
|
||||
if "LinkedObject" in obj.PropertiesList:
|
||||
obj.LinkedObject = group
|
||||
else:
|
||||
obj.ElementList = [first, second]
|
||||
document.recompute()
|
||||
document.UndoMode = 1
|
||||
initial = list(obj.VisibilityList)
|
||||
initial_objects = object_snapshot(document)
|
||||
direct_exception = exception_snapshot(lambda: setattr(obj, "VisibilityList", [False, True]))
|
||||
after_direct = list(obj.VisibilityList)
|
||||
invalid_element_result = int(obj.setElementVisible("MissingElement", False))
|
||||
after_invalid_element = list(obj.VisibilityList)
|
||||
document.openTransaction("property-boollist-link-cancel")
|
||||
transaction_before = list(obj.VisibilityList)
|
||||
hide_result = int(obj.setElementVisible(first.Name, False))
|
||||
transaction_edited = list(obj.VisibilityList)
|
||||
pending_after_edit = bool(document.HasPendingTransaction)
|
||||
active_after_edit = active_transaction_snapshot()
|
||||
document.abortTransaction()
|
||||
transaction_after = list(obj.VisibilityList)
|
||||
state_after_abort = [str(item) for item in obj.State]
|
||||
status_after_abort = str(obj.getStatusString())
|
||||
recovery_recompute_result = bool(document.recompute())
|
||||
state_after_recovery = [str(item) for item in obj.State]
|
||||
status_after_recovery = str(obj.getStatusString())
|
||||
return {
|
||||
"objectTypeId": object_type_id,
|
||||
"propertyTypeId": obj.getTypeIdOfProperty("VisibilityList"),
|
||||
"initial": initial,
|
||||
"status": [str(item) for item in obj.getPropertyStatus("VisibilityList")],
|
||||
"editorMode": [str(item) for item in obj.getEditorMode("VisibilityList")],
|
||||
"directWrite": {"requested": [False, True], "exception": direct_exception, "after": after_direct, "preserved": initial == after_direct},
|
||||
"invalidElement": {"name": "MissingElement", "result": invalid_element_result, "after": after_invalid_element, "preserved": initial == after_invalid_element},
|
||||
"transaction": {
|
||||
"before": transaction_before,
|
||||
"hideResult": hide_result,
|
||||
"edited": transaction_edited,
|
||||
"afterAbort": transaction_after,
|
||||
"pendingAfterEdit": pending_after_edit,
|
||||
"pendingAfterAbort": bool(document.HasPendingTransaction),
|
||||
"activeAfterEdit": active_after_edit,
|
||||
"activeAfterAbort": active_transaction_snapshot(),
|
||||
"restored": transaction_before == transaction_after,
|
||||
"stateAfterAbort": state_after_abort,
|
||||
"statusAfterAbort": status_after_abort,
|
||||
"recoveryRecomputeResult": recovery_recompute_result,
|
||||
"stateAfterRecovery": state_after_recovery,
|
||||
"statusAfterRecovery": status_after_recovery,
|
||||
},
|
||||
"objectState": state_after_recovery,
|
||||
"statusString": status_after_recovery,
|
||||
"objectsBefore": initial_objects,
|
||||
"objectsAfter": object_snapshot(document),
|
||||
"objectsPreserved": initial_objects == object_snapshot(document),
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
writable = writable_failures()
|
||||
links = [
|
||||
link_failure("App::Link", "LinkProbe"),
|
||||
link_failure("App::LinkGroup", "LinkGroupProbe"),
|
||||
link_failure("App::LinkGroupPython", "LinkGroupPythonProbe"),
|
||||
link_failure("App::LinkPython", "LinkPythonProbe"),
|
||||
]
|
||||
report = {
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-boollist-failure",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"propertyType": "App::PropertyBoolList",
|
||||
"writable": writable,
|
||||
"links": links,
|
||||
"cancellationBoundary": {
|
||||
"supported": False,
|
||||
"classification": "synchronous-property-setter-and-link-extension",
|
||||
"reason": "no-native-cancel-hook",
|
||||
"replacement": "abort-active-document-transaction",
|
||||
},
|
||||
}
|
||||
if not OUTPUT_PATH:
|
||||
raise RuntimeError("FREECAD_PROPERTY_BOOLLIST_FAILURE_OUTPUT is required")
|
||||
with open(OUTPUT_PATH, "w", encoding="utf-8") as handle:
|
||||
json.dump(report, handle, indent=2, sort_keys=True)
|
||||
handle.write("\n")
|
||||
print("FREECAD_PROPERTY_BOOLLIST_FAILURE_RESULT=" + json.dumps({"status": report["status"], "failureCount": len(writable["failures"]), "linkCount": len(links)}, sort_keys=True))
|
||||
169
scripts/freecad-property-boollist-mutation.py
Normal file
169
scripts/freecad-property-boollist-mutation.py
Normal file
@@ -0,0 +1,169 @@
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_BOOLLIST_MUTATION_OUTPUT", "")
|
||||
|
||||
|
||||
def object_snapshot(document):
|
||||
return [{"name": obj.Name, "typeId": obj.TypeId} for obj in document.Objects]
|
||||
|
||||
|
||||
def shape_snapshot(obj):
|
||||
if not hasattr(obj, "Shape"):
|
||||
return {"applicable": False, "reason": "host-has-no-shape-property"}
|
||||
shape = obj.Shape
|
||||
if shape.isNull():
|
||||
return {"applicable": True, "isNull": True, "shapeType": "Null", "solids": 0, "faces": 0, "edges": 0, "vertices": 0}
|
||||
brep = shape.exportBrepToString().encode("utf-8")
|
||||
bounds = shape.BoundBox
|
||||
return {
|
||||
"applicable": True,
|
||||
"isNull": False,
|
||||
"valid": bool(shape.isValid()),
|
||||
"shapeType": shape.ShapeType,
|
||||
"solids": len(shape.Solids),
|
||||
"faces": len(shape.Faces),
|
||||
"edges": len(shape.Edges),
|
||||
"vertices": len(shape.Vertexes),
|
||||
"area": round(float(shape.Area), 9),
|
||||
"volume": round(float(shape.Volume), 9),
|
||||
"bounds": [round(float(value), 9) for value in (bounds.XMin, bounds.YMin, bounds.ZMin, bounds.XMax, bounds.YMax, bounds.ZMax)],
|
||||
"brepSha256": hashlib.sha256(brep).hexdigest(),
|
||||
}
|
||||
|
||||
|
||||
def phase_snapshot(document, obj, property_name):
|
||||
return {
|
||||
"value": list(getattr(obj, property_name)),
|
||||
"propertyTypeId": obj.getTypeIdOfProperty(property_name),
|
||||
"propertyStatus": [str(item) for item in obj.getPropertyStatus(property_name)],
|
||||
"editorMode": [str(item) for item in obj.getEditorMode(property_name)],
|
||||
"objectState": [str(item) for item in obj.State],
|
||||
"statusString": str(obj.getStatusString()),
|
||||
"mustExecute": bool(obj.MustExecute),
|
||||
"shape": shape_snapshot(obj),
|
||||
"objectSet": object_snapshot(document),
|
||||
}
|
||||
|
||||
|
||||
def direct_mutation(object_type_id, object_name, property_name, edited_value, setup_surface=False):
|
||||
document = App.newDocument("PropertyBoolList" + object_name + "Mutation")
|
||||
try:
|
||||
obj = document.addObject(object_type_id, object_name)
|
||||
if setup_surface:
|
||||
import Part
|
||||
points = [App.Vector(0, 0, 0), App.Vector(10, 0, 0), App.Vector(10, 10, 0), App.Vector(0, 10, 0)]
|
||||
boundaries = []
|
||||
for index in range(4):
|
||||
edge = document.addObject("Part::Feature", "Boundary" + str(index + 1))
|
||||
edge.Shape = Part.makeLine(points[index], points[(index + 1) % 4])
|
||||
boundaries.append((edge, ["Edge1"]))
|
||||
obj.BoundaryList = boundaries
|
||||
document.recompute()
|
||||
before = phase_snapshot(document, obj, property_name)
|
||||
setattr(obj, property_name, edited_value)
|
||||
touched_after_edit = phase_snapshot(document, obj, property_name)
|
||||
edit_recompute = bool(document.recompute())
|
||||
edited = phase_snapshot(document, obj, property_name)
|
||||
setattr(obj, property_name, before["value"])
|
||||
touched_after_restore = phase_snapshot(document, obj, property_name)
|
||||
restore_recompute = bool(document.recompute())
|
||||
restored = phase_snapshot(document, obj, property_name)
|
||||
return {
|
||||
"objectTypeId": object_type_id,
|
||||
"objectName": object_name,
|
||||
"propertyName": property_name,
|
||||
"mode": "direct-native-property-setter",
|
||||
"hostExecution": "intrinsic-test-exception" if object_type_id == "App::FeatureTestException" else "normal",
|
||||
"before": before,
|
||||
"touchedAfterEdit": touched_after_edit,
|
||||
"editRecompute": edit_recompute,
|
||||
"edited": edited,
|
||||
"touchedAfterRestore": touched_after_restore,
|
||||
"restoreRecompute": restore_recompute,
|
||||
"restored": restored,
|
||||
"valueRestored": before["value"] == restored["value"],
|
||||
"shapeRestored": before["shape"] == restored["shape"],
|
||||
"objectsRestored": before["objectSet"] == restored["objectSet"],
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
def link_mutation(object_type_id, object_name):
|
||||
document = App.newDocument("PropertyBoolList" + object_name + "Mutation")
|
||||
try:
|
||||
first = document.addObject("Part::Feature", "TargetA")
|
||||
second = document.addObject("Part::Feature", "TargetB")
|
||||
group = document.addObject("App::DocumentObjectGroup", "TargetGroup")
|
||||
group.addObjects([first, second])
|
||||
obj = document.addObject(object_type_id, object_name)
|
||||
if "LinkedObject" in obj.PropertiesList:
|
||||
obj.LinkedObject = group
|
||||
setup_property = "LinkedObject"
|
||||
else:
|
||||
obj.ElementList = [first, second]
|
||||
setup_property = "ElementList"
|
||||
document.recompute()
|
||||
before = phase_snapshot(document, obj, "VisibilityList")
|
||||
hide_result = int(obj.setElementVisible(first.Name, False))
|
||||
touched_after_edit = phase_snapshot(document, obj, "VisibilityList")
|
||||
edit_recompute = bool(document.recompute())
|
||||
edited = phase_snapshot(document, obj, "VisibilityList")
|
||||
show_result = int(obj.setElementVisible(first.Name, True))
|
||||
touched_after_restore = phase_snapshot(document, obj, "VisibilityList")
|
||||
restore_recompute = bool(document.recompute())
|
||||
restored = phase_snapshot(document, obj, "VisibilityList")
|
||||
return {
|
||||
"objectTypeId": object_type_id,
|
||||
"objectName": object_name,
|
||||
"propertyName": "VisibilityList",
|
||||
"mode": "link-base-extension-element-visibility",
|
||||
"setupProperty": setup_property,
|
||||
"hideResult": hide_result,
|
||||
"showResult": show_result,
|
||||
"before": before,
|
||||
"touchedAfterEdit": touched_after_edit,
|
||||
"editRecompute": edit_recompute,
|
||||
"edited": edited,
|
||||
"touchedAfterRestore": touched_after_restore,
|
||||
"restoreRecompute": restore_recompute,
|
||||
"restored": restored,
|
||||
"valueRestored": before["value"] == restored["value"],
|
||||
"shapeRestored": before["shape"] == restored["shape"],
|
||||
"objectsRestored": before["objectSet"] == restored["objectSet"],
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
cases = [
|
||||
direct_mutation("App::FeatureTest", "FeatureTestProbe", "BoolList", [True, False, True]),
|
||||
direct_mutation("App::FeatureTestException", "FeatureTestExceptionProbe", "BoolList", [True, False, True]),
|
||||
direct_mutation("Surface::GeomFillSurface", "GeomFillSurfaceProbe", "ReversedList", [True, False, False, False], True),
|
||||
link_mutation("App::Link", "LinkProbe"),
|
||||
link_mutation("App::LinkGroup", "LinkGroupProbe"),
|
||||
link_mutation("App::LinkGroupPython", "LinkGroupPythonProbe"),
|
||||
link_mutation("App::LinkPython", "LinkPythonProbe"),
|
||||
]
|
||||
report = {
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-boollist-mutation",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"propertyType": "App::PropertyBoolList",
|
||||
"caseCount": len(cases),
|
||||
"cases": cases,
|
||||
}
|
||||
if not OUTPUT_PATH:
|
||||
raise RuntimeError("FREECAD_PROPERTY_BOOLLIST_MUTATION_OUTPUT is required")
|
||||
with open(OUTPUT_PATH, "w", encoding="utf-8") as handle:
|
||||
json.dump(report, handle, indent=2, sort_keys=True)
|
||||
handle.write("\n")
|
||||
print("FREECAD_PROPERTY_BOOLLIST_MUTATION_RESULT=" + json.dumps({"status": report["status"], "caseCount": report["caseCount"]}, sort_keys=True))
|
||||
80
scripts/freecad-property-boollist-roundtrip.py
Normal file
80
scripts/freecad-property-boollist-roundtrip.py
Normal file
@@ -0,0 +1,80 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
|
||||
|
||||
def object_snapshot(document):
|
||||
obj = document.getObject("BoolListProbe")
|
||||
return {
|
||||
"objectSet": [{"name": item.Name, "typeId": item.TypeId} for item in document.Objects],
|
||||
"object": {
|
||||
"name": obj.Name,
|
||||
"typeId": obj.TypeId,
|
||||
"propertyTypeId": obj.getTypeIdOfProperty("BoolList"),
|
||||
"value": [bool(value) for value in obj.BoolList],
|
||||
"propertyStatus": [str(item) for item in obj.getPropertyStatus("BoolList")],
|
||||
"editorMode": [str(item) for item in obj.getEditorMode("BoolList")],
|
||||
"state": [str(item) for item in obj.State],
|
||||
"statusString": str(obj.getStatusString()),
|
||||
"hasShapeProperty": "Shape" in obj.PropertiesList,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def create_native(path):
|
||||
document = App.newDocument("PropertyBoolListRoundtrip")
|
||||
try:
|
||||
obj = document.addObject("App::FeatureTest", "BoolListProbe")
|
||||
obj.BoolList = [False]
|
||||
document.recompute()
|
||||
document.saveAs(path)
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
reopened = App.openDocument(path)
|
||||
try:
|
||||
reopened.recompute()
|
||||
return object_snapshot(reopened)
|
||||
finally:
|
||||
App.closeDocument(reopened.Name)
|
||||
|
||||
|
||||
def verify_native(path, resaved_path):
|
||||
document = App.openDocument(path)
|
||||
try:
|
||||
document.recompute()
|
||||
reopened = object_snapshot(document)
|
||||
document.saveAs(resaved_path)
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
resaved_document = App.openDocument(resaved_path)
|
||||
try:
|
||||
resaved_document.recompute()
|
||||
resaved = object_snapshot(resaved_document)
|
||||
finally:
|
||||
App.closeDocument(resaved_document.Name)
|
||||
return {"reopened": reopened, "resaved": resaved}
|
||||
|
||||
|
||||
mode = os.environ.get("FREECAD_PROPERTY_BOOLLIST_ROUNDTRIP_MODE", "")
|
||||
path = os.environ.get("FREECAD_PROPERTY_BOOLLIST_ROUNDTRIP_PATH", "")
|
||||
resaved_path = os.environ.get("FREECAD_PROPERTY_BOOLLIST_ROUNDTRIP_RESAVED_PATH", "")
|
||||
if mode == "create":
|
||||
result = create_native(path)
|
||||
elif mode == "verify":
|
||||
result = verify_native(path, resaved_path)
|
||||
else:
|
||||
raise RuntimeError("FREECAD_PROPERTY_BOOLLIST_ROUNDTRIP_MODE must be create or verify")
|
||||
|
||||
print("FREECAD_PROPERTY_BOOLLIST_ROUNDTRIP_RESULT=" + json.dumps({
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-boollist-roundtrip",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"mode": mode,
|
||||
"result": result,
|
||||
}, sort_keys=True))
|
||||
153
scripts/freecad-property-boollist-success.py
Normal file
153
scripts/freecad-property-boollist-success.py
Normal file
@@ -0,0 +1,153 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_BOOLLIST_SUCCESS_OUTPUT", "")
|
||||
|
||||
|
||||
def shape_snapshot(obj):
|
||||
if not hasattr(obj, "Shape"):
|
||||
return {"applicable": False, "reason": "host-has-no-shape-property"}
|
||||
shape = obj.Shape
|
||||
if shape.isNull():
|
||||
return {"applicable": True, "isNull": True, "shapeType": "Null", "solids": 0, "faces": 0, "edges": 0, "vertices": 0}
|
||||
return {
|
||||
"applicable": True,
|
||||
"isNull": False,
|
||||
"shapeType": shape.ShapeType,
|
||||
"valid": bool(shape.isValid()),
|
||||
"solids": len(shape.Solids),
|
||||
"faces": len(shape.Faces),
|
||||
"edges": len(shape.Edges),
|
||||
"vertices": len(shape.Vertexes),
|
||||
}
|
||||
|
||||
|
||||
def property_snapshot(document, obj, property_name):
|
||||
recompute_result = bool(document.recompute())
|
||||
return {
|
||||
"value": list(getattr(obj, property_name)),
|
||||
"propertyTypeId": obj.getTypeIdOfProperty(property_name),
|
||||
"propertyStatus": [str(item) for item in obj.getPropertyStatus(property_name)],
|
||||
"editorMode": [str(item) for item in obj.getEditorMode(property_name)],
|
||||
"objectState": [str(item) for item in obj.State],
|
||||
"statusString": str(obj.getStatusString()),
|
||||
"recomputeResult": recompute_result,
|
||||
"shape": shape_snapshot(obj),
|
||||
"objectSet": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects],
|
||||
}
|
||||
|
||||
|
||||
def writable_case(object_type_id, object_name, property_name, valid_surface=False):
|
||||
document = App.newDocument("PropertyBoolList" + object_name)
|
||||
try:
|
||||
obj = document.addObject(object_type_id, object_name)
|
||||
if valid_surface:
|
||||
import Part
|
||||
points = [App.Vector(0, 0, 0), App.Vector(10, 0, 0), App.Vector(10, 10, 0), App.Vector(0, 10, 0)]
|
||||
boundaries = []
|
||||
for index in range(4):
|
||||
edge = document.addObject("Part::Feature", "Boundary" + str(index + 1))
|
||||
edge.Shape = Part.makeLine(points[index], points[(index + 1) % 4])
|
||||
boundaries.append((edge, ["Edge1"]))
|
||||
obj.BoundaryList = boundaries
|
||||
default_value = list(getattr(obj, property_name))
|
||||
phases = {"default": property_snapshot(document, obj, property_name)}
|
||||
values = ({
|
||||
"allFalse": [False, False, False, False],
|
||||
"oneReversed": [True, False, False, False],
|
||||
"alternating": [True, False, True, False],
|
||||
"allReversed": [True, True, True, True],
|
||||
"restored": default_value,
|
||||
} if valid_surface else {
|
||||
"empty": [],
|
||||
"singleton": [True],
|
||||
"mixed": [True, False, True, False],
|
||||
"large": [index % 3 == 0 for index in range(257)],
|
||||
"restored": default_value,
|
||||
})
|
||||
for phase, value in values.items():
|
||||
setattr(obj, property_name, value)
|
||||
phases[phase] = property_snapshot(document, obj, property_name)
|
||||
return {
|
||||
"objectTypeId": object_type_id,
|
||||
"objectName": object_name,
|
||||
"propertyName": property_name,
|
||||
"mode": "direct-native-property-setter",
|
||||
"propertyAssignmentsAccepted": True,
|
||||
"hostExecution": "intrinsic-test-exception" if object_type_id == "App::FeatureTestException" else "normal",
|
||||
"defaultValue": default_value,
|
||||
"phases": phases,
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
def link_case(object_type_id, object_name):
|
||||
document = App.newDocument("PropertyBoolList" + object_name)
|
||||
try:
|
||||
first = document.addObject("Part::Feature", "TargetA")
|
||||
second = document.addObject("Part::Feature", "TargetB")
|
||||
group = document.addObject("App::DocumentObjectGroup", "TargetGroup")
|
||||
group.addObjects([first, second])
|
||||
obj = document.addObject(object_type_id, object_name)
|
||||
if "LinkedObject" in obj.PropertiesList:
|
||||
obj.LinkedObject = group
|
||||
setup_property = "LinkedObject"
|
||||
elif "Link" in obj.PropertiesList:
|
||||
obj.Link = group
|
||||
setup_property = "Link"
|
||||
elif "ElementList" in obj.PropertiesList:
|
||||
obj.ElementList = [first, second]
|
||||
setup_property = "ElementList"
|
||||
else:
|
||||
raise RuntimeError(object_type_id + " has no native Link input property")
|
||||
phases = {"linked": property_snapshot(document, obj, "VisibilityList")}
|
||||
hide_result = int(obj.setElementVisible(first.Name, False))
|
||||
phases["hidden"] = property_snapshot(document, obj, "VisibilityList")
|
||||
show_result = int(obj.setElementVisible(first.Name, True))
|
||||
phases["restored"] = property_snapshot(document, obj, "VisibilityList")
|
||||
return {
|
||||
"objectTypeId": object_type_id,
|
||||
"objectName": object_name,
|
||||
"propertyName": "VisibilityList",
|
||||
"mode": "link-base-extension-element-visibility",
|
||||
"propertyAssignmentsAccepted": True,
|
||||
"hostExecution": "normal",
|
||||
"setupProperty": setup_property,
|
||||
"hideResult": hide_result,
|
||||
"showResult": show_result,
|
||||
"phases": phases,
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
cases = [
|
||||
writable_case("App::FeatureTest", "FeatureTestProbe", "BoolList"),
|
||||
writable_case("App::FeatureTestException", "FeatureTestExceptionProbe", "BoolList"),
|
||||
writable_case("Surface::GeomFillSurface", "GeomFillSurfaceProbe", "ReversedList", True),
|
||||
link_case("App::Link", "LinkProbe"),
|
||||
link_case("App::LinkGroup", "LinkGroupProbe"),
|
||||
link_case("App::LinkGroupPython", "LinkGroupPythonProbe"),
|
||||
link_case("App::LinkPython", "LinkPythonProbe"),
|
||||
]
|
||||
report = {
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-boollist-success",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"propertyType": "App::PropertyBoolList",
|
||||
"caseCount": len(cases),
|
||||
"cases": cases,
|
||||
}
|
||||
if not OUTPUT_PATH:
|
||||
raise RuntimeError("FREECAD_PROPERTY_BOOLLIST_SUCCESS_OUTPUT is required")
|
||||
with open(OUTPUT_PATH, "w", encoding="utf-8") as handle:
|
||||
json.dump(report, handle, indent=2, sort_keys=True)
|
||||
handle.write("\n")
|
||||
print("FREECAD_PROPERTY_BOOLLIST_SUCCESS_RESULT=" + json.dumps({"status": report["status"], "caseCount": report["caseCount"]}, sort_keys=True))
|
||||
201
scripts/freecad-property-color-failure.py
Normal file
201
scripts/freecad-property-color-failure.py
Normal file
@@ -0,0 +1,201 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_COLOR_FAILURE_OUTPUT", "")
|
||||
|
||||
|
||||
def color_value(obj):
|
||||
return [round(float(channel), 9) for channel in obj.Colour]
|
||||
|
||||
|
||||
def object_set(document):
|
||||
return [{"name": obj.Name, "typeId": obj.TypeId} for obj in document.Objects]
|
||||
|
||||
|
||||
def active_transaction_snapshot():
|
||||
active = App.getActiveTransaction()
|
||||
if not active:
|
||||
return {"name": "", "id": 0}
|
||||
return {"name": str(active[0]), "id": int(active[1])}
|
||||
|
||||
|
||||
def exception_snapshot(callback):
|
||||
try:
|
||||
callback()
|
||||
except Exception as error:
|
||||
return {"type": type(error).__name__, "message": str(error)}
|
||||
return None
|
||||
|
||||
|
||||
def state_snapshot(obj):
|
||||
return {"state": [str(item) for item in obj.State], "statusString": str(obj.getStatusString())}
|
||||
|
||||
|
||||
def run():
|
||||
document = App.newDocument("PropertyColorFailure")
|
||||
try:
|
||||
obj = document.addObject("App::FeatureTest", "ColorProbe")
|
||||
document.recompute()
|
||||
document.UndoMode = 1
|
||||
initial = color_value(obj)
|
||||
initial_objects = object_set(document)
|
||||
|
||||
coercions = []
|
||||
for case_id, requested in [
|
||||
("float-out-of-range", (2.0, -1.0, 0.5, 1.5)),
|
||||
("integer-out-of-range", (-1, 256, 511, -255)),
|
||||
("boolean-tuple", (True, False, True, False)),
|
||||
("packed-max", 0xFFFFFFFF),
|
||||
("packed-wrap", 0x100000000),
|
||||
("packed-negative", -1),
|
||||
]:
|
||||
before = color_value(obj)
|
||||
before_objects = object_set(document)
|
||||
exception = exception_snapshot(lambda value=requested: setattr(obj, "Colour", value))
|
||||
after = color_value(obj)
|
||||
after_objects = object_set(document)
|
||||
coercions.append({
|
||||
"id": case_id,
|
||||
"requested": repr(requested),
|
||||
"exception": exception,
|
||||
"accepted": exception is None,
|
||||
"before": before,
|
||||
"after": after,
|
||||
"valuePreserved": before == after,
|
||||
"beforeObjects": before_objects,
|
||||
"afterObjects": after_objects,
|
||||
"objectsPreserved": before_objects == after_objects,
|
||||
})
|
||||
if exception is None:
|
||||
obj.Colour = tuple(initial)
|
||||
|
||||
failures = []
|
||||
for case_id, requested in [
|
||||
("none", None),
|
||||
("list", [0.1, 0.2, 0.3]),
|
||||
("short-tuple", (0.1, 0.2)),
|
||||
("long-tuple", (0.1, 0.2, 0.3, 0.4, 0.5)),
|
||||
("string-first", ("bad", 0.2, 0.3)),
|
||||
("mixed-after-float", (0.1, 2, 0.3)),
|
||||
("mixed-after-integer", (1, 0.2, 3)),
|
||||
("scalar-float", 0.5),
|
||||
]:
|
||||
before = color_value(obj)
|
||||
before_objects = object_set(document)
|
||||
exception = exception_snapshot(lambda value=requested: setattr(obj, "Colour", value))
|
||||
after = color_value(obj)
|
||||
after_objects = object_set(document)
|
||||
failures.append({
|
||||
"id": case_id,
|
||||
"requested": repr(requested),
|
||||
"exception": exception,
|
||||
"before": before,
|
||||
"after": after,
|
||||
"valuePreserved": before == after,
|
||||
"beforeObjects": before_objects,
|
||||
"afterObjects": after_objects,
|
||||
"objectsPreserved": before_objects == after_objects,
|
||||
"polluted": before_objects != after_objects,
|
||||
})
|
||||
if exception is None:
|
||||
obj.Colour = tuple(initial)
|
||||
|
||||
disabled_before = color_value(obj)
|
||||
disabled_objects_before = object_set(document)
|
||||
obj.setEditorMode("Colour", ["ReadOnly"])
|
||||
editor_mode = [str(item) for item in obj.getEditorMode("Colour")]
|
||||
editor_read_only_exception = exception_snapshot(lambda: setattr(obj, "Colour", (0.2, 0.3, 0.4, 0.5)))
|
||||
editor_read_only_after = color_value(obj)
|
||||
obj.Colour = tuple(initial)
|
||||
obj.setPropertyStatus("Colour", "Immutable")
|
||||
immutable_status = [str(item) for item in obj.getPropertyStatus("Colour")]
|
||||
immutable_exception = exception_snapshot(lambda: setattr(obj, "Colour", (0.6, 0.7, 0.8, 0.9)))
|
||||
immutable_after = color_value(obj)
|
||||
obj.setPropertyStatus("Colour", "-Immutable")
|
||||
obj.setEditorMode("Colour", 0)
|
||||
disabled_objects_after = object_set(document)
|
||||
disabled = {
|
||||
"classification": "editor-readonly-python-mutable-until-immutable",
|
||||
"before": disabled_before,
|
||||
"editorMode": editor_mode,
|
||||
"editorReadOnlyRequested": [0.2, 0.3, 0.4, 0.5],
|
||||
"editorReadOnlyException": editor_read_only_exception,
|
||||
"editorReadOnlyAfter": editor_read_only_after,
|
||||
"pythonBypassesEditorReadOnly": editor_read_only_exception is None,
|
||||
"immutableStatus": immutable_status,
|
||||
"immutableRequested": [0.6, 0.7, 0.8, 0.9],
|
||||
"immutableException": immutable_exception,
|
||||
"immutableAfter": immutable_after,
|
||||
"immutableValuePreserved": initial == immutable_after,
|
||||
"restoredStatus": [str(item) for item in obj.getPropertyStatus("Colour")],
|
||||
"restoredEditorMode": [str(item) for item in obj.getEditorMode("Colour")],
|
||||
"objectsPreserved": disabled_objects_before == disabled_objects_after,
|
||||
}
|
||||
|
||||
document.recompute()
|
||||
document.openTransaction("property-color-cancel")
|
||||
transaction_before = color_value(obj)
|
||||
transaction_objects_before = object_set(document)
|
||||
obj.Colour = (0.75, 0.5, 0.25, 0.125)
|
||||
transaction_edited = color_value(obj)
|
||||
state_after_edit = state_snapshot(obj)
|
||||
pending_after_edit = bool(document.HasPendingTransaction)
|
||||
active_after_edit = active_transaction_snapshot()
|
||||
document.abortTransaction()
|
||||
transaction_after_abort = color_value(obj)
|
||||
state_after_abort = state_snapshot(obj)
|
||||
recovery_recompute_result = bool(document.recompute())
|
||||
transaction_after_recompute = color_value(obj)
|
||||
state_after_recompute = state_snapshot(obj)
|
||||
transaction_objects_after = object_set(document)
|
||||
|
||||
return {
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-color-failure",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"propertyType": "App::PropertyColor",
|
||||
"object": {"name": obj.Name, "typeId": obj.TypeId},
|
||||
"property": {"name": "Colour", "typeId": obj.getTypeIdOfProperty("Colour")},
|
||||
"initial": initial,
|
||||
"coercions": coercions,
|
||||
"failures": failures,
|
||||
"disabled": disabled,
|
||||
"transaction": {
|
||||
"before": transaction_before,
|
||||
"edited": transaction_edited,
|
||||
"afterAbort": transaction_after_abort,
|
||||
"afterRecompute": transaction_after_recompute,
|
||||
"stateAfterEdit": state_after_edit,
|
||||
"stateAfterAbort": state_after_abort,
|
||||
"stateAfterRecompute": state_after_recompute,
|
||||
"objectsBefore": transaction_objects_before,
|
||||
"objectsAfter": transaction_objects_after,
|
||||
"undoMode": int(document.UndoMode),
|
||||
"pendingAfterEdit": pending_after_edit,
|
||||
"pendingAfterAbort": bool(document.HasPendingTransaction),
|
||||
"activeAfterEdit": active_after_edit,
|
||||
"activeAfterAbort": active_transaction_snapshot(),
|
||||
"recoveryRecomputeResult": recovery_recompute_result,
|
||||
"restored": transaction_before == transaction_after_abort == transaction_after_recompute,
|
||||
"objectsRestored": transaction_objects_before == transaction_objects_after,
|
||||
},
|
||||
"cancellationBoundary": {"supported": False, "classification": "synchronous-property-setter", "reason": "no-native-cancel-hook", "replacement": "abort-active-document-transaction"},
|
||||
"documentIntegrity": {"initialObjects": initial_objects, "finalObjects": object_set(document), "objectsPreserved": initial_objects == object_set(document)},
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
report = run()
|
||||
if not OUTPUT_PATH:
|
||||
raise RuntimeError("FREECAD_PROPERTY_COLOR_FAILURE_OUTPUT is required")
|
||||
with open(OUTPUT_PATH, "w", encoding="utf-8") as handle:
|
||||
json.dump(report, handle, indent=2, sort_keys=True)
|
||||
handle.write("\n")
|
||||
print("FREECAD_PROPERTY_COLOR_FAILURE_RESULT=" + json.dumps({"status": report["status"], "coercionCount": len(report["coercions"]), "failureCount": len(report["failures"])}, sort_keys=True))
|
||||
112
scripts/freecad-property-color-mutation.py
Normal file
112
scripts/freecad-property-color-mutation.py
Normal file
@@ -0,0 +1,112 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_COLOR_MUTATION_OUTPUT", "")
|
||||
TARGET = (0.125, 0.375, 0.625, 0.875)
|
||||
HOSTS = [
|
||||
("App::FeatureTest", "FeatureTestColorMutation", "Colour"),
|
||||
("App::FeatureTestException", "FeatureTestExceptionColorMutation", "Colour"),
|
||||
("App::Part", "PartColorMutation", "Color"),
|
||||
("Assembly::AssemblyLink", "AssemblyLinkColorMutation", "Color"),
|
||||
("Assembly::AssemblyObject", "AssemblyObjectColorMutation", "Color"),
|
||||
("TechDraw::DrawViewAnnotation", "AnnotationColorMutation", "TextColor"),
|
||||
("TechDraw::DrawViewDraft", "DraftColorMutation", "Color"),
|
||||
("TechDraw::DrawViewSpreadsheet", "SpreadsheetColorMutation", "TextColor"),
|
||||
]
|
||||
|
||||
|
||||
def shape_snapshot(obj):
|
||||
if not hasattr(obj, "Shape"):
|
||||
return {"applicable": False, "reason": "host-has-no-shape-property"}
|
||||
shape = obj.Shape
|
||||
if shape.isNull():
|
||||
return {"applicable": True, "isNull": True, "shapeType": "Null", "solids": 0, "faces": 0, "edges": 0, "vertices": 0}
|
||||
return {
|
||||
"applicable": True,
|
||||
"isNull": False,
|
||||
"shapeType": shape.ShapeType,
|
||||
"valid": bool(shape.isValid()),
|
||||
"solids": len(shape.Solids),
|
||||
"faces": len(shape.Faces),
|
||||
"edges": len(shape.Edges),
|
||||
"vertices": len(shape.Vertexes),
|
||||
}
|
||||
|
||||
|
||||
def snapshot(document, obj, property_name):
|
||||
return {
|
||||
"value": [round(float(channel), 9) for channel in getattr(obj, property_name)],
|
||||
"propertyTypeId": obj.getTypeIdOfProperty(property_name),
|
||||
"propertyStatus": [str(item) for item in obj.getPropertyStatus(property_name)],
|
||||
"editorMode": [str(item) for item in obj.getEditorMode(property_name)],
|
||||
"objectState": [str(item) for item in obj.State],
|
||||
"statusString": str(obj.getStatusString()),
|
||||
"shape": shape_snapshot(obj),
|
||||
"objectSet": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects],
|
||||
}
|
||||
|
||||
|
||||
def run_case(object_type_id, object_name, property_name):
|
||||
document = App.newDocument("PropertyColor" + object_name)
|
||||
try:
|
||||
obj = document.addObject(object_type_id, object_name)
|
||||
setup = {"kind": "none"}
|
||||
if object_type_id == "TechDraw::DrawViewSpreadsheet":
|
||||
source = document.addObject("Spreadsheet::Sheet", "MutationSource")
|
||||
source.set("A1", "Mutation")
|
||||
source.set("B2", "1")
|
||||
obj.Source = source
|
||||
setup = {"kind": "spreadsheet-source", "objectName": source.Name, "objectTypeId": source.TypeId}
|
||||
document.recompute()
|
||||
before = snapshot(document, obj, property_name)
|
||||
original = list(before["value"])
|
||||
setattr(obj, property_name, TARGET)
|
||||
touched_after_edit = snapshot(document, obj, property_name)
|
||||
edit_recompute_result = bool(document.recompute())
|
||||
edited = snapshot(document, obj, property_name)
|
||||
setattr(obj, property_name, tuple(original))
|
||||
touched_after_restore = snapshot(document, obj, property_name)
|
||||
restore_recompute_result = bool(document.recompute())
|
||||
restored = snapshot(document, obj, property_name)
|
||||
return {
|
||||
"objectTypeId": object_type_id,
|
||||
"objectName": object_name,
|
||||
"propertyName": property_name,
|
||||
"hostExecution": "intrinsic-test-exception" if object_type_id == "App::FeatureTestException" else "normal",
|
||||
"setup": setup,
|
||||
"target": list(TARGET),
|
||||
"before": before,
|
||||
"touchedAfterEdit": touched_after_edit,
|
||||
"edited": edited,
|
||||
"touchedAfterRestore": touched_after_restore,
|
||||
"restored": restored,
|
||||
"editRecomputeResult": edit_recompute_result,
|
||||
"restoreRecomputeResult": restore_recompute_result,
|
||||
"mutationDetected": before["value"] != edited["value"],
|
||||
"restoredExactly": before["value"] == restored["value"] and before["objectSet"] == restored["objectSet"] and before["shape"] == restored["shape"],
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
cases = [run_case(*host) for host in HOSTS]
|
||||
report = {
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-color-mutation",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"propertyType": "App::PropertyColor",
|
||||
"caseCount": len(cases),
|
||||
"cases": cases,
|
||||
}
|
||||
if not OUTPUT_PATH:
|
||||
raise RuntimeError("FREECAD_PROPERTY_COLOR_MUTATION_OUTPUT is required")
|
||||
with open(OUTPUT_PATH, "w", encoding="utf-8") as handle:
|
||||
json.dump(report, handle, indent=2, sort_keys=True)
|
||||
handle.write("\n")
|
||||
print("FREECAD_PROPERTY_COLOR_MUTATION_RESULT=" + json.dumps({"status": report["status"], "caseCount": report["caseCount"], "restored": sum(1 for case in cases if case["restoredExactly"])}, sort_keys=True))
|
||||
80
scripts/freecad-property-color-roundtrip.py
Normal file
80
scripts/freecad-property-color-roundtrip.py
Normal file
@@ -0,0 +1,80 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
|
||||
|
||||
def object_snapshot(document):
|
||||
obj = document.getObject("ColorProbe")
|
||||
return {
|
||||
"objectSet": [{"name": item.Name, "typeId": item.TypeId} for item in document.Objects],
|
||||
"object": {
|
||||
"name": obj.Name,
|
||||
"typeId": obj.TypeId,
|
||||
"propertyTypeId": obj.getTypeIdOfProperty("Colour"),
|
||||
"value": [round(float(channel), 9) for channel in obj.Colour],
|
||||
"propertyStatus": [str(item) for item in obj.getPropertyStatus("Colour")],
|
||||
"editorMode": [str(item) for item in obj.getEditorMode("Colour")],
|
||||
"state": [str(item) for item in obj.State],
|
||||
"statusString": str(obj.getStatusString()),
|
||||
"hasShapeProperty": "Shape" in obj.PropertiesList,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def create_native(path):
|
||||
document = App.newDocument("PropertyColorRoundtrip")
|
||||
try:
|
||||
obj = document.addObject("App::FeatureTest", "ColorProbe")
|
||||
obj.Colour = 0x11223344
|
||||
document.recompute()
|
||||
document.saveAs(path)
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
reopened = App.openDocument(path)
|
||||
try:
|
||||
reopened.recompute()
|
||||
return object_snapshot(reopened)
|
||||
finally:
|
||||
App.closeDocument(reopened.Name)
|
||||
|
||||
|
||||
def verify_native(path, resaved_path):
|
||||
document = App.openDocument(path)
|
||||
try:
|
||||
document.recompute()
|
||||
reopened = object_snapshot(document)
|
||||
document.saveAs(resaved_path)
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
resaved_document = App.openDocument(resaved_path)
|
||||
try:
|
||||
resaved_document.recompute()
|
||||
resaved = object_snapshot(resaved_document)
|
||||
finally:
|
||||
App.closeDocument(resaved_document.Name)
|
||||
return {"reopened": reopened, "resaved": resaved}
|
||||
|
||||
|
||||
mode = os.environ.get("FREECAD_PROPERTY_COLOR_ROUNDTRIP_MODE", "")
|
||||
path = os.environ.get("FREECAD_PROPERTY_COLOR_ROUNDTRIP_PATH", "")
|
||||
resaved_path = os.environ.get("FREECAD_PROPERTY_COLOR_ROUNDTRIP_RESAVED_PATH", "")
|
||||
if mode == "create":
|
||||
result = create_native(path)
|
||||
elif mode == "verify":
|
||||
result = verify_native(path, resaved_path)
|
||||
else:
|
||||
raise RuntimeError("FREECAD_PROPERTY_COLOR_ROUNDTRIP_MODE must be create or verify")
|
||||
|
||||
print("FREECAD_PROPERTY_COLOR_ROUNDTRIP_RESULT=" + json.dumps({
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-color-roundtrip",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"mode": mode,
|
||||
"result": result,
|
||||
}, sort_keys=True))
|
||||
118
scripts/freecad-property-color-success.py
Normal file
118
scripts/freecad-property-color-success.py
Normal file
@@ -0,0 +1,118 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_COLOR_SUCCESS_OUTPUT", "")
|
||||
HOSTS = [
|
||||
("App::FeatureTest", "FeatureTestColorProbe", "Colour"),
|
||||
("App::FeatureTestException", "FeatureTestExceptionColorProbe", "Colour"),
|
||||
("App::Part", "PartColorProbe", "Color"),
|
||||
("Assembly::AssemblyLink", "AssemblyLinkColorProbe", "Color"),
|
||||
("Assembly::AssemblyObject", "AssemblyObjectColorProbe", "Color"),
|
||||
("TechDraw::DrawViewAnnotation", "AnnotationColorProbe", "TextColor"),
|
||||
("TechDraw::DrawViewDraft", "DraftColorProbe", "Color"),
|
||||
("TechDraw::DrawViewSpreadsheet", "SpreadsheetColorProbe", "TextColor"),
|
||||
]
|
||||
ASSIGNMENTS = [
|
||||
("floatRgb", (0.25, 0.5, 0.75)),
|
||||
("floatRgba", (0.1, 0.2, 0.3, 0.4)),
|
||||
("byteRgb", (1, 127, 255)),
|
||||
("byteRgba", (255, 128, 0, 64)),
|
||||
("packedRgba", 0x11223344),
|
||||
("transparentBoundary", (0.0, 0.0, 0.0, 0.0)),
|
||||
]
|
||||
|
||||
|
||||
def color_value(obj, property_name):
|
||||
return [round(float(channel), 9) for channel in getattr(obj, property_name)]
|
||||
|
||||
|
||||
def shape_snapshot(obj):
|
||||
if not hasattr(obj, "Shape"):
|
||||
return {"applicable": False, "reason": "host-has-no-shape-property"}
|
||||
shape = obj.Shape
|
||||
if shape.isNull():
|
||||
return {"applicable": True, "isNull": True, "shapeType": "Null", "solids": 0, "faces": 0, "edges": 0, "vertices": 0}
|
||||
return {
|
||||
"applicable": True,
|
||||
"isNull": False,
|
||||
"shapeType": shape.ShapeType,
|
||||
"valid": bool(shape.isValid()),
|
||||
"solids": len(shape.Solids),
|
||||
"faces": len(shape.Faces),
|
||||
"edges": len(shape.Edges),
|
||||
"vertices": len(shape.Vertexes),
|
||||
}
|
||||
|
||||
|
||||
def property_snapshot(document, obj, property_name):
|
||||
recompute_result = bool(document.recompute())
|
||||
return {
|
||||
"value": color_value(obj, property_name),
|
||||
"propertyTypeId": obj.getTypeIdOfProperty(property_name),
|
||||
"propertyStatus": [str(item) for item in obj.getPropertyStatus(property_name)],
|
||||
"editorMode": [str(item) for item in obj.getEditorMode(property_name)],
|
||||
"objectState": [str(item) for item in obj.State],
|
||||
"statusString": str(obj.getStatusString()),
|
||||
"recomputeResult": recompute_result,
|
||||
"shape": shape_snapshot(obj),
|
||||
"objectSet": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects],
|
||||
}
|
||||
|
||||
|
||||
def run_case(object_type_id, object_name, property_name):
|
||||
document = App.newDocument("PropertyColor" + object_name)
|
||||
try:
|
||||
obj = document.addObject(object_type_id, object_name)
|
||||
setup = {"kind": "none"}
|
||||
if object_type_id == "TechDraw::DrawViewSpreadsheet":
|
||||
source = document.addObject("Spreadsheet::Sheet", "ColorSource")
|
||||
source.set("A1", "Color probe")
|
||||
source.set("B2", "1")
|
||||
obj.Source = source
|
||||
setup = {"kind": "spreadsheet-source", "objectName": source.Name, "objectTypeId": source.TypeId}
|
||||
default_value = color_value(obj, property_name)
|
||||
phases = {"default": property_snapshot(document, obj, property_name)}
|
||||
requested = {}
|
||||
for phase, value in ASSIGNMENTS:
|
||||
requested[phase] = list(value) if isinstance(value, tuple) else value
|
||||
setattr(obj, property_name, value)
|
||||
phases[phase] = property_snapshot(document, obj, property_name)
|
||||
setattr(obj, property_name, tuple(default_value))
|
||||
phases["restored"] = property_snapshot(document, obj, property_name)
|
||||
return {
|
||||
"objectTypeId": object_type_id,
|
||||
"objectName": object_name,
|
||||
"propertyName": property_name,
|
||||
"mode": "direct-native-property-setter",
|
||||
"propertyAssignmentsAccepted": True,
|
||||
"hostExecution": "intrinsic-test-exception" if object_type_id == "App::FeatureTestException" else "normal",
|
||||
"setup": setup,
|
||||
"defaultValue": default_value,
|
||||
"requested": requested,
|
||||
"phases": phases,
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
cases = [run_case(*host) for host in HOSTS]
|
||||
report = {
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-color-success",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"propertyType": "App::PropertyColor",
|
||||
"caseCount": len(cases),
|
||||
"cases": cases,
|
||||
}
|
||||
if not OUTPUT_PATH:
|
||||
raise RuntimeError("FREECAD_PROPERTY_COLOR_SUCCESS_OUTPUT is required")
|
||||
with open(OUTPUT_PATH, "w", encoding="utf-8") as handle:
|
||||
json.dump(report, handle, indent=2, sort_keys=True)
|
||||
handle.write("\n")
|
||||
print("FREECAD_PROPERTY_COLOR_SUCCESS_RESULT=" + json.dumps({"status": report["status"], "caseCount": report["caseCount"]}, sort_keys=True))
|
||||
208
scripts/freecad-property-colorlist-failure.py
Normal file
208
scripts/freecad-property-colorlist-failure.py
Normal file
@@ -0,0 +1,208 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_COLORLIST_FAILURE_OUTPUT", "")
|
||||
|
||||
|
||||
def color_list_value(obj):
|
||||
return [[round(float(channel), 9) for channel in color] for color in obj.ColourList]
|
||||
|
||||
|
||||
def object_set(document):
|
||||
return [{"name": obj.Name, "typeId": obj.TypeId} for obj in document.Objects]
|
||||
|
||||
|
||||
def active_transaction_snapshot():
|
||||
active = App.getActiveTransaction()
|
||||
if not active:
|
||||
return {"name": "", "id": 0}
|
||||
return {"name": str(active[0]), "id": int(active[1])}
|
||||
|
||||
|
||||
def exception_snapshot(callback):
|
||||
try:
|
||||
callback()
|
||||
except Exception as error:
|
||||
return {"type": type(error).__name__, "message": str(error)}
|
||||
return None
|
||||
|
||||
|
||||
def state_snapshot(obj):
|
||||
return {"state": [str(item) for item in obj.State], "statusString": str(obj.getStatusString())}
|
||||
|
||||
|
||||
def run():
|
||||
document = App.newDocument("PropertyColorListFailure")
|
||||
try:
|
||||
obj = document.addObject("App::FeatureTest", "ColorListProbe")
|
||||
document.recompute()
|
||||
document.UndoMode = 1
|
||||
baseline_request = [0x11223344, 0xFFFFFFFF]
|
||||
obj.ColourList = baseline_request
|
||||
document.recompute()
|
||||
initial = color_list_value(obj)
|
||||
initial_objects = object_set(document)
|
||||
|
||||
coercions = []
|
||||
for case_id, requested in [
|
||||
("boolean-scalar", True),
|
||||
("bytes-sequence", bytes([1, 255])),
|
||||
("bare-float-tuple", (0.1, 0.2, 0.3, 0.4)),
|
||||
("bare-integer-tuple", (255, 128, 0, 64)),
|
||||
("tuple-packed-sequence", (0x11223344, 0xFFFFFFFF)),
|
||||
("empty-dictionary", {}),
|
||||
]:
|
||||
before = color_list_value(obj)
|
||||
before_objects = object_set(document)
|
||||
exception = exception_snapshot(lambda value=requested: setattr(obj, "ColourList", value))
|
||||
after = color_list_value(obj)
|
||||
after_objects = object_set(document)
|
||||
coercions.append({
|
||||
"id": case_id,
|
||||
"requested": repr(requested),
|
||||
"exception": exception,
|
||||
"accepted": exception is None,
|
||||
"before": before,
|
||||
"after": after,
|
||||
"valuePreserved": before == after,
|
||||
"beforeObjects": before_objects,
|
||||
"afterObjects": after_objects,
|
||||
"objectsPreserved": before_objects == after_objects,
|
||||
})
|
||||
obj.ColourList = baseline_request
|
||||
|
||||
failures = []
|
||||
for case_id, requested in [
|
||||
("none", None),
|
||||
("bare-float-list", [0.1, 0.2, 0.3, 0.4]),
|
||||
("bare-mixed-tuple", (0.1, 2, 0.3)),
|
||||
("nested-short-tuple", [(0.1, 0.2)]),
|
||||
("mixed-valid-none", [(0.1, 0.2, 0.3, 0.4), None]),
|
||||
("scalar-float", 0.5),
|
||||
("string-sequence", "rgb"),
|
||||
("nested-list", [[0.1, 0.2, 0.3, 0.4]]),
|
||||
("dictionary-string-key", {"0": 0x11223344}),
|
||||
("dictionary-high-index", {3: 0x11223344}),
|
||||
("dictionary-negative-index", {-2: 0x11223344}),
|
||||
("dictionary-partial-invalid", {0: 0x01020304, 1: None}),
|
||||
]:
|
||||
obj.ColourList = baseline_request
|
||||
before = color_list_value(obj)
|
||||
before_objects = object_set(document)
|
||||
exception = exception_snapshot(lambda value=requested: setattr(obj, "ColourList", value))
|
||||
after = color_list_value(obj)
|
||||
after_objects = object_set(document)
|
||||
failures.append({
|
||||
"id": case_id,
|
||||
"requested": repr(requested),
|
||||
"exception": exception,
|
||||
"before": before,
|
||||
"after": after,
|
||||
"valuePreserved": before == after,
|
||||
"beforeObjects": before_objects,
|
||||
"afterObjects": after_objects,
|
||||
"objectsPreserved": before_objects == after_objects,
|
||||
"polluted": before_objects != after_objects,
|
||||
})
|
||||
|
||||
obj.ColourList = baseline_request
|
||||
disabled_before = color_list_value(obj)
|
||||
disabled_objects_before = object_set(document)
|
||||
obj.setEditorMode("ColourList", ["ReadOnly"])
|
||||
editor_mode = [str(item) for item in obj.getEditorMode("ColourList")]
|
||||
editor_requested = [(0.2, 0.3, 0.4, 0.5)]
|
||||
editor_read_only_exception = exception_snapshot(lambda: setattr(obj, "ColourList", editor_requested))
|
||||
editor_read_only_after = color_list_value(obj)
|
||||
obj.ColourList = baseline_request
|
||||
obj.setPropertyStatus("ColourList", "Immutable")
|
||||
immutable_status = [str(item) for item in obj.getPropertyStatus("ColourList")]
|
||||
immutable_exception = exception_snapshot(lambda: setattr(obj, "ColourList", [(0.6, 0.7, 0.8, 0.9)]))
|
||||
immutable_after = color_list_value(obj)
|
||||
obj.setPropertyStatus("ColourList", "-Immutable")
|
||||
obj.setEditorMode("ColourList", 0)
|
||||
disabled_objects_after = object_set(document)
|
||||
disabled = {
|
||||
"classification": "editor-readonly-python-mutable-until-immutable",
|
||||
"before": disabled_before,
|
||||
"editorMode": editor_mode,
|
||||
"editorReadOnlyRequested": [list(color) for color in editor_requested],
|
||||
"editorReadOnlyException": editor_read_only_exception,
|
||||
"editorReadOnlyAfter": editor_read_only_after,
|
||||
"pythonBypassesEditorReadOnly": editor_read_only_exception is None,
|
||||
"immutableStatus": immutable_status,
|
||||
"immutableRequested": [[0.6, 0.7, 0.8, 0.9]],
|
||||
"immutableException": immutable_exception,
|
||||
"immutableAfter": immutable_after,
|
||||
"immutableValuePreserved": initial == immutable_after,
|
||||
"restoredStatus": [str(item) for item in obj.getPropertyStatus("ColourList")],
|
||||
"restoredEditorMode": [str(item) for item in obj.getEditorMode("ColourList")],
|
||||
"objectsPreserved": disabled_objects_before == disabled_objects_after,
|
||||
}
|
||||
|
||||
document.recompute()
|
||||
document.openTransaction("property-colorlist-cancel")
|
||||
transaction_before = color_list_value(obj)
|
||||
transaction_objects_before = object_set(document)
|
||||
obj.ColourList = [(0.75, 0.5, 0.25, 0.125), 0x11223344, (0, 255, 0, 128)]
|
||||
transaction_edited = color_list_value(obj)
|
||||
state_after_edit = state_snapshot(obj)
|
||||
pending_after_edit = bool(document.HasPendingTransaction)
|
||||
active_after_edit = active_transaction_snapshot()
|
||||
document.abortTransaction()
|
||||
transaction_after_abort = color_list_value(obj)
|
||||
state_after_abort = state_snapshot(obj)
|
||||
recovery_recompute_result = bool(document.recompute())
|
||||
transaction_after_recompute = color_list_value(obj)
|
||||
state_after_recompute = state_snapshot(obj)
|
||||
transaction_objects_after = object_set(document)
|
||||
|
||||
return {
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-colorlist-failure",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"propertyType": "App::PropertyColorList",
|
||||
"object": {"name": obj.Name, "typeId": obj.TypeId},
|
||||
"property": {"name": "ColourList", "typeId": obj.getTypeIdOfProperty("ColourList")},
|
||||
"initial": initial,
|
||||
"coercions": coercions,
|
||||
"failures": failures,
|
||||
"disabled": disabled,
|
||||
"transaction": {
|
||||
"before": transaction_before,
|
||||
"edited": transaction_edited,
|
||||
"afterAbort": transaction_after_abort,
|
||||
"afterRecompute": transaction_after_recompute,
|
||||
"stateAfterEdit": state_after_edit,
|
||||
"stateAfterAbort": state_after_abort,
|
||||
"stateAfterRecompute": state_after_recompute,
|
||||
"objectsBefore": transaction_objects_before,
|
||||
"objectsAfter": transaction_objects_after,
|
||||
"undoMode": int(document.UndoMode),
|
||||
"pendingAfterEdit": pending_after_edit,
|
||||
"pendingAfterAbort": bool(document.HasPendingTransaction),
|
||||
"activeAfterEdit": active_after_edit,
|
||||
"activeAfterAbort": active_transaction_snapshot(),
|
||||
"recoveryRecomputeResult": recovery_recompute_result,
|
||||
"restored": transaction_before == transaction_after_abort == transaction_after_recompute,
|
||||
"objectsRestored": transaction_objects_before == transaction_objects_after,
|
||||
},
|
||||
"cancellationBoundary": {"supported": False, "classification": "synchronous-property-setter", "reason": "no-native-cancel-hook", "replacement": "abort-active-document-transaction"},
|
||||
"documentIntegrity": {"initialObjects": initial_objects, "finalObjects": object_set(document), "objectsPreserved": initial_objects == object_set(document)},
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
report = run()
|
||||
if not OUTPUT_PATH:
|
||||
raise RuntimeError("FREECAD_PROPERTY_COLORLIST_FAILURE_OUTPUT is required")
|
||||
with open(OUTPUT_PATH, "w", encoding="utf-8") as handle:
|
||||
json.dump(report, handle, indent=2, sort_keys=True)
|
||||
handle.write("\n")
|
||||
print("FREECAD_PROPERTY_COLORLIST_FAILURE_RESULT=" + json.dumps({"status": report["status"], "coercionCount": len(report["coercions"]), "failureCount": len(report["failures"])}, sort_keys=True))
|
||||
84
scripts/freecad-property-colorlist-mutation.py
Normal file
84
scripts/freecad-property-colorlist-mutation.py
Normal file
@@ -0,0 +1,84 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_COLORLIST_MUTATION_OUTPUT", "")
|
||||
TARGET = [(0.125, 0.375, 0.625, 0.875), 0x11223344, (255, 0, 128, 64), (0.0, 0.0, 0.0, 0.0)]
|
||||
HOSTS = [
|
||||
("App::FeatureTest", "FeatureTestColorListMutation"),
|
||||
("App::FeatureTestException", "FeatureTestExceptionColorListMutation"),
|
||||
]
|
||||
|
||||
|
||||
def color_list_value(obj):
|
||||
return [[round(float(channel), 9) for channel in color] for color in obj.ColourList]
|
||||
|
||||
|
||||
def snapshot(document, obj):
|
||||
return {
|
||||
"value": color_list_value(obj),
|
||||
"propertyTypeId": obj.getTypeIdOfProperty("ColourList"),
|
||||
"propertyStatus": [str(item) for item in obj.getPropertyStatus("ColourList")],
|
||||
"editorMode": [str(item) for item in obj.getEditorMode("ColourList")],
|
||||
"objectState": [str(item) for item in obj.State],
|
||||
"statusString": str(obj.getStatusString()),
|
||||
"shape": {"applicable": False, "reason": "host-has-no-shape-property"} if "Shape" not in obj.PropertiesList else {"applicable": True},
|
||||
"objectSet": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects],
|
||||
}
|
||||
|
||||
|
||||
def run_case(object_type_id, object_name):
|
||||
document = App.newDocument("PropertyColorList" + object_name)
|
||||
try:
|
||||
obj = document.addObject(object_type_id, object_name)
|
||||
document.recompute()
|
||||
before = snapshot(document, obj)
|
||||
original = [tuple(color) for color in before["value"]]
|
||||
obj.ColourList = TARGET
|
||||
touched_after_edit = snapshot(document, obj)
|
||||
edit_recompute_result = bool(document.recompute())
|
||||
edited = snapshot(document, obj)
|
||||
obj.ColourList = original
|
||||
touched_after_restore = snapshot(document, obj)
|
||||
restore_recompute_result = bool(document.recompute())
|
||||
restored = snapshot(document, obj)
|
||||
return {
|
||||
"objectTypeId": object_type_id,
|
||||
"objectName": object_name,
|
||||
"propertyName": "ColourList",
|
||||
"hostExecution": "intrinsic-test-exception" if object_type_id == "App::FeatureTestException" else "normal",
|
||||
"target": [list(value) if isinstance(value, tuple) else value for value in TARGET],
|
||||
"before": before,
|
||||
"touchedAfterEdit": touched_after_edit,
|
||||
"edited": edited,
|
||||
"touchedAfterRestore": touched_after_restore,
|
||||
"restored": restored,
|
||||
"editRecomputeResult": edit_recompute_result,
|
||||
"restoreRecomputeResult": restore_recompute_result,
|
||||
"mutationDetected": before["value"] != edited["value"],
|
||||
"restoredExactly": before["value"] == restored["value"] and before["objectSet"] == restored["objectSet"] and before["shape"] == restored["shape"],
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
cases = [run_case(*host) for host in HOSTS]
|
||||
report = {
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-colorlist-mutation",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"propertyType": "App::PropertyColorList",
|
||||
"caseCount": len(cases),
|
||||
"cases": cases,
|
||||
}
|
||||
if not OUTPUT_PATH:
|
||||
raise RuntimeError("FREECAD_PROPERTY_COLORLIST_MUTATION_OUTPUT is required")
|
||||
with open(OUTPUT_PATH, "w", encoding="utf-8") as handle:
|
||||
json.dump(report, handle, indent=2, sort_keys=True)
|
||||
handle.write("\n")
|
||||
print("FREECAD_PROPERTY_COLORLIST_MUTATION_RESULT=" + json.dumps({"status": report["status"], "caseCount": report["caseCount"], "restored": sum(1 for case in cases if case["restoredExactly"])}, sort_keys=True))
|
||||
83
scripts/freecad-property-colorlist-roundtrip.py
Normal file
83
scripts/freecad-property-colorlist-roundtrip.py
Normal file
@@ -0,0 +1,83 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
|
||||
|
||||
def object_snapshot(document):
|
||||
obj = document.getObject("ColorListProbe")
|
||||
return {
|
||||
"objectSet": [{"name": item.Name, "typeId": item.TypeId} for item in document.Objects],
|
||||
"object": {
|
||||
"name": obj.Name,
|
||||
"typeId": obj.TypeId,
|
||||
"propertyTypeId": obj.getTypeIdOfProperty("ColourList"),
|
||||
"value": [
|
||||
[round(float(channel), 9) for channel in color]
|
||||
for color in obj.ColourList
|
||||
],
|
||||
"propertyStatus": [str(item) for item in obj.getPropertyStatus("ColourList")],
|
||||
"editorMode": [str(item) for item in obj.getEditorMode("ColourList")],
|
||||
"state": [str(item) for item in obj.State],
|
||||
"statusString": str(obj.getStatusString()),
|
||||
"hasShapeProperty": "Shape" in obj.PropertiesList,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def create_native(path):
|
||||
document = App.newDocument("PropertyColorListRoundtrip")
|
||||
try:
|
||||
obj = document.addObject("App::FeatureTest", "ColorListProbe")
|
||||
obj.ColourList = [0x11223344]
|
||||
document.recompute()
|
||||
document.saveAs(path)
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
reopened = App.openDocument(path)
|
||||
try:
|
||||
reopened.recompute()
|
||||
return object_snapshot(reopened)
|
||||
finally:
|
||||
App.closeDocument(reopened.Name)
|
||||
|
||||
|
||||
def verify_native(path, resaved_path):
|
||||
document = App.openDocument(path)
|
||||
try:
|
||||
document.recompute()
|
||||
reopened = object_snapshot(document)
|
||||
document.saveAs(resaved_path)
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
resaved_document = App.openDocument(resaved_path)
|
||||
try:
|
||||
resaved_document.recompute()
|
||||
resaved = object_snapshot(resaved_document)
|
||||
finally:
|
||||
App.closeDocument(resaved_document.Name)
|
||||
return {"reopened": reopened, "resaved": resaved}
|
||||
|
||||
|
||||
mode = os.environ.get("FREECAD_PROPERTY_COLORLIST_ROUNDTRIP_MODE", "")
|
||||
path = os.environ.get("FREECAD_PROPERTY_COLORLIST_ROUNDTRIP_PATH", "")
|
||||
resaved_path = os.environ.get("FREECAD_PROPERTY_COLORLIST_ROUNDTRIP_RESAVED_PATH", "")
|
||||
if mode == "create":
|
||||
result = create_native(path)
|
||||
elif mode == "verify":
|
||||
result = verify_native(path, resaved_path)
|
||||
else:
|
||||
raise RuntimeError("FREECAD_PROPERTY_COLORLIST_ROUNDTRIP_MODE must be create or verify")
|
||||
|
||||
print("FREECAD_PROPERTY_COLORLIST_ROUNDTRIP_RESULT=" + json.dumps({
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-colorlist-roundtrip",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"mode": mode,
|
||||
"result": result,
|
||||
}, sort_keys=True))
|
||||
88
scripts/freecad-property-colorlist-success.py
Normal file
88
scripts/freecad-property-colorlist-success.py
Normal file
@@ -0,0 +1,88 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_COLORLIST_SUCCESS_OUTPUT", "")
|
||||
HOSTS = [
|
||||
("App::FeatureTest", "FeatureTestColorListProbe"),
|
||||
("App::FeatureTestException", "FeatureTestExceptionColorListProbe"),
|
||||
]
|
||||
|
||||
|
||||
def color_list_value(obj):
|
||||
return [[round(float(channel), 9) for channel in color] for color in obj.ColourList]
|
||||
|
||||
|
||||
def snapshot(document, obj):
|
||||
recompute_result = bool(document.recompute())
|
||||
return {
|
||||
"value": color_list_value(obj),
|
||||
"propertyTypeId": obj.getTypeIdOfProperty("ColourList"),
|
||||
"propertyStatus": [str(item) for item in obj.getPropertyStatus("ColourList")],
|
||||
"editorMode": [str(item) for item in obj.getEditorMode("ColourList")],
|
||||
"objectState": [str(item) for item in obj.State],
|
||||
"statusString": str(obj.getStatusString()),
|
||||
"recomputeResult": recompute_result,
|
||||
"shape": {"applicable": False, "reason": "host-has-no-shape-property"} if "Shape" not in obj.PropertiesList else {"applicable": True},
|
||||
"objectSet": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects],
|
||||
}
|
||||
|
||||
|
||||
def run_case(object_type_id, object_name):
|
||||
document = App.newDocument("PropertyColorList" + object_name)
|
||||
try:
|
||||
obj = document.addObject(object_type_id, object_name)
|
||||
default_value = color_list_value(obj)
|
||||
phases = {"default": snapshot(document, obj)}
|
||||
|
||||
obj.ColourList = []
|
||||
phases["empty"] = snapshot(document, obj)
|
||||
obj.ColourList = [(0.25, 0.5, 0.75)]
|
||||
phases["singletonFloat"] = snapshot(document, obj)
|
||||
obj.ColourList = [(0.1, 0.2, 0.3, 0.4), (255, 128, 0, 64), 0x11223344]
|
||||
phases["mixedElements"] = snapshot(document, obj)
|
||||
obj.ColourList = 0x11223344
|
||||
phases["singlePacked"] = snapshot(document, obj)
|
||||
obj.ColourList = (color for color in [(0.0, 0.25, 0.5, 0.75), (1.0, 0.75, 0.5, 0.25)])
|
||||
phases["iterable"] = snapshot(document, obj)
|
||||
obj.ColourList = [(0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)]
|
||||
obj.ColourList = {1: 0x11223344, -1: (0, 255, 0, 128)}
|
||||
phases["indexedDictionary"] = snapshot(document, obj)
|
||||
obj.ColourList = [0x01020304] * 257
|
||||
phases["largeList"] = snapshot(document, obj)
|
||||
obj.ColourList = [tuple(color) for color in default_value]
|
||||
phases["restored"] = snapshot(document, obj)
|
||||
return {
|
||||
"objectTypeId": object_type_id,
|
||||
"objectName": object_name,
|
||||
"propertyName": "ColourList",
|
||||
"mode": "native-property-list-setter",
|
||||
"propertyAssignmentsAccepted": True,
|
||||
"hostExecution": "intrinsic-test-exception" if object_type_id == "App::FeatureTestException" else "normal",
|
||||
"defaultValue": default_value,
|
||||
"phases": phases,
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
cases = [run_case(*host) for host in HOSTS]
|
||||
report = {
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-colorlist-success",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"propertyType": "App::PropertyColorList",
|
||||
"caseCount": len(cases),
|
||||
"cases": cases,
|
||||
}
|
||||
if not OUTPUT_PATH:
|
||||
raise RuntimeError("FREECAD_PROPERTY_COLORLIST_SUCCESS_OUTPUT is required")
|
||||
with open(OUTPUT_PATH, "w", encoding="utf-8") as handle:
|
||||
json.dump(report, handle, indent=2, sort_keys=True)
|
||||
handle.write("\n")
|
||||
print("FREECAD_PROPERTY_COLORLIST_SUCCESS_RESULT=" + json.dumps({"status": report["status"], "caseCount": report["caseCount"]}, sort_keys=True))
|
||||
454
scripts/freecad-property-direction-failure.py
Normal file
454
scripts/freecad-property-direction-failure.py
Normal file
@@ -0,0 +1,454 @@
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
|
||||
import FreeCAD as App
|
||||
import Part
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_DIRECTION_FAILURE_OUTPUT", "")
|
||||
PROBE_MODE = os.environ.get("FREECAD_PROPERTY_DIRECTION_FAILURE_MODE", "main")
|
||||
|
||||
|
||||
def component_value(value):
|
||||
number = float(value)
|
||||
if math.isnan(number):
|
||||
return "NaN"
|
||||
if math.isinf(number):
|
||||
return "Infinity" if number > 0 else "-Infinity"
|
||||
return number
|
||||
|
||||
|
||||
def vector_value(obj, property_name):
|
||||
value = getattr(obj, property_name)
|
||||
return [component_value(value.x), component_value(value.y), component_value(value.z)]
|
||||
|
||||
|
||||
def object_set(document):
|
||||
return [{"name": obj.Name, "typeId": obj.TypeId} for obj in document.Objects]
|
||||
|
||||
|
||||
def exception_snapshot(callback):
|
||||
try:
|
||||
callback()
|
||||
except Exception as error:
|
||||
return {"type": type(error).__name__, "message": str(error)}
|
||||
return None
|
||||
|
||||
|
||||
def active_transaction_snapshot():
|
||||
active = App.getActiveTransaction()
|
||||
if not active:
|
||||
return {"name": "", "id": 0}
|
||||
return {"name": str(active[0]), "id": int(active[1])}
|
||||
|
||||
|
||||
def state_snapshot(obj):
|
||||
return {
|
||||
"state": [str(item) for item in obj.State],
|
||||
"statusString": str(obj.getStatusString()),
|
||||
"mustExecute": bool(obj.MustExecute),
|
||||
}
|
||||
|
||||
|
||||
def shape_snapshot(obj):
|
||||
shape = obj.Shape
|
||||
if shape.isNull():
|
||||
return {
|
||||
"isNull": True,
|
||||
"shapeType": "Null",
|
||||
"valid": False,
|
||||
"solids": 0,
|
||||
"faces": 0,
|
||||
"edges": 0,
|
||||
"vertices": 0,
|
||||
}
|
||||
return {
|
||||
"isNull": False,
|
||||
"shapeType": shape.ShapeType,
|
||||
"valid": bool(shape.isValid()),
|
||||
"solids": len(shape.Solids),
|
||||
"faces": len(shape.Faces),
|
||||
"edges": len(shape.Edges),
|
||||
"vertices": len(shape.Vertexes),
|
||||
"brepSha256": hashlib.sha256(shape.exportBrepToString().encode("utf-8")).hexdigest(),
|
||||
}
|
||||
|
||||
|
||||
def setup_mirroring(document):
|
||||
source = document.addObject("Part::Box", "MirrorSource")
|
||||
source.Length = 2
|
||||
source.Width = 3
|
||||
source.Height = 4
|
||||
host = document.addObject("Part::Mirroring", "DirectionMirror")
|
||||
host.Source = source
|
||||
host.Base = App.Vector(0, 0, 0)
|
||||
return host, "Normal"
|
||||
|
||||
|
||||
def setup_projection(document):
|
||||
support = document.addObject("Part::Feature", "ProjectionSupport")
|
||||
support.Shape = Part.makePlane(40, 40, App.Vector(-20, -20, 0))
|
||||
wire = document.addObject("Part::Feature", "ProjectionWire")
|
||||
wire.Shape = Part.makePolygon([
|
||||
App.Vector(-4, -3, -5),
|
||||
App.Vector(4, -3, -5),
|
||||
App.Vector(4, 3, -5),
|
||||
App.Vector(-4, 3, -5),
|
||||
App.Vector(-4, -3, -5),
|
||||
])
|
||||
host = document.addObject("Part::ProjectOnSurface", "DirectionProjection")
|
||||
host.SupportFace = (support, ["Face1"])
|
||||
host.Projection = [(wire, ["Wire1"])]
|
||||
host.Mode = "Edges"
|
||||
return host, "Direction"
|
||||
|
||||
|
||||
def run_setter_boundaries():
|
||||
document = App.newDocument("PropertyDirectionSetterFailure")
|
||||
try:
|
||||
obj, property_name = setup_mirroring(document)
|
||||
document.recompute()
|
||||
baseline = vector_value(obj, property_name)
|
||||
initial_objects = object_set(document)
|
||||
accepted = []
|
||||
for case_id, requested in [
|
||||
("mixed-numeric-tuple", (1, 0.5, 2)),
|
||||
("boolean-tuple", (True, False, True)),
|
||||
]:
|
||||
before = vector_value(obj, property_name)
|
||||
before_objects = object_set(document)
|
||||
exception = exception_snapshot(lambda value=requested: setattr(obj, property_name, value))
|
||||
after = vector_value(obj, property_name)
|
||||
after_objects = object_set(document)
|
||||
accepted.append({
|
||||
"id": case_id,
|
||||
"requested": repr(requested),
|
||||
"exception": exception,
|
||||
"accepted": exception is None,
|
||||
"before": before,
|
||||
"after": after,
|
||||
"objectsPreserved": before_objects == after_objects,
|
||||
})
|
||||
setattr(obj, property_name, tuple(baseline))
|
||||
|
||||
failures = []
|
||||
for case_id, requested in [
|
||||
("none", None),
|
||||
("list", [0.0, 0.0, 1.0]),
|
||||
("short-tuple", (0.0, 1.0)),
|
||||
("long-tuple", (0.0, 0.0, 1.0, 2.0)),
|
||||
("string-component", (0.0, "bad", 1.0)),
|
||||
("scalar-float", 1.0),
|
||||
("scalar-integer", 1),
|
||||
]:
|
||||
before = vector_value(obj, property_name)
|
||||
before_objects = object_set(document)
|
||||
exception = exception_snapshot(lambda value=requested: setattr(obj, property_name, value))
|
||||
after = vector_value(obj, property_name)
|
||||
after_objects = object_set(document)
|
||||
failures.append({
|
||||
"id": case_id,
|
||||
"requested": repr(requested),
|
||||
"exception": exception,
|
||||
"before": before,
|
||||
"after": after,
|
||||
"valuePreserved": before == after,
|
||||
"beforeObjects": before_objects,
|
||||
"afterObjects": after_objects,
|
||||
"objectsPreserved": before_objects == after_objects,
|
||||
"polluted": before_objects != after_objects,
|
||||
})
|
||||
if exception is None:
|
||||
setattr(obj, property_name, tuple(baseline))
|
||||
return {
|
||||
"object": {"name": obj.Name, "typeId": obj.TypeId},
|
||||
"property": {"name": property_name, "typeId": obj.getTypeIdOfProperty(property_name)},
|
||||
"baseline": baseline,
|
||||
"acceptedCoercions": accepted,
|
||||
"failures": failures,
|
||||
"documentIntegrity": {
|
||||
"initialObjects": initial_objects,
|
||||
"finalObjects": object_set(document),
|
||||
"objectsPreserved": initial_objects == object_set(document),
|
||||
},
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
def run_consumer_direction(case_id, object_type_id, setup, requested):
|
||||
document = App.newDocument("PropertyDirectionConsumer" + case_id)
|
||||
try:
|
||||
obj, property_name = setup(document)
|
||||
initial_recompute = bool(document.recompute())
|
||||
before = {
|
||||
"value": vector_value(obj, property_name),
|
||||
"state": state_snapshot(obj),
|
||||
"shape": shape_snapshot(obj),
|
||||
"objects": object_set(document),
|
||||
}
|
||||
setter_exception = exception_snapshot(lambda: setattr(obj, property_name, requested))
|
||||
after_set = {
|
||||
"value": vector_value(obj, property_name),
|
||||
"state": state_snapshot(obj),
|
||||
"shape": shape_snapshot(obj),
|
||||
}
|
||||
recompute_exception = None
|
||||
recompute_result = None
|
||||
try:
|
||||
recompute_result = bool(document.recompute())
|
||||
except Exception as error:
|
||||
recompute_exception = {"type": type(error).__name__, "message": str(error)}
|
||||
after_recompute = {
|
||||
"value": vector_value(obj, property_name),
|
||||
"state": state_snapshot(obj),
|
||||
"shape": shape_snapshot(obj),
|
||||
"objects": object_set(document),
|
||||
}
|
||||
return {
|
||||
"id": case_id,
|
||||
"objectTypeId": object_type_id,
|
||||
"propertyName": property_name,
|
||||
"requested": [component_value(value) for value in requested],
|
||||
"initialRecomputeResult": initial_recompute,
|
||||
"setterException": setter_exception,
|
||||
"recomputeException": recompute_exception,
|
||||
"recomputeResult": recompute_result,
|
||||
"before": before,
|
||||
"afterSet": after_set,
|
||||
"afterRecompute": after_recompute,
|
||||
"objectsPreserved": before["objects"] == after_recompute["objects"],
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
def run_missing_dependency(case_id, object_type_id, setup, remove_dependency):
|
||||
document = App.newDocument("PropertyDirectionDependency" + case_id)
|
||||
try:
|
||||
obj, property_name = setup(document)
|
||||
document.recompute()
|
||||
before = {
|
||||
"value": vector_value(obj, property_name),
|
||||
"state": state_snapshot(obj),
|
||||
"shape": shape_snapshot(obj),
|
||||
"objects": object_set(document),
|
||||
}
|
||||
removed = remove_dependency(obj)
|
||||
recompute_exception = None
|
||||
recompute_result = None
|
||||
try:
|
||||
recompute_result = bool(document.recompute())
|
||||
except Exception as error:
|
||||
recompute_exception = {"type": type(error).__name__, "message": str(error)}
|
||||
after = {
|
||||
"value": vector_value(obj, property_name),
|
||||
"state": state_snapshot(obj),
|
||||
"shape": shape_snapshot(obj),
|
||||
"objects": object_set(document),
|
||||
}
|
||||
return {
|
||||
"id": case_id,
|
||||
"objectTypeId": object_type_id,
|
||||
"propertyName": property_name,
|
||||
"removed": removed,
|
||||
"recomputeException": recompute_exception,
|
||||
"recomputeResult": recompute_result,
|
||||
"before": before,
|
||||
"afterRecompute": after,
|
||||
"objectsPreserved": before["objects"] == after["objects"],
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
def run_disabled_and_transaction():
|
||||
document = App.newDocument("PropertyDirectionDisabledAndTransaction")
|
||||
try:
|
||||
obj, property_name = setup_mirroring(document)
|
||||
document.recompute()
|
||||
document.UndoMode = 1
|
||||
baseline = vector_value(obj, property_name)
|
||||
initial_objects = object_set(document)
|
||||
|
||||
obj.setEditorMode(property_name, ["ReadOnly"])
|
||||
editor_mode = [str(item) for item in obj.getEditorMode(property_name)]
|
||||
editor_exception = exception_snapshot(lambda: setattr(obj, property_name, (1.0, 0.0, 1.0)))
|
||||
editor_after = vector_value(obj, property_name)
|
||||
setattr(obj, property_name, tuple(baseline))
|
||||
obj.setPropertyStatus(property_name, "Immutable")
|
||||
immutable_status = [str(item) for item in obj.getPropertyStatus(property_name)]
|
||||
immutable_exception = exception_snapshot(lambda: setattr(obj, property_name, (0.0, 1.0, 1.0)))
|
||||
immutable_after = vector_value(obj, property_name)
|
||||
obj.setPropertyStatus(property_name, "-Immutable")
|
||||
obj.setEditorMode(property_name, 0)
|
||||
disabled = {
|
||||
"classification": "editor-readonly-python-mutable-until-immutable",
|
||||
"before": baseline,
|
||||
"editorMode": editor_mode,
|
||||
"editorRequested": [1.0, 0.0, 1.0],
|
||||
"editorException": editor_exception,
|
||||
"editorAfter": editor_after,
|
||||
"pythonBypassesEditorReadOnly": editor_exception is None,
|
||||
"immutableStatus": immutable_status,
|
||||
"immutableRequested": [0.0, 1.0, 1.0],
|
||||
"immutableException": immutable_exception,
|
||||
"immutableAfter": immutable_after,
|
||||
"immutableValuePreserved": immutable_after == baseline,
|
||||
"restoredStatus": [str(item) for item in obj.getPropertyStatus(property_name)],
|
||||
"restoredEditorMode": [str(item) for item in obj.getEditorMode(property_name)],
|
||||
}
|
||||
|
||||
document.recompute()
|
||||
document.openTransaction("property-direction-cancel")
|
||||
transaction_before = vector_value(obj, property_name)
|
||||
transaction_objects_before = object_set(document)
|
||||
setattr(obj, property_name, (0.25, -0.5, 1.5))
|
||||
transaction_edited = vector_value(obj, property_name)
|
||||
state_after_edit = state_snapshot(obj)
|
||||
pending_after_edit = bool(document.HasPendingTransaction)
|
||||
active_after_edit = active_transaction_snapshot()
|
||||
document.abortTransaction()
|
||||
transaction_after_abort = vector_value(obj, property_name)
|
||||
state_after_abort = state_snapshot(obj)
|
||||
recovery_recompute_result = bool(document.recompute())
|
||||
transaction_after_recompute = vector_value(obj, property_name)
|
||||
state_after_recompute = state_snapshot(obj)
|
||||
transaction_objects_after = object_set(document)
|
||||
return {
|
||||
"object": {"name": obj.Name, "typeId": obj.TypeId},
|
||||
"property": {"name": property_name, "typeId": obj.getTypeIdOfProperty(property_name)},
|
||||
"disabled": disabled,
|
||||
"transaction": {
|
||||
"before": transaction_before,
|
||||
"edited": transaction_edited,
|
||||
"afterAbort": transaction_after_abort,
|
||||
"afterRecompute": transaction_after_recompute,
|
||||
"stateAfterEdit": state_after_edit,
|
||||
"stateAfterAbort": state_after_abort,
|
||||
"stateAfterRecompute": state_after_recompute,
|
||||
"objectsBefore": transaction_objects_before,
|
||||
"objectsAfter": transaction_objects_after,
|
||||
"undoMode": int(document.UndoMode),
|
||||
"pendingAfterEdit": pending_after_edit,
|
||||
"pendingAfterAbort": bool(document.HasPendingTransaction),
|
||||
"activeAfterEdit": active_after_edit,
|
||||
"activeAfterAbort": active_transaction_snapshot(),
|
||||
"recoveryRecomputeResult": recovery_recompute_result,
|
||||
"restored": transaction_before == transaction_after_abort == transaction_after_recompute,
|
||||
"objectsRestored": transaction_objects_before == transaction_objects_after,
|
||||
},
|
||||
"documentIntegrity": {
|
||||
"initialObjects": initial_objects,
|
||||
"finalObjects": object_set(document),
|
||||
"objectsPreserved": initial_objects == object_set(document),
|
||||
},
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
NON_FINITE_VALUES = {
|
||||
"nan": (float("nan"), 0.0, 1.0),
|
||||
"positive-infinity": (float("inf"), 0.0, 1.0),
|
||||
"negative-infinity": (float("-inf"), 0.0, 1.0),
|
||||
}
|
||||
if PROBE_MODE.startswith("non-finite:"):
|
||||
_, host_id, value_id = PROBE_MODE.split(":", 2)
|
||||
host_options = {
|
||||
"Mirroring": ("Part::Mirroring", setup_mirroring),
|
||||
"ProjectOnSurface": ("Part::ProjectOnSurface", setup_projection),
|
||||
}
|
||||
object_type_id, setup = host_options[host_id]
|
||||
print("FREECAD_PROPERTY_DIRECTION_PROGRESS=" + host_id + "-" + value_id, flush=True)
|
||||
isolated_case = run_consumer_direction(host_id + "-" + value_id, object_type_id, setup, NON_FINITE_VALUES[value_id])
|
||||
isolated_report = {
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-direction-failure-isolated",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"propertyType": "App::PropertyDirection",
|
||||
"case": isolated_case,
|
||||
}
|
||||
if not OUTPUT_PATH:
|
||||
raise RuntimeError("FREECAD_PROPERTY_DIRECTION_FAILURE_OUTPUT is required")
|
||||
with open(OUTPUT_PATH, "w", encoding="utf-8") as handle:
|
||||
json.dump(isolated_report, handle, indent=2, sort_keys=True, allow_nan=False)
|
||||
handle.write("\n")
|
||||
print("FREECAD_PROPERTY_DIRECTION_FAILURE_ISOLATED_RESULT=" + json.dumps({"status": "pass", "caseId": isolated_case["id"]}, sort_keys=True))
|
||||
raise SystemExit(0)
|
||||
|
||||
|
||||
setter_boundaries = run_setter_boundaries()
|
||||
consumer_failures = []
|
||||
for host_id, object_type_id, setup in [
|
||||
("Mirroring", "Part::Mirroring", setup_mirroring),
|
||||
("ProjectOnSurface", "Part::ProjectOnSurface", setup_projection),
|
||||
]:
|
||||
print("FREECAD_PROPERTY_DIRECTION_PROGRESS=" + host_id + "-zero", flush=True)
|
||||
consumer_failures.append(run_consumer_direction(host_id + "-zero", object_type_id, setup, (0.0, 0.0, 0.0)))
|
||||
|
||||
|
||||
def remove_mirror_source(obj):
|
||||
obj.Source = None
|
||||
return {"property": "Source", "value": None}
|
||||
|
||||
|
||||
def remove_projection_support(obj):
|
||||
obj.SupportFace = None
|
||||
return {"property": "SupportFace", "value": None}
|
||||
|
||||
|
||||
def remove_projection_inputs(obj):
|
||||
obj.Projection = []
|
||||
return {"property": "Projection", "value": []}
|
||||
|
||||
|
||||
dependency_failures = [
|
||||
run_missing_dependency("Mirroring-no-source", "Part::Mirroring", setup_mirroring, remove_mirror_source),
|
||||
run_missing_dependency("ProjectOnSurface-no-support", "Part::ProjectOnSurface", setup_projection, remove_projection_support),
|
||||
run_missing_dependency("ProjectOnSurface-no-projection", "Part::ProjectOnSurface", setup_projection, remove_projection_inputs),
|
||||
]
|
||||
disabled_and_transaction = run_disabled_and_transaction()
|
||||
report = {
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-direction-failure",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"propertyType": "App::PropertyDirection",
|
||||
"setterBoundaries": setter_boundaries,
|
||||
"consumerFailures": consumer_failures,
|
||||
"dependencyFailures": dependency_failures,
|
||||
"disabled": disabled_and_transaction["disabled"],
|
||||
"transaction": disabled_and_transaction["transaction"],
|
||||
"documentIntegrity": {
|
||||
"setter": setter_boundaries["documentIntegrity"],
|
||||
"disabledAndTransaction": disabled_and_transaction["documentIntegrity"],
|
||||
"allObjectsPreserved": setter_boundaries["documentIntegrity"]["objectsPreserved"]
|
||||
and disabled_and_transaction["documentIntegrity"]["objectsPreserved"]
|
||||
and all(entry["objectsPreserved"] for entry in consumer_failures)
|
||||
and all(entry["objectsPreserved"] for entry in dependency_failures),
|
||||
},
|
||||
"cancellationBoundary": {
|
||||
"supported": False,
|
||||
"classification": "synchronous-property-setter",
|
||||
"reason": "no-native-cancel-hook",
|
||||
"replacement": "abort-active-document-transaction",
|
||||
},
|
||||
}
|
||||
if not OUTPUT_PATH:
|
||||
raise RuntimeError("FREECAD_PROPERTY_DIRECTION_FAILURE_OUTPUT is required")
|
||||
with open(OUTPUT_PATH, "w", encoding="utf-8") as handle:
|
||||
json.dump(report, handle, indent=2, sort_keys=True, allow_nan=False)
|
||||
handle.write("\n")
|
||||
print("FREECAD_PROPERTY_DIRECTION_FAILURE_RESULT=" + json.dumps({
|
||||
"status": report["status"],
|
||||
"setterFailureCount": len(setter_boundaries["failures"]),
|
||||
"consumerFailureCount": len(consumer_failures),
|
||||
"dependencyFailureCount": len(dependency_failures),
|
||||
}, sort_keys=True))
|
||||
153
scripts/freecad-property-direction-mutation.py
Normal file
153
scripts/freecad-property-direction-mutation.py
Normal file
@@ -0,0 +1,153 @@
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
|
||||
import FreeCAD as App
|
||||
import Part
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_DIRECTION_MUTATION_OUTPUT", "")
|
||||
EDITED_VALUE = (0.25, -0.5, 1.5)
|
||||
|
||||
|
||||
def vector_value(obj, property_name):
|
||||
value = getattr(obj, property_name)
|
||||
return [float(value.x), float(value.y), float(value.z)]
|
||||
|
||||
|
||||
def object_set(document):
|
||||
return [{"name": obj.Name, "typeId": obj.TypeId} for obj in document.Objects]
|
||||
|
||||
|
||||
def shape_snapshot(obj):
|
||||
shape = obj.Shape
|
||||
if shape.isNull():
|
||||
return {
|
||||
"isNull": True,
|
||||
"valid": False,
|
||||
"shapeType": "Null",
|
||||
"solids": 0,
|
||||
"faces": 0,
|
||||
"edges": 0,
|
||||
"vertices": 0,
|
||||
}
|
||||
bounds = shape.BoundBox
|
||||
return {
|
||||
"isNull": False,
|
||||
"valid": bool(shape.isValid()),
|
||||
"shapeType": shape.ShapeType,
|
||||
"solids": len(shape.Solids),
|
||||
"faces": len(shape.Faces),
|
||||
"edges": len(shape.Edges),
|
||||
"vertices": len(shape.Vertexes),
|
||||
"area": round(float(shape.Area), 9),
|
||||
"volume": round(float(shape.Volume), 9),
|
||||
"bounds": [round(float(value), 9) for value in (bounds.XMin, bounds.YMin, bounds.ZMin, bounds.XMax, bounds.YMax, bounds.ZMax)],
|
||||
"brepSha256": hashlib.sha256(shape.exportBrepToString().encode("utf-8")).hexdigest(),
|
||||
}
|
||||
|
||||
|
||||
def snapshot(document, obj, property_name, recompute_result=None):
|
||||
return {
|
||||
"value": vector_value(obj, property_name),
|
||||
"propertyTypeId": obj.getTypeIdOfProperty(property_name),
|
||||
"propertyStatus": [str(item) for item in obj.getPropertyStatus(property_name)],
|
||||
"editorMode": [str(item) for item in obj.getEditorMode(property_name)],
|
||||
"objectState": [str(item) for item in obj.State],
|
||||
"statusString": str(obj.getStatusString()),
|
||||
"mustExecute": bool(obj.MustExecute),
|
||||
"recomputeResult": recompute_result,
|
||||
"shape": shape_snapshot(obj),
|
||||
"objectSet": object_set(document),
|
||||
}
|
||||
|
||||
|
||||
def setup_mirroring(document):
|
||||
source = document.addObject("Part::Box", "MirrorSource")
|
||||
source.Length = 2
|
||||
source.Width = 3
|
||||
source.Height = 4
|
||||
host = document.addObject("Part::Mirroring", "DirectionMirror")
|
||||
host.Source = source
|
||||
host.Base = App.Vector(0, 0, 0)
|
||||
return host, "Normal"
|
||||
|
||||
|
||||
def setup_projection(document):
|
||||
support = document.addObject("Part::Feature", "ProjectionSupport")
|
||||
support.Shape = Part.makePlane(40, 40, App.Vector(-20, -20, 0))
|
||||
wire = document.addObject("Part::Feature", "ProjectionWire")
|
||||
wire.Shape = Part.makePolygon([
|
||||
App.Vector(-4, -3, -5),
|
||||
App.Vector(4, -3, -5),
|
||||
App.Vector(4, 3, -5),
|
||||
App.Vector(-4, 3, -5),
|
||||
App.Vector(-4, -3, -5),
|
||||
])
|
||||
host = document.addObject("Part::ProjectOnSurface", "DirectionProjection")
|
||||
host.SupportFace = (support, ["Face1"])
|
||||
host.Projection = [(wire, ["Wire1"])]
|
||||
host.Mode = "Edges"
|
||||
return host, "Direction"
|
||||
|
||||
|
||||
def run_case(case_id, object_type_id, setup):
|
||||
document = App.newDocument("PropertyDirectionMutation" + case_id)
|
||||
try:
|
||||
obj, property_name = setup(document)
|
||||
before_recompute = bool(document.recompute())
|
||||
before = snapshot(document, obj, property_name, before_recompute)
|
||||
|
||||
setattr(obj, property_name, EDITED_VALUE)
|
||||
after_set = snapshot(document, obj, property_name)
|
||||
edited_recompute = bool(document.recompute())
|
||||
after_recompute = snapshot(document, obj, property_name, edited_recompute)
|
||||
|
||||
setattr(obj, property_name, tuple(before["value"]))
|
||||
after_restore_set = snapshot(document, obj, property_name)
|
||||
restored_recompute = bool(document.recompute())
|
||||
after_restore_recompute = snapshot(document, obj, property_name, restored_recompute)
|
||||
return {
|
||||
"caseId": case_id,
|
||||
"objectTypeId": object_type_id,
|
||||
"objectName": obj.Name,
|
||||
"propertyName": property_name,
|
||||
"editedValue": list(EDITED_VALUE),
|
||||
"phases": {
|
||||
"before": before,
|
||||
"afterSet": after_set,
|
||||
"afterRecompute": after_recompute,
|
||||
"afterRestoreSet": after_restore_set,
|
||||
"afterRestoreRecompute": after_restore_recompute,
|
||||
},
|
||||
"valueChanged": before["value"] != after_recompute["value"],
|
||||
"valueRestored": before["value"] == after_restore_recompute["value"],
|
||||
"geometryChanged": before["shape"]["brepSha256"] != after_recompute["shape"]["brepSha256"],
|
||||
"geometryRestored": before["shape"] == after_restore_recompute["shape"],
|
||||
"objectsPreserved": before["objectSet"] == after_recompute["objectSet"] == after_restore_recompute["objectSet"],
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
cases = [
|
||||
run_case("Mirroring", "Part::Mirroring", setup_mirroring),
|
||||
run_case("ProjectOnSurface", "Part::ProjectOnSurface", setup_projection),
|
||||
]
|
||||
report = {
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-direction-mutation",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"propertyType": "App::PropertyDirection",
|
||||
"caseCount": len(cases),
|
||||
"cases": cases,
|
||||
}
|
||||
if not OUTPUT_PATH:
|
||||
raise RuntimeError("FREECAD_PROPERTY_DIRECTION_MUTATION_OUTPUT is required")
|
||||
with open(OUTPUT_PATH, "w", encoding="utf-8") as handle:
|
||||
json.dump(report, handle, indent=2, sort_keys=True)
|
||||
handle.write("\n")
|
||||
print("FREECAD_PROPERTY_DIRECTION_MUTATION_RESULT=" + json.dumps({"status": report["status"], "caseCount": report["caseCount"]}, sort_keys=True))
|
||||
110
scripts/freecad-property-direction-roundtrip.py
Normal file
110
scripts/freecad-property-direction-roundtrip.py
Normal file
@@ -0,0 +1,110 @@
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
|
||||
|
||||
def vector_value(obj):
|
||||
value = obj.Normal
|
||||
return {"x": float(value.x), "y": float(value.y), "z": float(value.z)}
|
||||
|
||||
|
||||
def shape_snapshot(obj):
|
||||
shape = obj.Shape
|
||||
bounds = shape.BoundBox
|
||||
return {
|
||||
"isNull": bool(shape.isNull()),
|
||||
"valid": bool(shape.isValid()),
|
||||
"shapeType": shape.ShapeType,
|
||||
"solids": len(shape.Solids),
|
||||
"faces": len(shape.Faces),
|
||||
"edges": len(shape.Edges),
|
||||
"vertices": len(shape.Vertexes),
|
||||
"area": round(float(shape.Area), 9),
|
||||
"volume": round(float(shape.Volume), 9),
|
||||
"bounds": [round(float(value), 9) for value in (bounds.XMin, bounds.YMin, bounds.ZMin, bounds.XMax, bounds.YMax, bounds.ZMax)],
|
||||
"brepSha256": hashlib.sha256(shape.exportBrepToString().encode("utf-8")).hexdigest(),
|
||||
}
|
||||
|
||||
|
||||
def object_snapshot(document):
|
||||
obj = document.getObject("DirectionMirror")
|
||||
return {
|
||||
"objectSet": [{"name": item.Name, "typeId": item.TypeId} for item in document.Objects],
|
||||
"object": {
|
||||
"name": obj.Name,
|
||||
"typeId": obj.TypeId,
|
||||
"propertyTypeId": obj.getTypeIdOfProperty("Normal"),
|
||||
"value": vector_value(obj),
|
||||
"propertyStatus": [str(item) for item in obj.getPropertyStatus("Normal")],
|
||||
"editorMode": [str(item) for item in obj.getEditorMode("Normal")],
|
||||
"state": [str(item) for item in obj.State],
|
||||
"statusString": str(obj.getStatusString()),
|
||||
"shape": shape_snapshot(obj),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def create_native(path):
|
||||
document = App.newDocument("PropertyDirectionRoundtrip")
|
||||
try:
|
||||
source = document.addObject("Part::Box", "MirrorSource")
|
||||
source.Length = 2
|
||||
source.Width = 3
|
||||
source.Height = 4
|
||||
obj = document.addObject("Part::Mirroring", "DirectionMirror")
|
||||
obj.Source = source
|
||||
obj.Base = App.Vector(0, 0, 0)
|
||||
obj.Normal = App.Vector(0, 0, 1)
|
||||
document.recompute()
|
||||
document.saveAs(path)
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
reopened = App.openDocument(path)
|
||||
try:
|
||||
reopened.recompute()
|
||||
return object_snapshot(reopened)
|
||||
finally:
|
||||
App.closeDocument(reopened.Name)
|
||||
|
||||
|
||||
def verify_native(path, resaved_path):
|
||||
document = App.openDocument(path)
|
||||
try:
|
||||
document.recompute()
|
||||
reopened = object_snapshot(document)
|
||||
document.saveAs(resaved_path)
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
resaved_document = App.openDocument(resaved_path)
|
||||
try:
|
||||
resaved_document.recompute()
|
||||
resaved = object_snapshot(resaved_document)
|
||||
finally:
|
||||
App.closeDocument(resaved_document.Name)
|
||||
return {"reopened": reopened, "resaved": resaved}
|
||||
|
||||
|
||||
mode = os.environ.get("FREECAD_PROPERTY_DIRECTION_ROUNDTRIP_MODE", "")
|
||||
path = os.environ.get("FREECAD_PROPERTY_DIRECTION_ROUNDTRIP_PATH", "")
|
||||
resaved_path = os.environ.get("FREECAD_PROPERTY_DIRECTION_ROUNDTRIP_RESAVED_PATH", "")
|
||||
if mode == "create":
|
||||
result = create_native(path)
|
||||
elif mode == "verify":
|
||||
result = verify_native(path, resaved_path)
|
||||
else:
|
||||
raise RuntimeError("FREECAD_PROPERTY_DIRECTION_ROUNDTRIP_MODE must be create or verify")
|
||||
|
||||
print("FREECAD_PROPERTY_DIRECTION_ROUNDTRIP_RESULT=" + json.dumps({
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-direction-roundtrip",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"mode": mode,
|
||||
"result": result,
|
||||
}, sort_keys=True))
|
||||
168
scripts/freecad-property-direction-success.py
Normal file
168
scripts/freecad-property-direction-success.py
Normal file
@@ -0,0 +1,168 @@
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
|
||||
import FreeCAD as App
|
||||
import Part
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_DIRECTION_SUCCESS_OUTPUT", "")
|
||||
ASSIGNMENTS = [
|
||||
("axisZTuple", (0.0, 0.0, 1.0), "tuple"),
|
||||
("integerTuple", (1, 1, 2), "tuple"),
|
||||
("nonUnitFloatTuple", (0.25, -0.5, 1.5), "tuple"),
|
||||
("vectorObject", App.Vector(-0.125, 0.25, 1.0), "Base.Vector"),
|
||||
("nearAxisTuple", (1.0e-9, -1.0e-9, 1.0), "tuple"),
|
||||
("largeFiniteTuple", (1.0e6, -2.0e6, 3.0e6), "tuple"),
|
||||
]
|
||||
|
||||
|
||||
def vector_value(obj, property_name):
|
||||
value = getattr(obj, property_name)
|
||||
return [float(value.x), float(value.y), float(value.z)]
|
||||
|
||||
|
||||
def object_snapshot(document):
|
||||
return [{"name": obj.Name, "typeId": obj.TypeId} for obj in document.Objects]
|
||||
|
||||
|
||||
def shape_snapshot(obj):
|
||||
shape = obj.Shape
|
||||
if shape.isNull():
|
||||
return {"applicable": True, "isNull": True, "shapeType": "Null", "solids": 0, "faces": 0, "edges": 0, "vertices": 0}
|
||||
brep = shape.exportBrepToString().encode("utf-8")
|
||||
bounds = shape.BoundBox
|
||||
return {
|
||||
"applicable": True,
|
||||
"isNull": False,
|
||||
"valid": bool(shape.isValid()),
|
||||
"shapeType": shape.ShapeType,
|
||||
"solids": len(shape.Solids),
|
||||
"faces": len(shape.Faces),
|
||||
"edges": len(shape.Edges),
|
||||
"vertices": len(shape.Vertexes),
|
||||
"area": round(float(shape.Area), 9),
|
||||
"volume": round(float(shape.Volume), 9),
|
||||
"bounds": [round(float(value), 9) for value in (bounds.XMin, bounds.YMin, bounds.ZMin, bounds.XMax, bounds.YMax, bounds.ZMax)],
|
||||
"brepSha256": hashlib.sha256(brep).hexdigest(),
|
||||
}
|
||||
|
||||
|
||||
def host_snapshot(document, obj, property_name, recompute_result):
|
||||
return {
|
||||
"value": vector_value(obj, property_name),
|
||||
"propertyTypeId": obj.getTypeIdOfProperty(property_name),
|
||||
"propertyStatus": [str(item) for item in obj.getPropertyStatus(property_name)],
|
||||
"editorMode": [str(item) for item in obj.getEditorMode(property_name)],
|
||||
"objectState": [str(item) for item in obj.State],
|
||||
"statusString": str(obj.getStatusString()),
|
||||
"mustExecute": bool(obj.MustExecute),
|
||||
"recomputeResult": bool(recompute_result),
|
||||
"shape": shape_snapshot(obj),
|
||||
"objectSet": object_snapshot(document),
|
||||
}
|
||||
|
||||
|
||||
def touched_snapshot(obj, property_name):
|
||||
return {
|
||||
"value": vector_value(obj, property_name),
|
||||
"objectState": [str(item) for item in obj.State],
|
||||
"statusString": str(obj.getStatusString()),
|
||||
"mustExecute": bool(obj.MustExecute),
|
||||
}
|
||||
|
||||
|
||||
def setup_mirroring(document):
|
||||
source = document.addObject("Part::Box", "MirrorSource")
|
||||
source.Length = 2
|
||||
source.Width = 3
|
||||
source.Height = 4
|
||||
host = document.addObject("Part::Mirroring", "DirectionMirror")
|
||||
host.Source = source
|
||||
host.Base = App.Vector(0, 0, 0)
|
||||
return host, "Normal", {
|
||||
"kind": "box-mirror",
|
||||
"source": {"name": source.Name, "typeId": source.TypeId, "dimensions": [2, 3, 4]},
|
||||
"base": [0, 0, 0],
|
||||
"mirrorPlane": None,
|
||||
}
|
||||
|
||||
|
||||
def setup_projection(document):
|
||||
support = document.addObject("Part::Feature", "ProjectionSupport")
|
||||
support.Shape = Part.makePlane(40, 40, App.Vector(-20, -20, 0))
|
||||
wire = document.addObject("Part::Feature", "ProjectionWire")
|
||||
points = [
|
||||
App.Vector(-4, -3, -5),
|
||||
App.Vector(4, -3, -5),
|
||||
App.Vector(4, 3, -5),
|
||||
App.Vector(-4, 3, -5),
|
||||
App.Vector(-4, -3, -5),
|
||||
]
|
||||
wire.Shape = Part.makePolygon(points)
|
||||
host = document.addObject("Part::ProjectOnSurface", "DirectionProjection")
|
||||
host.SupportFace = (support, ["Face1"])
|
||||
host.Projection = [(wire, ["Wire1"])]
|
||||
host.Mode = "Edges"
|
||||
return host, "Direction", {
|
||||
"kind": "wire-to-planar-face",
|
||||
"support": {"name": support.Name, "typeId": support.TypeId, "subElements": ["Face1"]},
|
||||
"projection": {"name": wire.Name, "typeId": wire.TypeId, "subElements": ["Wire1"], "z": -5},
|
||||
"mode": "Edges",
|
||||
}
|
||||
|
||||
|
||||
def run_case(case_id, object_type_id, setup):
|
||||
document = App.newDocument("PropertyDirection" + case_id)
|
||||
try:
|
||||
obj, property_name, setup_report = setup(document)
|
||||
initial_recompute = document.recompute()
|
||||
default_value = vector_value(obj, property_name)
|
||||
phases = {"default": {"afterRecompute": host_snapshot(document, obj, property_name, initial_recompute)}}
|
||||
requested = {}
|
||||
for phase, value, input_kind in ASSIGNMENTS:
|
||||
requested[phase] = {"inputKind": input_kind, "value": [float(component) for component in value]}
|
||||
setattr(obj, property_name, value)
|
||||
touched = touched_snapshot(obj, property_name)
|
||||
recompute_result = document.recompute()
|
||||
phases[phase] = {"afterSet": touched, "afterRecompute": host_snapshot(document, obj, property_name, recompute_result)}
|
||||
setattr(obj, property_name, tuple(default_value))
|
||||
restored_touched = touched_snapshot(obj, property_name)
|
||||
restored_recompute = document.recompute()
|
||||
phases["restored"] = {"afterSet": restored_touched, "afterRecompute": host_snapshot(document, obj, property_name, restored_recompute)}
|
||||
return {
|
||||
"caseId": case_id,
|
||||
"objectTypeId": object_type_id,
|
||||
"objectName": obj.Name,
|
||||
"propertyName": property_name,
|
||||
"mode": "direct-native-property-setter-with-valid-host-geometry",
|
||||
"setup": setup_report,
|
||||
"defaultValue": default_value,
|
||||
"requested": requested,
|
||||
"phases": phases,
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
cases = [
|
||||
run_case("Mirroring", "Part::Mirroring", setup_mirroring),
|
||||
run_case("ProjectOnSurface", "Part::ProjectOnSurface", setup_projection),
|
||||
]
|
||||
report = {
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-direction-success",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"propertyType": "App::PropertyDirection",
|
||||
"caseCount": len(cases),
|
||||
"cases": cases,
|
||||
}
|
||||
if not OUTPUT_PATH:
|
||||
raise RuntimeError("FREECAD_PROPERTY_DIRECTION_SUCCESS_OUTPUT is required")
|
||||
with open(OUTPUT_PATH, "w", encoding="utf-8") as handle:
|
||||
json.dump(report, handle, indent=2, sort_keys=True)
|
||||
handle.write("\n")
|
||||
print("FREECAD_PROPERTY_DIRECTION_SUCCESS_RESULT=" + json.dumps({"status": report["status"], "caseCount": report["caseCount"]}, sort_keys=True))
|
||||
105
scripts/freecad-property-file-failure.py
Normal file
105
scripts/freecad-property-file-failure.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import json
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
|
||||
|
||||
def object_set(document):
|
||||
return [{"name": obj.Name, "typeId": obj.TypeId} for obj in document.Objects]
|
||||
|
||||
|
||||
def snapshot(document, obj):
|
||||
document.recompute()
|
||||
return {
|
||||
"value": str(obj.FileName),
|
||||
"propertyTypeId": obj.getTypeIdOfProperty("FileName"),
|
||||
"propertyStatus": [str(item) for item in obj.getPropertyStatus("FileName")],
|
||||
"editorMode": [str(item) for item in obj.getEditorMode("FileName")],
|
||||
"objectState": [str(item) for item in obj.State],
|
||||
"statusString": str(obj.getStatusString()),
|
||||
"objectSet": object_set(document),
|
||||
}
|
||||
|
||||
|
||||
def exception_snapshot(callback):
|
||||
try:
|
||||
callback()
|
||||
except Exception as error:
|
||||
return {"type": type(error).__name__, "message": str(error)}
|
||||
return None
|
||||
|
||||
|
||||
def active_transaction_snapshot():
|
||||
active = App.getActiveTransaction()
|
||||
if not active:
|
||||
return {"name": "", "id": 0}
|
||||
return {"name": str(active[0]), "id": int(active[1])}
|
||||
|
||||
|
||||
def run():
|
||||
document = App.newDocument("PropertyFileFailure")
|
||||
try:
|
||||
obj = document.addObject("Mesh::Import", "FileProbe")
|
||||
document.recompute()
|
||||
document.UndoMode = 1
|
||||
initial = snapshot(document, obj)
|
||||
failures = []
|
||||
for case_id, requested in [("missing-path", "/definitely/missing/bitbybit-file.stl"), ("wrong-type", 12345)]:
|
||||
before = snapshot(document, obj)
|
||||
before_objects = object_set(document)
|
||||
assignment_exception = exception_snapshot(lambda: setattr(obj, "FileName", requested))
|
||||
recompute_exception = None
|
||||
try:
|
||||
document.recompute()
|
||||
except Exception as error:
|
||||
recompute_exception = {"type": type(error).__name__, "message": str(error)}
|
||||
after = snapshot(document, obj)
|
||||
after_objects = object_set(document)
|
||||
failures.append({"id": case_id, "requested": requested, "assignmentException": assignment_exception, "recomputeException": recompute_exception, "before": before, "after": after, "beforeObjects": before_objects, "afterObjects": after_objects, "objectsPreserved": before_objects == after_objects})
|
||||
|
||||
obj.FileName = ""
|
||||
document.recompute()
|
||||
disabled_before = snapshot(document, obj)
|
||||
disabled_objects_before = object_set(document)
|
||||
obj.setEditorMode("FileName", ["ReadOnly"])
|
||||
obj.setPropertyStatus("FileName", "Immutable")
|
||||
disabled_status = [str(item) for item in obj.getPropertyStatus("FileName")]
|
||||
disabled_editor_mode = [str(item) for item in obj.getEditorMode("FileName")]
|
||||
disabled_exception = exception_snapshot(lambda: setattr(obj, "FileName", "/tmp/blocked.stl"))
|
||||
disabled_after = snapshot(document, obj)
|
||||
disabled_objects_after = object_set(document)
|
||||
obj.setPropertyStatus("FileName", "-Immutable")
|
||||
obj.setEditorMode("FileName", 0)
|
||||
|
||||
document.openTransaction("property-file-cancel")
|
||||
transaction_before = snapshot(document, obj)
|
||||
transaction_objects_before = object_set(document)
|
||||
obj.FileName = "/tmp/transaction-file.stl"
|
||||
transaction_edited = snapshot(document, obj)
|
||||
transaction_pending_after_edit = bool(document.HasPendingTransaction)
|
||||
transaction_active_after_edit = active_transaction_snapshot()
|
||||
document.abortTransaction()
|
||||
transaction_after_abort = snapshot(document, obj)
|
||||
transaction_objects_after = object_set(document)
|
||||
return {
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-file-failure",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"object": {"name": obj.Name, "typeId": obj.TypeId},
|
||||
"property": {"name": "FileName", "typeId": obj.getTypeIdOfProperty("FileName")},
|
||||
"initial": initial,
|
||||
"failures": failures,
|
||||
"disabled": {"classification": "editor-read-only-and-python-immutable", "status": disabled_status, "editorMode": disabled_editor_mode, "exception": disabled_exception, "before": disabled_before, "after": disabled_after, "beforeObjects": disabled_objects_before, "afterObjects": disabled_objects_after, "restoredStatus": [str(item) for item in obj.getPropertyStatus("FileName")], "restoredEditorMode": [str(item) for item in obj.getEditorMode("FileName")]},
|
||||
"transaction": {"before": transaction_before, "edited": transaction_edited, "afterAbort": transaction_after_abort, "objectsBefore": transaction_objects_before, "objectsAfter": transaction_objects_after, "undoMode": int(document.UndoMode), "pendingAfterEdit": transaction_pending_after_edit, "pendingAfterAbort": bool(document.HasPendingTransaction), "activeAfterEdit": transaction_active_after_edit, "activeAfterAbort": active_transaction_snapshot(), "restored": transaction_before == transaction_after_abort, "objectsRestored": transaction_objects_before == transaction_objects_after},
|
||||
"cancellationBoundary": {"supported": False, "classification": "synchronous-property-setter", "reason": "no-native-cancel-hook", "replacement": "abort-active-document-transaction"},
|
||||
"documentIntegrity": {"initialObjects": initial["objectSet"], "finalObjects": object_set(document), "objectsPreserved": initial["objectSet"] == object_set(document), "objectCount": len(document.Objects)},
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
print("FREECAD_PROPERTY_FILE_FAILURE_RESULT=" + json.dumps(run(), sort_keys=True))
|
||||
33
scripts/freecad-property-file-mutation.py
Normal file
33
scripts/freecad-property-file-mutation.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
ASSET = os.path.abspath(".cache/freecad/FreeCAD/data/tests/mesh.obj")
|
||||
ALTERNATE = "/tmp/property-file-alternate.stl"
|
||||
|
||||
|
||||
def snapshot(document, obj):
|
||||
recompute_result = bool(document.recompute())
|
||||
return {"value": str(obj.FileName), "propertyTypeId": obj.getTypeIdOfProperty("FileName"), "propertyStatus": [str(item) for item in obj.getPropertyStatus("FileName")], "editorMode": [str(item) for item in obj.getEditorMode("FileName")], "objectState": [str(item) for item in obj.State], "statusString": str(obj.getStatusString()), "recomputeResult": recompute_result, "objectSet": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects]}
|
||||
|
||||
|
||||
def run():
|
||||
document = App.newDocument("PropertyFileMutation")
|
||||
try:
|
||||
obj = document.addObject("Mesh::Import", "FileProbe")
|
||||
obj.FileName = ASSET
|
||||
document.recompute()
|
||||
before = snapshot(document, obj)
|
||||
obj.FileName = ALTERNATE
|
||||
edited = snapshot(document, obj)
|
||||
obj.FileName = ASSET
|
||||
restored = snapshot(document, obj)
|
||||
return {"schemaVersion": 1, "status": "pass", "baselineId": "freecad-1.1.1-property-file-mutation", "freecadVersion": ".".join(str(value) for value in App.Version()[:3]), "gitCommit": FREECAD_COMMIT, "propertyType": "App::PropertyFile", "object": {"name": obj.Name, "typeId": obj.TypeId}, "property": {"name": "FileName", "typeId": obj.getTypeIdOfProperty("FileName")}, "requested": {"before": ASSET, "edited": ALTERNATE, "restored": ASSET}, "phases": {"before": before, "edited": edited, "restored": restored}, "classification": {"valueChanged": before["value"] != edited["value"], "invalidEditedPath": edited["statusString"] == "File does not exist", "restoreExact": restored["value"] == before["value"] and restored["statusString"] == "Valid", "objectSetStable": before["objectSet"] == edited["objectSet"] == restored["objectSet"], "unknownSemanticDrift": False}}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
print("FREECAD_PROPERTY_FILE_MUTATION_RESULT=" + json.dumps(run(), sort_keys=True))
|
||||
102
scripts/freecad-property-file-roundtrip.py
Normal file
102
scripts/freecad-property-file-roundtrip.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
|
||||
|
||||
def mesh_snapshot(obj):
|
||||
mesh = obj.Mesh
|
||||
bounds = mesh.BoundBox
|
||||
return {
|
||||
"isEmpty": int(mesh.CountFacets) == 0,
|
||||
"facets": int(mesh.CountFacets),
|
||||
"points": int(mesh.CountPoints),
|
||||
"bounds": [round(float(value), 9) for value in (bounds.XMin, bounds.YMin, bounds.ZMin, bounds.XMax, bounds.YMax, bounds.ZMax)],
|
||||
}
|
||||
|
||||
|
||||
def object_snapshot(document):
|
||||
obj = document.getObject("FileProbe")
|
||||
path = str(obj.FileName)
|
||||
asset_bytes = open(path, "rb").read() if os.path.isfile(path) else b""
|
||||
return {
|
||||
"objectSet": [{"name": item.Name, "typeId": item.TypeId} for item in document.Objects],
|
||||
"object": {
|
||||
"name": obj.Name,
|
||||
"typeId": obj.TypeId,
|
||||
"propertyTypeId": obj.getTypeIdOfProperty("FileName"),
|
||||
"value": path,
|
||||
"propertyStatus": [str(item) for item in obj.getPropertyStatus("FileName")],
|
||||
"editorMode": [str(item) for item in obj.getEditorMode("FileName")],
|
||||
"state": [str(item) for item in obj.State],
|
||||
"statusString": str(obj.getStatusString()),
|
||||
"hasShapeProperty": "Shape" in obj.PropertiesList,
|
||||
"hasMeshProperty": "Mesh" in obj.PropertiesList,
|
||||
"asset": {
|
||||
"exists": bool(asset_bytes),
|
||||
"bytes": len(asset_bytes),
|
||||
"sha256": hashlib.sha256(asset_bytes).hexdigest() if asset_bytes else "",
|
||||
},
|
||||
"mesh": mesh_snapshot(obj),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def create_native(path, initial_value):
|
||||
document = App.newDocument("PropertyFileRoundtrip")
|
||||
try:
|
||||
obj = document.addObject("Mesh::Import", "FileProbe")
|
||||
obj.FileName = initial_value
|
||||
document.recompute()
|
||||
document.saveAs(path)
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
reopened = App.openDocument(path)
|
||||
try:
|
||||
reopened.recompute()
|
||||
return object_snapshot(reopened)
|
||||
finally:
|
||||
App.closeDocument(reopened.Name)
|
||||
|
||||
|
||||
def verify_native(path, resaved_path):
|
||||
document = App.openDocument(path)
|
||||
try:
|
||||
document.recompute()
|
||||
reopened = object_snapshot(document)
|
||||
document.saveAs(resaved_path)
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
resaved_document = App.openDocument(resaved_path)
|
||||
try:
|
||||
resaved_document.recompute()
|
||||
resaved = object_snapshot(resaved_document)
|
||||
finally:
|
||||
App.closeDocument(resaved_document.Name)
|
||||
return {"reopened": reopened, "resaved": resaved}
|
||||
|
||||
|
||||
mode = os.environ.get("FREECAD_PROPERTY_FILE_ROUNDTRIP_MODE", "")
|
||||
path = os.environ.get("FREECAD_PROPERTY_FILE_ROUNDTRIP_PATH", "")
|
||||
resaved_path = os.environ.get("FREECAD_PROPERTY_FILE_ROUNDTRIP_RESAVED_PATH", "")
|
||||
initial_value = os.environ.get("FREECAD_PROPERTY_FILE_ROUNDTRIP_INITIAL", "")
|
||||
if mode == "create":
|
||||
result = create_native(path, initial_value)
|
||||
elif mode == "verify":
|
||||
result = verify_native(path, resaved_path)
|
||||
else:
|
||||
raise RuntimeError("FREECAD_PROPERTY_FILE_ROUNDTRIP_MODE must be create or verify")
|
||||
|
||||
print("FREECAD_PROPERTY_FILE_ROUNDTRIP_RESULT=" + json.dumps({
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-file-roundtrip",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"mode": mode,
|
||||
"result": result,
|
||||
}, sort_keys=True))
|
||||
74
scripts/freecad-property-file-success.py
Normal file
74
scripts/freecad-property-file-success.py
Normal file
@@ -0,0 +1,74 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
ASSET = os.path.abspath(".cache/freecad/FreeCAD/data/tests/mesh.obj")
|
||||
|
||||
|
||||
def mesh_snapshot(obj):
|
||||
mesh = getattr(obj, "Mesh", None)
|
||||
if mesh is None:
|
||||
return {"applicable": False}
|
||||
try:
|
||||
bounds = mesh.BoundBox
|
||||
return {
|
||||
"applicable": True,
|
||||
"facets": len(mesh.Facets),
|
||||
"vertices": len(mesh.Points),
|
||||
"bounds": [round(float(value), 9) for value in (bounds.XMin, bounds.YMin, bounds.ZMin, bounds.XMax, bounds.YMax, bounds.ZMax)],
|
||||
"isEmpty": len(mesh.Facets) == 0,
|
||||
}
|
||||
except Exception as error:
|
||||
return {"applicable": True, "error": str(error)}
|
||||
|
||||
|
||||
def snapshot(document, obj, requested):
|
||||
recompute_result = bool(document.recompute())
|
||||
return {
|
||||
"requested": requested,
|
||||
"value": str(obj.FileName),
|
||||
"propertyTypeId": obj.getTypeIdOfProperty("FileName"),
|
||||
"propertyStatus": [str(item) for item in obj.getPropertyStatus("FileName")],
|
||||
"editorMode": [str(item) for item in obj.getEditorMode("FileName")],
|
||||
"objectState": [str(item) for item in obj.State],
|
||||
"statusString": str(obj.getStatusString()),
|
||||
"mustExecute": bool(obj.mustExecute()) if hasattr(obj, "mustExecute") else None,
|
||||
"recomputeResult": recompute_result,
|
||||
"mesh": mesh_snapshot(obj),
|
||||
"objectSet": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects],
|
||||
}
|
||||
|
||||
|
||||
def run():
|
||||
document = App.newDocument("PropertyFileSuccess")
|
||||
try:
|
||||
obj = document.addObject("Mesh::Import", "FileProbe")
|
||||
document.recompute()
|
||||
phases = {"default": snapshot(document, obj, "")}
|
||||
obj.FileName = ASSET
|
||||
phases["valid-mesh"] = snapshot(document, obj, ASSET)
|
||||
obj.FileName = "mesh-data/Cube.stl"
|
||||
phases["relative-path"] = snapshot(document, obj, "mesh-data/Cube.stl")
|
||||
obj.FileName = ASSET
|
||||
phases["restored"] = snapshot(document, obj, ASSET)
|
||||
return {
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-file-success",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"propertyType": "App::PropertyFile",
|
||||
"object": {"name": obj.Name, "typeId": obj.TypeId},
|
||||
"property": {"name": "FileName", "typeId": obj.getTypeIdOfProperty("FileName"), "group": obj.getGroupOfProperty("FileName")},
|
||||
"asset": {"path": ASSET, "exists": os.path.isfile(ASSET), "bytes": os.path.getsize(ASSET) if os.path.isfile(ASSET) else 0},
|
||||
"phases": phases,
|
||||
"diagnostics": {"exception": None, "documentObjectCount": len(document.Objects)},
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
print("FREECAD_PROPERTY_FILE_SUCCESS_RESULT=" + json.dumps(run(), sort_keys=True))
|
||||
208
scripts/freecad-property-fileincluded-failure.py
Normal file
208
scripts/freecad-property-fileincluded-failure.py
Normal file
@@ -0,0 +1,208 @@
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import tempfile
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_FILEINCLUDED_FAILURE_OUTPUT", "")
|
||||
|
||||
|
||||
def object_set(document):
|
||||
return [{"name": obj.Name, "typeId": obj.TypeId} for obj in document.Objects]
|
||||
|
||||
|
||||
def sha256_file(path):
|
||||
if not path or not os.path.isfile(path):
|
||||
return None
|
||||
with open(path, "rb") as handle:
|
||||
return hashlib.sha256(handle.read()).hexdigest()
|
||||
|
||||
|
||||
def snapshot(document, obj):
|
||||
path = str(obj.File)
|
||||
path_exists = os.path.exists(path) if path else False
|
||||
is_file = os.path.isfile(path) if path else False
|
||||
is_directory = os.path.isdir(path) if path else False
|
||||
mode = stat.S_IMODE(os.stat(path).st_mode) if path_exists else None
|
||||
return {
|
||||
"value": path,
|
||||
"baseName": os.path.basename(path) if path else "",
|
||||
"exists": is_file,
|
||||
"pathExists": path_exists,
|
||||
"isFile": is_file,
|
||||
"isDirectory": is_directory,
|
||||
"bytes": os.path.getsize(path) if is_file else 0,
|
||||
"sha256": sha256_file(path),
|
||||
"writeBits": mode & 0o222 if mode is not None else None,
|
||||
"underTransientDir": bool(path) and os.path.commonpath([os.path.abspath(path), os.path.abspath(document.TransientDir)]) == os.path.abspath(document.TransientDir),
|
||||
"propertyTypeId": obj.getTypeIdOfProperty("File"),
|
||||
"propertyStatus": [str(item) for item in obj.getPropertyStatus("File")],
|
||||
"editorMode": [str(item) for item in obj.getEditorMode("File")],
|
||||
"objectState": [str(item) for item in obj.State],
|
||||
"statusString": str(obj.getStatusString()),
|
||||
"objectSet": object_set(document),
|
||||
}
|
||||
|
||||
|
||||
def exception_snapshot(callback):
|
||||
try:
|
||||
callback()
|
||||
except Exception as error:
|
||||
return {"type": type(error).__name__, "message": str(error)}
|
||||
return None
|
||||
|
||||
|
||||
def active_transaction_snapshot():
|
||||
active = App.getActiveTransaction()
|
||||
if not active:
|
||||
return {"name": "", "id": 0}
|
||||
return {"name": str(active[0]), "id": int(active[1])}
|
||||
|
||||
|
||||
def run():
|
||||
source_dir = tempfile.mkdtemp(prefix="freecad-property-fileincluded-failure-")
|
||||
document = App.newDocument("PropertyFileIncludedFailure")
|
||||
try:
|
||||
source = os.path.join(source_dir, "source.bin")
|
||||
replacement = os.path.join(source_dir, "replacement.bin")
|
||||
with open(source, "wb") as handle:
|
||||
handle.write(b"stable-fileincluded-source\x00\xff\n")
|
||||
with open(replacement, "wb") as handle:
|
||||
handle.write(b"replacement-fileincluded-source\x00\x80\n")
|
||||
source_directory = os.path.join(source_dir, "directory")
|
||||
os.mkdir(source_directory)
|
||||
|
||||
obj = document.addObject("App::DocumentObjectFileIncluded", "FileProbe")
|
||||
obj.File = (source, "stable.bin")
|
||||
document.recompute()
|
||||
document.UndoMode = 1
|
||||
initial = snapshot(document, obj)
|
||||
initial_objects = object_set(document)
|
||||
failures = []
|
||||
cases = [
|
||||
("missing-path", "/definitely/missing/property-fileincluded.bin"),
|
||||
("wrong-type", 12345),
|
||||
("tuple-wrong-arity", (source,)),
|
||||
("tuple-wrong-name-type", (source, 123)),
|
||||
("dictionary-wrong-filename-type", {"filename": 123}),
|
||||
]
|
||||
for case_id, requested in cases:
|
||||
before = snapshot(document, obj)
|
||||
before_objects = object_set(document)
|
||||
assignment_exception = exception_snapshot(lambda value=requested: setattr(obj, "File", value))
|
||||
after = snapshot(document, obj)
|
||||
failures.append({
|
||||
"id": case_id,
|
||||
"requested": repr(requested),
|
||||
"assignmentException": assignment_exception,
|
||||
"before": before,
|
||||
"after": after,
|
||||
"valuePreserved": before["value"] == after["value"] and before["sha256"] == after["sha256"],
|
||||
"objectsPreserved": before_objects == after["objectSet"],
|
||||
"objectSetBefore": before_objects,
|
||||
"objectSetAfter": after["objectSet"],
|
||||
})
|
||||
|
||||
directory_before = snapshot(document, obj)
|
||||
directory_exception = exception_snapshot(lambda: setattr(obj, "File", source_directory))
|
||||
directory_after = snapshot(document, obj)
|
||||
obj.File = (source, "restored-after-directory.bin")
|
||||
document.recompute()
|
||||
directory_recovery = snapshot(document, obj)
|
||||
accepted_risks = [{
|
||||
"id": "directory-path",
|
||||
"requested": repr(source_directory),
|
||||
"assignmentException": directory_exception,
|
||||
"before": directory_before,
|
||||
"after": directory_after,
|
||||
"oldFileRemoved": directory_before["isFile"] and not directory_after["isFile"],
|
||||
"directoryAccepted": directory_after["pathExists"] and directory_after["isDirectory"],
|
||||
"recovery": directory_recovery,
|
||||
"objectsPreserved": directory_before["objectSet"] == directory_after["objectSet"] == directory_recovery["objectSet"],
|
||||
}]
|
||||
|
||||
same_before = snapshot(document, obj)
|
||||
same_exception = exception_snapshot(lambda: setattr(obj, "File", str(obj.File)))
|
||||
same_after = snapshot(document, obj)
|
||||
failures.append({
|
||||
"id": "same-current-transient",
|
||||
"requested": repr(same_before["value"]),
|
||||
"assignmentException": same_exception,
|
||||
"before": same_before,
|
||||
"after": same_after,
|
||||
"valuePreserved": same_before["value"] == same_after["value"] and same_before["sha256"] == same_after["sha256"],
|
||||
"objectsPreserved": same_before["objectSet"] == same_after["objectSet"],
|
||||
"objectSetBefore": same_before["objectSet"],
|
||||
"objectSetAfter": same_after["objectSet"],
|
||||
})
|
||||
|
||||
filter_before = snapshot(document, obj)
|
||||
filter_exception = exception_snapshot(lambda: setattr(obj, "File", {"filter": "Binary (*.bin)"}))
|
||||
filter_after = snapshot(document, obj)
|
||||
filter_only = {"before": filter_before, "after": filter_after, "exception": filter_exception, "valuePreserved": filter_before["value"] == filter_after["value"] and filter_before["sha256"] == filter_after["sha256"], "objectsPreserved": filter_before["objectSet"] == filter_after["objectSet"]}
|
||||
|
||||
editor_before = snapshot(document, obj)
|
||||
obj.setEditorMode("File", ["ReadOnly"])
|
||||
editor_mode = [str(item) for item in obj.getEditorMode("File")]
|
||||
editor_exception = exception_snapshot(lambda: setattr(obj, "File", (replacement, "editor-write.bin")))
|
||||
editor_after = snapshot(document, obj)
|
||||
editor_read_only = {"before": editor_before, "editorMode": editor_mode, "exception": editor_exception, "after": editor_after, "pythonBypassesEditorReadOnly": editor_exception is None}
|
||||
obj.setEditorMode("File", 0)
|
||||
|
||||
obj.setPropertyStatus("File", "Immutable")
|
||||
immutable_status = [str(item) for item in obj.getPropertyStatus("File")]
|
||||
immutable_before = snapshot(document, obj)
|
||||
immutable_exception = exception_snapshot(lambda: setattr(obj, "File", (replacement, "immutable-write.bin")))
|
||||
immutable_after = snapshot(document, obj)
|
||||
obj.setPropertyStatus("File", "-Immutable")
|
||||
status_restored = [str(item) for item in obj.getPropertyStatus("File")]
|
||||
|
||||
document.recompute()
|
||||
document.openTransaction("property-fileincluded-cancel")
|
||||
transaction_before = snapshot(document, obj)
|
||||
transaction_objects_before = object_set(document)
|
||||
setattr(obj, "File", (source, "transaction-write.bin"))
|
||||
transaction_edited = snapshot(document, obj)
|
||||
edited_path = transaction_edited["value"]
|
||||
pending_after_edit = bool(document.HasPendingTransaction)
|
||||
active_after_edit = active_transaction_snapshot()
|
||||
document.abortTransaction()
|
||||
transaction_after_abort = snapshot(document, obj)
|
||||
pending_after_abort = bool(document.HasPendingTransaction)
|
||||
active_after_abort = active_transaction_snapshot()
|
||||
return {
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-fileincluded-failure",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"propertyType": "App::PropertyFileIncluded",
|
||||
"object": {"name": obj.Name, "typeId": obj.TypeId},
|
||||
"property": {"name": "File", "typeId": obj.getTypeIdOfProperty("File")},
|
||||
"initial": initial,
|
||||
"failures": failures,
|
||||
"acceptedRisks": accepted_risks,
|
||||
"filterOnly": filter_only,
|
||||
"editorReadOnly": editor_read_only,
|
||||
"immutable": {"status": immutable_status, "before": immutable_before, "exception": immutable_exception, "after": immutable_after, "restoredStatus": status_restored},
|
||||
"transaction": {"before": transaction_before, "edited": transaction_edited, "afterAbort": transaction_after_abort, "editedPath": edited_path, "editedPathExistsAfterAbort": os.path.exists(edited_path), "pendingAfterEdit": pending_after_edit, "pendingAfterAbort": pending_after_abort, "activeAfterEdit": active_after_edit, "activeAfterAbort": active_after_abort, "objectsBefore": transaction_objects_before, "objectsAfter": object_set(document), "undoMode": int(document.UndoMode), "restored": transaction_before["value"] == transaction_after_abort["value"] and transaction_before["sha256"] == transaction_after_abort["sha256"]},
|
||||
"cancellationBoundary": {"supported": False, "classification": "synchronous-property-setter", "reason": "no-native-cancel-hook", "replacement": "abort-active-document-transaction"},
|
||||
"documentIntegrity": {"initialObjects": initial_objects, "finalObjects": object_set(document), "objectsPreserved": initial_objects == object_set(document), "objectCount": len(document.Objects)},
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
shutil.rmtree(source_dir, ignore_errors=True)
|
||||
|
||||
|
||||
report = run()
|
||||
if not OUTPUT_PATH:
|
||||
raise RuntimeError("FREECAD_PROPERTY_FILEINCLUDED_FAILURE_OUTPUT is required")
|
||||
with open(OUTPUT_PATH, "w", encoding="utf-8") as handle:
|
||||
json.dump(report, handle, indent=2, sort_keys=True)
|
||||
handle.write("\n")
|
||||
print("FREECAD_PROPERTY_FILEINCLUDED_FAILURE_RESULT=" + json.dumps({"status": report["status"], "failureCount": len(report["failures"])}, sort_keys=True))
|
||||
165
scripts/freecad-property-fileincluded-mutation.py
Normal file
165
scripts/freecad-property-fileincluded-mutation.py
Normal file
@@ -0,0 +1,165 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import tempfile
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_FILEINCLUDED_MUTATION_OUTPUT", "")
|
||||
HOSTS = [
|
||||
("App::DocumentObjectFileIncluded", "File"),
|
||||
("App::VRMLObject", "VrmlFile"),
|
||||
("Image::ImagePlane", "ImageFile"),
|
||||
("Robot::RobotObject", "RobotKinematicFile"),
|
||||
("Robot::RobotObject", "RobotVrmlFile"),
|
||||
("Sketcher::SketchObjectSF", "SketchFlatFile"),
|
||||
("TechDraw::DrawComplexSection", "PatIncluded"),
|
||||
("TechDraw::DrawComplexSection", "SvgIncluded"),
|
||||
("TechDraw::DrawComplexSectionPython", "PatIncluded"),
|
||||
("TechDraw::DrawComplexSectionPython", "SvgIncluded"),
|
||||
("TechDraw::DrawGeomHatch", "PatIncluded"),
|
||||
("TechDraw::DrawHatch", "SvgIncluded"),
|
||||
("TechDraw::DrawSVGTemplate", "PageResult"),
|
||||
("TechDraw::DrawTileWeld", "SymbolIncluded"),
|
||||
("TechDraw::DrawTileWeldPython", "SymbolIncluded"),
|
||||
("TechDraw::DrawViewImage", "ImageIncluded"),
|
||||
("TechDraw::DrawViewSection", "PatIncluded"),
|
||||
("TechDraw::DrawViewSection", "SvgIncluded"),
|
||||
("TechDraw::DrawViewSectionPython", "PatIncluded"),
|
||||
("TechDraw::DrawViewSectionPython", "SvgIncluded"),
|
||||
]
|
||||
|
||||
|
||||
def sha256_file(path):
|
||||
with open(path, "rb") as handle:
|
||||
return hashlib.sha256(handle.read()).hexdigest()
|
||||
|
||||
|
||||
def write_asset(directory, name, data):
|
||||
path = os.path.join(directory, name)
|
||||
with open(path, "wb") as handle:
|
||||
handle.write(data)
|
||||
return path
|
||||
|
||||
|
||||
def create_assets(directory):
|
||||
png_a = base64.b64decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=")
|
||||
png_b = base64.b64decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=") + b"mutation"
|
||||
csv_a = b"a,alpha,d,theta,rotDir,maxAngle,minAngle,velocity\n500,-90,1045,0,-1,185,-185,156\n1300,0,0,0,1,35,-155,156\n55,90,0,-90,1,154,-130,156\n0,-90,-1025,0,1,350,-350,330\n0,90,0,0,1,130,-130,330\n0,180,-300,0,1,350,-350,615\n"
|
||||
csv_b = b"a,alpha,d,theta,rotDir,maxAngle,minAngle,velocity\n501,-90,1045,0,-1,185,-185,156\n1300,0,0,0,1,35,-155,156\n55,90,0,-90,1,154,-130,156\n0,-90,-1025,0,1,350,-350,330\n0,90,0,0,1,130,-130,330\n0,180,-300,0,1,350,-350,615\n"
|
||||
return {
|
||||
"binA": write_asset(directory, "before.bin", b"before-fileincluded\x00A\xff\n"),
|
||||
"binB": write_asset(directory, "edited.bin", b"edited-fileincluded\x00B\x80\n"),
|
||||
"svgA": write_asset(directory, "before.svg", b'<svg xmlns="http://www.w3.org/2000/svg" width="4" height="3"><path d="M0 0L4 3"/></svg>\n'),
|
||||
"svgB": write_asset(directory, "edited.svg", b'<svg xmlns="http://www.w3.org/2000/svg" width="5" height="4"><path d="M0 4L5 0"/></svg>\n'),
|
||||
"patA": write_asset(directory, "before.pat", b"*BEFORE,PropertyFileIncluded\n0, 0,0, 0,2\n"),
|
||||
"patB": write_asset(directory, "edited.pat", b"*EDITED,PropertyFileIncluded\n0, 0,0, 0,3\n"),
|
||||
"pngA": write_asset(directory, "before.png", png_a),
|
||||
"pngB": write_asset(directory, "edited.png", png_b),
|
||||
"wrlA": write_asset(directory, "before.wrl", b"#VRML V2.0 utf8\nShape { geometry Box { size 1 1 1 } }\n"),
|
||||
"wrlB": write_asset(directory, "edited.wrl", b"#VRML V2.0 utf8\nShape { geometry Box { size 2 1 1 } }\n"),
|
||||
"csvA": write_asset(directory, "before.csv", csv_a),
|
||||
"csvB": write_asset(directory, "edited.csv", csv_b),
|
||||
"skfA": write_asset(directory, "before.skf", b"# SketchFlat before fixture\n"),
|
||||
"skfB": write_asset(directory, "edited.skf", b"# SketchFlat edited fixture\n"),
|
||||
}
|
||||
|
||||
|
||||
def asset_for(property_name, assets, phase):
|
||||
suffix = "A" if phase == "before" or phase == "restored" else "B"
|
||||
if property_name == "PatIncluded": return assets[f"pat{suffix}"], "pat"
|
||||
if property_name in ("SvgIncluded", "PageResult", "SymbolIncluded"): return assets[f"svg{suffix}"], "svg"
|
||||
if property_name in ("ImageFile", "ImageIncluded"): return assets[f"png{suffix}"], "png"
|
||||
if property_name in ("VrmlFile", "RobotVrmlFile"): return assets[f"wrl{suffix}"], "wrl"
|
||||
if property_name == "RobotKinematicFile": return assets[f"csv{suffix}"], "csv"
|
||||
if property_name == "SketchFlatFile": return assets[f"skf{suffix}"], "skf"
|
||||
return assets[f"bin{suffix}"], "bin"
|
||||
|
||||
|
||||
def shape_snapshot(obj):
|
||||
if "Shape" not in obj.PropertiesList:
|
||||
return {"applicable": False, "reason": "host-has-no-shape-property"}
|
||||
shape = obj.Shape
|
||||
return {"applicable": True, "isNull": shape is None or shape.isNull(), "solids": len(shape.Solids), "faces": len(shape.Faces), "edges": len(shape.Edges), "vertices": len(shape.Vertexes)}
|
||||
|
||||
|
||||
def snapshot(document, obj, property_name):
|
||||
path = str(getattr(obj, property_name))
|
||||
is_file = os.path.isfile(path) if path else False
|
||||
mode = stat.S_IMODE(os.stat(path).st_mode) if is_file else None
|
||||
return {
|
||||
"value": path,
|
||||
"baseName": os.path.basename(path) if path else "",
|
||||
"exists": is_file,
|
||||
"bytes": os.path.getsize(path) if is_file else 0,
|
||||
"sha256": sha256_file(path) if is_file else None,
|
||||
"writeBits": mode & 0o222 if mode is not None else None,
|
||||
"underTransientDir": bool(path) and os.path.commonpath([os.path.abspath(path), os.path.abspath(document.TransientDir)]) == os.path.abspath(document.TransientDir),
|
||||
"propertyTypeId": obj.getTypeIdOfProperty(property_name),
|
||||
"propertyStatus": [str(item) for item in obj.getPropertyStatus(property_name)],
|
||||
"editorMode": [str(item) for item in obj.getEditorMode(property_name)],
|
||||
"objectState": [str(item) for item in obj.State],
|
||||
"statusString": str(obj.getStatusString()),
|
||||
"shape": shape_snapshot(obj),
|
||||
"objectSet": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects],
|
||||
}
|
||||
|
||||
|
||||
def run_case(index, object_type_id, property_name, assets):
|
||||
document = App.newDocument("PropertyFileIncludedMutation%02d" % index)
|
||||
try:
|
||||
obj = document.addObject(object_type_id, "MutationProbe")
|
||||
archive_name = "mutation-%02d.%s" % (index, "pat" if property_name == "PatIncluded" else "svg" if property_name in ("SvgIncluded", "PageResult", "SymbolIncluded") else "png" if property_name in ("ImageFile", "ImageIncluded") else "wrl" if property_name in ("VrmlFile", "RobotVrmlFile") else "csv" if property_name == "RobotKinematicFile" else "skf" if property_name == "SketchFlatFile" else "bin")
|
||||
before_source, _ = asset_for(property_name, assets, "before")
|
||||
edited_source, _ = asset_for(property_name, assets, "edited")
|
||||
setattr(obj, property_name, (before_source, archive_name))
|
||||
document.recompute()
|
||||
before = snapshot(document, obj, property_name)
|
||||
setattr(obj, property_name, (edited_source, archive_name))
|
||||
edited = snapshot(document, obj, property_name)
|
||||
edited_recompute = bool(document.recompute())
|
||||
edited_after_recompute = snapshot(document, obj, property_name)
|
||||
setattr(obj, property_name, (before_source, archive_name))
|
||||
restored = snapshot(document, obj, property_name)
|
||||
restored_recompute = bool(document.recompute())
|
||||
restored_after_recompute = snapshot(document, obj, property_name)
|
||||
return {
|
||||
"objectTypeId": object_type_id,
|
||||
"propertyName": property_name,
|
||||
"archiveName": archive_name,
|
||||
"beforeSource": {"path": before_source, "bytes": os.path.getsize(before_source), "sha256": sha256_file(before_source)},
|
||||
"editedSource": {"path": edited_source, "bytes": os.path.getsize(edited_source), "sha256": sha256_file(edited_source)},
|
||||
"before": before,
|
||||
"edited": edited,
|
||||
"editedRecompute": {"result": edited_recompute, "snapshot": edited_after_recompute},
|
||||
"restored": restored,
|
||||
"restoredRecompute": {"result": restored_recompute, "snapshot": restored_after_recompute},
|
||||
"pathStable": before["value"] == edited["value"] == restored["value"],
|
||||
"objectSetStable": before["objectSet"] == edited["objectSet"] == restored["objectSet"] == restored_after_recompute["objectSet"],
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
def run():
|
||||
directory = tempfile.mkdtemp(prefix="freecad-property-fileincluded-mutation-")
|
||||
try:
|
||||
assets = create_assets(directory)
|
||||
cases = [run_case(index, *host, assets) for index, host in enumerate(HOSTS)]
|
||||
return {"schemaVersion": 1, "status": "pass", "baselineId": "freecad-1.1.1-property-fileincluded-mutation", "freecadVersion": ".".join(str(value) for value in App.Version()[:3]), "gitCommit": FREECAD_COMMIT, "propertyType": "App::PropertyFileIncluded", "caseCount": len(cases), "cases": cases}
|
||||
finally:
|
||||
shutil.rmtree(directory, ignore_errors=True)
|
||||
|
||||
|
||||
report = run()
|
||||
if not OUTPUT_PATH:
|
||||
raise RuntimeError("FREECAD_PROPERTY_FILEINCLUDED_MUTATION_OUTPUT is required")
|
||||
with open(OUTPUT_PATH, "w", encoding="utf-8") as handle:
|
||||
json.dump(report, handle, indent=2, sort_keys=True)
|
||||
handle.write("\n")
|
||||
print("FREECAD_PROPERTY_FILEINCLUDED_MUTATION_RESULT=" + json.dumps({"status": report["status"], "caseCount": report["caseCount"]}, sort_keys=True))
|
||||
94
scripts/freecad-property-fileincluded-roundtrip.py
Normal file
94
scripts/freecad-property-fileincluded-roundtrip.py
Normal file
@@ -0,0 +1,94 @@
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
|
||||
|
||||
def file_snapshot(document):
|
||||
obj = document.getObject("IncludedProbe")
|
||||
path = str(obj.File)
|
||||
exists = os.path.isfile(path)
|
||||
payload = open(path, "rb").read() if exists else b""
|
||||
mode = stat.S_IMODE(os.stat(path).st_mode) if exists else None
|
||||
return {
|
||||
"objectSet": [{"name": item.Name, "typeId": item.TypeId} for item in document.Objects],
|
||||
"object": {
|
||||
"name": obj.Name,
|
||||
"typeId": obj.TypeId,
|
||||
"propertyTypeId": obj.getTypeIdOfProperty("File"),
|
||||
"value": path,
|
||||
"baseName": os.path.basename(path) if path else "",
|
||||
"exists": exists,
|
||||
"bytes": len(payload),
|
||||
"sha256": hashlib.sha256(payload).hexdigest() if payload else "",
|
||||
"mode": mode,
|
||||
"writeBits": mode & 0o222 if mode is not None else None,
|
||||
"underTransientDir": bool(path) and os.path.commonpath([os.path.abspath(path), os.path.abspath(document.TransientDir)]) == os.path.abspath(document.TransientDir),
|
||||
"propertyStatus": [str(item) for item in obj.getPropertyStatus("File")],
|
||||
"editorMode": [str(item) for item in obj.getEditorMode("File")],
|
||||
"state": [str(item) for item in obj.State],
|
||||
"statusString": str(obj.getStatusString()),
|
||||
"hasShapeProperty": "Shape" in obj.PropertiesList,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def create_native(path, source_path):
|
||||
document = App.newDocument("PropertyFileIncludedRoundtrip")
|
||||
try:
|
||||
obj = document.addObject("App::DocumentObjectFileIncluded", "IncludedProbe")
|
||||
obj.File = (source_path, "payload.bin")
|
||||
document.recompute()
|
||||
document.saveAs(path)
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
reopened = App.openDocument(path)
|
||||
try:
|
||||
reopened.recompute()
|
||||
return file_snapshot(reopened)
|
||||
finally:
|
||||
App.closeDocument(reopened.Name)
|
||||
|
||||
|
||||
def verify_native(path, resaved_path):
|
||||
document = App.openDocument(path)
|
||||
try:
|
||||
document.recompute()
|
||||
reopened = file_snapshot(document)
|
||||
document.saveAs(resaved_path)
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
resaved_document = App.openDocument(resaved_path)
|
||||
try:
|
||||
resaved_document.recompute()
|
||||
resaved = file_snapshot(resaved_document)
|
||||
finally:
|
||||
App.closeDocument(resaved_document.Name)
|
||||
return {"reopened": reopened, "resaved": resaved}
|
||||
|
||||
|
||||
mode = os.environ.get("FREECAD_PROPERTY_FILEINCLUDED_ROUNDTRIP_MODE", "")
|
||||
path = os.environ.get("FREECAD_PROPERTY_FILEINCLUDED_ROUNDTRIP_PATH", "")
|
||||
resaved_path = os.environ.get("FREECAD_PROPERTY_FILEINCLUDED_ROUNDTRIP_RESAVED_PATH", "")
|
||||
source_path = os.environ.get("FREECAD_PROPERTY_FILEINCLUDED_ROUNDTRIP_SOURCE", "")
|
||||
if mode == "create":
|
||||
result = create_native(path, source_path)
|
||||
elif mode == "verify":
|
||||
result = verify_native(path, resaved_path)
|
||||
else:
|
||||
raise RuntimeError("FREECAD_PROPERTY_FILEINCLUDED_ROUNDTRIP_MODE must be create or verify")
|
||||
|
||||
print("FREECAD_PROPERTY_FILEINCLUDED_ROUNDTRIP_RESULT=" + json.dumps({
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-fileincluded-roundtrip",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"mode": mode,
|
||||
"result": result,
|
||||
}, sort_keys=True))
|
||||
237
scripts/freecad-property-fileincluded-success.py
Normal file
237
scripts/freecad-property-fileincluded-success.py
Normal file
@@ -0,0 +1,237 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import tempfile
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_FILEINCLUDED_SUCCESS_OUTPUT", "")
|
||||
HOSTS = [
|
||||
("App::DocumentObjectFileIncluded", "File"),
|
||||
("App::VRMLObject", "VrmlFile"),
|
||||
("Image::ImagePlane", "ImageFile"),
|
||||
("Robot::RobotObject", "RobotKinematicFile"),
|
||||
("Robot::RobotObject", "RobotVrmlFile"),
|
||||
("Sketcher::SketchObjectSF", "SketchFlatFile"),
|
||||
("TechDraw::DrawComplexSection", "PatIncluded"),
|
||||
("TechDraw::DrawComplexSection", "SvgIncluded"),
|
||||
("TechDraw::DrawComplexSectionPython", "PatIncluded"),
|
||||
("TechDraw::DrawComplexSectionPython", "SvgIncluded"),
|
||||
("TechDraw::DrawGeomHatch", "PatIncluded"),
|
||||
("TechDraw::DrawHatch", "SvgIncluded"),
|
||||
("TechDraw::DrawSVGTemplate", "PageResult"),
|
||||
("TechDraw::DrawTileWeld", "SymbolIncluded"),
|
||||
("TechDraw::DrawTileWeldPython", "SymbolIncluded"),
|
||||
("TechDraw::DrawViewImage", "ImageIncluded"),
|
||||
("TechDraw::DrawViewSection", "PatIncluded"),
|
||||
("TechDraw::DrawViewSection", "SvgIncluded"),
|
||||
("TechDraw::DrawViewSectionPython", "PatIncluded"),
|
||||
("TechDraw::DrawViewSectionPython", "SvgIncluded"),
|
||||
]
|
||||
|
||||
|
||||
def sha256_file(path):
|
||||
with open(path, "rb") as handle:
|
||||
return hashlib.sha256(handle.read()).hexdigest()
|
||||
|
||||
|
||||
def write_asset(directory, name, data):
|
||||
path = os.path.join(directory, name)
|
||||
with open(path, "wb") as handle:
|
||||
handle.write(data)
|
||||
return path
|
||||
|
||||
|
||||
def create_assets(directory):
|
||||
png = base64.b64decode(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
|
||||
)
|
||||
kinematic = (
|
||||
"a,alpha,d,theta,rotDir,maxAngle,minAngle,velocity\n"
|
||||
"500,-90,1045,0,-1,185,-185,156\n"
|
||||
"1300,0,0,0,1,35,-155,156\n"
|
||||
"55,90,0,-90,1,154,-130,156\n"
|
||||
"0,-90,-1025,0,1,350,-350,330\n"
|
||||
"0,90,0,0,1,130,-130,330\n"
|
||||
"0,180,-300,0,1,350,-350,615\n"
|
||||
).encode("ascii")
|
||||
return {
|
||||
"bin": write_asset(directory, "payload.bin", b"FreeCAD-PropertyFileIncluded\x00success\xff\n"),
|
||||
"bin2": write_asset(directory, "payload-two.bin", b"FreeCAD-PropertyFileIncluded\x00second\x80\n"),
|
||||
"svg": write_asset(directory, "pattern.svg", b'<svg xmlns="http://www.w3.org/2000/svg" width="4" height="3"><path d="M0 0L4 3"/></svg>\n'),
|
||||
"pat": write_asset(directory, "pattern.pat", b"*TEST,PropertyFileIncluded\n0, 0,0, 0,2\n"),
|
||||
"png": write_asset(directory, "pixel.png", png),
|
||||
"wrl": write_asset(directory, "model.wrl", b"#VRML V2.0 utf8\nShape { geometry Box { size 1 1 1 } }\n"),
|
||||
"csv": write_asset(directory, "kinematic.csv", kinematic),
|
||||
"skf": write_asset(directory, "sketch.skf", b"# SketchFlat PropertyFileIncluded fixture\n"),
|
||||
}
|
||||
|
||||
|
||||
def asset_for(object_type_id, property_name, assets):
|
||||
if property_name in ("PatIncluded",):
|
||||
return assets["pat"], "pat"
|
||||
if property_name in ("SvgIncluded", "PageResult", "SymbolIncluded"):
|
||||
return assets["svg"], "svg"
|
||||
if property_name in ("ImageFile", "ImageIncluded"):
|
||||
return assets["png"], "png"
|
||||
if property_name in ("VrmlFile", "RobotVrmlFile"):
|
||||
return assets["wrl"], "wrl"
|
||||
if property_name == "RobotKinematicFile":
|
||||
return assets["csv"], "csv"
|
||||
if property_name == "SketchFlatFile":
|
||||
return assets["skf"], "skf"
|
||||
return assets["bin"], "bin"
|
||||
|
||||
|
||||
def shape_snapshot(obj):
|
||||
if "Shape" not in obj.PropertiesList:
|
||||
return {"applicable": False, "reason": "host-has-no-shape-property"}
|
||||
shape = obj.Shape
|
||||
if shape is None or shape.isNull():
|
||||
return {"applicable": True, "isNull": True, "solids": 0, "faces": 0, "edges": 0, "vertices": 0}
|
||||
return {
|
||||
"applicable": True,
|
||||
"isNull": False,
|
||||
"isValid": bool(shape.isValid()),
|
||||
"solids": len(shape.Solids),
|
||||
"faces": len(shape.Faces),
|
||||
"edges": len(shape.Edges),
|
||||
"vertices": len(shape.Vertexes),
|
||||
}
|
||||
|
||||
|
||||
def file_snapshot(document, obj, property_name):
|
||||
path = str(getattr(obj, property_name))
|
||||
exists = os.path.isfile(path) if path else False
|
||||
mode = stat.S_IMODE(os.stat(path).st_mode) if exists else None
|
||||
return {
|
||||
"value": path,
|
||||
"baseName": os.path.basename(path) if path else "",
|
||||
"exists": exists,
|
||||
"bytes": os.path.getsize(path) if exists else 0,
|
||||
"sha256": sha256_file(path) if exists else None,
|
||||
"mode": mode,
|
||||
"writeBits": mode & 0o222 if mode is not None else None,
|
||||
"underTransientDir": bool(path) and os.path.commonpath([os.path.abspath(path), os.path.abspath(document.TransientDir)]) == os.path.abspath(document.TransientDir),
|
||||
"propertyTypeId": obj.getTypeIdOfProperty(property_name),
|
||||
"propertyStatus": [str(item) for item in obj.getPropertyStatus(property_name)],
|
||||
"editorMode": [str(item) for item in obj.getEditorMode(property_name)],
|
||||
"objectState": [str(item) for item in obj.State],
|
||||
"statusString": str(obj.getStatusString()),
|
||||
"shape": shape_snapshot(obj),
|
||||
"objectSet": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects],
|
||||
}
|
||||
|
||||
|
||||
def run_setter_variants(assets):
|
||||
document = App.newDocument("PropertyFileIncludedSetterSuccess")
|
||||
try:
|
||||
phases = []
|
||||
|
||||
def assign(name, value, expected_asset, expected_base_name=None):
|
||||
obj = document.addObject("App::DocumentObjectFileIncluded", name)
|
||||
setattr(obj, "File", value)
|
||||
snapshot = file_snapshot(document, obj, "File")
|
||||
snapshot.update({
|
||||
"id": name,
|
||||
"sourcePath": expected_asset,
|
||||
"sourceSha256": sha256_file(expected_asset),
|
||||
"expectedBaseName": expected_base_name or os.path.basename(expected_asset),
|
||||
})
|
||||
phases.append(snapshot)
|
||||
return obj
|
||||
|
||||
assign("StringPath", assets["bin"], assets["bin"])
|
||||
assign("BytesPath", os.fsencode(assets["bin2"]), assets["bin2"])
|
||||
assign("TupleRename", (assets["bin"], "renamed.dat"), assets["bin"], "renamed.dat")
|
||||
with open(assets["bin2"], "rb") as source:
|
||||
assign("OpenIoFile", source, assets["bin2"])
|
||||
assign("Dictionary", {"filename": assets["bin"], "filter": "Binary files (*.bin)"}, assets["bin"])
|
||||
|
||||
empty_no_op = document.addObject("App::DocumentObjectFileIncluded", "EmptyNoOp")
|
||||
empty_no_op.File = assets["bin"]
|
||||
before_empty = file_snapshot(document, empty_no_op, "File")
|
||||
empty_no_op.File = ""
|
||||
after_empty = file_snapshot(document, empty_no_op, "File")
|
||||
|
||||
collision_a = assign("CollisionA", (assets["bin"], "collision.bin"), assets["bin"], "collision.bin")
|
||||
collision_b = assign("CollisionB", (assets["bin2"], "collision.bin"), assets["bin2"], "collision1.bin")
|
||||
recompute_result = bool(document.recompute())
|
||||
return {
|
||||
"phases": phases,
|
||||
"emptyString": {"before": before_empty, "after": after_empty, "preserved": before_empty["value"] == after_empty["value"] and before_empty["sha256"] == after_empty["sha256"]},
|
||||
"collision": {
|
||||
"first": file_snapshot(document, collision_a, "File"),
|
||||
"second": file_snapshot(document, collision_b, "File"),
|
||||
"distinctPaths": str(collision_a.File) != str(collision_b.File),
|
||||
"distinctBytes": sha256_file(str(collision_a.File)) != sha256_file(str(collision_b.File)),
|
||||
},
|
||||
"recomputeResult": recompute_result,
|
||||
"objectCount": len(document.Objects),
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
def run_host_case(index, object_type_id, property_name, assets):
|
||||
document = App.newDocument("PropertyFileIncludedHost%02d" % index)
|
||||
try:
|
||||
obj = document.addObject(object_type_id, "HostProbe")
|
||||
before_object_set = [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects]
|
||||
default = file_snapshot(document, obj, property_name)
|
||||
source_path, asset_kind = asset_for(object_type_id, property_name, assets)
|
||||
archive_name = "host-%02d.%s" % (index, asset_kind)
|
||||
setattr(obj, property_name, (source_path, archive_name))
|
||||
after_set = file_snapshot(document, obj, property_name)
|
||||
recompute_result = bool(document.recompute())
|
||||
after_recompute = file_snapshot(document, obj, property_name)
|
||||
return {
|
||||
"objectTypeId": object_type_id,
|
||||
"propertyName": property_name,
|
||||
"group": obj.getGroupOfProperty(property_name),
|
||||
"source": {"path": source_path, "kind": asset_kind, "bytes": os.path.getsize(source_path), "sha256": sha256_file(source_path)},
|
||||
"archiveName": archive_name,
|
||||
"default": default,
|
||||
"afterSet": after_set,
|
||||
"afterRecompute": after_recompute,
|
||||
"recomputeResult": recompute_result,
|
||||
"objectSetStable": before_object_set == after_set["objectSet"] == after_recompute["objectSet"],
|
||||
}
|
||||
finally:
|
||||
App.closeDocument(document.Name)
|
||||
|
||||
|
||||
def run():
|
||||
asset_directory = tempfile.mkdtemp(prefix="freecad-property-fileincluded-success-")
|
||||
try:
|
||||
assets = create_assets(asset_directory)
|
||||
setter_variants = run_setter_variants(assets)
|
||||
host_cases = [run_host_case(index, *host, assets) for index, host in enumerate(HOSTS)]
|
||||
return {
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"baselineId": "freecad-1.1.1-property-fileincluded-success",
|
||||
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"propertyType": "App::PropertyFileIncluded",
|
||||
"setterVariants": setter_variants,
|
||||
"hostCaseCount": len(host_cases),
|
||||
"hostCases": host_cases,
|
||||
"diagnostics": {"exception": None},
|
||||
}
|
||||
finally:
|
||||
shutil.rmtree(asset_directory, ignore_errors=True)
|
||||
|
||||
|
||||
report = run()
|
||||
if not OUTPUT_PATH:
|
||||
raise RuntimeError("FREECAD_PROPERTY_FILEINCLUDED_SUCCESS_OUTPUT is required")
|
||||
with open(OUTPUT_PATH, "w", encoding="utf-8") as handle:
|
||||
json.dump(report, handle, indent=2, sort_keys=True)
|
||||
handle.write("\n")
|
||||
print("FREECAD_PROPERTY_FILEINCLUDED_SUCCESS_RESULT=" + json.dumps({"status": report["status"], "hostCaseCount": report["hostCaseCount"], "setterPhaseCount": len(report["setterVariants"]["phases"])}, sort_keys=True))
|
||||
@@ -17,8 +17,11 @@ TRACEBACK_SECONDS = int(os.environ.get("FREECAD_REFERENCE_TRACEBACK_SECONDS", "0
|
||||
if TRACEBACK_SECONDS > 0:
|
||||
faulthandler.dump_traceback_later(TRACEBACK_SECONDS, repeat=True, file=2)
|
||||
ISOLATED_GUI_CONFIG = os.environ.get("FREECAD_ORACLE_ISOLATED_CONFIG") == "1"
|
||||
if PROBE_SCOPE == "gui-commands" and ISOLATED_GUI_CONFIG:
|
||||
App.ParamGet("User parameter:BaseApp/Preferences/Mod/BIM").SetBool("FirstTime", False)
|
||||
BIM_FIRST_TIME_WELCOME_SUPPRESSED = False
|
||||
if ISOLATED_GUI_CONFIG:
|
||||
bim_preferences = App.ParamGet("User parameter:BaseApp/Preferences/Mod/BIM")
|
||||
bim_preferences.SetBool("FirstTime", False)
|
||||
BIM_FIRST_TIME_WELCOME_SUPPRESSED = not bim_preferences.GetBool("FirstTime", True)
|
||||
|
||||
|
||||
def probe_progress(message):
|
||||
@@ -359,7 +362,7 @@ result = {
|
||||
"probeScope": PROBE_SCOPE,
|
||||
"oracleSetup": {
|
||||
"isolatedConfig": ISOLATED_GUI_CONFIG,
|
||||
"bimFirstTimeWelcome": "suppressed-before-workbench-activation" if ISOLATED_GUI_CONFIG else "native-user-config",
|
||||
"bimFirstTimeWelcome": "suppressed-before-workbench-activation" if BIM_FIRST_TIME_WELCOME_SUPPRESSED else "native-user-config",
|
||||
},
|
||||
"determinism": {
|
||||
"schemaVersion": 1,
|
||||
|
||||
@@ -146,10 +146,17 @@ const makeCapabilityTasks = (prefix, capabilities, firstDependency) => {
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
if (followUpProgress.schemaVersion !== 1 || followUpProgress.baseline?.freecadVersion !== '1.1.1' || followUpProgress.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || !Array.isArray(followUpProgress.completedTasks)) throw new Error('Follow-up task progress ledger is invalid.')
|
||||
const recordedFollowUpIds = new Set()
|
||||
for (const entry of followUpProgress.completedTasks) {
|
||||
if (typeof entry.id !== 'string' || recordedFollowUpIds.has(entry.id) || !Array.isArray(entry.evidence) || entry.evidence.length === 0) throw new Error(`Follow-up progress entry ${entry.id} is invalid.`)
|
||||
recordedFollowUpIds.add(entry.id)
|
||||
}
|
||||
const propertyCapabilities = propertySemantics.types
|
||||
.filter(({ support }) => support === 'opaque-fcstd-proxy')
|
||||
.filter(({ typeId, support }) => support === 'opaque-fcstd-proxy' || [...recordedFollowUpIds].some((id) => id.startsWith(`PROP-${slug(typeId)}-`)))
|
||||
.map(({ typeId, recordCount }) => ({ id: typeId, title: typeId, recordCount }))
|
||||
if (propertyCapabilities.length !== propertySemantics.supportSummary?.['opaque-fcstd-proxy']?.typeCount) throw new Error('Opaque Property capability inventory is inconsistent.')
|
||||
const carriedPromotedPropertyTypes = propertyCapabilities.filter(({ id }) => propertySemantics.types.find(({ typeId }) => typeId === id)?.support !== 'opaque-fcstd-proxy')
|
||||
if (propertyCapabilities.length !== propertySemantics.supportSummary?.['opaque-fcstd-proxy']?.typeCount + carriedPromotedPropertyTypes.length) throw new Error('Opaque Property capability inventory is inconsistent.')
|
||||
|
||||
const followUpMilestoneDefinitions = [
|
||||
{
|
||||
@@ -220,7 +227,6 @@ const followUpMilestones = followUpMilestoneDefinitions.map((definition) => {
|
||||
return { id: definition.id, title: definition.title, exactTasks: definition.exactTasks, status: 'pending', tasks }
|
||||
})
|
||||
const followUpTasks = followUpMilestones.flatMap(({ tasks }) => tasks)
|
||||
if (followUpProgress.schemaVersion !== 1 || followUpProgress.baseline?.freecadVersion !== '1.1.1' || followUpProgress.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || !Array.isArray(followUpProgress.completedTasks)) throw new Error('Follow-up task progress ledger is invalid.')
|
||||
const followUpTaskIds = new Set(followUpTasks.map(({ id }) => id))
|
||||
const completedFollowUpIds = new Set()
|
||||
for (const entry of followUpProgress.completedTasks) {
|
||||
|
||||
57
scripts/generate-freecad-property-acceleration-inventory.mjs
Normal file
57
scripts/generate-freecad-property-acceleration-inventory.mjs
Normal file
@@ -0,0 +1,57 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFile, stat, writeFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const sourcePath = resolve(root, '.cache/freecad/reference-desktop.json')
|
||||
const outputPath = resolve(root, 'config/freecad-property-acceleration-inventory.json')
|
||||
const sourceContent = await readFile(sourcePath)
|
||||
const source = JSON.parse(sourceContent)
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyAcceleration inventory generation failed: ${message}`) }
|
||||
|
||||
if (source.schemaVersion !== 1 || source.freecadVersion !== '1.1.1' || source.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('desktop oracle is not the locked FreeCAD baseline')
|
||||
const records = []
|
||||
for (const objectType of source.runtimeObjects?.types ?? []) {
|
||||
for (const property of objectType.properties ?? []) {
|
||||
if (property.typeId !== 'App::PropertyAcceleration') continue
|
||||
records.push({
|
||||
objectTypeId: objectType.typeId,
|
||||
objectAvailable: objectType.available === true,
|
||||
probeStatus: objectType.probeStatus,
|
||||
propertyName: property.name,
|
||||
group: property.group,
|
||||
status: property.status,
|
||||
defaultRaw: property.default,
|
||||
valueModel: {
|
||||
kind: 'quantity',
|
||||
dimension: 'length/time^2',
|
||||
defaultValue: 1000,
|
||||
defaultUnit: 'mm/s^2',
|
||||
acceptedBoundaryEvidence: 'inventory-only; value and boundary acceptance remain pending in phase B/C',
|
||||
},
|
||||
dependencies: [],
|
||||
applicability: {
|
||||
requiredObjectTypeId: objectType.typeId,
|
||||
source: 'Document.supportedTypes runtime inventory',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
if (records.length !== 1) fail(`expected one native App::PropertyAcceleration record, found ${records.length}`)
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
status: 'pass',
|
||||
baseline: { freecadVersion: source.freecadVersion, commit: source.gitCommit },
|
||||
propertyType: 'App::PropertyAcceleration',
|
||||
recordCount: records.length,
|
||||
records,
|
||||
source: {
|
||||
path: '.cache/freecad/reference-desktop.json',
|
||||
bytes: sourceContent.length,
|
||||
sha256: createHash('sha256').update(sourceContent).digest('hex'),
|
||||
},
|
||||
classification: 'opaque-fcstd-proxy',
|
||||
nextPhases: ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'],
|
||||
}
|
||||
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
|
||||
console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-acceleration-inventory.json', propertyType: report.propertyType, recordCount: report.recordCount }, null, 2))
|
||||
59
scripts/generate-freecad-property-acceleration-promotion.mjs
Normal file
59
scripts/generate-freecad-property-acceleration-promotion.mjs
Normal file
@@ -0,0 +1,59 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
|
||||
const outputPath = resolve(root, 'config/freecad-property-acceleration-promotion.json')
|
||||
const [inventory, success, failure, mutation, roundTrip, chrome, semantics, progress] = await Promise.all([
|
||||
load('config/freecad-property-acceleration-inventory.json'),
|
||||
load('config/freecad-property-acceleration-success.json'),
|
||||
load('config/freecad-property-acceleration-failure.json'),
|
||||
load('config/freecad-property-acceleration-mutation.json'),
|
||||
load('config/freecad-property-acceleration-roundtrip.json'),
|
||||
load('config/chrome-property-acceleration-verification.json'),
|
||||
load('config/freecad-native-property-semantics.json'),
|
||||
load('config/freecad-follow-up-task-progress.json'),
|
||||
])
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyAcceleration promotion generation failed: ${message}`) }
|
||||
const requiredCompletedPhases = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
|
||||
const completed = new Map(progress.completedTasks?.map((entry) => [entry.id, entry]) ?? [])
|
||||
for (const phase of requiredCompletedPhases) if (!completed.has(`PROP-app-propertyacceleration-${phase}`)) fail(`phase ${phase} is not completed in the evidence ledger`)
|
||||
if (inventory.status !== 'pass' || inventory.propertyType !== 'App::PropertyAcceleration' || inventory.recordCount !== 1) fail('inventory evidence is invalid')
|
||||
for (const [name, artifact] of Object.entries({ success, failure, mutation, roundTrip, chrome })) if (artifact.status !== 'pass') fail(`${name} evidence is not passing`)
|
||||
const propertyType = semantics.types?.find(({ typeId }) => typeId === 'App::PropertyAcceleration')
|
||||
if (propertyType?.support !== 'native-editable-codec' || propertyType.recordCount !== 1) fail('global property semantics did not promote App::PropertyAcceleration')
|
||||
const opaque = semantics.supportSummary?.['opaque-fcstd-proxy']
|
||||
const editable = semantics.supportSummary?.['native-editable-codec']
|
||||
if (opaque?.typeCount !== 50 || opaque.recordCount !== 466 || editable?.typeCount !== 30 || editable.recordCount !== 4361) fail('global property support summary is not synchronized')
|
||||
if (roundTrip.classification?.zeroUnknownDrift !== true || chrome.persistence?.loadedType !== 'App::PropertyAcceleration' || chrome.persistence.loadedValue !== 500 || chrome.release?.workerTerminated !== true) fail('round-trip or browser closure evidence is incomplete')
|
||||
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
status: 'pass',
|
||||
taskId: 'PROP-app-propertyacceleration-I',
|
||||
baseline: semantics.baseline,
|
||||
propertyType: 'App::PropertyAcceleration',
|
||||
recordCount: 1,
|
||||
phaseEvidence: Object.fromEntries(requiredCompletedPhases.map((phase) => [phase, completed.get(`PROP-app-propertyacceleration-${phase}`).evidence])),
|
||||
promotion: {
|
||||
from: inventory.classification,
|
||||
to: propertyType.support,
|
||||
facadeValueModel: 'finite-number-mm/s^2',
|
||||
fcstdElement: chrome.persistence.fcstdElement,
|
||||
nativeRoundTripValue: roundTrip.classification.resavedValue,
|
||||
browserRoundTripValue: chrome.persistence.loadedValue,
|
||||
zeroUnknownDrift: roundTrip.classification.zeroUnknownDrift,
|
||||
},
|
||||
exactBlockerSync: {
|
||||
nativeEditableTypes: editable.typeCount,
|
||||
nativeEditableRecords: editable.recordCount,
|
||||
opaqueTypes: opaque.typeCount,
|
||||
opaqueRecords: opaque.recordCount,
|
||||
exactPromotionReady: semantics.exactPromotionReady,
|
||||
exactBlocker: semantics.exactBlocker,
|
||||
},
|
||||
systemExact: false,
|
||||
generatedAt: new Date().toISOString(),
|
||||
}
|
||||
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
|
||||
console.log(JSON.stringify({ status: 'freecad-property-acceleration-promotion-generated', output: 'config/freecad-property-acceleration-promotion.json', promotion: report.promotion, exactBlockerSync: report.exactBlockerSync }, null, 2))
|
||||
62
scripts/generate-freecad-property-area-inventory.mjs
Normal file
62
scripts/generate-freecad-property-area-inventory.mjs
Normal file
@@ -0,0 +1,62 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const runtimePath = resolve(root, '.cache/freecad/reference-desktop.json')
|
||||
const sourcePaths = [
|
||||
'.cache/freecad/FreeCAD/src/Mod/Measure/App/MeasureArea.cpp',
|
||||
'.cache/freecad/FreeCAD/src/Mod/Measure/App/MeasureArea.h',
|
||||
]
|
||||
const outputPath = resolve(root, 'config/freecad-property-area-inventory.json')
|
||||
const runtimeContent = await readFile(runtimePath)
|
||||
const runtime = JSON.parse(runtimeContent)
|
||||
const sourceContents = await Promise.all(sourcePaths.map((path) => readFile(resolve(root, path))))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyArea inventory generation failed: ${message}`) }
|
||||
|
||||
if (runtime.schemaVersion !== 1 || runtime.freecadVersion !== '1.1.1' || runtime.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('desktop oracle is not the locked FreeCAD baseline')
|
||||
const records = []
|
||||
for (const objectType of runtime.runtimeObjects?.types ?? []) {
|
||||
for (const property of objectType.properties ?? []) {
|
||||
if (property.typeId !== 'App::PropertyArea') continue
|
||||
const input = objectType.properties.find((candidate) => candidate.name === 'Elements')
|
||||
records.push({
|
||||
objectTypeId: objectType.typeId,
|
||||
objectAvailable: objectType.available === true,
|
||||
probeStatus: objectType.probeStatus,
|
||||
propertyName: property.name,
|
||||
group: property.group,
|
||||
status: property.status,
|
||||
normalizedStatus: property.status.map((entry) => entry === 24 ? 'PropReadOnly' : entry === 27 ? 'PropOutput' : String(entry)),
|
||||
defaultRaw: property.default,
|
||||
valueModel: { kind: 'quantity', dimension: 'length^2', defaultValue: 0, defaultUnit: 'mm^2', derived: true, writable: false },
|
||||
inputs: [{ propertyName: input?.name, typeId: input?.typeId, group: input?.group, defaultValue: input?.default, scope: 'Global', allowExternal: true, cardinality: 'one-or-more' }],
|
||||
dependencies: [{ sourceProperty: 'Elements', targetProperty: 'Area', relation: 'link-sub-list', effect: 'immediate-recompute-on-change' }],
|
||||
applicability: {
|
||||
selectionMustBeNonEmpty: true,
|
||||
everyElementMustBeValid: true,
|
||||
supportedMeasureElementTypes: ['PLANE', 'CYLINDER', 'SURFACE', 'VOLUME'],
|
||||
unsupportedMeasureElementTypes: ['INVALID', 'POINT', 'LINE', 'CURVE'],
|
||||
externalDocumentElementsAllowed: true,
|
||||
},
|
||||
execution: { aggregation: 'sum-area', resultProperty: 'Area', invalidGeometryDiagnostic: 'Cannot calculate area', shape: 'not-applicable-derived-measurement' },
|
||||
})
|
||||
}
|
||||
}
|
||||
if (records.length !== 1) fail(`expected one native App::PropertyArea record, found ${records.length}`)
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
status: 'pass',
|
||||
baseline: { freecadVersion: runtime.freecadVersion, commit: runtime.gitCommit },
|
||||
propertyType: 'App::PropertyArea',
|
||||
recordCount: records.length,
|
||||
records,
|
||||
provenance: {
|
||||
runtime: { path: '.cache/freecad/reference-desktop.json', bytes: runtimeContent.length, sha256: createHash('sha256').update(runtimeContent).digest('hex') },
|
||||
sources: sourcePaths.map((path, index) => ({ path, bytes: sourceContents[index].length, sha256: createHash('sha256').update(sourceContents[index]).digest('hex') })),
|
||||
},
|
||||
classification: 'opaque-fcstd-proxy',
|
||||
nextPhases: ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'],
|
||||
}
|
||||
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
|
||||
console.log(JSON.stringify({ status: 'freecad-property-area-inventory-generated', output: 'config/freecad-property-area-inventory.json', propertyType: report.propertyType, recordCount: report.recordCount, objectTypeId: records[0].objectTypeId }, null, 2))
|
||||
63
scripts/generate-freecad-property-area-promotion.mjs
Normal file
63
scripts/generate-freecad-property-area-promotion.mjs
Normal file
@@ -0,0 +1,63 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
|
||||
const outputPath = resolve(root, 'config/freecad-property-area-promotion.json')
|
||||
const [inventory, success, failure, mutation, roundTrip, chrome, semantics, progress] = await Promise.all([
|
||||
load('config/freecad-property-area-inventory.json'),
|
||||
load('config/freecad-property-area-success.json'),
|
||||
load('config/freecad-property-area-failure.json'),
|
||||
load('config/freecad-property-area-mutation.json'),
|
||||
load('config/freecad-property-area-roundtrip.json'),
|
||||
load('config/chrome-property-area-verification.json'),
|
||||
load('config/freecad-native-property-semantics.json'),
|
||||
load('config/freecad-follow-up-task-progress.json'),
|
||||
])
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyArea promotion generation failed: ${message}`) }
|
||||
const requiredCompletedPhases = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
|
||||
const completed = new Map(progress.completedTasks?.map((entry) => [entry.id, entry]) ?? [])
|
||||
for (const phase of requiredCompletedPhases) if (!completed.has(`PROP-app-propertyarea-${phase}`)) fail(`phase ${phase} is not completed in the evidence ledger`)
|
||||
if (inventory.status !== 'pass' || inventory.propertyType !== 'App::PropertyArea' || inventory.recordCount !== 1 || inventory.records?.[0]?.valueModel?.derived !== true || inventory.records[0].valueModel.writable !== false) fail('inventory evidence is invalid')
|
||||
for (const [name, artifact] of Object.entries({ success, failure, mutation, roundTrip, chrome })) if (artifact.status !== 'pass') fail(`${name} evidence is not passing`)
|
||||
const propertyType = semantics.types?.find(({ typeId }) => typeId === 'App::PropertyArea')
|
||||
if (propertyType?.support !== 'native-editable-codec' || propertyType.recordCount !== 1 || propertyType.statusNames?.join(',') !== 'PropOutput,PropReadOnly') fail('global property semantics did not promote App::PropertyArea with its native status')
|
||||
const opaque = semantics.supportSummary?.['opaque-fcstd-proxy']
|
||||
const editable = semantics.supportSummary?.['native-editable-codec']
|
||||
if (opaque?.typeCount !== 50 || opaque.recordCount !== 466 || editable?.typeCount !== 30 || editable.recordCount !== 4361) fail('global property support summary is not synchronized')
|
||||
if (roundTrip.classification?.zeroUnknownDrift !== true || chrome.ui?.areaReadOnly !== true || chrome.persistence?.loadedAreaType !== 'App::PropertyArea' || chrome.persistence.loadedElementsType !== 'App::PropertyLinkSubList' || chrome.release?.workerTerminated !== true) fail('round-trip or browser closure evidence is incomplete')
|
||||
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
status: 'pass',
|
||||
taskId: 'PROP-app-propertyarea-I',
|
||||
baseline: semantics.baseline,
|
||||
propertyType: 'App::PropertyArea',
|
||||
recordCount: 1,
|
||||
phaseEvidence: Object.fromEntries(requiredCompletedPhases.map((phase) => [phase, completed.get(`PROP-app-propertyarea-${phase}`).evidence])),
|
||||
promotion: {
|
||||
from: inventory.classification,
|
||||
to: propertyType.support,
|
||||
facadeValueModel: 'read-only-derived-mm^2-with-link-sub-list-input',
|
||||
inputType: 'App::PropertyLinkSubList',
|
||||
fcstdAreaElement: chrome.persistence.fcstdAreaElement,
|
||||
fcstdElementsElement: chrome.persistence.fcstdElementsElement,
|
||||
nativeRoundTripArea: roundTrip.classification.resavedArea,
|
||||
browserRoundTripArea: chrome.persistence.loadedArea,
|
||||
browserRoundTripElementCount: chrome.persistence.loadedElementCount,
|
||||
sourceGeometryPreserved: roundTrip.classification.sourceGeometryPreserved,
|
||||
zeroUnknownDrift: roundTrip.classification.zeroUnknownDrift,
|
||||
},
|
||||
exactBlockerSync: {
|
||||
nativeEditableTypes: editable.typeCount,
|
||||
nativeEditableRecords: editable.recordCount,
|
||||
opaqueTypes: opaque.typeCount,
|
||||
opaqueRecords: opaque.recordCount,
|
||||
exactPromotionReady: semantics.exactPromotionReady,
|
||||
exactBlocker: semantics.exactBlocker,
|
||||
},
|
||||
systemExact: false,
|
||||
generatedAt: new Date().toISOString(),
|
||||
}
|
||||
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
|
||||
console.log(JSON.stringify({ status: 'freecad-property-area-promotion-generated', output: 'config/freecad-property-area-promotion.json', promotion: report.promotion, exactBlockerSync: report.exactBlockerSync }, null, 2))
|
||||
42
scripts/generate-freecad-property-boollist-inventory.mjs
Normal file
42
scripts/generate-freecad-property-boollist-inventory.mjs
Normal file
@@ -0,0 +1,42 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const sourcePath = resolve(root, '.cache/freecad/reference-desktop.json')
|
||||
const outputPath = resolve(root, 'config/freecad-property-boollist-inventory.json')
|
||||
const sourceContent = await readFile(sourcePath)
|
||||
const source = JSON.parse(sourceContent)
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyBoolList inventory generation failed: ${message}`) }
|
||||
if (source.schemaVersion !== 1 || source.freecadVersion !== '1.1.1' || source.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('desktop oracle is not the locked FreeCAD baseline')
|
||||
const records = []
|
||||
for (const objectType of source.runtimeObjects?.types ?? []) for (const property of objectType.properties ?? []) {
|
||||
if (property.typeId !== 'App::PropertyBoolList') continue
|
||||
records.push({
|
||||
objectTypeId: objectType.typeId,
|
||||
objectAvailable: objectType.available === true,
|
||||
probeStatus: objectType.probeStatus,
|
||||
propertyName: property.name,
|
||||
group: property.group,
|
||||
status: property.status,
|
||||
defaultValue: property.default,
|
||||
valueModel: { kind: 'boolean-list', writable: property.status.length === 0, elementType: 'bool', cardinality: 'variable' },
|
||||
dependencies: [],
|
||||
applicability: { requiredObjectTypeId: objectType.typeId, source: 'Document.supportedTypes runtime inventory' },
|
||||
})
|
||||
}
|
||||
records.sort((left, right) => `${left.objectTypeId}.${left.propertyName}`.localeCompare(`${right.objectTypeId}.${right.propertyName}`))
|
||||
if (records.length !== 7) fail(`expected seven native App::PropertyBoolList records, found ${records.length}`)
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
status: 'pass',
|
||||
baseline: { freecadVersion: source.freecadVersion, commit: source.gitCommit },
|
||||
propertyType: 'App::PropertyBoolList',
|
||||
recordCount: records.length,
|
||||
records,
|
||||
source: { path: '.cache/freecad/reference-desktop.json', bytes: sourceContent.length, sha256: createHash('sha256').update(sourceContent).digest('hex') },
|
||||
classification: 'opaque-fcstd-proxy',
|
||||
nextPhases: ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'],
|
||||
}
|
||||
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
|
||||
console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-boollist-inventory.json', propertyType: report.propertyType, recordCount: report.recordCount }, null, 2))
|
||||
61
scripts/generate-freecad-property-boollist-promotion.mjs
Normal file
61
scripts/generate-freecad-property-boollist-promotion.mjs
Normal file
@@ -0,0 +1,61 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
|
||||
const outputPath = resolve(root, 'config/freecad-property-boollist-promotion.json')
|
||||
const [inventory, success, failure, mutation, roundTrip, chrome, semantics, progress] = await Promise.all([
|
||||
load('config/freecad-property-boollist-inventory.json'),
|
||||
load('config/freecad-property-boollist-success.json'),
|
||||
load('config/freecad-property-boollist-failure.json'),
|
||||
load('config/freecad-property-boollist-mutation.json'),
|
||||
load('config/freecad-property-boollist-roundtrip.json'),
|
||||
load('config/chrome-property-boollist-verification.json'),
|
||||
load('config/freecad-native-property-semantics.json'),
|
||||
load('config/freecad-follow-up-task-progress.json'),
|
||||
])
|
||||
const fail = (message) => { throw new Error(`FreeCAD PropertyBoolList promotion generation failed: ${message}`) }
|
||||
const requiredCompletedPhases = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
|
||||
const completed = new Map(progress.completedTasks?.map((entry) => [entry.id, entry]) ?? [])
|
||||
for (const phase of requiredCompletedPhases) if (!completed.has(`PROP-app-propertyboollist-${phase}`)) fail(`phase ${phase} is not completed in the evidence ledger`)
|
||||
if (inventory.status !== 'pass' || inventory.propertyType !== 'App::PropertyBoolList' || inventory.recordCount !== 7 || inventory.records?.filter(({ valueModel }) => valueModel?.writable === true).length !== 3) fail('inventory evidence is invalid')
|
||||
for (const [name, artifact] of Object.entries({ success, failure, mutation, roundTrip, chrome })) if (artifact.status !== 'pass') fail(`${name} evidence is not passing`)
|
||||
const propertyType = semantics.types?.find(({ typeId }) => typeId === 'App::PropertyBoolList')
|
||||
if (propertyType?.support !== 'native-editable-codec' || propertyType.recordCount !== 7 || propertyType.objectTypeCount !== 7 || propertyType.statusNames?.join(',') !== 'Hidden,Immutable,LockDynamic') fail('global property semantics did not promote App::PropertyBoolList with its native status')
|
||||
const opaque = semantics.supportSummary?.['opaque-fcstd-proxy']
|
||||
const editable = semantics.supportSummary?.['native-editable-codec']
|
||||
if (opaque?.typeCount !== 50 || opaque.recordCount !== 466 || editable?.typeCount !== 30 || editable.recordCount !== 4361) fail('global property support summary is not synchronized')
|
||||
if (roundTrip.classification?.zeroUnknownDrift !== true || chrome.persistence?.loadedType !== 'App::PropertyBoolList' || chrome.persistence.fcstdElement !== 'BoolList' || chrome.release?.workerTerminated !== true) fail('round-trip or browser closure evidence is incomplete')
|
||||
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
status: 'pass',
|
||||
taskId: 'PROP-app-propertyboollist-I',
|
||||
baseline: semantics.baseline,
|
||||
propertyType: 'App::PropertyBoolList',
|
||||
recordCount: 7,
|
||||
phaseEvidence: Object.fromEntries(requiredCompletedPhases.map((phase) => [phase, completed.get(`PROP-app-propertyboollist-${phase}`).evidence])),
|
||||
promotion: {
|
||||
from: inventory.classification,
|
||||
to: propertyType.support,
|
||||
facadeValueModel: 'variable-boolean-array',
|
||||
writableRecords: 3,
|
||||
immutableHiddenRecords: 4,
|
||||
fcstdElement: chrome.persistence.fcstdElement,
|
||||
nativeRoundTripValue: roundTrip.classification.resavedValue,
|
||||
browserRoundTripValue: chrome.persistence.loadedValue,
|
||||
zeroUnknownDrift: roundTrip.classification.zeroUnknownDrift,
|
||||
},
|
||||
exactBlockerSync: {
|
||||
nativeEditableTypes: editable.typeCount,
|
||||
nativeEditableRecords: editable.recordCount,
|
||||
opaqueTypes: opaque.typeCount,
|
||||
opaqueRecords: opaque.recordCount,
|
||||
exactPromotionReady: semantics.exactPromotionReady,
|
||||
exactBlocker: semantics.exactBlocker,
|
||||
},
|
||||
systemExact: false,
|
||||
generatedAt: new Date().toISOString(),
|
||||
}
|
||||
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
|
||||
console.log(JSON.stringify({ status: 'freecad-property-boollist-promotion-generated', output: 'config/freecad-property-boollist-promotion.json', promotion: report.promotion, exactBlockerSync: report.exactBlockerSync }, null, 2))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user