[test_reflective_loader] Pass test locations to `pkg:test` to improve IDE navigation (#2090)
diff --git a/.github/workflows/test_reflective_loader.yaml b/.github/workflows/test_reflective_loader.yaml
index 7550fff..8e70b85 100644
--- a/.github/workflows/test_reflective_loader.yaml
+++ b/.github/workflows/test_reflective_loader.yaml
@@ -26,7 +26,7 @@
strategy:
fail-fast: false
matrix:
- sdk: [dev, 3.1]
+ sdk: [dev, 3.5]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
diff --git a/pkgs/test_reflective_loader/CHANGELOG.md b/pkgs/test_reflective_loader/CHANGELOG.md
index 803eb0e..61ba81b 100644
--- a/pkgs/test_reflective_loader/CHANGELOG.md
+++ b/pkgs/test_reflective_loader/CHANGELOG.md
@@ -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`.
diff --git a/pkgs/test_reflective_loader/lib/test_reflective_loader.dart b/pkgs/test_reflective_loader/lib/test_reflective_loader.dart
index cb69bf3..9c3a103 100644
--- a/pkgs/test_reflective_loader/lib/test_reflective_loader.dart
+++ b/pkgs/test_reflective_loader/lib/test_reflective_loader.dart
@@ -87,7 +87,8 @@
{
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);
}
@@ -104,7 +105,7 @@
// 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) ||
@@ -137,7 +138,7 @@
}
// skip_test_
if (memberName.startsWith('skip_test_')) {
- group.addSkippedTest(memberName);
+ group.addSkippedTest(memberName, memberMirror.testLocation);
}
});
@@ -154,7 +155,9 @@
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);
}
}
}
@@ -304,15 +307,16 @@
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,
@@ -320,7 +324,8 @@
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));
}
}
@@ -341,14 +346,26 @@
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;
+ }
+ }
+}
diff --git a/pkgs/test_reflective_loader/pubspec.yaml b/pkgs/test_reflective_loader/pubspec.yaml
index f63ab01..262a349 100644
--- a/pkgs/test_reflective_loader/pubspec.yaml
+++ b/pkgs/test_reflective_loader/pubspec.yaml
@@ -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
diff --git a/pkgs/test_reflective_loader/test/location_test.dart b/pkgs/test_reflective_loader/test/location_test.dart
new file mode 100644
index 0000000..14984bb
--- /dev/null
+++ b/pkgs/test_reflective_loader/test/location_test.dart
@@ -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"');
+ }
+ }
+ });
+}