2026-04-07 11:05:05 +02:00
|
|
|
import { spawn, execSync } from 'child_process';
|
2026-04-06 19:36:13 +02:00
|
|
|
import { join, dirname } from 'path';
|
|
|
|
|
import { fileURLToPath } from 'url';
|
2026-04-06 19:58:17 +02:00
|
|
|
import { existsSync } from 'fs';
|
2026-04-06 19:36:13 +02:00
|
|
|
|
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
2026-04-06 20:05:50 +02:00
|
|
|
const LOCAL_BIN = join(__dirname, '..', 'sitespeed.io', 'bin', 'sitespeed.js');
|
2026-04-07 10:49:20 +02:00
|
|
|
const REPORTS_DIR = process.env.REPORTS_DIR || join(__dirname, 'reports');
|
2026-04-06 19:36:13 +02:00
|
|
|
|
|
|
|
|
export function runTest(job, onLine) {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
2026-04-07 10:49:20 +02:00
|
|
|
const outputFolder = join(REPORTS_DIR, job.id);
|
2026-04-06 20:05:50 +02:00
|
|
|
const isDocker = !!process.env.IN_DOCKER;
|
2026-04-06 19:58:17 +02:00
|
|
|
|
2026-04-07 11:05:05 +02:00
|
|
|
// 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}`);
|
|
|
|
|
|
2026-04-06 19:58:17 +02:00
|
|
|
const sitespeedArgs = [
|
2026-04-06 19:36:13 +02:00
|
|
|
job.url,
|
|
|
|
|
'--browser', job.browser,
|
|
|
|
|
'-n', String(job.runs),
|
|
|
|
|
'--outputFolder', outputFolder,
|
|
|
|
|
'--json',
|
|
|
|
|
'--sustainable.enable',
|
2026-04-07 10:44:14 +02:00
|
|
|
'--sustainable.useGreenWebHostingAPI',
|
2026-04-06 19:36:13 +02:00
|
|
|
'--axe.enable',
|
|
|
|
|
'--coach',
|
|
|
|
|
];
|
|
|
|
|
|
2026-04-06 19:58:17 +02:00
|
|
|
if (job.mobile) sitespeedArgs.push('--mobile');
|
2026-04-06 19:36:13 +02:00
|
|
|
|
2026-04-06 20:05:50 +02:00
|
|
|
if (isDocker) {
|
2026-04-06 19:58:17 +02:00
|
|
|
sitespeedArgs.push('--browsertime.chrome.args', 'no-sandbox');
|
|
|
|
|
sitespeedArgs.push('--browsertime.chrome.args', 'disable-dev-shm-usage');
|
|
|
|
|
sitespeedArgs.push('--browsertime.chrome.args', 'disable-gpu');
|
2026-04-06 19:36:13 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-07 10:13:08 +02:00
|
|
|
const env = { ...process.env };
|
2026-04-06 19:58:17 +02:00
|
|
|
|
2026-04-06 20:05:50 +02:00
|
|
|
let child;
|
|
|
|
|
|
|
|
|
|
if (isDocker) {
|
2026-04-06 20:09:29 +02:00
|
|
|
const bin = process.env.SITESPEED_BIN;
|
|
|
|
|
if (!bin) {
|
|
|
|
|
return reject(new Error(
|
2026-04-07 11:05:05 +02:00
|
|
|
'SITESPEED_BIN is not set. Check build logs for "Build-time sitespeed.js found at:"'
|
2026-04-06 20:09:29 +02:00
|
|
|
));
|
|
|
|
|
}
|
2026-04-07 11:05:05 +02:00
|
|
|
console.log(`[runner] spawning: node ${bin} ${sitespeedArgs.slice(0,3).join(' ')} ...`);
|
2026-04-06 20:09:29 +02:00
|
|
|
child = spawn('node', [bin, ...sitespeedArgs], { cwd: __dirname, env });
|
2026-04-06 20:05:50 +02:00
|
|
|
} else {
|
|
|
|
|
if (!existsSync(LOCAL_BIN)) {
|
2026-04-07 11:05:05 +02:00
|
|
|
return reject(new Error(`Local sitespeed.io not found at ${LOCAL_BIN}`));
|
2026-04-06 20:05:50 +02:00
|
|
|
}
|
|
|
|
|
child = spawn('node', [LOCAL_BIN, ...sitespeedArgs], { cwd: __dirname, env });
|
|
|
|
|
}
|
2026-04-06 19:58:17 +02:00
|
|
|
|
|
|
|
|
const allLines = [];
|
2026-04-06 19:36:13 +02:00
|
|
|
|
|
|
|
|
child.stdout.on('data', (data) => {
|
|
|
|
|
const lines = data.toString().split('\n').filter(Boolean);
|
2026-04-07 11:05:05 +02:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-06 19:36:13 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
child.stderr.on('data', (data) => {
|
|
|
|
|
const lines = data.toString().split('\n').filter(Boolean);
|
2026-04-07 11:05:05 +02:00
|
|
|
for (const line of lines) {
|
|
|
|
|
allLines.push('[stderr] ' + line);
|
|
|
|
|
onLine('[stderr] ' + line);
|
|
|
|
|
if (line.includes('ERROR:')) console.error('[sitespeed stderr]', line);
|
|
|
|
|
}
|
2026-04-06 19:36:13 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
child.on('close', (code) => {
|
|
|
|
|
if (code === 0) {
|
2026-04-07 11:05:05 +02:00
|
|
|
// 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 {}
|
2026-04-06 19:36:13 +02:00
|
|
|
resolve(outputFolder);
|
|
|
|
|
} else {
|
2026-04-06 19:58:17 +02:00
|
|
|
const tail = allLines.slice(-20).join('\n');
|
|
|
|
|
reject(new Error(`sitespeed.io exited with code ${code}\n${tail}`));
|
2026-04-06 19:36:13 +02:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
child.on('error', (err) => {
|
2026-04-06 20:05:50 +02:00
|
|
|
reject(new Error(`Failed to spawn process: ${err.message}`));
|
2026-04-06 19:36:13 +02:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|