Skip to content

Commit 5f9fe11

Browse files
authored
Add capabilities for building direct test runners (#1332)
Towards #1310, #1328 Add `directRunTest` to configure a reporter and run tests directly in the same isolate. Add `enumerateTestCases` to collect test names without running them, and `directRunSingleTest` to run a specific test by its full name. These APIs ensure the uniqueness of test names to avoid ambiguity. This restriction may be spread to tests run through the normal test runner as well. - Add `fullTestName` option on `Declarer`. When used, only the test (or tests if uniqueness is not checked separately) will be considered as a test case. - Add `directRunTest`, `enumerateTestCases`, and `directRunSingleTest` APIs. These are kept under a `lib/src/` import for now, and any other package that uses these APIs should pin to a specific version of `package:test_core`. The details of these APIs might change without a major version bump.
1 parent 2214ebc commit 5f9fe11

File tree

6 files changed

+190
-12
lines changed

6 files changed

+190
-12
lines changed

pkgs/test_api/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.2.19-nullsafety.3-dev
2+
3+
* Add capability to filter to a single exact test name in `Declarer`.
4+
15
## 0.2.19-nullsafety.2
26

37
* Allow `2.10` stable and `2.11.0-dev` SDKs.

pkgs/test_api/lib/src/backend/declarer.dart

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,17 @@ class Declarer {
9191
/// Whether any tests and/or groups have been flagged as solo.
9292
bool get _solo => _soloEntries.isNotEmpty;
9393

94+
/// An exact full test name to match.
95+
///
96+
/// When non-null only tests with exactly this name will be considered. The
97+
/// full test name is the combination of the test case name with all group
98+
/// prefixes. All other tests, including their metadata like `solo`, is
99+
/// ignored. Uniqueness is not guaranteed so this may match more than one
100+
/// test.
101+
///
102+
/// Groups which are not a strict prefix of this name will be ignored.
103+
final String? _fullTestName;
104+
94105
/// The current zone-scoped declarer.
95106
static Declarer? get current => Zone.current[#test.declarer] as Declarer?;
96107

@@ -113,18 +124,28 @@ class Declarer {
113124
{Metadata? metadata,
114125
Set<String>? platformVariables,
115126
bool collectTraces = false,
116-
bool noRetry = false})
127+
bool noRetry = false,
128+
String? fullTestName})
117129
: this._(
118130
null,
119131
null,
120132
metadata ?? Metadata(),
121133
platformVariables ?? const UnmodifiableSetView.empty(),
122134
collectTraces,
123135
null,
124-
noRetry);
125-
126-
Declarer._(this._parent, this._name, this._metadata, this._platformVariables,
127-
this._collectTraces, this._trace, this._noRetry);
136+
noRetry,
137+
fullTestName);
138+
139+
Declarer._(
140+
this._parent,
141+
this._name,
142+
this._metadata,
143+
this._platformVariables,
144+
this._collectTraces,
145+
this._trace,
146+
this._noRetry,
147+
this._fullTestName,
148+
);
128149

