Index: lib/src/backend/declarer.dart |
diff --git a/lib/src/backend/declarer.dart b/lib/src/backend/declarer.dart |
index df2ec06ee47b600e9a1b9a5baad65a326091e43f..a3c201172575148630e887c3a9529e9dcc2e7961 100644 |
--- a/lib/src/backend/declarer.dart |
+++ b/lib/src/backend/declarer.dart |
@@ -4,41 +4,76 @@ |
library test.backend.declarer; |
-import 'dart:collection'; |
+import 'dart:async'; |
import '../frontend/timeout.dart'; |
+import '../utils.dart'; |
import 'group.dart'; |
import 'invoker.dart'; |
import 'metadata.dart'; |
-import 'test.dart'; |
+import 'suite_entry.dart'; |
/// A class that manages the state of tests as they're declared. |
/// |
-/// This is in charge of tracking the current group, set-up, and tear-down |
-/// functions. It produces a list of runnable [tests]. |
+/// A nested tree of Declarers tracks the current group, set-up, and tear-down |
+/// functions. Each Declarer in the tree corresponds to a group. This tree is |
+/// tracked by a zone-scoped "current" Declarer; the current declarer can be set |
+/// for a block using [Declarer.declare], and it can be accessed using |
+/// [Declarer.current]. |
class Declarer { |
- /// The current group. |
- var _group = new Group.root(); |
+ /// The parent declarer, or `null` if this corresponds to the root group. |
+ final Declarer _parent; |
- /// The list of tests that have been defined. |
- List<Test> get tests => new UnmodifiableListView<Test>(_tests); |
- final _tests = new List<Test>(); |
+ /// The name of the current test group, including the name of any parent |
+ /// groups. |
+ /// |
+ /// This is `null` if this is the root group. |
+ final String _name; |
- Declarer(); |
+ /// The metadata for this group, including the metadata of any parent groups |
+ /// and of the test suite. |
+ final Metadata _metadata; |
- /// Defines a test case with the given description and body. |
- void test(String description, body(), {String testOn, Timeout timeout, |
- skip, Map<String, dynamic> onPlatform}) { |
- // TODO(nweiz): Once tests have begun running, throw an error if [test] is |
- // called. |
- var prefix = _group.description; |
- if (prefix != null) description = "$prefix $description"; |
+ /// The set-up functions for this group. |
+ final _setUps = new List<AsyncFunction>(); |
- var metadata = _group.metadata.merge(new Metadata.parse( |
+ /// The tear-down functions for this group. |
+ final _tearDowns = new List<AsyncFunction>(); |
+ |
+ /// The children of this group, either tests or sub-groups. |
+ final _entries = new List<SuiteEntry>(); |
+ |
+ /// Whether [build] has been called for this declarer. |
+ bool _built = false; |
+ |
+ /// The current zone-scoped declarer. |
+ static Declarer get current => Zone.current[#test.declarer]; |
+ |
+ /// Creates a new declarer for the root group. |
+ /// |
+ /// This is the implicit group that exists outside of any calls to `group()`. |
+ /// [metadata] should be the suite's metadata, if available. |
+ Declarer([Metadata metadata]) |
+ : this._(null, null, metadata == null ? new Metadata() : metadata); |
+ |
+ Declarer._(this._parent, this._name, this._metadata); |
+ |
+ /// Runs [body] with this declarer as [Declarer.current]. |
+ /// |
+ /// Returns the return value of [body]. |
+ declare(body()) => runZoned(body, zoneValues: {#test.declarer: this}); |
+ |
+ /// Defines a test case with the given name and body. |
+ void test(String name, body(), {String testOn, Timeout timeout, skip, |
+ Map<String, dynamic> onPlatform}) { |
+ if (_built) { |
+ throw new StateError("Can't call test() once tests have begun running."); |
+ } |
+ |
+ var metadata = _metadata.merge(new Metadata.parse( |
testOn: testOn, timeout: timeout, skip: skip, onPlatform: onPlatform)); |
- var group = _group; |
- _tests.add(new LocalTest(description, metadata, () { |
+ _entries.add(new LocalTest(_prefix(name), metadata, () { |
// TODO(nweiz): It might be useful to throw an error here if a test starts |
// running while other tests from the same declarer are also running, |
// since they might share closurized state. |
@@ -46,40 +81,110 @@ class Declarer { |
// TODO(nweiz): Use async/await here once issue 23497 has been fixed in |
// two stable versions. |
return Invoker.current.waitForOutstandingCallbacks(() { |
- return group.runSetUps().then((_) => body()); |
- }).then((_) => group.runTearDowns()); |
+ return _runSetUps().then((_) => body()); |
+ }).then((_) => _runTearDowns()); |
})); |
} |
/// Creates a group of tests. |
- void group(String description, void body(), {String testOn, |
- Timeout timeout, skip, Map<String, dynamic> onPlatform}) { |
- var oldGroup = _group; |
+ void group(String name, void body(), {String testOn, Timeout timeout, skip, |
+ Map<String, dynamic> onPlatform}) { |
+ if (_built) { |
+ throw new StateError("Can't call group() once tests have begun running."); |
+ } |
- var metadata = new Metadata.parse( |
- testOn: testOn, timeout: timeout, skip: skip, onPlatform: onPlatform); |
+ var metadata = _metadata.merge(new Metadata.parse( |
+ testOn: testOn, timeout: timeout, skip: skip, onPlatform: onPlatform)); |
- // Don' load the tests for a skipped group. |
+ // Don't load the tests for a skipped group. |
if (metadata.skip) { |
- _tests.add(new LocalTest(description, metadata, () {})); |
+ _entries.add(new Group(name, metadata, [])); |
return; |
} |
- _group = new Group(oldGroup, description, metadata); |
- try { |
- body(); |
- } finally { |
- _group = oldGroup; |
- } |
+ var declarer = new Declarer._(this, _prefix(name), metadata); |
+ declarer.declare(body); |
+ _entries.add(new Group(declarer._name, metadata, declarer.build())); |
} |
- /// Registers a function to be run before tests. |
+ /// Returns [name] prefixed with this declarer's group name. |
+ String _prefix(String name) => _name == null ? name : "$_name $name"; |
+ |
+ /// Registers a function to be run before each test in this group. |
void setUp(callback()) { |
- _group.setUps.add(callback); |
+ if (_built) { |
+ throw new StateError("Can't call setUp() once tests have begun running."); |
+ } |
+ |
+ _setUps.add(callback); |
} |
- /// Registers a function to be run after tests. |
+ /// Registers a function to be run after each test in this group. |
void tearDown(callback()) { |
- _group.tearDowns.add(callback); |
+ if (_built) { |
+ throw new StateError( |
+ "Can't call tearDown() once tests have begun running."); |
+ } |
+ |
+ _tearDowns.add(callback); |
+ } |
+ |
+ /// Finalizes and returns the tests and groups being declared. |
+ List<SuiteEntry> build() { |
+ if (_built) { |
+ throw new StateError("Can't call Declarer.build() more than once."); |
+ } |
+ |
+ _built = true; |
+ return _entries.toList(); |
+ } |
+ |
+ /// Run the set-up functions for this and any parent groups. |
+ /// |
+ /// If no set-up functions are declared, this returns a [Future] that |
+ /// completes immediately. |
+ Future _runSetUps() { |
+ // TODO(nweiz): Use async/await here once issue 23497 has been fixed in two |
+ // stable versions. |
+ if (_parent != null) { |
+ return _parent._runSetUps().then((_) { |
+ return Future.forEach(_setUps, (setUp) => setUp()); |
+ }); |
+ } |
+ |
+ return Future.forEach(_setUps, (setUp) => setUp()); |
+ } |
+ |
+ /// Run the tear-up functions for this and any parent groups. |
+ /// |
+ /// If no set-up functions are declared, this returns a [Future] that |
+ /// completes immediately. |
+ /// |
+ /// This should only be called within a test. |
+ Future _runTearDowns() { |
+ return Invoker.current.unclosable(() { |
+ var tearDowns = []; |
+ for (var declarer = this; declarer != null; declarer = declarer._parent) { |
+ tearDowns.addAll(declarer._tearDowns.reversed); |
+ } |
+ |
+ return Future.forEach(tearDowns, _errorsDontStopTest); |
+ }); |
+ } |
+ |
+ /// Runs [body] with special error-handling behavior. |
+ /// |
+ /// Errors emitted [body] will still cause the current test to fail, but they |
+ /// won't cause it to *stop*. In particular, they won't remove any outstanding |
+ /// callbacks registered outside of [body]. |
+ Future _errorsDontStopTest(body()) { |
+ var completer = new Completer(); |
+ |
+ Invoker.current.addOutstandingCallback(); |
+ Invoker.current.waitForOutstandingCallbacks(() { |
+ new Future.sync(body).whenComplete(completer.complete); |
+ }).then((_) => Invoker.current.removeOutstandingCallback()); |
+ |
+ return completer.future; |
} |
} |