Skip to content

Allow running tests by line and column #1604

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Oct 19, 2021
Merged
Show file tree
Hide file tree
Changes from 3 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
8 changes: 6 additions & 2 deletions pkgs/test/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
## 1.19.0-dev

* Support query parameters `name` and `full-name` on test paths, which will
apply the filters to only those test suites.
* Support query parameters `name`, `full-name`, `line`, and `col` on test paths,
which will apply the filters to only those test suites.
* All specified filters must match for a test to run.
* Global filters (ie: `--name`) are also still respected and must match.
* The `line` and `col` will match if any frame from the test trace matches
(the test trace is the current stack trace where `test` is invoked).
* If either `line` or `col` are used, stack trace chaining is enabled for
that suite, regardless of the global setting.
* Give a better exception when using `markTestSkipped` outside of a test.

## 1.18.2
Expand Down
22 changes: 18 additions & 4 deletions pkgs/test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,17 +173,31 @@ description (including any group descriptions) match that regular expression
will be run. You can also use the `-N` flag to run tests whose names contain a
plain-text string.

Alternatively, you can filter tests by name only for a specific file/directory
by specifying a query on a path: `dart test "path/to/test.dart?name=test name"`.
That ensures that the name filters applies to one path but not others.

By default, tests are run in the Dart VM, but you can run them in the browser as
well by passing `dart test -p chrome path/to/test.dart`. `test` will take
care of starting the browser and loading the tests, and all the results will be
reported on the command line just like for VM tests. In fact, you can even run
tests on both platforms with a single command: `dart test -p "chrome,vm"
path/to/test.dart`.

### Test Path Queries

Some query parameters are supported on test paths, which allow you to filter the
tests that will run within just those paths.

