From bc0083a34953309376ac0fe6d8a8d88fde103af5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Gra=C5=BCy=C5=84ski?= Date: Thu, 14 Aug 2025 14:09:13 +0200 Subject: [PATCH 1/2] Exit with error code when any of threads exists with non-zero exit code --- lib/cli.js | 37 ++++++++++++++++++++++++++----------- lib/thread.js | 7 +++++++ 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/lib/cli.js b/lib/cli.js index f616e8c..c2519ae 100755 --- a/lib/cli.js +++ b/lib/cli.js @@ -6,7 +6,11 @@ const path = require('path'); const fs = require('fs-extra'); const { settings } = require('./settings'); -const { getTestSuitePaths, distributeTestsByWeight, getMaxPathLenghtFrom } = require('./test-suites'); +const { + getTestSuitePaths, + distributeTestsByWeight, + getMaxPathLenghtFrom +} = require('./test-suites'); const { formatTime, generateWeightsFile, @@ -15,25 +19,29 @@ const { const { executeThread } = require('./thread'); const { resultsPath } = require('./shared-config'); -function cleanResultsPath() { - if(!fs.existsSync(resultsPath)) { - fs.mkdirSync(resultsPath, { recursive: true }) +function cleanResultsPath() { + if (!fs.existsSync(resultsPath)) { + fs.mkdirSync(resultsPath, { recursive: true }); } else { fs.readdir(resultsPath, (err, files) => { - if (err) console.log(err); - for (const file of files) { - fs.unlink(path.join(resultsPath, file), err => { if (err) console.log(err); }); - } - }); - } + if (err) console.log(err); + for (const file of files) { + fs.unlink(path.join(resultsPath, file), (err) => { + if (err) console.log(err); + }); + } + }); } +} async function start() { cleanResultsPath(); const testSuitePaths = await getTestSuitePaths(); const threads = distributeTestsByWeight(testSuitePaths); const start = new Date(); - await Promise.all(threads.map(executeThread)); + const failedResults = ( + await Promise.allSettled(threads.map(executeThread)) + ).filter(({ status }) => status === 'rejected'); const end = new Date(); const timeTaken = end.getTime() - start.getTime(); @@ -117,6 +125,13 @@ async function start() { process.exit(1); } + if (failedResults.length > 0) { + process.stderr.write( + `\x1b[31m${failedResults.length} thread(s) exited with errors\n` + ); + process.exit(1); + } + const timeSaved = totalDuration - timeTaken; console.log( `Total run time: ${totalDuration / 1000}s, executed in: ${ diff --git a/lib/thread.js b/lib/thread.js index 70db2bd..7cbdb49 100644 --- a/lib/thread.js +++ b/lib/thread.js @@ -109,6 +109,13 @@ async function executeThread(thread, index) { process.exit(exitCode); } } + + if (exitCode !== 0) { + console.error(`Thread ${index} failed with exit code: ${exitCode}`); + + reject(exitCode); + } + resolve(timeMap); }); }); From 3b332b5718b2affbc7c8fd47fdaa429021c46d3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Gra=C5=BCy=C5=84ski?= Date: Thu, 14 Aug 2025 14:12:23 +0200 Subject: [PATCH 2/2] Add thread prefix to the printed logs --- lib/thread.js | 49 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/lib/thread.js b/lib/thread.js index 7cbdb49..040981a 100644 --- a/lib/thread.js +++ b/lib/thread.js @@ -12,8 +12,8 @@ function getPackageManager() { const pckManager = isYarn ? 'yarn' : process.platform === 'win32' - ? 'npm.cmd' - : 'npm'; + ? 'npm.cmd' + : 'npm'; return pckManager; } @@ -50,7 +50,7 @@ function createReporterConfigFile(path) { } function createCommandArguments(thread) { - const specFiles = `${thread.list.map(path => globEscape(path)).join(',')}`; + const specFiles = `${thread.list.map((path) => globEscape(path)).join(',')}`; const childOptions = [ 'run', `${settings.script}`, @@ -77,33 +77,66 @@ function createCommandArguments(thread) { async function executeThread(thread, index) { const packageManager = getPackageManager(); const commandArguments = createCommandArguments(thread); + const threadNumber = index + 1; + const logPrefix = `[${threadNumber}/${settings.threadCount}]`; // staggered start (when executed in container with xvfb ends up having a race condition causing intermittent failures) - await sleep((index +1) * 2000); + await sleep(threadNumber * 2000); const timeMap = new Map(); const promise = new Promise((resolve, reject) => { const processOptions = { cwd: process.cwd(), - stdio: 'inherit', + stdio: 'pipe', env: { ...process.env, - CYPRESS_THREAD: (index + 1).toString() + CYPRESS_THREAD: threadNumber.toString() } }; const child = spawn(packageManager, commandArguments, processOptions); + let childStdout = ''; + let childStderr = ''; + + child.stdout.on('data', (chunk) => { + childStdout += chunk; + const lines = childStdout.split('\n'); + while (lines.length > 1) { + console.log(logPrefix, lines.shift()); + } + childStdout = lines.shift(); + }); + + child.stdout.on('end', () => { + console.log(logPrefix, childStdout); + }); + + child.stderr.on('data', (chunk) => { + childStderr += chunk; + const lines = childStderr.split('\n'); + while (lines.length > 1) { + console.error(logPrefix, lines.shift()); + } + childStderr = lines.shift(); + }); + + child.stderr.on('end', () => { + console.error(logPrefix, childStderr); + }); + child.on('exit', (exitCode) => { if (settings.isVerbose) { console.log( - `Thread ${index} likely finished with failure count: ${exitCode}` + logPrefix, + `Thread likely finished with failure count: ${exitCode}` ); } // should preferably exit earlier, but this is simple and better than nothing if (settings.shouldBail) { if (exitCode > 0) { console.error( + logPrefix, 'BAIL set and thread exited with errors, exit early with error' ); process.exit(exitCode); @@ -111,7 +144,7 @@ async function executeThread(thread, index) { } if (exitCode !== 0) { - console.error(`Thread ${index} failed with exit code: ${exitCode}`); + console.error(logPrefix, `Thread failed with exit code: ${exitCode}`); reject(exitCode); }