Skip to content

Commit 1222ce9

Browse files
ulkennovemberborn
andauthored
Run tests at selected line numbers
Co-Authored-By: Mark Wubben <[email protected]>
1 parent 75cbc3b commit 1222ce9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+762
-73
lines changed

docs/05-command-line.md

+63-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ Commands:
1515

1616
Positionals:
1717
pattern Glob patterns to select what test files to run. Leave empty if you
18-
want AVA to run all test files instead [string]
18+
want AVA to run all test files instead. Add a colon and specify line
19+
numbers of specific tests to run [string]
1920

2021
Options:
2122
--version Show version number [boolean]
@@ -42,6 +43,7 @@ Options:
4243
Examples:
4344
ava
4445
ava test.js
46+
ava test.js:4,7-9
4547
```
4648

4749
*Note that the CLI will use your local install of AVA when available, even when run globally.*
@@ -149,6 +151,66 @@ test(function foo(t) {
149151
});
150152
```
151153

154+
## Running tests at specific line numbers
155+
156+
AVA lets you run tests exclusively by referring to their line numbers. Target a single line, a range of lines or both. You can select any line number of a test.
157+
158+
The format is a comma-separated list of `[X|Y-Z]` where `X`, `Y` and `Z` are integers between `1` and the last line number of the file.
159+
160+
This feature is only available from the command line. It won't work if you use tools like `ts-node/register` or `@babel/register`, and it does not currently work with `@ava/babel` and `@ava/typescript`.
161+
162+
### Running a single test
163+
164+
To only run a particular test in a file, append the line number of the test to the path or pattern passed to AVA.
165+
166+
Given the following test file:
167+
168+
`test.js`
169+
170+
```js
171+
1: test('unicorn', t => {
172+
2: t.pass();
173+
3: });
174+
4:
175+
5: test('rainbow', t => {
176+
6: t.fail();
177+
7: });
178+
```
179+
180+
Running `npx ava test.js:2` for would run the `unicorn` test. In fact you could use any line number between `1` and `3`.
181+
182+
### Running multiple tests
183+
184+
To run multiple tests, either target them one by one or select a range of line numbers. As line numbers are given per file, you can run multiple files with different line numbers for each file. If the same file is provided multiple times, line numbers are merged and only run once.
185+
186+
### Examples
187+
188+
Single line numbers:
189+
190+
```console
191+
npx ava test.js:2,9
192+
```
193+
194+
Range:
195+
196+
```console
197+
npx ava test.js:4-7
198+
```
199+
200+
Mix of single line number and range:
201+
202+
```console
203+
npx ava test.js:4,9-12
204+
```
205+
206+
Different files:
207+
208+
```console
209+
npx ava test.js:3 test2.js:4,7-9
210+
```
211+
212+
When running a file with and without line numbers, line numbers take precedence.
213+
152214
## Resetting AVA's cache
153215

