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

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

Issue 12251012: Add built-in timeouts to scheduled_test. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Code review changes Created 7 years, 10 months 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
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 'mock_clock.dart' as mock_clock;
12 import 'schedule_error.dart'; 13 import 'schedule_error.dart';
14 import 'substitute_future.dart';
13 import 'task.dart'; 15 import 'task.dart';
14 16
15 /// The schedule of tasks to run for a single test. This has three separate task 17 /// The schedule of tasks to run for a single test. This has three separate task
16 /// queues: [tasks], [onComplete], and [onException]. It also provides 18 /// queues: [tasks], [onComplete], and [onException]. It also provides
17 /// visibility into the current state of the schedule. 19 /// visibility into the current state of the schedule.
18 class Schedule { 20 class Schedule {
19 /// The main task queue for the schedule. These tasks are run before the other 21 /// The main task queue for the schedule. These tasks are run before the other
20 /// queues and generally constitute the main test body. 22 /// queues and generally constitute the main test body.
21 TaskQueue get tasks => _tasks; 23 TaskQueue get tasks => _tasks;
22 TaskQueue _tasks; 24 TaskQueue _tasks;
(...skipping 23 matching lines...) Expand all
46 /// skipped. 48 /// skipped.
47 TaskQueue get onComplete => _onComplete; 49 TaskQueue get onComplete => _onComplete;
48 TaskQueue _onComplete; 50 TaskQueue _onComplete;
49 51
50 /// Returns the [Task] that's currently executing, or `null` if there is no 52 /// 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 53 /// such task. This will be `null` both before the schedule starts running and
52 /// after it's finished. 54 /// after it's finished.
53 Task get currentTask => _currentTask; 55 Task get currentTask => _currentTask;
54 Task _currentTask; 56 Task _currentTask;
55 57
56 /// Whether the schedule has finished running. This is only set once 58 /// The current state of the schedule.
57 /// [onComplete] has finished running. It will be set whether or not an 59 ScheduleState get state => _state;
58 /// exception has occurred. 60 ScheduleState _state = ScheduleState.SET_UP;
59 bool get done => _done;
60 bool _done = false;
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 63
64 /// Errors thrown by the task queues. 64 /// Errors thrown by the task queues.
65 /// 65 ///
66 /// When running tasks in [tasks], this will always be empty. If an error 66 /// 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] 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 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 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 70 /// also be added to this list, although no scheduled tasks will be run
71 /// afterwards. 71 /// afterwards.
72 /// 72 ///
73 /// Any out-of-band callbacks that throw errors will also have those errors 73 /// Any out-of-band callbacks that throw errors will also have those errors
74 /// added to this list. 74 /// added to this list.
75 final errors = <ScheduleError>[]; 75 final errors = <ScheduleError>[];
76 76
77 /// The task queue that's currently being run, or `null` if there is no such 77 /// The task queue that's currently being run. One of [tasks], [onException],
78 /// queue. One of [tasks], [onException], or [onComplete]. This will be `null` 78 /// or [onComplete]. This starts as [tasks], and can only be `null` after the
79 /// before the schedule starts running. 79 /// schedule is done.
80 TaskQueue get currentQueue => _done ? null : _currentQueue; 80 TaskQueue get currentQueue =>
81 _state == ScheduleState.DONE ? null : _currentQueue;
81 TaskQueue _currentQueue; 82 TaskQueue _currentQueue;
82 83
84 /// 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.
86 ///
87 /// 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
89 /// out, that can only be handled in [onComplete]; if [onComplete] times out,
90 /// that cannot be handled.
91 ///
92 /// 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.
94 Duration get timeout => _timeout;
95 Duration _timeout = new Duration(seconds: 30);
96 set timeout(Duration duration) {
97 _timeout = duration;
98 heartbeat();
99 }
100
83 /// The number of out-of-band callbacks that have been registered with 101 /// The number of out-of-band callbacks that have been registered with
84 /// [wrapAsync] but have yet to be called. 102 /// [wrapAsync] but have yet to be called.
85 int _pendingCallbacks = 0; 103 int _pendingCallbacks = 0;
86 104
87 /// A completer that will be completed once [_pendingCallbacks] reaches zero. 105 /// A completer that will be completed once [_pendingCallbacks] reaches zero.
88 /// This will only be non-`null` if [_awaitPendingCallbacks] has been called 106 /// This will only be non-`null` if [_awaitPendingCallbacks] has been called
89 /// while [_pendingCallbacks] is non-zero. 107 /// while [_pendingCallbacks] is non-zero.
90 Completer _noPendingCallbacks; 108 Completer _noPendingCallbacks;
91 109
110 /// The timer for keeping track of task timeouts. This may be null.
111 Timer _timeoutTimer;
112
92 /// Creates a new schedule with empty task queues. 113 /// Creates a new schedule with empty task queues.
93 Schedule() { 114 Schedule() {
94 _tasks = new TaskQueue._("tasks", this); 115 _tasks = new TaskQueue._("tasks", this);
95 _onComplete = new TaskQueue._("onComplete", this); 116 _onComplete = new TaskQueue._("onComplete", this);
96 _onException = new TaskQueue._("onException", this); 117 _onException = new TaskQueue._("onException", this);
118 _currentQueue = _tasks;
119
120 heartbeat();
97 } 121 }
98 122
99 /// 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
100 /// order. Any errors in [setUp] will cause [onException] to run. 124 /// order. Any errors in [setUp] will cause [onException] to run.
101 Future run(void setUp()) { 125 Future run(void setUp()) {
102 return new Future.immediate(null).then((_) { 126 return new Future.immediate(null).then((_) {
103 try { 127 try {
104 setUp(); 128 setUp();
105 } catch (e, stackTrace) { 129 } catch (e, stackTrace) {
106 throw new ScheduleError.from(this, e, stackTrace: stackTrace); 130 throw new ScheduleError.from(this, e, stackTrace: stackTrace);
107 } 131 }
108 132
133 _state = ScheduleState.RUNNING;
109 return tasks._run(); 134 return tasks._run();
110 }).catchError((e) { 135 }).catchError((e) {
111 errors.add(e); 136 errors.add(e);
112 return onException._run().catchError((innerError) { 137 return onException._run().catchError((innerError) {
113 // If an error occurs in a task in the onException queue, make sure it's 138 // 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 139 // 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 140 // `e`; ultimately, all the errors will be shown to the user if any
116 // ScheduleError is thrown. 141 // ScheduleError is thrown.
117 errors.add(innerError); 142 errors.add(innerError);
118 throw innerError; 143 throw innerError;
119 }).then((_) { 144 }).then((_) {
120 // If there are no errors in the onException queue, re-throw the 145 // If there are no errors in the onException queue, re-throw the
121 // original error that caused it to run. 146 // original error that caused it to run.
122 throw e; 147 throw e;
123 }); 148 });
124 }).whenComplete(() { 149 }).whenComplete(() {
125 return onComplete._run().catchError((e) { 150 return onComplete._run().catchError((e) {
126 // If an error occurs in a task in the onComplete queue, make sure it's 151 // 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. 152 // registered in the error list and re-throw it.
128 errors.add(e); 153 errors.add(e);
129 throw e; 154 throw e;
130 }); 155 });
131 }).whenComplete(() { 156 }).whenComplete(() {
132 _done = true; 157 if (_timeoutTimer != null) _timeoutTimer.cancel();
158 _state = ScheduleState.DONE;
133 }); 159 });
134 } 160 }
135 161
136 /// Signals that an out-of-band error has occurred. Using [wrapAsync] along 162 /// Signals that an out-of-band error has occurred. Using [wrapAsync] along
137 /// with `throw` is usually preferable to calling this directly. 163 /// with `throw` is usually preferable to calling this directly.
138 /// 164 ///
139 /// The metadata in [AsyncError]s and [ScheduleError]s will be preserved. 165 /// The metadata in [AsyncError]s and [ScheduleError]s will be preserved.
140 void signalError(error, [stackTrace]) { 166 void signalError(error, [stackTrace]) {
167 heartbeat();
168
141 var scheduleError = new ScheduleError.from(this, error, 169 var scheduleError = new ScheduleError.from(this, error,
142 stackTrace: stackTrace, task: currentTask); 170 stackTrace: stackTrace);
143 if (_done) { 171 if (_state == ScheduleState.DONE) {
144 errors.add(scheduleError);
145 throw new StateError( 172 throw new StateError(
146 "An out-of-band error was signaled outside of wrapAsync after the " 173 "An out-of-band error was signaled outside of wrapAsync after the "
147 "schedule finished running.\n" 174 "schedule finished running.\n"
148 "${errorString()}"); 175 "${errorString()}");
149 } else if (currentQueue == null) { 176 } else if (state == ScheduleState.SET_UP) {
150 // If we're not done but there's no current queue, that means we haven't 177 // If we're setting up, throwing the error will pipe it into the main
151 // 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
153 // error-handling code. 178 // error-handling code.
154 throw scheduleError; 179 throw scheduleError;
155 } else { 180 } else {
156 _currentQueue._signalError(scheduleError); 181 _currentQueue._signalError(scheduleError);
157 } 182 }
158 } 183 }
159 184
185 /// 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
187 /// 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.
189 void _signalPostTimeoutError(error, [stackTrace]) {
190 var scheduleError = new ScheduleError.from(this, error,
191 stackTrace: stackTrace);
192 errors.add(scheduleError);
193 if (_state == ScheduleState.DONE) {
194 throw new StateError(
195 "An out-of-band error was caught after the test timed out.\n"
196 "${errorString()}");
197 }
198 }
199
160 /// Returns a function wrapping [fn] that pipes any errors into the schedule 200 /// Returns a function wrapping [fn] that pipes any errors into the schedule
161 /// chain. This will also block the current task queue from completing until 201 /// chain. This will also block the current task queue from completing until
162 /// the returned function has been called. It's used to ensure that 202 /// the returned function has been called. It's used to ensure that
163 /// out-of-band callbacks are properly handled by the scheduled test. 203 /// out-of-band callbacks are properly handled by the scheduled test.
164 /// 204 ///
165 /// The top-level `wrapAsync` function should usually be used in preference to 205 /// The top-level `wrapAsync` function should usually be used in preference to
166 /// this. 206 /// this.
167 Function wrapAsync(fn(arg)) { 207 Function wrapAsync(fn(arg)) {
168 if (_done) { 208 if (_state == ScheduleState.DONE) {
169 throw new StateError("wrapAsync called after the schedule has finished " 209 throw new StateError("wrapAsync called after the schedule has finished "
170 "running."); 210 "running.");
171 } 211 }
212 heartbeat();
213
214 var queue = currentQueue;
215 // It's possible that the queue timed out before this.
216 bool _timedOut() => queue != currentQueue || _pendingCallbacks == 0;
172 217
173 _pendingCallbacks++; 218 _pendingCallbacks++;
174 return (arg) { 219 return (arg) {
175 try { 220 try {
176 return fn(arg); 221 return fn(arg);
177 } catch (e, stackTrace) { 222 } catch (e, stackTrace) {
178 signalError(e, stackTrace); 223 if (_timedOut()) {
224 _signalPostTimeoutError(e, stackTrace);
225 } else {
226 signalError(e, stackTrace);
227 }
179 } finally { 228 } finally {
229 if (_timedOut()) return;
230
180 _pendingCallbacks--; 231 _pendingCallbacks--;
181 if (_pendingCallbacks == 0 && _noPendingCallbacks != null) { 232 if (_pendingCallbacks == 0 && _noPendingCallbacks != null) {
182 _noPendingCallbacks.complete(); 233 _noPendingCallbacks.complete();
183 _noPendingCallbacks = null; 234 _noPendingCallbacks = null;
184 } 235 }
185 } 236 }
186 }; 237 };
187 } 238 }
188 239
189 /// Returns a string representation of all errors registered on this schedule. 240 /// Returns a string representation of all errors registered on this schedule.
190 String errorString() { 241 String errorString() {
191 if (errors.isEmpty) return "The schedule had no errors."; 242 if (errors.isEmpty) return "The schedule had no errors.";
192 if (errors.length == 1) return errors.first.toString(); 243 if (errors.length == 1) return errors.first.toString();
193 var errorStrings = errors.map((e) => e.toString()).join("\n================" 244 var errorStrings = errors.map((e) => e.toString()).join("\n================"
194 "================================================================\n"); 245 "================================================================\n");
195 return "The schedule had ${errors.length} errors:\n$errorStrings"; 246 return "The schedule had ${errors.length} errors:\n$errorStrings";
196 } 247 }
197 248
249 /// Notifies the schedule that progress is being made on an asynchronous task.
250 /// This resets the timeout timer, and can be used in long-running tasks to
251 /// keep them from timing out.
252 void heartbeat() {
253 if (_timeoutTimer != null) _timeoutTimer.cancel();
254 if (_timeout == null) {
255 _timeoutTimer = null;
256 } else {
257 _timeoutTimer = mock_clock.newTimer(_timeout, _signalTimeout);
258 }
259 }
260
261 /// The callback to run when the timeout timer fires. Notifies the current
262 /// queue that a timeout has occurred.
263 void _signalTimeout(_) {
264 // Reset the timer so that we can detect timeouts in the onException and
265 // onComplete queues.
266 _timeoutTimer = null;
267
268 var error = new ScheduleError.from(this, "The schedule timed out after "
269 "$_timeout of inactivity.");
270
271 _pendingCallbacks = 0;
272 if (_noPendingCallbacks != null) {
273 var noPendingCallbacks = _noPendingCallbacks;
274 _noPendingCallbacks = null;
275 noPendingCallbacks.completeError(error);
276 } else {
277 currentQueue._signalTimeout(error);
278 }
279 }
280
198 /// Returns a [Future] that will complete once there are no pending 281 /// Returns a [Future] that will complete once there are no pending
199 /// out-of-band callbacks. 282 /// out-of-band callbacks.
200 Future _awaitNoPendingCallbacks() { 283 Future _awaitNoPendingCallbacks() {
201 if (_pendingCallbacks == 0) return new Future.immediate(null); 284 if (_pendingCallbacks == 0) return new Future.immediate(null);
202 if (_noPendingCallbacks == null) _noPendingCallbacks = new Completer(); 285 if (_noPendingCallbacks == null) _noPendingCallbacks = new Completer();
203 return _noPendingCallbacks.future; 286 return _noPendingCallbacks.future;
204 } 287 }
205 } 288 }
206 289
290 /// An enum of states for a [Schedule].
291 class ScheduleState {
292 /// The schedule can have tasks added to its queue, but is not yet running
293 /// them.
294 static const SET_UP = const ScheduleState._("SET_UP");
295
296 /// The schedule is actively running tasks. This includes running tasks in
297 /// [Schedule.onException] and [Schedule.onComplete].
298 static const RUNNING = const ScheduleState._("RUNNING");
299
300 /// The schedule has finished running all its tasks, either successfully or
301 /// with an error.
302 static const DONE = const ScheduleState._("DONE");
303
304 /// The name of the state.
305 final String name;
306
307 const ScheduleState._(this.name);
308
309 String toString() => name;
310 }
311
207 /// A queue of asynchronous tasks to execute in order. 312 /// A queue of asynchronous tasks to execute in order.
208 class TaskQueue { 313 class TaskQueue {
209 // TODO(nweiz): make this a read-only view when issue 8321 is fixed. 314 // TODO(nweiz): make this a read-only view when issue 8321 is fixed.
210 /// The tasks in the queue. 315 /// The tasks in the queue.
211 Collection<Task> get contents => _contents; 316 Collection<Task> get contents => _contents;
212 final _contents = new Queue<Task>(); 317 final _contents = new Queue<Task>();
213 318
214 /// The name of the queue, for debugging purposes. 319 /// The name of the queue, for debugging purposes.
215 final String name; 320 final String name;
216 321
217 /// The [Schedule] that created this queue. 322 /// The [Schedule] that created this queue.
218 final Schedule _schedule; 323 final Schedule _schedule;
219 324
220 /// An out-of-band error signaled by [_schedule]. If this is non-null, it 325 /// An out-of-band error signaled by [_schedule]. If this is non-null, it
221 /// indicates that the queue should stop as soon as possible and re-throw this 326 /// indicates that the queue should stop as soon as possible and re-throw this
222 /// error. 327 /// error.
223 ScheduleError _error; 328 ScheduleError _error;
224 329
330 /// The [SubstituteFuture] for the currently-running task in the queue, or
331 /// null if no task is currently running.
332 SubstituteFuture _taskFuture;
333
225 TaskQueue._(this.name, this._schedule); 334 TaskQueue._(this.name, this._schedule);
226 335
227 /// Schedules a task, [fn], to run asynchronously as part of this queue. Tasks 336 /// Schedules a task, [fn], to run asynchronously as part of this queue. Tasks
228 /// will be run in the order they're scheduled. In [fn] returns a [Future], 337 /// will be run in the order they're scheduled. In [fn] returns a [Future],
229 /// tasks after it won't be run until that [Future] completes. 338 /// tasks after it won't be run until that [Future] completes.
230 /// 339 ///
231 /// The return value will be completed once the scheduled task has finished 340 /// The return value will be completed once the scheduled task has finished
232 /// running. Its return value is the same as the return value of [fn], or the 341 /// running. Its return value is the same as the return value of [fn], or the
233 /// value it completes to if it's a [Future]. 342 /// value it completes to if it's a [Future].
234 /// 343 ///
235 /// If [description] is passed, it's used to describe the task for debugging 344 /// If [description] is passed, it's used to describe the task for debugging
236 /// purposes when an error occurs. 345 /// purposes when an error occurs.
237 Future schedule(fn(), [String description]) { 346 Future schedule(fn(), [String description]) {
238 var task = new Task(fn, this, description); 347 var task = new Task(fn, this, description);
239 _contents.add(task); 348 _contents.add(task);
240 return task.result; 349 return task.result;
241 } 350 }
242 351
243 /// Runs all the tasks in this queue in order. 352 /// Runs all the tasks in this queue in order.
244 Future _run() { 353 Future _run() {
245 _schedule._currentQueue = this; 354 _schedule._currentQueue = this;
355 _schedule.heartbeat();
246 return Future.forEach(_contents, (task) { 356 return Future.forEach(_contents, (task) {
247 _schedule._currentTask = task; 357 _schedule._currentTask = task;
248 if (_error != null) throw _error; 358 if (_error != null) throw _error;
249 return task.fn().catchError((e) { 359
360 _taskFuture = new SubstituteFuture(task.fn());
361 return _taskFuture.whenComplete(() {
362 _taskFuture = null;
363 _schedule.heartbeat();
364 }).catchError((e) {
250 if (_error != null) _schedule.errors.add(_error); 365 if (_error != null) _schedule.errors.add(_error);
251 throw new ScheduleError.from(_schedule, e, task: task); 366 throw new ScheduleError.from(_schedule, e);
252 }); 367 });
253 }).whenComplete(() { 368 }).whenComplete(() {
254 _schedule._currentTask = null; 369 _schedule._currentTask = null;
255 return _schedule._awaitNoPendingCallbacks(); 370 return _schedule._awaitNoPendingCallbacks();
256 }).then((_) { 371 }).then((_) {
372 _schedule.heartbeat();
257 if (_error != null) throw _error; 373 if (_error != null) throw _error;
258 }); 374 });
259 } 375 }
260 376
261 /// Signals that an out-of-band error has been detected and the queue should 377 /// Signals that an out-of-band error has been detected and the queue should
262 /// stop running as soon as possible. 378 /// stop running as soon as possible.
263 void _signalError(ScheduleError error) { 379 void _signalError(ScheduleError error) {
264 // If multiple errors are detected while a task is running, make sure the 380 // If multiple errors are detected while a task is running, make sure the
265 // earlier ones are recorded in the schedule. 381 // earlier ones are recorded in the schedule.
266 if (_error != null) _schedule.errors.add(_error); 382 if (_error != null) _schedule.errors.add(_error);
267 _error = error; 383 _error = error;
268 } 384 }
269 385
386 /// Notifies the queue that it has timed out and it needs to terminate
387 /// immediately with a timeout error.
388 void _signalTimeout(ScheduleError error) {
389 if (_taskFuture != null) {
390 // Catch errors coming off the old task future, in case it completes after
391 // timing out.
392 _taskFuture.substitute(new Future.immediateError(error)).catchError((e) {
393 _schedule._signalPostTimeoutError(e);
394 });
395 } else {
396 // This branch probably won't be reached, but it's conceivable that the
397 // event loop might get pumped when _taskFuture is null but we haven't yet
398 // called _awaitNoPendingCallbacks.
399 _signalError(error);
400 }
401 }
402
270 String toString() => name; 403 String toString() => name;
271 404
272 /// Returns a detailed representation of the queue as a tree of tasks. If 405 /// Returns a detailed representation of the queue as a tree of tasks. If
273 /// [highlight] is passed, that task is specially highlighted. 406 /// [highlight] is passed, that task is specially highlighted.
274 /// 407 ///
275 /// [highlight] must be a task in this queue. 408 /// [highlight] must be a task in this queue.
276 String generateTree([Task highlight]) { 409 String generateTree([Task highlight]) {
277 assert(highlight == null || highlight.queue == this); 410 assert(highlight == null || highlight.queue == this);
278 return _contents.map((task) { 411 return _contents.map((task) {
279 var lines = task.toString().split("\n"); 412 var lines = task.toString().split("\n");
280 var firstLine = task == highlight ? 413 var firstLine = task == highlight ?
281 "> ${lines.first}" : "* ${lines.first}"; 414 "> ${lines.first}" : "* ${lines.first}";
282 lines = new List.from(lines.skip(1).map((line) => "| $line")); 415 lines = new List.from(lines.skip(1).map((line) => "| $line"));
283 lines.insertRange(0, 1, firstLine); 416 lines.insertRange(0, 1, firstLine);
284 return lines.join("\n"); 417 return lines.join("\n");
285 }).join("\n"); 418 }).join("\n");
286 } 419 }
287 } 420 }
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