| // Copyright 2014 The Flutter Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| /// @docImport 'package:test_api/backend.dart'; | |
| /// @docImport 'package:test_api/scaffolding.dart'; | |
| library; | |
| import 'dart:async'; | |
| import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; | |
| import 'package:meta/meta.dart'; | |
| import 'package:test_api/scaffolding.dart' show Timeout; | |
| import 'package:test_api/src/backend/declarer.dart'; // ignore: implementation_imports | |
| import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports | |
| import 'package:test_api/src/backend/group_entry.dart'; // ignore: implementation_imports | |
| import 'package:test_api/src/backend/invoker.dart'; // ignore: implementation_imports | |
| import 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports | |
| import 'package:test_api/src/backend/message.dart'; // ignore: implementation_imports | |
| import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports | |
| import 'package:test_api/src/backend/state.dart'; // ignore: implementation_imports | |
| import 'package:test_api/src/backend/suite.dart'; // ignore: implementation_imports | |
| import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports | |
| import 'package:test_api/src/backend/test.dart'; // ignore: implementation_imports | |
| export 'package:test_api/fake.dart' show Fake; | |
| Declarer? _localDeclarer; | |
| Declarer get _declarer { | |
| final declarer = Zone.current[#test.declarer] as Declarer?; | |
| if (declarer != null) { | |
| return declarer; | |
| } | |
| // If no declarer is defined, this test is being run via `flutter run -t test_file.dart`. | |
| if (_localDeclarer == null) { | |
| _localDeclarer = Declarer(); | |
| Future<void>(() { | |
| Invoker.guard<Future<void>>(() async { | |
| final reporter = _Reporter(color: false); // disable color when run directly. | |
| // ignore: recursive_getters, this self-call is safe since it will just fetch the declarer instance | |
| final Group group = _declarer.build(); | |
| final suite = Suite(group, SuitePlatform(Runtime.vm)); | |
| await _runGroup(suite, group, <Group>[], reporter); | |
| reporter._onDone(); | |
| }); | |
| }); | |
| } | |
| return _localDeclarer!; | |
| } | |
| Future<void> _runGroup( | |
| Suite suiteConfig, | |
| Group group, | |
| List<Group> parents, | |
| _Reporter reporter, | |
| ) async { | |
| parents.add(group); | |
| try { | |
| final bool skipGroup = group.metadata.skip; | |
| var setUpAllSucceeded = true; | |
| if (!skipGroup && group.setUpAll != null) { | |
| final LiveTest liveTest = group.setUpAll!.load(suiteConfig, groups: parents); | |
| await _runLiveTest(suiteConfig, liveTest, reporter, countSuccess: false); | |
| setUpAllSucceeded = liveTest.state.result.isPassing; | |
| } | |
| if (setUpAllSucceeded) { | |
| for (final GroupEntry entry in group.entries) { | |
| if (entry is Group) { | |
| await _runGroup(suiteConfig, entry, parents, reporter); | |
| } else if (entry.metadata.skip) { | |
| await _runSkippedTest(suiteConfig, entry as Test, parents, reporter); | |
| } else { | |
| final test = entry as Test; | |
| await _runLiveTest(suiteConfig, test.load(suiteConfig, groups: parents), reporter); | |
| } | |
| } | |
| } | |
| // Even if we're closed or setUpAll failed, we want to run all the | |
| // teardowns to ensure that any state is properly cleaned up. | |
| if (!skipGroup && group.tearDownAll != null) { | |
| final LiveTest liveTest = group.tearDownAll!.load(suiteConfig, groups: parents); | |
| await _runLiveTest(suiteConfig, liveTest, reporter, countSuccess: false); | |
| } | |
| } finally { | |
| parents.remove(group); | |
| } | |
| } | |
| Future<void> _runLiveTest( | |
| Suite suiteConfig, | |
| LiveTest liveTest, | |
| _Reporter reporter, { | |
| bool countSuccess = true, | |
| }) async { | |
| reporter._onTestStarted(liveTest); | |
| // Schedule a microtask to ensure that [onTestStarted] fires before the | |
| // first [LiveTest.onStateChange] event. | |
| await Future<void>.microtask(liveTest.run); | |
| // Once the test finishes, use await null to do a coarse-grained event | |
| // loop pump to avoid starving non-microtask events. | |
| await null; | |
| final bool isSuccess = liveTest.state.result.isPassing; | |
| if (isSuccess) { | |
| reporter.passed.add(liveTest); | |
| } else { | |
| reporter.failed.add(liveTest); | |
| } | |
| } | |
| Future<void> _runSkippedTest( | |
| Suite suiteConfig, | |
| Test test, | |
| List<Group> parents, | |
| _Reporter reporter, | |
| ) async { | |
| final skipped = LocalTest(test.name, test.metadata, () {}, trace: test.trace); | |
| if (skipped.metadata.skipReason != null) { | |
| reporter.log('Skip: ${skipped.metadata.skipReason}'); | |
| } | |
| final LiveTest liveTest = skipped.load(suiteConfig); | |
| reporter._onTestStarted(liveTest); | |
| reporter.skipped.add(skipped); | |
| } | |
| // TODO(nweiz): This and other top-level functions should throw exceptions if | |
| // they're called after the declarer has finished declaring. | |
| /// Creates a new test case with the given description (converted to a string) | |
| /// and body. | |
| /// | |
| /// The description will be added to the descriptions of any surrounding | |
| /// [group]s. If [testOn] is passed, it's parsed as a [platform selector][]; the | |
| /// test will only be run on matching platforms. | |
| /// | |
| /// [platform selector]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-selectors | |
| /// | |
| /// If [timeout] is passed, it's used to modify or replace the default timeout | |
| /// of 30 seconds. Timeout modifications take precedence in suite-group-test | |
| /// order, so [timeout] will also modify any timeouts set on the group or suite. | |
| /// | |
| /// If [skip] is a String or `true`, the test is skipped. If it's a String, it | |
| /// should explain why the test is skipped; this reason will be printed instead | |
| /// of running the test. | |
| /// | |
| /// If [tags] is passed, it declares user-defined tags that are applied to the | |
| /// test. These tags can be used to select or skip the test on the command line, | |
| /// or to do bulk test configuration. All tags should be declared in the | |
| /// [package configuration file][configuring tags]. The parameter can be an | |
| /// [Iterable] of tag names, or a [String] representing a single tag. | |
| /// | |
| /// If [retry] is passed, the test will be retried the provided number of times | |
| /// before being marked as a failure. | |
| /// | |
| /// [configuring tags]: https://github.com/dart-lang/test/blob/44d6cb196f34a93a975ed5f3cb76afcc3a7b39b0/doc/package_config.md#configuring-tags | |
| /// | |
| /// [onPlatform] allows tests to be configured on a platform-by-platform | |
| /// basis. It's a map from strings that are parsed as [PlatformSelector]s to | |
| /// annotation classes: [Timeout], [Skip], or lists of those. These | |
| /// annotations apply only on the given platforms. For example: | |
| /// | |
| /// test('potentially slow test', () { | |
| /// // ... | |
| /// }, onPlatform: { | |
| /// // This test is especially slow on Windows. | |
| /// 'windows': Timeout.factor(2), | |
| /// 'browser': [ | |
| /// Skip('add browser support'), | |
| /// // This will be slow on browsers once it works on them. | |
| /// Timeout.factor(2) | |
| /// ] | |
| /// }); | |
| /// | |
| /// If multiple platforms match, the annotations apply in order as through | |
| /// they were in nested groups. | |
| @isTest | |
| void test( | |
| Object description, | |
| dynamic Function() body, { | |
| String? testOn, | |
| Timeout? timeout, | |
| dynamic skip, | |
| dynamic tags, | |
| Map<String, dynamic>? onPlatform, | |
| int? retry, | |
| }) { | |
| _maybeConfigureTearDownForTestFile(); | |
| _declarer.test( | |
| description.toString(), | |
| body, | |
| testOn: testOn, | |
| timeout: timeout, | |
| skip: skip, | |
| onPlatform: onPlatform, | |
| tags: tags, | |
| retry: retry, | |
| ); | |
| } | |
| /// Creates a group of tests. | |
| /// | |
| /// A group's description (converted to a string) is included in the descriptions | |
| /// of any tests or sub-groups it contains. [setUp] and [tearDown] are also scoped | |
| /// to the containing group. | |
| /// | |
| /// If `skip` is a String or `true`, the group is skipped. If it's a String, it | |
| /// should explain why the group is skipped; this reason will be printed instead | |
| /// of running the group's tests. | |
| @isTestGroup | |
| void group(Object description, void Function() body, {dynamic skip, int? retry}) { | |
| _maybeConfigureTearDownForTestFile(); | |
| _declarer.group(description.toString(), body, skip: skip, retry: retry); | |
| } | |
| /// Registers a function to be run before tests. | |
| /// | |
| /// This function will be called before each test is run. The `body` may be | |
| /// asynchronous; if so, it must return a [Future]. | |
| /// | |
| /// If this is called within a test group, it applies only to tests in that | |
| /// group. The `body` will be run after any set-up callbacks in parent groups or | |
| /// at the top level. | |
| /// | |
| /// Each callback at the top level or in a given group will be run in the order | |
| /// they were declared. | |
| void setUp(dynamic Function() body) { | |
| _maybeConfigureTearDownForTestFile(); | |
| _declarer.setUp(body); | |
| } | |
| /// Registers a function to be run after tests. | |
| /// | |
| /// This function will be called after each test is run. The `body` may be | |
| /// asynchronous; if so, it must return a [Future]. | |
| /// | |
| /// If this is called within a test group, it applies only to tests in that | |
| /// group. The `body` will be run before any tear-down callbacks in parent | |
| /// groups or at the top level. | |
| /// | |
| /// Each callback at the top level or in a given group will be run in the | |
| /// reverse of the order they were declared. | |
| /// | |
| /// See also [addTearDown], which adds tear-downs to a running test. | |
| void tearDown(dynamic Function() body) { | |
| _maybeConfigureTearDownForTestFile(); | |
| _declarer.tearDown(body); | |
| } | |
| /// Registers a function to be run once before all tests. | |
| /// | |
| /// The `body` may be asynchronous; if so, it must return a [Future]. | |
| /// | |
| /// If this is called within a test group, The `body` will run before all tests | |
| /// in that group. It will be run after any [setUpAll] callbacks in parent | |
| /// groups or at the top level. It won't be run if none of the tests in the | |
| /// group are run. | |
| /// | |
| /// **Note**: This function makes it very easy to accidentally introduce hidden | |
| /// dependencies between tests that should be isolated. In general, you should | |
| /// prefer [setUp], and only use [setUpAll] if the callback is prohibitively | |
| /// slow. | |
| void setUpAll(dynamic Function() body) { | |
| _maybeConfigureTearDownForTestFile(); | |
| _declarer.setUpAll(body); | |
| } | |
| /// Registers a function to be run once after all tests. | |
| /// | |
| /// If this is called within a test group, `body` will run after all tests | |
| /// in that group. It will be run before any [tearDownAll] callbacks in parent | |
| /// groups or at the top level. It won't be run if none of the tests in the | |
| /// group are run. | |
| /// | |
| /// **Note**: This function makes it very easy to accidentally introduce hidden | |
| /// dependencies between tests that should be isolated. In general, you should | |
| /// prefer [tearDown], and only use [tearDownAll] if the callback is | |
| /// prohibitively slow. | |
| void tearDownAll(dynamic Function() body) { | |
| _maybeConfigureTearDownForTestFile(); | |
| _declarer.tearDownAll(body); | |
| } | |
| bool _isTearDownForTestFileConfigured = false; | |
| /// If needed, configures `tearDownAll` after all user defined `tearDownAll` in the test file. | |
| /// | |
| /// This function should be invoked in all functions, that may be invoked by user in the test file, | |
| /// to be invoked before any other `tearDownAll`. | |
| void _maybeConfigureTearDownForTestFile() { | |
| if (_isTearDownForTestFileConfigured || !_shouldConfigureTearDownForTestFile()) { | |
| return; | |
| } | |
| _declarer.tearDownAll(_tearDownForTestFile); | |
| _isTearDownForTestFileConfigured = true; | |
| } | |
| /// Returns true if tear down for the test file needs to be configured. | |
| bool _shouldConfigureTearDownForTestFile() { | |
| return LeakTesting.enabled; | |
| } | |
| /// Tear down that should happen after all user defined tear down. | |
| Future<void> _tearDownForTestFile() async { | |
| await maybeTearDownLeakTrackingForAll(); | |
| } | |
| /// A reporter that prints each test on its own line. | |
| /// | |
| /// This is currently used in place of `CompactReporter` by `lib/test.dart`, | |
| /// which can't transitively import `dart:io` but still needs access to a runner | |
| /// so that test files can be run directly. This means that until issue 6943 is | |
| /// fixed, this must not import `dart:io`. | |
| class _Reporter { | |
| _Reporter({bool color = true}) | |
| : _printPath = true, | |
| _green = color ? '\u001b[32m' : '', | |
| _red = color ? '\u001b[31m' : '', | |
| _yellow = color ? '\u001b[33m' : '', | |
| _bold = color ? '\u001b[1m' : '', | |
| _noColor = color ? '\u001b[0m' : ''; | |
| final List<LiveTest> passed = <LiveTest>[]; | |
| final List<LiveTest> failed = <LiveTest>[]; | |
| final List<Test> skipped = <Test>[]; | |
| /// The terminal escape for green text, or the empty string if this is Windows | |
| /// or not outputting to a terminal. | |
| final String _green; | |
| /// The terminal escape for red text, or the empty string if this is Windows | |
| /// or not outputting to a terminal. | |
| final String _red; | |
| /// The terminal escape for yellow text, or the empty string if this is | |
| /// Windows or not outputting to a terminal. | |
| final String _yellow; | |
| /// The terminal escape for bold text, or the empty string if this is | |
| /// Windows or not outputting to a terminal. | |
| final String _bold; | |
| /// The terminal escape for removing test coloring, or the empty string if | |
| /// this is Windows or not outputting to a terminal. | |
| final String _noColor; | |
| /// Whether the path to each test's suite should be printed. | |
| final bool _printPath; | |
| /// A stopwatch that tracks the duration of the full run. | |
| final Stopwatch _stopwatch = Stopwatch(); // flutter_ignore: stopwatch (see analyze.dart) | |
| // Ignore context: Used for logging of actual test runs, outside of FakeAsync. | |
| /// The size of `_engine.passed` last time a progress notification was | |
| /// printed. | |
| int? _lastProgressPassed; | |
| /// The size of `_engine.skipped` last time a progress notification was | |
| /// printed. | |
| int? _lastProgressSkipped; | |
| /// The size of `_engine.failed` last time a progress notification was | |
| /// printed. | |
| int? _lastProgressFailed; | |
| /// The message printed for the last progress notification. | |
| String? _lastProgressMessage; | |
| /// The suffix added to the last progress notification. | |
| String? _lastProgressSuffix; | |
| /// The set of all subscriptions to various streams. | |
| final Set<StreamSubscription<void>> _subscriptions = <StreamSubscription<void>>{}; | |
| /// A callback called when the engine begins running [liveTest]. | |
| void _onTestStarted(LiveTest liveTest) { | |
| if (!_stopwatch.isRunning) { | |
| _stopwatch.start(); | |
| } | |
| _progressLine(_description(liveTest)); | |
| _subscriptions.add( | |
| liveTest.onStateChange.listen((State state) => _onStateChange(liveTest, state)), | |
| ); | |
| _subscriptions.add( | |
| liveTest.onError.listen( | |
| (AsyncError error) => _onError(liveTest, error.error, error.stackTrace), | |
| ), | |
| ); | |
| _subscriptions.add( | |
| liveTest.onMessage.listen((Message message) { | |
| _progressLine(_description(liveTest)); | |
| String text = message.text; | |
| if (message.type == MessageType.skip) { | |
| text = ' $_yellow$text$_noColor'; | |
| } | |
| log(text); | |
| }), | |
| ); | |
| } | |
| /// A callback called when [liveTest]'s state becomes [state]. | |
| void _onStateChange(LiveTest liveTest, State state) { | |
| if (state.status != Status.complete) { | |
| return; | |
| } | |
| } | |
| /// A callback called when [liveTest] throws [error]. | |
| void _onError(LiveTest liveTest, Object error, StackTrace stackTrace) { | |
| if (liveTest.state.status != Status.complete) { | |
| return; | |
| } | |
| _progressLine(_description(liveTest), suffix: ' $_bold$_red[E]$_noColor'); | |
| log(_indent(error.toString())); | |
| log(_indent('$stackTrace')); | |
| } | |
| /// A callback called when the engine is finished running tests. | |
| void _onDone() { | |
| final bool success = failed.isEmpty; | |
| if (!success) { | |
| _progressLine('Some tests failed.', color: _red); | |
| } else if (passed.isEmpty) { | |
| _progressLine('All tests skipped.'); | |
| } else { | |
| _progressLine('All tests passed!'); | |
| } | |
| } | |
| /// Prints a line representing the current state of the tests. | |
| /// | |
| /// [message] goes after the progress report. If [color] is passed, it's used | |
| /// as the color for [message]. If [suffix] is passed, it's added to the end | |
| /// of [message]. | |
| void _progressLine(String message, {String? color, String? suffix}) { | |
| // Print nothing if nothing has changed since the last progress line. | |
| if (passed.length == _lastProgressPassed && | |
| skipped.length == _lastProgressSkipped && | |
| failed.length == _lastProgressFailed && | |
| message == _lastProgressMessage && | |
| // Don't re-print just because a suffix was removed. | |
| (suffix == null || suffix == _lastProgressSuffix)) { | |
| return; | |
| } | |
| _lastProgressPassed = passed.length; | |
| _lastProgressSkipped = skipped.length; | |
| _lastProgressFailed = failed.length; | |
| _lastProgressMessage = message; | |
| _lastProgressSuffix = suffix; | |
| if (suffix != null) { | |
| message += suffix; | |
| } | |
| color ??= ''; | |
| final Duration duration = _stopwatch.elapsed; | |
| final buffer = StringBuffer(); | |
| // \r moves back to the beginning of the current line. | |
| buffer.write('${_timeString(duration)} '); | |
| buffer.write(_green); | |
| buffer.write('+'); | |
| buffer.write(passed.length); | |
| buffer.write(_noColor); | |
| if (skipped.isNotEmpty) { | |
| buffer.write(_yellow); | |
| buffer.write(' ~'); | |
| buffer.write(skipped.length); | |
| buffer.write(_noColor); | |
| } | |
| if (failed.isNotEmpty) { | |
| buffer.write(_red); | |
| buffer.write(' -'); | |
| buffer.write(failed.length); | |
| buffer.write(_noColor); | |
| } | |
| buffer.write(': '); | |
| buffer.write(color); | |
| buffer.write(message); | |
| buffer.write(_noColor); | |
| log(buffer.toString()); | |
| } | |
| /// Returns a representation of [duration] as `MM:SS`. | |
| String _timeString(Duration duration) { | |
| final String minutes = duration.inMinutes.toString().padLeft(2, '0'); | |
| final String seconds = (duration.inSeconds % 60).toString().padLeft(2, '0'); | |
| return '$minutes:$seconds'; | |
| } | |
| /// Returns a description of [liveTest]. | |
| /// | |
| /// This differs from the test's own description in that it may also include | |
| /// the suite's name. | |
| String _description(LiveTest liveTest) { | |
| String name = liveTest.test.name; | |
| if (_printPath && liveTest.suite.path != null) { | |
| name = '${liveTest.suite.path}: $name'; | |
| } | |
| return name; | |
| } | |
| /// Print the message to the console. | |
| void log(String message) { | |
| // We centralize all the prints in this file through this one method so that | |
| // in principle we can reroute the output easily should we need to. | |
| print(message); // ignore: avoid_print | |
| } | |
| } | |
| String _indent(String string, {int? size, String? first}) { | |
| size ??= first == null ? 2 : first.length; | |
| return _prefixLines(string, ' ' * size, first: first); | |
| } | |
| String _prefixLines(String text, String prefix, {String? first, String? last, String? single}) { | |
| first ??= prefix; | |
| last ??= prefix; | |
| single ??= first; | |
| final List<String> lines = text.split('\n'); | |
| if (lines.length == 1) { | |
| return '$single$text'; | |
| } | |
| final buffer = StringBuffer('$first${lines.first}\n'); | |
| // Write out all but the first and last lines with [prefix]. | |
| for (final String line in lines.skip(1).take(lines.length - 2)) { | |
| buffer.writeln('$prefix$line'); | |
| } | |
| buffer.write('$last${lines.last}'); | |
| return buffer.toString(); | |
| } |