154216
AVA may cache certain files, especially when you use our [`@ava/babel`](https://github.com/avajs/babel) provider. If it seems like your latest changes aren't being picked up by AVA you can reset the cache by running:

lib/api.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const isCi = require('./is-ci');
1616
const RunStatus = require('./run-status');
1717
const fork = require('./fork');
1818
const serializeError = require('./serialize-error');
19+
const {getApplicableLineNumbers} = require('./line-numbers');
1920

2021
function resolveModules(modules) {
2122
return arrify(modules).map(name => {
@@ -118,7 +119,11 @@ class Api extends Emittery {
118119
if (filter.length === 0) {
119120
selectedFiles = testFiles;
120121
} else {
121-
selectedFiles = globs.applyTestFileFilter({cwd: this.options.projectDir, filter, testFiles});
122+
selectedFiles = globs.applyTestFileFilter({
123+
cwd: this.options.projectDir,
124+
filter: filter.map(({pattern}) => pattern),
125+
testFiles
126+
});
122127
}
123128
}
124129
} catch (error) {
@@ -209,9 +214,11 @@ class Api extends Emittery {
209214
return;
210215
}
211216

217+
const lineNumbers = getApplicableLineNumbers(globs.normalizeFileForMatching(apiOptions.projectDir, file), filter);
212218
const options = {
213219
...apiOptions,
214220
providerStates,
221+
lineNumbers,
215222
recordNewSnapshots: !isCi,
216223
// If we're looking for matches, run every single test process in exclusive-only mode
217224
runOnlyExclusive: apiOptions.match.length > 0 || runtimeOptions.runOnlyExclusive === true
@@ -223,7 +230,7 @@ class Api extends Emittery {
223230
}
224231

225232
const worker = fork(file, options, apiOptions.nodeArguments);
226-
runStatus.observeWorker(worker, file);
233+
runStatus.observeWorker(worker, file, {selectingLines: lineNumbers.length > 0});
227234

228235
pendingWorkers.add(worker);
229236
worker.promise.then(() => {

lib/cli.js

+11-4
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ exports.run = async () => { // eslint-disable-line complexity
120120
})
121121
.command('* [<pattern>...]', 'Run tests', yargs => yargs.options(FLAGS).positional('pattern', {
122122
array: true,
123-
describe: 'Glob patterns to select what test files to run. Leave empty if you want AVA to run all test files instead',
123+
describe: 'Glob patterns to select what test files to run. Leave empty if you want AVA to run all test files instead. Add a colon and specify line numbers of specific tests to run',
124124
type: 'string'
125125
}))
126126
.command(
@@ -143,7 +143,7 @@ exports.run = async () => { // eslint-disable-line complexity
143143
}
144144
}).positional('pattern', {
145145
demand: true,
146-
describe: 'Glob patterns to select a single test file to debug',
146+
describe: 'Glob patterns to select a single test file to debug. Add a colon and specify line numbers of specific tests to run',
147147
type: 'string'
148148
}),
149149
argv => {
@@ -163,6 +163,7 @@ exports.run = async () => { // eslint-disable-line complexity
163163
})
164164
.example('$0')
165165
.example('$0 test.js')
166+
.example('$0 test.js:4,7-9')
166167
.help();
167168

168169
const combined = {...conf};
@@ -263,9 +264,10 @@ exports.run = async () => { // eslint-disable-line complexity
263264
const TapReporter = require('./reporters/tap');
264265
const Watcher = require('./watcher');
265266
const normalizeExtensions = require('./extensions');
266-
const {normalizeGlobs, normalizePatterns} = require('./globs');
267+
const {normalizeGlobs, normalizePattern} = require('./globs');
267268
const normalizeNodeArguments = require('./node-arguments');
268269
const validateEnvironmentVariables = require('./environment-variables');
270+
const {splitPatternAndLineNumbers} = require('./line-numbers');
269271
const providerManager = require('./provider-manager');
270272

271273
let pkg;
@@ -349,7 +351,12 @@ exports.run = async () => { // eslint-disable-line complexity
349351
const match = combined.match === '' ? [] : arrify(combined.match);
350352

351353
const input = debug ? debug.files : (argv.pattern || []);
352-
const filter = normalizePatterns(input.map(fileOrPattern => path.relative(projectDir, path.resolve(process.cwd(), fileOrPattern))));
354+
const filter = input
355+
.map(pattern => splitPatternAndLineNumbers(pattern))
356+
.map(({pattern, ...rest}) => ({
357+
pattern: normalizePattern(path.relative(projectDir, path.resolve(process.cwd(), pattern))),
358+
...rest
359+
}));
353360

354361
const api = new Api({
355362
cacheEnabled: combined.cache !== false,

lib/fork.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ module.exports = (file, options, execArgv = process.execArgv) => {
4848
let forcedExit = false;
4949
const send = evt => {
5050
if (subprocess.connected && !finished && !forcedExit) {
51-
subprocess.send({ava: evt});
51+
subprocess.send({ava: evt}, () => {
52+
// Disregard errors.
53+
});
5254
}
5355
};
5456

lib/globs.js

+15-11
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,27 @@ const defaultIgnoredByWatcherPatterns = [
2323

2424
const buildExtensionPattern = extensions => extensions.length === 1 ? extensions[0] : `{${extensions.join(',')}}`;
2525

26-
function normalizePatterns(patterns) {
26+
function normalizePattern(pattern) {
2727
// Always use `/` in patterns, harmonizing matching across platforms
2828
if (process.platform === 'win32') {
29-
patterns = patterns.map(pattern => slash(pattern));
29+
pattern = slash(pattern);
3030
}
3131

32-
return patterns.map(pattern => {
33-
if (pattern.startsWith('./')) {
34-
return pattern.slice(2);
35-
}
32+
if (pattern.startsWith('./')) {
33+
return pattern.slice(2);
34+
}
3635

37-
if (pattern.startsWith('!./')) {
38-
return `!${pattern.slice(3)}`;
39-
}
36+
if (pattern.startsWith('!./')) {
37+
return `!${pattern.slice(3)}`;
38+
}
4039

41-
return pattern;
42-
});
40+
return pattern;
41+
}
42+
43+
exports.normalizePattern = normalizePattern;
44+
45+
function normalizePatterns(patterns) {
46+
return patterns.map(pattern => normalizePattern(pattern));
4347
}
4448

4549
exports.normalizePatterns = normalizePatterns;

lib/line-numbers.js

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
'use strict';
2+
3+
const micromatch = require('micromatch');
4+
const flatten = require('lodash/flatten');
5+
6+
const NUMBER_REGEX = /^\d+$/;
7+
const RANGE_REGEX = /^(?<startGroup>\d+)-(?<endGroup>\d+)$/;
8+
const LINE_NUMBERS_REGEX = /^(?:\d+(?:-\d+)?,?)+$/;
9+
const DELIMITER = ':';
10+
11+
const distinctArray = array => [...new Set(array)];
12+
const sortNumbersAscending = array => {
13+
const sorted = [...array];
14+
sorted.sort((a, b) => a - b);
15+
return sorted;
16+
};
17+
18+
const parseNumber = string => Number.parseInt(string, 10);
19+
const removeAllWhitespace = string => string.replace(/\s/g, '');
20+
const range = (start, end) => new Array(end - start + 1).fill(start).map((element, index) => element + index);
21+
22+
const parseLineNumbers = suffix => sortNumbersAscending(distinctArray(flatten(
23+
suffix.split(',').map(part => {
24+
if (NUMBER_REGEX.test(part)) {
25+
return parseNumber(part);
26+
}
27+
28+
const {groups: {startGroup, endGroup}} = RANGE_REGEX.exec(part);
29+
const start = parseNumber(startGroup);
30+
const end = parseNumber(endGroup);
31+
32+
if (start > end) {
33+
return range(end, start);
34+
}
35+
36+
return range(start, end);
37+
})
38+
)));
39+
40+
function splitPatternAndLineNumbers(pattern) {
41+
const parts = pattern.split(DELIMITER);
42+
if (parts.length === 1) {
43+
return {pattern, lineNumbers: null};
44+
}
45+
46+
const suffix = removeAllWhitespace(parts.pop());
47+
if (!LINE_NUMBERS_REGEX.test(suffix)) {
48+
return {pattern, lineNumbers: null};
49+
}
50+
51+
return {pattern: parts.join(DELIMITER), lineNumbers: parseLineNumbers(suffix)};
52+
}
53+
54+
exports.splitPatternAndLineNumbers = splitPatternAndLineNumbers;
55+
56+
function getApplicableLineNumbers(normalizedFilePath, filter) {
57+
return sortNumbersAscending(distinctArray(flatten(
58+
filter
59+
.filter(({pattern, lineNumbers}) => lineNumbers && micromatch.isMatch(normalizedFilePath, pattern))
60+
.map(({lineNumbers}) => lineNumbers)
61+
)));
62+
}
63+
64+
exports.getApplicableLineNumbers = getApplicableLineNumbers;

lib/reporters/mini.js

+29-3
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,10 @@ class MiniReporter {
109109
this.failures = [];
110110
this.filesWithMissingAvaImports = new Set();
111111
this.filesWithoutDeclaredTests = new Set();
112+
this.filesWithoutMatchedLineNumbers = new Set();
112113
this.internalErrors = [];
113114
this.knownFailures = [];
115+
this.lineNumberErrors = [];
114116
this.matching = false;
115117
this.prefixTitle = (testFile, title) => title;
116118
this.previousFailures = 0;
@@ -148,6 +150,8 @@ class MiniReporter {
148150
}
149151

150152
consumeStateChange(evt) { // eslint-disable-line complexity
153+
const fileStats = this.stats && evt.testFile ? this.stats.byFile.get(evt.testFile) : null;
154+
151155
switch (evt.type) {
152156
case 'declared-test':
153157
// Ignore
@@ -164,6 +168,10 @@ class MiniReporter {
164168
this.writeWithCounts(colors.error(`${figures.cross} Internal error`));
165169
}
166170

171+
break;
172+
case 'line-number-selection-error':
173+
this.lineNumberErrors.push(evt);
174+
this.writeWithCounts(colors.information(`${figures.warning} Could not parse ${this.relativeFile(evt.testFile)} for line number selection`));
167175
break;
168176
case 'missing-ava-import':
169177
this.filesWithMissingAvaImports.add(evt.testFile);
@@ -203,15 +211,18 @@ class MiniReporter {
203211
this.unhandledRejections.push(evt);
204212
break;
205213
case 'worker-failed':
206-
if (this.stats.byFile.get(evt.testFile).declaredTests === 0) {
214+
if (fileStats.declaredTests === 0) {
207215
this.filesWithoutDeclaredTests.add(evt.testFile);
208216
}
209217

210218
break;
211219
case 'worker-finished':
212-
if (this.stats.byFile.get(evt.testFile).declaredTests === 0) {
220+
if (fileStats.declaredTests === 0) {
213221
this.filesWithoutDeclaredTests.add(evt.testFile);
214222
this.writeWithCounts(colors.error(`${figures.cross} No tests found in ${this.relativeFile(evt.testFile)}`));
223+
} else if (fileStats.selectingLines && fileStats.selectedTests === 0) {
224+
this.filesWithoutMatchedLineNumbers.add(evt.testFile);
225+
this.writeWithCounts(colors.error(`${figures.cross} Line numbers for ${this.relativeFile(evt.testFile)} did not match any tests`));
215226
}
216227

217228
break;
@@ -432,7 +443,22 @@ class MiniReporter {
432443
}
433444
}
434445

435-
if (this.filesWithMissingAvaImports.size > 0 || this.filesWithoutDeclaredTests.size > 0) {
446+
if (this.lineNumberErrors.length > 0) {
447+
for (const evt of this.lineNumberErrors) {
448+
this.lineWriter.writeLine(colors.information(`${figures.warning} Could not parse ${this.relativeFile(evt.testFile)} for line number selection`));
449+
}
450+
}
451+
452+
if (this.filesWithoutMatchedLineNumbers.size > 0) {
453+
for (const testFile of this.filesWithoutMatchedLineNumbers) {
454+
if (!this.filesWithMissingAvaImports.has(testFile) && !this.filesWithoutDeclaredTests.has(testFile)) {
455+
this.lineWriter.writeLine(colors.error(`${figures.cross} Line numbers for ${this.relativeFile(testFile)} did not match any tests`) + firstLinePostfix);
456+
firstLinePostfix = '';
457+
}
458+
}
459+
}
460+
461+
if (this.filesWithMissingAvaImports.size > 0 || this.filesWithoutDeclaredTests.size > 0 || this.filesWithoutMatchedLineNumbers.size > 0) {
436462
this.lineWriter.writeLine();
437463
}
438464

lib/reporters/verbose.js

+5
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ class VerboseReporter {
132132
this.lineWriter.writeLine();
133133
this.lineWriter.writeLine();
134134
break;
135+
case 'line-number-selection-error':
136+
this.lineWriter.writeLine(colors.information(`${figures.warning} Could not parse ${this.relativeFile(evt.testFile)} for line number selection`));
137+
break;
135138
case 'missing-ava-import':
136139
this.filesWithMissingAvaImports.add(evt.testFile);
137140
this.lineWriter.writeLine(colors.error(`${figures.cross} No tests found in ${this.relativeFile(evt.testFile)}, make sure to import "ava" at the top of your test file`));
@@ -203,6 +206,8 @@ class VerboseReporter {
203206
if (!evt.forcedExit && !this.filesWithMissingAvaImports.has(evt.testFile)) {
204207
if (fileStats.declaredTests === 0) {
205208
this.lineWriter.writeLine(colors.error(`${figures.cross} No tests found in ${this.relativeFile(evt.testFile)}`));
209+
} else if (fileStats.selectingLines && fileStats.selectedTests === 0) {
210+
this.lineWriter.writeLine(colors.error(`${figures.cross} Line numbers for ${this.relativeFile(evt.testFile)} did not match any tests`));
206211
} else if (!this.failFastEnabled && fileStats.remainingTests > 0) {
207212
this.lineWriter.writeLine(colors.error(`${figures.cross} ${fileStats.remainingTests} ${plur('test', fileStats.remainingTests)} remaining in ${this.relativeFile(evt.testFile)}`));
208213
}

0 commit comments

Comments
 (0)