diff --git a/.github/workflows/test_reflective_loader.yaml b/.github/workflows/test_reflective_loader.yaml index 7550fff5f..8e70b85a3 100644 --- a/.github/workflows/test_reflective_loader.yaml +++ b/.github/workflows/test_reflective_loader.yaml @@ -26,7 +26,7 @@ jobs: 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 803eb0e0c..61ba81bde 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 cb69bf3ba..9c3a103ce 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 @@ 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); } @@ -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) || @@ -137,7 +138,7 @@ void defineReflectiveTests(Type type) { } // skip_test_ if (memberName.startsWith('skip_test_')) { - group.addSkippedTest(memberName); + group.addSkippedTest(memberName, memberMirror.testLocation); } }); @@ -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); } } } @@ -304,15 +307,16 @@ 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, @@ -320,7 +324,8 @@ class _Group { 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 @@ 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; + } + } +} diff --git a/pkgs/test_reflective_loader/pubspec.yaml b/pkgs/test_reflective_loader/pubspec.yaml index f63ab0140..262a3498f 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 000000000..14984bb83 --- /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 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"'); + } + } + }); +}