blob: 46a52d9e387da63ddbc9c6049445db6cc4239805 [file] [log] [blame]
// 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.backend.declarer;
import 'dart:async';
import '../frontend/timeout.dart';
import '../utils.dart';
import 'group.dart';
import 'group_entry.dart';
import 'invoker.dart';
import 'metadata.dart';
import 'test.dart';
/// A class that manages the state of tests as they're declared.
///
/// 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 parent declarer, or `null` if this corresponds to the root group.
final Declarer _parent;
/// 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;
/// The metadata for this group, including the metadata of any parent groups
/// and of the test suite.
final Metadata _metadata;
/// The set-up functions to run for each test in this group.
final _setUps = new List<AsyncFunction>();
/// The tear-down functions to run for each test in this group.
final _tearDowns = new List<AsyncFunction>();
/// The set-up functions to run once for this group.
final _setUpAlls = new List<AsyncFunction>();
/// The tear-down functions to run once for this group.
final _tearDownAlls = new List<AsyncFunction>();
/// The children of this group, either tests or sub-groups.
final _entries = new List<GroupEntry>();
/// 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()`.
/// If [metadata] is passed, it's used as the metadata for the implicit root
/// group.
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}) {
_checkNotBuilt("test");
var metadata = _metadata.merge(new Metadata.parse(
testOn: testOn, timeout: timeout, skip: skip, onPlatform: onPlatform));
_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.
// TODO(nweiz): Use async/await here once issue 23497 has been fixed in
// two stable versions.
return Invoker.current.waitForOutstandingCallbacks(() {
return _runSetUps().then((_) => body());
}).then((_) => _runTearDowns());
}));
}
/// Creates a group of tests.
void group(String name, void body(), {String testOn, Timeout timeout, skip,
Map<String, dynamic> onPlatform}) {
_checkNotBuilt("group");
var metadata = _metadata.merge(new Metadata.parse(
testOn: testOn, timeout: timeout, skip: skip, onPlatform: onPlatform));
// Don't load the tests for a skipped group.
if (metadata.skip) {
_entries.add(new Group(name, [], metadata: metadata));
return;
}
var declarer = new Declarer._(this, _prefix(name), metadata);
declarer.declare(body);
_entries.add(declarer.build());
}
/// 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()) {
_checkNotBuilt("setUp");
_setUps.add(callback);
}
/// Registers a function to be run after each test in this group.
void tearDown(callback()) {
_checkNotBuilt("tearDown");
_tearDowns.add(callback);
}
/// Registers a function to be run once before all tests.
void setUpAll(callback()) {
_checkNotBuilt("setUpAll");
_setUpAlls.add(callback);
}
/// Registers a function to be run once after all tests.
void tearDownAll(callback()) {
_checkNotBuilt("tearDownAll");
_tearDownAlls.add(callback);
}
/// Finalizes and returns the group being declared.
Group build() {
_checkNotBuilt("build");
_built = true;
return new Group(_name, _entries.toList(),
metadata: _metadata,
setUpAll: _setUpAll,
tearDownAll: _tearDownAll);
}
/// Throws a [StateError] if [build] has been called.
///
/// [name] should be the name of the method being called.
void _checkNotBuilt(String name) {
if (!_built) return;
throw new StateError("Can't call $name() once tests have begun running.");
}
/// 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);
});
}
/// Returns a [Test] that runs the callbacks in [_setUpAll].
Test get _setUpAll {
if (_setUpAlls.isEmpty) return null;
return new LocalTest(_prefix("(setUpAll)"), _metadata, () {
return Future.forEach(_setUpAlls, (setUp) => setUp());
});
}
/// Returns a [Test] that runs the callbacks in [_tearDownAll].
Test get _tearDownAll {
if (_tearDownAlls.isEmpty) return null;
return new LocalTest(_prefix("(tearDownAll)"), _metadata, () {
return Invoker.current.unclosable(() {
return Future.forEach(_tearDownAlls.reversed, _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;
}
}