Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion docs/05-command-line.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Translations: [Français](https://github.com/avajs/ava-docs/blob/master/fr_FR/do
$ npx ava --help

Usage
ava [<file> ...]
ava [<file[:line]> ...]

Options
--watch, -w Re-run tests when tests and source files change
Expand All @@ -27,6 +27,7 @@ $ npx ava --help
Examples
ava
ava test.js test2.js
ava test.js:5 test2.js:4-5,9
ava test-*.js
ava test

Expand Down Expand Up @@ -125,6 +126,48 @@ test(function foo(t) {
});
```

## Running tests residing on a given line

Sometimes during development it's helpful to run a test that is on a specific line. AVA allows you to run tests based on their location in the source code.

Match the test on line `5`:

```console
npx ava test.js:5
```

It is also possible to use a range:

```console
npx ava test.js:5-10
```

Using multiple ranges is also possible:

```console
npx ava test.js:5-10,18,10
```

To run a test you can use any line from where you call `test` until your next call to the `test` function, as an example you can use any of the lines from 3 to 6 in the following code to run `foo`.

```js
1 import test from 'ava';
2
3 test('foo', t => {
4 t.pass();
5 });
6
7 test('bar', async t => {
8 const bar = Promise.resolve('bar');
9 t.is(await bar, 'bar');
10 });
```

> Please note that using any number more than the maximum line-number will execute the last test.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't make this a quote. Use italics instead:

Note that ranges beyond the last line of the file will select the last test.


Using `--match` beside this feature will only match the tests on the given area.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about:

You can combine line ranges with the --match option.



## Resetting AVA's cache

AVA caches the compiled test and helper files. It automatically recompiles these files when you change them. AVA tries its best to detect changes to your Babel configuration files, plugins and presets. If it seems like your latest Babel configuration isn't being applied, however, you can run AVA with the `--reset-cache` flag to reset AVA's cache. If set, all files in the `node_modules/.cache/ava` directory are deleted. Run AVA as normal to apply your new Babel configuration.
Expand Down
10 changes: 8 additions & 2 deletions lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ class Api extends Emittery {
}

async run(files = [], runtimeOptions = {}) {
files = files.map(file => path.resolve(this.options.resolveTestsFrom, file));
const ranges = new Map(runtimeOptions.ranges || []);
files = files.map(file => {
const filename = path.resolve(this.options.resolveTestsFrom, file);
ranges.set(filename, ranges.get(file));
return filename;
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not do this processing in the CLI instead?


const apiOptions = this.options;

Expand Down Expand Up @@ -223,7 +228,8 @@ class Api extends Emittery {
...apiOptions,
recordNewSnapshots: !isCi,
// If we're looking for matches, run every single test process in exclusive-only mode
runOnlyExclusive: apiOptions.match.length > 0 || runtimeOptions.runOnlyExclusive === true
runOnlyExclusive: apiOptions.match.length > 0 || runtimeOptions.runOnlyExclusive === true,
lines: ranges.get(file) || []
};
if (precompilation) {
options.cacheDir = precompilation.cacheDir;
Expand Down
13 changes: 11 additions & 2 deletions lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const meow = require('meow');
const Promise = require('bluebird');
const isCi = require('is-ci');
const loadConfig = require('./load-config');
const rangeParser = require('./range');

// Bluebird specific
Promise.longStackTraces();
Expand Down Expand Up @@ -35,7 +36,7 @@ exports.run = async () => { // eslint-disable-line complexity

const cli = meow(`
Usage
ava [<file> ...]
ava [<file[:line]> ...]

Options
--watch, -w Re-run tests when tests and source files change
Expand All @@ -56,6 +57,7 @@ exports.run = async () => { // eslint-disable-line complexity
Examples
ava
ava test.js test2.js
ava test.js:5 test2.js:4-5,9
ava test-*.js
ava test

Expand Down Expand Up @@ -280,7 +282,14 @@ exports.run = async () => { // eslint-disable-line complexity
});
watcher.observeStdin(process.stdin);
} else {
const runStatus = await api.run(files);
const {filenames, ranges, ignored} = rangeParser.parseFileSelections(files);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about executing this when cli.input is processed? Then in the conf.watch branch we can exit with an error if lines are selected.

if (ignored.length > 0) {
for (const file of ignored) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to add the length check.

console.error(`Ignoring other tests from ${file} since you selected lines.`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't write to the console from here, it messes with the reporters. I think for now we can use exit() here and explain that if you select lines, you shouldn't then also specify the same file without a line selection, and then print the offending files.

}
}

const runStatus = await api.run(filenames, {ranges});
process.exitCode = runStatus.suggestExitCode({matching: match.length > 0});
reporter.endRun();
}
Expand Down
53 changes: 53 additions & 0 deletions lib/range.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use strict';
const rangeParser = require('parse-numeric-range');

