blob: 48dc68b202ea3c15c27d2787ff9895aef8987df8 [file] [log] [blame]
// Copyright (c) 2013, 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.
// TODO(nweiz): Add support for calling [schedule] while the schedule is already
// running.
// TODO(nweiz): Port the non-Pub-specific scheduled test libraries from Pub.
import 'dart:async';
import 'package:stack_trace/stack_trace.dart';
import 'package:test/test.dart' as test_pkg;
import 'src/schedule.dart';
export 'package:test/test.dart'
hide test, group, setUp, tearDown, setUpAll, tearDownAll, StreamMatcher;
export 'src/schedule.dart';
export 'src/schedule_error.dart';
export 'src/task.dart';
typedef void _NonaryFunction();
/// The [Schedule] for the current test. This is used to add new tasks and
/// inspect the state of the schedule.
///
/// This is `null` when there's no test currently running.
Schedule get currentSchedule => _currentSchedule;
Schedule _currentSchedule;
/// The user-provided set-up function for the currently-running test.
///
/// This is set for each test during [test_pkg.setUp].
_NonaryFunction _setUpFn;
/// The user-provided tear-down function for the currently-running test.
///
/// This is set for each test during [test_pkg.setUp].
_NonaryFunction _tearDownFn;
/// The user-provided set-up function for the current test scope.
final _setUpForGroup = new _DeclarerProperty<_NonaryFunction>();
/// The user-provided tear-down function for the current test scope.
final _tearDownForGroup = new _DeclarerProperty<_NonaryFunction>();
/// Whether [_initializeForGroup] has been called in this group scope.
final _initializedForGroup = new _DeclarerProperty<bool>(false);
/// Whether or not the tests currently being defined are in a group.
///
/// This is only true when defining tests, not when executing them.
final _inGroup = new _DeclarerProperty<bool>(false);
/// Creates a new test case with the given description and body.
///
/// This has the same semantics as [test_pkg.test].
void test(String description, body(), {String testOn, test_pkg.Timeout timeout,
skip, tags, Map<String, dynamic> onPlatform}) {
maybeWrapFuture(future) {
if (future != null) test_pkg.expect(future, test_pkg.completes);
}
_initializeForGroup();
test_pkg.test(description, () {
_currentSchedule = new Schedule();
return currentSchedule.run(() {
if (_setUpFn != null) maybeWrapFuture(_setUpFn());
maybeWrapFuture(body());
if (_tearDownFn != null) maybeWrapFuture(_tearDownFn());
}).then((_) {
if (currentSchedule.errors.isEmpty) return;
// Pass an empty trace so that the test package doesn't try to construct
// its own useless stack trace. All the trace information we need is in
// the schedule's error string.
test_pkg.registerException(currentSchedule.errorString(), new Trace([]));
});
},
testOn: testOn,
timeout: timeout,
skip: skip,
tags: tags,
onPlatform: onPlatform);
}
/// Creates a new named group of tests. This has the same semantics as
/// [test_pkg.group].
void group(String description, void body(), {String testOn,
test_pkg.Timeout timeout, skip, tags, Map<String, dynamic> onPlatform}) {
_initializeForGroup();
test_pkg.group(description, () {
var oldSetUp = _setUpForGroup.value;
var oldTearDown = _tearDownForGroup.value;
var wasInitializedForGroup = _initializedForGroup.value;
var wasInGroup = _inGroup.value;
_setUpForGroup.value = null;
_tearDownForGroup.value = null;
_initializedForGroup.value = false;
_inGroup.value = true;
body();
_setUpForGroup.value = oldSetUp;
_tearDownForGroup.value = oldTearDown;
_initializedForGroup.value = wasInitializedForGroup;
_inGroup.value = wasInGroup;
},
testOn: testOn,
timeout: timeout,
skip: skip,
tags: tags,
onPlatform: onPlatform);
}
/// Registers a function to be run once before all tests.
///
/// This has the same semantics as [test_pkg.setUpAll].
void setUpAll(body()) {
maybeWrapFuture(future) {
if (future != null) test_pkg.expect(future, test_pkg.completes);
}
test_pkg.setUpAll(() {
_currentSchedule = new Schedule();
return currentSchedule.run(() {
maybeWrapFuture(body());
}).then((_) {
if (currentSchedule.errors.isEmpty) return;
// Pass an empty trace so that the test package doesn't try to construct
// its own useless stack trace. All the trace information we need is in
// the schedule's error string.
test_pkg.registerException(currentSchedule.errorString(), new Trace([]));
}).whenComplete(() {
_currentSchedule = null;
});
});
}
/// Registers a function to be run once after all tests.
///
/// This has the same semantics as [test_pkg.tearDownAll].
void tearDownAll(body()) {
maybeWrapFuture(future) {
if (future != null) test_pkg.expect(future, test_pkg.completes);
}
test_pkg.tearDownAll(() {
_currentSchedule = new Schedule();
return currentSchedule.run(() {
maybeWrapFuture(body());
}).then((_) {
if (currentSchedule.errors.isEmpty) return;
// Pass an empty trace so that the test package doesn't try to construct
// its own useless stack trace. All the trace information we need is in
// the schedule's error string.
test_pkg.registerException(currentSchedule.errorString(), new Trace([]));
}).whenComplete(() {
_currentSchedule = null;
});
});
}
/// Schedules a task, [fn], to run asynchronously as part of the main task queue
/// of [currentSchedule]. Tasks will be run in the order they're scheduled. If
/// [fn] returns a [Future], tasks after it won't be run until that [Future]
/// completes.
///
/// The return value will be completed once the scheduled task has finished
/// running. Its return value is the same as the return value of [fn], or the
/// value it completes to if it's a [Future].
///
/// If [description] is passed, it's used to describe the task for debugging
/// purposes when an error occurs.
///
/// If this is called when a task queue is currently running, it will run [fn]
/// on the next event loop iteration rather than adding it to a queue. The
/// current task will not complete until [fn] (and any [Future] it returns) has
/// finished running.
Future<T> schedule<T>(T fn(), [String description]) =>
currentSchedule.tasks.schedule(fn, description);
/// Register a [setUp] function for a test [group].
///
/// This has the same semantics as [test_pkg.setUp]. Tasks may be scheduled
/// using [schedule] within [setUpFn], and [currentSchedule] may be accessed as
/// well.
void setUp(setUpFn()) {
_setUpForGroup.value = setUpFn;
}
/// Register a [tearDown] function for a test [group].
///
/// This has the same semantics as [test_pkg.tearDown]. Tasks may be scheduled
/// using [schedule] within [tearDownFn], and [currentSchedule] may be accessed
/// as well. Note that [tearDownFn] will be run synchronously after the test
/// body finishes running, which means it will run before any scheduled tasks
/// have begun.
///
/// To run code after the schedule has finished running, use
/// `currentSchedule.onComplete.schedule`.
void tearDown(tearDownFn()) {
_tearDownForGroup.value = tearDownFn;
}
/// Registers callbacks for [test_pkg.setUp] and [test_pkg.tearDown] that set up
/// and tear down the scheduled test infrastructure and run the user's [setUp]
/// and [tearDown] callbacks.
void _initializeForGroup() {
if (_initializedForGroup.value) return;
_initializedForGroup.value = true;
var setUpFn = _setUpForGroup.value;
var tearDownFn = _tearDownForGroup.value;
if (_inGroup.value) {
test_pkg.setUp(() => _addSetUpTearDown(setUpFn, tearDownFn));
return;
}
test_pkg.setUp(() {
if (currentSchedule != null) {
throw new StateError('There seems to be another scheduled test '
'still running.');
}
_addSetUpTearDown(setUpFn, tearDownFn);
});
test_pkg.tearDown(() {
_currentSchedule = null;
_setUpFn = null;
_tearDownFn = null;
});
}
/// Set [_setUpFn] and [_tearDownFn] appropriately.
void _addSetUpTearDown(void setUpFn(), void tearDownFn()) {
if (setUpFn != null) {
if (_setUpFn != null) {
var parentFn = _setUpFn;
_setUpFn = () { parentFn(); setUpFn(); };
} else {
_setUpFn = setUpFn;
}
}
if (tearDownFn != null) {
if (_tearDownFn != null) {
var parentFn = _tearDownFn;
_tearDownFn = () { parentFn(); tearDownFn(); };
} else {
_tearDownFn = tearDownFn;
}
}
}
/// A property attached to the test runner's current declarer.
///
/// This is used to scope otherwise-global fields so that multiple instances of
/// scheduled test can coexist in the same isolate, albeit not at the same time.
class _DeclarerProperty<T> {
/// The expando used to attach the property to the declarers.
final _expando = new Expando<T>();
/// The default value, if any.
final T _defaultValue;
/// The object to associate with the property.
///
/// This will usually be a declarer, but if there is no declarer this will be
/// an Expando-safe value that's used to fall back to global properties.
Object get _declarer {
var declarer = Zone.current[#test.declarer];
if (declarer == null) return #declarer;
return declarer;
}
// TODO(nweiz): Use the test API to get the declarer when dart-lang/test#48 is
// fixed.
/// Returns the value of the property.
T get value {
var value = _expando[_declarer];
return value == null ? _defaultValue : value;
}
/// Sets the value of the property.
set value(T value) {
_expando[_declarer] = value;
}
/// Creates a new property.
///
/// If [defaultValue] is passed, it's the default for when the property is
/// unset.
_DeclarerProperty([this._defaultValue]);
}