Index: lib/src/runner.dart |
diff --git a/lib/src/runner.dart b/lib/src/runner.dart |
index 394348d37f5754e637e4613ed251383f8fb33761..c0c91f063f7b9f9e9cf151412183bc2449b27168 100644 |
--- a/lib/src/runner.dart |
+++ b/lib/src/runner.dart |
@@ -7,20 +7,26 @@ library test.runner; |
import 'dart:async'; |
import 'dart:io'; |
-import 'package:async/async.dart'; |
- |
import 'backend/metadata.dart'; |
+import 'backend/suite.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 'util/async_thunk.dart'; |
+import 'util/io.dart'; |
import 'utils.dart'; |
+/// The set of platforms for which debug flags are (currently) not supported. |
+final _debugUnsupportedPlatforms = new Set.from( |
+ [TestPlatform.vm, TestPlatform.phantomJS, TestPlatform.contentShell]); |
+ |
/// 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 |
@@ -36,13 +42,16 @@ class Runner { |
/// 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 thunk for ensuring [close] only runs once. |
final _closeThunk = new AsyncThunk(); |
bool get _closed => _closeThunk.hasRun; |
- /// Whether [run] has been called. |
- bool _hasRun = false; |
- |
/// Creates a new runner based on [configuration]. |
factory Runner(Configuration configuration) { |
var metadata = new Metadata( |
@@ -60,7 +69,7 @@ class Runner { |
? CompactReporter.watch |
: ExpandedReporter.watch; |
- watch( |
+ var reporter = watch( |
engine, |
color: configuration.color, |
verboseTrace: configuration.verboseTrace, |
@@ -68,28 +77,34 @@ class Runner { |
new Directory(configuration.paths.single).existsSync(), |
printPlatform: configuration.platforms.length > 1); |
- return new Runner._(configuration, loader, engine); |
+ return new Runner._(configuration, loader, engine, reporter); |
} |
- Runner._(this._configuration, this._loader, this._engine); |
+ Runner._(this._configuration, 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 { |
- _hasRun = true; |
- |
if (_closed) { |
throw new StateError("run() may not be called on a closed Runner."); |
} |
+ var suites = _loadSuites(); |
+ |
var success; |
- var results = await Future.wait([ |
- _loadSuites(), |
- _engine.run() |
- ], eagerError: true); |
- success = results.last; |
+ if (_configuration.pauseAfterLoad) { |
+ // TODO(nweiz): disable timeouts when debugging. |
+ 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; |
@@ -118,18 +133,22 @@ class Runner { |
/// filesystem. |
Future close() => _closeThunk.run(() async { |
var timer; |
- if (_hasRun) { |
+ 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), () { |
- // Print a blank line first to ensure that this doesn't interfere with |
- // the compact reporter's unfinished line. |
- print(''); |
+ // 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(); |
@@ -137,12 +156,12 @@ class Runner { |
await _loader.close(); |
}); |
- /// Load the test suites in [_configuration.paths] that match |
- /// [_configuration.pattern]. |
- Future _loadSuites() async { |
- var group = new FutureGroup(); |
- |
- mergeStreams(_configuration.paths.map((path) { |
+ /// Return a stream of [LoadSuite]s in [_configuration.paths]. |
+ /// |
+ /// Only tests that match [_configuration.pattern] will be included in the |
+ /// suites once they're loaded. |
+ Stream<LoadSuite> _loadSuites() { |
+ return mergeStreams(_configuration.paths.map((path) { |
if (new Directory(path).existsSync()) return _loader.loadDir(path); |
if (new File(path).existsSync()) return _loader.loadFile(path); |
@@ -150,22 +169,79 @@ class Runner { |
new LoadSuite("loading $path", () => |
throw new LoadException(path, 'Does not exist.')) |
]); |
- })).listen((loadSuite) { |
- group.add(new Future.sync(() { |
- _engine.suiteSink.add(loadSuite.changeSuite((suite) { |
- if (_configuration.pattern == null) return suite; |
- return suite.change(tests: suite.tests.where( |
- (test) => test.name.contains(_configuration.pattern))); |
- })); |
- })); |
- }, onError: (error, stackTrace) { |
- group.add(new Future.error(error, stackTrace)); |
- }, onDone: group.close); |
- |
- await group.future; |
- |
- // Once we've loaded all the suites, notify the engine that no more will be |
- // coming. |
- _engine.suiteSink.close(); |
+ })).map((loadSuite) { |
+ return loadSuite.changeSuite((suite) { |
+ if (_configuration.pattern == null) return suite; |
+ return suite.change(tests: suite.tests.where((test) => |
+ test.name.contains(_configuration.pattern))); |
+ }); |
+ }); |
+ } |
+ |
+ /// Loads each suite in [suites] in order, pausing after load for platforms |
+ /// that support debugging. |
+ Future<bool> _loadThenPause(Stream<LoadSuite> suites) async { |
+ var unsupportedPlatforms = _configuration.platforms |
+ .where(_debugUnsupportedPlatforms.contains) |
+ .map((platform) => |
+ platform == TestPlatform.vm ? "the Dart VM" : platform.name) |
+ .toList(); |
+ |
+ if (unsupportedPlatforms.isNotEmpty) { |
+ warn( |
+ wordWrap("Debugging is currently unsupported on " |
+ "${toSentence(unsupportedPlatforms)}."), |
+ color: _configuration.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(Suite suite) async { |
+ if (suite.platform == null) return; |
+ if (_debugUnsupportedPlatforms.contains(suite.platform)) return; |
+ |
+ try { |
+ _reporter.pause(); |
+ |
+ var bold = _configuration.color ? '\u001b[1m' : ''; |
+ var noColor = _configuration.color ? '\u001b[0m' : ''; |
+ print(''); |
+ print(wordWrap( |
+ "${bold}The test runner is paused.${noColor} Open the dev console in " |
+ "${suite.platform} and set breakpoints. Once you're finished, " |
+ "return to this terminal and press Enter.")); |
+ |
+ // TODO(nweiz): Display something in the paused browsers indicating that |
+ // they're paused. |
+ |
+ await stdinLines.next; |
+ } finally { |
+ _reporter.resume(); |
+ } |
} |
} |