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; |
11 | 11 |
12 import 'schedule_error.dart'; | 12 import 'schedule_error.dart'; |
13 import 'task.dart'; | 13 import 'task.dart'; |
14 | 14 |
15 /// The schedule of tasks to run for a single test. This has three separate task | 15 /// The schedule of tasks to run for a single test. This has three separate task |
16 /// queues: [tasks], [onComplete], and [onException]. It also provides | 16 /// queues: [tasks], [onComplete], and [onException]. It also provides |
17 /// visibility into the current state of the schedule. | 17 /// visibility into the current state of the schedule. |
18 class Schedule { | 18 class Schedule { |
19 /// The main task queue for the schedule. These tasks are run before the other | 19 /// The main task queue for the schedule. These tasks are run before the other |
20 /// queues and generally constitute the main test body. | 20 /// queues and generally constitute the main test body. |
21 TaskQueue get tasks => _tasks; | 21 TaskQueue get tasks => _tasks; |
22 TaskQueue _tasks; | 22 TaskQueue _tasks; |
23 | 23 |
24 /// The queue of tasks to run if an error is caught while running [tasks]. The | 24 /// The queue of tasks to run if an error is caught while running [tasks]. The |
25 /// error will be available in [errors]. These tasks won't be run if no error | 25 /// error will be available via [error]. These tasks won't be run if no error |
26 /// occurs. Note that expectation failures count as errors. | 26 /// occurs. Note that expectation failures count as errors. |
27 /// | 27 /// |
28 /// This queue runs before [onComplete], and errors in [onComplete] will not | 28 /// This queue runs before [onComplete], and errors in [onComplete] will not |
29 /// cause this queue to be run. | 29 /// cause this queue to be run. |
30 /// | 30 /// |
31 /// If an error occurs in a task in this queue, all further tasks will be | 31 /// If an error occurs in a task in this queue, all further tasks will be |
32 /// skipped. | 32 /// skipped. |
33 TaskQueue get onException => _onException; | 33 TaskQueue get onException => _onException; |
34 TaskQueue _onException; | 34 TaskQueue _onException; |
35 | 35 |
36 /// The queue of tasks to run after [tasks] and possibly [onException] have | 36 /// The queue of tasks to run after [tasks] and possibly [onException] have |
37 /// run. This queue will run whether or not an error occurred. If one did, it | 37 /// run. This queue will run whether or not an error occurred. If one did, it |
38 /// will be available in [errors]. Note that expectation failures count as | 38 /// will be available via [error]. Note that expectation failures count as |
39 /// errors. | 39 /// errors. |
40 /// | 40 /// |
41 /// This queue runs after [onException]. If an error occurs while running | 41 /// This queue runs after [onException]. If an error occurs while running |
42 /// [onException], that error will be available in [errors] after the original | 42 /// [onException], that error will be available via [error] in place of the |
43 /// error. | 43 /// original error. |
44 /// | 44 /// |
45 /// If an error occurs in a task in this queue, all further tasks will be | 45 /// If an error occurs in a task in this queue, all further tasks will be |
46 /// skipped. | 46 /// skipped. |
47 TaskQueue get onComplete => _onComplete; | 47 TaskQueue get onComplete => _onComplete; |
48 TaskQueue _onComplete; | 48 TaskQueue _onComplete; |
49 | 49 |
50 /// Returns the [Task] that's currently executing, or `null` if there is no | 50 /// Returns the [Task] that's currently executing, or `null` if there is no |
51 /// such task. This will be `null` both before the schedule starts running and | 51 /// such task. This will be `null` both before the schedule starts running and |
52 /// after it's finished. | 52 /// after it's finished. |
53 Task get currentTask => _currentTask; | 53 Task get currentTask => _currentTask; |
54 Task _currentTask; | 54 Task _currentTask; |
55 | 55 |
56 /// Whether the schedule has finished running. This is only set once | 56 /// Whether the schedule has finished running. This is only set once |
57 /// [onComplete] has finished running. It will be set whether or not an | 57 /// [onComplete] has finished running. It will be set whether or not an |
58 /// exception has occurred. | 58 /// exception has occurred. |
59 bool get done => _done; | 59 bool get done => _done; |
60 bool _done = false; | 60 bool _done = false; |
61 | 61 |
62 // TODO(nweiz): make this a read-only view once issue 8321 is fixed. | 62 /// The error thrown by the task queue. This will only be set while running |
63 | 63 /// [onException] and [onComplete], since an error in [tasks] will cause it to |
64 /// Errors thrown by the task queues. | 64 /// terminate immediately. |
65 /// | 65 ScheduleError get error => _error; |
66 /// When running tasks in [tasks], this will always be empty. If an error | 66 ScheduleError _error; |
67 /// 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 | |
69 /// 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 | |
71 /// afterwards. | |
72 /// | |
73 /// Any out-of-band callbacks that throw errors will also have those errors | |
74 /// added to this list. | |
75 final errors = <ScheduleError>[]; | |
76 | 67 |
77 /// The task queue that's currently being run, or `null` if there is no such | 68 /// The task queue that's currently being run, or `null` if there is no such |
78 /// queue. One of [tasks], [onException], or [onComplete]. This will be `null` | 69 /// queue. One of [tasks], [onException], or [onComplete]. This will be `null` |
79 /// before the schedule starts running. | 70 /// before the schedule starts running. |
80 TaskQueue get currentQueue => _done ? null : _currentQueue; | 71 TaskQueue get currentQueue => _done ? null : _currentQueue; |
81 TaskQueue _currentQueue; | 72 TaskQueue _currentQueue; |
82 | 73 |
83 /// The number of out-of-band callbacks that have been registered with | 74 /// The number of out-of-band callbacks that have been registered with |
84 /// [wrapAsync] but have yet to be called. | 75 /// [wrapAsync] but have yet to be called. |
85 int _pendingCallbacks = 0; | 76 int _pendingCallbacks = 0; |
(...skipping 15 matching lines...) Expand all Loading... |
101 Future run(void setUp()) { | 92 Future run(void setUp()) { |
102 return new Future.immediate(null).then((_) { | 93 return new Future.immediate(null).then((_) { |
103 try { | 94 try { |
104 setUp(); | 95 setUp(); |
105 } catch (e, stackTrace) { | 96 } catch (e, stackTrace) { |
106 throw new ScheduleError.from(this, e, stackTrace: stackTrace); | 97 throw new ScheduleError.from(this, e, stackTrace: stackTrace); |
107 } | 98 } |
108 | 99 |
109 return tasks._run(); | 100 return tasks._run(); |
110 }).catchError((e) { | 101 }).catchError((e) { |
111 errors.add(e); | 102 _error = e; |
112 return onException._run().catchError((innerError) { | 103 return onException._run().then((_) { |
113 // If an error occurs in a task in the onException queue, make sure it's | |
114 // registered in the error list and re-throw it. We could also re-throw | |
115 // `e`; ultimately, all the errors will be shown to the user if any | |
116 // ScheduleError is thrown. | |
117 errors.add(innerError); | |
118 throw innerError; | |
119 }).then((_) { | |
120 // If there are no errors in the onException queue, re-throw the | |
121 // original error that caused it to run. | |
122 throw e; | 104 throw e; |
123 }); | 105 }); |
124 }).whenComplete(() { | 106 }).whenComplete(() => onComplete._run()).whenComplete(() { |
125 return onComplete._run().catchError((e) { | |
126 // If an error occurs in a task in the onComplete queue, make sure it's | |
127 // registered in the error list and re-throw it. | |
128 errors.add(e); | |
129 throw e; | |
130 }); | |
131 }).whenComplete(() { | |
132 _done = true; | 107 _done = true; |
133 }); | 108 }); |
134 } | 109 } |
135 | 110 |
136 /// Signals that an out-of-band error has occurred. Using [wrapAsync] along | 111 /// Signals that an out-of-band error has occurred. Using [wrapAsync] along |
137 /// with `throw` is usually preferable to calling this directly. | 112 /// with `throw` is usually preferable to calling this directly. |
138 /// | 113 /// |
139 /// The metadata in [AsyncError]s and [ScheduleError]s will be preserved. | 114 /// The metadata in [AsyncError]s and [ScheduleError]s will be preserved. |
140 void signalError(error, [stackTrace]) { | 115 void signalError(error, [stackTrace]) { |
141 var scheduleError = new ScheduleError.from(this, error, | 116 var scheduleError = new ScheduleError.from(this, error, |
142 stackTrace: stackTrace, task: currentTask); | 117 stackTrace: stackTrace, task: currentTask); |
143 if (_done) { | 118 if (_done) { |
144 errors.add(scheduleError); | |
145 throw new StateError( | 119 throw new StateError( |
146 "An out-of-band error was signaled outside of wrapAsync after the " | 120 "An out-of-band error was signaled outside of wrapAsync after the " |
147 "schedule finished running.\n" | 121 "schedule finished running:" |
148 "${errorString()}"); | 122 "${prefixLines(scheduleError.toString())}"); |
149 } else if (currentQueue == null) { | 123 } else if (currentQueue == null) { |
150 // If we're not done but there's no current queue, that means we haven't | 124 // If we're not done but there's no current queue, that means we haven't |
151 // started yet and thus we're in setUp or the synchronous body of the | 125 // started yet and thus we're in setUp or the synchronous body of the |
152 // function. Throwing the error will thus pipe it into the main | 126 // function. Throwing the error will thus pipe it into the main |
153 // error-handling code. | 127 // error-handling code. |
154 throw scheduleError; | 128 throw scheduleError; |
155 } else { | 129 } else { |
156 _currentQueue._signalError(scheduleError); | 130 _currentQueue._signalError(scheduleError); |
157 } | 131 } |
158 } | 132 } |
(...skipping 20 matching lines...) Expand all Loading... |
179 } finally { | 153 } finally { |
180 _pendingCallbacks--; | 154 _pendingCallbacks--; |
181 if (_pendingCallbacks == 0 && _noPendingCallbacks != null) { | 155 if (_pendingCallbacks == 0 && _noPendingCallbacks != null) { |
182 _noPendingCallbacks.complete(); | 156 _noPendingCallbacks.complete(); |
183 _noPendingCallbacks = null; | 157 _noPendingCallbacks = null; |
184 } | 158 } |
185 } | 159 } |
186 }; | 160 }; |
187 } | 161 } |
188 | 162 |
189 /// Returns a string representation of all errors registered on this schedule. | |
190 String errorString() { | |
191 if (errors.isEmpty) return "The schedule had no errors."; | |
192 if (errors.length == 1) return errors.first.toString(); | |
193 var errorStrings = errors.map((e) => e.toString()).join("\n================" | |
194 "================================================================\n"); | |
195 return "The schedule had ${errors.length} errors:\n$errorStrings"; | |
196 } | |
197 | |
198 /// Returns a [Future] that will complete once there are no pending | 163 /// Returns a [Future] that will complete once there are no pending |
199 /// out-of-band callbacks. | 164 /// out-of-band callbacks. |
200 Future _awaitNoPendingCallbacks() { | 165 Future _awaitNoPendingCallbacks() { |
201 if (_pendingCallbacks == 0) return new Future.immediate(null); | 166 if (_pendingCallbacks == 0) return new Future.immediate(null); |
202 if (_noPendingCallbacks == null) _noPendingCallbacks = new Completer(); | 167 if (_noPendingCallbacks == null) _noPendingCallbacks = new Completer(); |
203 return _noPendingCallbacks.future; | 168 return _noPendingCallbacks.future; |
204 } | 169 } |
205 } | 170 } |
206 | 171 |
207 /// A queue of asynchronous tasks to execute in order. | 172 /// A queue of asynchronous tasks to execute in order. |
(...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
240 return task.result; | 205 return task.result; |
241 } | 206 } |
242 | 207 |
243 /// Runs all the tasks in this queue in order. | 208 /// Runs all the tasks in this queue in order. |
244 Future _run() { | 209 Future _run() { |
245 _schedule._currentQueue = this; | 210 _schedule._currentQueue = this; |
246 return Future.forEach(_contents, (task) { | 211 return Future.forEach(_contents, (task) { |
247 _schedule._currentTask = task; | 212 _schedule._currentTask = task; |
248 if (_error != null) throw _error; | 213 if (_error != null) throw _error; |
249 return task.fn().catchError((e) { | 214 return task.fn().catchError((e) { |
250 if (_error != null) _schedule.errors.add(_error); | |
251 throw new ScheduleError.from(_schedule, e, task: task); | 215 throw new ScheduleError.from(_schedule, e, task: task); |
252 }); | 216 }); |
253 }).whenComplete(() { | 217 }).whenComplete(() { |
254 _schedule._currentTask = null; | 218 _schedule._currentTask = null; |
255 return _schedule._awaitNoPendingCallbacks(); | 219 return _schedule._awaitNoPendingCallbacks(); |
256 }).then((_) { | 220 }).then((_) { |
257 if (_error != null) throw _error; | 221 if (_error != null) throw _error; |
258 }); | 222 }); |
259 } | 223 } |
260 | 224 |
261 /// Signals that an out-of-band error has been detected and the queue should | 225 /// Signals that an out-of-band error has been detected and the queue should |
262 /// stop running as soon as possible. | 226 /// stop running as soon as possible. |
263 void _signalError(ScheduleError error) { | 227 void _signalError(ScheduleError error) { |
264 // If multiple errors are detected while a task is running, make sure the | |
265 // earlier ones are recorded in the schedule. | |
266 if (_error != null) _schedule.errors.add(_error); | |
267 _error = error; | 228 _error = error; |
268 } | 229 } |
269 | 230 |
270 String toString() => name; | 231 String toString() => name; |
271 | 232 |
272 /// Returns a detailed representation of the queue as a tree of tasks. If | 233 /// Returns a detailed representation of the queue as a tree of tasks. If |
273 /// [highlight] is passed, that task is specially highlighted. | 234 /// [highlight] is passed, that task is specially highlighted. |
274 /// | 235 /// |
275 /// [highlight] must be a task in this queue. | 236 /// [highlight] must be a task in this queue. |
276 String generateTree([Task highlight]) { | 237 String generateTree([Task highlight]) { |
277 assert(highlight == null || highlight.queue == this); | 238 assert(highlight == null || highlight.queue == this); |
278 return _contents.map((task) { | 239 return _contents.map((task) { |
279 var lines = task.toString().split("\n"); | 240 var lines = task.toString().split("\n"); |
280 var firstLine = task == highlight ? | 241 var firstLine = task == highlight ? |
281 "> ${lines.first}" : "* ${lines.first}"; | 242 "> ${lines.first}" : "* ${lines.first}"; |
282 lines = new List.from(lines.skip(1).map((line) => "| $line")); | 243 lines = new List.from(lines.skip(1).map((line) => "| $line")); |
283 lines.insertRange(0, 1, firstLine); | 244 lines.insertRange(0, 1, firstLine); |
284 return lines.join("\n"); | 245 return lines.join("\n"); |
285 }).join("\n"); | 246 }).join("\n"); |
286 } | 247 } |
287 } | 248 } |
OLD | NEW |