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