import * as child_process from 'node:child_process'; import path from 'node:path'; import { stripVTControlCharacters } from 'node:util'; const initialStatusRegex = /Running (\d+) tests/; async function main() { const [runfilesDir, targetName, testArgs] = process.argv.slice(2); const maxShards = 4; const testEntrypoint = path.resolve(runfilesDir, '../', targetName); const testWorkingDir = path.resolve(runfilesDir, 'angular_cli'); const tasks = []; const progress = {}; for (let i = 0; i < maxShards; i++) { tasks.push( spawnTest( 'bash', [testEntrypoint, ...testArgs.split(' ').filter((arg) => arg !== '')], { cwd: testWorkingDir, env: { // Try to construct a pretty hermetic environment, as within Bazel. PATH: process.env.PATH, TEST_TOTAL_SHARDS: maxShards, TEST_SHARD_INDEX: i, E2E_SHARD_TOTAL: process.env.E2E_SHARD_TOTAL, E2E_SHARD_INDEX: process.env.E2E_SHARD_INDEX, FORCE_COLOR: '3', // Needed by `rules_js` BAZEL_BINDIR: '.', }, }, (s) => (progress[i] = s), ), ); } const printUpdate = () => { console.error(`----`); for (const [taskId, status] of Object.entries(progress)) { const durationInMin = (Date.now() - status.startTime) / 1000 / 60; console.error( `Shard #${taskId}: stage ${status.state} | ` + `${status.current}/${status.max} tests completed (${durationInMin.toFixed(2)}min)`, ); } }; const progressInterval = setInterval(printUpdate, 4000); try { const outputs = await Promise.all(tasks); printUpdate(); for (const [idx, text] of outputs.entries()) { console.log(`---------- ${idx} -----------`); console.log(text); } console.error(''); console.error('Done! Passing'); } catch (e) { if (e instanceof TestSpawnError) { console.error(e.output); console.error(e.message); } else if (e instanceof Error) { console.error(e.message, e.stack); } else { console.error(e); } console.error('Tests failed!'); process.exitCode = 1; } finally { clearInterval(progressInterval); } } function spawnTest(cmd, args, options, reportStatus, startTime = Date.now(), testAttempts = 2) { testAttempts -= 1; const testProgressRegex = /Running test[^\(]+\((\d+) of/g; return new Promise((resolve, reject) => { let output = ''; let state = 'setup'; let current = 0; let max = 0; const proc = child_process.spawn(cmd, args, { ...options, stdio: 'pipe' }); const syncStatus = () => reportStatus({ current, max, state, startTime }); const restartTest = () => { console.error(output); console.error(`Test restarted due to failure.`); resolve(spawnTest(cmd, args, options, reportStatus, startTime, testAttempts)); }; const onOutputChange = () => { // Extract initial status (i.e. how many tests there are in this shard) if (initialStatusRegex.test(output) && state === 'setup') { max = Number(output.match(initialStatusRegex)[1]); } if (/Running initializer/.test(output) && state === 'setup') { state = 'initializing'; } if (/Running test/.test(output) && state === 'initializing') { state = 'testing'; } if (state === 'testing') { const oldLastIndex = testProgressRegex.lastIndex; const newMatch = testProgressRegex.exec(stripVTControlCharacters(output))?.[1]; // Do not advance the Regex, or more precisely, reset to index `0`. if (newMatch === undefined) { testProgressRegex.lastIndex = oldLastIndex; } else { current = Number(newMatch); } } syncStatus(); }; proc.stdout.on('data', (data) => { output += data; onOutputChange(); }); proc.stderr.on('data', (data) => { output += data; onOutputChange(); }); proc.on('error', (err) => { syncStatus(); // If this test failed and there are test attempts remaining, re-run. if (testAttempts > 0) { restartTest(); return; } reject(new TestSpawnError(err.message, output)); }); proc.on('close', (code, signal) => { syncStatus(); if (code === 0 && signal === null) { resolve(output); } else { if (testAttempts > 0) { restartTest(); return; } reject( new TestSpawnError(`Command failed with code: ${code} and signal ${signal}`, output), ); } }); // Report initial status, without knowing anything. syncStatus(); }); } class TestSpawnError extends Error { /** @type {string} */ output; constructor(message, output) { super(message); this.output = output; } } try { main(); } catch (e) { console.error(e); process.exitCode = 1; }