Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(492)

Side by Side Diff: pkg/scheduled_test/lib/src/schedule.dart

Issue 812253002: Delete a bunch of packages that are now on GitHub. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Un-delete http Created 6 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file.
4
5 library schedule;
6
7 import 'dart:async';
8 import 'dart:collection';
9
10 import 'package:stack_trace/stack_trace.dart';
11
12 import 'mock_clock.dart' as mock_clock;
13 import 'schedule_error.dart';
14 import 'substitute_future.dart';
15 import 'task.dart';
16 import 'utils.dart';
17
18 /// The schedule of tasks to run for a single test. This has three separate task
19 /// queues: [tasks], [onComplete], and [onException]. It also provides
20 /// visibility into the current state of the schedule.
21 class Schedule {
22 /// The main task queue for the schedule. These tasks are run before the other
23 /// queues and generally constitute the main test body.
24 TaskQueue get tasks => _tasks;
25 TaskQueue _tasks;
26
27 /// The queue of tasks to run if an error is caught while running [tasks]. The
28 /// error will be available in [errors]. These tasks won't be run if no error
29 /// occurs. Note that expectation failures count as errors.
30 ///
31 /// This queue runs before [onComplete], and errors in [onComplete] will not
32 /// cause this queue to be run.
33 ///
34 /// If an error occurs in a task in this queue, all further tasks will be
35 /// skipped.
36 TaskQueue get onException => _onException;
37 TaskQueue _onException;
38
39 /// The queue of tasks to run after [tasks] and possibly [onException] have
40 /// run. This queue will run whether or not an error occurred. If one did, it
41 /// will be available in [errors]. Note that expectation failures count as
42 /// errors.
43 ///
44 /// This queue runs after [onException]. If an error occurs while running
45 /// [onException], that error will be available in [errors] after the original
46 /// error.
47 ///
48 /// If an error occurs in a task in this queue, all further tasks will be
49 /// skipped.
50 TaskQueue get onComplete => _onComplete;
51 TaskQueue _onComplete;
52
53 /// Returns the [Task] that's currently executing, or `null` if there is no
54 /// such task. This will be `null` both before the schedule starts running and
55 /// after it's finished.
56 Task get currentTask => _currentTask;
57 Task _currentTask;
58
59 /// The current state of the schedule.
60 ScheduleState get state => _state;
61 ScheduleState _state = ScheduleState.SET_UP;
62
63 /// Errors thrown by the task queues.
64 ///
65 /// When running tasks in [tasks], this will always be empty. If an error
66 /// occurs in [tasks], it will be added to this list and then [onException]
67 /// will be run. If an error occurs there as well, it will be added to this
68 /// list and [onComplete] will be run. Errors thrown during [onComplete] will
69 /// also be added to this list, although no scheduled tasks will be run
70 /// afterwards.
71 ///
72 /// Any out-of-band callbacks that throw errors will also have those errors
73 /// added to this list.
74 List<ScheduleError> get errors =>
75 new UnmodifiableListView<ScheduleError>(_errors);
76 final _errors = <ScheduleError>[];
77
78 /// Additional debugging info registered via [addDebugInfo].
79 List<String> get debugInfo => new UnmodifiableListView<String>(_debugInfo);
80 final _debugInfo = <String>[];
81
82 /// The task queue that's currently being run. One of [tasks], [onException],
83 /// or [onComplete]. This starts as [tasks], and can only be `null` after the
84 /// schedule is done.
85 TaskQueue get currentQueue =>
86 _state == ScheduleState.DONE ? null : _currentQueue;
87 TaskQueue _currentQueue;
88
89 /// The time to wait before terminating a task queue for inactivity. Defaults
90 /// to 5 seconds. This can be set to `null` to disable timeouts entirely. Note
91 /// that the timeout is the maximum time a task is allowed between
92 /// interactions with the schedule, *not* the maximum time an entire test is
93 /// allowed. See also [heartbeat].
94 ///
95 /// If a task queue times out, an error will be raised that can be handled as
96 /// usual in the [onException] and [onComplete] queues. If [onException] times
97 /// out, that can only be handled in [onComplete]; if [onComplete] times out,
98 /// that cannot be handled.
99 ///
100 /// If a task times out and then later completes with an error, that error
101 /// cannot be handled. The user will still be notified of it.
102 Duration get timeout => _timeout;
103 Duration _timeout = new Duration(seconds: 5);
104 set timeout(Duration duration) {
105 _timeout = duration;
106 heartbeat();
107 }
108
109 /// The timer for keeping track of task timeouts. This may be null.
110 Timer _timeoutTimer;
111
112 /// Creates a new schedule with empty task queues.
113 Schedule() {
114 _tasks = new TaskQueue._("tasks", this);
115 _onComplete = new TaskQueue._("onComplete", this);
116 _onException = new TaskQueue._("onException", this);
117 _currentQueue = _tasks;
118
119 heartbeat();
120 }
121
122 /// Sets up this schedule by running [setUp], then runs all the task queues in
123 /// order. Any errors in [setUp] will cause [onException] to run.
124 Future run(void setUp()) {
125 return new Future.value().then((_) {
126 try {
127 setUp();
128 } catch (e, stackTrace) {
129 // Even though the scheduling failed, we need to run the onException and
130 // onComplete queues, so we set the schedule state to RUNNING.
131 _state = ScheduleState.RUNNING;
132 throw new ScheduleError.from(this, e, stackTrace: stackTrace);
133 }
134
135 _state = ScheduleState.RUNNING;
136 return tasks._run();
137 }).catchError((error, stackTrace) {
138 _addError(error, stackTrace);
139 return onException._run().catchError((innerError, innerTrace) {
140 // If an error occurs in a task in the onException queue, make sure it's
141 // registered in the error list and re-throw it. We could also re-throw
142 // `error`; ultimately, all the errors will be shown to the user if any
143 // ScheduleError is thrown.
144 _addError(innerError, innerTrace);
145 throw innerError;
146 }).then((_) {
147 // If there are no errors in the onException queue, re-throw the
148 // original error that caused it to run.
149 throw error;
150 });
151 }).whenComplete(() {
152 return onComplete._run().catchError((error, stackTrace) {
153 // If an error occurs in a task in the onComplete queue, make sure it's
154 // registered in the error list and re-throw it.
155 _addError(error, stackTrace);
156 throw error;
157 });
158 }).whenComplete(() {
159 if (_timeoutTimer != null) _timeoutTimer.cancel();
160 _state = ScheduleState.DONE;
161 });
162 }
163
164 /// Stop the current [TaskQueue] after the current task and any out-of-band
165 /// tasks stop executing. If this is called before [this] has started running,
166 /// no tasks in the [tasks] queue will be run.
167 ///
168 /// This won't cause an error, but any errors that are otherwise signaled will
169 /// still cause the test to fail.
170 void abort() {
171 if (_state == ScheduleState.DONE) {
172 throw new StateError("Called abort() after the schedule has finished "
173 "running.");
174 }
175
176 currentQueue._abort();
177 }
178
179 /// Signals that an out-of-band error has occurred. Using [wrapAsync] along
180 /// with `throw` is usually preferable to calling this directly.
181 ///
182 /// The metadata in [ScheduleError]s will be preserved.
183 void signalError(error, [stackTrace]) {
184 heartbeat();
185
186 var scheduleError = new ScheduleError.from(this, error,
187 stackTrace: stackTrace);
188 if (_state == ScheduleState.DONE) {
189 throw new StateError(
190 "An out-of-band error was signaled outside of wrapAsync after the "
191 "schedule finished running.\n"
192 "${errorString()}");
193 } else if (state == ScheduleState.SET_UP) {
194 // If we're setting up, throwing the error will pipe it into the main
195 // error-handling code.
196 throw scheduleError;
197 } else {
198 _currentQueue._signalError(scheduleError);
199 }
200 }
201
202 /// Adds [info] to the debugging output that will be printed if the test
203 /// fails. Unlike [signalError], this won't cause the test to fail, nor will
204 /// it short-circuit the current [TaskQueue]; it's just useful for providing
205 /// additional information that may not fit cleanly into an existing error.
206 void addDebugInfo(String info) => _debugInfo.add(info);
207
208 /// Notifies the schedule of an error that occurred in a task or out-of-band
209 /// callback after the appropriate queue has timed out. If this schedule is
210 /// still running, the error will be added to the errors list to be shown
211 /// along with the timeout error; otherwise, a top-level error will be thrown.
212 void _signalPostTimeoutError(error, [stackTrace]) {
213 var scheduleError = new ScheduleError.from(this, error,
214 stackTrace: stackTrace);
215 _addError(scheduleError);
216 if (_state == ScheduleState.DONE) {
217 throw new StateError(
218 "An out-of-band error was caught after the test timed out.\n"
219 "${errorString()}");
220 }
221 }
222
223 /// Returns a function wrapping [fn] that pipes any errors into the schedule
224 /// chain. This will also block the current task queue from completing until
225 /// the returned function has been called. It's used to ensure that
226 /// out-of-band callbacks are properly handled by the scheduled test.
227 ///
228 /// [description] provides an optional description of the callback, which is
229 /// used when generating error messages.
230 ///
231 /// The top-level `wrapAsync` function should usually be used in preference to
232 /// this in test code.
233 Function wrapAsync(fn(arg), [String description]) {
234 if (_state == ScheduleState.DONE) {
235 throw new StateError("wrapAsync called after the schedule has finished "
236 "running.");
237 }
238 heartbeat();
239
240 return currentQueue._wrapAsync(fn, description);
241 }
242
243 /// Like [wrapAsync], this ensures that the current task queue waits for
244 /// out-of-band asynchronous code, and that errors raised in that code are
245 /// handled correctly. However, [wrapFuture] wraps a [Future] chain rather
246 /// than a single callback.
247 ///
248 /// The returned [Future] completes to the same value or error as [future].
249 ///
250 /// [description] provides an optional description of the future, which is
251 /// used when generating error messages.
252 ///
253 /// The top-level `wrapFuture` function should usually be used in preference
254 /// to this in test code.
255 Future wrapFuture(Future future, [String description]) {
256 var done = wrapAsync((fn) => fn(), description);
257
258 future = future.then((result) => done(() => result))
259 .catchError((error, stackTrace) {
260 done(() {
261 throw new ScheduleError.from(this, error, stackTrace: stackTrace);
262 });
263 // wrapAsync will catch the first throw, so we throw [e] again so it
264 // propagates through the Future chain.
265 throw error;
266 });
267
268 // Don't top-level the error, since it's already been signaled to the
269 // schedule.
270 future.catchError((_) => null);
271
272 return future;
273 }
274
275 /// Returns a string representation of all errors registered on this schedule.
276 String errorString() {
277 if (errors.isEmpty) return "The schedule had no errors.";
278 if (errors.length == 1 && debugInfo.isEmpty) return errors.first.toString();
279
280 var border = "\n==========================================================="
281 "=====================\n";
282 var errorStrings = errors.map((e) => e.toString()).join(border);
283 var message = "The schedule had ${errors.length} errors:\n$errorStrings";
284
285 if (!debugInfo.isEmpty) {
286 message = "$message$border\nDebug info:\n${debugInfo.join(border)}";
287 }
288
289 return message;
290 }
291
292 /// Notifies the schedule that progress is being made on an asynchronous task.
293 /// This resets the timeout timer, and can be used in long-running tasks to
294 /// keep them from timing out.
295 void heartbeat() {
296 if (_timeoutTimer != null) _timeoutTimer.cancel();
297 if (_timeout == null) {
298 _timeoutTimer = null;
299 } else {
300 _timeoutTimer = mock_clock.newTimer(_timeout, () {
301 _timeoutTimer = null;
302 currentQueue._signalTimeout(new ScheduleError.from(this, "The schedule "
303 "timed out after $_timeout of inactivity."));
304 });
305 }
306 }
307
308 /// Register an error in the schedule's error list. This ensures that there
309 /// are no duplicate errors, and that all errors are wrapped in
310 /// [ScheduleError].
311 void _addError(error, [StackTrace stackTrace]) {
312 error = new ScheduleError.from(this, error, stackTrace: stackTrace);
313 if (errors.contains(error)) return;
314 _errors.add(error);
315 }
316 }
317
318 /// An enum of states for a [Schedule].
319 class ScheduleState {
320 /// The schedule can have tasks added to its queue, but is not yet running
321 /// them.
322 static const SET_UP = const ScheduleState._("SET_UP");
323
324 /// The schedule is actively running tasks. This includes running tasks in
325 /// [Schedule.onException] and [Schedule.onComplete].
326 static const RUNNING = const ScheduleState._("RUNNING");
327
328 /// The schedule has finished running all its tasks, either successfully or
329 /// with an error.
330 static const DONE = const ScheduleState._("DONE");
331
332 /// The name of the state.
333 final String name;
334
335 const ScheduleState._(this.name);
336
337 String toString() => name;
338 }
339
340 /// A queue of asynchronous tasks to execute in order.
341 class TaskQueue {
342 /// The tasks in the queue.
343 List<Task> get contents => new UnmodifiableListView<Task>(_contents);
344 final _contents = new Queue<Task>();
345
346 /// The name of the queue, for debugging purposes.
347 final String name;
348
349 /// The [Schedule] that created this queue.
350 final Schedule _schedule;
351
352 /// An out-of-band error signaled by [_schedule]. If this is non-null, it
353 /// indicates that the queue should stop as soon as possible and re-throw this
354 /// error.
355 ScheduleError _error;
356
357 /// The [SubstituteFuture] for the currently-running task in the queue, or
358 /// null if no task is currently running.
359 SubstituteFuture _taskFuture;
360
361 /// The toal number of out-of-band callbacks that have been registered on
362 /// [this].
363 int _totalCallbacks = 0;
364
365 /// Whether to stop running after the current task.
366 bool _aborted = false;
367
368 /// The descriptions of all callbacks that are blocking the completion of
369 /// [this].
370 List<PendingCallback> get pendingCallbacks =>
371 new UnmodifiableListView<PendingCallback>(_pendingCallbacks);
372 final _pendingCallbacks = new Queue<PendingCallback>();
373
374 /// A completer that will be completed once [_pendingCallbacks] becomes empty
375 /// after the queue finishes running its tasks.
376 Future get _noPendingCallbacks => _noPendingCallbacksCompleter.future;
377 final Completer _noPendingCallbacksCompleter = new Completer();
378
379 /// A [Future] that completes when the tasks in [this] are all complete. If an
380 /// error occurs while running this queue, the returned [Future] will complete
381 /// with that error.
382 ///
383 /// The returned [Future] can complete before outstanding out-of-band
384 /// callbacks have finished running.
385 Future get onTasksComplete => _onTasksCompleteCompleter.future;
386 final _onTasksCompleteCompleter = new Completer();
387
388 TaskQueue._(this.name, this._schedule) {
389 // Avoid top-leveling errors that are passed to onTasksComplete if there are
390 // no listeners.
391 onTasksComplete.catchError((_) {});
392 }
393
394 /// Whether this queue is currently running.
395 bool get isRunning => _schedule.state == ScheduleState.RUNNING &&
396 _schedule.currentQueue == this;
397
398 /// Whether this queue is running its tasks (as opposed to waiting for
399 /// out-of-band callbacks or not running at all).
400 bool get isRunningTasks => isRunning && _schedule.currentTask != null;
401
402 /// Schedules a task, [fn], to run asynchronously as part of this queue. Tasks
403 /// will be run in the order they're scheduled. In [fn] returns a [Future],
404 /// tasks after it won't be run until that [Future] completes.
405 ///
406 /// The return value will be completed once the scheduled task has finished
407 /// running. Its return value is the same as the return value of [fn], or the
408 /// value it completes to if it's a [Future].
409 ///
410 /// If [description] is passed, it's used to describe the task for debugging
411 /// purposes when an error occurs.
412 ///
413 /// If this is called when this queue is currently running, it will run [fn]
414 /// on the next event loop iteration rather than adding it to a queue--this is
415 /// known as a "nested task". The current task will not complete until [fn]
416 /// (and any [Future] it returns) has finished running. Any errors in [fn]
417 /// will automatically be handled. Nested tasks run in parallel, unlike
418 /// top-level tasks which run in sequence.
419 Future schedule(fn(), [String description]) {
420 if (isRunning) {
421 var task = _schedule.currentTask;
422 var wrappedFn = () => _schedule.wrapFuture(
423 new Future.value().then((_) => fn()));
424 if (task == null) return wrappedFn();
425 return task.runChild(wrappedFn, description);
426 }
427
428 var task = new Task(() {
429 return syncFuture(fn).catchError((e, stackTrace) {
430 throw new ScheduleError.from(_schedule, e, stackTrace: stackTrace);
431 });
432 }, description, this);
433 _contents.add(task);
434 return task.result;
435 }
436
437 /// Runs all the tasks in this queue in order.
438 Future _run() {
439 _schedule._currentQueue = this;
440 _schedule.heartbeat();
441 return Future.forEach(_contents, (task) {
442 _schedule._currentTask = task;
443 if (_error != null) throw _error;
444 if (_aborted) return null;
445
446 _taskFuture = new SubstituteFuture(task.fn());
447 return _taskFuture.whenComplete(() {
448 _taskFuture = null;
449 _schedule.heartbeat();
450 }).catchError((e, trace) {
451 var error = new ScheduleError.from(_schedule, e, stackTrace: trace);
452 _signalError(error);
453 throw _error;
454 });
455 }).whenComplete(() {
456 _schedule._currentTask = null;
457 }).then((_) {
458 _onTasksCompleteCompleter.complete();
459 }).catchError((e, stackTrace) {
460 _onTasksCompleteCompleter.completeError(e, stackTrace);
461 throw e;
462 }).whenComplete(() {
463 if (pendingCallbacks.isEmpty) return null;
464 return _noPendingCallbacks.catchError((e, stackTrace) {
465 // Signal the error rather than passing it through directly so that if a
466 // timeout happens after an in-task error, both are reported.
467 _signalError(new ScheduleError.from(_schedule, e,
468 stackTrace: stackTrace));
469 });
470 }).whenComplete(() {
471 _schedule.heartbeat();
472 // If the tasks were otherwise successful, make sure we throw any
473 // out-of-band errors. If a task failed, make sure we throw the most
474 // recent error.
475 if (_error != null) throw _error;
476 });
477 }
478
479 /// Stops this queue after the current task and any out-of-band callbacks
480 /// finish running.
481 void _abort() {
482 assert(_schedule.state == ScheduleState.SET_UP || isRunning);
483 _aborted = true;
484 }
485
486 /// Returns a function wrapping [fn] that pipes any errors into the schedule
487 /// chain. This will also block [this] from completing until the returned
488 /// function has been called. It's used to ensure that out-of-band callbacks
489 /// are properly handled by the scheduled test.
490 Function _wrapAsync(fn(arg), String description) {
491 assert(_schedule.state == ScheduleState.SET_UP || isRunning);
492
493 // It's possible that the queue timed out before [fn] finished.
494 bool _timedOut() =>
495 _schedule.currentQueue != this || pendingCallbacks.isEmpty;
496
497 _totalCallbacks++;
498 var chain = new Chain.current();
499 var pendingCallback = new PendingCallback._(() {
500 var fullDescription = description;
501 if (fullDescription == null) {
502 fullDescription = "Out-of-band operation #${_totalCallbacks}";
503 }
504
505 var stackString = prefixLines(terseTraceString(chain));
506 fullDescription += "\n\nStack chain:\n$stackString";
507 return fullDescription;
508 });
509 _pendingCallbacks.add(pendingCallback);
510
511 return (arg) {
512 try {
513 return fn(arg);
514 } catch (e, stackTrace) {
515 var error = new ScheduleError.from(
516 _schedule, e, stackTrace: stackTrace);
517 if (_timedOut()) {
518 _schedule._signalPostTimeoutError(error);
519 } else {
520 _schedule.signalError(error);
521 }
522 } finally {
523 if (_timedOut()) return null;
524
525 _pendingCallbacks.remove(pendingCallback);
526 if (_pendingCallbacks.isEmpty && !isRunningTasks) {
527 _noPendingCallbacksCompleter.complete();
528 }
529 }
530 };
531 }
532
533 /// Signals that an out-of-band error has been detected and the queue should
534 /// stop running as soon as possible.
535 void _signalError(ScheduleError error) {
536 // If multiple errors are detected while a task is running, make sure the
537 // earlier ones are recorded in the schedule.
538 if (_error != null) _schedule._addError(_error);
539 _error = error;
540 }
541
542 /// Notifies the queue that it has timed out and it needs to terminate
543 /// immediately with a timeout error.
544 void _signalTimeout(ScheduleError error) {
545 _pendingCallbacks.clear();
546 if (!isRunningTasks) {
547 _noPendingCallbacksCompleter.completeError(error);
548 } else if (_taskFuture != null) {
549 // Catch errors coming off the old task future, in case it completes after
550 // timing out.
551 _taskFuture.substitute(new Future.error(error))
552 .catchError((e, stackTrace) {
553 _schedule._signalPostTimeoutError(e, stackTrace);
554 });
555 } else {
556 // This branch probably won't be reached, but it's conceivable that the
557 // event loop might get pumped when _taskFuture is null but we haven't yet
558 // finished running all the tasks.
559 _signalError(error);
560 }
561 }
562
563 String toString() => name;
564
565 /// Returns a detailed representation of the queue as a tree of tasks. If
566 /// [highlight] is passed, that task is specially highlighted.
567 ///
568 /// [highlight] must be a task in this queue.
569 String generateTree([Task highlight]) {
570 assert(highlight == null || highlight.queue == this);
571 return _contents.map((task) {
572 var taskString = task == highlight
573 ? task.toStringWithStackTrace()
574 : task.toString();
575 taskString = prefixLines(taskString,
576 firstPrefix: task == highlight ? "> " : "* ");
577
578 if (task == highlight && !task.children.isEmpty) {
579 var childrenString = task.children.map((child) {
580 var prefix = ">";
581 if (child.state == TaskState.ERROR) {
582 prefix = "X";
583 } else if (child.state == TaskState.SUCCESS) {
584 prefix = "*";
585 }
586
587 var childString = prefix == "*"
588 ? child.toString()
589 : child.toStringWithStackTrace();
590 return prefixLines(childString,
591 firstPrefix: " $prefix ", prefix: " | ");
592 }).join('\n');
593 taskString = '$taskString\n$childrenString';
594 }
595
596 return taskString;
597 }).join("\n");
598 }
599 }
600
601 /// A thunk for lazily resolving the description of a [PendingCallback].
602 typedef String _DescriptionThunk();
603
604 /// An identifier for an out-of-band callback running during a schedule.
605 class PendingCallback {
606 final _DescriptionThunk _thunk;
607 String _description;
608
609 /// The string description of the callback.
610 String get description {
611 if (_description == null) _description = _thunk();
612 return _description;
613 }
614
615 String toString() => description;
616
617 PendingCallback._(this._thunk);
618 }
OLDNEW
« no previous file with comments | « pkg/scheduled_test/lib/src/mock_clock.dart ('k') | pkg/scheduled_test/lib/src/schedule_error.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698