| 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..dd9e6d3a776d09ca31c854bab4d73029717cf046
|
| --- /dev/null
|
| +++ b/pkg/scheduled_test/lib/src/schedule.dart
|
| @@ -0,0 +1,248 @@
|
| +// 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) {
|
| + throw new StateError(
|
| + "An out-of-band error was signaled outside of wrapAsync after the "
|
| + "schedule finished running:"
|
| + "${prefixLines(scheduleError.toString())}");
|
| + } else if (currentQueue == null) {
|
| + // If we're not done but there's no current queue, that means we haven't
|
| + // started yet and thus we're in setUp or the synchronous body of the
|
| + // function. Throwing the error will thus pipe it into the main
|
| + // error-handling code.
|
| + 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.
|
| + Function wrapAsync(fn(arg)) {
|
| + if (_done) {
|
| + throw new StateError("wrapAsync called after the schedule has finished "
|
| + "running.");
|
| + }
|
| +
|
| + _pendingCallbacks++;
|
| + return (arg) {
|
| + try {
|
| + return fn(arg);
|
| + } catch (e, stackTrace) {
|
| + signalError(e, stackTrace);
|
| + } finally {
|
| + _pendingCallbacks--;
|
| + if (_pendingCallbacks == 0 && _noPendingCallbacks != null) {
|
| + _noPendingCallbacks.complete();
|
| + _noPendingCallbacks = null;
|
| + }
|
| + }
|
| + };
|
| + }
|
| +
|
| + /// 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);
|
| + 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");
|
| + }
|
| +}
|
|
|