Chromium Code Reviews| Index: lib/src/backend/declarer.dart |
| diff --git a/lib/src/backend/declarer.dart b/lib/src/backend/declarer.dart |
| index df2ec06ee47b600e9a1b9a5baad65a326091e43f..ba536f828787483f089a061e99237579bdfb4180 100644 |
| --- a/lib/src/backend/declarer.dart |
| +++ b/lib/src/backend/declarer.dart |
| @@ -4,41 +4,75 @@ |
| 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. |
|
kevmoo
2015/10/02 18:27:21
long line
nweiz
2015/10/02 20:56:54
Done.
|
| + /// |
| + /// 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 +80,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; |
| } |
| } |