Skip to content

Commit 29cb29a

Browse files
authored
Remove special .only() behavior in watch mode
Remove special .only() behavior in watch mode. This is a historical feature back from when AVA tried to persist "runExclusive" during regular runs. This behavior conflicts with interactive mode, and in any case interactive mode makes it easier to focus on a specific test file or title. Implement --match using the selected flag. This is a newer flag, previously used for line number selection. It's a better choice than overloading the `exclusive` flag used with `.only()`. Together these changes simplify the runner logic, allowing for the change of behavior.
1 parent 36934b2 commit 29cb29a

File tree

7 files changed

+26
-104
lines changed

7 files changed

+26
-104
lines changed

docs/recipes/watch-mode.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,6 @@ Dependency tracking works for `require()` and `import` syntax, as supported by [
4242

4343
Files accessed using the `fs` module are not tracked.
4444

45-
## Watch mode and the `.only` modifier
46-
47-
The [`.only` modifier] disables watch mode's dependency tracking algorithm. When a change is made, all `.only` tests will be rerun, regardless of whether the test depends on the changed file.
48-
4945
## Watch mode and CI
5046

5147
If you run AVA in your CI with watch mode, the execution will exit with an error (`Error : Watch mode is not available in CI, as it prevents AVA from terminating.`). AVA will not run with the `--watch` (`-w`) option in CI, because CI processes should terminate, and with the `--watch` option, AVA will never terminate.

lib/api.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,6 @@ export default class Api extends Emittery {
203203
files: selectedFiles,
204204
matching: apiOptions.match.length > 0,
205205
previousFailures: runtimeOptions.previousFailures ?? 0,
206-
runOnlyExclusive: runtimeOptions.runOnlyExclusive === true,
207206
firstRun: runtimeOptions.firstRun ?? true,
208207
status: runStatus,
209208
});
@@ -272,8 +271,6 @@ export default class Api extends Emittery {
272271
providerStates,
273272
lineNumbers,
274273
recordNewSnapshots: !isCi,
275-
// If we're looking for matches, run every single test process in exclusive-only mode
276-
runOnlyExclusive: apiOptions.match.length > 0 || runtimeOptions.runOnlyExclusive === true,
277274
};
278275

