Skip to content

Commit d742672

Browse files
Schedule previously failing tests to run first
Fixes #1546. Co-authored-by: Mark Wubben <mark@novemberborn.net>
1 parent 2eebb60 commit d742672

File tree

10 files changed

+146
-2
lines changed

10 files changed

+146
-2
lines changed

lib/api.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const fork = require('./fork');
1818
const serializeError = require('./serialize-error');
1919
const {getApplicableLineNumbers} = require('./line-numbers');
2020
const sharedWorkers = require('./plugin-support/shared-workers');
21+
const scheduler = require('./scheduler');
2122

2223
function resolveModules(modules) {
2324
return arrify(modules).map(name => {
@@ -142,6 +143,8 @@ class Api extends Emittery {
142143
runStatus = new RunStatus(selectedFiles.length, null);
143144
}
144145

146+
selectedFiles = scheduler.failingTestsFirst(selectedFiles, this._getLocalCacheDir(), this.options.cacheEnabled);
147+
145148
const debugWithoutSpecificFile = Boolean(this.options.debug) && !this.options.debug.active && selectedFiles.length !== 1;
146149

147150
await this.emit('run', {
@@ -243,6 +246,7 @@ class Api extends Emittery {
243246

244247
// Allow shared workers to clean up before the run ends.
245248
await Promise.all(deregisteredSharedWorkers);
249+
scheduler.storeFailedTestFiles(runStatus, this.options.cacheEnabled === false ? null : this._createCacheDir());
246250
} catch (error) {
247251
if (error && error.name === 'AggregateError') {
248252
for (const error_ of error) {
@@ -257,14 +261,18 @@ class Api extends Emittery {
257261
return runStatus;
258262
}
259263

264+
_getLocalCacheDir() {
265+
return path.join(this.options.projectDir, 'node_modules', '.cache', 'ava');
266+
}
267+
260268
_createCacheDir() {
261269
if (this._cacheDir) {
262270
return this._cacheDir;
263271
}
264272

265273
const cacheDir = this.options.cacheEnabled === false ?
266274
fs.mkdtempSync(`${tempDir}${path.sep}`) :
267-
path.join(this.options.projectDir, 'node_modules', '.cache', 'ava');
275+
this._getLocalCacheDir();
268276

269277
// Ensure cacheDir exists
270278
fs.mkdirSync(cacheDir, {recursive: true});

lib/run-status.js

+4
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ class RunStatus extends Emittery {
194194
this.pendingTests.get(event.testFile).delete(event.title);
195195
}
196196
}
197+
198+
getFailedTestFiles() {
199+
return [...this.stats.byFile].filter(statByFile => statByFile[1].failedTests).map(statByFile => statByFile[0]);
200+
}
197201
}
198202

199203
module.exports = RunStatus;

lib/scheduler.js

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const writeFileAtomic = require('write-file-atomic');
4+
const isCi = require('./is-ci');
5+
6+
const FILE_NAME_FAILING_TEST = 'failing-test.json';
7+
8+
module.exports.storeFailedTestFiles = (runStatus, cacheDir) => {
9+
if (isCi || !cacheDir) {
10+
return;
11+
}
12+
13+
writeFileAtomic(path.join(cacheDir, FILE_NAME_FAILING_TEST), JSON.stringify(runStatus.getFailedTestFiles()));
14+
};
15+
16+
// Order test-files, so that files with failing tests come first
17+
module.exports.failingTestsFirst = (selectedFiles, cacheDir, cacheEnabled) => {
18+
if (isCi || cacheEnabled === false) {
19+
return selectedFiles;
20+
}
21+
22+
const filePath = path.join(cacheDir, FILE_NAME_FAILING_TEST);
23+
let failedTestFiles;
24+
try {
25+
failedTestFiles = JSON.parse(fs.readFileSync(filePath));
26+
} catch {
27+
return selectedFiles;
28+
}
29+
30+
return [...selectedFiles].sort((f, s) => {
31+
if (failedTestFiles.some(tf => tf === f) && failedTestFiles.some(tf => tf === s)) {
32+
return 0;
33+
}
34+
35+
if (failedTestFiles.some(tf => tf === f)) {
36+
return -1;
37+
}
38+
39+
if (failedTestFiles.some(tf => tf === s)) {
40+
return 1;
41+
}
42+
43+
return 0;
44+
});
45+
};

test-tap/helper/report.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ const run = (type, reporter, {match = [], filter} = {}) => {
108108
failWithoutAssertions: false,
109109
serial: type === 'failFast' || type === 'failFast2',
110110
require: [],
111-
cacheEnabled: true,
111+
cacheEnabled: false,
112112
experiments: {},
113113
match,
114114
providers,

test/helpers/exec.js

+1
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ exports.fixture = async (args, options = {}) => {
119119
const statObject = {title, file: normalizePath(cwd, testFile)};
120120
errors.set(statObject, statusEvent.err);
121121
stats.failed.push(statObject);
122+
logs.set(statObject, statusEvent.logs);
122123
break;
123124
}
124125

test/scheduler/fixtures/1pass.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const test = require('ava');
2+
3+
test('pass', t => {
4+
t.log(Date.now());
5+
t.pass();
6+
});

test/scheduler/fixtures/2fail.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const test = require('ava');
2+
3+
test('fail', t => {
4+
t.log(Date.now());
5+
t.fail();
6+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
files: [
3+
"*.js"
4+
],
5+
cache: false
6+
};

test/scheduler/fixtures/package.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"ava": {
3+
"files": [
4+
"*.js"
5+
]
6+
}
7+
}

test/scheduler/test.js

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
const test = require('@ava/test');
2+
const exec = require('../helpers/exec');
3+
4+
const options = {
5+
// The scheduler only works when not in CI, so trick it into believing it is
6+
// not in CI even when it's being tested by AVA's CI.
7+
env: {AVA_FORCE_CI: 'not-ci'}
8+
};
9+
10+
function getTimestamps(stats) {
11+
return {passed: BigInt(stats.getLogs(stats.passed[0])), failed: BigInt(stats.getLogs(stats.failed[0]))};
12+
}
13+
14+
test.serial('failing tests come first', async t => {
15+
try {
16+
await exec.fixture(['1pass.js', '2fail.js'], options);
17+
} catch {}
18+
19+
try {
20+
await exec.fixture(['-t', '--concurrency=1', '1pass.js', '2fail.js'], options);
21+
} catch (error) {
22+
const timestamps = getTimestamps(error.stats);
23+
t.true(timestamps.failed < timestamps.passed);
24+
}
25+
});
26+
27+
test.serial('scheduler disabled when cache empty', async t => {
28+
await exec.fixture(['reset-cache'], options); // `ava reset-cache` resets the cache but does not run tests.
29+
try {
30+
await exec.fixture(['-t', '--concurrency=1', '1pass.js', '2fail.js'], options);
31+
} catch (error) {
32+
const timestamps = getTimestamps(error.stats);
33+
t.true(timestamps.passed < timestamps.failed);
34+
}
35+
});
36+
37+
test.serial('scheduler disabled when cache disabled', async t => {
38+
try {
39+
await exec.fixture(['1pass.js', '2fail.js'], options);
40+
} catch {}
41+
42+
try {
43+
await exec.fixture(['-t', '--concurrency=1', '--config', 'disabled-cache.cjs', '1pass.js', '2fail.js'], options);
44+
} catch (error) {
45+
const timestamps = getTimestamps(error.stats);
46+
t.true(timestamps.passed < timestamps.failed);
47+
}
48+
});
49+
50+
test.serial('scheduler disabled in CI', async t => {
51+
try {
52+
await exec.fixture(['1pass.js', '2fail.js'], {env: {AVA_FORCE_CI: 'ci'}});
53+
} catch {}
54+
55+
try {
56+
await exec.fixture(['-t', '--concurrency=1', '--config', 'disabled-cache.cjs', '1pass.js', '2fail.js'], options);
57+
} catch (error) {
58+
const timestamps = getTimestamps(error.stats);
59+
t.true(timestamps.passed < timestamps.failed);
60+
}
61+
});

0 commit comments

Comments
 (0)