109 lines
7.4 KiB
JavaScript
109 lines
7.4 KiB
JavaScript
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
import { resolve } from 'node:path'
|
|
import { spawnSync } from 'node:child_process'
|
|
import { loadGoldenManifest } from './freecad-golden-contract.mjs'
|
|
|
|
const root = resolve(new URL('..', import.meta.url).pathname)
|
|
const scenarioDir = resolve(root, 'fixtures/freecad-golden/scenarios')
|
|
const failureDir = resolve(root, 'fixtures/freecad-golden/failures')
|
|
const workDir = resolve(root, '.cache/freecad/generated-fixtures')
|
|
const command = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-native/bin/FreeCADCmd')
|
|
const driver = resolve(root, 'scripts/freecad-golden-driver.py')
|
|
await mkdir(scenarioDir, { recursive: true })
|
|
await mkdir(failureDir, { recursive: true })
|
|
await mkdir(workDir, { recursive: true })
|
|
|
|
const manifest = await loadGoldenManifest(resolve(root, 'fixtures/freecad-golden/manifest.json'))
|
|
const existing = manifest.scenarios.map(({ scenario }) => scenario)
|
|
const generated = []
|
|
const add = (id, category, description, operation) => generated.push({ schemaVersion: 1, id, category, description, operation, tolerance: { linear: 1e-6, scalar: 1e-6 } })
|
|
|
|
for (let index = 1; index <= 20; index += 1) {
|
|
add(`part-box-${String(index).padStart(3, '0')}`, 'Part', `Parametric box fixture ${index}`, {
|
|
type: 'box', length: 2 + (index % 9), width: 3 + ((index * 2) % 11), height: 4 + ((index * 3) % 13),
|
|
placement: { translation: [index % 5, (index * 2) % 7, index % 3] },
|
|
})
|
|
}
|
|
for (let index = 1; index <= 20; index += 1) {
|
|
add(`part-cylinder-${String(index).padStart(3, '0')}`, 'Part', `Parametric cylinder fixture ${index}`, {
|
|
type: 'cylinder', radius: 1 + (index % 8) / 2, height: 5 + (index * 3) % 17, angle: 360,
|
|
placement: { translation: [-(index % 4), (index * 2) % 5, index % 3] },
|
|
})
|
|
}
|
|
for (let index = 1; index <= 15; index += 1) {
|
|
add(`part-sphere-${String(index).padStart(3, '0')}`, 'Part', `Parametric sphere fixture ${index}`, {
|
|
type: 'sphere', radius: 1 + (index % 9) / 3,
|
|
placement: { translation: [index % 6, -(index % 5), (index * 2) % 4] },
|
|
})
|
|
}
|
|
for (let index = 1; index <= 15; index += 1) {
|
|
add(`part-cone-${String(index).padStart(3, '0')}`, 'Part', `Parametric cone fixture ${index}`, {
|
|
type: 'cone', radius1: 2 + (index % 6), radius2: 0.5 + (index % 4) / 2, height: 4 + (index % 15), angle: 360,
|
|
placement: { translation: [index % 4, index % 6, -(index % 2)] },
|
|
})
|
|
}
|
|
for (let index = 1; index <= 25; index += 1) {
|
|
const kind = index % 3 === 0 ? 'common' : index % 3 === 1 ? 'cut' : 'fuse'
|
|
const size = 8 + (index % 5)
|
|
const base = { type: 'box', length: size, width: size - 1, height: size + 1, placement: { translation: [0, 0, index % 2] } }
|
|
const tool = kind === 'cut'
|
|
? { type: 'cylinder', radius: 1.5 + (index % 3), height: size + 1, angle: 360, placement: { translation: [size / 2, (size - 1) / 2, index % 2] } }
|
|
: { type: 'box', length: 4 + (index % 4), width: 4 + ((index + 1) % 4), height: 4 + ((index + 2) % 4), placement: { translation: [2, 2, index % 2] } }
|
|
add(`part-${kind}-${String(index).padStart(3, '0')}`, 'Part', `${kind} Boolean fixture ${index}`, { type: kind, base, tool })
|
|
}
|
|
|
|
const expectedFromOracle = (scenario) => {
|
|
const tempFile = resolve(workDir, `${scenario.id}.json`)
|
|
return writeFile(tempFile, `${JSON.stringify(scenario, null, 2)}\n`).then(() => {
|
|
const execution = spawnSync(command, [driver], {
|
|
cwd: root,
|
|
encoding: 'utf8',
|
|
timeout: 120000,
|
|
maxBuffer: 4 * 1024 * 1024,
|
|
env: { ...process.env, FREECAD_GOLDEN_SCENARIO: tempFile },
|
|
})
|
|
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
|
|
const line = output.split(/\r?\n/).find((entry) => entry.startsWith('FREECAD_GOLDEN_RESULT='))
|
|
if (execution.error || execution.status !== 0 || !line) throw new Error(`${scenario.id} oracle failure: ${execution.error?.message || output.trim()}`)
|
|
const actual = JSON.parse(line.slice('FREECAD_GOLDEN_RESULT='.length))
|
|
if (!['Solid', 'Compound'].includes(actual.shapeType) || actual.isNull !== false || actual.isValid !== true || actual.solids < 1) throw new Error(`${scenario.id} produced an invalid success shape.`)
|
|
return {
|
|
...scenario,
|
|
expected: {
|
|
shapeType: actual.shapeType,
|
|
isNull: actual.isNull,
|
|
isValid: actual.isValid,
|
|
solids: actual.solids,
|
|
faces: actual.faces,
|
|
edges: actual.edges,
|
|
vertices: actual.vertices,
|
|
volume: actual.volume,
|
|
area: actual.area,
|
|
boundingBox: actual.boundingBox,
|
|
},
|
|
}
|
|
})
|
|
}
|
|
|
|
const allSuccess = [...existing]
|
|
for (const scenario of generated) {
|
|
const completed = await expectedFromOracle(scenario)
|
|
allSuccess.push(completed)
|
|
await writeFile(resolve(scenarioDir, `${completed.id}.json`), `${JSON.stringify(completed, null, 2)}\n`)
|
|
}
|
|
const successEntries = allSuccess.map((scenario) => ({ id: scenario.id, file: `scenarios/${scenario.id}.json` }))
|
|
await writeFile(resolve(root, 'fixtures/freecad-golden/manifest.json'), `${JSON.stringify({ schemaVersion: 1, baselineId: 'freecad-1.1.1', driver: 'scripts/freecad-golden-driver.py', scenarios: successEntries }, null, 2)}\n`)
|
|
|
|
const failures = []
|
|
const addFailure = (id, description, operation, expectedError) => failures.push({ schemaVersion: 1, id, category: 'Part', description, operation, expectedError })
|
|
for (let index = 1; index <= 12; index += 1) addFailure(`invalid-box-${String(index).padStart(3, '0')}`, 'Box dimension rejection', { type: 'box', length: index % 2 ? 0 : -index, width: 2 + index, height: 3 }, 'positive')
|
|
for (let index = 1; index <= 10; index += 1) addFailure(`invalid-cylinder-${String(index).padStart(3, '0')}`, 'Cylinder parameter rejection', { type: 'cylinder', radius: index % 2 ? 0 : -index, height: index % 3 ? 5 : 0, angle: index % 4 ? 360 : 361 }, 'parameters')
|
|
for (let index = 1; index <= 8; index += 1) addFailure(`invalid-sphere-${String(index).padStart(3, '0')}`, 'Sphere radius rejection', { type: 'sphere', radius: index % 2 ? 0 : -index }, 'positive')
|
|
for (let index = 1; index <= 8; index += 1) addFailure(`invalid-cone-${String(index).padStart(3, '0')}`, 'Cone parameter rejection', { type: 'cone', radius1: index % 2 ? -1 : 0, radius2: index % 2 ? 0 : -1, height: index % 3 ? 5 : 0, angle: index % 4 ? 360 : 361 }, 'parameters')
|
|
for (let index = 1; index <= 6; index += 1) addFailure(`invalid-boolean-${String(index).padStart(3, '0')}`, 'Boolean operand rejection', { type: index % 2 ? 'cut' : 'fuse', base: { type: 'box', length: 1, width: 1, height: 1 }, ...(index % 3 ? { tool: { type: 'unknown' } } : {}) }, index % 3 ? 'Unsupported' : 'operation')
|
|
addFailure('invalid-common-001', 'Common operand rejection', { type: 'common', base: { type: 'box', length: 1, width: 1, height: 1 } }, 'operation')
|
|
for (let index = 1; index <= 6; index += 1) addFailure(`invalid-operation-${String(index).padStart(3, '0')}`, 'Unknown operation rejection', { type: `unsupported-${index}` }, 'Unsupported')
|
|
for (const failure of failures) await writeFile(resolve(failureDir, `${failure.id}.json`), `${JSON.stringify(failure, null, 2)}\n`)
|
|
await writeFile(resolve(failureDir, 'manifest.json'), `${JSON.stringify({ schemaVersion: 1, baselineId: 'freecad-1.1.1', driver: 'scripts/freecad-golden-driver.py', failures: failures.map((failure) => ({ id: failure.id, file: `${failure.id}.json` })) }, null, 2)}\n`)
|
|
console.log(JSON.stringify({ status: 'freecad-golden-fixtures-generated', successCount: allSuccess.length, generatedSuccessCount: generated.length, failureCount: failures.length }, null, 2))
|