279276
if (runtimeOptions.updateSnapshots) {

lib/runner.js

Lines changed: 23 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import process from 'node:process';
22
import {pathToFileURL} from 'node:url';
33

44
import Emittery from 'emittery';
5-
import {matcher} from 'matcher';
5+
import * as matcher from 'matcher';
66

77
import ContextRef from './context-ref.js';
88
import createChain from './create-chain.js';
@@ -13,6 +13,15 @@ import Runnable from './test.js';
1313
import {waitForReady} from './worker/state.cjs';
1414

1515
const makeFileURL = file => file.startsWith('file://') ? file : pathToFileURL(file).toString();
16+
17+
const isTitleMatch = (title, patterns) => {
18+
if (patterns.length === 0) {
19+
return true;
20+
}
21+
22+
return matcher.isMatch(title, patterns);
23+
};
24+
1625
export default class Runner extends Emittery {
1726
constructor(options = {}) {
1827
super();
@@ -22,10 +31,9 @@ export default class Runner extends Emittery {
2231
this.failWithoutAssertions = options.failWithoutAssertions !== false;
2332
this.file = options.file;
2433
this.checkSelectedByLineNumbers = options.checkSelectedByLineNumbers;
25-
this.match = options.match ?? [];
34+
this.matchPatterns = options.match ?? [];
2635
this.projectDir = options.projectDir;
2736
this.recordNewSnapshots = options.recordNewSnapshots === true;
28-
this.runOnlyExclusive = options.runOnlyExclusive === true;
2937
this.serial = options.serial === true;
3038
this.snapshotDir = options.snapshotDir;
3139
this.updateSnapshots = options.updateSnapshots;
@@ -34,6 +42,7 @@ export default class Runner extends Emittery {
3442
this.boundCompareTestSnapshot = this.compareTestSnapshot.bind(this);
3543
this.boundSkipSnapshot = this.skipSnapshot.bind(this);
3644
this.interrupted = false;
45+
this.runOnlyExclusive = false;
3746

3847
this.nextTaskIndex = 0;
3948
this.tasks = {
@@ -92,9 +101,7 @@ export default class Runner extends Emittery {
92101

93102
const {args, implementation, title} = parseTestArgs(testArgs);
94103

95-
if (this.checkSelectedByLineNumbers) {
96-
metadata.selected = this.checkSelectedByLineNumbers();
97-
}
104+
metadata.selected &&= this.checkSelectedByLineNumbers?.() ?? true;
98105

99106
if (metadata.todo) {
100107
if (implementation) {
@@ -110,10 +117,7 @@ export default class Runner extends Emittery {
110117
}
111118

112119
// --match selects TODO tests.
113-
if (this.match.length > 0 && matcher(title.value, this.match).length === 1) {
114-
metadata.exclusive = true;
115-
this.runOnlyExclusive = true;
116-
}
120+
metadata.selected &&= isTitleMatch(title.value, this.matchPatterns);
117121

118122
this.tasks.todo.push({title: title.value, metadata});
119123
this.emit('stateChange', {
@@ -154,14 +158,10 @@ export default class Runner extends Emittery {
154158
};
155159

156160
if (metadata.type === 'test') {
157-
if (this.match.length > 0) {
158-
// --match overrides .only()
159-
task.metadata.exclusive = matcher(title.value, this.match).length === 1;
160-
}
161-
162-
if (task.metadata.exclusive) {
163-
this.runOnlyExclusive = true;
164-
}
161+
task.metadata.selected &&= isTitleMatch(title.value, this.matchPatterns);
162+
// Unmatched .only() are not selected and won't run. However, runOnlyExclusive can only be true if no titles
163+
// are being matched.
164+
this.runOnlyExclusive ||= this.matchPatterns.length === 0 && task.metadata.exclusive && task.metadata.selected;
165165

166166
this.tasks[metadata.serial ? 'serial' : 'concurrent'].push(task);
167167

@@ -181,6 +181,7 @@ export default class Runner extends Emittery {
181181
serial: false,
182182
exclusive: false,
183183
skipped: false,
184+
selected: true,
184185
todo: false,
185186
failing: false,
186187
callback: false,
@@ -402,16 +403,11 @@ export default class Runner extends Emittery {
402403
return alwaysOk && hooksOk && testOk;
403404
}
404405

405-
async start() { // eslint-disable-line complexity
406+
async start() {
406407
const concurrentTests = [];
407408
const serialTests = [];
408409
for (const task of this.tasks.serial) {
409-
if (this.runOnlyExclusive && !task.metadata.exclusive) {
410-
this.snapshots.skipBlock(task.title, task.metadata.taskIndex);
411-
continue;
412-
}
413-
414-
if (this.checkSelectedByLineNumbers && !task.metadata.selected) {
410+
if (!task.metadata.selected || (this.runOnlyExclusive && !task.metadata.exclusive)) {
415411
this.snapshots.skipBlock(task.title, task.metadata.taskIndex);
416412
continue;
417413
}
@@ -432,12 +428,7 @@ export default class Runner extends Emittery {
432428
}
433429

434430
for (const task of this.tasks.concurrent) {
435-
if (this.runOnlyExclusive && !task.metadata.exclusive) {
436-
this.snapshots.skipBlock(task.title, task.metadata.taskIndex);
437-
continue;
438-
}
439-
440-
if (this.checkSelectedByLineNumbers && !task.metadata.selected) {
431+
if (!task.metadata.selected || (this.runOnlyExclusive && !task.metadata.exclusive)) {
441432
this.snapshots.skipBlock(task.title, task.metadata.taskIndex);
442433
continue;
443434
}
@@ -460,11 +451,7 @@ export default class Runner extends Emittery {
460451
}
461452

462453
for (const task of this.tasks.todo) {
463-
if (this.runOnlyExclusive && !task.metadata.exclusive) {
464-
continue;
465-
}
466-
467-
if (this.checkSelectedByLineNumbers && !task.metadata.selected) {
454+
if (!task.metadata.selected || (this.runOnlyExclusive && !task.metadata.exclusive)) {
468455
continue;
469456
}
470457

lib/watcher.js

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ async function * plan({api, filter, globs, projectDir, providers, stdin, abortSi
7979
}))));
8080

8181
// State tracked for test runs.
82-
const filesWithExclusiveTests = new Set();
8382
const touchedFiles = new Set();
8483
const temporaryFiles = new Set();
8584
const failureCounts = new Map();
@@ -117,17 +116,6 @@ async function * plan({api, filter, globs, projectDir, providers, stdin, abortSi
117116
break;
118117
}
119118

120-
case 'worker-finished': {
121-
const fileStats = status.stats.byFile.get(evt.testFile);
122-
if (fileStats.selectedTests > 0 && fileStats.declaredTests > fileStats.selectedTests) {
123-
filesWithExclusiveTests.add(nodePath.relative(projectDir, evt.testFile));
124-
} else {
125-
filesWithExclusiveTests.delete(nodePath.relative(projectDir, evt.testFile));
126-
}
127-
128-
break;
129-
}
130-
131119
default: {
132120
break;
133121
}
@@ -329,18 +317,6 @@ async function * plan({api, filter, globs, projectDir, providers, stdin, abortSi
329317

330318
// Select the test files to run, and how to run them.
331319
let testFiles = [...uniqueTestFiles];
332-
let runOnlyExclusive = false;
333-
334-
if (testFiles.length > 0) {
335-
const exclusiveFiles = testFiles.filter(path => filesWithExclusiveTests.has(path));
336-
runOnlyExclusive = exclusiveFiles.length !== filesWithExclusiveTests.size;
337-
if (runOnlyExclusive) {
338-
// The test files that previously contained exclusive tests are always
339-
// run, together with the test files.
340-
debug('Running exclusive tests in %o', [...filesWithExclusiveTests]);
341-
testFiles = [...new Set([...filesWithExclusiveTests, ...testFiles])];
342-
}
343-
}
344320

345321
if (filter.length > 0) {
346322
testFiles = applyTestFileFilter({
@@ -355,14 +331,14 @@ async function * plan({api, filter, globs, projectDir, providers, stdin, abortSi
355331
if (nonTestFiles.length > 0) {
356332
debug('Non-test files changed, running all tests');
357333
failureCounts.clear(); // All tests are run, so clear previous failures.
358-
signalChanged({runOnlyExclusive});
334+
signalChanged({});
359335
} else if (testFiles.length > 0) {
360336
// Remove previous failures for tests that will run again.
361337
for (const path of testFiles) {
362338
failureCounts.delete(path);
363339
}
364340

365-
signalChanged({runOnlyExclusive, testFiles});
341+
signalChanged({testFiles});
366342
}
367343

368344
takeCoverageForSelfTests?.();
@@ -383,7 +359,7 @@ async function * plan({api, filter, globs, projectDir, providers, stdin, abortSi
383359

384360
// And finally, the watch loop.
385361
while (abortSignal?.aborted !== true) {
386-
const {testFiles: files = [], runOnlyExclusive = false} = await changed; // eslint-disable-line no-await-in-loop
362+
const {testFiles: files = []} = await changed; // eslint-disable-line no-await-in-loop
387363

388364
if (abortSignal?.aborted) {
389365
break;
@@ -398,7 +374,6 @@ async function * plan({api, filter, globs, projectDir, providers, stdin, abortSi
398374
files: files.map(file => nodePath.join(projectDir, file)),
399375
firstRun, // Value is changed by refresh() so record now.
400376
previousFailures,
401-
runOnlyExclusive,
402377
updateSnapshots, // Value is changed by refresh() so record now.
403378
};
404379
reset(); // Make sure the next run can be triggered.

lib/worker/base.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ const run = async options => {
8181
match: options.match,
8282
projectDir: options.projectDir,
8383
recordNewSnapshots: options.recordNewSnapshots,
84-
runOnlyExclusive: options.runOnlyExclusive,
8584
serial: options.serial,
8685
snapshotDir: options.snapshotDir,
8786
updateSnapshots: options.updateSnapshots,

test-tap/runner.js

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -345,20 +345,6 @@ test('only test', t => {
345345
});
346346
});
347347

348-
test('options.runOnlyExclusive means only exclusive tests are run', t => {
349-
t.plan(1);
350-
351-
return promiseEnd(new Runner({file: import.meta.url, runOnlyExclusive: true}), runner => {
352-
runner.chain('test', () => {
353-
t.fail();
354-
});
355-
356-
runner.chain.only('test 2', () => {
357-
t.pass();
358-
});
359-
});
360-
});
361-
362348
test('options.serial forces all tests to be serial', t => {
363349
t.plan(1);
364350

test/watch-mode/scenarios.js

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -95,24 +95,6 @@ test('runs test file when source it depends on is deleted', withFixture('basic')
9595
});
9696
});
9797

98-
test('once test files containing .only() tests are encountered, always run those, but exclusively the .only tests', withFixture('exclusive'), async (t, fixture) => {
99-
await fixture.watch({
100-
async 1({stats}) {
101-
t.is(stats.failed.length, 2);
102-
t.is(stats.passed.length, 3);
103-
const contents = await this.read('a.test.js');
104-
await this.write('a.test.js', contents.replace('test(\'pass', 'test.only(\'pass'));
105-
return stats.passed.filter(({file}) => file !== 'c.test.js');
106-
},
107-
async 2({stats}, passed) {
108-
t.is(stats.failed.length, 0);
109-
t.is(stats.passed.length, 2);
110-
t.deepEqual(stats.passed, passed);
111-
this.done();
112-
},
113-
});
114-
});
115-
11698
test('filters test files', withFixture('basic'), async (t, fixture) => {
11799
await fixture.watch({
118100
async 1({stats}) {

0 commit comments

Comments
 (0)