function parseFileSelections(files) {
const ranges = new Map();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ranges no longer holds ranges. It holds line numbers. Shall we rename this?

const ignored = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a set, so you don't need to dedupe it later.

const filenames = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make this a set, too.


for (const file of files) {
const [actualFilename, lines] = parseFileSelection(file);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With filenames a set, you can add the actualFilename to it here, regardless of any other condition.


if (!ranges.has(actualFilename)) {
ranges.set(actualFilename, []);
filenames.push(actualFilename);
}

if (lines.length === 0 && ranges.get(actualFilename).length > 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this up, and change the condition to lines.length === 0 && ranges.has(actualFilename).

ignored.push(file);
continue;
}

ranges.set(actualFilename, ranges.get(actualFilename).concat(lines));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We now need to set the first value or otherwise modify the arary:

if (!ranges.has(actualFilename)) {
  ranges.set(actualFilename, lines)
} else {
  ranges.get(actualFileName).push(...lines)
}

}

for (const file of filenames) {
ranges.set(file, [...new Set(ranges.get(file))]);
}

return {
filenames,
ranges,
ignored: [...new Set(ignored)]
};
}

exports.parseFileSelections = parseFileSelections;

const rangeRegExp = /^(\d+(-\d+)?,?)+$/;

function parseFileSelection(file) {
const colonIndex = file.lastIndexOf(':');
const mightHaveRange = colonIndex > -1 && file[colonIndex + 1] !== '\\';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the '\\' check for Windows drives? Could you add that as a comment and perhaps ensure there's a test for it too?

On the other hand, given that a range must start with a digit, do we really need this clause?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That regex I'm using might be a bit heavy so I use a fast and dirty check first and store the result in mightHaveRange, also there is no need for a test if you remove this check (and the regex), most of the tests will fail on windows.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not too fussed about performance overheads, presumably you only run this with a handful of ranges so it'll be really hard to tell what's faster or slower.

here is no need for a test if you remove this check (and the regex), most of the tests will fail on windows.

Are you saying the test is not necessary, or it is because of Windows?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can look at this from when this check was not there https://travis-ci.org/avajs/ava/builds/556309917

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, so let's add a comment explaining this handles file paths on Windows that do not include ranges.

const rangeStr = file.slice(colonIndex + 1);
const actualFilename = file.slice(0, colonIndex);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't really a filename though, it's still a file path. Let's rename this throughout.


if (mightHaveRange && rangeRegExp.test(rangeStr)) {
return [actualFilename, rangeParser.parse(rangeStr)];
}

return [file, []];
}

