Skip to content

Commit

Permalink
perf: use queue instead of groups for parallel tests (#886)
Browse files Browse the repository at this point in the history
* perf: use queue instead of group for parallel tests

* refactor: removes unreachable code

* ci: add tests

* chore: force interruption of already started tests when using `failFast`

* refactor: remove unreachable code

* ci: compare without concurrency limit

* ci: rollback concurrency limit

* chore: leave user errors to the user

* chore: leave user errors to the user
  • Loading branch information
wellwelwel authored Dec 10, 2024
1 parent 5a8af21 commit 96b8707
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 43 deletions.
84 changes: 46 additions & 38 deletions src/services/run-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { availableParallelism } from '../polyfills/os.js';
import { hasOnly, hasDescribeOnly, hasItOnly } from '../parsers/get-arg.js';

const cwd = process.cwd();
const failFastError = ` ${format('ℹ').fail()} ${format('failFast').bold()} is enabled`;

if (hasDescribeOnly) deepOptions.push('--only=describe');
else if (hasItOnly) deepOptions.push('--only=it');
Expand Down Expand Up @@ -76,9 +77,7 @@ export const runTests = async (

if (showLogs) {
Write.hr();
Write.log(
` ${format('ℹ').fail()} ${format('failFast').bold()} is enabled`
);
Write.log(failFastError);
}

break;
Expand All @@ -93,53 +92,62 @@ export const runTestsParallel = async (
dir: string,
configs?: Configs
): Promise<boolean> => {
let allPassed = true;
let activeTests = 0;
let resolveDone: (value: boolean) => void;
let rejectDone: (reason?: Error) => void;

const testDir = join(cwd, dir);
const files = await listFiles(testDir, configs);
const filesByConcurrency: string[][] = [];
const concurrencyLimit =
configs?.concurrency ?? Math.max(availableParallelism() - 1, 1);
const concurrencyResults: (boolean | undefined)[][] = [];
const showLogs = !isQuiet(configs);
const concurrency: number = (() => {
const limit =
configs?.concurrency ?? Math.max(availableParallelism() - 1, 1);
return limit <= 0 ? files.length || 1 : limit;
})();

const done = new Promise<boolean>((resolve, reject) => {
resolveDone = resolve;
rejectDone = reject;
});

const runNext = async () => {
if (files.length === 0 && activeTests === 0) {
resolveDone(allPassed);
return;
}

if (concurrencyLimit > 0) {
for (let i = 0; i < files.length; i += concurrencyLimit)
filesByConcurrency.push(files.slice(i, i + concurrencyLimit));
} else filesByConcurrency.push(files);
const filePath = files.shift();
if (!filePath) return;

try {
for (const fileGroup of filesByConcurrency) {
const promises = fileGroup.map(async (filePath) => {
const testPassed = await runTestFile(filePath, configs);
activeTests++;

if (!testPassed) {
++results.fail;
try {
const testPassed = await runTestFile(filePath, configs);

if (configs?.failFast) {
process.exitCode = 1;
if (testPassed) ++results.success;
else {
++results.fail;
allPassed = false;

throw new Error(
` ${format('ℹ').fail()} ${format('failFast').bold()} is enabled`
);
if (configs?.failFast) {
if (showLogs) {
Write.hr();
console.error(failFastError);
Write.hr();
}

return false;
process.exit(1);
}

++results.success;
return true;
});

const concurrency = await Promise.all(promises);
concurrencyResults.push(concurrency);
}
} finally {
activeTests--;
}

return concurrencyResults.every((group) => group.every((result) => result));
} catch (error) {
if (showLogs) {
Write.hr();
error instanceof Error && console.error(error.message);
}
runNext().catch(rejectDone);
};

return false;
}
for (let i = 0; i < concurrency; i++) runNext();

return await done;
};
4 changes: 1 addition & 3 deletions test/e2e/fail-fast.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { getRuntime } from '../../src/parsers/get-runtime.js';

if (isBuild || getRuntime() === 'deno') skip();

describe('Fast Fast', async () => {
describe('Fail Fast', async () => {
await it('Parallel / Concurrent', async () => {
const results = await inspectPoku('', {
cwd: 'test/__fixtures__/e2e/fail-fast/parallel',
Expand All @@ -20,7 +20,6 @@ describe('Fast Fast', async () => {

assert.strictEqual(results.exitCode, 1, 'Failed');
assert.match(results.stderr, /failFast/, 'Fail Fast is enabled');
assert.match(results.stdout, /FAIL 1/, 'Needs to fail 1');
});

await it('Sequential', async () => {
Expand All @@ -35,6 +34,5 @@ describe('Fast Fast', async () => {

assert.strictEqual(results.exitCode, 1, 'Failed');
assert.match(results.stdout, /failFast/, 'Fail Fast is enabled');
assert.match(results.stdout, /FAIL 1/, 'Needs to fail 1');
});
});
24 changes: 24 additions & 0 deletions test/e2e/no-tests-with-unlimited-concurrency.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { test } from '../../src/modules/helpers/test.js';
import { assert } from '../../src/modules/essentials/assert.js';
import { inspectPoku, isBuild } from '../__utils__/capture-cli.test.js';
import { skip } from '../../src/modules/helpers/skip.js';

if (isBuild) {
skip();
}

test('No tests with unlimited concurrency', async () => {
const output = await inspectPoku('--debug --parallel --concurrency=0', {
cwd: 'test/__fixtures__/e2e/no-tests',
});

assert.strictEqual(output.exitCode, 0, 'Exit Code needs to be 0');
assert(
/debug(.+)?:(.+)?true/.test(output.stdout),
'CLI needs to able "debug"'
);
assert(
/concurrency(.+)?:(.+)?0/.test(output.stdout),
'CLI needs to able "concurrency"'
);
});
4 changes: 2 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
"fixtures/sintax/invalid-file.js"
],
"compilerOptions": {
"target": "ES2018",
"lib": ["ES2018"],
"target": "ES2019",
"lib": ["ES2019"],
"module": "CommonJS",
"moduleResolution": "Node",
"outDir": "lib",
Expand Down

0 comments on commit 96b8707

Please sign in to comment.