Skip to content

Conversation

@qti3e
Copy link

@qti3e qti3e commented Jul 8, 2019

🦄 Fixes: #2138

There are just a few edges case that I found it useful to have your comments on :)

  • Currently, negative numbers are just ignored, maybe throw an error or something?
  • What should happen if the :... is actually part of the file name, (test.js:5 a valid filename on Linux at least)!?
  • When running in the watch mode, the line number might change and results in different tests to be executed.
  • Due to the implementation using line numbers that are higher than the actual number of lines in the file will result in the last test being executed, it would be nice to have a way to get line numbers in the file without the overhead of reading the file from disk.

Also, there is definitely room for tests to be improved, I couldn't actually get very useful information from runStatus.stats for what I had in mind, (name of executed tests) if the current test is not enough I'd be happy to hear about a way to get that information from the API.


IssueHunt Summary

@sindresorhus
Copy link
Member

Negative numbers should throw a user-friendly error.

@sindresorhus
Copy link
Member

While test.js:5 might be a valid file name on Linux, it’s not something anyone would actually use. It’s not a concern.

@sindresorhus
Copy link
Member

The range syntax should work for the files setting in the AVA config too. I don’t see that documented.

@sindresorhus
Copy link
Member

I’m not sure it makes sense at all to allow the range syntax when using watch.

@qti3e qti3e force-pushed the q/ln branch 2 times, most recently from a48a3b5 to 561d222 Compare July 11, 2019 08:54
@qti3e
Copy link
Author

qti3e commented Jul 11, 2019

@sindresorhus I guess everything works as expected now, I moved the whole parser logic to a new file, and also added the support for ranges in glob as well as in the files argument in API.run() so that this range syntax thing works everywhere the same way (configs, CLI, etc.)

That being said now we can expect a glob pattern such as test-*.js:5 to work just fine.

There is only one option added to the runner named lines, which determines which lines are expected to be executed (an empty array means every line).

There is no negative number allowed in a range string, which means test.js:-4 is treated as a file with the name test.js:-4 rather than line -4 in test.js and well that leads to a file not found status, which I believe is best to be addressed as part of #2158 (which I'm going to take right after this PR if that's ok), and I'd rather avoid doing this error handling as part of this PR to keep it as simple as it is and keep it clean.

/c.c @novemberborn

Copy link
Member

@novemberborn novemberborn left a comment

Choose a reason for hiding this comment

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

Hi @qti3e, thanks for the PR!

The range syntax should work for the files setting in the AVA config too. I don’t see that documented.

@sindresorhus I don't see how this is useful. If you want to always run part of a test file, make a new test file. Our globbing logic is still really complex and I don't think we should add more complexity to it.

I’m not sure it makes sense at all to allow the range syntax when using watch.

Agreed. Another argument for not allowing it in the configuration. Then, we can enforce this in lib/cli.js by looking for watch mode and the presence of ranges in the provided files.

Having reviewed the PR it definitely looks easier to implement and maintain if we only support this syntax on the CLI.


… that leads to a file not found status, which I believe is best to be addressed as part of #2158 (which I'm going to take right after this PR if that's ok)

@qti3e see #2184.

  • Due to the implementation using line numbers that are higher than the actual number of lines in the file will result in the last test being executed, it would be nice to have a way to get line numbers in the file without the overhead of reading the file from disk.

If we restrict this feature to files passed on the CLI, it's definitely legit to parse the file counting its lines.

