Files
Web_FreeCAD_Bitbybit/scripts/run-geometry-input-fuzz.ts

118 lines
9.0 KiB
TypeScript

import assert from 'node:assert/strict'
import { writeFile } from 'node:fs/promises'
import { performance } from 'node:perf_hooks'
import {
validateBooleanCutInput,
validateBooleanUnionInput,
validateBoxInput,
validateChamferInput,
validateConeInput,
validateCylinderInput,
validateFilletInput,
validateGeometryFileImport,
validateGrooveInput,
validateLoftInput,
validateMirrorInput,
validatePadInput,
validatePipeInput,
validatePlacementInput,
validatePocketInput,
validateRevolutionInput,
validateTorusInput,
} from '../src/facade/geometryRuntime'
import type { PlanarProfile, ShapeHandle } from '../src/facade/types'
type GeometryFuzzCase = { name: string; expected: 'accept' | 'reject'; invoke: () => void }
let state = 0x9e3779b9
const random = () => {
state ^= state << 13
state ^= state >>> 17
state ^= state << 5
return state >>> 0
}
const positive = () => (random() % 100_000 + 1) / 1000
const context = () => ({ documentId: `geometry-fuzz-${random() % 7}`, documentVersion: random() % 100 })
const shape = (documentId: string, documentVersion: number, suffix: string): ShapeHandle => ({ id: `shape-${suffix}`, kernel: 'bitbybit-occt', kind: 'solid', documentId, documentVersion })
const square = (size = positive()): PlanarProfile => ({ outer: [[0, 0, 0], [size, 0, 0], [size, size, 0], [0, size, 0]] })
const elevatedSquare = (): PlanarProfile => {
const profile = square()
const z = positive()
return { outer: profile.outer.map(([x, y]): [number, number, number] => [x, y, z]) }
}
const invalidScalar = () => [0, -positive(), Number.NaN, Number.POSITIVE_INFINITY][random() % 4]
const createCase = (index: number): GeometryFuzzCase => {
const owner = context()
const base = shape(owner.documentId, Math.max(0, owner.documentVersion - 1), 'base')
const tool = shape(owner.documentId, owner.documentVersion, 'tool')
switch (index % 30) {
case 0: return { name: 'valid-box', expected: 'accept', invoke: () => validateBoxInput({ ...owner, width: positive(), length: positive(), height: positive(), center: [positive(), -positive(), 0] }) }
case 1: return { name: 'invalid-box-scalar', expected: 'reject', invoke: () => validateBoxInput({ ...owner, width: invalidScalar(), length: positive(), height: positive() }) }
case 2: return { name: 'valid-cylinder', expected: 'accept', invoke: () => validateCylinderInput({ ...owner, radius: positive(), height: positive(), direction: [positive(), positive(), positive()], angle: positive() % 360 || 360 }) }
case 3: return { name: 'zero-cylinder-axis', expected: 'reject', invoke: () => validateCylinderInput({ ...owner, radius: positive(), height: positive(), direction: [0, 0, 0] }) }
case 4: return { name: 'zero-cone-radii', expected: 'reject', invoke: () => validateConeInput({ ...owner, radius1: 0, radius2: 0, height: positive() }) }
case 5: return { name: 'valid-placement', expected: 'accept', invoke: () => validatePlacementInput({ ...owner, shape: base, placement: { translation: [positive(), 0, -positive()], rotationAxis: [0, 0, 1], rotationAngle: random() % 361 } }) }
case 6: return { name: 'foreign-placement-shape', expected: 'reject', invoke: () => validatePlacementInput({ ...owner, shape: { ...base, documentId: 'foreign-document' }, placement: { translation: [0, 0, 0], rotationAxis: [0, 1, 0], rotationAngle: 0 } }) }
case 7: return { name: 'valid-union', expected: 'accept', invoke: () => validateBooleanUnionInput({ ...owner, shapes: [base, tool] }) }
case 8: return { name: 'short-union', expected: 'reject', invoke: () => validateBooleanUnionInput({ ...owner, shapes: [base] }) }
case 9: return { name: 'empty-cut-tools', expected: 'reject', invoke: () => validateBooleanCutInput({ ...owner, base, tools: [] }) }
case 10: return { name: 'fractional-fillet-index', expected: 'reject', invoke: () => validateFilletInput({ ...owner, base, radius: positive(), indexes: [random() % 10 + 0.5] }) }
case 11: return { name: 'valid-chamfer', expected: 'accept', invoke: () => validateChamferInput({ ...owner, base, distance: positive(), indexes: [random() % 20] }) }
case 12: return { name: 'valid-pad', expected: 'accept', invoke: () => validatePadInput({ ...owner, profile: square(), length: positive(), direction: [0, 0, 1] }) }
case 13: return { name: 'self-intersecting-pad', expected: 'reject', invoke: () => validatePadInput({ ...owner, profile: { outer: [[0, 0, 0], [2, 2, 0], [0, 2, 0], [2, 0, 0]] }, length: positive() }) }
case 14: return { name: 'future-pocket-base', expected: 'reject', invoke: () => validatePocketInput({ ...owner, base: { ...base, documentVersion: owner.documentVersion + 1 }, profile: square(), length: positive() }) }
case 15: return { name: 'zero-revolution-axis', expected: 'reject', invoke: () => validateRevolutionInput({ ...owner, profile: square(), axisDirection: [0, 0, 0], angle: 360 }) }
case 16: return { name: 'invalid-groove-angle', expected: 'reject', invoke: () => validateGrooveInput({ ...owner, base, profile: square(), axisDirection: [0, 1, 0], angle: 361 }) }
case 17: return { name: 'valid-step-import', expected: 'accept', invoke: () => validateGeometryFileImport({ ...owner, format: 'step', text: `ISO-10303-21;\n/* ${random().toString(16)} */\nEND-ISO-10303-21;` }) }
case 18: return { name: 'unsupported-import-format', expected: 'reject', invoke: () => validateGeometryFileImport({ ...owner, format: 'stl', text: 'solid fuzz' } as never) }
case 19: return { name: 'zero-mirror-normal', expected: 'reject', invoke: () => validateMirrorInput({ ...owner, shape: base, origin: [0, 0, 0], normal: [0, 0, 0] }) }
case 20: return { name: 'valid-standalone-loft', expected: 'accept', invoke: () => validateLoftInput({ ...owner, sections: [square(), elevatedSquare()] }) }
case 21: return { name: 'valid-additive-loft', expected: 'accept', invoke: () => validateLoftInput({ ...owner, sections: [square(), elevatedSquare()], mode: 'additive', base }) }
case 22: return { name: 'short-loft-sections', expected: 'reject', invoke: () => validateLoftInput({ ...owner, sections: [square()] }) }
case 23: return { name: 'loft-hole-unsupported', expected: 'reject', invoke: () => validateLoftInput({ ...owner, sections: [{ ...square(), holes: [square(0.1).outer] }, square()] }) }
case 24: return { name: 'valid-standalone-pipe', expected: 'accept', invoke: () => validatePipeInput({ ...owner, profile: square(), path: [[0, 0, 0], [positive(), 0, positive()], [positive(), positive(), positive()]] }) }
case 25: return { name: 'valid-subtractive-pipe', expected: 'accept', invoke: () => validatePipeInput({ ...owner, profile: square(), path: [[0, 0, 0], [0, positive(), positive()]], mode: 'subtractive', base }) }
case 26: return { name: 'duplicate-pipe-path-point', expected: 'reject', invoke: () => validatePipeInput({ ...owner, profile: square(), path: [[0, 0, 0], [0, 0, 0]] }) }
case 27: return { name: 'pipe-hole-unsupported', expected: 'reject', invoke: () => validatePipeInput({ ...owner, profile: { ...square(), holes: [square(0.1).outer] }, path: [[0, 0, 0], [0, 0, positive()]] }) }
case 28: return { name: 'valid-torus', expected: 'accept', invoke: () => validateTorusInput({ ...owner, majorRadius: positive(), minorRadius: positive(), direction: [0, 0, 1], angle: random() % 360 + 1 }) }
default: return { name: 'invalid-torus-radius', expected: 'reject', invoke: () => validateTorusInput({ ...owner, majorRadius: positive(), minorRadius: invalidScalar() }) }
}
}
const durations: number[] = []
const categories = new Map<string, number>()
const counts = { accepted: 0, rejected: 0, validCases: 0, invalidCases: 0 }
for (let index = 0; index < 2000; index += 1) {
const fuzzCase = createCase(index)
categories.set(fuzzCase.name, (categories.get(fuzzCase.name) ?? 0) + 1)
const started = performance.now()
let outcome: 'accept' | 'reject' = 'accept'
let caught: unknown
try {
fuzzCase.invoke()
} catch (error) {
outcome = 'reject'
caught = error
}
durations.push(performance.now() - started)
counts[outcome === 'accept' ? 'accepted' : 'rejected'] += 1
counts[fuzzCase.expected === 'accept' ? 'validCases' : 'invalidCases'] += 1
assert.equal(outcome, fuzzCase.expected, `Geometry fuzz category ${fuzzCase.name} produced ${outcome}, expected ${fuzzCase.expected}.`)
if (outcome === 'reject') assert.ok(caught instanceof Error, `Geometry fuzz category ${fuzzCase.name} rejected with a non-Error value.`)
}
durations.sort((left, right) => left - right)
const p95Ms = durations[Math.floor(durations.length * 0.95)]
assert.ok(p95Ms < 10, `Geometry input fuzz p95 exceeded 10 ms: ${p95Ms.toFixed(3)} ms.`)
const report = {
status: 'geometry-input-fuzz-pass',
seed: '0x9e3779b9',
cases: durations.length,
counts,
categories: Object.fromEntries([...categories].sort(([left], [right]) => left.localeCompare(right))),
timing: { p95Ms: Number(p95Ms.toFixed(3)), maxMs: Number(durations.at(-1)?.toFixed(3)) },
}
await writeFile(new URL('../config/qa04-geometry-fuzz-verification.json', import.meta.url), `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify(report, null, 2))