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

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

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