lib/range.js Outdated
for (const file of files) {
const [actualFilename, rangeStr] = splitRangeStr(file);

if (any.has(actualFilename)) {
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 consider merging the ranges?

lib/range.js Outdated

function parseRange(files) {
const any = new Set();
const ranges = {};
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 use a Map instead.

lib/range.js Outdated
const union = require('array-union');

function parseRange(files) {
const any = new Set();
Copy link
Member

Choose a reason for hiding this comment

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

What does any mean? I can't quite tell from the logic below.

Copy link
Author

Choose a reason for hiding this comment

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

@nov parseRange should work on both ranged paths and normal paths, imagine we have an input like ['a.js', 'a.js:4'] in this case the a.js path has a higher priority and should cancel out a.js, this set is used to store file paths that do not have a range syntax in them.

Copy link
Member

Choose a reason for hiding this comment

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

I see. Should that maybe print an error instead? Or perhaps we could run only the selected lines and print a warning "ignoring other tests from a.js since you selected lines" (well not quite that, but you get the idea).

lib/range.js Outdated
const lines = rangeParser.parse(rangeStr);

if (ranges[actualFilename]) {
ranges[actualFilename] = union(ranges[actualFilename], lines);
Copy link
Member

Choose a reason for hiding this comment

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

Rather than using array-union you could concatenate the arrays, and then pass them through a [...newSet(lines)] at the very end to get unique values out.

lib/range.js Outdated
continue;
}

const lines = rangeParser.parse(rangeStr);
Copy link
Member

Choose a reason for hiding this comment

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

Why isn't the range string parsed in splitRangeStr()?

That function could be rename to parseFileSelection(), returning a file and extracted lines, and then this parseRange() function could be renamed to processFileSelections() or something. Maybe rename the range module too.

Copy link
Author

Choose a reason for hiding this comment

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

Because of how this implementation handles globs I needed to have the range part as a string, and of course, we need another function to parse that part.

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 this will be a lot simpler without globs 😄

lib/runner.js Outdated
for (const jsCallsite of callsites()) {
const callsite = sourceMapSupport.wrapCallSite(jsCallsite);
const fileName = callsite.getFileName();
if (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.

I imagine this will break when using AVA with pre-compiled test files. The selected file may be build/test.js but source-map-support may translate the callsite to src/test.js. You should check the files before the source map is applied.

This also needs a comment to explain why we need to check the files in the first place (because the test() may be invoked in a helper file — which is its own edge case that I wouldn't know how to deal with).

Copy link
Author

Choose a reason for hiding this comment

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

We can't check the file before the source map is applied after the translation line numbers are all changed, (a.js:5 might end up running line 6.)

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 check the file before the source map is applied after the translation line numbers are all changed, (a.js:5 might end up running line 6.)

Yes I get that. My point though is that applying the source map may change the fileName of the call site.

lib/runner.js Outdated
// get the last line of the previous test.
const range = [lastCallsiteLineNumber, callsiteLineNumber - 1];
const includedInRange = this.lines.some(n => n >= range[0] && n <= range[1]);
lastMetadata.exclusive = this.match.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.

exclusive will be true if the test is declared using .only(). But, crucially, tests can also be skipped.

I think the ranges should narrow down from the initial test selection. If there are exclusive tests, then only those that match the ranges should be run. If there are no exclusive tests, then skipped tests should never be run.

lib/runner.js Outdated
}

lastCallsiteLineNumber = callsiteLineNumber;
lastMetadata = metadata;
Copy link
Member

Choose a reason for hiding this comment

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

Rather than tracking the last metedata object and callsite, perhaps you could add the callsite number to the metadata, and then adjust the test selection once all tests have been declared?

lib/runner.js Outdated

if (lastMetadata) {
// `callsiteLineNumber` is the beginning of the current test, -1 to
// get the last line of the previous test.
Copy link
Member

Choose a reason for hiding this comment

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

This does include whitespace, but I think that's OK for now.

Copy link
Author

Choose a reason for hiding this comment

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

I don't think there is a way to handle this case unless you parse the source file to it's AST and then extract when the function call actually ends (this information is not in the callsite data.)

@qti3e
Copy link
Author

qti3e commented Jul 18, 2019

@novemberborn Please let me know your final decision about dropping ranged globs.
(Because now it's done and it works really well, anyway just let me know and I'll drop it.)

@sindresorhus
Copy link
Member

I don't see how this is useful. If you want to always run part of a test file, make a new test file. Our globbing logic is still really complex and I don't think we should add more complexity to it.

I agree. I didn't think that through when first commenting.

Having reviewed the PR it definitely looks easier to implement and maintain if we only support this syntax on the CLI.

👍

@sindresorhus
Copy link
Member

There is no negative number allowed in a range string, which means test.js:-4 is treated as a file with the name test.js:-4 rather than line -4 in test.js and well that leads to a file not found status

I think that should be handled here. It's clearly not a valid filename, so we should give the user a helpful and relevant error message that it's an invalid range.

@qti3e
Copy link
Author

qti3e commented Jul 26, 2019

@novemberborn PTAL, also please restart that Travis task.

Copy link
Member

@novemberborn novemberborn left a comment

Choose a reason for hiding this comment

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

Very nice, I think we're getting there @qti3e!

There is no negative number allowed in a range string, which means test.js:-4 is treated as a file with the name test.js:-4 rather than line -4 in test.js and well that leads to a file not found status

I think that should be handled here. It's clearly not a valid filename, so we should give the user a helpful and relevant error message that it's an invalid range.

I agree with this.

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.


> Please note that using any number more than the maximum line-number will execute 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.

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)
}

function parseFileSelections(files) {
const ranges = new Map();
const ignored = [];
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.

const filenames = [];

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.

for (const jsCallsite of callsites()) {
const callsite = sourceMapSupport.wrapCallSite(jsCallsite);
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.

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.

let callsiteLineNumber;

for (const jsCallsite of callsites()) {
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.

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.

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?

@novemberborn
Copy link
Member

Closing due to inactivity. @qti3e I do hope you'll find the time to come back to this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add ability to run a test residing on a given line

3 participants