cypress-parallel-extended
Version:
Reduce up to 40% your Cypress suite execution time parallelizing the test run on the same machine.
196 lines (166 loc) • 5.7 kB
JavaScript
import colors from '@colors/colors/safe.js';
import Table from 'cli-table3';
import fs from 'fs-extra';
import path from 'path';
import { settings } from './settings.js';
import { resultsPath } from './shared-config.js';
import {
distributeTestsByWeight,
getMaxPathLenghtFrom,
getTestSuitePaths
} from './test-suites.js';
import { executeThread } from './thread.js';
import { collectResults, formatTime, generateWeightsFile } from './utility.js';
async function cleanResultsPath() {
try {
const exists = await fs.pathExists(resultsPath);
if (!exists) {
await fs.mkdir(resultsPath, { recursive: true });
return;
}
const files = await fs.readdir(resultsPath);
await Promise.all(
files.map((file) => fs.unlink(path.join(resultsPath, file)))
);
} catch (err) {
console.error('Error cleaning results path:', err);
}
}
function calculateStatistics(resultMaps) {
const timeMap = new Map();
resultMaps.forEach((m) => {
for (const [name, test] of m) {
timeMap.set(name, test);
}
});
return timeMap;
}
function displayStatisticsTable(timeMap, testSuitePaths) {
const table = new Table({
head: ['Spec', 'Time', 'Tests', 'Passing', 'Failing', 'Pending'],
style: { head: ['blue'] },
colWidths: [getMaxPathLenghtFrom(testSuitePaths), 10, 10, 10, 10, 10]
});
let totals = {
tests: 0,
passes: 0,
failures: 0,
duration: 0,
pending: 0
};
const specWeights = {};
for (const [name, suite] of timeMap) {
const nbTests = suite.passes + suite.pending + suite.failures;
totals.duration += suite.duration;
totals.tests += nbTests;
totals.passes += suite.passes;
totals.pending += suite.pending;
totals.failures += suite.failures;
specWeights[name] = { time: suite.duration, weight: 0 };
table.push([
name,
`${formatTime(suite.duration)}`,
nbTests,
suite.passes > 0 ? colors.green(suite.passes) : suite.passes,
suite.failures > 0 ? colors.red(suite.failures) : suite.failures,
suite.pending
]);
}
table.push([
'Results',
`${formatTime(totals.duration)}`,
totals.tests,
totals.passes > 0 ? colors.green(totals.passes) : totals.passes,
totals.failures > 0 ? colors.red(totals.failures) : totals.failures,
totals.pending
]);
console.log(table.toString());
return { totals, specWeights };
}
function validateResults(timeMap, testSuitePaths) {
if (settings.strictMode && timeMap.size !== testSuitePaths.length) {
console.error(
`Test suites found (${testSuitePaths.length}) do not match results (${timeMap.size}).`
);
const missingTestResults = testSuitePaths.filter(
(path) => !timeMap.get(path)
);
console.log(
`Missing results for the following test suites: ${missingTestResults}`
);
process.exit(1);
}
}
function calculateTimeSaved(totalDuration, timeTaken) {
const timeSaved = totalDuration - timeTaken;
console.log(
`Total run time: ${totalDuration / 1000}s, executed in: ${
timeTaken / 1000
}, saved ${timeSaved / 1000} (~${Math.round(
(timeSaved / totalDuration) * 100
)}%)`
);
return timeSaved;
}
function handleFailures(totalFailures) {
if (totalFailures > 0) {
process.stderr.write(`\x1b[31m${totalFailures} test failure(s)\n`);
process.exit(1);
}
}
async function start() {
try {
console.log('Cypress Parallel Extended starting...');
console.log('Environment info:', {
workingDirectory: process.cwd(),
specsDir: settings.testSuitesPath,
threadCount: settings.threadCount,
scriptToRun: settings.script
});
await cleanResultsPath();
const testSuitePaths = await getTestSuitePaths();
if (!testSuitePaths || testSuitePaths.length === 0) {
console.error('ERROR: No test suites found. Please check your configuration:');
console.error(`- Directory/Pattern: ${settings.testSuitesPath}`);
console.error('- The directory might not exist or might not contain test files');
console.error('- Consider using a glob pattern instead (e.g. "$TMP_DIR/**/*.cy.js")');
process.exit(1);
}
const threads = await distributeTestsByWeight(testSuitePaths);
console.log(`Starting execution with ${threads.length} thread(s)`);
const threadInfo = threads.map((thread, index) => ({
threadNumber: index + 1,
specCount: thread.list.length,
specs: settings.isVerbose ? thread.list : undefined
}));
console.log('Thread distribution:', JSON.stringify(threadInfo, null, 2));
if (threads.every(thread => thread.list.length === 0)) {
console.error('WARNING: All threads have 0 test files. Nothing to run.');
process.exit(0);
}
const start = new Date();
await Promise.all(threads.map((thread, index) => {
console.log(`Executing thread ${index + 1} with ${thread.list.length} spec(s)`);
return executeThread(thread, index);
}));
const end = new Date();
const timeTaken = end.getTime() - start.getTime();
const resultMaps = await collectResults();
const timeMap = calculateStatistics([resultMaps]);
const { totals, specWeights } = displayStatisticsTable(
timeMap,
testSuitePaths
);
validateResults(timeMap, testSuitePaths);
calculateTimeSaved(totals.duration, timeTaken);
generateWeightsFile(specWeights, totals.duration, totals.tests * 10);
handleFailures(totals.failures);
} catch (error) {
console.error('Fatal error occurred during execution:');
console.error(error);
console.error(error.stack);
process.exit(1);
}
}
start();