| Index: lib/src/runner/engine.dart
|
| diff --git a/lib/src/runner/engine.dart b/lib/src/runner/engine.dart
|
| index 73d48466f642df709283d78a90add734b8e6e0b7..f50fd4885c924f3952d129f597759c701bbb8c09 100644
|
| --- a/lib/src/runner/engine.dart
|
| +++ b/lib/src/runner/engine.dart
|
| @@ -7,6 +7,7 @@ library test.runner.engine;
|
| import 'dart:async';
|
| import 'dart:collection';
|
|
|
| +import 'package:async/async.dart' hide Result;
|
| import 'package:collection/collection.dart';
|
| import 'package:pool/pool.dart';
|
|
|
| @@ -15,7 +16,7 @@ import '../backend/live_test_controller.dart';
|
| import '../backend/state.dart';
|
| import '../backend/suite.dart';
|
| import '../backend/test.dart';
|
| -import '../utils.dart';
|
| +import '../util/delegating_sink.dart';
|
|
|
| /// An [Engine] manages a run that encompasses multiple test suites.
|
| ///
|
| @@ -31,24 +32,50 @@ class Engine {
|
| /// Whether [close] has been called.
|
| var _closed = false;
|
|
|
| + /// Whether [close] was called before all the tests finished running.
|
| + ///
|
| + /// This is `null` if close hasn't been called and the tests are still
|
| + /// running, `true` if close was called before the tests finished running, and
|
| + /// `false` if the tests finished running before close was called.
|
| + var _closedBeforeDone;
|
| +
|
| /// A pool that limits the number of test suites running concurrently.
|
| final Pool _pool;
|
|
|
| - /// An unmodifiable list of tests to run.
|
| + /// Whether all tests passed.
|
| + ///
|
| + /// This fires once all tests have completed and [suiteSink] has been closed.
|
| + /// This will be `null` if [close] was called before all the tests finished
|
| + /// running.
|
| + Future<bool> get success async {
|
| + await _group.future;
|
| + if (_closedBeforeDone) return null;
|
| + return liveTests.every((liveTest) =>
|
| + liveTest.state.result == Result.success);
|
| + }
|
| +
|
| + /// A group of futures for each test suite.
|
| + final _group = new FutureGroup();
|
| +
|
| + /// A sink used to pass [Suite]s in to the engine to run.
|
| + ///
|
| + /// Suites may be added as quickly as they're available; the Engine will only
|
| + /// run as many as necessary at a time based on its concurrency settings.
|
| + Sink<Suite> get suiteSink => new DelegatingSink(_suiteController.sink);
|
| + final _suiteController = new StreamController<Suite>();
|
| +
|
| + /// All the currently-known tests that have run, are running, or will run.
|
| ///
|
| /// These are [LiveTest]s, representing the in-progress state of each test.
|
| /// Tests that have not yet begun running are marked [Status.pending]; tests
|
| /// that have finished are marked [Status.complete].
|
| ///
|
| - /// [LiveTest.run] must not be called on these tests.
|
| - List<LiveTest> get liveTests =>
|
| - new UnmodifiableListView(flatten(_liveTestsBySuite));
|
| -
|
| - /// The tests in [liveTests], organized by their original test suite.
|
| + /// This is guaranteed to contain the same tests as the union of [passed],
|
| + /// [skipped], [failed], and [active].
|
| ///
|
| - /// This allows test suites to be run in parallel without running multiple
|
| - /// tests in the same suite at once.
|
| - final List<List<LiveTest>> _liveTestsBySuite;
|
| + /// [LiveTest.run] must not be called on these tests.
|
| + List<LiveTest> get liveTests => new UnmodifiableListView(_liveTests);
|
| + final _liveTests = new List<LiveTest>();
|
|
|
| /// A stream that emits each [LiveTest] as it's about to start running.
|
| ///
|
| @@ -72,56 +99,53 @@ class Engine {
|
| List<LiveTest> get active => new UnmodifiableListView(_active);
|
| final _active = new List<LiveTest>();
|
|
|
| - /// Returns the tests in [suites] grouped by suite.
|
| + /// Creates an [Engine] that will run all tests provided via [suiteSink].
|
| ///
|
| - /// Also replaces tests marked as "skip" with dummy [LiveTest]s.
|
| - static List<List<LiveTest>> _computeLiveTestsBySuite(Iterable<Suite> suites) {
|
| - return suites.map((suite) {
|
| - return suite.tests.map((test) {
|
| - return test.metadata.skip
|
| - ? _skippedTest(suite, test)
|
| - : test.load(suite);
|
| - }).toList();
|
| - }).toList();
|
| - }
|
| -
|
| - /// Returns a dummy [LiveTest] for a test marked as "skip".
|
| - static LiveTest _skippedTest(Suite suite, Test test) {
|
| - var controller;
|
| - controller = new LiveTestController(suite, test, () {
|
| - controller.setState(const State(Status.running, Result.success));
|
| - controller.setState(const State(Status.complete, Result.success));
|
| - controller.completer.complete();
|
| - }, () {});
|
| - return controller.liveTest;
|
| + /// [concurrency] controls how many suites are run at once.
|
| + Engine({int concurrency})
|
| + : _pool = new Pool(concurrency == null ? 1 : concurrency) {
|
| + _group.future.then((_) {
|
| + if (_closedBeforeDone == null) _closedBeforeDone = false;
|
| + }).catchError((_) {
|
| + // Don't top-level errors. They'll be thrown via [success] anyway.
|
| + });
|
| }
|
|
|
| /// Creates an [Engine] that will run all tests in [suites].
|
| ///
|
| - /// [concurrency] controls how many suites are run at once.
|
| - Engine(Iterable<Suite> suites, {int concurrency})
|
| - : _liveTestsBySuite = _computeLiveTestsBySuite(suites),
|
| - _pool = new Pool(concurrency == null ? 1 : concurrency);
|
| + /// [concurrency] controls how many suites are run at once. An engine
|
| + /// constructed this way will automatically close its [suiteSink], meaning
|
| + /// that no further suites may be provided.
|
| + factory Engine.withSuites(List<Suite> suites, {int concurrency}) {
|
| + var engine = new Engine(concurrency: concurrency);
|
| + for (var suite in suites) engine.suiteSink.add(suite);
|
| + engine.suiteSink.close();
|
| + return engine;
|
| + }
|
|
|
| /// Runs all tests in all suites defined by this engine.
|
| ///
|
| /// This returns `true` if all tests succeed, and `false` otherwise. It will
|
| - /// only return once all tests have finished running.
|
| - Future<bool> run() async {
|
| + /// only return once all tests have finished running and [suiteSink] has been
|
| + /// closed.
|
| + Future<bool> run() {
|
| if (_runCalled) {
|
| throw new StateError("Engine.run() may not be called more than once.");
|
| }
|
| _runCalled = true;
|
|
|
| - await Future.wait(_liveTestsBySuite.map((suite) {
|
| - return _pool.withResource(() {
|
| + _suiteController.stream.listen((suite) {
|
| + _group.add(_pool.withResource(() {
|
| if (_closed) return null;
|
|
|
| // TODO(nweiz): Use a real for loop when issue 23394 is fixed.
|
| - return Future.forEach(suite, (liveTest) async {
|
| - // TODO(nweiz): Just "return;" when issue 23200 is fixed.
|
| - if (_closed) return null;
|
| + return Future.forEach(suite.tests, (test) async {
|
| + if (_closed) return;
|
|
|
| + var liveTest = test.metadata.skip
|
| + ? _skippedTest(suite, test)
|
| + : test.load(suite);
|
| + _liveTests.add(liveTest);
|
| _active.add(liveTest);
|
|
|
| liveTest.onStateChange.listen((state) {
|
| @@ -143,15 +167,25 @@ class Engine {
|
| // First, schedule a microtask to ensure that [onTestStarted] fires
|
| // before the first [LiveTest.onStateChange] event. Once the test
|
| // finishes, use [new Future] to do a coarse-grained event loop pump
|
| - // to avoid starving the DOM or other non-microtask events.
|
| + // to avoid starving non-microtask events.
|
| await new Future.microtask(liveTest.run);
|
| await new Future(() {});
|
| });
|
| - });
|
| - }));
|
| + }));
|
| + }, onDone: _group.close);
|
| +
|
| + return success;
|
| + }
|
|
|
| - return liveTests.every(
|
| - (liveTest) => liveTest.state.result == Result.success);
|
| + /// Returns a dummy [LiveTest] for a test marked as "skip".
|
| + LiveTest _skippedTest(Suite suite, Test test) {
|
| + var controller;
|
| + controller = new LiveTestController(suite, test, () {
|
| + controller.setState(const State(Status.running, Result.success));
|
| + controller.setState(const State(Status.complete, Result.success));
|
| + controller.completer.complete();
|
| + }, () {});
|
| + return controller.liveTest;
|
| }
|
|
|
| /// Signals that the caller is done paying attention to test results and the
|
| @@ -160,8 +194,15 @@ class Engine {
|
| /// Any actively-running tests are also closed. VM tests are allowed to finish
|
| /// running so that any modifications they've made to the filesystem can be
|
| /// cleaned up.
|
| + ///
|
| + /// **Note that closing the engine is not the same as closing [suiteSink].**
|
| + /// Closing [suiteSink] indicates that no more input will be provided, closing
|
| + /// the engine indicates that no more output should be emitted.
|
| Future close() {
|
| _closed = true;
|
| + if (_closedBeforeDone == null) _closedBeforeDone = true;
|
| + _suiteController.close();
|
| +
|
| return Future.wait(liveTests.map((liveTest) => liveTest.close()));
|
| }
|
| }
|
|
|