127 lines
5.4 KiB
JavaScript
127 lines
5.4 KiB
JavaScript
// 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 })
|
|
}
|