import { spawn, execSync } from 'child_process'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { existsSync } from 'fs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const LOCAL_BIN = join(__dirname, '..', 'sitespeed.io', 'bin', 'sitespeed.js'); const REPORTS_DIR = process.env.REPORTS_DIR || join(__dirname, 'reports'); export function runTest(job, onLine) { return new Promise((resolve, reject) => { const outputFolder = join(REPORTS_DIR, job.id); const isDocker = !!process.env.IN_DOCKER; // Log to docker logs (stdout of main process) so it appears in `docker logs` console.log(`[runner] REPORTS_DIR=${REPORTS_DIR}`); console.log(`[runner] outputFolder=${outputFolder}`); const sitespeedArgs = [ job.url, '--browser', job.browser, '-n', String(job.runs), '--outputFolder', outputFolder, '--plugins.add', 'analysisstorer', // not in default set — needed to write pageSummary JSON '--sustainable.enable', '--sustainable.useGreenWebHostingAPI', '--axe.enable', '--coach', ]; if (job.mobile) sitespeedArgs.push('--mobile'); if (isDocker) { sitespeedArgs.push('--browsertime.chrome.args', 'no-sandbox'); sitespeedArgs.push('--browsertime.chrome.args', 'disable-dev-shm-usage'); sitespeedArgs.push('--browsertime.chrome.args', 'disable-gpu'); } const env = { ...process.env }; let child; if (isDocker) { const bin = process.env.SITESPEED_BIN; if (!bin) { return reject(new Error( 'SITESPEED_BIN is not set. Check build logs for "Build-time sitespeed.js found at:"' )); } console.log(`[runner] spawning: node ${bin} ${sitespeedArgs.slice(0,3).join(' ')} ...`); child = spawn('node', [bin, ...sitespeedArgs], { cwd: __dirname, env }); } else { if (!existsSync(LOCAL_BIN)) { return reject(new Error(`Local sitespeed.io not found at ${LOCAL_BIN}`)); } child = spawn('node', [LOCAL_BIN, ...sitespeedArgs], { cwd: __dirname, env }); } const allLines = []; child.stdout.on('data', (data) => { const lines = data.toString().split('\n').filter(Boolean); for (const line of lines) { allLines.push(line); onLine(line); // Mirror INFO/ERROR lines to docker logs too if (line.includes('INFO:') || line.includes('ERROR:') || line.includes('stored in')) { console.log('[sitespeed]', line); } } }); child.stderr.on('data', (data) => { const lines = data.toString().split('\n').filter(Boolean); for (const line of lines) { allLines.push('[stderr] ' + line); onLine('[stderr] ' + line); if (line.includes('ERROR:')) console.error('[sitespeed stderr]', line); } }); child.on('close', (code) => { if (code === 0) { // Log what was actually written so we can debug the parser try { const found = execSync( `find "${outputFolder}" -name "*.json" 2>/dev/null | head -30`, { encoding: 'utf8' } ).trim(); console.log(`[runner] JSON files written:\n${found || '(none found)'}`); } catch {} resolve(outputFolder); } else { const tail = allLines.slice(-20).join('\n'); reject(new Error(`sitespeed.io exited with code ${code}\n${tail}`)); } }); child.on('error', (err) => { reject(new Error(`Failed to spawn process: ${err.message}`)); }); }); }