Chromium Code Reviews| Index: pkg/scheduled_test/lib/src/schedule.dart |
| diff --git a/pkg/scheduled_test/lib/src/schedule.dart b/pkg/scheduled_test/lib/src/schedule.dart |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..5f5d42749aa2757ebddc1c4b0ecec657566d2d08 |
| --- /dev/null |
| +++ b/pkg/scheduled_test/lib/src/schedule.dart |
| @@ -0,0 +1,235 @@ |
| +// 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. |
| + |
| +library schedule; |
| + |
| +import 'dart:async'; |
| +import 'dart:collection'; |
| + |
| +import 'package:unittest/unittest.dart' as unittest; |
| + |
| +import 'schedule_error.dart'; |
| +import 'task.dart'; |
| + |
| +/// The schedule of tasks to run for a single test. This has three separate task |
| +/// queues: [tasks], [onComplete], and [onException]. It also provides |
| +/// visibility into the current state of the schedule. |
| +class Schedule { |
| + /// The main task queue for the schedule. These tasks are run before the other |
| + /// queues and generally constitute the main test body. |
| + TaskQueue get tasks => _tasks; |
| + TaskQueue _tasks; |
| + |
| + /// The queue of tasks to run if an error is caught while running [tasks]. The |
| + /// error will be available via [error]. These tasks won't be run if no error |
| + /// occurs. Note that expectation failures count as errors. |
| + /// |
| + /// This queue runs before [onComplete], and errors in [onComplete] will not |
| + /// cause this queue to be run. |
| + /// |
| + /// If an error occurs in a task in this queue, all further tasks will be |
| + /// skipped. |
| + TaskQueue get onException => _onException; |
| + TaskQueue _onException; |
| + |
| + /// The queue of tasks to run after [tasks] and possibly [onException] have |
| + /// run. This queue will run whether or not an error occurred. If one did, it |
| + /// will be available via [error]. Note that expectation failures count as |
| + /// errors. |
| + /// |
| + /// This queue runs after [onException]. If an error occurs while running |
| + /// [onException], that error will be available via [error] in place of the |
| + /// original error. |
| + /// |
| + /// If an error occurs in a task in this queue, all further tasks will be |
| + /// skipped. |
| + TaskQueue get onComplete => _onComplete; |
| + TaskQueue _onComplete; |
| + |
| + /// Returns the [Task] that's currently executing, or `null` if there is no |
| + /// such task. This will be `null` both before the schedule starts running and |
| + /// after it's finished. |
| + Task get currentTask => _currentTask; |
| + Task _currentTask; |
| + |
| + /// Whether the schedule has finished running. This is only set once |
| + /// [onComplete] has finished running. It will be set whether or not an |
| + /// exception has occurred. |
| + bool get done => _done; |
| + bool _done = false; |
| + |
| + /// The error thrown by the task queue. This will only be set while running |
| + /// [onException] and [onComplete], since an error in [tasks] will cause it to |
| + /// terminate immediately. |
| + ScheduleError get error => _error; |
| + ScheduleError _error; |
| + |
| + /// The task queue that's currently being run, or `null` if there is no such |
| + /// queue. One of [tasks], [onException], or [onComplete]. This will be `null` |
| + /// before the schedule starts running. |
| + TaskQueue get currentQueue => _done ? null : _currentQueue; |
| + TaskQueue _currentQueue; |
| + |
| + /// The number of out-of-band callbacks that have been registered with |
| + /// [wrapAsync] but have yet to be called. |
| + int _pendingCallbacks = 0; |
| + |
| + /// A completer that will be completed once [_pendingCallbacks] reaches zero. |
| + /// This will only be non-`null` if [_awaitPendingCallbacks] has been called |
| + /// while [_pendingCallbacks] is non-zero. |
| + Completer _noPendingCallbacks; |
| + |
| + /// Creates a new schedule with empty task queues. |
| + Schedule() { |
| + _tasks = new TaskQueue._("tasks", this); |
| + _onComplete = new TaskQueue._("onComplete", this); |
| + _onException = new TaskQueue._("onException", this); |
| + } |
| + |
| + /// Sets up this schedule by running [setUp], then runs all the task queues in |
| + /// order. Any errors in [setUp] will cause [onException] to run. |
| + Future run(void setUp()) { |
| + return new Future.immediate(null).then((_) { |
| + try { |
| + setUp(); |
| + } catch (e, stackTrace) { |
| + throw new ScheduleError.from(this, e, stackTrace: stackTrace); |
| + } |
| + |
| + return tasks._run(); |
| + }).catchError((e) { |
| + _error = e; |
| + return onException._run().then((_) { |
| + throw e; |
| + }); |
| + }).whenComplete(() => onComplete._run()).whenComplete(() { |
| + _done = true; |
| + }); |
| + } |
| + |
| + /// Signals that an out-of-band error has occurred. Using [wrapAsync] along |
| + /// with `throw` is usually preferable to calling this directly. |
| + /// |
| + /// The metadata in [AsyncError]s and [ScheduleError]s will be preserved. |
| + void signalError(error, [stackTrace]) { |
| + var scheduleError = new ScheduleError.from(this, error, |
| + stackTrace: stackTrace, task: currentTask); |
| + if (_done) { |
| + unittest.registerException(new ExpectException(scheduleError.toString())); |
|
Bob Nystrom
2013/02/08 16:15:37
Are we ever supposed to get here? I think this may
nweiz
2013/02/08 22:14:38
Yeah, a StateError is probably better here. I real
|
| + } else if (currentQueue == null) { |
| + throw scheduleError; |
| + } else { |
| + _currentQueue._signalError(scheduleError); |
| + } |
| + } |
| + |
| + /// Returns a function wrapping [fn] that pipes any errors into the schedule |
| + /// chain. This will also block the current task queue from completing until |
| + /// the returned function has been called. It's used to ensure that |
| + /// out-of-band callbacks are properly handled by the scheduled test. |
| + /// |
| + /// The top-level `wrapAsync` function should usually be used in preference to |
| + /// this. |
|
Bob Nystrom
2013/02/08 16:15:37
Make this one private then?
nweiz
2013/02/08 22:14:38
"Usually", not necessarily "always". Also, schedul
|
| + Function wrapAsync(fn(arg)) { |
|
Bob Nystrom
2013/02/08 16:15:37
Should check that _noPendingCallbacks isn't alread
nweiz
2013/02/08 22:14:38
It's possible to wait for pending callbacks multip
|
| + _pendingCallbacks++; |
| + return (arg) { |
| + try { |
| + return fn(arg); |
| + } catch (e, stackTrace) { |
| + signalError(e, stackTrace); |
| + } finally { |
| + _pendingCallbacks--; |
| + if (_pendingCallbacks == 0 && _noPendingCallbacks != null) { |
| + _noPendingCallbacks.complete(); |
| + } |
| + } |
| + }; |
| + } |
| + |
| + /// Returns a [Future] that will complete once there are no pending |
| + /// out-of-band callbacks. |
| + Future _awaitNoPendingCallbacks() { |
| + if (_pendingCallbacks == 0) return new Future.immediate(null); |
|
Bob Nystrom
2013/02/08 16:15:37
What if callbacks are added after this is called?
nweiz
2013/02/08 22:14:38
Then they're not pending :p.
In seriousness, this
|
| + if (_noPendingCallbacks == null) _noPendingCallbacks = new Completer(); |
| + return _noPendingCallbacks.future; |
| + } |
| +} |
| + |
| +/// A queue of asynchronous tasks to execute in order. |
| +class TaskQueue { |
| + // TODO(nweiz): make this a read-only view when issue 8321 is fixed. |
| + /// The tasks in the queue. |
| + Collection<Task> get contents => _contents; |
| + final _contents = new Queue<Task>(); |
| + |
| + /// The name of the queue, for debugging purposes. |
| + final String name; |
| + |
| + /// The [Schedule] that created this queue. |
| + final Schedule _schedule; |
| + |
| + /// An out-of-band error signaled by [_schedule]. If this is non-null, it |
| + /// indicates that the queue should stop as soon as possible and re-throw this |
| + /// error. |
| + ScheduleError _error; |
| + |
| + TaskQueue._(this.name, this._schedule); |
| + |
| + /// Schedules a task, [fn], to run asynchronously as part of this queue. Tasks |
| + /// will be run in the order they're scheduled. In [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. |
| + Future schedule(fn(), [String description]) { |
| + var task = new Task(fn, this, description); |
| + _contents.add(task); |
| + return task.result; |
| + } |
| + |
| + /// Runs all the tasks in this queue in order. |
| + Future _run() { |
| + _schedule._currentQueue = this; |
| + return Future.forEach(_contents, (task) { |
| + _schedule._currentTask = task; |
| + if (_error != null) throw _error; |
| + return task.fn().catchError((e) { |
| + throw new ScheduleError.from(_schedule, e, task: task); |
| + }); |
| + }).whenComplete(() { |
| + _schedule._currentTask = null; |
| + return _schedule._awaitNoPendingCallbacks(); |
| + }).then((_) { |
| + if (_error != null) throw _error; |
| + }); |
| + } |
| + |
| + /// Signals that an out-of-band error has been detected and the queue should |
| + /// stop running as soon as possible. |
| + void _signalError(ScheduleError error) { |
| + _error = error; |
| + } |
| + |
| + String toString() => name; |
| + |
| + /// Returns a detailed representation of the queue as a tree of tasks. If |
| + /// [highlight] is passed, that task is specially highlighted. |
| + /// |
| + /// [highlight] must be a task in this queue. |
| + String generateTree([Task highlight]) { |
| + assert(highlight == null || highlight.queue == this); |
| + return _contents.map((task) { |
| + var lines = task.toString().split("\n"); |
| + var firstLine = task == highlight ? |
| + "> ${lines.first}" : "* ${lines.first}"; |
| + lines = new List.from(lines.skip(1).map((line) => "| $line")); |
| + lines.insertRange(0, 1, firstLine); |
| + return lines.join("\n"); |
| + }).join("\n"); |
| + } |
| +} |