Skip to content

[test_reflective_loader] Pass test locations to pkg:test to improve IDE navigation #2090

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 6 commits into from
May 22, 2025
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/test_reflective_loader.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
strategy:
fail-fast: false
matrix:
sdk: [dev, 3.1]
sdk: [dev, 3.5]

steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
Expand Down
7 changes: 7 additions & 0 deletions pkgs/test_reflective_loader/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 0.3.0

- Require Dart `^3.5.0`.
- Update to `package:test` 1.26.1.
- Pass locations of groups/tests to `package:test` to improve locations reported
in the JSON reporter that may be used for navigation in IDEs.

## 0.2.3

- Require Dart `^3.1.0`.
Expand Down
37 changes: 27 additions & 10 deletions pkgs/test_reflective_loader/lib/test_reflective_loader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ void defineReflectiveTests(Type type) {
{
var isSolo = _hasAnnotationInstance(classMirror, soloTest);
var className = MirrorSystem.getName(classMirror.simpleName);
group = _Group(isSolo, _combineNames(_currentSuiteName, className));
group = _Group(isSolo, _combineNames(_currentSuiteName, className),
classMirror.testLocation);
_currentGroups.add(group);
}

Expand All @@ -104,7 +105,7 @@ void defineReflectiveTests(Type type) {
// test_
if (memberName.startsWith('test_')) {
if (_hasSkippedTestAnnotation(memberMirror)) {
group.addSkippedTest(memberName);
group.addSkippedTest(memberName, memberMirror.testLocation);
} else {
group.addTest(isSolo, memberName, memberMirror, () {
if (_hasFailingTestAnnotation(memberMirror) ||
Expand Down Expand Up @@ -137,7 +138,7 @@ void defineReflectiveTests(Type type) {
}
// skip_test_
if (memberName.startsWith('skip_test_')) {
group.addSkippedTest(memberName);
group.addSkippedTest(memberName, memberMirror.testLocation);
}
});

Expand All @@ -154,7 +155,9 @@ void _addTestsIfTopLevelSuite() {
for (var test in group.tests) {
if (allTests || test.isSolo) {
test_package.test(test.name, test.function,
timeout: test.timeout, skip: test.isSkipped);
timeout: test.timeout,
skip: test.isSkipped,
location: test.location);
}
}
}
Expand Down Expand Up @@ -304,23 +307,25 @@ class _AssertFailingTest {
class _Group {
final bool isSolo;
final String name;
final test_package.TestLocation? location;
final List<_Test> tests = <_Test>[];

_Group(this.isSolo, this.name);
_Group(this.isSolo, this.name, this.location);

bool get hasSoloTest => tests.any((test) => test.isSolo);

void addSkippedTest(String name) {
void addSkippedTest(String name, test_package.TestLocation? location) {
var fullName = _combineNames(this.name, name);
tests.add(_Test.skipped(isSolo, fullName));
tests.add(_Test.skipped(isSolo, fullName, location));
}

void addTest(bool isSolo, String name, MethodMirror memberMirror,
_TestFunction function) {
var fullName = _combineNames(this.name, name);
var timeout =
_getAnnotationInstance(memberMirror, TestTimeout) as TestTimeout?;
tests.add(_Test(isSolo, fullName, function, timeout?._timeout));
tests.add(_Test(isSolo, fullName, function, timeout?._timeout,
memberMirror.testLocation));
}
}

Expand All @@ -341,14 +346,26 @@ class _Test {
final String name;
final _TestFunction function;
final test_package.Timeout? timeout;
final test_package.TestLocation? location;

final bool isSkipped;

_Test(this.isSolo, this.name, this.function, this.timeout)
_Test(this.isSolo, this.name, this.function, this.timeout, this.location)
: isSkipped = false;

_Test.skipped(this.isSolo, this.name)
_Test.skipped(this.isSolo, this.name, this.location)
: isSkipped = true,
function = (() {}),
timeout = null;
}

extension on DeclarationMirror {
test_package.TestLocation? get testLocation {
if (location case var location?) {
return test_package.TestLocation(
location.sourceUri, location.line, location.column);
} else {
return null;
}
}
}
7 changes: 4 additions & 3 deletions pkgs/test_reflective_loader/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
name: test_reflective_loader
version: 0.2.3
version: 0.3.0
description: Support for discovering tests and test suites using reflection.
repository: https://github.com/dart-lang/tools/tree/main/pkgs/test_reflective_loader
issue_tracker: https://github.com/dart-lang/tools/labels/package%3Atest_reflective_loader

environment:
sdk: ^3.1.0
sdk: ^3.5.0

dependencies:
test: ^1.16.0
test: ^1.26.1

dev_dependencies:
dart_flutter_team_lints: ^3.0.0
path: ^1.8.0
69 changes: 69 additions & 0 deletions pkgs/test_reflective_loader/test/location_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) 2025, 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.

import 'dart:convert';
import 'dart:io';
import 'dart:isolate';

import 'package:path/path.dart' as path;
import 'package:test/test.dart';

void main() {
test("reports correct locations in the JSON output from 'dart test'",
() async {
var testPackagePath = (await Isolate.resolvePackageUri(
Uri.parse('package:test_reflective_loader/')))!
.toFilePath();
var testFilePath = path.normalize(path.join(
testPackagePath, '..', 'test', 'test_reflective_loader_test.dart'));
var testFileContent = File(testFilePath).readAsLinesSync();
var result = await Process.run(
Platform.resolvedExecutable, ['test', '-r', 'json', testFilePath]);

var error = result.stderr.toString().trim();
var output = result.stdout.toString().trim();

expect(error, isEmpty);
expect(output, isNotEmpty);

for (var event in LineSplitter.split(output).map(jsonDecode)) {
if (event case {'type': 'testStart', 'test': Map<String, Object?> test}) {
var name = test['name'] as String;

// Skip the "loading" test, it never has a location.
if (name.startsWith('loading')) {
continue;
}

// Split just the method name from the combined test so we can search
// the source code to ensure the locations match up.
name = name.split('|').last.trim();

// Expect locations for all remaining fields.
var url = test['url'] as String;
var line = test['line'] as int;
var column = test['column'] as int;

expect(path.equals(Uri.parse(url).toFilePath(), testFilePath), isTrue);

// Verify the location provided matches where this test appears in the
// file.
var lineContent = testFileContent[line - 1];
// If the line is an annotation, skip to the next line
if (lineContent.trim().startsWith('@')) {
lineContent = testFileContent[line];
}
expect(lineContent, contains(name),
reason: 'JSON reports test $name on line $line, '
'but line content is "$lineContent"');

// Verify the column too.
var columnContent = lineContent.substring(column - 1);
expect(columnContent, contains(name),
reason: 'JSON reports test $name at column $column, '
'but text at column is "$columnContent"');
}
}
});
}