feat: establish reproducible FreeCAD web compatibility baseline
This commit is contained in:
99
scripts/run-browser-occt-harness.mjs
Normal file
99
scripts/run-browser-occt-harness.mjs
Normal file
@@ -0,0 +1,99 @@
|
||||
import { createServer } from 'node:http'
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
||||
import { createReadStream } from 'node:fs'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { resolve, extname, join, normalize } from 'node:path'
|
||||
import { spawn } from 'node:child_process'
|
||||
import { createChromeProfile, removeChromeProfile } from './chrome-profile.mjs'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const reportDir = resolve(root, '.cache/freecad/browser-occt')
|
||||
await mkdir(reportDir, { recursive: true })
|
||||
const contentTypes = { '.html': 'text/html', '.js': 'text/javascript', '.wasm': 'application/wasm', '.json': 'application/json' }
|
||||
const reportPromises = new Map()
|
||||
const reportResolvers = new Map()
|
||||
const waitForReport = (browserId) => {
|
||||
if (reportPromises.has(browserId)) return reportPromises.get(browserId)
|
||||
const promise = new Promise((resolveReport, reject) => { reportResolvers.set(browserId, { resolve: resolveReport, reject }) })
|
||||
reportPromises.set(browserId, promise)
|
||||
return promise
|
||||
}
|
||||
const server = createServer(async (request, response) => {
|
||||
response.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
|
||||
response.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
|
||||
response.setHeader('Cross-Origin-Resource-Policy', 'same-origin')
|
||||
if (request.method === 'POST' && request.url?.startsWith('/__occt-report')) {
|
||||
let body = ''
|
||||
request.setEncoding('utf8')
|
||||
request.on('data', (chunk) => { body += chunk })
|
||||
request.on('end', async () => {
|
||||
try {
|
||||
const report = JSON.parse(body)
|
||||
const browserId = report.browserId || new URL(request.url, 'http://127.0.0.1').searchParams.get('browser') || 'unknown'
|
||||
await writeFile(resolve(reportDir, `${browserId}.json`), `${JSON.stringify(report, null, 2)}\n`)
|
||||
reportResolvers.get(browserId)?.resolve(report)
|
||||
response.writeHead(204)
|
||||
response.end()
|
||||
} catch (error) {
|
||||
response.writeHead(400)
|
||||
response.end(String(error))
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
const pathname = decodeURIComponent((request.url || '/').split('?')[0])
|
||||
const relative = pathname === '/occt-browser-harness.html' ? 'public/occt-browser-harness.html' : pathname.startsWith('/native/') ? `public${pathname}` : null
|
||||
if (!relative) { response.writeHead(404); response.end('not found'); return }
|
||||
const file = normalize(join(root, relative))
|
||||
if (!file.startsWith(resolve(root, 'public')) || !existsSync(file)) { response.writeHead(404); response.end('not found'); return }
|
||||
response.setHeader('content-type', contentTypes[extname(file)] || 'application/octet-stream')
|
||||
createReadStream(file).pipe(response)
|
||||
})
|
||||
await new Promise((resolveServer) => server.listen(0, '127.0.0.1', resolveServer))
|
||||
const port = server.address().port
|
||||
|
||||
const runBrowser = (browserId, executable, args) => new Promise((resolveRun) => {
|
||||
if (!executable || !existsSync(executable)) { resolveRun({ browserId, status: 'unavailable', executable }); return }
|
||||
const reportPromise = waitForReport(browserId)
|
||||
const child = spawn(executable, [...args, `http://127.0.0.1:${port}/occt-browser-harness.html?browser=${browserId}`], { cwd: root, stdio: ['ignore', 'pipe', 'pipe'] })
|
||||
let stdout = ''
|
||||
child.stdout.on('data', (chunk) => { stdout += String(chunk) })
|
||||
let stderr = ''
|
||||
child.stderr.on('data', (chunk) => { stderr += String(chunk) })
|
||||
let settled = false
|
||||
const finish = (result) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
clearTimeout(timer)
|
||||
resolveRun({ ...result, stdout: stdout.slice(-4000), stderr: stderr.slice(-2000) })
|
||||
}
|
||||
const timer = setTimeout(() => { child.kill('SIGTERM'); finish({ browserId, status: 'timeout' }) }, 240000)
|
||||
reportPromise.then((report) => {
|
||||
if (settled) return
|
||||
child.kill('SIGTERM')
|
||||
finish({ browserId, status: report.status || 'pass', report, exitCode: child.exitCode, signal: null })
|
||||
}).catch(() => {})
|
||||
child.on('close', async (code, signal) => {
|
||||
if (!settled) {
|
||||
try {
|
||||
const report = await Promise.race([reportPromise, new Promise((_, reject) => setTimeout(() => reject(new Error('report timeout')), 5000))])
|
||||
finish({ browserId, status: report.status || 'pass', report, exitCode: code, signal })
|
||||
} catch {
|
||||
finish({ browserId, status: 'failed', exitCode: code, signal })
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const results = []
|
||||
const chromeProfile = await createChromeProfile('occt')
|
||||
try {
|
||||
results.push(await runBrowser('chrome', process.env.CHROME_BIN || '/home/mes123456/.local/bin/google-chrome', ['--headless=new', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage', '--enable-logging=stderr', '--v=1', `--user-data-dir=${chromeProfile}`]))
|
||||
} finally {
|
||||
await removeChromeProfile(chromeProfile)
|
||||
}
|
||||
await new Promise((resolveServer) => server.close(resolveServer))
|
||||
const verification = { schemaVersion: 1, generatedAt: new Date().toISOString(), headers: { coop: 'same-origin', coep: 'require-corp' }, browsers: results, status: results.every((result) => result.status === 'pass') ? 'pass' : 'partial' }
|
||||
await writeFile(resolve(root, 'config/occt-browser-verification.json'), `${JSON.stringify(verification, null, 2)}\n`)
|
||||
console.log(JSON.stringify(verification, null, 2))
|
||||
if (results.some((result) => result.status !== 'pass')) process.exit(1)
|
||||
Reference in New Issue
Block a user