1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
---|---|
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | /// @docImport 'package:test_api/backend.dart'; |
6 | /// @docImport 'package:test_api/scaffolding.dart'; |
7 | library; |
8 | |
9 | import 'dart:async'; |
10 | |
11 | import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; |
12 | import 'package:meta/meta.dart'; |
13 | import 'package:test_api/scaffolding.dart'show Timeout; |
14 | import 'package:test_api/src/backend/declarer.dart'; // ignore: implementation_imports |
15 | import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports |
16 | import 'package:test_api/src/backend/group_entry.dart'; // ignore: implementation_imports |
17 | import 'package:test_api/src/backend/invoker.dart'; // ignore: implementation_imports |
18 | import 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports |
19 | import 'package:test_api/src/backend/message.dart'; // ignore: implementation_imports |
20 | import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports |
21 | import 'package:test_api/src/backend/state.dart'; // ignore: implementation_imports |
22 | import 'package:test_api/src/backend/suite.dart'; // ignore: implementation_imports |
23 | import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports |
24 | import 'package:test_api/src/backend/test.dart'; // ignore: implementation_imports |
25 | |
26 | export 'package:test_api/fake.dart'show Fake; |
27 | |
28 | Declarer? _localDeclarer; |
29 | Declarer get _declarer { |
30 | final Declarer? declarer = Zone.current[#test.declarer] as Declarer?; |
31 | if (declarer != null) { |
32 | return declarer; |
33 | } |
34 | // If no declarer is defined, this test is being run via `flutter run -t test_file.dart`. |
35 | if (_localDeclarer == null) { |
36 | _localDeclarer = Declarer(); |
37 | Future<void>(() { |
38 | Invoker.guard<Future<void>>(() async { |
39 | final _Reporter reporter = _Reporter(color: false); // disable color when run directly. |
40 | // ignore: recursive_getters, this self-call is safe since it will just fetch the declarer instance |
41 | final Group group = _declarer.build(); |
42 | final Suite suite = Suite(group, SuitePlatform(Runtime.vm)); |
43 | await _runGroup(suite, group, <Group>[], reporter); |
44 | reporter._onDone(); |
45 | }); |
46 | }); |
47 | } |
48 | return _localDeclarer!; |
49 | } |
50 | |
51 | Future<void> _runGroup( |
52 | Suite suiteConfig, |
53 | Group group, |
54 | List<Group> parents, |
55 | _Reporter reporter, |
56 | ) async { |
57 | parents.add(group); |
58 | try { |
59 | final bool skipGroup = group.metadata.skip; |
60 | bool setUpAllSucceeded = true; |
61 | if (!skipGroup && group.setUpAll != null) { |
62 | final LiveTest liveTest = group.setUpAll!.load(suiteConfig, groups: parents); |
63 | await _runLiveTest(suiteConfig, liveTest, reporter, countSuccess: false); |
64 | setUpAllSucceeded = liveTest.state.result.isPassing; |
65 | } |
66 | if (setUpAllSucceeded) { |
67 | for (final GroupEntry entry in group.entries) { |
68 | if (entry is Group) { |
69 | await _runGroup(suiteConfig, entry, parents, reporter); |
70 | } else if (entry.metadata.skip) { |
71 | await _runSkippedTest(suiteConfig, entry as Test, parents, reporter); |
72 | } else { |
73 | final Test test = entry as Test; |
74 | await _runLiveTest(suiteConfig, test.load(suiteConfig, groups: parents), reporter); |
75 | } |
76 | } |
77 | } |
78 | // Even if we're closed or setUpAll failed, we want to run all the |
79 | // teardowns to ensure that any state is properly cleaned up. |
80 | if (!skipGroup && group.tearDownAll != null) { |
81 | final LiveTest liveTest = group.tearDownAll!.load(suiteConfig, groups: parents); |
82 | await _runLiveTest(suiteConfig, liveTest, reporter, countSuccess: false); |
83 | } |
84 | } finally { |
85 | parents.remove(group); |
86 | } |
87 | } |
88 | |
89 | Future<void> _runLiveTest( |
90 | Suite suiteConfig, |
91 | LiveTest liveTest, |
92 | _Reporter reporter, { |
93 | bool countSuccess = true, |
94 | }) async { |
95 | reporter._onTestStarted(liveTest); |
96 | // Schedule a microtask to ensure that [onTestStarted] fires before the |
97 | // first [LiveTest.onStateChange] event. |
98 | await Future<void>.microtask(liveTest.run); |
99 | // Once the test finishes, use await null to do a coarse-grained event |
100 | // loop pump to avoid starving non-microtask events. |
101 | await null; |
102 | final bool isSuccess = liveTest.state.result.isPassing; |
103 | if (isSuccess) { |
104 | reporter.passed.add(liveTest); |
105 | } else { |
106 | reporter.failed.add(liveTest); |
107 | } |
108 | } |
109 | |
110 | Future<void> _runSkippedTest( |
111 | Suite suiteConfig, |
112 | Test test, |
113 | List<Group> parents, |
114 | _Reporter reporter, |
115 | ) async { |
116 | final LocalTest skipped = LocalTest(test.name, test.metadata, () {}, trace: test.trace); |
117 | if (skipped.metadata.skipReason != null) { |
118 | reporter.log('Skip:${skipped.metadata.skipReason} '); |
119 | } |
120 | final LiveTest liveTest = skipped.load(suiteConfig); |
121 | reporter._onTestStarted(liveTest); |
122 | reporter.skipped.add(skipped); |
123 | } |
124 | |
125 | // TODO(nweiz): This and other top-level functions should throw exceptions if |
126 | // they're called after the declarer has finished declaring. |
127 | /// Creates a new test case with the given description (converted to a string) |
128 | /// and body. |
129 | /// |
130 | /// The description will be added to the descriptions of any surrounding |
131 | /// [group]s. If [testOn] is passed, it's parsed as a [platform selector][]; the |
132 | /// test will only be run on matching platforms. |
133 | /// |
134 | /// [platform selector]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-selectors |
135 | /// |
136 | /// If [timeout] is passed, it's used to modify or replace the default timeout |
137 | /// of 30 seconds. Timeout modifications take precedence in suite-group-test |
138 | /// order, so [timeout] will also modify any timeouts set on the group or suite. |
139 | /// |
140 | /// If [skip] is a String or `true`, the test is skipped. If it's a String, it |
141 | /// should explain why the test is skipped; this reason will be printed instead |
142 | /// of running the test. |
143 | /// |
144 | /// If [tags] is passed, it declares user-defined tags that are applied to the |
145 | /// test. These tags can be used to select or skip the test on the command line, |
146 | /// or to do bulk test configuration. All tags should be declared in the |
147 | /// [package configuration file][configuring tags]. The parameter can be an |
148 | /// [Iterable] of tag names, or a [String] representing a single tag. |
149 | /// |
150 | /// If [retry] is passed, the test will be retried the provided number of times |
151 | /// before being marked as a failure. |
152 | /// |
153 | /// [configuring tags]: https://github.com/dart-lang/test/blob/44d6cb196f34a93a975ed5f3cb76afcc3a7b39b0/doc/package_config.md#configuring-tags |
154 | /// |
155 | /// [onPlatform] allows tests to be configured on a platform-by-platform |
156 | /// basis. It's a map from strings that are parsed as [PlatformSelector]s to |
157 | /// annotation classes: [Timeout], [Skip], or lists of those. These |
158 | /// annotations apply only on the given platforms. For example: |
159 | /// |
160 | /// test('potentially slow test', () { |
161 | /// // ... |
162 | /// }, onPlatform: { |
163 | /// // This test is especially slow on Windows. |
164 | /// 'windows': Timeout.factor(2), |
165 | /// 'browser': [ |
166 | /// Skip('add browser support'), |
167 | /// // This will be slow on browsers once it works on them. |
168 | /// Timeout.factor(2) |
169 | /// ] |
170 | /// }); |
171 | /// |
172 | /// If multiple platforms match, the annotations apply in order as through |
173 | /// they were in nested groups. |
174 | @isTest |
175 | void test( |
176 | Object description, |
177 | dynamic Function() body, { |
178 | String? testOn, |
179 | Timeout? timeout, |
180 | dynamic skip, |
181 | dynamic tags, |
182 | Map<String, dynamic>? onPlatform, |
183 | int? retry, |
184 | }) { |
185 | _maybeConfigureTearDownForTestFile(); |
186 | _declarer.test( |
187 | description.toString(), |
188 | body, |
189 | testOn: testOn, |
190 | timeout: timeout, |
191 | skip: skip, |
192 | onPlatform: onPlatform, |
193 | tags: tags, |
194 | retry: retry, |
195 | ); |
196 | } |
197 | |
198 | /// Creates a group of tests. |
199 | /// |
200 | /// A group's description (converted to a string) is included in the descriptions |
201 | /// of any tests or sub-groups it contains. [setUp] and [tearDown] are also scoped |
202 | /// to the containing group. |
203 | /// |
204 | /// If `skip` is a String or `true`, the group is skipped. If it's a String, it |
205 | /// should explain why the group is skipped; this reason will be printed instead |
206 | /// of running the group's tests. |
207 | @isTestGroup |
208 | void group(Object description, void Function() body, {dynamic skip, int? retry}) { |
209 | _maybeConfigureTearDownForTestFile(); |
210 | _declarer.group(description.toString(), body, skip: skip, retry: retry); |
211 | } |
212 | |
213 | /// Registers a function to be run before tests. |
214 | /// |
215 | /// This function will be called before each test is run. The `body` may be |
216 | /// asynchronous; if so, it must return a [Future]. |
217 | /// |
218 | /// If this is called within a test group, it applies only to tests in that |
219 | /// group. The `body` will be run after any set-up callbacks in parent groups or |
220 | /// at the top level. |
221 | /// |
222 | /// Each callback at the top level or in a given group will be run in the order |
223 | /// they were declared. |
224 | void setUp(dynamic Function() body) { |
225 | _maybeConfigureTearDownForTestFile(); |
226 | _declarer.setUp(body); |
227 | } |
228 | |
229 | /// Registers a function to be run after tests. |
230 | /// |
231 | /// This function will be called after each test is run. The `body` may be |
232 | /// asynchronous; if so, it must return a [Future]. |
233 | /// |
234 | /// If this is called within a test group, it applies only to tests in that |
235 | /// group. The `body` will be run before any tear-down callbacks in parent |
236 | /// groups or at the top level. |
237 | /// |
238 | /// Each callback at the top level or in a given group will be run in the |
239 | /// reverse of the order they were declared. |
240 | /// |
241 | /// See also [addTearDown], which adds tear-downs to a running test. |
242 | void tearDown(dynamic Function() body) { |
243 | _maybeConfigureTearDownForTestFile(); |
244 | _declarer.tearDown(body); |
245 | } |
246 | |
247 | /// Registers a function to be run once before all tests. |
248 | /// |
249 | /// The `body` may be asynchronous; if so, it must return a [Future]. |
250 | /// |
251 | /// If this is called within a test group, The `body` will run before all tests |
252 | /// in that group. It will be run after any [setUpAll] callbacks in parent |
253 | /// groups or at the top level. It won't be run if none of the tests in the |
254 | /// group are run. |
255 | /// |
256 | /// **Note**: This function makes it very easy to accidentally introduce hidden |
257 | /// dependencies between tests that should be isolated. In general, you should |
258 | /// prefer [setUp], and only use [setUpAll] if the callback is prohibitively |
259 | /// slow. |
260 | void setUpAll(dynamic Function() body) { |
261 | _maybeConfigureTearDownForTestFile(); |
262 | _declarer.setUpAll(body); |
263 | } |
264 | |
265 | /// Registers a function to be run once after all tests. |
266 | /// |
267 | /// If this is called within a test group, `body` will run after all tests |
268 | /// in that group. It will be run before any [tearDownAll] callbacks in parent |
269 | /// groups or at the top level. It won't be run if none of the tests in the |
270 | /// group are run. |
271 | /// |
272 | /// **Note**: This function makes it very easy to accidentally introduce hidden |
273 | /// dependencies between tests that should be isolated. In general, you should |
274 | /// prefer [tearDown], and only use [tearDownAll] if the callback is |
275 | /// prohibitively slow. |
276 | void tearDownAll(dynamic Function() body) { |
277 | _maybeConfigureTearDownForTestFile(); |
278 | _declarer.tearDownAll(body); |
279 | } |
280 | |
281 | bool _isTearDownForTestFileConfigured = false; |
282 | |
283 | /// If needed, configures `tearDownAll` after all user defined `tearDownAll` in the test file. |
284 | /// |
285 | /// This function should be invoked in all functions, that may be invoked by user in the test file, |
286 | /// to be invoked before any other `tearDownAll`. |
287 | void _maybeConfigureTearDownForTestFile() { |
288 | if (_isTearDownForTestFileConfigured || !_shouldConfigureTearDownForTestFile()) { |
289 | return; |
290 | } |
291 | _declarer.tearDownAll(_tearDownForTestFile); |
292 | _isTearDownForTestFileConfigured = true; |
293 | } |
294 | |
295 | /// Returns true if tear down for the test file needs to be configured. |
296 | bool _shouldConfigureTearDownForTestFile() { |
297 | return LeakTesting.enabled; |
298 | } |
299 | |
300 | /// Tear down that should happen after all user defined tear down. |
301 | Future<void> _tearDownForTestFile() async { |
302 | await maybeTearDownLeakTrackingForAll(); |
303 | } |
304 | |
305 | /// A reporter that prints each test on its own line. |
306 | /// |
307 | /// This is currently used in place of `CompactReporter` by `lib/test.dart`, |
308 | /// which can't transitively import `dart:io` but still needs access to a runner |
309 | /// so that test files can be run directly. This means that until issue 6943 is |
310 | /// fixed, this must not import `dart:io`. |
311 | class _Reporter { |
312 | _Reporter({bool color = true, bool printPath = true}) |
313 | : _printPath = printPath, |
314 | _green = color ? '\u001b[32m': '', |
315 | _red = color ? '\u001b[31m': '', |
316 | _yellow = color ? '\u001b[33m': '', |
317 | _bold = color ? '\u001b[1m': '', |
318 | _noColor = color ? '\u001b[0m': ''; |
319 | |
320 | final List<LiveTest> passed = <LiveTest>[]; |
321 | final List<LiveTest> failed = <LiveTest>[]; |
322 | final List<Test> skipped = <Test>[]; |
323 | |
324 | /// The terminal escape for green text, or the empty string if this is Windows |
325 | /// or not outputting to a terminal. |
326 | final String _green; |
327 | |
328 | /// The terminal escape for red text, or the empty string if this is Windows |
329 | /// or not outputting to a terminal. |
330 | final String _red; |
331 | |
332 | /// The terminal escape for yellow text, or the empty string if this is |
333 | /// Windows or not outputting to a terminal. |
334 | final String _yellow; |
335 | |
336 | /// The terminal escape for bold text, or the empty string if this is |
337 | /// Windows or not outputting to a terminal. |
338 | final String _bold; |
339 | |
340 | /// The terminal escape for removing test coloring, or the empty string if |
341 | /// this is Windows or not outputting to a terminal. |
342 | final String _noColor; |
343 | |
344 | /// Whether the path to each test's suite should be printed. |
345 | final bool _printPath; |
346 | |
347 | /// A stopwatch that tracks the duration of the full run. |
348 | final Stopwatch _stopwatch = Stopwatch(); // flutter_ignore: stopwatch (see analyze.dart) |
349 | // Ignore context: Used for logging of actual test runs, outside of FakeAsync. |
350 | |
351 | /// The size of `_engine.passed` last time a progress notification was |
352 | /// printed. |
353 | int? _lastProgressPassed; |
354 | |
355 | /// The size of `_engine.skipped` last time a progress notification was |
356 | /// printed. |
357 | int? _lastProgressSkipped; |
358 | |
359 | /// The size of `_engine.failed` last time a progress notification was |
360 | /// printed. |
361 | int? _lastProgressFailed; |
362 | |
363 | /// The message printed for the last progress notification. |
364 | String? _lastProgressMessage; |
365 | |
366 | /// The suffix added to the last progress notification. |
367 | String? _lastProgressSuffix; |
368 | |
369 | /// The set of all subscriptions to various streams. |
370 | final Set<StreamSubscription<void>> _subscriptions = <StreamSubscription<void>>{}; |
371 | |
372 | /// A callback called when the engine begins running [liveTest]. |
373 | void _onTestStarted(LiveTest liveTest) { |
374 | if (!_stopwatch.isRunning) { |
375 | _stopwatch.start(); |
376 | } |
377 | _progressLine(_description(liveTest)); |
378 | _subscriptions.add( |
379 | liveTest.onStateChange.listen((State state) => _onStateChange(liveTest, state)), |
380 | ); |
381 | _subscriptions.add( |
382 | liveTest.onError.listen( |
383 | (AsyncError error) => _onError(liveTest, error.error, error.stackTrace), |
384 | ), |
385 | ); |
386 | _subscriptions.add( |
387 | liveTest.onMessage.listen((Message message) { |
388 | _progressLine(_description(liveTest)); |
389 | String text = message.text; |
390 | if (message.type == MessageType.skip) { |
391 | text = '$_yellow $text$_noColor'; |
392 | } |
393 | log(text); |
394 | }), |
395 | ); |
396 | } |
397 | |
398 | /// A callback called when [liveTest]'s state becomes [state]. |
399 | void _onStateChange(LiveTest liveTest, State state) { |
400 | if (state.status != Status.complete) { |
401 | return; |
402 | } |
403 | } |
404 | |
405 | /// A callback called when [liveTest] throws [error]. |
406 | void _onError(LiveTest liveTest, Object error, StackTrace stackTrace) { |
407 | if (liveTest.state.status != Status.complete) { |
408 | return; |
409 | } |
410 | _progressLine(_description(liveTest), suffix:'$_bold$_red[E]$_noColor'); |
411 | log(_indent(error.toString())); |
412 | log(_indent('$stackTrace')); |
413 | } |
414 | |
415 | /// A callback called when the engine is finished running tests. |
416 | void _onDone() { |
417 | final bool success = failed.isEmpty; |
418 | if (!success) { |
419 | _progressLine('Some tests failed.', color: _red); |
420 | } else if (passed.isEmpty) { |
421 | _progressLine('All tests skipped.'); |
422 | } else { |
423 | _progressLine('All tests passed!'); |
424 | } |
425 | } |
426 | |
427 | /// Prints a line representing the current state of the tests. |
428 | /// |
429 | /// [message] goes after the progress report. If [color] is passed, it's used |
430 | /// as the color for [message]. If [suffix] is passed, it's added to the end |
431 | /// of [message]. |
432 | void _progressLine(String message, {String? color, String? suffix}) { |
433 | // Print nothing if nothing has changed since the last progress line. |
434 | if (passed.length == _lastProgressPassed && |
435 | skipped.length == _lastProgressSkipped && |
436 | failed.length == _lastProgressFailed && |
437 | message == _lastProgressMessage && |
438 | // Don't re-print just because a suffix was removed. |
439 | (suffix == null || suffix == _lastProgressSuffix)) { |
440 | return; |
441 | } |
442 | _lastProgressPassed = passed.length; |
443 | _lastProgressSkipped = skipped.length; |
444 | _lastProgressFailed = failed.length; |
445 | _lastProgressMessage = message; |
446 | _lastProgressSuffix = suffix; |
447 | |
448 | if (suffix != null) { |
449 | message += suffix; |
450 | } |
451 | color ??=''; |
452 | final Duration duration = _stopwatch.elapsed; |
453 | final StringBuffer buffer = StringBuffer(); |
454 | |
455 | // \r moves back to the beginning of the current line. |
456 | buffer.write('${_timeString(duration)}'); |
457 | buffer.write(_green); |
458 | buffer.write('+'); |
459 | buffer.write(passed.length); |
460 | buffer.write(_noColor); |
461 | |
462 | if (skipped.isNotEmpty) { |
463 | buffer.write(_yellow); |
464 | buffer.write(' ~'); |
465 | buffer.write(skipped.length); |
466 | buffer.write(_noColor); |
467 | } |
468 | |
469 | if (failed.isNotEmpty) { |
470 | buffer.write(_red); |
471 | buffer.write(' -'); |
472 | buffer.write(failed.length); |
473 | buffer.write(_noColor); |
474 | } |
475 | |
476 | buffer.write(': '); |
477 | buffer.write(color); |
478 | buffer.write(message); |
479 | buffer.write(_noColor); |
480 | |
481 | log(buffer.toString()); |
482 | } |
483 | |
484 | /// Returns a representation of [duration] as `MM:SS`. |
485 | String _timeString(Duration duration) { |
486 | final String minutes = duration.inMinutes.toString().padLeft(2,'0'); |
487 | final String seconds = (duration.inSeconds % 60).toString().padLeft(2,'0'); |
488 | return'$minutes:$seconds'; |
489 | } |
490 | |
491 | /// Returns a description of [liveTest]. |
492 | /// |
493 | /// This differs from the test's own description in that it may also include |
494 | /// the suite's name. |
495 | String _description(LiveTest liveTest) { |
496 | String name = liveTest.test.name; |
497 | if (_printPath && liveTest.suite.path != null) { |
498 | name ='${liveTest.suite.path}:$name'; |
499 | } |
500 | return name; |
501 | } |
502 | |
503 | /// Print the message to the console. |
504 | void log(String message) { |
505 | // We centralize all the prints in this file through this one method so that |
506 | // in principle we can reroute the output easily should we need to. |
507 | print(message); // ignore: avoid_print |
508 | } |
509 | } |
510 | |
511 | String _indent(String string, {int? size, String? first}) { |
512 | size ??= first == null ? 2 : first.length; |
513 | return _prefixLines(string,' '* size, first: first); |
514 | } |
515 | |
516 | String _prefixLines(String text, String prefix, {String? first, String? last, String? single}) { |
517 | first ??= prefix; |
518 | last ??= prefix; |
519 | single ??= first; |
520 | final List<String> lines = text.split('\n'); |
521 | if (lines.length == 1) { |
522 | return'$single$text'; |
523 | } |
524 | final StringBuffer buffer = StringBuffer('$first${lines.first}\n'); |
525 | // Write out all but the first and last lines with [prefix]. |
526 | for (final String line in lines.skip(1).take(lines.length - 2)) { |
527 | buffer.writeln('$prefix$line'); |
528 | } |
529 | buffer.write('$last${lines.last}'); |
530 | return buffer.toString(); |
531 | } |
532 |
Definitions
- _localDeclarer
- _declarer
- _runGroup
- _runLiveTest
- _runSkippedTest
- test
- group
- setUp
- tearDown
- setUpAll
- tearDownAll
- _isTearDownForTestFileConfigured
- _maybeConfigureTearDownForTestFile
- _shouldConfigureTearDownForTestFile
- _tearDownForTestFile
- _Reporter
- _Reporter
- _onTestStarted
- _onStateChange
- _onError
- _onDone
- _progressLine
- _timeString
- _description
- log
- _indent
Learn more about Flutter for embedded and desktop on industrialflutter.com