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