129150
/// Runs [body] with this declarer as [Declarer.current].
130151
///
@@ -143,6 +164,11 @@ class Declarer {
143164
bool solo = false}) {
144165
_checkNotBuilt('test');
145166

167+
final fullName = _prefix(name);
168+
if (_fullTestName != null && fullName != _fullTestName) {
169+
return;
170+
}
171+
146172
var newMetadata = Metadata.parse(
147173
testOn: testOn,
148174
timeout: timeout,
@@ -152,8 +178,7 @@ class Declarer {
152178
retry: _noRetry ? 0 : retry);
153179
newMetadata.validatePlatformSelectors(_platformVariables);
154180
var metadata = _metadata.merge(newMetadata);
155-
156-
_entries.add(LocalTest(_prefix(name), metadata, () async {
181+
_entries.add(LocalTest(fullName, metadata, () async {
157182
var parents = <Declarer>[];
158183
for (Declarer? declarer = this;
159184
declarer != null;
@@ -195,6 +220,11 @@ class Declarer {
195220
bool solo = false}) {
196221
_checkNotBuilt('group');
197222

223+
final fullTestPrefix = _prefix(name);
224+
if (_fullTestName != null && !_fullTestName!.startsWith(fullTestPrefix)) {
225+
return;
226+
}
227+
198228
var newMetadata = Metadata.parse(
199229
testOn: testOn,
200230
timeout: timeout,
@@ -206,8 +236,8 @@ class Declarer {
206236
var metadata = _metadata.merge(newMetadata);
207237
var trace = _collectTraces ? Trace.current(2) : null;
208238

209-
var declarer = Declarer._(this, _prefix(name), metadata, _platformVariables,
210-
_collectTraces, trace, _noRetry);
239+
var declarer = Declarer._(this, fullTestPrefix, metadata,
240+
_platformVariables, _collectTraces, trace, _noRetry, _fullTestName);
211241
declarer.declare(() {
212242
// Cast to dynamic to avoid the analyzer complaining about us using the
213243
// result of a void method.

pkgs/test_api/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: test_api
2-
version: 0.2.19-nullsafety.2
2+
version: 0.2.19-nullsafety.3-dev
33
description: A library for writing Dart tests.
44
homepage: https://github.com/dart-lang/test/blob/master/pkgs/test_api
55

pkgs/test_core/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 0.3.12-nullsafety.6-dev
2+
3+
* Add experimental `directRunTests`, `directRunSingle`, and `enumerateTestCases`
4+
APIs to enable test runners written around a single executable that can report
5+
and run any single test case.
6+
17
## 0.3.12-nullsafety.5
28

39
* Allow `2.10` stable and `2.11.0-dev` SDKs.
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:collection';
7+
8+
import 'package:path/path.dart' as p;
9+
import 'package:test_api/backend.dart'; //ignore: deprecated_member_use
10+
import 'package:test_api/src/backend/declarer.dart'; //ignore: implementation_imports
11+
import 'package:test_api/src/backend/group.dart'; //ignore: implementation_imports
12+
import 'package:test_api/src/backend/group_entry.dart'; //ignore: implementation_imports
13+
import 'package:test_api/src/backend/invoker.dart'; // ignore: implementation_imports
14+
import 'package:test_api/src/backend/test.dart'; //ignore: implementation_imports
15+
import 'package:test_api/src/utils.dart'; // ignore: implementation_imports
16+
17+
import 'runner/configuration.dart';
18+
import 'runner/engine.dart';
19+
import 'runner/plugin/environment.dart';
20+
import 'runner/reporter.dart';
21+
import 'runner/reporter/expanded.dart';
22+
import 'runner/runner_suite.dart';
23+
import 'runner/suite.dart';
24+
import 'util/print_sink.dart';
25+
26+
/// Runs all unskipped test cases declared in [testMain].
27+
///
28+
/// Test suite level metadata defined in annotations is not read. No filtering
29+
/// is applied except for the filtering defined by `solo` or `skip` arguments to
30+
/// `group` and `test`. Returns [true] if all tests passed.
31+
Future<bool> directRunTests(FutureOr<void> Function() testMain,
32+
{Reporter Function(Engine)? reporterFactory}) =>
33+
_directRunTests(testMain, reporterFactory: reporterFactory);
34+
35+
/// Runs a single test declared in [testMain] matched by it's full test name.
36+
///
37+
/// There must be exactly one test defined with the name [fullTestName]. Note
38+
/// that not all tests and groups are checked, so a test case that is not be
39+
/// intended to be run (due to a `solo` on a different test) may still be run
40+
/// with this API. Only the test names returned by [enumerateTestCases] should
41+
/// be used to prevent running skipped tests.
42+
///
43+
/// Return [true] if the test passes.
44+
///
45+
/// If there are no tests matching [fullTestName] a [MissingTestException] is
46+
/// thrown. If there is more than one test with the name [fullTestName] they
47+
/// will both be run, then a [DuplicateTestnameException] will be thrown.
48+
Future<bool> directRunSingleTest(
49+
FutureOr<void> Function() testMain, String fullTestName,
50+
{Reporter Function(Engine)? reporterFactory}) =>
51+
_directRunTests(testMain,
52+
reporterFactory: reporterFactory, fullTestName: fullTestName);
53+
54+
Future<bool> _directRunTests(FutureOr<void> Function() testMain,
55+
{Reporter Function(Engine)? reporterFactory, String? fullTestName}) async {
56+
reporterFactory ??= (engine) => ExpandedReporter.watch(engine, PrintSink(),
57+
color: Configuration.empty.color, printPath: false, printPlatform: false);
58+
final declarer = Declarer(fullTestName: fullTestName);
59+
await declarer.declare(testMain);
60+
61+
final suite = RunnerSuite(const PluginEnvironment(), SuiteConfiguration.empty,
62+
declarer.build(), SuitePlatform(Runtime.vm, os: currentOSGuess),
63+
path: p.prettyUri(Uri.base));
64+
65+
final engine = Engine()
66+
..suiteSink.add(suite)
67+
..suiteSink.close();
68+
69+
reporterFactory(engine);
70+
71+
final success = await runZoned(() => Invoker.guard(engine.run),
72+
zoneValues: {#test.declarer: declarer});
73+
74+
if (fullTestName != null) {
75+
final testCount = engine.liveTests.length;
76+
if (testCount > 1) {
77+
throw DuplicateTestNameException(fullTestName);
78+
}
79+
if (testCount == 0) {
80+
throw MissingTestException(fullTestName);
81+
}
82+
}
83+
return success!;
84+
}
85+
86+
/// Runs [testMain] and returns the names of all declared tests.
87+
///
88+
/// Test names declared must be unique. If any test repeats the full name,
89+
/// including group prefixes, of a prior test a [DuplicateTestNameException]
90+
/// will be thrown.
91+
///
92+
/// Skipped tests are ignored.
93+
Future<Set<String>> enumerateTestCases(
94+
FutureOr<void> Function() testMain) async {
95+
final declarer = Declarer();
96+
await declarer.declare(testMain);
97+
98+
final toVisit = Queue<GroupEntry>.of([declarer.build()]);
99+
final allTestNames = <String>{};
100+
final unskippedTestNames = <String>{};
101+
while (toVisit.isNotEmpty) {
102+
final current = toVisit.removeLast();
103+
if (current is Group) {
104+
toVisit.addAll(current.entries.reversed);
105+
} else if (current is Test) {
106+
if (!allTestNames.add(current.name)) {
107+
throw DuplicateTestNameException(current.name);
108+
}
109+
if (current.metadata.skip) continue;
110+
unskippedTestNames.add(current.name);
111+
} else {
112+
throw StateError('Unandled Group Entry: ${current.runtimeType}');
113+
}
114+
}
115+
return unskippedTestNames;
116+
}
117+
118+
/// An exception thrown when two test cases in the same test suite (same `main`)
119+
/// have an identical name.
120+
class DuplicateTestNameException implements Exception {
121+
final String name;
122+
DuplicateTestNameException(this.name);
123+
124+
@override
125+
String toString() => 'A test with the name "$name" was already declared. '
126+
'Test cases must have unique names.';
127+
}
128+
129+
/// An exception thrown when a specific test was requested by name that does not
130+
/// exist.
131+
class MissingTestException implements Exception {
132+
final String name;
133+
MissingTestException(this.name);
134+
135+
@override
136+
String toString() =>
137+
'A test with the name "$name" was not declared in the test suite.';
138+
}

pkgs/test_core/pubspec.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: test_core
2-
version: 0.3.12-nullsafety.5
2+
version: 0.3.12-nullsafety.6-dev
33
description: A basic library for writing tests and running them on the VM.
44
homepage: https://github.com/dart-lang/test/blob/master/pkgs/test_core
55

@@ -31,7 +31,7 @@ dependencies:
3131
# matcher is tightly constrained by test_api
3232
matcher: any
3333
# Use an exact version until the test_api package is stable.
34-
test_api: 0.2.19-nullsafety.2
34+
test_api: 0.2.19-nullsafety.3
3535

3636
dependency_overrides:
3737
test_api:

0 commit comments

Comments
 (0)