ava 6.3.0 → 6.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/entrypoints/main.d.cts +4 -4
- package/entrypoints/main.d.mts +4 -4
- package/lib/api.js +18 -16
- package/lib/assert.js +2 -2
- package/lib/chalk.js +1 -1
- package/lib/cli.js +9 -6
- package/lib/fork.js +5 -0
- package/lib/line-numbers.js +15 -19
- package/lib/reporters/default.js +12 -7
- package/lib/runner.js +31 -44
- package/lib/snapshot-manager.js +15 -19
- package/lib/test.js +7 -5
- package/lib/watcher.js +318 -83
- package/lib/worker/base.js +9 -1
- package/lib/worker/channel.cjs +2 -0
- package/lib/worker/main.cjs +1 -1
- package/lib/worker/plugin.cjs +1 -1
- package/package.json +20 -17
- package/types/assertions.d.cts +1 -1
- package/types/shared-worker.d.cts +1 -1
package/entrypoints/main.d.cts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type {TestFn} from '../types/test-fn.cjs';
|
|
2
2
|
|
|
3
|
-
export * from '../types/assertions.cjs';
|
|
4
|
-
export * from '../types/try-fn.cjs';
|
|
5
|
-
export * from '../types/test-fn.cjs';
|
|
6
|
-
export * from '../types/subscribable.cjs';
|
|
3
|
+
export type * from '../types/assertions.cjs';
|
|
4
|
+
export type * from '../types/try-fn.cjs';
|
|
5
|
+
export type * from '../types/test-fn.cjs';
|
|
6
|
+
export type * from '../types/subscribable.cjs';
|
|
7
7
|
|
|
8
8
|
/** Call to declare a test, or chain to declare hooks or test modifiers */
|
|
9
9
|
declare const test: TestFn;
|
package/entrypoints/main.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type {TestFn} from '../types/test-fn.cjs';
|
|
2
2
|
|
|
3
|
-
export * from '../types/assertions.cjs';
|
|
4
|
-
export * from '../types/try-fn.cjs';
|
|
5
|
-
export * from '../types/test-fn.cjs';
|
|
6
|
-
export * from '../types/subscribable.cjs';
|
|
3
|
+
export type * from '../types/assertions.cjs';
|
|
4
|
+
export type * from '../types/try-fn.cjs';
|
|
5
|
+
export type * from '../types/test-fn.cjs';
|
|
6
|
+
export type * from '../types/subscribable.cjs';
|
|
7
7
|
|
|
8
8
|
/** Call to declare a test, or chain to declare hooks or test modifiers */
|
|
9
9
|
declare const test: TestFn;
|
package/lib/api.js
CHANGED
|
@@ -88,7 +88,7 @@ export default class Api extends Emittery {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
async run({files: selectedFiles = [], filter = [], runtimeOptions = {}} = {}) { // eslint-disable-line complexity
|
|
91
|
+
async run({files: selectedFiles = [], filter = [], runtimeOptions = {}, testFileSelector} = {}) { // eslint-disable-line complexity
|
|
92
92
|
let setupOrGlobError;
|
|
93
93
|
|
|
94
94
|
const apiOptions = this.options;
|
|
@@ -149,13 +149,17 @@ export default class Api extends Emittery {
|
|
|
149
149
|
let testFiles;
|
|
150
150
|
try {
|
|
151
151
|
testFiles = await globs.findTests({cwd: this.options.projectDir, ...apiOptions.globs});
|
|
152
|
-
if (
|
|
153
|
-
selectedFiles =
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
152
|
+
if (typeof testFileSelector === 'function') {
|
|
153
|
+
selectedFiles = testFileSelector(testFiles, selectedFiles);
|
|
154
|
+
} else if (selectedFiles.length === 0) {
|
|
155
|
+
selectedFiles = filter.length === 0
|
|
156
|
+
? testFiles
|
|
157
|
+
: globs.applyTestFileFilter({
|
|
158
|
+
cwd: this.options.projectDir,
|
|
159
|
+
filter: filter.map(({pattern}) => pattern),
|
|
160
|
+
providers,
|
|
161
|
+
testFiles,
|
|
162
|
+
});
|
|
159
163
|
}
|
|
160
164
|
} catch (error) {
|
|
161
165
|
selectedFiles = [];
|
|
@@ -163,7 +167,7 @@ export default class Api extends Emittery {
|
|
|
163
167
|
}
|
|
164
168
|
|
|
165
169
|
const selectionInsights = {
|
|
166
|
-
filter,
|
|
170
|
+
filter: selectedFiles.appliedFilters ?? filter,
|
|
167
171
|
ignoredFilterPatternFiles: selectedFiles.ignoredFilterPatternFiles ?? [],
|
|
168
172
|
testFileCount: testFiles.length,
|
|
169
173
|
selectionCount: selectedFiles.length,
|
|
@@ -201,9 +205,8 @@ export default class Api extends Emittery {
|
|
|
201
205
|
failFastEnabled: failFast,
|
|
202
206
|
filePathPrefix: getFilePathPrefix(selectedFiles),
|
|
203
207
|
files: selectedFiles,
|
|
204
|
-
matching: apiOptions.match.length > 0,
|
|
205
|
-
previousFailures: runtimeOptions.
|
|
206
|
-
runOnlyExclusive: runtimeOptions.runOnlyExclusive === true,
|
|
208
|
+
matching: apiOptions.match.length > 0 || runtimeOptions.interactiveMatchPattern !== undefined,
|
|
209
|
+
previousFailures: runtimeOptions.countPreviousFailures?.() ?? 0,
|
|
207
210
|
firstRun: runtimeOptions.firstRun ?? true,
|
|
208
211
|
status: runStatus,
|
|
209
212
|
});
|
|
@@ -266,14 +269,13 @@ export default class Api extends Emittery {
|
|
|
266
269
|
|
|
267
270
|
const lineNumbers = getApplicableLineNumbers(globs.normalizeFileForMatching(apiOptions.projectDir, file), filter);
|
|
268
271
|
// Removing `providers` and `sortTestFiles` fields because they cannot be transferred to the worker threads.
|
|
269
|
-
const {providers, sortTestFiles, ...forkOptions} = apiOptions;
|
|
272
|
+
const {providers, sortTestFiles, match, ...forkOptions} = apiOptions;
|
|
270
273
|
const options = {
|
|
271
274
|
...forkOptions,
|
|
272
275
|
providerStates,
|
|
273
276
|
lineNumbers,
|
|
274
277
|
recordNewSnapshots: !isCi,
|
|
275
|
-
|
|
276
|
-
runOnlyExclusive: apiOptions.match.length > 0 || runtimeOptions.runOnlyExclusive === true,
|
|
278
|
+
match: runtimeOptions.interactiveMatchPattern === undefined ? match : [...match, runtimeOptions.interactiveMatchPattern],
|
|
277
279
|
};
|
|
278
280
|
|
|
279
281
|
if (runtimeOptions.updateSnapshots) {
|
|
@@ -291,7 +293,7 @@ export default class Api extends Emittery {
|
|
|
291
293
|
deregisteredSharedWorkers.push(observeWorkerProcess(worker, runStatus));
|
|
292
294
|
|
|
293
295
|
pendingWorkers.add(worker);
|
|
294
|
-
worker.promise.then(() => {
|
|
296
|
+
worker.promise.then(() => { // eslint-disable-line promise/prefer-await-to-then
|
|
295
297
|
pendingWorkers.delete(worker);
|
|
296
298
|
});
|
|
297
299
|
timeoutTrigger.debounce();
|
package/lib/assert.js
CHANGED
|
@@ -490,7 +490,7 @@ export class Assertions {
|
|
|
490
490
|
// Record the stack before it gets lost in the promise chain.
|
|
491
491
|
const assertionStack = getAssertionStack();
|
|
492
492
|
// Handle "promise like" objects by casting to a real Promise.
|
|
493
|
-
const intermediate = Promise.resolve(promise).then(value => { // eslint-disable-line promise/prefer-await-to-then
|
|
493
|
+
const intermediate = Promise.resolve(promise).then(value => { // eslint-disable-line promise/prefer-catch, promise/prefer-await-to-then
|
|
494
494
|
throw failPending(new AssertionError(message, {
|
|
495
495
|
assertion: 't.throwsAsync()',
|
|
496
496
|
assertionStack,
|
|
@@ -592,7 +592,7 @@ export class Assertions {
|
|
|
592
592
|
// Create an error object to record the stack before it gets lost in the promise chain.
|
|
593
593
|
const assertionStack = getAssertionStack();
|
|
594
594
|
// Handle "promise like" objects by casting to a real Promise.
|
|
595
|
-
const intermediate = Promise.resolve(promise).then(noop, error => { // eslint-disable-line promise/prefer-await-to-then
|
|
595
|
+
const intermediate = Promise.resolve(promise).then(noop, error => { // eslint-disable-line promise/prefer-catch, promise/prefer-await-to-then
|
|
596
596
|
throw failPending(new AssertionError(message, {
|
|
597
597
|
assertion: 't.notThrowsAsync()',
|
|
598
598
|
assertionStack,
|
package/lib/chalk.js
CHANGED
package/lib/cli.js
CHANGED
|
@@ -100,9 +100,7 @@ export default async function loadCli() { // eslint-disable-line complexity
|
|
|
100
100
|
const {argv: {config: configFile}} = yargs(hideBin(process.argv)).help(false).version(false);
|
|
101
101
|
const loaded = await loadConfig({configFile});
|
|
102
102
|
if (loaded.unsupportedFiles.length > 0) {
|
|
103
|
-
console.log(chalk.magenta(
|
|
104
|
-
` ${figures.warning} AVA does not support JSON config, ignoring:\n\n ${loaded.unsupportedFiles.join('\n ')}`,
|
|
105
|
-
));
|
|
103
|
+
console.log(chalk.magenta(` ${figures.warning} AVA does not support JSON config, ignoring:\n\n ${loaded.unsupportedFiles.join('\n ')}`));
|
|
106
104
|
}
|
|
107
105
|
|
|
108
106
|
conf = loaded.config;
|
|
@@ -130,7 +128,8 @@ export default async function loadCli() { // eslint-disable-line complexity
|
|
|
130
128
|
files: [],
|
|
131
129
|
host: undefined,
|
|
132
130
|
port: undefined,
|
|
133
|
-
}
|
|
131
|
+
}
|
|
132
|
+
: null;
|
|
134
133
|
|
|
135
134
|
let resetCache = false;
|
|
136
135
|
const {argv} = yargs(hideBin(process.argv))
|
|
@@ -165,6 +164,7 @@ export default async function loadCli() { // eslint-disable-line complexity
|
|
|
165
164
|
})
|
|
166
165
|
.command('* [<pattern>...]', 'Run tests', yargs => yargs.options(FLAGS).positional('pattern', {
|
|
167
166
|
array: true,
|
|
167
|
+
// eslint-disable-next-line @stylistic/max-len
|
|
168
168
|
describe: 'Select which test files to run. Leave empty if you want AVA to run all test files as per your configuration. Accepts glob patterns, directories that (recursively) contain test files, and file paths optionally suffixed with a colon and comma-separated numbers and/or ranges identifying the 1-based line(s) of specific tests to run',
|
|
169
169
|
type: 'string',
|
|
170
170
|
}), argv => {
|
|
@@ -192,6 +192,7 @@ export default async function loadCli() { // eslint-disable-line complexity
|
|
|
192
192
|
},
|
|
193
193
|
}).positional('pattern', {
|
|
194
194
|
demand: true,
|
|
195
|
+
// eslint-disable-next-line @stylistic/max-len
|
|
195
196
|
describe: 'Glob pattern to select a single test file to debug, optionally suffixed with a colon and comma-separated numbers and/or ranges identifying the 1-based line(s) of specific tests to run',
|
|
196
197
|
type: 'string',
|
|
197
198
|
}),
|
|
@@ -203,14 +204,16 @@ export default async function loadCli() { // eslint-disable-line complexity
|
|
|
203
204
|
host: argv.host,
|
|
204
205
|
port: argv.port,
|
|
205
206
|
};
|
|
206
|
-
}
|
|
207
|
+
},
|
|
208
|
+
)
|
|
207
209
|
.command(
|
|
208
210
|
'reset-cache',
|
|
209
211
|
'Delete any temporary files and state kept by AVA, then exit',
|
|
210
212
|
yargs => yargs,
|
|
211
213
|
() => {
|
|
212
214
|
resetCache = true;
|
|
213
|
-
}
|
|
215
|
+
},
|
|
216
|
+
)
|
|
214
217
|
.example('$0')
|
|
215
218
|
.example('$0 test.js')
|
|
216
219
|
.example('$0 test.js:4,7-9')
|
package/lib/fork.js
CHANGED
|
@@ -104,6 +104,11 @@ export default function loadFork(file, options, execArgv = process.execArgv) {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
switch (message.ava.type) {
|
|
107
|
+
case 'worker-finished': {
|
|
108
|
+
send({type: 'free-worker'});
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
|
|
107
112
|
case 'ready-for-options': {
|
|
108
113
|
send({type: 'options', options});
|
|
109
114
|
break;
|
package/lib/line-numbers.js
CHANGED
|
@@ -16,23 +16,21 @@ const parseNumber = string => Number.parseInt(string, 10);
|
|
|
16
16
|
const removeAllWhitespace = string => string.replaceAll(/\s/g, '');
|
|
17
17
|
const range = (start, end) => Array.from({length: end - start + 1}).fill(start).map((element, index) => element + index);
|
|
18
18
|
|
|
19
|
-
const parseLineNumbers = suffix => sortNumbersAscending(distinctArray(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
19
|
+
const parseLineNumbers = suffix => sortNumbersAscending(distinctArray(suffix.split(',').flatMap(part => {
|
|
20
|
+
if (NUMBER_REGEX.test(part)) {
|
|
21
|
+
return parseNumber(part);
|
|
22
|
+
}
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
const {groups: {startGroup, endGroup}} = RANGE_REGEX.exec(part);
|
|
25
|
+
const start = parseNumber(startGroup);
|
|
26
|
+
const end = parseNumber(endGroup);
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
if (start > end) {
|
|
29
|
+
return range(end, start);
|
|
30
|
+
}
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
));
|
|
32
|
+
return range(start, end);
|
|
33
|
+
})));
|
|
36
34
|
|
|
37
35
|
export function splitPatternAndLineNumbers(pattern) {
|
|
38
36
|
const parts = pattern.split(DELIMITER);
|
|
@@ -49,9 +47,7 @@ export function splitPatternAndLineNumbers(pattern) {
|
|
|
49
47
|
}
|
|
50
48
|
|
|
51
49
|
export function getApplicableLineNumbers(normalizedFilePath, filter) {
|
|
52
|
-
return sortNumbersAscending(distinctArray(
|
|
53
|
-
filter
|
|
54
|
-
|
|
55
|
-
.flatMap(({lineNumbers}) => lineNumbers),
|
|
56
|
-
));
|
|
50
|
+
return sortNumbersAscending(distinctArray(filter
|
|
51
|
+
.filter(({pattern, lineNumbers}) => lineNumbers && picomatch.isMatch(normalizedFilePath, pattern))
|
|
52
|
+
.flatMap(({lineNumbers}) => lineNumbers)));
|
|
57
53
|
}
|
package/lib/reporters/default.js
CHANGED
|
@@ -26,7 +26,7 @@ class LineWriter extends stream.Writable {
|
|
|
26
26
|
|
|
27
27
|
this.dest = dest;
|
|
28
28
|
this.columns = dest.columns ?? 80;
|
|
29
|
-
this.lastLineIsEmpty =
|
|
29
|
+
this.lastLineIsEmpty = true;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
_write(chunk, _, callback) {
|
|
@@ -34,9 +34,9 @@ class LineWriter extends stream.Writable {
|
|
|
34
34
|
callback();
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
writeLine(string) {
|
|
37
|
+
writeLine(string, indent = true) {
|
|
38
38
|
if (string) {
|
|
39
|
-
this.write(indentString(string, 2) + os.EOL);
|
|
39
|
+
this.write((indent ? indentString(string, 2) : string) + os.EOL);
|
|
40
40
|
this.lastLineIsEmpty = false;
|
|
41
41
|
} else {
|
|
42
42
|
this.write(os.EOL);
|
|
@@ -44,6 +44,11 @@ class LineWriter extends stream.Writable {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
write(string) {
|
|
48
|
+
this.lastLineIsEmpty = false;
|
|
49
|
+
super.write(string);
|
|
50
|
+
}
|
|
51
|
+
|
|
47
52
|
ensureEmptyLine() {
|
|
48
53
|
if (!this.lastLineIsEmpty) {
|
|
49
54
|
this.writeLine();
|
|
@@ -120,7 +125,6 @@ export default class Reporter {
|
|
|
120
125
|
this.previousFailures = 0;
|
|
121
126
|
|
|
122
127
|
this.failFastEnabled = false;
|
|
123
|
-
this.lastLineIsEmpty = false;
|
|
124
128
|
this.matching = false;
|
|
125
129
|
|
|
126
130
|
this.removePreviousListener = null;
|
|
@@ -323,6 +327,7 @@ export default class Reporter {
|
|
|
323
327
|
|
|
324
328
|
this.lineWriter.writeLine(colors.error(`${figures.cross} Line numbers for ${this.relativeFile(event.testFile)} did not match any tests`));
|
|
325
329
|
} else if (!this.failFastEnabled && fileStats.remainingTests > 0) {
|
|
330
|
+
// eslint-disable-next-line @stylistic/max-len
|
|
326
331
|
this.lineWriter.writeLine(colors.error(`${figures.cross} ${fileStats.remainingTests} ${plur('test', fileStats.remainingTests)} remaining in ${this.relativeFile(event.testFile)}`));
|
|
327
332
|
}
|
|
328
333
|
}
|
|
@@ -628,7 +633,8 @@ export default class Reporter {
|
|
|
628
633
|
this.lineWriter.writeLine(colors.error(`${figures.cross} Couldn’t find any files to test` + firstLinePostfix));
|
|
629
634
|
} else {
|
|
630
635
|
const {testFileCount: count} = this.selectionInsights;
|
|
631
|
-
|
|
636
|
+
// eslint-disable-next-line @stylistic/max-len
|
|
637
|
+
this.lineWriter.writeLine(colors.error(`${figures.cross} Based on your configuration, ${count} test ${plur('file was', 'files were', count)} found, but did not match the filters:` + firstLinePostfix));
|
|
632
638
|
this.lineWriter.writeLine();
|
|
633
639
|
for (const {pattern} of this.selectionInsights.filter) {
|
|
634
640
|
this.lineWriter.writeLine(colors.error(`* ${pattern}`));
|
|
@@ -708,8 +714,7 @@ export default class Reporter {
|
|
|
708
714
|
&& this.stats.failedTests === 0
|
|
709
715
|
&& this.stats.passedTests > 0
|
|
710
716
|
) {
|
|
711
|
-
this.lineWriter.writeLine(colors.pass(`${this.stats.passedTests} ${plur('test', this.stats.passedTests)} passed`) + firstLinePostfix
|
|
712
|
-
);
|
|
717
|
+
this.lineWriter.writeLine(colors.pass(`${this.stats.passedTests} ${plur('test', this.stats.passedTests)} passed`) + firstLinePostfix);
|
|
713
718
|
firstLinePostfix = '';
|
|
714
719
|
}
|
|
715
720
|
|
package/lib/runner.js
CHANGED
|
@@ -2,7 +2,7 @@ import process from 'node:process';
|
|
|
2
2
|
import {pathToFileURL} from 'node:url';
|
|
3
3
|
|
|
4
4
|
import Emittery from 'emittery';
|
|
5
|
-
import
|
|
5
|
+
import * as matcher from 'matcher';
|
|
6
6
|
|
|
7
7
|
import ContextRef from './context-ref.js';
|
|
8
8
|
import createChain from './create-chain.js';
|
|
@@ -13,6 +13,15 @@ import Runnable from './test.js';
|
|
|
13
13
|
import {waitForReady} from './worker/state.cjs';
|
|
14
14
|
|
|
15
15
|
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
|
+
|
|
16
25
|
export default class Runner extends Emittery {
|
|
17
26
|
constructor(options = {}) {
|
|
18
27
|
super();
|
|
@@ -22,10 +31,9 @@ export default class Runner extends Emittery {
|
|
|
22
31
|
this.failWithoutAssertions = options.failWithoutAssertions !== false;
|
|
23
32
|
this.file = options.file;
|
|
24
33
|
this.checkSelectedByLineNumbers = options.checkSelectedByLineNumbers;
|
|
25
|
-
this.
|
|
34
|
+
this.matchPatterns = options.match ?? [];
|
|
26
35
|
this.projectDir = options.projectDir;
|
|
27
36
|
this.recordNewSnapshots = options.recordNewSnapshots === true;
|
|
28
|
-
this.runOnlyExclusive = options.runOnlyExclusive === true;
|
|
29
37
|
this.serial = options.serial === true;
|
|
30
38
|
this.snapshotDir = options.snapshotDir;
|
|
31
39
|
this.updateSnapshots = options.updateSnapshots;
|
|
@@ -34,6 +42,7 @@ export default class Runner extends Emittery {
|
|
|
34
42
|
this.boundCompareTestSnapshot = this.compareTestSnapshot.bind(this);
|
|
35
43
|
this.boundSkipSnapshot = this.skipSnapshot.bind(this);
|
|
36
44
|
this.interrupted = false;
|
|
45
|
+
this.runOnlyExclusive = false;
|
|
37
46
|
|
|
38
47
|
this.nextTaskIndex = 0;
|
|
39
48
|
this.tasks = {
|
|
@@ -92,9 +101,7 @@ export default class Runner extends Emittery {
|
|
|
92
101
|
|
|
93
102
|
const {args, implementation, title} = parseTestArgs(testArgs);
|
|
94
103
|
|
|
95
|
-
|
|
96
|
-
metadata.selected = this.checkSelectedByLineNumbers();
|
|
97
|
-
}
|
|
104
|
+
metadata.selected &&= this.checkSelectedByLineNumbers?.() ?? true;
|
|
98
105
|
|
|
99
106
|
if (metadata.todo) {
|
|
100
107
|
if (implementation) {
|
|
@@ -110,10 +117,7 @@ export default class Runner extends Emittery {
|
|
|
110
117
|
}
|
|
111
118
|
|
|
112
119
|
// --match selects TODO tests.
|
|
113
|
-
|
|
114
|
-
metadata.exclusive = true;
|
|
115
|
-
this.runOnlyExclusive = true;
|
|
116
|
-
}
|
|
120
|
+
metadata.selected &&= isTitleMatch(title.value, this.matchPatterns);
|
|
117
121
|
|
|
118
122
|
this.tasks.todo.push({title: title.value, metadata});
|
|
119
123
|
this.emit('stateChange', {
|
|
@@ -154,14 +158,10 @@ export default class Runner extends Emittery {
|
|
|
154
158
|
};
|
|
155
159
|
|
|
156
160
|
if (metadata.type === 'test') {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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;
|
|
165
165
|
|
|
166
166
|
this.tasks[metadata.serial ? 'serial' : 'concurrent'].push(task);
|
|
167
167
|
|
|
@@ -181,6 +181,7 @@ export default class Runner extends Emittery {
|
|
|
181
181
|
serial: false,
|
|
182
182
|
exclusive: false,
|
|
183
183
|
skipped: false,
|
|
184
|
+
selected: true,
|
|
184
185
|
todo: false,
|
|
185
186
|
failing: false,
|
|
186
187
|
callback: false,
|
|
@@ -253,23 +254,21 @@ export default class Runner extends Emittery {
|
|
|
253
254
|
let waitForSerial = Promise.resolve();
|
|
254
255
|
await runnables.reduce((previous, runnable) => { // eslint-disable-line unicorn/no-array-reduce
|
|
255
256
|
if (runnable.metadata.serial || this.serial) {
|
|
256
|
-
waitForSerial = previous.then(() =>
|
|
257
|
+
waitForSerial = previous.then(() => // eslint-disable-line promise/prefer-await-to-then
|
|
257
258
|
// Serial runnables run as long as there was no previous failure, unless
|
|
258
259
|
// the runnable should always be run.
|
|
259
|
-
(allPassed || runnable.metadata.always) && runAndStoreResult(runnable)
|
|
260
|
-
);
|
|
260
|
+
(allPassed || runnable.metadata.always) && runAndStoreResult(runnable));
|
|
261
261
|
return waitForSerial;
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
return Promise.all([
|
|
265
265
|
previous,
|
|
266
|
-
waitForSerial.then(() =>
|
|
266
|
+
waitForSerial.then(() => // eslint-disable-line promise/prefer-await-to-then
|
|
267
267
|
// Concurrent runnables are kicked off after the previous serial
|
|
268
268
|
// runnables have completed, as long as there was no previous failure
|
|
269
269
|
// (or if the runnable should always be run). One concurrent runnable's
|
|
270
270
|
// failure does not prevent the next runnable from running.
|
|
271
|
-
(allPassed || runnable.metadata.always) && runAndStoreResult(runnable),
|
|
272
|
-
),
|
|
271
|
+
(allPassed || runnable.metadata.always) && runAndStoreResult(runnable)),
|
|
273
272
|
]);
|
|
274
273
|
}, waitForSerial);
|
|
275
274
|
|
|
@@ -378,7 +377,8 @@ export default class Runner extends Emittery {
|
|
|
378
377
|
{
|
|
379
378
|
titleSuffix: hookSuffix,
|
|
380
379
|
testPassed: testOk,
|
|
381
|
-
}
|
|
380
|
+
},
|
|
381
|
+
);
|
|
382
382
|
} else {
|
|
383
383
|
this.emit('stateChange', {
|
|
384
384
|
type: 'test-failed',
|
|
@@ -398,20 +398,16 @@ export default class Runner extends Emittery {
|
|
|
398
398
|
{
|
|
399
399
|
titleSuffix: hookSuffix,
|
|
400
400
|
testPassed: testOk,
|
|
401
|
-
}
|
|
401
|
+
},
|
|
402
|
+
);
|
|
402
403
|
return alwaysOk && hooksOk && testOk;
|
|
403
404
|
}
|
|
404
405
|
|
|
405
|
-
async start() {
|
|
406
|
+
async start() {
|
|
406
407
|
const concurrentTests = [];
|
|
407
408
|
const serialTests = [];
|
|
408
409
|
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)) {
|
|
415
411
|
this.snapshots.skipBlock(task.title, task.metadata.taskIndex);
|
|
416
412
|
continue;
|
|
417
413
|
}
|
|
@@ -432,12 +428,7 @@ export default class Runner extends Emittery {
|
|
|
432
428
|
}
|
|
433
429
|
|
|
434
430
|
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)) {
|
|
441
432
|
this.snapshots.skipBlock(task.title, task.metadata.taskIndex);
|
|
442
433
|
continue;
|
|
443
434
|
}
|
|
@@ -460,11 +451,7 @@ export default class Runner extends Emittery {
|
|
|
460
451
|
}
|
|
461
452
|
|
|
462
453
|
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)) {
|
|
468
455
|
continue;
|
|
469
456
|
}
|
|
470
457
|
|
package/lib/snapshot-manager.js
CHANGED
|
@@ -160,26 +160,24 @@ class BufferBuilder {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
function sortBlocks(blocksByTitle, blockIndices) {
|
|
163
|
-
return [...blocksByTitle].sort(
|
|
164
|
-
(
|
|
165
|
-
|
|
166
|
-
const b = blockIndices.get(bTitle);
|
|
167
|
-
|
|
168
|
-
if (a === undefined) {
|
|
169
|
-
if (b === undefined) {
|
|
170
|
-
return 0;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return 1;
|
|
174
|
-
}
|
|
163
|
+
return [...blocksByTitle].sort(([aTitle], [bTitle]) => {
|
|
164
|
+
const a = blockIndices.get(aTitle);
|
|
165
|
+
const b = blockIndices.get(bTitle);
|
|
175
166
|
|
|
167
|
+
if (a === undefined) {
|
|
176
168
|
if (b === undefined) {
|
|
177
|
-
return
|
|
169
|
+
return 0;
|
|
178
170
|
}
|
|
179
171
|
|
|
180
|
-
return
|
|
181
|
-
}
|
|
182
|
-
|
|
172
|
+
return 1;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (b === undefined) {
|
|
176
|
+
return -1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return a - b;
|
|
180
|
+
});
|
|
183
181
|
}
|
|
184
182
|
|
|
185
183
|
async function encodeSnapshots(snapshotData) {
|
|
@@ -368,9 +366,7 @@ class Manager {
|
|
|
368
366
|
}
|
|
369
367
|
|
|
370
368
|
const snapshots = {
|
|
371
|
-
blocks: sortBlocks(this.newBlocksByTitle, this.blockIndices).map(
|
|
372
|
-
([title, block]) => ({title, ...block}),
|
|
373
|
-
),
|
|
369
|
+
blocks: sortBlocks(this.newBlocksByTitle, this.blockIndices).map(([title, block]) => ({title, ...block})),
|
|
374
370
|
};
|
|
375
371
|
|
|
376
372
|
const buffer = await encodeSnapshots(snapshots);
|
package/lib/test.js
CHANGED
|
@@ -630,11 +630,13 @@ export default class Test {
|
|
|
630
630
|
if (this.metadata.failing) {
|
|
631
631
|
passed = !passed;
|
|
632
632
|
|
|
633
|
-
error = passed
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
633
|
+
error = passed
|
|
634
|
+
? null
|
|
635
|
+
: new AssertionError('Test was expected to fail, but succeeded, you should stop marking the test as failing', {
|
|
636
|
+
// TODO: Provide an assertion stack that traces to the test declaration,
|
|
637
|
+
// rather than AVA internals.
|
|
638
|
+
assertionStack: '',
|
|
639
|
+
});
|
|
638
640
|
}
|
|
639
641
|
|
|
640
642
|
return {
|
package/lib/watcher.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import nodePath from 'node:path';
|
|
3
3
|
import process from 'node:process';
|
|
4
|
+
import * as readline from 'node:readline/promises';
|
|
4
5
|
import v8 from 'node:v8';
|
|
5
6
|
|
|
6
7
|
import {nodeFileTrace} from '@vercel/nft';
|
|
@@ -9,6 +10,7 @@ import createDebug from 'debug';
|
|
|
9
10
|
import {chalk} from './chalk.js';
|
|
10
11
|
import {
|
|
11
12
|
applyTestFileFilter, classify, buildIgnoreMatcher, findTests,
|
|
13
|
+
normalizePattern,
|
|
12
14
|
} from './globs.js';
|
|
13
15
|
import {levels as providerLevels} from './provider-manager.js';
|
|
14
16
|
|
|
@@ -18,8 +20,6 @@ const debug = createDebug('ava:watcher');
|
|
|
18
20
|
// to make Node.js write out interim reports in various places.
|
|
19
21
|
const takeCoverageForSelfTests = process.env.TEST_AVA ? v8.takeCoverage : undefined;
|
|
20
22
|
|
|
21
|
-
const END_MESSAGE = chalk.gray('Type `r` and press enter to rerun tests\nType `u` and press enter to update snapshots\n');
|
|
22
|
-
|
|
23
23
|
export function available(projectDir) {
|
|
24
24
|
try {
|
|
25
25
|
fs.watch(projectDir, {persistent: false, recursive: true, signal: AbortSignal.abort()});
|
|
@@ -34,18 +34,156 @@ export function available(projectDir) {
|
|
|
34
34
|
return true;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
const cancel = Symbol('cancel');
|
|
38
|
+
const close = Symbol('close');
|
|
39
|
+
|
|
40
|
+
const promiseWithResolvers = Promise.withResolvers?.bind(Promise) ?? (() => {
|
|
41
|
+
let resolve;
|
|
42
|
+
let reject;
|
|
43
|
+
const promise = new Promise((_resolve, _reject) => {
|
|
44
|
+
resolve = _resolve;
|
|
45
|
+
reject = _reject;
|
|
46
|
+
});
|
|
47
|
+
return {promise, resolve, reject};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
async function * readLines(stream) {
|
|
51
|
+
const rl = readline.createInterface({input: stream, output: process.stdout});
|
|
52
|
+
let promise;
|
|
53
|
+
let resolve;
|
|
54
|
+
let values = [];
|
|
55
|
+
rl.addListener('close', () => {
|
|
56
|
+
values.push(close);
|
|
57
|
+
resolve?.();
|
|
58
|
+
});
|
|
59
|
+
rl.addListener('SIGINT', () => {
|
|
60
|
+
values.push(cancel);
|
|
61
|
+
resolve?.();
|
|
62
|
+
});
|
|
63
|
+
rl.addListener('line', line => {
|
|
64
|
+
values.push(line.trim());
|
|
65
|
+
resolve?.();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
while (true) {
|
|
69
|
+
yield * values;
|
|
70
|
+
values = [];
|
|
71
|
+
await promise; // eslint-disable-line no-await-in-loop
|
|
72
|
+
// Immediately create a new promise to wait for the next line.
|
|
73
|
+
({promise, resolve} = promiseWithResolvers());
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const eachLine = async (lineReader, callback) => {
|
|
78
|
+
for await (const line of lineReader) {
|
|
79
|
+
await callback(line);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const writeCommandInstructions = (reporter, interactiveGlobPattern, interactiveMatchPattern) => {
|
|
84
|
+
reporter.lineWriter.writeLine(chalk.gray('Type `g` followed by enter to filter test files by a glob pattern'));
|
|
85
|
+
reporter.lineWriter.writeLine(chalk.gray('Type `m` followed by enter to filter tests by their title (similar to --match)'));
|
|
86
|
+
if (interactiveGlobPattern || interactiveMatchPattern) {
|
|
87
|
+
reporter.lineWriter.writeLine(chalk.gray('Type `a` followed by enter to rerun all tests (while preserving filters)'));
|
|
88
|
+
reporter.lineWriter.writeLine(chalk.gray('Type `r` followed by enter to rerun tests that match your filters'));
|
|
89
|
+
} else {
|
|
90
|
+
reporter.lineWriter.writeLine(chalk.gray('Type `r` followed by enter to rerun tests'));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
reporter.lineWriter.writeLine(chalk.gray('Type `u` followed by enter to update snapshots in selected tests'));
|
|
94
|
+
|
|
95
|
+
if (interactiveGlobPattern || interactiveMatchPattern) {
|
|
96
|
+
reporter.lineWriter.writeLine();
|
|
97
|
+
|
|
98
|
+
if (interactiveGlobPattern) {
|
|
99
|
+
reporter.lineWriter.writeLine(chalk.gray(`Current test file glob pattern: ${chalk.italic(interactiveGlobPattern)}`));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (interactiveMatchPattern) {
|
|
103
|
+
reporter.lineWriter.writeLine(chalk.gray(`Current test title match pattern: ${chalk.italic(interactiveMatchPattern)}`));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
reporter.lineWriter.writeLine();
|
|
108
|
+
reporter.lineWriter.write('> ');
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const promptForGlobPattern = async (reporter, lineReader, currentPattern, projectDir) => {
|
|
112
|
+
reporter.lineWriter.ensureEmptyLine();
|
|
113
|
+
reporter.lineWriter.writeLine('Type the glob pattern then press enter. Leave blank to clear.', false);
|
|
114
|
+
if (currentPattern === undefined) {
|
|
115
|
+
reporter.lineWriter.writeLine();
|
|
116
|
+
reporter.lineWriter.writeLine(chalk.italic('Tip: Start with `**/` to select files in any directory.'), false);
|
|
117
|
+
reporter.lineWriter.writeLine(chalk.italic('Tip: Start with `!` to exclude files.'), false);
|
|
118
|
+
} else {
|
|
119
|
+
reporter.lineWriter.writeLine();
|
|
120
|
+
reporter.lineWriter.writeLine(`Current glob pattern is: ${chalk.italic(currentPattern)}`, false);
|
|
121
|
+
reporter.lineWriter.writeLine();
|
|
122
|
+
reporter.lineWriter.writeLine(chalk.italic('Tip: Ctrl+C to exit without any changes.'), false);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
reporter.lineWriter.write('> ');
|
|
126
|
+
|
|
127
|
+
const {value} = await lineReader.next();
|
|
128
|
+
if (value === close || value === cancel) {
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (value === '') {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return normalizePattern(nodePath.relative(projectDir, nodePath.resolve(process.cwd(), value)));
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const promptForMatchPattern = async (reporter, lineReader, currentPattern) => {
|
|
140
|
+
reporter.lineWriter.writeLine();
|
|
141
|
+
reporter.lineWriter.writeLine('Type the match pattern then press enter. Leave blank to clear.', false);
|
|
142
|
+
if (currentPattern === undefined) {
|
|
143
|
+
reporter.lineWriter.writeLine();
|
|
144
|
+
reporter.lineWriter.writeLine(chalk.italic('Tip: Start with `*` to match suffixes'), false);
|
|
145
|
+
reporter.lineWriter.writeLine(chalk.italic('Tip: End with `*` to match prefixes.'), false);
|
|
146
|
+
reporter.lineWriter.writeLine(chalk.italic('Tip: Start with `!` to exclude titles.'), false);
|
|
147
|
+
} else {
|
|
148
|
+
reporter.lineWriter.writeLine();
|
|
149
|
+
reporter.lineWriter.writeLine(`Current match pattern is: ${chalk.italic(currentPattern)}`, false);
|
|
150
|
+
reporter.lineWriter.writeLine();
|
|
151
|
+
reporter.lineWriter.writeLine(chalk.italic('Tip: Ctrl+C to exit without any changes.'), false);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
reporter.lineWriter.write('> ');
|
|
155
|
+
|
|
156
|
+
const {value} = await lineReader.next();
|
|
157
|
+
return value === '' ? undefined : value;
|
|
158
|
+
};
|
|
159
|
+
|
|
37
160
|
export async function start({api, filter, globs, projectDir, providers, reporter, stdin, signal}) {
|
|
38
161
|
providers = providers.filter(({level}) => level >= providerLevels.ava6);
|
|
39
|
-
for await (const {files, ...runtimeOptions} of plan({
|
|
40
|
-
api,
|
|
162
|
+
for await (const {files, testFileSelector, ...runtimeOptions} of plan({
|
|
163
|
+
api,
|
|
164
|
+
filter,
|
|
165
|
+
globs,
|
|
166
|
+
projectDir,
|
|
167
|
+
providers,
|
|
168
|
+
stdin,
|
|
169
|
+
abortSignal: signal,
|
|
170
|
+
reporter,
|
|
41
171
|
})) {
|
|
42
|
-
await api.run({files,
|
|
172
|
+
await api.run({files, testFileSelector, runtimeOptions});
|
|
43
173
|
reporter.endRun();
|
|
44
|
-
reporter.lineWriter.writeLine(END_MESSAGE);
|
|
45
174
|
}
|
|
46
175
|
}
|
|
47
176
|
|
|
48
|
-
async function * plan({
|
|
177
|
+
async function * plan({
|
|
178
|
+
api,
|
|
179
|
+
filter,
|
|
180
|
+
globs,
|
|
181
|
+
projectDir,
|
|
182
|
+
providers,
|
|
183
|
+
stdin,
|
|
184
|
+
abortSignal,
|
|
185
|
+
reporter,
|
|
186
|
+
}) {
|
|
49
187
|
const fileTracer = new FileTracer({base: projectDir});
|
|
50
188
|
const isIgnored = buildIgnoreMatcher(globs);
|
|
51
189
|
const patternFilters = filter.map(({pattern}) => pattern);
|
|
@@ -79,11 +217,19 @@ async function * plan({api, filter, globs, projectDir, providers, stdin, abortSi
|
|
|
79
217
|
}))));
|
|
80
218
|
|
|
81
219
|
// State tracked for test runs.
|
|
82
|
-
const filesWithExclusiveTests = new Set();
|
|
83
220
|
const touchedFiles = new Set();
|
|
84
221
|
const temporaryFiles = new Set();
|
|
85
222
|
const failureCounts = new Map();
|
|
86
223
|
|
|
224
|
+
const countPreviousFailures = () => {
|
|
225
|
+
let previousFailures = 0;
|
|
226
|
+
for (const count of failureCounts.values()) {
|
|
227
|
+
previousFailures += count;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return previousFailures;
|
|
231
|
+
};
|
|
232
|
+
|
|
87
233
|
// Observe all test runs.
|
|
88
234
|
api.on('run', ({status}) => {
|
|
89
235
|
status.on('stateChange', evt => {
|
|
@@ -117,17 +263,6 @@ async function * plan({api, filter, globs, projectDir, providers, stdin, abortSi
|
|
|
117
263
|
break;
|
|
118
264
|
}
|
|
119
265
|
|
|
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
|
-
|
|
131
266
|
default: {
|
|
132
267
|
break;
|
|
133
268
|
}
|
|
@@ -151,21 +286,124 @@ async function * plan({api, filter, globs, projectDir, providers, stdin, abortSi
|
|
|
151
286
|
updateSnapshots = false;
|
|
152
287
|
};
|
|
153
288
|
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (runAll || updateSnapshots) {
|
|
161
|
-
signalChanged({});
|
|
289
|
+
// Interactive filters.
|
|
290
|
+
let interactiveGlobPattern;
|
|
291
|
+
let interactiveMatchPattern;
|
|
292
|
+
const testFileSelector = (allTestFiles, selectedFiles = [], skipInteractive = runAll) => {
|
|
293
|
+
if (selectedFiles.length === 0) {
|
|
294
|
+
selectedFiles = allTestFiles;
|
|
162
295
|
}
|
|
163
|
-
|
|
296
|
+
|
|
297
|
+
if (patternFilters.length > 0) {
|
|
298
|
+
selectedFiles = applyTestFileFilter({
|
|
299
|
+
cwd: projectDir,
|
|
300
|
+
filter: patternFilters,
|
|
301
|
+
testFiles: selectedFiles,
|
|
302
|
+
treatFilterPatternsAsFiles: runAll, // This option is additive, so only select individual files on full runs.
|
|
303
|
+
});
|
|
304
|
+
selectedFiles.appliedFilters = filter; // `filter` is the original input.
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!skipInteractive && interactiveGlobPattern !== undefined) {
|
|
308
|
+
const {appliedFilters = [], ignoredFilterPatternFiles} = selectedFiles;
|
|
309
|
+
selectedFiles = applyTestFileFilter({
|
|
310
|
+
cwd: projectDir,
|
|
311
|
+
filter: [interactiveGlobPattern],
|
|
312
|
+
testFiles: selectedFiles,
|
|
313
|
+
treatFilterPatternsAsFiles: false,
|
|
314
|
+
});
|
|
315
|
+
selectedFiles.appliedFilters = [...appliedFilters, {pattern: interactiveGlobPattern}];
|
|
316
|
+
selectedFiles.ignoredFilterPatternFiles = ignoredFilterPatternFiles;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Remove previous failures for tests that will run again.
|
|
320
|
+
for (const file of selectedFiles) {
|
|
321
|
+
const path = nodePath.relative(projectDir, file);
|
|
322
|
+
failureCounts.delete(path);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return selectedFiles;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const lineReader = readLines(stdin);
|
|
329
|
+
|
|
330
|
+
// Don't let the reader keep the process alive.
|
|
164
331
|
stdin.unref();
|
|
165
332
|
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
333
|
+
// Handle commands.
|
|
334
|
+
eachLine(lineReader, async line => {
|
|
335
|
+
if (line === cancel || line === close) {
|
|
336
|
+
process.exit(); // eslint-disable-line unicorn/no-process-exit
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
switch (line.toLowerCase()) {
|
|
340
|
+
case 'r': {
|
|
341
|
+
signalChanged();
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
case 'u': {
|
|
346
|
+
updateSnapshots = true;
|
|
347
|
+
signalChanged();
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
case 'a': {
|
|
352
|
+
runAll = true;
|
|
353
|
+
signalChanged();
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
case 'g': {
|
|
358
|
+
respondToChanges = false;
|
|
359
|
+
const oldGlobPattern = interactiveGlobPattern;
|
|
360
|
+
const promptValue = await promptForGlobPattern(reporter, lineReader, interactiveGlobPattern, projectDir);
|
|
361
|
+
respondToChanges = true;
|
|
362
|
+
reporter.lineWriter.writeLine();
|
|
363
|
+
if (promptValue === close) {
|
|
364
|
+
process.exit(); // eslint-disable-line unicorn/no-process-exit
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (promptValue === cancel || (promptValue === oldGlobPattern)) {
|
|
368
|
+
signalChanged();
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
interactiveGlobPattern = promptValue;
|
|
373
|
+
signalChanged();
|
|
374
|
+
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
case 'm': {
|
|
379
|
+
respondToChanges = false;
|
|
380
|
+
const oldMatchPattern = interactiveMatchPattern;
|
|
381
|
+
const promptValue = await promptForMatchPattern(reporter, lineReader, interactiveMatchPattern);
|
|
382
|
+
respondToChanges = true;
|
|
383
|
+
reporter.lineWriter.writeLine();
|
|
384
|
+
if (promptValue === close) {
|
|
385
|
+
process.exit(); // eslint-disable-line unicorn/no-process-exit
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (promptValue === cancel || (promptValue === oldMatchPattern)) {
|
|
389
|
+
signalChanged();
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
interactiveMatchPattern = promptValue;
|
|
394
|
+
signalChanged();
|
|
395
|
+
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
default: {
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// Whether to respond to file system changes. Used to control when the next run is prepared.
|
|
406
|
+
let respondToChanges = true;
|
|
169
407
|
|
|
170
408
|
// Tracks file paths we know have changed since the previous test run.
|
|
171
409
|
const dirtyPaths = new Set();
|
|
@@ -177,9 +415,9 @@ async function * plan({api, filter, globs, projectDir, providers, stdin, abortSi
|
|
|
177
415
|
return;
|
|
178
416
|
}
|
|
179
417
|
|
|
180
|
-
// Equally, if tests are currently running, then keep accumulating changes.
|
|
181
|
-
// The timer is refreshed
|
|
182
|
-
if (
|
|
418
|
+
// Equally, if tests are currently running, or the user is being prompted, then keep accumulating changes.
|
|
419
|
+
// The timer is refreshed when we're ready to resume.
|
|
420
|
+
if (!respondToChanges) {
|
|
183
421
|
takeCoverageForSelfTests?.();
|
|
184
422
|
return;
|
|
185
423
|
}
|
|
@@ -327,42 +565,12 @@ async function * plan({api, filter, globs, projectDir, providers, stdin, abortSi
|
|
|
327
565
|
fileTracer.update(changes);
|
|
328
566
|
}
|
|
329
567
|
|
|
330
|
-
// Select the test files to run, and how to run them.
|
|
331
|
-
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
|
-
}
|
|
344
|
-
|
|
345
|
-
if (filter.length > 0) {
|
|
346
|
-
testFiles = applyTestFileFilter({
|
|
347
|
-
cwd: projectDir,
|
|
348
|
-
expandDirectories: false,
|
|
349
|
-
filter: patternFilters,
|
|
350
|
-
testFiles,
|
|
351
|
-
treatFilterPatternsAsFiles: false,
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
|
|
355
568
|
if (nonTestFiles.length > 0) {
|
|
356
569
|
debug('Non-test files changed, running all tests');
|
|
357
570
|
failureCounts.clear(); // All tests are run, so clear previous failures.
|
|
358
|
-
signalChanged(
|
|
359
|
-
} else if (
|
|
360
|
-
|
|
361
|
-
for (const path of testFiles) {
|
|
362
|
-
failureCounts.delete(path);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
signalChanged({runOnlyExclusive, testFiles});
|
|
571
|
+
signalChanged();
|
|
572
|
+
} else if (uniqueTestFiles.size > 0) {
|
|
573
|
+
signalChanged({testFiles: [...uniqueTestFiles]});
|
|
366
574
|
}
|
|
367
575
|
|
|
368
576
|
takeCoverageForSelfTests?.();
|
|
@@ -378,34 +586,61 @@ async function * plan({api, filter, globs, projectDir, providers, stdin, abortSi
|
|
|
378
586
|
});
|
|
379
587
|
|
|
380
588
|
abortSignal?.addEventListener('abort', () => {
|
|
381
|
-
signalChanged?.(
|
|
589
|
+
signalChanged?.();
|
|
382
590
|
});
|
|
383
591
|
|
|
384
592
|
// And finally, the watch loop.
|
|
385
593
|
while (abortSignal?.aborted !== true) {
|
|
386
|
-
const {testFiles
|
|
594
|
+
const {testFiles = []} = (await changed) ?? {}; // eslint-disable-line no-await-in-loop
|
|
387
595
|
|
|
388
596
|
if (abortSignal?.aborted) {
|
|
389
597
|
break;
|
|
390
598
|
}
|
|
391
599
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
600
|
+
// Values are changed by refresh() so copy them now.
|
|
601
|
+
const instructFirstRun = firstRun;
|
|
602
|
+
const skipInteractive = runAll;
|
|
603
|
+
const instructUpdateSnapshots = updateSnapshots;
|
|
604
|
+
reset(); // Make sure the next run can be triggered.
|
|
605
|
+
|
|
606
|
+
let files = testFiles.map(file => nodePath.join(projectDir, file));
|
|
607
|
+
let instructTestFileSelector = testFileSelector;
|
|
608
|
+
if (files.length > 0) {
|
|
609
|
+
files = testFileSelector(files, [], skipInteractive);
|
|
610
|
+
if (files.length === 0) {
|
|
611
|
+
debug('Filters rejected all test files');
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Make a no-op for the API to avoid filtering `files` again.
|
|
616
|
+
instructTestFileSelector = () => files;
|
|
617
|
+
} else if (skipInteractive) {
|
|
618
|
+
instructTestFileSelector = (allTestFiles, selectedFiles = []) => testFileSelector(allTestFiles, selectedFiles, true);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Clear any prompt.
|
|
622
|
+
if (!reporter.lineWriter.lastLineIsEmpty && reporter.reportStream.isTTY) {
|
|
623
|
+
reporter.reportStream.clearLine(0);
|
|
624
|
+
reporter.lineWriter.writeLine();
|
|
395
625
|
}
|
|
396
626
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
627
|
+
// Let the tests run.
|
|
628
|
+
respondToChanges = false;
|
|
629
|
+
yield {
|
|
630
|
+
countPreviousFailures,
|
|
631
|
+
files,
|
|
632
|
+
firstRun: instructFirstRun,
|
|
633
|
+
testFileSelector: instructTestFileSelector,
|
|
634
|
+
updateSnapshots: instructUpdateSnapshots,
|
|
635
|
+
interactiveMatchPattern: skipInteractive ? undefined : interactiveMatchPattern,
|
|
403
636
|
};
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
637
|
+
respondToChanges = true;
|
|
638
|
+
|
|
639
|
+
// Write command instructions after the tests have run and been reported.
|
|
640
|
+
writeCommandInstructions(reporter, interactiveGlobPattern, interactiveMatchPattern);
|
|
641
|
+
|
|
642
|
+
// Trigger the callback, which if there were changes will run the tests again.
|
|
643
|
+
debounce.refresh();
|
|
409
644
|
}
|
|
410
645
|
}
|
|
411
646
|
|
package/lib/worker/base.js
CHANGED
|
@@ -81,7 +81,6 @@ const run = async options => {
|
|
|
81
81
|
match: options.match,
|
|
82
82
|
projectDir: options.projectDir,
|
|
83
83
|
recordNewSnapshots: options.recordNewSnapshots,
|
|
84
|
-
runOnlyExclusive: options.runOnlyExclusive,
|
|
85
84
|
serial: options.serial,
|
|
86
85
|
snapshotDir: options.snapshotDir,
|
|
87
86
|
updateSnapshots: options.updateSnapshots,
|
|
@@ -121,6 +120,15 @@ const run = async options => {
|
|
|
121
120
|
return;
|
|
122
121
|
}
|
|
123
122
|
|
|
123
|
+
channel.send({type: 'worker-finished'});
|
|
124
|
+
|
|
125
|
+
// Reference the channel until the worker is freed. This should prevent Node.js from terminating the child process
|
|
126
|
+
// prematurely, which has been witnessed on Windows. See discussion at
|
|
127
|
+
// <https://github.com/avajs/ava/issues/3390#issuecomment-3056119361>.
|
|
128
|
+
channel.ref();
|
|
129
|
+
await channel.workerFreed;
|
|
130
|
+
channel.unref();
|
|
131
|
+
|
|
124
132
|
nowAndTimers.setImmediate(() => {
|
|
125
133
|
const unhandled = currentlyUnhandled();
|
|
126
134
|
if (unhandled.length === 0) {
|
package/lib/worker/channel.cjs
CHANGED
|
@@ -107,7 +107,9 @@ handle.ref();
|
|
|
107
107
|
|
|
108
108
|
exports.options = selectAvaMessage(handle.channel, 'options').then(message => message.ava.options);
|
|
109
109
|
exports.peerFailed = selectAvaMessage(handle.channel, 'peer-failed');
|
|
110
|
+
exports.workerFreed = selectAvaMessage(handle.channel, 'free-worker');
|
|
110
111
|
exports.send = handle.send.bind(handle);
|
|
112
|
+
exports.ref = handle.ref.bind(handle);
|
|
111
113
|
exports.unref = handle.unref.bind(handle);
|
|
112
114
|
|
|
113
115
|
let channelCounter = 0;
|
package/lib/worker/main.cjs
CHANGED
package/lib/worker/plugin.cjs
CHANGED
|
@@ -4,7 +4,7 @@ const {registerSharedWorker: register} = require('./channel.cjs');
|
|
|
4
4
|
const options = require('./options.cjs');
|
|
5
5
|
const {sharedWorkerTeardowns, waitForReady} = require('./state.cjs');
|
|
6
6
|
|
|
7
|
-
require('./guard-environment.cjs'); // eslint-disable-line import/no-unassigned-import
|
|
7
|
+
require('./guard-environment.cjs'); // eslint-disable-line import-x/no-unassigned-import
|
|
8
8
|
|
|
9
9
|
const workers = new Map();
|
|
10
10
|
const workerTeardownFns = new WeakMap();
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ava",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.4.1",
|
|
4
4
|
"description": "Node.js test runner that lets you develop with confidence.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"repository":
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/avajs/ava.git"
|
|
9
|
+
},
|
|
7
10
|
"homepage": "https://avajs.dev",
|
|
8
11
|
"bin": {
|
|
9
12
|
"ava": "entrypoints/cli.mjs"
|
|
@@ -36,7 +39,7 @@
|
|
|
36
39
|
},
|
|
37
40
|
"type": "module",
|
|
38
41
|
"engines": {
|
|
39
|
-
"node": "^18.18 || ^20.8 || ^22 || >=
|
|
42
|
+
"node": "^18.18 || ^20.8 || ^22 || ^23 || >=24"
|
|
40
43
|
},
|
|
41
44
|
"scripts": {
|
|
42
45
|
"test": "./scripts/test.sh"
|
|
@@ -83,25 +86,25 @@
|
|
|
83
86
|
"typescript"
|
|
84
87
|
],
|
|
85
88
|
"dependencies": {
|
|
86
|
-
"@vercel/nft": "^0.29.
|
|
87
|
-
"acorn": "^8.
|
|
89
|
+
"@vercel/nft": "^0.29.4",
|
|
90
|
+
"acorn": "^8.15.0",
|
|
88
91
|
"acorn-walk": "^8.3.4",
|
|
89
92
|
"ansi-styles": "^6.2.1",
|
|
90
93
|
"arrgv": "^1.0.2",
|
|
91
94
|
"arrify": "^3.0.0",
|
|
92
95
|
"callsites": "^4.2.0",
|
|
93
|
-
"cbor": "^10.0.
|
|
96
|
+
"cbor": "^10.0.9",
|
|
94
97
|
"chalk": "^5.4.1",
|
|
95
98
|
"chunkd": "^2.0.1",
|
|
96
|
-
"ci-info": "^4.
|
|
99
|
+
"ci-info": "^4.3.0",
|
|
97
100
|
"ci-parallel-vars": "^1.0.1",
|
|
98
101
|
"cli-truncate": "^4.0.0",
|
|
99
102
|
"code-excerpt": "^4.0.0",
|
|
100
103
|
"common-path-prefix": "^3.0.0",
|
|
101
104
|
"concordance": "^5.0.4",
|
|
102
105
|
"currently-unhandled": "^0.4.1",
|
|
103
|
-
"debug": "^4.4.
|
|
104
|
-
"emittery": "^1.
|
|
106
|
+
"debug": "^4.4.1",
|
|
107
|
+
"emittery": "^1.2.0",
|
|
105
108
|
"figures": "^6.1.0",
|
|
106
109
|
"globby": "^14.1.0",
|
|
107
110
|
"ignore-by-default": "^2.1.0",
|
|
@@ -126,19 +129,19 @@
|
|
|
126
129
|
},
|
|
127
130
|
"devDependencies": {
|
|
128
131
|
"@ava/test": "github:avajs/test",
|
|
129
|
-
"@ava/typescript": "^
|
|
132
|
+
"@ava/typescript": "^6.0.0",
|
|
130
133
|
"@sindresorhus/tsconfig": "^5.1.1",
|
|
131
|
-
"@types/node": "^22.
|
|
134
|
+
"@types/node": "^22.16.3",
|
|
132
135
|
"ansi-escapes": "^7.0.0",
|
|
133
136
|
"c8": "^10.1.3",
|
|
134
|
-
"execa": "^9.
|
|
135
|
-
"expect": "^
|
|
136
|
-
"sinon": "^
|
|
137
|
+
"execa": "^9.6.0",
|
|
138
|
+
"expect": "^30.0.4",
|
|
139
|
+
"sinon": "^21.0.0",
|
|
137
140
|
"tap": "^21.1.0",
|
|
138
141
|
"tempy": "^3.1.0",
|
|
139
142
|
"tsd": "^0.32.0",
|
|
140
143
|
"typescript": "~5.8.3",
|
|
141
|
-
"xo": "^
|
|
144
|
+
"xo": "^1.1.1",
|
|
142
145
|
"zen-observable": "^0.10.0"
|
|
143
146
|
},
|
|
144
147
|
"peerDependencies": {
|
|
@@ -150,7 +153,7 @@
|
|
|
150
153
|
}
|
|
151
154
|
},
|
|
152
155
|
"volta": {
|
|
153
|
-
"node": "22.
|
|
154
|
-
"npm": "11.
|
|
156
|
+
"node": "22.16.0",
|
|
157
|
+
"npm": "11.4.1"
|
|
155
158
|
}
|
|
156
159
|
}
|
package/types/assertions.d.cts
CHANGED
|
@@ -143,7 +143,7 @@ export type Assertions = {
|
|
|
143
143
|
truthy: TruthyAssertion;
|
|
144
144
|
};
|
|
145
145
|
|
|
146
|
-
type FalsyValue = false | 0 | 0n | '' |
|
|
146
|
+
type FalsyValue = false | 0 | 0n | '' | undefined;
|
|
147
147
|
type Falsy<T> = T extends Exclude<T, FalsyValue> ? (T extends number | string | bigint ? T & FalsyValue : never) : T;
|
|
148
148
|
|
|
149
149
|
export type AssertAssertion = {
|
|
@@ -2,7 +2,7 @@ export namespace SharedWorker {
|
|
|
2
2
|
export type ProtocolIdentifier = 'ava-4';
|
|
3
3
|
|
|
4
4
|
export type FactoryOptions = {
|
|
5
|
-
negotiateProtocol
|
|
5
|
+
negotiateProtocol<Data = unknown>(supported: readonly ['ava-4']): Protocol<Data>;
|
|
6
6
|
// Add overloads for additional protocols.
|
|
7
7
|
};
|
|
8
8
|
|