OLD | NEW |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 library schedule; | 5 library schedule; |
6 | 6 |
7 import 'dart:async'; | 7 import 'dart:async'; |
8 import 'dart:collection'; | 8 import 'dart:collection'; |
9 | 9 |
10 import 'package:unittest/unittest.dart' as unittest; | 10 import 'package:unittest/unittest.dart' as unittest; |
(...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
53 /// such task. This will be `null` both before the schedule starts running and | 53 /// such task. This will be `null` both before the schedule starts running and |
54 /// after it's finished. | 54 /// after it's finished. |
55 Task get currentTask => _currentTask; | 55 Task get currentTask => _currentTask; |
56 Task _currentTask; | 56 Task _currentTask; |
57 | 57 |
58 /// The current state of the schedule. | 58 /// The current state of the schedule. |
59 ScheduleState get state => _state; | 59 ScheduleState get state => _state; |
60 ScheduleState _state = ScheduleState.SET_UP; | 60 ScheduleState _state = ScheduleState.SET_UP; |
61 | 61 |
62 // TODO(nweiz): make this a read-only view once issue 8321 is fixed. | 62 // TODO(nweiz): make this a read-only view once issue 8321 is fixed. |
63 | |
64 /// Errors thrown by the task queues. | 63 /// Errors thrown by the task queues. |
65 /// | 64 /// |
66 /// When running tasks in [tasks], this will always be empty. If an error | 65 /// When running tasks in [tasks], this will always be empty. If an error |
67 /// occurs in [tasks], it will be added to this list and then [onException] | 66 /// occurs in [tasks], it will be added to this list and then [onException] |
68 /// will be run. If an error occurs there as well, it will be added to this | 67 /// will be run. If an error occurs there as well, it will be added to this |
69 /// list and [onComplete] will be run. Errors thrown during [onComplete] will | 68 /// list and [onComplete] will be run. Errors thrown during [onComplete] will |
70 /// also be added to this list, although no scheduled tasks will be run | 69 /// also be added to this list, although no scheduled tasks will be run |
71 /// afterwards. | 70 /// afterwards. |
72 /// | 71 /// |
73 /// Any out-of-band callbacks that throw errors will also have those errors | 72 /// Any out-of-band callbacks that throw errors will also have those errors |
74 /// added to this list. | 73 /// added to this list. |
75 final errors = <ScheduleError>[]; | 74 final errors = <ScheduleError>[]; |
76 | 75 |
| 76 // TODO(nweiz): make this a read-only view once issue 8321 is fixed. |
| 77 /// Additional debugging info registered via [addDebugInfo]. |
| 78 final debugInfo = <String>[]; |
| 79 |
77 /// The task queue that's currently being run. One of [tasks], [onException], | 80 /// The task queue that's currently being run. One of [tasks], [onException], |
78 /// or [onComplete]. This starts as [tasks], and can only be `null` after the | 81 /// or [onComplete]. This starts as [tasks], and can only be `null` after the |
79 /// schedule is done. | 82 /// schedule is done. |
80 TaskQueue get currentQueue => | 83 TaskQueue get currentQueue => |
81 _state == ScheduleState.DONE ? null : _currentQueue; | 84 _state == ScheduleState.DONE ? null : _currentQueue; |
82 TaskQueue _currentQueue; | 85 TaskQueue _currentQueue; |
83 | 86 |
84 /// The time to wait before terminating a task queue for inactivity. Defaults | 87 /// The time to wait before terminating a task queue for inactivity. Defaults |
85 /// to 30 seconds. This can be set to `null` to disable timeouts entirely. | 88 /// to 30 seconds. This can be set to `null` to disable timeouts entirely. |
86 /// | 89 /// |
87 /// If a task queue times out, an error will be raised that can be handled as | 90 /// If a task queue times out, an error will be raised that can be handled as |
88 /// usual in the [onException] and [onComplete] queues. If [onException] times | 91 /// usual in the [onException] and [onComplete] queues. If [onException] times |
89 /// out, that can only be handled in [onComplete]; if [onComplete] times out, | 92 /// out, that can only be handled in [onComplete]; if [onComplete] times out, |
90 /// that cannot be handled. | 93 /// that cannot be handled. |
91 /// | 94 /// |
92 /// If a task times out and then later completes with an error, that error | 95 /// If a task times out and then later completes with an error, that error |
93 /// cannot be handled. The user will still be notified of it. | 96 /// cannot be handled. The user will still be notified of it. |
94 Duration get timeout => _timeout; | 97 Duration get timeout => _timeout; |
95 Duration _timeout = new Duration(seconds: 30); | 98 Duration _timeout = new Duration(seconds: 2); |
96 set timeout(Duration duration) { | 99 set timeout(Duration duration) { |
97 _timeout = duration; | 100 _timeout = duration; |
98 heartbeat(); | 101 heartbeat(); |
99 } | 102 } |
100 | 103 |
101 /// The number of out-of-band callbacks that have been registered with | 104 /// The number of out-of-band callbacks that have been registered with |
102 /// [wrapAsync] but have yet to be called. | 105 /// [wrapAsync] but have yet to be called. |
103 int _pendingCallbacks = 0; | 106 int _pendingCallbacks = 0; |
104 | 107 |
105 /// A completer that will be completed once [_pendingCallbacks] reaches zero. | 108 /// A completer that will be completed once [_pendingCallbacks] reaches zero. |
(...skipping 72 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
178 "${errorString()}"); | 181 "${errorString()}"); |
179 } else if (state == ScheduleState.SET_UP) { | 182 } else if (state == ScheduleState.SET_UP) { |
180 // If we're setting up, throwing the error will pipe it into the main | 183 // If we're setting up, throwing the error will pipe it into the main |
181 // error-handling code. | 184 // error-handling code. |
182 throw scheduleError; | 185 throw scheduleError; |
183 } else { | 186 } else { |
184 _currentQueue._signalError(scheduleError); | 187 _currentQueue._signalError(scheduleError); |
185 } | 188 } |
186 } | 189 } |
187 | 190 |
| 191 /// Adds [info] to the debugging output that will be printed if the test |
| 192 /// fails. Unlike [signalError], this won't cause the test to fail, nor will |
| 193 /// it short-circuit the current [TaskQueue]; it's just useful for providing |
| 194 /// additional information that may not fit cleanly into an existing error. |
| 195 void addDebugInfo(String info) => debugInfo.add(info); |
| 196 |
188 /// Notifies the schedule of an error that occurred in a task or out-of-band | 197 /// Notifies the schedule of an error that occurred in a task or out-of-band |
189 /// callback after the appropriate queue has timed out. If this schedule is | 198 /// callback after the appropriate queue has timed out. If this schedule is |
190 /// still running, the error will be added to the errors list to be shown | 199 /// still running, the error will be added to the errors list to be shown |
191 /// along with the timeout error; otherwise, a top-level error will be thrown. | 200 /// along with the timeout error; otherwise, a top-level error will be thrown. |
192 void _signalPostTimeoutError(error, [stackTrace]) { | 201 void _signalPostTimeoutError(error, [stackTrace]) { |
193 var scheduleError = new ScheduleError.from(this, error, | 202 var scheduleError = new ScheduleError.from(this, error, |
194 stackTrace: stackTrace); | 203 stackTrace: stackTrace); |
195 _addError(scheduleError); | 204 _addError(scheduleError); |
196 if (_state == ScheduleState.DONE) { | 205 if (_state == ScheduleState.DONE) { |
197 throw new StateError( | 206 throw new StateError( |
(...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
265 // Don't top-level the error, since it's already been signaled to the | 274 // Don't top-level the error, since it's already been signaled to the |
266 // schedule. | 275 // schedule. |
267 future.catchError((_) => null); | 276 future.catchError((_) => null); |
268 | 277 |
269 return future; | 278 return future; |
270 } | 279 } |
271 | 280 |
272 /// Returns a string representation of all errors registered on this schedule. | 281 /// Returns a string representation of all errors registered on this schedule. |
273 String errorString() { | 282 String errorString() { |
274 if (errors.isEmpty) return "The schedule had no errors."; | 283 if (errors.isEmpty) return "The schedule had no errors."; |
275 if (errors.length == 1) return errors.first.toString(); | 284 if (errors.length == 1 && debugInfo.isEmpty) return errors.first.toString(); |
276 var errorStrings = errors.map((e) => e.toString()).join("\n================" | 285 |
277 "================================================================\n"); | 286 var border = "\n===========================================================" |
278 return "The schedule had ${errors.length} errors:\n$errorStrings"; | 287 "=====================\n"; |
| 288 var errorStrings = errors.map((e) => e.toString()).join(border); |
| 289 var message = "The schedule had ${errors.length} errors:\n$errorStrings"; |
| 290 |
| 291 if (!debugInfo.isEmpty) { |
| 292 message = "$message$border\nDebug info:\n${debugInfo.join(border)}"; |
| 293 } |
| 294 |
| 295 return message; |
279 } | 296 } |
280 | 297 |
281 /// Notifies the schedule that progress is being made on an asynchronous task. | 298 /// Notifies the schedule that progress is being made on an asynchronous task. |
282 /// This resets the timeout timer, and can be used in long-running tasks to | 299 /// This resets the timeout timer, and can be used in long-running tasks to |
283 /// keep them from timing out. | 300 /// keep them from timing out. |
284 void heartbeat() { | 301 void heartbeat() { |
285 if (_timeoutTimer != null) _timeoutTimer.cancel(); | 302 if (_timeoutTimer != null) _timeoutTimer.cancel(); |
286 if (_timeout == null) { | 303 if (_timeout == null) { |
287 _timeoutTimer = null; | 304 _timeoutTimer = null; |
288 } else { | 305 } else { |
(...skipping 75 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
364 | 381 |
365 /// An out-of-band error signaled by [_schedule]. If this is non-null, it | 382 /// An out-of-band error signaled by [_schedule]. If this is non-null, it |
366 /// indicates that the queue should stop as soon as possible and re-throw this | 383 /// indicates that the queue should stop as soon as possible and re-throw this |
367 /// error. | 384 /// error. |
368 ScheduleError _error; | 385 ScheduleError _error; |
369 | 386 |
370 /// The [SubstituteFuture] for the currently-running task in the queue, or | 387 /// The [SubstituteFuture] for the currently-running task in the queue, or |
371 /// null if no task is currently running. | 388 /// null if no task is currently running. |
372 SubstituteFuture _taskFuture; | 389 SubstituteFuture _taskFuture; |
373 | 390 |
374 TaskQueue._(this.name, this._schedule); | 391 /// A [Future] that completes when the tasks in [this] are all complete. If an |
| 392 /// error occurs while running this queue, the returned [Future] will complete |
| 393 /// with that error. |
| 394 /// |
| 395 /// The returned [Future] can complete before outstanding out-of-band |
| 396 /// callbacks have finished running. |
| 397 Future get onTasksComplete => _onTasksCompleteCompleter.future; |
| 398 final _onTasksCompleteCompleter = new Completer(); |
| 399 |
| 400 TaskQueue._(this.name, this._schedule) { |
| 401 // Avoid top-leveling errors that are passed to onTasksComplete if there are |
| 402 // no listeners. |
| 403 onTasksComplete.catchError((_) {}); |
| 404 } |
375 | 405 |
376 /// Whether this queue is currently running. | 406 /// Whether this queue is currently running. |
377 bool get isRunning => _schedule.state == ScheduleState.RUNNING && | 407 bool get isRunning => _schedule.state == ScheduleState.RUNNING && |
378 _schedule.currentQueue == this; | 408 _schedule.currentQueue == this; |
379 | 409 |
380 /// Schedules a task, [fn], to run asynchronously as part of this queue. Tasks | 410 /// Schedules a task, [fn], to run asynchronously as part of this queue. Tasks |
381 /// will be run in the order they're scheduled. In [fn] returns a [Future], | 411 /// will be run in the order they're scheduled. In [fn] returns a [Future], |
382 /// tasks after it won't be run until that [Future] completes. | 412 /// tasks after it won't be run until that [Future] completes. |
383 /// | 413 /// |
384 /// The return value will be completed once the scheduled task has finished | 414 /// The return value will be completed once the scheduled task has finished |
(...skipping 11 matching lines...) Expand all Loading... |
396 /// top-level tasks which run in sequence. | 426 /// top-level tasks which run in sequence. |
397 Future schedule(fn(), [String description]) { | 427 Future schedule(fn(), [String description]) { |
398 if (isRunning) { | 428 if (isRunning) { |
399 var task = _schedule.currentTask; | 429 var task = _schedule.currentTask; |
400 var wrappedFn = () => _schedule.wrapFuture( | 430 var wrappedFn = () => _schedule.wrapFuture( |
401 new Future.immediate(null).then((_) => fn())); | 431 new Future.immediate(null).then((_) => fn())); |
402 if (task == null) return wrappedFn(); | 432 if (task == null) return wrappedFn(); |
403 return task.runChild(wrappedFn, description); | 433 return task.runChild(wrappedFn, description); |
404 } | 434 } |
405 | 435 |
406 var task = new Task(fn, description, this); | 436 var task = new Task(() { |
| 437 return new Future.of(fn).catchError((e) { |
| 438 throw new ScheduleError.from(_schedule, e); |
| 439 }); |
| 440 }, description, this); |
407 _contents.add(task); | 441 _contents.add(task); |
408 return task.result; | 442 return task.result; |
409 } | 443 } |
410 | 444 |
411 /// Runs all the tasks in this queue in order. | 445 /// Runs all the tasks in this queue in order. |
412 Future _run() { | 446 Future _run() { |
413 _schedule._currentQueue = this; | 447 _schedule._currentQueue = this; |
414 _schedule.heartbeat(); | 448 _schedule.heartbeat(); |
415 return Future.forEach(_contents, (task) { | 449 return Future.forEach(_contents, (task) { |
416 _schedule._currentTask = task; | 450 _schedule._currentTask = task; |
417 if (_error != null) throw _error; | 451 if (_error != null) throw _error; |
418 | 452 |
419 _taskFuture = new SubstituteFuture(task.fn()); | 453 _taskFuture = new SubstituteFuture(task.fn()); |
420 return _taskFuture.whenComplete(() { | 454 return _taskFuture.whenComplete(() { |
421 _taskFuture = null; | 455 _taskFuture = null; |
422 _schedule.heartbeat(); | 456 _schedule.heartbeat(); |
423 }).catchError((e) { | 457 }).catchError((e) { |
424 if (_error != null) _schedule._addError(_error); | 458 var error = new ScheduleError.from(_schedule, e); |
425 throw new ScheduleError.from(_schedule, e); | 459 _signalError(error); |
| 460 throw _error; |
| 461 }); |
| 462 }).then((_) { |
| 463 _onTasksCompleteCompleter.complete(); |
| 464 }).catchError((e) { |
| 465 _onTasksCompleteCompleter.completeError(e); |
| 466 throw e; |
| 467 }).whenComplete(() { |
| 468 _schedule._currentTask = null; |
| 469 return _schedule._awaitNoPendingCallbacks().catchError((e) { |
| 470 // Signal the error rather than passing it through directly so that if a |
| 471 // timeout happens after an in-task error, both are reported. |
| 472 _signalError(new ScheduleError.from(_schedule, e)); |
426 }); | 473 }); |
427 }).whenComplete(() { | 474 }).whenComplete(() { |
428 _schedule._currentTask = null; | |
429 return _schedule._awaitNoPendingCallbacks(); | |
430 }).then((_) { | |
431 _schedule.heartbeat(); | 475 _schedule.heartbeat(); |
| 476 // If the tasks were otherwise successful, make sure we throw any |
| 477 // out-of-band errors. If a task failed, make sure we throw the most |
| 478 // recent error. |
432 if (_error != null) throw _error; | 479 if (_error != null) throw _error; |
433 }); | 480 }); |
434 } | 481 } |
435 | 482 |
436 /// Signals that an out-of-band error has been detected and the queue should | 483 /// Signals that an out-of-band error has been detected and the queue should |
437 /// stop running as soon as possible. | 484 /// stop running as soon as possible. |
438 void _signalError(ScheduleError error) { | 485 void _signalError(ScheduleError error) { |
439 // If multiple errors are detected while a task is running, make sure the | 486 // If multiple errors are detected while a task is running, make sure the |
440 // earlier ones are recorded in the schedule. | 487 // earlier ones are recorded in the schedule. |
441 if (_error != null) _schedule._addError(_error); | 488 if (_error != null) _schedule._addError(_error); |
(...skipping 28 matching lines...) Expand all Loading... |
470 return _contents.map((task) { | 517 return _contents.map((task) { |
471 var lines = task.toString().split("\n"); | 518 var lines = task.toString().split("\n"); |
472 var firstLine = task == highlight ? | 519 var firstLine = task == highlight ? |
473 "> ${lines.first}" : "* ${lines.first}"; | 520 "> ${lines.first}" : "* ${lines.first}"; |
474 lines = new List.from(lines.skip(1).map((line) => "| $line")); | 521 lines = new List.from(lines.skip(1).map((line) => "| $line")); |
475 lines.insertRange(0, 1, firstLine); | 522 lines.insertRange(0, 1, firstLine); |
476 return lines.join("\n"); | 523 return lines.join("\n"); |
477 }).join("\n"); | 524 }).join("\n"); |
478 } | 525 } |
479 } | 526 } |
OLD | NEW |