99 lines
5.7 KiB
JavaScript
99 lines
5.7 KiB
JavaScript
import { existsSync, mkdtempSync, rmSync } from 'node:fs'
|
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
import { spawnSync } from 'node:child_process'
|
|
import { join, resolve } from 'node:path'
|
|
import { tmpdir } from 'node:os'
|
|
|
|
const root = resolve(new URL('..', import.meta.url).pathname)
|
|
const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd')
|
|
const sysroot = resolve(root, '.cache/freecad/sysroot')
|
|
const scriptPath = resolve(root, 'scripts/freecad-property-direction-failure.py')
|
|
const outputPath = resolve(root, 'config/freecad-property-direction-failure.json')
|
|
if (!existsSync(executable)) throw new Error(`FreeCAD PropertyDirection executable is missing: ${executable}`)
|
|
const baseEnv = {
|
|
...process.env,
|
|
PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`,
|
|
LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}`,
|
|
}
|
|
const runProbe = ({ mode = 'main', target = outputPath, timeout = 120_000 } = {}) => spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), scriptPath], {
|
|
cwd: root,
|
|
encoding: 'utf8',
|
|
timeout,
|
|
maxBuffer: 32 * 1024 * 1024,
|
|
env: {
|
|
...baseEnv,
|
|
FREECAD_PROPERTY_DIRECTION_FAILURE_OUTPUT: target,
|
|
FREECAD_PROPERTY_DIRECTION_FAILURE_MODE: mode,
|
|
},
|
|
})
|
|
const execution = runProbe({ timeout: Number(process.env.FREECAD_PROPERTY_DIRECTION_FAILURE_TIMEOUT_MS || 120_000) })
|
|
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
|
|
if (execution.error || execution.status !== 0 || !output.includes('FREECAD_PROPERTY_DIRECTION_FAILURE_RESULT=')) throw new Error(`FreeCAD PropertyDirection failure probe failed with status ${execution.status}: ${execution.error?.message || 'process failed'}\n${output.trim()}`)
|
|
const report = JSON.parse(await readFile(outputPath, 'utf8'))
|
|
const isolatedRoot = mkdtempSync(join(tmpdir(), 'freecad-property-direction-'))
|
|
try {
|
|
report.nonFiniteConsumerBoundaries = []
|
|
const isolatedTimeout = Number(process.env.FREECAD_PROPERTY_DIRECTION_NON_FINITE_TIMEOUT_MS || 5_000)
|
|
for (const hostId of ['Mirroring', 'ProjectOnSurface']) {
|
|
for (const valueId of ['nan', 'positive-infinity', 'negative-infinity']) {
|
|
const caseId = `${hostId}-${valueId}`
|
|
const attempts = []
|
|
for (let attempt = 1; attempt <= 2; attempt += 1) {
|
|
const target = join(isolatedRoot, `${caseId}-${attempt}.json`)
|
|
const startedAt = Date.now()
|
|
const isolated = runProbe({ mode: `non-finite:${hostId}:${valueId}`, target, timeout: isolatedTimeout })
|
|
const isolatedOutput = `${isolated.stdout || ''}\n${isolated.stderr || ''}`.trim()
|
|
const timedOut = isolated.error?.code === 'ETIMEDOUT'
|
|
let completedCase = null
|
|
if (!timedOut && isolated.status === 0 && isolatedOutput.includes('FREECAD_PROPERTY_DIRECTION_FAILURE_ISOLATED_RESULT=') && existsSync(target)) {
|
|
completedCase = JSON.parse(await readFile(target, 'utf8')).case
|
|
}
|
|
attempts.push({
|
|
attempt,
|
|
timeoutMs: isolatedTimeout,
|
|
elapsedMs: Date.now() - startedAt,
|
|
timedOut,
|
|
exitStatus: isolated.status,
|
|
signal: isolated.signal,
|
|
errorCode: isolated.error?.code || null,
|
|
progressMarkerObserved: isolatedOutput.includes(`FREECAD_PROPERTY_DIRECTION_PROGRESS=${caseId}`),
|
|
completionMarkerObserved: isolatedOutput.includes('FREECAD_PROPERTY_DIRECTION_FAILURE_ISOLATED_RESULT='),
|
|
diagnostic: isolatedOutput.split('\n').filter((line) => line && !line.startsWith('FreeCAD 0.')).slice(-8),
|
|
completedCase,
|
|
outcome: timedOut ? 'timeout' : completedCase ? 'completed' : 'process-exited-without-completion',
|
|
})
|
|
}
|
|
const outcomes = [...new Set(attempts.map(({ outcome }) => outcome))]
|
|
const stable = outcomes.length === 1 && attempts.every(({ progressMarkerObserved }) => progressMarkerObserved)
|
|
report.nonFiniteConsumerBoundaries.push({
|
|
id: caseId,
|
|
mode: `non-finite:${hostId}:${valueId}`,
|
|
attemptCount: attempts.length,
|
|
attempts,
|
|
stable,
|
|
classification: !stable
|
|
? 'native-nondeterministic'
|
|
: outcomes[0] === 'timeout'
|
|
? 'native-synchronous-consumer-timeout'
|
|
: outcomes[0] === 'process-exited-without-completion'
|
|
? 'native-process-exit-without-probe-completion'
|
|
: 'native-consumer-returned',
|
|
})
|
|
}
|
|
}
|
|
report.status = report.nonFiniteConsumerBoundaries.every(({ stable, classification }) => stable && classification !== 'native-consumer-returned') ? 'pass' : 'fail'
|
|
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
|
|
} finally {
|
|
rmSync(isolatedRoot, { recursive: true, force: true })
|
|
}
|
|
console.log(JSON.stringify({
|
|
status: report.status,
|
|
output: 'config/freecad-property-direction-failure.json',
|
|
acceptedCoercions: report.setterBoundaries.acceptedCoercions.map(({ id, accepted, after }) => ({ id, accepted, after })),
|
|
setterFailures: report.setterBoundaries.failures.map(({ id, exception, valuePreserved }) => ({ id, exception, valuePreserved })),
|
|
consumerFailures: report.consumerFailures.map(({ id, setterException, recomputeResult, afterRecompute }) => ({ id, setterException, recomputeResult, state: afterRecompute.state })),
|
|
nonFiniteConsumerBoundaries: report.nonFiniteConsumerBoundaries.map(({ id, classification, attemptCount, stable }) => ({ id, classification, attemptCount, stable })),
|
|
dependencyFailures: report.dependencyFailures.map(({ id, recomputeResult, afterRecompute }) => ({ id, recomputeResult, state: afterRecompute.state })),
|
|
transactionRestored: report.transaction.restored,
|
|
}, null, 2))
|