| // 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/group.dart'; |
| import '../backend/invoker.dart'; |
| import '../backend/live_test.dart'; |
| import '../backend/live_test_controller.dart'; |
| import '../backend/state.dart'; |
| import '../backend/group_entry.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 they're declared. |
| /// |
| /// 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; |
| await _runGroup(suite, suite.group); |
| loadResource.allowRelease(() => suite.close()); |
| }); |
| })); |
| }, onDone: _group.close); |
| |
| return success; |
| } |
| |
| /// Runs all the entries in [entries] in sequence. |
| Future _runGroup(RunnerSuite suite, Group group) async { |
| if (group.metadata.skip) { |
| await _runLiveTest(_skippedTest(suite, group)); |
| return; |
| } |
| |
| var setUpAllSucceeded = true; |
| if (group.setUpAll != null) { |
| var liveTest = group.setUpAll.load(suite); |
| await _runLiveTest(liveTest, countSuccess: false); |
| setUpAllSucceeded = liveTest.state.result == Result.success; |
| } |
| |
| if (!_closed && setUpAllSucceeded) { |
| for (var entry in group.entries) { |
| if (_closed) return; |
| |
| if (entry is Group) { |
| await _runGroup(suite, entry); |
| } else if (entry.metadata.skip) { |
| await _runLiveTest(_skippedTest(suite, entry)); |
| } else { |
| var test = entry as Test; |
| await _runLiveTest(test.load(suite)); |
| } |
| } |
| } |
| |
| // Even if we're closed or setUpAll failed, we want to run all the teardowns |
| // to ensure that any state is properly cleaned up. |
| if (group.tearDownAll != null) { |
| var liveTest = group.tearDownAll.load(suite); |
| await _runLiveTest(liveTest, countSuccess: false); |
| if (_closed) await liveTest.close(); |
| } |
| } |
| |
| /// Returns a dummy [LiveTest] for a test or group marked as "skip". |
| LiveTest _skippedTest(RunnerSuite suite, GroupEntry entry) { |
| // The netry name will be `null` for the root group. |
| var test = new LocalTest(entry.name ?? "(suite)", entry.metadata, () {}); |
| |
| 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; |
| } |
| |
| /// Runs [liveTest]. |
| /// |
| /// If [countSuccess] is `true` (the default), the test is put into [passed] |
| /// if it succeeds. Otherwise, it's removed from [liveTests] entirely. |
| Future _runLiveTest(LiveTest liveTest, {bool countSuccess: true}) async { |
| _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 if (countSuccess) { |
| _passed.add(liveTest); |
| } else { |
| _liveTests.remove(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(() {}); |
| } |
| |
| /// Adds listeners for [suite]. |
| /// |
| /// Load suites have specific logic apart from normal test suites. |
| Future<RunnerSuite> _addLoadSuite(LoadSuite suite) async { |
| var liveTest = await suite.test.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); |
| var futures = allLiveTests.map((liveTest) => liveTest.close()).toList(); |
| |
| // Closing the load pool will close the test suites as soon as their tests |
| // are done. For browser suites this is effectively immediate since their |
| // tests shut down as soon as they're closed, but for VM suites we may need |
| // to wait for tearDowns or tearDownAlls to run. |
| futures.add(_loadPool.close()); |
| await Future.wait(futures, eagerError: true); |
| } |
| } |