Skip to content

Commit ada1a4f

Browse files
Allow customized sorting of test files prior to execution
This allows you to better control the distribution of test files across parallel runs. You can also control execution order for regular runs, including on your local machine. Co-authored-by: Mark Wubben <[email protected]>
1 parent 7276f08 commit ada1a4f

23 files changed

+143
-8
lines changed

Diff for: docs/06-configuration.md

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Arguments passed to the CLI will always take precedence over the CLI options con
5858
- `require`: extra modules to require before tests are run. Modules are required in the [worker processes](./01-writing-tests.md#process-isolation)
5959
- `timeout`: Timeouts in AVA behave differently than in other test frameworks. AVA resets a timer after each test, forcing tests to quit if no new test results were received within the specified timeout. This can be used to handle stalled tests. See our [timeout documentation](./07-test-timeouts.md) for more options.
6060
- `nodeArguments`: Configure Node.js arguments used to launch worker processes.
61+
- `sortTestFiles`: A comparator function to sort test files with. Available only when using a `ava.config.*` file. See an example use case [here](recipes/splitting-tests-ci.md).
6162

6263
Note that providing files on the CLI overrides the `files` option.
6364

Diff for: docs/recipes/splitting-tests-ci.md

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Splitting tests in CI
2+
3+
AVA automatically detects whether your CI environment supports parallel builds using [ci-parallel-vars](https://www.npmjs.com/package/ci-parallel-vars). When parallel builds support is detected, AVA sorts the all detected test files by name, and splits them into chunks. Each CI machine is assigned a chunk (subset) of the tests, and then each chunk is run in parallel.
4+
5+
To better distribute the tests across the machines, you can configure a custom comparator function:
6+
7+
**`ava.config.js`:**
8+
9+
```js
10+
import fs from 'node:fs';
11+
12+
// Assuming 'test-data.json' structure is:
13+
// {
14+
// 'tests/test1.js': { order: 1 },
15+
// 'tests/test2.js': { order: 0 }
16+
// }
17+
const testData = JSON.parse(fs.readFileSync('test-data.json', 'utf8'));
18+
19+
export default {
20+
sortTestFiles: (file1, file2) => testData[file1].order - testData[file2].order,
21+
};
22+
```

Diff for: lib/api.js

+9-3
Original file line numberDiff line numberDiff line change
@@ -177,13 +177,19 @@ export default class Api extends Emittery {
177177
const fileCount = selectedFiles.length;
178178

179179
// The files must be in the same order across all runs, so sort them.
180-
selectedFiles = selectedFiles.sort((a, b) => a.localeCompare(b, [], {numeric: true}));
180+
const defaultComparator = (a, b) => a.localeCompare(b, [], {numeric: true});
181+
selectedFiles = selectedFiles.sort(this.options.sortTestFiles || defaultComparator);
181182
selectedFiles = chunkd(selectedFiles, currentIndex, totalRuns);
182183

183184
const currentFileCount = selectedFiles.length;
184185

185186
runStatus = new RunStatus(fileCount, {currentFileCount, currentIndex, totalRuns}, selectionInsights);
186187
} else {
188+
// If a custom sorter was configured, use it.
189+
if (this.options.sortTestFiles) {
190+
selectedFiles = selectedFiles.sort(this.options.sortTestFiles);
191+
}
192+
187193
runStatus = new RunStatus(selectedFiles.length, null, selectionInsights);
188194
}
189195

@@ -261,8 +267,8 @@ export default class Api extends Emittery {
261267
}
262268

263269
const lineNumbers = getApplicableLineNumbers(globs.normalizeFileForMatching(apiOptions.projectDir, file), filter);
264-
// Removing `providers` field because they cannot be transfered to the worker threads.
265-
const {providers, ...forkOptions} = apiOptions;
270+
// Removing `providers` and `sortTestFiles` fields because they cannot be transferred to the worker threads.
271+
const {providers, sortTestFiles, ...forkOptions} = apiOptions;
266272
const options = {
267273
...forkOptions,
268274
providerStates,

Diff for: lib/cli.js

+5
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,10 @@ export default async function loadCli() { // eslint-disable-line complexity
321321
exit('’sources’ has been removed. Use ’ignoredByWatcher’ to provide glob patterns of files that the watcher should ignore.');
322322
}
323323

324+
if (Reflect.has(conf, 'sortTestFiles') && typeof conf.sortTestFiles !== 'function') {
325+
exit('’sortTestFiles’ must be a comparator function.');
326+
}
327+
324328
let projectPackageObject;
325329
try {
326330
projectPackageObject = JSON.parse(fs.readFileSync(path.resolve(projectDir, 'package.json')));
@@ -413,6 +417,7 @@ export default async function loadCli() { // eslint-disable-line complexity
413417
moduleTypes,
414418
nodeArguments,
415419
parallelRuns,
420+
sortTestFiles: conf.sortTestFiles,
416421
projectDir,
417422
providers,
418423
ranFromCli: true,

Diff for: readme.md

+6-5
Original file line numberDiff line numberDiff line change
@@ -139,15 +139,16 @@ We have a growing list of [common pitfalls](docs/08-common-pitfalls.md) you may
139139

140140
### Recipes
141141

142-
- [Shared workers](docs/recipes/shared-workers.md)
143142
- [Test setup](docs/recipes/test-setup.md)
144-
- [Code coverage](docs/recipes/code-coverage.md)
143+
- [TypeScript](docs/recipes/typescript.md)
144+
- [Shared workers](docs/recipes/shared-workers.md)
145145
- [Watch mode](docs/recipes/watch-mode.md)
146-
- [Endpoint testing](docs/recipes/endpoint-testing.md)
147146
- [When to use `t.plan()`](docs/recipes/when-to-use-plan.md)
148-
- [Browser testing](docs/recipes/browser-testing.md)
149-
- [TypeScript](docs/recipes/typescript.md)
150147
- [Passing arguments to your test files](docs/recipes/passing-arguments-to-your-test-files.md)
148+
- [Splitting tests in CI](docs/recipes/splitting-tests-ci.md)
149+
- [Code coverage](docs/recipes/code-coverage.md)
150+
- [Endpoint testing](docs/recipes/endpoint-testing.md)
151+
- [Browser testing](docs/recipes/browser-testing.md)
151152
- [Testing Vue.js components](docs/recipes/vue.md)
152153
- [Debugging tests with Chrome DevTools](docs/recipes/debugging-with-chrome-devtools.md)
153154
- [Debugging tests with VSCode](docs/recipes/debugging-with-vscode.md)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const test = require('../../../../entrypoints/main.cjs');
2+
3+
test('at expected index', t => {
4+
t.is(process.env.CI_NODE_INDEX, '2');
5+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const test = require('../../../../entrypoints/main.cjs');
2+
3+
test('at expected index', t => {
4+
t.is(process.env.CI_NODE_INDEX, '2');
5+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const test = require('../../../../entrypoints/main.cjs');
2+
3+
test('at expected index', t => {
4+
t.is(process.env.CI_NODE_INDEX, '2');
5+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const test = require('../../../../entrypoints/main.cjs');
2+
3+
test('at expected index', t => {
4+
t.is(process.env.CI_NODE_INDEX, '1');
5+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const test = require('../../../../entrypoints/main.cjs');
2+
3+
test('at expected index', t => {
4+
t.is(process.env.CI_NODE_INDEX, '1');
5+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const test = require('../../../../entrypoints/main.cjs');
2+
3+
test('at expected index', t => {
4+
t.is(process.env.CI_NODE_INDEX, '1');
5+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const test = require('../../../../entrypoints/main.cjs');
2+
3+
test('at expected index', t => {
4+
t.is(process.env.CI_NODE_INDEX, '0');
5+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const test = require('../../../../entrypoints/main.cjs');
2+
3+
test('at expected index', t => {
4+
t.is(process.env.CI_NODE_INDEX, '0');
5+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const test = require('../../../../entrypoints/main.cjs');
2+
3+
test('at expected index', t => {
4+
t.is(process.env.CI_NODE_INDEX, '0');
5+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default {
2+
files: ['*.cjs'],
3+
// Descending order
4+
sortTestFiles: (a, b) => b.localeCompare(a, [], {numeric: true}),
5+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"type": "module"
3+
}

Diff for: test-tap/fixture/sort-tests/0.cjs

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const test = require('../../../entrypoints/main.cjs');
2+
3+
test('should run third', t => {
4+
t.pass();
5+
});

Diff for: test-tap/fixture/sort-tests/1.cjs

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const test = require('../../../entrypoints/main.cjs');
2+
3+
test('should run second', t => {
4+
t.pass();
5+
});

Diff for: test-tap/fixture/sort-tests/2.cjs

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const test = require('../../../entrypoints/main.cjs');
2+
3+
test('should run first', t => {
4+
t.pass();
5+
});

Diff for: test-tap/fixture/sort-tests/ava.config.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default {
2+
files: ['*.cjs'],
3+
// Descending order
4+
sortTestFiles: (a, b) => b.localeCompare(a, [], {numeric: true}),
5+
concurrency: 1,
6+
verbose: true,
7+
};

Diff for: test-tap/fixture/sort-tests/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"type": "module"
3+
}

Diff for: test-tap/integration/assorted.js

+8
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,11 @@ test('load .js test files as ESM modules', t => {
152152
t.end();
153153
});
154154
});
155+
156+
test('uses sortTestFiles to sort test files', t => {
157+
execCli([], {dirname: 'fixture/sort-tests'}, (error, stdout) => {
158+
t.error(error);
159+
t.match(stdout, /should run first[\s\S]+?should run second[\s\S]+?should run third/);
160+
t.end();
161+
});
162+
});

Diff for: test-tap/integration/parallel-runs.js

+14
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,17 @@ test('fail when there are no files', t => {
4343
}, error => t.ok(error));
4444
}
4545
});
46+
47+
test('correctly applies custom comparator', t => {
48+
t.plan(3);
49+
for (let i = 0; i < 3; i++) {
50+
execCli([], {
51+
dirname: 'fixture/parallel-runs/custom-comparator',
52+
env: {
53+
AVA_FORCE_CI: 'ci',
54+
CI_NODE_INDEX: String(i),
55+
CI_NODE_TOTAL: '3',
56+
},
57+
}, error => t.error(error));
58+
}
59+
});

0 commit comments

Comments
 (0)