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';
7library;
8
9import 'dart:async';
10
11import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
12import 'package:meta/meta.dart';
13import 'package:test_api/scaffolding.dart' show Timeout;
14import 'package:test_api/src/backend/declarer.dart'; // ignore: implementation_imports
15import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports
16import 'package:test_api/src/backend/group_entry.dart'; // ignore: implementation_imports
17import 'package:test_api/src/backend/invoker.dart'; // ignore: implementation_imports
18import 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports
19import 'package:test_api/src/backend/message.dart'; // ignore: implementation_imports
20import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
21import 'package:test_api/src/backend/state.dart'; // ignore: implementation_imports
22import 'package:test_api/src/backend/suite.dart'; // ignore: implementation_imports
23import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports
24import 'package:test_api/src/backend/test.dart'; // ignore: implementation_imports
25
26export 'package:test_api/fake.dart' show Fake;
27
28Declarer? _localDeclarer;
29Declarer 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
51Future<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
89Future<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
110Future<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
175void 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
208void 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.
224void 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.
242void 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.
260void 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.
276void tearDownAll(dynamic Function() body) {
277 _maybeConfigureTearDownForTestFile();
278 _declarer.tearDownAll(body);
279}
280
281bool _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`.
287void _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.
296bool _shouldConfigureTearDownForTestFile() {
297 return LeakTesting.enabled;
298}
299
300/// Tear down that should happen after all user defined tear down.
301Future<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`.
311class _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
511String _indent(String string, {int? size, String? first}) {
512 size ??= first == null ? 2 : first.length;
513 return _prefixLines(string, ' ' * size, first: first);
514}
515
516String _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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com