Skip to content

Commit c4f607c

Browse files
jamiebuildsnovemberborn
authored andcommitted
Automatically spread test file runs across parallel CI jobs
1 parent dbeebd1 commit c4f607c

18 files changed

+109
-4
lines changed

api.js

+17-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,23 @@ class Api extends Emittery {
8383
// Find all test files.
8484
return new AvaFiles({cwd: apiOptions.resolveTestsFrom, files, extensions: this._allExtensions}).findTestFiles()
8585
.then(files => {
86-
runStatus = new RunStatus(files.length);
86+
if (this.options.parallelRuns) {
87+
// The files must be in the same order across all runs, so sort them.
88+
files = files.sort((a, b) => a.localeCompare(b, [], {numeric: true}));
89+
90+
const {currentIndex, totalRuns} = this.options.parallelRuns;
91+
const fileCount = files.length;
92+
const each = Math.floor(fileCount / totalRuns);
93+
const remainder = fileCount % totalRuns;
94+
95+
const offset = Math.min(currentIndex, remainder) + (currentIndex * each);
96+
const currentFileCount = each + (currentIndex < remainder ? 1 : 0);
97+
98+
files = files.slice(offset, offset + currentFileCount);
99+
runStatus = new RunStatus(fileCount, {currentFileCount, currentIndex, totalRuns});
100+
} else {
101+
runStatus = new RunStatus(files.length, null);
102+
}
87103

88104
const emittedRun = this.emit('run', {
89105
clearLogOnNextRun: runtimeOptions.clearLogOnNextRun === true,

lib/cli.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ exports.run = () => { // eslint-disable-line complexity
155155
exit('The \'source\' option has been renamed. Use \'sources\' instead.');
156156
}
157157

158+
const ciParallelVars = require('ci-parallel-vars');
158159
const Api = require('../api');
159160
const VerboseReporter = require('./reporters/verbose');
160161
const MiniReporter = require('./reporters/mini');
@@ -180,6 +181,12 @@ exports.run = () => { // eslint-disable-line complexity
180181
// Copy resultant cli.flags into conf for use with Api and elsewhere
181182
Object.assign(conf, cli.flags);
182183

184+
let parallelRuns = null;
185+
if (isCi && ciParallelVars) {
186+
const {index: currentIndex, total: totalRuns} = ciParallelVars;
187+
parallelRuns = {currentIndex, totalRuns};
188+
}
189+
183190
const match = arrify(conf.match);
184191
const api = new Api({
185192
failFast: conf.failFast,
@@ -198,7 +205,8 @@ exports.run = () => { // eslint-disable-line complexity
198205
updateSnapshots: conf.updateSnapshots,
199206
snapshotDir: conf.snapshotDir ? path.resolve(projectDir, conf.snapshotDir) : null,
200207
color: conf.color,
201-
workerArgv: cli.flags['--']
208+
workerArgv: cli.flags['--'],
209+
parallelRuns
202210
});
203211

204212
let reporter;

lib/reporters/tap.js

+5
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ class TapReporter {
7676
skipped: this.stats.skippedTests,
7777
todo: this.stats.todoTests
7878
}) + os.EOL);
79+
80+
if (this.stats.parallelRuns) {
81+
const {currentFileCount, currentIndex, totalRuns} = this.stats.parallelRuns;
82+
this.reportStream.write(`# Ran ${currentFileCount} test ${plur('file', currentFileCount)} out of ${this.stats.files} for job ${currentIndex + 1} of ${totalRuns}` + os.EOL + os.EOL);
83+
}
7984
} else {
8085
this.reportStream.write(supertap.finish({
8186
crashed: this.crashCount,

lib/reporters/verbose.js

+6
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,12 @@ class VerboseReporter {
296296

297297
this.lineWriter.writeLine();
298298

299+
if (this.stats.parallelRuns) {
300+
const {currentFileCount, currentIndex, totalRuns} = this.stats.parallelRuns;
301+
this.lineWriter.writeLine(colors.information(`Ran ${currentFileCount} test ${plur('file', currentFileCount)} out of ${this.stats.files} for job ${currentIndex + 1} of ${totalRuns}`));
302+
this.lineWriter.writeLine();
303+
}
304+
299305
let firstLinePostfix = this.watching ?
300306
' ' + chalk.gray.dim('[' + new Date().toLocaleTimeString('en-US', {hour12: false}) + ']') :
301307
'';

lib/run-status.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const cloneDeep = require('lodash.clonedeep');
33
const Emittery = require('./emittery');
44

55
class RunStatus extends Emittery {
6-
constructor(files) {
6+
constructor(files, parallelRuns) {
77
super();
88

99
this.stats = {
@@ -13,6 +13,7 @@ class RunStatus extends Emittery {
1313
failedTests: 0,
1414
failedWorkers: 0,
1515
files,
16+
parallelRuns,
1617
finishedWorkers: 0,
1718
internalErrors: 0,
1819
remainingTests: 0,

package-lock.json

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"bluebird": "^3.5.1",
7878
"chalk": "^2.4.1",
7979
"chokidar": "^2.0.3",
80+
"ci-parallel-vars": "^1.0.0",
8081
"clean-stack": "^1.1.1",
8182
"clean-yaml-object": "^0.1.0",
8283
"cli-cursor": "^2.1.0",

profile.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ const reporter = new VerboseReporter({
9696
watching: false
9797
});
9898

99-
const runStatus = new RunStatus([file]);
99+
const runStatus = new RunStatus(1, null);
100100
runStatus.observeWorker({
101101
file,
102102
onStateChange(listener) {

readme.md

+5
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ Translations: [Español](https://github.com/avajs/ava-docs/blob/master/es_ES/rea
5353
- [Async function support](#async-function-support)
5454
- [Observable support](#observable-support)
5555
- [Enhanced assertion messages](#enhanced-assertion-messages)
56+
- [Automatic parallel test runs in CI](#parallel-runs-in-ci)
5657
- [TAP reporter](#tap-reporter)
5758
- [Automatic migration from other test runners](https://github.com/avajs/ava-codemods#migrating-to-ava)
5859

@@ -819,6 +820,10 @@ $ ava --timeout=2m # 2 minutes
819820
$ ava --timeout=100 # 100 milliseconds
820821
```
821822

823+
### Parallel runs in CI
824+
825+
AVA automatically detects whether your CI environment supports parallel builds. Each build will run a subset of all test files, while still making sure all tests get executed. See the [`ci-parallel-vars`](https://www.npmjs.com/package/ci-parallel-vars) package for a list of supported CI environments.
826+
822827
## API
823828

824829
### `test([title], implementation)`

test/fixture/parallel-runs/10.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import test from '../../..';
2+
3+
test('at expected index', t => {
4+
t.is(process.env.CI_NODE_INDEX, '1');
5+
});

test/fixture/parallel-runs/2.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import test from '../../..';
2+
3+
test('at expected index', t => {
4+
t.is(process.env.CI_NODE_INDEX, '0');
5+
});

test/fixture/parallel-runs/2a.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import test from '../../..';
2+
3+
test('at expected index', t => {
4+
t.is(process.env.CI_NODE_INDEX, '0');
5+
});

test/fixture/parallel-runs/9.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import test from '../../..';
2+
3+
test('at expected index', t => {
4+
t.is(process.env.CI_NODE_INDEX, '0');
5+
});

test/fixture/parallel-runs/Ab.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/* eslint-disable unicorn/filename-case */
2+
import test from '../../..';
3+
4+
test('at expected index', t => {
5+
t.is(process.env.CI_NODE_INDEX, '1');
6+
});

test/fixture/parallel-runs/a.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import test from '../../..';
2+
3+
test('at expected index', t => {
4+
t.is(process.env.CI_NODE_INDEX, '1');
5+
});

test/fixture/parallel-runs/b.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import test from '../../..';
2+
3+
test('at expected index', t => {
4+
t.is(process.env.CI_NODE_INDEX, '2');
5+
});

test/fixture/parallel-runs/c.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import test from '../../..';
2+
3+
test('at expected index', t => {
4+
t.is(process.env.CI_NODE_INDEX, '2');
5+
});

test/integration/parallel-runs.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
'use strict';
2+
const test = require('tap').test;
3+
const {execCli} = require('../helper/cli');
4+
5+
test('correctly distributes the test files', t => {
6+
t.plan(3);
7+
for (let i = 0; i < 3; i++) {
8+
execCli('*.js', {
9+
dirname: 'fixture/parallel-runs',
10+
env: {
11+
CI: '1',
12+
CI_NODE_INDEX: String(i),
13+
CI_NODE_TOTAL: '3'
14+
}
15+
}, err => t.ifError(err));
16+
}
17+
});

0 commit comments

Comments
 (0)