Line data Source code
1 : // Copyright (c) 2015, 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 :
7 : import 'package:collection/collection.dart';
8 : import 'package:stack_trace/stack_trace.dart';
9 :
10 : import 'configuration/timeout.dart';
11 : import 'group.dart';
12 : import 'group_entry.dart';
13 : import 'invoker.dart';
14 : import 'metadata.dart';
15 : import 'test.dart';
16 :
17 : /// A class that manages the state of tests as they're declared.
18 : ///
19 : /// A nested tree of Declarers tracks the current group, set-up, and tear-down
20 : /// functions. Each Declarer in the tree corresponds to a group. This tree is
21 : /// tracked by a zone-scoped "current" Declarer; the current declarer can be set
22 : /// for a block using [Declarer.declare], and it can be accessed using
23 : /// [Declarer.current].
24 : class Declarer {
25 : /// The parent declarer, or `null` if this corresponds to the root group.
26 : final Declarer? _parent;
27 :
28 : /// The name of the current test group, including the name of any parent
29 : /// groups.
30 : ///
31 : /// This is `null` if this is the root group.
32 : final String? _name;
33 :
34 : /// The metadata for this group, including the metadata of any parent groups
35 : /// and of the test suite.
36 : final Metadata _metadata;
37 :
38 : /// The set of variables that are valid for platform selectors, in addition to
39 : /// the built-in variables that are allowed everywhere.
40 : final Set<String> _platformVariables;
41 :
42 : /// The stack trace for this group.
43 : ///
44 : /// This is `null` for the root (implicit) group.
45 : final Trace? _trace;
46 :
47 : /// Whether to collect stack traces for [GroupEntry]s.
48 : final bool _collectTraces;
49 :
50 : /// Whether to disable retries of tests.
51 : final bool _noRetry;
52 :
53 : /// The set-up functions to run for each test in this group.
54 : final _setUps = <dynamic Function()>[];
55 :
56 : /// The tear-down functions to run for each test in this group.
57 : final _tearDowns = <dynamic Function()>[];
58 :
59 : /// The set-up functions to run once for this group.
60 : final _setUpAlls = <dynamic Function()>[];
61 :
62 : /// The default timeout for synthetic tests.
63 : final _timeout = Timeout(Duration(minutes: 12));
64 :
65 : /// The trace for the first call to [setUpAll].
66 : ///
67 : /// All [setUpAll]s are run in a single logical test, so they can only have
68 : /// one trace. The first trace is most often correct, since the first
69 : /// [setUpAll] is always run and the rest are only run if that one succeeds.
70 : Trace? _setUpAllTrace;
71 :
72 : /// The tear-down functions to run once for this group.
73 : final _tearDownAlls = <Function()>[];
74 :
75 : /// The trace for the first call to [tearDownAll].
76 : ///
77 : /// All [tearDownAll]s are run in a single logical test, so they can only have
78 : /// one trace. The first trace matches [_setUpAllTrace].
79 : Trace? _tearDownAllTrace;
80 :
81 : /// The children of this group, either tests or sub-groups.
82 : ///
83 : /// All modifications to this must go through [_addEntry].
84 : final _entries = <GroupEntry>[];
85 :
86 : /// Whether [build] has been called for this declarer.
87 : bool _built = false;
88 :
89 : /// The tests and/or groups that have been flagged as solo.
90 : final _soloEntries = <GroupEntry>[];
91 :
92 : /// Whether any tests and/or groups have been flagged as solo.
93 33 : bool get _solo => _soloEntries.isNotEmpty;
94 :
95 : /// An exact full test name to match.
96 : ///
97 : /// When non-null only tests with exactly this name will be considered. The
98 : /// full test name is the combination of the test case name with all group
99 : /// prefixes. All other tests, including their metadata like `solo`, is
100 : /// ignored. Uniqueness is not guaranteed so this may match more than one
101 : /// test.
102 : ///
103 : /// Groups which are not a strict prefix of this name will be ignored.
104 : final String? _fullTestName;
105 :
106 : /// The current zone-scoped declarer.
107 33 : static Declarer? get current => Zone.current[#test.declarer] as Declarer?;
108 :
109 : /// All the test and group names that have been declared in the entire suite.
110 : ///
111 : /// If duplicate test names are allowed, this is not tracked and it will be
112 : /// `null`.
113 : final Set<String>? _seenNames;
114 :
115 : /// Creates a new declarer for the root group.
116 : ///
117 : /// This is the implicit group that exists outside of any calls to `group()`.
118 : /// If [metadata] is passed, it's used as the metadata for the implicit root
119 : /// group.
120 : ///
121 : /// The [platformVariables] are the set of variables that are valid for
122 : /// platform selectors in test and group metadata, in addition to the built-in
123 : /// variables that are allowed everywhere.
124 : ///
125 : /// If [collectTraces] is `true`, this will set [GroupEntry.trace] for all
126 : /// entries built by the declarer. Note that this can be noticeably slow when
127 : /// thousands of tests are being declared (see #457).
128 : ///
129 : /// If [noRetry] is `true` tests will be run at most once.
130 : ///
131 : /// If [allowDuplicateTestNames] is `false`, then a
132 : /// [DuplicateTestNameException] will be thrown if two tests (or groups) have
133 : /// the same name.
134 11 : Declarer({
135 : Metadata? metadata,
136 : Set<String>? platformVariables,
137 : bool collectTraces = false,
138 : bool noRetry = false,
139 : String? fullTestName,
140 : // TODO: Change the default https://github.com/dart-lang/test/issues/1571
141 : bool allowDuplicateTestNames = true,
142 11 : }) : this._(
143 : null,
144 : null,
145 0 : metadata ?? Metadata(),
146 : platformVariables ?? const UnmodifiableSetView.empty(),
147 : collectTraces,
148 : null,
149 : noRetry,
150 : fullTestName,
151 : allowDuplicateTestNames ? null : <String>{});
152 :
153 11 : Declarer._(
154 : this._parent,
155 : this._name,
156 : this._metadata,
157 : this._platformVariables,
158 : this._collectTraces,
159 : this._trace,
160 : this._noRetry,
161 : this._fullTestName,
162 : this._seenNames,
163 : );
164 :
165 : /// Runs [body] with this declarer as [Declarer.current].
166 : ///
167 : /// Returns the return value of [body].
168 11 : T declare<T>(T Function() body) =>
169 22 : runZoned(body, zoneValues: {#test.declarer: this});
170 :
171 : /// Defines a test case with the given name and body.
172 11 : void test(String name, dynamic Function() body,
173 : {String? testOn,
174 : Timeout? timeout,
175 : skip,
176 : Map<String, dynamic>? onPlatform,
177 : tags,
178 : int? retry,
179 : bool solo = false}) {
180 11 : _checkNotBuilt('test');
181 :
182 11 : final fullName = _prefix(name);
183 11 : if (_fullTestName != null && fullName != _fullTestName) {
184 : return;
185 : }
186 :
187 11 : var newMetadata = Metadata.parse(
188 : testOn: testOn,
189 : timeout: timeout,
190 : skip: skip,
191 : onPlatform: onPlatform,
192 : tags: tags,
193 11 : retry: _noRetry ? 0 : retry);
194 22 : newMetadata.validatePlatformSelectors(_platformVariables);
195 22 : var metadata = _metadata.merge(newMetadata);
196 33 : _addEntry(LocalTest(fullName, metadata, () async {
197 11 : var parents = <Declarer>[];
198 : for (Declarer? declarer = this;
199 : declarer != null;
200 11 : declarer = declarer._parent) {
201 11 : parents.add(declarer);
202 : }
203 :
204 : // Register all tear-down functions in all declarers. Iterate through
205 : // parents outside-in so that the Invoker gets the functions in the order
206 : // they were declared in source.
207 22 : for (var declarer in parents.reversed) {
208 14 : for (var tearDown in declarer._tearDowns) {
209 6 : Invoker.current!.addTearDown(tearDown);
210 : }
211 : }
212 :
213 33 : await runZoned(() async {
214 22 : await _runSetUps();
215 11 : await body();
216 : },
217 : // Make the declarer visible to running tests so that they'll throw
218 : // useful errors when calling `test()` and `group()` within a test.
219 11 : zoneValues: {#test.declarer: this});
220 22 : }, trace: _collectTraces ? Trace.current(2) : null, guarded: false));
221 :
222 : if (solo) {
223 0 : _soloEntries.add(_entries.last);
224 : }
225 : }
226 :
227 : /// Creates a group of tests.
228 2 : void group(String name, void Function() body,
229 : {String? testOn,
230 : Timeout? timeout,
231 : skip,
232 : Map<String, dynamic>? onPlatform,
233 : tags,
234 : int? retry,
235 : bool solo = false}) {
236 2 : _checkNotBuilt('group');
237 :
238 2 : final fullTestPrefix = _prefix(name);
239 2 : if (_fullTestName != null && !_fullTestName!.startsWith(fullTestPrefix)) {
240 : return;
241 : }
242 :
243 2 : var newMetadata = Metadata.parse(
244 : testOn: testOn,
245 : timeout: timeout,
246 : skip: skip,
247 : onPlatform: onPlatform,
248 : tags: tags,
249 2 : retry: _noRetry ? 0 : retry);
250 4 : newMetadata.validatePlatformSelectors(_platformVariables);
251 4 : var metadata = _metadata.merge(newMetadata);
252 4 : var trace = _collectTraces ? Trace.current(2) : null;
253 :
254 2 : var declarer = Declarer._(
255 : this,
256 : fullTestPrefix,
257 : metadata,
258 2 : _platformVariables,
259 2 : _collectTraces,
260 : trace,
261 2 : _noRetry,
262 2 : _fullTestName,
263 2 : _seenNames);
264 4 : declarer.declare(() {
265 : // Cast to dynamic to avoid the analyzer complaining about us using the
266 : // result of a void method.
267 2 : var result = (body as dynamic)();
268 2 : if (result is! Future) return;
269 0 : throw ArgumentError('Groups may not be async.');
270 : });
271 4 : _addEntry(declarer.build());
272 :
273 2 : if (solo || declarer._solo) {
274 0 : _soloEntries.add(_entries.last);
275 : }
276 : }
277 :
278 : /// Returns [name] prefixed with this declarer's group name.
279 26 : String _prefix(String name) => _name == null ? name : '$_name $name';
280 :
281 : /// Registers a function to be run before each test in this group.
282 3 : void setUp(dynamic Function() callback) {
283 3 : _checkNotBuilt('setUp');
284 6 : _setUps.add(callback);
285 : }
286 :
287 : /// Registers a function to be run after each test in this group.
288 3 : void tearDown(dynamic Function() callback) {
289 3 : _checkNotBuilt('tearDown');
290 6 : _tearDowns.add(callback);
291 : }
292 :
293 : /// Registers a function to be run once before all tests.
294 0 : void setUpAll(dynamic Function() callback) {
295 0 : _checkNotBuilt('setUpAll');
296 0 : if (_collectTraces) _setUpAllTrace ??= Trace.current(2);
297 0 : _setUpAlls.add(callback);
298 : }
299 :
300 : /// Registers a function to be run once after all tests.
301 0 : void tearDownAll(dynamic Function() callback) {
302 0 : _checkNotBuilt('tearDownAll');
303 0 : if (_collectTraces) _tearDownAllTrace ??= Trace.current(2);
304 0 : _tearDownAlls.add(callback);
305 : }
306 :
307 : /// Like [tearDownAll], but called from within a running [setUpAll] test to
308 : /// dynamically add a [tearDownAll].
309 0 : void addTearDownAll(dynamic Function() callback) =>
310 0 : _tearDownAlls.add(callback);
311 :
312 : /// Finalizes and returns the group being declared.
313 : ///
314 : /// **Note**: The tests in this group must be run in a [Invoker.guard]
315 : /// context; otherwise, test errors won't be captured.
316 11 : Group build() {
317 11 : _checkNotBuilt('build');
318 :
319 11 : _built = true;
320 33 : var entries = _entries.map((entry) {
321 11 : if (_solo && !_soloEntries.contains(entry)) {
322 0 : entry = LocalTest(
323 0 : entry.name,
324 0 : entry.metadata
325 0 : .change(skip: true, skipReason: 'does not have "solo"'),
326 0 : () {});
327 : }
328 : return entry;
329 11 : }).toList();
330 :
331 22 : return Group(_name ?? '', entries,
332 11 : metadata: _metadata,
333 11 : trace: _trace,
334 11 : setUpAll: _setUpAll,
335 11 : tearDownAll: _tearDownAll);
336 : }
337 :
338 : /// Throws a [StateError] if [build] has been called.
339 : ///
340 : /// [name] should be the name of the method being called.
341 11 : void _checkNotBuilt(String name) {
342 11 : if (!_built) return;
343 0 : throw StateError("Can't call $name() once tests have begun running.");
344 : }
345 :
346 : /// Run the set-up functions for this and any parent groups.
347 : ///
348 : /// If no set-up functions are declared, this returns a [Future] that
349 : /// completes immediately.
350 11 : Future _runSetUps() async {
351 17 : if (_parent != null) await _parent!._runSetUps();
352 : // TODO: why does type inference not work here?
353 39 : await Future.forEach<Function>(_setUps, (setUp) => setUp());
354 : }
355 :
356 : /// Returns a [Test] that runs the callbacks in [_setUpAll], or `null`.
357 11 : Test? get _setUpAll {
358 22 : if (_setUpAlls.isEmpty) return null;
359 :
360 0 : return LocalTest(_prefix('(setUpAll)'), _metadata.change(timeout: _timeout),
361 0 : () {
362 0 : return runZoned(
363 0 : () => Future.forEach<Function>(_setUpAlls, (setUp) => setUp()),
364 : // Make the declarer visible to running scaffolds so they can add to
365 : // the declarer's `tearDownAll()` list.
366 0 : zoneValues: {#test.declarer: this});
367 0 : }, trace: _setUpAllTrace, guarded: false, isScaffoldAll: true);
368 : }
369 :
370 : /// Returns a [Test] that runs the callbacks in [_tearDownAll], or `null`.
371 11 : Test? get _tearDownAll {
372 : // We have to create a tearDownAll if there's a setUpAll, since it might
373 : // dynamically add tear-down code using [addTearDownAll].
374 44 : if (_setUpAlls.isEmpty && _tearDownAlls.isEmpty) return null;
375 :
376 0 : return LocalTest(
377 0 : _prefix('(tearDownAll)'), _metadata.change(timeout: _timeout), () {
378 0 : return runZoned(() => Invoker.current!.runTearDowns(_tearDownAlls),
379 : // Make the declarer visible to running scaffolds so they can add to
380 : // the declarer's `tearDownAll()` list.
381 0 : zoneValues: {#test.declarer: this});
382 0 : }, trace: _tearDownAllTrace, guarded: false, isScaffoldAll: true);
383 : }
384 :
385 11 : void _addEntry(GroupEntry entry) {
386 22 : if (_seenNames?.add(entry.name) == false) {
387 0 : throw DuplicateTestNameException(entry.name);
388 : }
389 22 : _entries.add(entry);
390 : }
391 : }
392 :
393 : /// An exception thrown when two test cases in the same test suite (same `main`)
394 : /// have an identical name.
395 : class DuplicateTestNameException implements Exception {
396 : final String name;
397 0 : DuplicateTestNameException(this.name);
398 :
399 0 : @override
400 0 : String toString() => 'A test with the name "$name" was already declared. '
401 : 'Test cases must have unique names.\n\n'
402 : 'See https://github.com/dart-lang/test/blob/master/pkgs/test/doc/'
403 : 'configuration.md#allow_test_randomization for info on enabling this.';
404 : }
|