- **name**: Works the same as `--name` (simple contains check).
- This is the only option that supports more than one entry.
- **full-name**: Requires an match for the name of the test.
- **line**: Matches any test that originates from this line in the test suite.
- Not supported for browser tests.
- **col**: Matches any test that originates from this column in the test suite.
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 confused who would use this? Is this a specific column? Like the position of the first t in test(?

Copy link
Contributor Author

@jakemac53 jakemac53 Oct 15, 2021

Choose a reason for hiding this comment

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

Yes - only IDEs would use it most likely

Copy link
Contributor

Choose a reason for hiding this comment

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

The reason for it is protection against accidentally running tests the user doesn't expect if they have duplicate names:

// Click "Run" next to this test currently runs both tests, which seems odd to the user
test('foo', () {});

test('foo', () {});

Although the IDE will need to be sure of the package:test version before trying to use these new features.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ya and having the col option specifically guards against having two tests on the same line. A rare situation but its trivial to support once we are already supporting filtering by line.

Copy link
Member

Choose a reason for hiding this comment

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

I still don't understand the precise position this matches.

Is it exactly the first t in test? Is it 0 based? Does this match the same frame as the line number, or can the line match one frame and the column match a different frame?

I'll read through the test cases tomorrow to find out more, but I do think we should expand the user facing docs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It matches the column position of the function invocation in the stack trace. That is the position of the first character of the name of the function (so if your cursor is to the left of it). It is one based, which matches what the IDE reports (or at least VsCode, I assume this is standard?).

I'll read through the test cases tomorrow to find out more, but I do think we should expand the user facing docs.

There is a sentence after these that says "all filters must match for a test to be ran". I could move it to the top?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok I added a mini section under this describing the specific line/col semantics, and also moved the note about all filters matching up to the first paragraph.

- Not supported for browser tests.

**Example Usage**: `dart test "path/to/test.dart?line=10&col=2"`

These filters are merged with any global options that are passed, and all
filters must match for a test to be ran.

### Sharding Tests
Tests can also be sharded with the `--total-shards` and `--shard-index` arguments,
allowing you to split up your test suites and run them separately. For example,
Expand Down
319 changes: 319 additions & 0 deletions pkgs/test/test/runner/line_and_col_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
// Copyright (c) 20121, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

@TestOn('vm')

import 'package:test_descriptor/test_descriptor.dart' as d;

import 'package:test_core/src/util/exit_codes.dart' as exit_codes;
import 'package:test/test.dart';

import '../io.dart';

void main() {
group('with test.dart?line=<line> query', () {
test('selects test with the matching line', () async {
await d.file('test.dart', '''
import 'package:test/test.dart';

void main() {
test("a", () {});
test("b", () => throw TestFailure("oh no"));
test("c", () {});
}
''').create();

var test = await runTest(['test.dart?line=6']);

expect(
test.stdout,
emitsThrough(contains('+1: All tests passed!')),
);

await test.shouldExit(0);
});

test('selects multiple tests on the same line', () async {
await d.file('test.dart', '''
import 'package:test/test.dart';

void main() {
test("a", () {}); test("b", () {});
test("c", () => throw TestFailure("oh no"));
}
''').create();

var test = await runTest(['test.dart?line=4']);

expect(
test.stdout,
emitsThrough(contains('+2: All tests passed!')),
);

await test.shouldExit(0);
});

test('selects groups with a matching line', () async {
await d.file('test.dart', '''
import 'package:test/test.dart';

void main() {
group("a", () {
test("b", () {});
});
group("b", () {
test("b", () => throw TestFailure("oh no"));
});
}
''').create();

var test = await runTest(['test.dart?line=4']);

expect(
test.stdout,
emitsThrough(contains('+1: All tests passed!')),
);

await test.shouldExit(0);
});

test('No matching tests', () async {
await d.file('test.dart', '''
import 'package:test/test.dart';

void main() {
test("a", () {});
}
''').create();

var test = await runTest(['test.dart?line=1']);

expect(
test.stderr,
emitsThrough(contains('No tests were found.')),
);

await test.shouldExit(exit_codes.noTestsRan);
});

test('allows the line anywhere in the stack trace', () async {
await d.file('test.dart', '''
import 'package:test/test.dart';

void runTest(String name) {
test(name, () {});
}

void main() {
runTest("a");
test("b", () {});
}
''').create();

var test = await runTest(['test.dart?line=8']);

expect(
test.stdout,
emitsThrough(contains('+1: All tests passed!')),
);

await test.shouldExit(0);
});
});

group('with test.dart?col=<col> query', () {
test('selects single test with the matching column', () async {
await d.file('test.dart', '''
import 'package:test/test.dart';

void main() {
test("a", () {});
test("b", () => throw TestFailure("oh no"));
}
''').create();

var test = await runTest(['test.dart?col=11']);

expect(
test.stdout,
emitsThrough(contains('+1: All tests passed!')),
);

await test.shouldExit(0);
});

test('selects multiple tests starting on the same column', () async {
await d.file('test.dart', '''
import 'package:test/test.dart';

void main() {
test("a", () {});
test("b", () {});
test("c", () => throw TestFailure("oh no"));
}
''').create();

var test = await runTest(['test.dart?col=11']);

expect(
test.stdout,
emitsThrough(contains('+2: All tests passed!')),
);

await test.shouldExit(0);
});

test('selects groups with a matching column', () async {
await d.file('test.dart', '''
import 'package:test/test.dart';

void main() {
group("a", () {
test("b", () {});
});
group("b", () {
test("b", () => throw TestFailure("oh no"));
});
}
''').create();

var test = await runTest(['test.dart?col=11']);

expect(
test.stdout,
emitsThrough(contains('+1: All tests passed!')),
);

await test.shouldExit(0);
});

test('No matching tests', () async {
await d.file('test.dart', '''
import 'package:test/test.dart';

void main() {
test("a", () {});
}
''').create();

var test = await runTest(['test.dart?col=1']);

expect(
test.stderr,
emitsThrough(contains('No tests were found.')),
);

await test.shouldExit(exit_codes.noTestsRan);
});

test('allows the col anywhere in the stack trace', () async {
await d.file('test.dart', '''
import 'package:test/test.dart';

void runTest(String name) {
test(name, () {});
}

void main() {
runTest("a");
test("b", () => throw TestFailure("oh no"));
}
''').create();

var test = await runTest(['test.dart?col=13']);

expect(
test.stdout,
emitsThrough(contains('+1: All tests passed!')),
);

await test.shouldExit(0);
});
});

group('with test.dart?line=<line>&col=<col> query', () {
test('selects test with the matching line and col', () async {
await d.file('test.dart', '''
import 'package:test/test.dart';

void main() {
test("a", () {});test("b", () => throw TestFailure("oh no"));
test("b", () => throw TestFailure("oh no"));
}
''').create();

var test = await runTest(['test.dart?line=4&col=11']);

expect(
test.stdout,
emitsThrough(contains('+1: All tests passed!')),
);

await test.shouldExit(0);
});

test('selects group with the matching line and col', () async {
await d.file('test.dart', '''
import 'package:test/test.dart';

void main() {
group("a", () {
test("b", () {});
test("c", () {});
});
group("d", () {
test("e", () => throw TestFailure("oh no"));
});
}
''').create();

var test = await runTest(['test.dart?line=4&col=11']);

expect(
test.stdout,
emitsThrough(contains('+2: All tests passed!')),
);

await test.shouldExit(0);
});

test('no matching tests - col doesnt match', () async {
await d.file('test.dart', '''
import 'package:test/test.dart';

void main() {
test("a", () {});
}
''').create();

var test = await runTest(['test.dart?line=4&col=1']);

expect(
test.stderr,
emitsThrough(contains('No tests were found.')),
);

await test.shouldExit(exit_codes.noTestsRan);
});

test('no matching tests - line doesnt match', () async {
await d.file('test.dart', '''
import 'package:test/test.dart';

void main() {
test("a", () {});
}
''').create();

var test = await runTest(['test.dart?line=1&col=11']);

expect(
test.stderr,
emitsThrough(contains('No tests were found.')),
);

await test.shouldExit(exit_codes.noTestsRan);
});
});
}
4 changes: 4 additions & 0 deletions pkgs/test/test/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ SuiteConfiguration suiteConfiguration(
BooleanSelector? excludeTags,
Map<BooleanSelector, SuiteConfiguration>? tags,
Map<PlatformSelector, SuiteConfiguration>? onPlatform,
int? line,
int? col,

// Test-level configuration
Timeout? timeout,
Expand All @@ -250,6 +252,8 @@ SuiteConfiguration suiteConfiguration(
excludeTags: excludeTags,
tags: tags,
onPlatform: onPlatform,
line: line,
col: col,
timeout: timeout,
verboseTrace: verboseTrace,
chainStackTraces: chainStackTraces,
Expand Down
Loading