| 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 109 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 120 heartbeat(); | 120 heartbeat(); |
| 121 } | 121 } |
| 122 | 122 |
| 123 /// Sets up this schedule by running [setUp], then runs all the task queues in | 123 /// Sets up this schedule by running [setUp], then runs all the task queues in |
| 124 /// order. Any errors in [setUp] will cause [onException] to run. | 124 /// order. Any errors in [setUp] will cause [onException] to run. |
| 125 Future run(void setUp()) { | 125 Future run(void setUp()) { |
| 126 return new Future.immediate(null).then((_) { | 126 return new Future.immediate(null).then((_) { |
| 127 try { | 127 try { |
| 128 setUp(); | 128 setUp(); |
| 129 } catch (e, stackTrace) { | 129 } catch (e, stackTrace) { |
| 130 // Even though the scheduling failed, we need to run the onException and |
| 131 // onComplete queues, so we set the schedule state to RUNNING. |
| 132 _state = ScheduleState.RUNNING; |
| 130 throw new ScheduleError.from(this, e, stackTrace: stackTrace); | 133 throw new ScheduleError.from(this, e, stackTrace: stackTrace); |
| 131 } | 134 } |
| 132 | 135 |
| 133 _state = ScheduleState.RUNNING; | 136 _state = ScheduleState.RUNNING; |
| 134 return tasks._run(); | 137 return tasks._run(); |
| 135 }).catchError((e) { | 138 }).catchError((e) { |
| 136 errors.add(e); | 139 _addError(e); |
| 137 return onException._run().catchError((innerError) { | 140 return onException._run().catchError((innerError) { |
| 138 // If an error occurs in a task in the onException queue, make sure it's | 141 // If an error occurs in a task in the onException queue, make sure it's |
| 139 // registered in the error list and re-throw it. We could also re-throw | 142 // registered in the error list and re-throw it. We could also re-throw |
| 140 // `e`; ultimately, all the errors will be shown to the user if any | 143 // `e`; ultimately, all the errors will be shown to the user if any |
| 141 // ScheduleError is thrown. | 144 // ScheduleError is thrown. |
| 142 errors.add(innerError); | 145 _addError(innerError); |
| 143 throw innerError; | 146 throw innerError; |
| 144 }).then((_) { | 147 }).then((_) { |
| 145 // If there are no errors in the onException queue, re-throw the | 148 // If there are no errors in the onException queue, re-throw the |
| 146 // original error that caused it to run. | 149 // original error that caused it to run. |
| 147 throw e; | 150 throw e; |
| 148 }); | 151 }); |
| 149 }).whenComplete(() { | 152 }).whenComplete(() { |
| 150 return onComplete._run().catchError((e) { | 153 return onComplete._run().catchError((e) { |
| 151 // If an error occurs in a task in the onComplete queue, make sure it's | 154 // If an error occurs in a task in the onComplete queue, make sure it's |
| 152 // registered in the error list and re-throw it. | 155 // registered in the error list and re-throw it. |
| 153 errors.add(e); | 156 _addError(e); |
| 154 throw e; | 157 throw e; |
| 155 }); | 158 }); |
| 156 }).whenComplete(() { | 159 }).whenComplete(() { |
| 157 if (_timeoutTimer != null) _timeoutTimer.cancel(); | 160 if (_timeoutTimer != null) _timeoutTimer.cancel(); |
| 158 _state = ScheduleState.DONE; | 161 _state = ScheduleState.DONE; |
| 159 }); | 162 }); |
| 160 } | 163 } |
| 161 | 164 |
| 162 /// Signals that an out-of-band error has occurred. Using [wrapAsync] along | 165 /// Signals that an out-of-band error has occurred. Using [wrapAsync] along |
| 163 /// with `throw` is usually preferable to calling this directly. | 166 /// with `throw` is usually preferable to calling this directly. |
| (...skipping 18 matching lines...) Expand all Loading... |
| 182 } | 185 } |
| 183 } | 186 } |
| 184 | 187 |
| 185 /// Notifies the schedule of an error that occurred in a task or out-of-band | 188 /// Notifies the schedule of an error that occurred in a task or out-of-band |
| 186 /// callback after the appropriate queue has timed out. If this schedule is | 189 /// callback after the appropriate queue has timed out. If this schedule is |
| 187 /// still running, the error will be added to the errors list to be shown | 190 /// still running, the error will be added to the errors list to be shown |
| 188 /// along with the timeout error; otherwise, a top-level error will be thrown. | 191 /// along with the timeout error; otherwise, a top-level error will be thrown. |
| 189 void _signalPostTimeoutError(error, [stackTrace]) { | 192 void _signalPostTimeoutError(error, [stackTrace]) { |
| 190 var scheduleError = new ScheduleError.from(this, error, | 193 var scheduleError = new ScheduleError.from(this, error, |
| 191 stackTrace: stackTrace); | 194 stackTrace: stackTrace); |
| 192 errors.add(scheduleError); | 195 _addError(scheduleError); |
| 193 if (_state == ScheduleState.DONE) { | 196 if (_state == ScheduleState.DONE) { |
| 194 throw new StateError( | 197 throw new StateError( |
| 195 "An out-of-band error was caught after the test timed out.\n" | 198 "An out-of-band error was caught after the test timed out.\n" |
| 196 "${errorString()}"); | 199 "${errorString()}"); |
| 197 } | 200 } |
| 198 } | 201 } |
| 199 | 202 |
| 200 /// Returns a function wrapping [fn] that pipes any errors into the schedule | 203 /// Returns a function wrapping [fn] that pipes any errors into the schedule |
| 201 /// chain. This will also block the current task queue from completing until | 204 /// chain. This will also block the current task queue from completing until |
| 202 /// the returned function has been called. It's used to ensure that | 205 /// the returned function has been called. It's used to ensure that |
| 203 /// out-of-band callbacks are properly handled by the scheduled test. | 206 /// out-of-band callbacks are properly handled by the scheduled test. |
| 204 /// | 207 /// |
| 205 /// The top-level `wrapAsync` function should usually be used in preference to | 208 /// The top-level `wrapAsync` function should usually be used in preference to |
| 206 /// this. | 209 /// this in test code. |
| 207 Function wrapAsync(fn(arg)) { | 210 Function wrapAsync(fn(arg)) { |
| 208 if (_state == ScheduleState.DONE) { | 211 if (_state == ScheduleState.DONE) { |
| 209 throw new StateError("wrapAsync called after the schedule has finished " | 212 throw new StateError("wrapAsync called after the schedule has finished " |
| 210 "running."); | 213 "running."); |
| 211 } | 214 } |
| 212 heartbeat(); | 215 heartbeat(); |
| 213 | 216 |
| 214 var queue = currentQueue; | 217 var queue = currentQueue; |
| 215 // It's possible that the queue timed out before this. | 218 // It's possible that the queue timed out before this. |
| 216 bool _timedOut() => queue != currentQueue || _pendingCallbacks == 0; | 219 bool _timedOut() => queue != currentQueue || _pendingCallbacks == 0; |
| (...skipping 13 matching lines...) Expand all Loading... |
| 230 | 233 |
| 231 _pendingCallbacks--; | 234 _pendingCallbacks--; |
| 232 if (_pendingCallbacks == 0 && _noPendingCallbacks != null) { | 235 if (_pendingCallbacks == 0 && _noPendingCallbacks != null) { |
| 233 _noPendingCallbacks.complete(); | 236 _noPendingCallbacks.complete(); |
| 234 _noPendingCallbacks = null; | 237 _noPendingCallbacks = null; |
| 235 } | 238 } |
| 236 } | 239 } |
| 237 }; | 240 }; |
| 238 } | 241 } |
| 239 | 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 /// The top-level `wrapFuture` function should usually be used in preference |
| 251 /// to this in test code. |
| 252 Future wrapFuture(Future future) { |
| 253 var doneCallback = wrapAsync((_) => null); |
| 254 done() => new Future.immediate(null).then(doneCallback); |
| 255 |
| 256 future = future.then((result) { |
| 257 done(); |
| 258 return result; |
| 259 }).catchError((e) { |
| 260 signalError(e); |
| 261 done(); |
| 262 throw e; |
| 263 }); |
| 264 |
| 265 // Don't top-level the error, since it's already been signaled to the |
| 266 // schedule. |
| 267 future.catchError((_) => null); |
| 268 |
| 269 return future; |
| 270 } |
| 271 |
| 240 /// Returns a string representation of all errors registered on this schedule. | 272 /// Returns a string representation of all errors registered on this schedule. |
| 241 String errorString() { | 273 String errorString() { |
| 242 if (errors.isEmpty) return "The schedule had no errors."; | 274 if (errors.isEmpty) return "The schedule had no errors."; |
| 243 if (errors.length == 1) return errors.first.toString(); | 275 if (errors.length == 1) return errors.first.toString(); |
| 244 var errorStrings = errors.map((e) => e.toString()).join("\n================" | 276 var errorStrings = errors.map((e) => e.toString()).join("\n================" |
| 245 "================================================================\n"); | 277 "================================================================\n"); |
| 246 return "The schedule had ${errors.length} errors:\n$errorStrings"; | 278 return "The schedule had ${errors.length} errors:\n$errorStrings"; |
| 247 } | 279 } |
| 248 | 280 |
| 249 /// Notifies the schedule that progress is being made on an asynchronous task. | 281 /// Notifies the schedule that progress is being made on an asynchronous task. |
| (...skipping 28 matching lines...) Expand all Loading... |
| 278 } | 310 } |
| 279 } | 311 } |
| 280 | 312 |
| 281 /// Returns a [Future] that will complete once there are no pending | 313 /// Returns a [Future] that will complete once there are no pending |
| 282 /// out-of-band callbacks. | 314 /// out-of-band callbacks. |
| 283 Future _awaitNoPendingCallbacks() { | 315 Future _awaitNoPendingCallbacks() { |
| 284 if (_pendingCallbacks == 0) return new Future.immediate(null); | 316 if (_pendingCallbacks == 0) return new Future.immediate(null); |
| 285 if (_noPendingCallbacks == null) _noPendingCallbacks = new Completer(); | 317 if (_noPendingCallbacks == null) _noPendingCallbacks = new Completer(); |
| 286 return _noPendingCallbacks.future; | 318 return _noPendingCallbacks.future; |
| 287 } | 319 } |
| 320 |
| 321 /// Register an error in the schedule's error list. This ensures that there |
| 322 /// are no duplicate errors, and that all errors are wrapped in |
| 323 /// [ScheduleError]. |
| 324 void _addError(error) { |
| 325 if (errors.contains(error)) return; |
| 326 errors.add(new ScheduleError.from(this, error)); |
| 327 } |
| 288 } | 328 } |
| 289 | 329 |
| 290 /// An enum of states for a [Schedule]. | 330 /// An enum of states for a [Schedule]. |
| 291 class ScheduleState { | 331 class ScheduleState { |
| 292 /// The schedule can have tasks added to its queue, but is not yet running | 332 /// The schedule can have tasks added to its queue, but is not yet running |
| 293 /// them. | 333 /// them. |
| 294 static const SET_UP = const ScheduleState._("SET_UP"); | 334 static const SET_UP = const ScheduleState._("SET_UP"); |
| 295 | 335 |
| 296 /// The schedule is actively running tasks. This includes running tasks in | 336 /// The schedule is actively running tasks. This includes running tasks in |
| 297 /// [Schedule.onException] and [Schedule.onComplete]. | 337 /// [Schedule.onException] and [Schedule.onComplete]. |
| (...skipping 28 matching lines...) Expand all Loading... |
| 326 /// indicates that the queue should stop as soon as possible and re-throw this | 366 /// indicates that the queue should stop as soon as possible and re-throw this |
| 327 /// error. | 367 /// error. |
| 328 ScheduleError _error; | 368 ScheduleError _error; |
| 329 | 369 |
| 330 /// The [SubstituteFuture] for the currently-running task in the queue, or | 370 /// The [SubstituteFuture] for the currently-running task in the queue, or |
| 331 /// null if no task is currently running. | 371 /// null if no task is currently running. |
| 332 SubstituteFuture _taskFuture; | 372 SubstituteFuture _taskFuture; |
| 333 | 373 |
| 334 TaskQueue._(this.name, this._schedule); | 374 TaskQueue._(this.name, this._schedule); |
| 335 | 375 |
| 376 /// Whether this queue is currently running. |
| 377 bool get isRunning => _schedule.state == ScheduleState.RUNNING && |
| 378 _schedule.currentQueue == this; |
| 379 |
| 336 /// Schedules a task, [fn], to run asynchronously as part of this queue. Tasks | 380 /// Schedules a task, [fn], to run asynchronously as part of this queue. Tasks |
| 337 /// will be run in the order they're scheduled. In [fn] returns a [Future], | 381 /// will be run in the order they're scheduled. In [fn] returns a [Future], |
| 338 /// tasks after it won't be run until that [Future] completes. | 382 /// tasks after it won't be run until that [Future] completes. |
| 339 /// | 383 /// |
| 340 /// The return value will be completed once the scheduled task has finished | 384 /// The return value will be completed once the scheduled task has finished |
| 341 /// running. Its return value is the same as the return value of [fn], or the | 385 /// running. Its return value is the same as the return value of [fn], or the |
| 342 /// value it completes to if it's a [Future]. | 386 /// value it completes to if it's a [Future]. |
| 343 /// | 387 /// |
| 344 /// If [description] is passed, it's used to describe the task for debugging | 388 /// If [description] is passed, it's used to describe the task for debugging |
| 345 /// purposes when an error occurs. | 389 /// purposes when an error occurs. |
| 390 /// |
| 391 /// If this is called when this queue is currently running, it will run [fn] |
| 392 /// on the next event loop iteration rather than adding it to a queue--this is |
| 393 /// known as a "nested task". The current task will not complete until [fn] |
| 394 /// (and any [Future] it returns) has finished running. Any errors in [fn] |
| 395 /// will automatically be handled. Nested tasks run in parallel, unlike |
| 396 /// top-level tasks which run in sequence. |
| 346 Future schedule(fn(), [String description]) { | 397 Future schedule(fn(), [String description]) { |
| 347 var task = new Task(fn, this, description); | 398 if (isRunning) { |
| 399 var task = _schedule.currentTask; |
| 400 var wrappedFn = () => _schedule.wrapFuture( |
| 401 new Future.immediate(null).then((_) => fn())); |
| 402 if (task == null) return wrappedFn(); |
| 403 return task.runChild(wrappedFn, description); |
| 404 } |
| 405 |
| 406 var task = new Task(fn, description, this); |
| 348 _contents.add(task); | 407 _contents.add(task); |
| 349 return task.result; | 408 return task.result; |
| 350 } | 409 } |
| 351 | 410 |
| 352 /// Runs all the tasks in this queue in order. | 411 /// Runs all the tasks in this queue in order. |
| 353 Future _run() { | 412 Future _run() { |
| 354 _schedule._currentQueue = this; | 413 _schedule._currentQueue = this; |
| 355 _schedule.heartbeat(); | 414 _schedule.heartbeat(); |
| 356 return Future.forEach(_contents, (task) { | 415 return Future.forEach(_contents, (task) { |
| 357 _schedule._currentTask = task; | 416 _schedule._currentTask = task; |
| 358 if (_error != null) throw _error; | 417 if (_error != null) throw _error; |
| 359 | 418 |
| 360 _taskFuture = new SubstituteFuture(task.fn()); | 419 _taskFuture = new SubstituteFuture(task.fn()); |
| 361 return _taskFuture.whenComplete(() { | 420 return _taskFuture.whenComplete(() { |
| 362 _taskFuture = null; | 421 _taskFuture = null; |
| 363 _schedule.heartbeat(); | 422 _schedule.heartbeat(); |
| 364 }).catchError((e) { | 423 }).catchError((e) { |
| 365 if (_error != null) _schedule.errors.add(_error); | 424 if (_error != null) _schedule._addError(_error); |
| 366 throw new ScheduleError.from(_schedule, e); | 425 throw new ScheduleError.from(_schedule, e); |
| 367 }); | 426 }); |
| 368 }).whenComplete(() { | 427 }).whenComplete(() { |
| 369 _schedule._currentTask = null; | 428 _schedule._currentTask = null; |
| 370 return _schedule._awaitNoPendingCallbacks(); | 429 return _schedule._awaitNoPendingCallbacks(); |
| 371 }).then((_) { | 430 }).then((_) { |
| 372 _schedule.heartbeat(); | 431 _schedule.heartbeat(); |
| 373 if (_error != null) throw _error; | 432 if (_error != null) throw _error; |
| 374 }); | 433 }); |
| 375 } | 434 } |
| 376 | 435 |
| 377 /// Signals that an out-of-band error has been detected and the queue should | 436 /// Signals that an out-of-band error has been detected and the queue should |
| 378 /// stop running as soon as possible. | 437 /// stop running as soon as possible. |
| 379 void _signalError(ScheduleError error) { | 438 void _signalError(ScheduleError error) { |
| 380 // If multiple errors are detected while a task is running, make sure the | 439 // If multiple errors are detected while a task is running, make sure the |
| 381 // earlier ones are recorded in the schedule. | 440 // earlier ones are recorded in the schedule. |
| 382 if (_error != null) _schedule.errors.add(_error); | 441 if (_error != null) _schedule._addError(_error); |
| 383 _error = error; | 442 _error = error; |
| 384 } | 443 } |
| 385 | 444 |
| 386 /// Notifies the queue that it has timed out and it needs to terminate | 445 /// Notifies the queue that it has timed out and it needs to terminate |
| 387 /// immediately with a timeout error. | 446 /// immediately with a timeout error. |
| 388 void _signalTimeout(ScheduleError error) { | 447 void _signalTimeout(ScheduleError error) { |
| 389 if (_taskFuture != null) { | 448 if (_taskFuture != null) { |
| 390 // Catch errors coming off the old task future, in case it completes after | 449 // Catch errors coming off the old task future, in case it completes after |
| 391 // timing out. | 450 // timing out. |
| 392 _taskFuture.substitute(new Future.immediateError(error)).catchError((e) { | 451 _taskFuture.substitute(new Future.immediateError(error)).catchError((e) { |
| (...skipping 18 matching lines...) Expand all Loading... |
| 411 return _contents.map((task) { | 470 return _contents.map((task) { |
| 412 var lines = task.toString().split("\n"); | 471 var lines = task.toString().split("\n"); |
| 413 var firstLine = task == highlight ? | 472 var firstLine = task == highlight ? |
| 414 "> ${lines.first}" : "* ${lines.first}"; | 473 "> ${lines.first}" : "* ${lines.first}"; |
| 415 lines = new List.from(lines.skip(1).map((line) => "| $line")); | 474 lines = new List.from(lines.skip(1).map((line) => "| $line")); |
| 416 lines.insertRange(0, 1, firstLine); | 475 lines.insertRange(0, 1, firstLine); |
| 417 return lines.join("\n"); | 476 return lines.join("\n"); |
| 418 }).join("\n"); | 477 }).join("\n"); |
| 419 } | 478 } |
| 420 } | 479 } |
| OLD | NEW |