| // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| library test.runner; |
| |
| import 'dart:async'; |
| import 'dart:io'; |
| |
| import 'package:async/async.dart'; |
| |
| import 'backend/test_platform.dart'; |
| import 'runner/application_exception.dart'; |
| import 'runner/configuration.dart'; |
| import 'runner/engine.dart'; |
| import 'runner/load_exception.dart'; |
| import 'runner/load_suite.dart'; |
| import 'runner/loader.dart'; |
| import 'runner/reporter.dart'; |
| import 'runner/reporter/compact.dart'; |
| import 'runner/reporter/expanded.dart'; |
| import 'runner/runner_suite.dart'; |
| import 'util/io.dart'; |
| import 'utils.dart'; |
| |
| /// A class that loads and runs tests based on a [Configuration]. |
| /// |
| /// This maintains a [Loader] and an [Engine] and passes test suites from one to |
| /// the other, as well as printing out tests with a [CompactReporter] or an |
| /// [ExpandedReporter]. |
| class Runner { |
| /// The configuration for the runner. |
| final Configuration _config; |
| |
| /// The loader that loads the test suites from the filesystem. |
| final Loader _loader; |
| |
| /// The engine that runs the test suites. |
| final Engine _engine; |
| |
| /// The reporter that's emitting the test runner's results. |
| final Reporter _reporter; |
| |
| /// The subscription to the stream returned by [_loadSuites]. |
| StreamSubscription _suiteSubscription; |
| |
| /// The memoizer for ensuring [close] only runs once. |
| final _closeMemo = new AsyncMemoizer(); |
| bool get _closed => _closeMemo.hasRun; |
| |
| /// Creates a new runner based on [configuration]. |
| factory Runner(Configuration config) { |
| var loader = new Loader(config); |
| var engine = new Engine(concurrency: config.concurrency); |
| |
| var watch = config.reporter == "compact" |
| ? CompactReporter.watch |
| : ExpandedReporter.watch; |
| |
| var reporter = watch( |
| engine, |
| color: config.color, |
| verboseTrace: config.verboseTrace, |
| printPath: config.paths.length > 1 || |
| new Directory(config.paths.single).existsSync(), |
| printPlatform: config.platforms.length > 1); |
| |
| return new Runner._(config, loader, engine, reporter); |
| } |
| |
| Runner._(this._config, this._loader, this._engine, this._reporter); |
| |
| /// Starts the runner. |
| /// |
| /// This starts running tests and printing their progress. It returns whether |
| /// or not they ran successfully. |
| Future<bool> run() async { |
| if (_closed) { |
| throw new StateError("run() may not be called on a closed Runner."); |
| } |
| |
| var suites = _loadSuites(); |
| |
| var success; |
| if (_config.pauseAfterLoad) { |
| success = await _loadThenPause(suites); |
| } else { |
| _suiteSubscription = suites.listen(_engine.suiteSink.add); |
| var results = await Future.wait([ |
| _suiteSubscription.asFuture().then((_) => _engine.suiteSink.close()), |
| _engine.run() |
| ], eagerError: true); |
| success = results.last; |
| } |
| |
| if (_closed) return false; |
| |
| if (_engine.passed.length == 0 && _engine.failed.length == 0 && |
| _engine.skipped.length == 0 && _config.pattern != null) { |
| var message = 'No tests match '; |
| |
| if (_config.pattern is RegExp) { |
| var pattern = (_config.pattern as RegExp).pattern; |
| message += 'regular expression "$pattern".'; |
| } else { |
| message += '"${_config.pattern}".'; |
| } |
| throw new ApplicationException(message); |
| } |
| |
| // Explicitly check "== true" here because [Engine.run] can return `null` |
| // if the engine was closed prematurely. |
| return success == true; |
| } |
| |
| /// Closes the runner. |
| /// |
| /// This stops any future test suites from running. It will wait for any |
| /// currently-running VM tests, in case they have stuff to clean up on the |
| /// filesystem. |
| Future close() => _closeMemo.runOnce(() async { |
| var timer; |
| if (!_engine.isIdle) { |
| // Wait a bit to print this message, since printing it eagerly looks weird |
| // if the tests then finish immediately. |
| timer = new Timer(new Duration(seconds: 1), () { |
| // Pause the reporter while we print to ensure that we don't interfere |
| // with its output. |
| _reporter.pause(); |
| print("Waiting for current test(s) to finish."); |
| print("Press Control-C again to terminate immediately."); |
| _reporter.resume(); |
| }); |
| } |
| |
| if (_suiteSubscription != null) _suiteSubscription.cancel(); |
| _suiteSubscription = null; |
| |
| // Make sure we close the engine *before* the loader. Otherwise, |
| // LoadSuites provided by the loader may get into bad states. |
| await _engine.close(); |
| if (timer != null) timer.cancel(); |
| await _loader.close(); |
| }); |
| |
| /// Return a stream of [LoadSuite]s in [_config.paths]. |
| /// |
| /// Only tests that match [_config.pattern] will be included in the |
| /// suites once they're loaded. |
| Stream<LoadSuite> _loadSuites() { |
| return mergeStreams(_config.paths.map((path) { |
| if (new Directory(path).existsSync()) return _loader.loadDir(path); |
| if (new File(path).existsSync()) return _loader.loadFile(path); |
| |
| return new Stream.fromIterable([ |
| new LoadSuite("loading $path", () => |
| throw new LoadException(path, 'Does not exist.')) |
| ]); |
| })).map((loadSuite) { |
| return loadSuite.changeSuite((suite) { |
| if (_config.pattern == null) return suite; |
| return suite.filter((test) => test.name.contains(_config.pattern)); |
| }); |
| }); |
| } |
| |
| /// Loads each suite in [suites] in order, pausing after load for platforms |
| /// that support debugging. |
| Future<bool> _loadThenPause(Stream<LoadSuite> suites) async { |
| if (_config.platforms.contains(TestPlatform.vm)) { |
| warn("Debugging is currently unsupported on the Dart VM.", |
| color: _config.color); |
| } |
| |
| _suiteSubscription = suites.asyncMap((loadSuite) async { |
| // Make the underlying suite null so that the engine doesn't start running |
| // it immediately. |
| _engine.suiteSink.add(loadSuite.changeSuite((_) => null)); |
| |
| var suite = await loadSuite.suite; |
| if (suite == null) return; |
| |
| await _pause(suite); |
| if (_closed) return; |
| |
| _engine.suiteSink.add(suite); |
| await _engine.onIdle.first; |
| }).listen(null); |
| |
| var results = await Future.wait([ |
| _suiteSubscription.asFuture().then((_) => _engine.suiteSink.close()), |
| _engine.run() |
| ]); |
| return results.last; |
| } |
| |
| /// Pauses the engine and the reporter so that the user can set breakpoints as |
| /// necessary. |
| /// |
| /// This is a no-op for test suites that aren't on platforms where debugging |
| /// is supported. |
| Future _pause(RunnerSuite suite) async { |
| if (suite.platform == null) return; |
| if (suite.platform == TestPlatform.vm) return; |
| |
| try { |
| _reporter.pause(); |
| |
| var bold = _config.color ? '\u001b[1m' : ''; |
| var yellow = _config.color ? '\u001b[33m' : ''; |
| var noColor = _config.color ? '\u001b[0m' : ''; |
| print(''); |
| |
| if (suite.platform.isDartVM) { |
| var url = suite.environment.observatoryUrl; |
| if (url == null) { |
| print("${yellow}Observatory URL not found. Make sure you're using " |
| "${suite.platform.name} 1.11 or later.$noColor"); |
| } else { |
| print("Observatory URL: $bold$url$noColor"); |
| } |
| } |
| |
| if (suite.platform.isHeadless) { |
| var url = suite.environment.remoteDebuggerUrl; |
| if (url == null) { |
| print("${yellow}Remote debugger URL not found.$noColor"); |
| } else { |
| print("Remote debugger URL: $bold$url$noColor"); |
| } |
| } |
| |
| var buffer = new StringBuffer( |
| "${bold}The test runner is paused.${noColor} "); |
| if (!suite.platform.isHeadless) { |
| buffer.write("Open the dev console in ${suite.platform} "); |
| } else { |
| buffer.write("Open the remote debugger "); |
| } |
| if (suite.platform.isDartVM) buffer.write("or the Observatory "); |
| |
| buffer.write("and set breakpoints. Once you're finished, return to this " |
| "terminal and press Enter."); |
| |
| print(wordWrap(buffer.toString())); |
| |
| await inCompletionOrder([ |
| suite.environment.displayPause(), |
| cancelableNext(stdinLines) |
| ]).first; |
| } finally { |
| _reporter.resume(); |
| } |
| } |
| } |