| Index: pkg/scheduled_test/lib/scheduled_test.dart
|
| diff --git a/pkg/scheduled_test/lib/scheduled_test.dart b/pkg/scheduled_test/lib/scheduled_test.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..23a060ff052cfc5764980f6603abda3411a80eaa
|
| --- /dev/null
|
| +++ b/pkg/scheduled_test/lib/scheduled_test.dart
|
| @@ -0,0 +1,292 @@
|
| +// 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): Keep track of and display multiple errors so there's more
|
| +// visibility into cascading errors.
|
| +// TODO(nweiz): Add timeouts to scheduled tests.
|
| +// 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.
|
| +/// A package for writing readable tests of asynchronous behavior.
|
| +///
|
| +/// This package works by building up a queue of asynchronous tasks called a
|
| +/// "schedule", then executing those tasks in order. This allows the tests to
|
| +/// read like synchronous, linear code, despite executing asynchronously.
|
| +///
|
| +/// The `scheduled_test` package is built on top of `unittest`, and should be
|
| +/// imported instead of `unittest`. It provides its own version of [group],
|
| +/// [test], and [setUp], and re-exports most other APIs from unittest.
|
| +///
|
| +/// To schedule a task, call the [schedule] function. For example:
|
| +///
|
| +/// import 'package:scheduled_test/scheduled_test.dart';
|
| +///
|
| +/// void main() {
|
| +/// test('writing to a file and reading it back should work', () {
|
| +/// schedule(() {
|
| +/// // The schedule won't proceed until the returned Future has
|
| +/// // completed.
|
| +/// return new File("output.txt").writeAsString("contents");
|
| +/// });
|
| +///
|
| +/// schedule(() {
|
| +/// return new File("output.txt").readAsString().then((contents) {
|
| +/// // The normal unittest matchers can still be used.
|
| +/// expect(contents, equals("contents"));
|
| +/// });
|
| +/// });
|
| +/// });
|
| +/// }
|
| +///
|
| +/// ## Setting Up and Tearing Down
|
| +///
|
| +/// The `scheduled_test` package defines its own [setUp] method that works just
|
| +/// like the one in `unittest`. Tasks can be scheduled in [setUp]; they'll be
|
| +/// run before the tasks scheduled by tests in that group. [currentSchedule] is
|
| +/// also set in the [setUp] callback.
|
| +///
|
| +/// This package doesn't have an explicit `tearDown` method. Instead, the
|
| +/// [currentSchedule.onComplete] and [currentSchedule.onException] task queues
|
| +/// can have tasks scheduled during [setUp]. For example:
|
| +///
|
| +/// import 'package:scheduled_test/scheduled_test.dart';
|
| +///
|
| +/// void main() {
|
| +/// var tempDir;
|
| +/// setUp(() {
|
| +/// schedule(() {
|
| +/// return createTempDir().then((dir) {
|
| +/// tempDir = dir;
|
| +/// });
|
| +/// });
|
| +///
|
| +/// currentSchedule.onComplete.schedule(() => deleteDir(tempDir));
|
| +/// });
|
| +///
|
| +/// // ...
|
| +/// }
|
| +///
|
| +/// ## Passing Values Between Tasks
|
| +///
|
| +/// It's often useful to use values computed in one task in other tasks that are
|
| +/// scheduled afterwards. There are two ways to do this. The most
|
| +/// straightforward is just to define a local variable and assign to it. For
|
| +/// example:
|
| +///
|
| +/// import 'package:scheduled_test/scheduled_test.dart';
|
| +///
|
| +/// void main() {
|
| +/// test('computeValue returns 12', () {
|
| +/// var value;
|
| +///
|
| +/// schedule(() {
|
| +/// return computeValue().then((computedValue) {
|
| +/// value = computedValue;
|
| +/// });
|
| +/// });
|
| +///
|
| +/// schedule(() => expect(value, equals(12)));
|
| +/// });
|
| +/// }
|
| +///
|
| +/// However, this doesn't scale well, especially when you start factoring out
|
| +/// calls to [schedule] into library methods. For that reason, [schedule]
|
| +/// returns a [Future] that will complete to the same value as the return
|
| +/// value of the task. For example:
|
| +///
|
| +/// import 'package:scheduled_test/scheduled_test.dart';
|
| +///
|
| +/// void main() {
|
| +/// test('computeValue returns 12', () {
|
| +/// var valueFuture = schedule(() => computeValue());
|
| +/// schedule(() {
|
| +/// valueFuture.then((value) => expect(value, equals(12)));
|
| +/// });
|
| +/// });
|
| +/// }
|
| +///
|
| +/// ## Out-of-Band Callbacks
|
| +///
|
| +/// Sometimes your tests will have callbacks that don't fit into the schedule.
|
| +/// It's important that errors in these callbacks are still registered, though,
|
| +/// and that [Schedule.onException] and [Schedule.onComplete] still run after
|
| +/// they finish. When using `unittest`, you wrap these callbacks with
|
| +/// `expectAsyncN`; when using `scheduled_test`, you use [wrapAsync].
|
| +///
|
| +/// [wrapAsync] has two important functions. First, any errors that occur in it
|
| +/// will be passed into the [Schedule] instead of causing the whole test to
|
| +/// crash. They can then be handled by [Schedule.onException] and
|
| +/// [Schedule.onComplete]. Second, a task queue isn't considered finished until
|
| +/// all of its [wrapAsync]-wrapped functions have been called. This ensures that
|
| +/// [Schedule.onException] and [Schedule.onComplete] will always run after all
|
| +/// the test code in the main queue.
|
| +///
|
| +/// Note that the [completes], [completion], and [throws] matchers use
|
| +/// [wrapAsync] internally, so they're safe to use in conjunction with scheduled
|
| +/// tests.
|
| +///
|
| +/// Here's an example of a test using [wrapAsync] to catch errors thrown in the
|
| +/// callback of a fictional `startServer` function:
|
| +///
|
| +/// import 'package:scheduled_test/scheduled_test.dart';
|
| +///
|
| +/// void main() {
|
| +/// test('sendRequest sends a request', () {
|
| +/// startServer(wrapAsync((request) {
|
| +/// expect(request.body, equals('payload'));
|
| +/// request.response.close();
|
| +/// }));
|
| +///
|
| +/// schedule(() => sendRequest('payload'));
|
| +/// });
|
| +/// }
|
| +library scheduled_test;
|
| +
|
| +import 'dart:async';
|
| +
|
| +import 'package:unittest/unittest.dart' as unittest;
|
| +
|
| +import 'src/schedule.dart';
|
| +import 'src/schedule_error.dart';
|
| +import 'src/utils.dart';
|
| +
|
| +export 'package:unittest/matcher.dart';
|
| +export 'package:unittest/unittest.dart' show
|
| + config, configure, Configuration, logMessage, expectThrow, fail;
|
| +
|
| +export 'src/schedule.dart';
|
| +export 'src/schedule_error.dart';
|
| +export 'src/task.dart';
|
| +
|
| +/// 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 setUp function. This is set for each test during
|
| +/// `unittest.setUp`.
|
| +Function _setUpFn;
|
| +
|
| +/// Creates a new test case with the given description and body. This has the
|
| +/// same semantics as [unittest.test].
|
| +void test(String description, void body()) =>
|
| + _test(description, body, unittest.test);
|
| +
|
| +/// Creates a new test case with the given description and body that will be the
|
| +/// only test run in this file. This has the same semantics as
|
| +/// [unittest.solo_test].
|
| +void solo_test(String description, void body()) =>
|
| + _test(description, body, unittest.solo_test);
|
| +
|
| +void _test(String description, void body(), Function testFn) {
|
| + _ensureInitialized();
|
| + _ensureSetUpForTopLevel();
|
| + testFn(description, () {
|
| + var asyncDone = unittest.expectAsync0(() {});
|
| + return currentSchedule.run(() {
|
| + if (_setUpFn != null) _setUpFn();
|
| + body();
|
| + }).then((_) {
|
| + // If we got here, the test completed successfully so tell unittest so.
|
| + asyncDone();
|
| + }).catchError((e) {
|
| + if (e is ScheduleError) {
|
| + unittest.registerException(new ExpectException(e.toString()));
|
| + } else if (e is AsyncError) {
|
| + unittest.registerException(e.error, e.stackTrace);
|
| + } else {
|
| + unittest.registerException(e);
|
| + }
|
| + });
|
| + });
|
| +}
|
| +
|
| +/// Whether or not the tests currently being defined are in a group. This is
|
| +/// only true when defining tests, not when executing them.
|
| +bool _inGroup = false;
|
| +
|
| +/// Creates a new named group of tests. This has the same semantics as
|
| +/// [unittest.group].
|
| +void group(String description, void body()) {
|
| + unittest.group(description, () {
|
| + var wasInGroup = _inGroup;
|
| + _inGroup = true;
|
| + _setUpScheduledTest();
|
| + body();
|
| + _inGroup = wasInGroup;
|
| + });
|
| +}
|
| +
|
| +/// 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.
|
| +///
|
| +/// This function is identical to [currentSchedule.tasks.schedule].
|
| +Future schedule(fn(), [String description]) =>
|
| + currentSchedule.tasks.schedule(fn, description);
|
| +
|
| +/// Register a [setUp] function for a test [group]. This has the same semantics
|
| +/// as [unittest.setUp]. Tasks may be scheduled using [schedule] within
|
| +/// [setUpFn], and [currentSchedule] may be accessed as well.
|
| +///
|
| +/// Note that there is no associated [tearDown] function. Instead, tasks should
|
| +/// be scheduled for [currentSchedule.onComplete] or
|
| +/// [currentSchedule.onException]. These tasks will be run after each test's
|
| +/// schedule is completed.
|
| +void setUp(void setUpFn()) {
|
| + _setUpScheduledTest(setUpFn);
|
| +}
|
| +
|
| +/// Whether [unittest.setUp] has been called in the top level scope.
|
| +bool _setUpForTopLevel = false;
|
| +
|
| +/// If we're in the top-level scope (that is, not in any [group]s) and
|
| +/// [unittest.setUp] hasn't been called yet, call it.
|
| +void _ensureSetUpForTopLevel() {
|
| + if (_inGroup || _setUpForTopLevel) return;
|
| + _setUpScheduledTest();
|
| +}
|
| +
|
| +/// Registers callbacks for [unittest.setUp] and [unittest.tearDown] that set up
|
| +/// and tear down the scheduled test infrastructure.
|
| +void _setUpScheduledTest([void setUpFn()]) {
|
| + if (!_inGroup) _setUpForTopLevel = true;
|
| +
|
| + unittest.setUp(() {
|
| + if (currentSchedule != null) {
|
| + throw new StateError('There seems to be another scheduled test '
|
| + 'still running.');
|
| + }
|
| + _currentSchedule = new Schedule();
|
| + _setUpFn = setUpFn;
|
| + });
|
| +
|
| + unittest.tearDown(() {
|
| + _currentSchedule = null;
|
| + });
|
| +}
|
| +
|
| +/// Ensures that the global configuration for `scheduled_test` has been
|
| +/// initialized.
|
| +void _ensureInitialized() {
|
| + unittest.ensureInitialized();
|
| + unittest.wrapAsync = (f) {
|
| + if (currentSchedule == null) {
|
| + throw new StateError("Unexpected call to wrapAsync with no current "
|
| + "schedule.");
|
| + }
|
| +
|
| + return currentSchedule.wrapAsync(f);
|
| + };
|
| +}
|
|
|