Files
Web_FreeCAD_Bitbybit/scripts/run-camotics-native-verification.mjs

143 lines
7.3 KiB
JavaScript

import { createHash } from 'node:crypto'
import { execFileSync, spawnSync } from 'node:child_process'
import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const sha256 = (value) => createHash('sha256').update(value).digest('hex')
const run = (command, args, options = {}) => {
const result = spawnSync(command, args, { encoding: 'utf8', ...options })
if (result.error || result.status !== 0) throw new Error(`${command} failed: ${result.error?.message || result.stderr || result.stdout}`)
return result
}
const artifactInfo = async (path) => {
const bytes = await readFile(resolve(root, path))
const metadata = await stat(resolve(root, path))
return { bytes: bytes.byteLength, sha256: sha256(bytes), mode: metadata.mode.toString(8) }
}
const camoticsRevision = execFileSync('git', ['-C', resolve(root, 'CAMotics'), 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim()
const cbangRevision = execFileSync('git', ['-C', resolve(root, '.cache/camotics-native/cbang'), 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim()
const camoticsVersionProbe = run(resolve(root, 'CAMotics/camotics'), ['--version'])
const tplVersionProbe = run(resolve(root, 'CAMotics/tplang'), ['--version'])
const camoticsVersion = `${camoticsVersionProbe.stdout}${camoticsVersionProbe.stderr}`.trim()
const tplVersion = `${tplVersionProbe.stdout}${tplVersionProbe.stderr}`.trim()
if (camoticsVersion !== tplVersion) throw new Error(`CAMotics/TPL version mismatch: ${camoticsVersion}/${tplVersion}`)
const tplTests = run(resolve(root, 'CAMotics/tests/testHarness'), ['--no-color'], { cwd: resolve(root, 'CAMotics/tests/tplTests') })
const passed = Number(tplTests.stdout.match(/^Passed\s+(\d+)/m)?.[1] ?? -1)
const failed = Number(tplTests.stdout.match(/^Failed\s+(\d+)/m)?.[1] ?? -1)
if (passed < 0 || failed < 0) throw new Error('Could not parse CAMotics TPL test output.')
const tplExample = run(resolve(root, 'CAMotics/tplang'), [resolve(root, 'CAMotics/examples/box/box.tpl')])
const tplExampleBytes = Buffer.from(tplExample.stdout)
const guiDir = await mkdtemp(join(tmpdir(), 'camotics-native-gui-'))
let guiSmoke
try {
const gui = spawnSync('xvfb-run', ['-a', '-s', '-screen 0 1280x800x24 +extension GLX', 'bash', '-lc', `
set -u
smoke_dir="$CAMOTICS_SMOKE_DIR"
export XDG_CONFIG_HOME="$smoke_dir/config" XDG_CACHE_HOME="$smoke_dir/cache" XDG_DATA_HOME="$smoke_dir/data" LIBGL_ALWAYS_SOFTWARE=1
mkdir -p "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME" "$XDG_DATA_HOME"
"$CAMOTICS_BINARY" >"$smoke_dir/stdout" 2>"$smoke_dir/stderr" &
pid=$!
found=0
for _ in $(seq 1 100); do
if xdotool search --onlyvisible --name "^CAMotics$" >/dev/null 2>&1; then found=1; break; fi
sleep 0.1
done
if [ "$found" -eq 1 ]; then
wid=$(xdotool search --onlyvisible --name "^CAMotics$" | head -n1)
xwininfo -id "$wid" >"$smoke_dir/xwininfo"
fi
alive=0
kill -0 "$pid" 2>/dev/null && alive=1
kill -TERM "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
printf '%s\n' "$found" >"$smoke_dir/found"
printf '%s\n' "$alive" >"$smoke_dir/alive"
`], {
encoding: 'utf8',
env: { ...process.env, CAMOTICS_SMOKE_DIR: guiDir, CAMOTICS_BINARY: resolve(root, 'CAMotics/camotics') },
timeout: 30_000,
})
if (gui.error || gui.status !== 0) throw new Error(`Xvfb GUI smoke failed: ${gui.error?.message || gui.stderr || gui.stdout}`)
const found = (await readFile(join(guiDir, 'found'), 'utf8')).trim() === '1'
const alive = (await readFile(join(guiDir, 'alive'), 'utf8')).trim() === '1'
const startupStdout = await readFile(join(guiDir, 'stdout'), 'utf8')
const startupStderr = await readFile(join(guiDir, 'stderr'), 'utf8')
const xwininfo = found ? await readFile(join(guiDir, 'xwininfo'), 'utf8') : ''
const numberFrom = (label) => Number(xwininfo.match(new RegExp(`^ ${label}:\\s+(\\d+)`, 'm'))?.[1] ?? -1)
const mapState = xwininfo.match(/^ Map State:\s+(.+)$/m)?.[1]?.trim() ?? ''
guiSmoke = {
status: found && alive && mapState === 'IsViewable' && !/Fatal error|FATAL/i.test(startupStderr) ? 'pass' : 'fail',
processAliveBeforeTerminate: alive,
browserRuntime: false,
window: { title: xwininfo.match(/^xwininfo: Window id: \S+ "([^"]+)"$/m)?.[1] ?? '', width: numberFrom('Width'), height: numberFrom('Height'), depth: numberFrom('Depth'), mapState },
startupBannerSha256: sha256(Buffer.from(startupStdout)),
startupStderrSha256: sha256(Buffer.from(startupStderr)),
}
} finally {
await rm(guiDir, { recursive: true, force: true })
}
if (guiSmoke.status !== 'pass') throw new Error(`CAMotics GUI smoke did not pass: ${JSON.stringify(guiSmoke)}`)
const artifacts = {}
for (const path of ['CAMotics/camotics', 'CAMotics/tplang']) artifacts[path] = await artifactInfo(path)
const cliArtifacts = {}
for (const path of ['CAMotics/camsim', 'CAMotics/gcodetool', 'CAMotics/planner', 'CAMotics/build/camotics.so']) cliArtifacts[path] = await artifactInfo(path)
const cliSmoke = {}
for (const path of ['CAMotics/camsim', 'CAMotics/gcodetool', 'CAMotics/planner']) {
const probe = run(resolve(root, path), ['--help'])
cliSmoke[path] = { exitCode: probe.status, containsUsage: `${probe.stdout}${probe.stderr}`.includes('Usage:') }
}
const config = {
schemaVersion: 2,
status: 'pass',
source: {
path: 'CAMotics',
repository: 'https://github.com/CauldronDevelopmentLLC/CAMotics.git',
revision: camoticsRevision,
version: camoticsVersion,
},
cbang: {
path: '.cache/camotics-native/cbang',
repository: 'https://github.com/CauldronDevelopmentLLC/cbang.git',
revision: cbangRevision,
version: '1.7.2',
v8PointerCompression: false,
},
build: {
mode: 'release-native-qt5-tpl-cli',
compiler: execFileSync('g++', ['--version'], { encoding: 'utf8' }).split('\n')[0],
sconsVersion: execFileSync('scons', ['--version'], { encoding: 'utf8' }).split('\n')[0],
nodeV8PackageVersion: execFileSync('dpkg-query', ['-W', '-f=${Version}', 'libnode-dev'], { encoding: 'utf8' }).trim(),
qtVersion: execFileSync('qmake', ['-query', 'QT_VERSION'], { encoding: 'utf8' }).trim(),
options: ['strict=0', 'cxxstd=c++17', 'v8_compress_pointers=0'],
withGui: true,
withTpl: true,
browserExecutable: false,
},
artifacts,
cliArtifacts,
cliSmoke,
tplTests: { command: 'cd CAMotics/tests/tplTests && ../testHarness --no-color', passed, failed, outputSha256: sha256(Buffer.from(tplTests.stdout)) },
tplExample: { input: 'CAMotics/examples/box/box.tpl', bytes: tplExampleBytes.byteLength, lines: tplExample.stdout.trimEnd().split('\n').length, sha256: sha256(tplExampleBytes) },
guiSmoke,
pipelineBoundary: {
workspaceNativeGuiAvailable: true,
workspaceNativeTplAvailable: true,
workspaceNativeCliAvailable: true,
browserNativeGuiAvailable: false,
browserNativeTplAvailable: false,
nativeRole: 'host-native-qt5-tpl-cli-sidecar',
generationAuthority: 'camotics-source-stage',
canonicalGcodeParser: 'linuxcnc-wasm',
camoticsParsesCanonicalPipelineGcode: false,
order: ['cad', 'opencamlib-wasm', 'camotics-source-stage', 'gcode', 'linuxcnc-wasm-parse-execute'],
},
}
await writeFile(resolve(root, 'config/camotics-native-artifact.json'), `${JSON.stringify(config, null, 2)}\n`)
console.log(JSON.stringify(config, null, 2))