Index: lib/src/runner/engine.dart |
diff --git a/lib/src/runner/engine.dart b/lib/src/runner/engine.dart |
index 30053ba14aa0d091dde5ea845891e2b66618e657..208d4f766ea170ff500b95118ab58abaefe4f8a0 100644 |
--- a/lib/src/runner/engine.dart |
+++ b/lib/src/runner/engine.dart |
@@ -42,6 +42,9 @@ import 'load_suite.dart'; |
/// 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 { |
@@ -59,7 +62,13 @@ class Engine { |
var _closedBeforeDone; |
/// A pool that limits the number of test suites running concurrently. |
- final Pool _pool; |
+ 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. |
/// |
@@ -80,6 +89,9 @@ class Engine { |
/// |
/// 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<Suite> get suiteSink => new DelegatingSink(_suiteController.sink); |
final _suiteController = new StreamController<Suite>(); |
@@ -126,9 +138,14 @@ class Engine { |
/// Creates an [Engine] that will run all tests provided via [suiteSink]. |
/// |
- /// [concurrency] controls how many suites are run at once. |
- Engine({int concurrency}) |
- : _pool = new Pool(concurrency == null ? 1 : concurrency) { |
+ /// [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 ? 4 : concurrency * 4) |
+ : maxSuites) { |
_group.future.then((_) { |
if (_closedBeforeDone == null) _closedBeforeDone = false; |
}).catchError((_) { |
@@ -160,59 +177,68 @@ class Engine { |
_runCalled = true; |
_suiteController.stream.listen((suite) { |
- if (suite is LoadSuite) { |
- _group.add(_addLoadSuite(suite)); |
- return; |
- } |
- |
- _group.add(_pool.withResource(() { |
- if (_closed) return null; |
- |
- // TODO(nweiz): Use a real for loop when issue 23394 is fixed. |
- return Future.forEach(suite.tests, (test) async { |
- if (_closed) return; |
+ _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; |
+ } |
+ } |
- var liveTest = test.metadata.skip |
- ? _skippedTest(suite, test) |
- : test.load(suite); |
- _liveTests.add(liveTest); |
- _active.add(liveTest); |
+ await _runPool.withResource(() async { |
+ if (_closed) return null; |
- // 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()); |
- } |
+ // TODO(nweiz): Use a real for loop when issue 23394 is fixed. |
+ await Future.forEach(suite.tests, (test) async { |
+ if (_closed) return; |
- liveTest.onStateChange.listen((state) { |
- if (state.status != Status.complete) return; |
- _active.remove(liveTest); |
+ var liveTest = test.metadata.skip |
+ ? _skippedTest(suite, test) |
+ : test.load(suite); |
+ _liveTests.add(liveTest); |
+ _active.add(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 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()); |
} |
- if (state.result != Result.success) { |
- _passed.remove(liveTest); |
- _failed.add(liveTest); |
- } else if (liveTest.test.metadata.skip) { |
- _skipped.add(liveTest); |
- } else { |
- _passed.add(liveTest); |
- } |
+ 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(() {}); |
}); |
- _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); |
@@ -234,7 +260,7 @@ class Engine { |
/// Adds listeners for [suite]. |
/// |
/// Load suites have specific logic apart from normal test suites. |
- Future _addLoadSuite(LoadSuite suite) async { |
+ Future<Suite> _addLoadSuite(LoadSuite suite) async { |
var liveTest = await suite.tests.single.load(suite); |
_activeLoadTests.add(liveTest); |
@@ -272,6 +298,8 @@ class Engine { |
// 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 |
@@ -284,12 +312,17 @@ class Engine { |
/// **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() { |
+ 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); |
- return Future.wait(allLiveTests.map((liveTest) => liveTest.close())); |
+ await Future.wait(allLiveTests.map((liveTest) => liveTest.close())); |
+ |
+ var allSuites = allLiveTests.map((liveTest) => liveTest.suite).toSet(); |
+ await Future.wait(allSuites.map((suite) => suite.close())); |
} |
} |