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); |
+ }; |
+} |