Index: mojo/public/dart/third_party/test/lib/src/runner/engine.dart |
diff --git a/mojo/public/dart/third_party/test/lib/src/runner/engine.dart b/mojo/public/dart/third_party/test/lib/src/runner/engine.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..c93e793a2de1ce102f06573e1a309348e9caee91 |
--- /dev/null |
+++ b/mojo/public/dart/third_party/test/lib/src/runner/engine.dart |
@@ -0,0 +1,334 @@ |
+// 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.engine; |
+ |
+import 'dart:async'; |
+import 'dart:collection'; |
+ |
+import 'package:async/async.dart' hide Result; |
+import 'package:collection/collection.dart'; |
+import 'package:pool/pool.dart'; |
+ |
+import '../backend/live_test.dart'; |
+import '../backend/live_test_controller.dart'; |
+import '../backend/state.dart'; |
+import '../backend/test.dart'; |
+import 'load_suite.dart'; |
+import 'runner_suite.dart'; |
+ |
+/// An [Engine] manages a run that encompasses multiple test suites. |
+/// |
+/// Test suites are provided by passing them into [suiteSink]. Once all suites |
+/// have been provided, the user should close [suiteSink] to indicate this. |
+/// [run] won't terminate until [suiteSink] is closed. Suites will be run in the |
+/// order they're provided to [suiteSink]. Tests within those suites will |
+/// likewise be run in the order of [Suite.tests]. |
+/// |
+/// The current status of every test is visible via [liveTests]. [onTestStarted] |
+/// can also be used to be notified when a test is about to be run. |
+/// |
+/// The engine has some special logic for [LoadSuite]s and the tests they |
+/// contain, referred to as "load tests". Load tests exist to provide visibility |
+/// into the process of loading test files, but as long as that process is |
+/// proceeding normally users usually don't care about it, so the engine only |
+/// surfaces running load tests (that is, includes them in [liveTests] and other |
+/// collections) under specific circumstances. |
+/// |
+/// If only load tests are running, exactly one load test will be in [active] |
+/// and [liveTests]. If this test passes, it will be removed from both [active] |
+/// and [liveTests] and *will not* be added to [passed]. If at any point a load |
+/// test fails, it will be added to [failed] and [liveTests]. |
+/// |
+/// The test suite loaded by a load suite will be automatically be run by the |
+/// engine; it doesn't need to be added to [suiteSink] manually. |
+/// |
+/// Load tests will always be emitted through [onTestStarted] so users can watch |
+/// their event streams once they start running. |
+class Engine { |
+ /// Whether [run] has been called yet. |
+ var _runCalled = false; |
+ |
+ /// 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 _runPool; |
+ |
+ /// A pool that limits the number of test suites loaded concurrently. |
+ /// |
+ /// Once this reaches its limit, loading any additional test suites will cause |
+ /// previous suites to be unloaded in the order they completed. |
+ final Pool _loadPool; |
+ |
+ /// 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 [RunnerSuite]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. |
+ /// |
+ /// Suites added to the sink will be closed by the engine based on its |
+ /// internal logic. |
+ Sink<RunnerSuite> get suiteSink => new DelegatingSink(_suiteController.sink); |
+ final _suiteController = new StreamController<RunnerSuite>(); |
+ |
+ /// 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]. |
+ /// |
+ /// This is guaranteed to contain the same tests as the union of [passed], |
+ /// [skipped], [failed], and [active]. |
+ /// |
+ /// [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. |
+ /// |
+ /// This is guaranteed to fire before [LiveTest.onStateChange] first fires. |
+ Stream<LiveTest> get onTestStarted => _onTestStartedController.stream; |
+ final _onTestStartedController = new StreamController<LiveTest>.broadcast(); |
+ |
+ /// The set of tests that have completed and been marked as passing. |
+ Set<LiveTest> get passed => new UnmodifiableSetView(_passed); |
+ final _passed = new Set<LiveTest>(); |
+ |
+ /// The set of tests that have completed and been marked as skipped. |
+ Set<LiveTest> get skipped => new UnmodifiableSetView(_skipped); |
+ final _skipped = new Set<LiveTest>(); |
+ |
+ /// The set of tests that have completed and been marked as failing or error. |
+ Set<LiveTest> get failed => new UnmodifiableSetView(_failed); |
+ final _failed = new Set<LiveTest>(); |
+ |
+ /// The tests that are still running, in the order they begain running. |
+ List<LiveTest> get active => new UnmodifiableListView(_active); |
+ final _active = new QueueList<LiveTest>(); |
+ |
+ /// The tests from [LoadSuite]s that are still running, in the order they |
+ /// began running. |
+ /// |
+ /// This is separate from [active] because load tests aren't always surfaced. |
+ final _activeLoadTests = new List<LiveTest>(); |
+ |
+ /// Whether this engine is idle—that is, not currently executing a test. |
+ bool get isIdle => _group.isIdle; |
+ |
+ /// A broadcast stream that fires an event whenever [isIdle] switches from |
+ /// `false` to `true`. |
+ Stream get onIdle => _group.onIdle; |
+ |
+ /// Creates an [Engine] that will run all tests provided via [suiteSink]. |
+ /// |
+ /// [concurrency] controls how many suites are run at once, and defaults to 1. |
+ /// [maxSuites] controls how many suites are *loaded* at once, and defaults to |
+ /// four times [concurrency]. |
+ Engine({int concurrency, int maxSuites}) |
+ : _runPool = new Pool(concurrency == null ? 1 : concurrency), |
+ _loadPool = new Pool(maxSuites == null |
+ ? (concurrency == null ? 2 : concurrency * 2) |
+ : maxSuites) { |
+ _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. An engine |
+ /// constructed this way will automatically close its [suiteSink], meaning |
+ /// that no further suites may be provided. |
+ factory Engine.withSuites(List<RunnerSuite> 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 and [suiteSink] has been |
+ /// closed. |
+ Future<bool> run() { |
+ if (_runCalled) { |
+ throw new StateError("Engine.run() may not be called more than once."); |
+ } |
+ _runCalled = true; |
+ |
+ _suiteController.stream.listen((suite) { |
+ _group.add(new Future.sync(() async { |
+ var loadResource = await _loadPool.request(); |
+ |
+ if (suite is LoadSuite) { |
+ suite = await _addLoadSuite(suite); |
+ if (suite == null) { |
+ loadResource.release(); |
+ return; |
+ } |
+ } |
+ |
+ await _runPool.withResource(() async { |
+ if (_closed) return null; |
+ |
+ // TODO(nweiz): Use a real for loop when issue 23394 is fixed. |
+ await 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); |
+ |
+ // If there were no active non-load tests, the current active test |
+ // would have been a load test. In that case, remove it, since now we |
+ // have a non-load test to add. |
+ if (_active.isNotEmpty && _active.first.suite is LoadSuite) { |
+ _liveTests.remove(_active.removeFirst()); |
+ } |
+ |
+ liveTest.onStateChange.listen((state) { |
+ if (state.status != Status.complete) return; |
+ _active.remove(liveTest); |
+ |
+ // If we're out of non-load tests, surface a load test. |
+ if (_active.isEmpty && _activeLoadTests.isNotEmpty) { |
+ _active.add(_activeLoadTests.first); |
+ _liveTests.add(_activeLoadTests.first); |
+ } |
+ |
+ if (state.result != Result.success) { |
+ _passed.remove(liveTest); |
+ _failed.add(liveTest); |
+ } else if (liveTest.test.metadata.skip) { |
+ _skipped.add(liveTest); |
+ } else { |
+ _passed.add(liveTest); |
+ } |
+ }); |
+ |
+ _onTestStartedController.add(liveTest); |
+ |
+ // 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 non-microtask events. |
+ await new Future.microtask(liveTest.run); |
+ await new Future(() {}); |
+ }); |
+ |
+ loadResource.allowRelease(() => suite.close()); |
+ }); |
+ })); |
+ }, onDone: _group.close); |
+ |
+ return success; |
+ } |
+ |
+ /// Returns a dummy [LiveTest] for a test marked as "skip". |
+ LiveTest _skippedTest(RunnerSuite 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; |
+ } |
+ |
+ /// Adds listeners for [suite]. |
+ /// |
+ /// Load suites have specific logic apart from normal test suites. |
+ Future<RunnerSuite> _addLoadSuite(LoadSuite suite) async { |
+ var liveTest = await suite.tests.single.load(suite); |
+ |
+ _activeLoadTests.add(liveTest); |
+ |
+ // Only surface the load test if there are no other tests currently running. |
+ if (_active.isEmpty) { |
+ _liveTests.add(liveTest); |
+ _active.add(liveTest); |
+ } |
+ |
+ liveTest.onStateChange.listen((state) { |
+ if (state.status != Status.complete) return; |
+ _activeLoadTests.remove(liveTest); |
+ |
+ // Only one load test will be active at any given time, and it will always |
+ // be the only active test. Remove it and, if possible, surface another |
+ // load test. |
+ if (_active.isNotEmpty && _active.first.suite == suite) { |
+ _active.remove(liveTest); |
+ _liveTests.remove(liveTest); |
+ |
+ if (_activeLoadTests.isNotEmpty) { |
+ _active.add(_activeLoadTests.last); |
+ _liveTests.add(_activeLoadTests.last); |
+ } |
+ } |
+ |
+ // Surface the load test if it fails so that the user can see the failure. |
+ if (state.result == Result.success) return; |
+ _failed.add(liveTest); |
+ _liveTests.add(liveTest); |
+ }); |
+ |
+ // Run the test immediately. We don't want loading to be blocked on suites |
+ // that are already running. |
+ _onTestStartedController.add(liveTest); |
+ await liveTest.run(); |
+ |
+ return suite.suite; |
+ } |
+ |
+ /// Signals that the caller is done paying attention to test results and the |
+ /// engine should release any resources it has allocated. |
+ /// |
+ /// 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() async { |
+ _closed = true; |
+ if (_closedBeforeDone == null) _closedBeforeDone = true; |
+ _suiteController.close(); |
+ |
+ // Close the running tests first so that we're sure to wait for them to |
+ // finish before we close their suites and cause them to become unloaded. |
+ var allLiveTests = liveTests.toSet()..addAll(_activeLoadTests); |
+ await Future.wait(allLiveTests.map((liveTest) => liveTest.close())); |
+ |
+ var allSuites = allLiveTests.map((liveTest) => liveTest.suite).toSet(); |
+ await Future.wait(allSuites.map((suite) => suite.close())); |
+ } |
+} |