exports.parseFileSelection = parseFileSelection;
39 changes: 36 additions & 3 deletions lib/runner.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use strict';
const Emittery = require('emittery');
const matcher = require('matcher');
const callsites = require('callsites');
const sourceMapSupport = require('source-map-support');
const ContextRef = require('./context-ref');
const createChain = require('./create-chain');
const snapshotManager = require('./snapshot-manager');
Expand All @@ -15,13 +17,15 @@ class Runner extends Emittery {
this.failWithoutAssertions = options.failWithoutAssertions !== false;
this.file = options.file;
this.match = options.match || [];
this.lines = options.lines || [];
this.projectDir = options.projectDir;
this.recordNewSnapshots = options.recordNewSnapshots === true;
this.runOnlyExclusive = options.runOnlyExclusive === true;
this.serial = options.serial === true;
this.snapshotDir = options.snapshotDir;
this.updateSnapshots = options.updateSnapshots;

this.callsiteLineNumbers = [];
this.activeRunnables = new Set();
this.boundCompareTestSnapshot = this.compareTestSnapshot.bind(this);
this.interrupted = false;
Expand Down Expand Up @@ -52,11 +56,29 @@ class Runner extends Emittery {
if (!scheduledStart) {
scheduledStart = true;
process.nextTick(() => {
this.callsiteLineNumbers.push(Infinity);
this.callsiteLineNumbers.sort((a, b) => a - b);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do this work inside the start() method instead.

We should deduplicate the line numbers as well. It's an edge case but you could specify two tests on the same line.

hasStarted = true;
this.start();
});
}

if (this.lines.length > 0) {
let callsiteLineNumber;

for (const jsCallsite of callsites()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's rename jsCallsite to callsite.

const callsite = sourceMapSupport.wrapCallSite(jsCallsite);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And rename this to wrapped.

const fileName = callsite.getFileName();
if (jsCallsite === this.file || fileName === this.file) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean jsCallsite.getFileName()?

I don't think this needs to be compared to the source-map converted filename. You can't pass those on the CLI since you need to provide the file that AVA can actually load.

callsiteLineNumber = callsite.getLineNumber();
break;
}
}

metadata.callsiteLineNumber = callsiteLineNumber;
this.callsiteLineNumbers.push(callsiteLineNumber);
}

const specifiedTitle = typeof args[0] === 'string' ?
args.shift() :
undefined;
Expand Down Expand Up @@ -347,10 +369,21 @@ class Runner extends Emittery {
}

async start() {
const isIgnoredByLineSelection = task => {
if (this.lines.length === 0) {
return false;
}

const lineNumber = task.metadata.callsiteLineNumber;
const endingLineNumber = this.callsiteLineNumbers[this.callsiteLineNumbers.indexOf(lineNumber) + 1] - 1;

return !this.lines.some(line => line >= lineNumber && line <= endingLineNumber);
};

const concurrentTests = [];
const serialTests = [];
for (const task of this.tasks.serial) {
if (this.runOnlyExclusive && !task.metadata.exclusive) {
if (isIgnoredByLineSelection(task) || (this.runOnlyExclusive && !task.metadata.exclusive)) {
continue;
}

Expand All @@ -368,7 +401,7 @@ class Runner extends Emittery {
}

for (const task of this.tasks.concurrent) {
if (this.runOnlyExclusive && !task.metadata.exclusive) {
if (isIgnoredByLineSelection(task) || (this.runOnlyExclusive && !task.metadata.exclusive)) {
continue;
}

Expand All @@ -390,7 +423,7 @@ class Runner extends Emittery {
}

for (const task of this.tasks.todo) {
if (this.runOnlyExclusive && !task.metadata.exclusive) {
if (isIgnoredByLineSelection(task) || (this.runOnlyExclusive && !task.metadata.exclusive)) {
continue;
}

Expand Down
1 change: 1 addition & 0 deletions lib/worker/subprocess.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ ipc.options.then(options => {
failFast: options.failFast,
failWithoutAssertions: options.failWithoutAssertions,
file: options.file,
lines: options.lines,
match: options.match,
projectDir: options.projectDir,
recordNewSnapshots: options.recordNewSnapshots,
Expand Down
14 changes: 9 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"array-uniq": "^2.1.0",
"arrify": "^2.0.1",
"bluebird": "^3.5.5",
"callsites": "^3.1.0",
"chalk": "^2.4.2",
"chokidar": "^3.0.2",
"chunkd": "^1.0.0",
Expand Down Expand Up @@ -116,6 +117,7 @@
"observable-to-promise": "^1.0.0",
"ora": "^3.4.0",
"package-hash": "^4.0.0",
"parse-numeric-range": "0.0.2",
"pkg-conf": "^3.1.0",
"plur": "^3.1.1",
"pretty-ms": "^5.0.0",
Expand Down
22 changes: 22 additions & 0 deletions test/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -1212,3 +1212,25 @@ test('`esm` package support', t => {
t.is(runStatus.stats.passedTests, 1);
});
});

test('`range` should filter tests', t => {
const api = apiCreator();
const tests = [];

api.on('run', plan => {
plan.status.on('stateChange', evt => {
if (evt.type === 'test-failed' || evt.type === 'test-passed') {
tests.push(evt.title);
}
});
});

const filename = path.join(__dirname, 'fixture/range.js');

return api.run([filename], {ranges: new Map([[filename, [8]]])})
.then(runStatus => {
t.strictDeepEqual(tests, ['unicorn']);
t.is(runStatus.stats.passedTests, 1);
t.is(runStatus.stats.declaredTests, 3);
});
});
14 changes: 14 additions & 0 deletions test/fixture/range.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import test from '../..';

test('foo', t => {
t.pass();
});

test('unicorn', t => {
t.pass();
});

test('rainbow', t => {
t.is(1, 1);
t.pass();
});
Loading