Finished up most documentation
This commit is contained in:
126
spikes/card-harness/scripts/shader-pixel-regression.mjs
Normal file
126
spikes/card-harness/scripts/shader-pixel-regression.mjs
Normal file
@@ -0,0 +1,126 @@
|
||||
// Run: node scripts/shader-pixel-regression.mjs
|
||||
// Optional: SHADER_BROWSER=/path/to/firefox SHADER_TIMEOUT_MS=180000
|
||||
// Chromium: SHADER_BROWSER=/path/to/chrome-headless-shell SHADER_BROWSER_KIND=chromium
|
||||
// Hosts without user namespaces: SHADER_NO_SANDBOX=1 (local test content only).
|
||||
// No automation dependency: the browser POSTs WebGL readback results to a loopback Vite server.
|
||||
// Also checks that prepared packs create no new programs through reveal and restart.
|
||||
import { spawn } from 'node:child_process'
|
||||
import { once } from 'node:events'
|
||||
import { readFile, mkdir, writeFile, rm } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { createServer } from 'vite'
|
||||
import { originalFragment, baselineFragment, referenceCommit, referenceHash } from './shader-wear-reference.mjs'
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const profile = resolve(root, 'scripts', `.shader-browser-${process.pid}`)
|
||||
const timeoutMs = Number(process.env.SHADER_TIMEOUT_MS ?? 180000)
|
||||
if (!Number.isFinite(timeoutMs) || timeoutMs < 1000 || timeoutMs > 600000) {
|
||||
throw new Error('SHADER_TIMEOUT_MS must be between 1000 and 600000')
|
||||
}
|
||||
const fragmentShader = originalFragment(await readFile(resolve(root, 'src/cardMaterial.ts'), 'utf8'))
|
||||
const baselineShader = baselineFragment(root)
|
||||
let resolveResult
|
||||
let rejectResult
|
||||
const result = new Promise((resolve, reject) => {
|
||||
resolveResult = resolve
|
||||
rejectResult = reject
|
||||
})
|
||||
// Attach a handler immediately; startup errors can precede awaiting the result.
|
||||
result.catch(() => {})
|
||||
let server
|
||||
let browser
|
||||
let timer
|
||||
try {
|
||||
await mkdir(profile)
|
||||
await writeFile(resolve(profile, 'user.js'), [
|
||||
'user_pref("webgl.force-enabled", true);',
|
||||
'user_pref("webgl.disabled", false);',
|
||||
'user_pref("browser.shell.checkDefaultBrowser", false);',
|
||||
'user_pref("browser.startup.homepage_override.mstone", "ignore");',
|
||||
'user_pref("datareporting.policy.dataSubmissionEnabled", false);',
|
||||
].join('\n'))
|
||||
server = await createServer({
|
||||
root,
|
||||
configFile: false,
|
||||
cacheDir: resolve(profile, 'vite'),
|
||||
logLevel: 'error',
|
||||
server: { host: '127.0.0.1', port: 0, watch: null },
|
||||
plugins: [{
|
||||
name: 'shader-pixel-regression',
|
||||
configureServer(vite) {
|
||||
vite.middlewares.use((req, res, next) => {
|
||||
if (req.url === '/__shader_reference') {
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.end(JSON.stringify({ fragmentShader, baselineShader }))
|
||||
} else if (req.url === '/__shader_pixels' && req.method === 'POST') {
|
||||
let body = ''
|
||||
req.on('data', (chunk) => {
|
||||
body += chunk
|
||||
if (body.length > 2_000_000) req.destroy()
|
||||
})
|
||||
req.on('end', () => {
|
||||
try {
|
||||
resolveResult(JSON.parse(body))
|
||||
res.end('ok')
|
||||
} catch (error) {
|
||||
rejectResult(error)
|
||||
res.statusCode = 400
|
||||
res.end('Invalid JSON')
|
||||
}
|
||||
})
|
||||
} else if (req.url === '/__shader_test') {
|
||||
res.setHeader('Content-Type', 'text/html')
|
||||
res.end('<!doctype html><title>Shader pixel regression</title><script type="module" src="/scripts/shader-pixel-client.mjs"></script>')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
},
|
||||
}],
|
||||
})
|
||||
await server.listen()
|
||||
const address = server.httpServer.address()
|
||||
const url = `http://127.0.0.1:${address.port}/__shader_test`
|
||||
const probe = await fetch(url)
|
||||
if (!probe.ok) throw new Error(`Local test server failed: ${probe.status}`)
|
||||
console.log(`Shader reference verified against ${referenceCommit} (${referenceHash})`)
|
||||
console.log(`Serving ${url}; browser timeout ${timeoutMs}ms`)
|
||||
timer = setTimeout(() => rejectResult(new Error('Browser pixel regression timed out')), timeoutMs)
|
||||
const browserArgs = process.env.SHADER_BROWSER_KIND === 'chromium' ? [
|
||||
'--headless', `--user-data-dir=${profile}`, '--no-first-run',
|
||||
'--disable-background-networking', '--disable-dev-shm-usage',
|
||||
'--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader', url,
|
||||
] : ['--headless', '--no-remote', '--profile', profile, url]
|
||||
if (process.env.SHADER_BROWSER_KIND === 'chromium' && process.env.SHADER_NO_SANDBOX === '1') {
|
||||
browserArgs.unshift('--no-sandbox')
|
||||
}
|
||||
browser = spawn(process.env.SHADER_BROWSER ?? '/usr/bin/firefox', browserArgs, {
|
||||
env: { ...process.env, TMPDIR: profile, MOZ_HEADLESS: '1' },
|
||||
stdio: ['ignore', 'ignore', 'pipe'],
|
||||
})
|
||||
let browserLog = ''
|
||||
browser.stderr.on('data', (chunk) => { browserLog = (browserLog + chunk).slice(-12000) })
|
||||
browser.once('error', rejectResult)
|
||||
browser.once('exit', (code) => rejectResult(new Error(`Browser exited with ${code}\n${browserLog}`)))
|
||||
let report
|
||||
try {
|
||||
report = await result
|
||||
} catch (error) {
|
||||
console.error(browserLog)
|
||||
throw error
|
||||
}
|
||||
console.log(JSON.stringify({ referenceCommit, referenceHash, ...report }, null, 2))
|
||||
if (!report.passed) process.exitCode = 1
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
if (browser?.pid && browser.exitCode === null && browser.signalCode === null) {
|
||||
const exited = once(browser, 'exit')
|
||||
browser.kill('SIGTERM')
|
||||
const forceKill = setTimeout(() => browser.kill('SIGKILL'), 3000)
|
||||
await exited
|
||||
clearTimeout(forceKill)
|
||||
}
|
||||
await server?.close()
|
||||
await rm(profile, { recursive: true, force: true })
|
||||
}
|
||||
Reference in New Issue
Block a user