Chromium Code Reviews| 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 83 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 94 /// | 94 /// |
| 95 /// 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 |
| 96 /// cannot be handled. The user will still be notified of it. | 96 /// cannot be handled. The user will still be notified of it. |
| 97 Duration get timeout => _timeout; | 97 Duration get timeout => _timeout; |
| 98 Duration _timeout = new Duration(seconds: 30); | 98 Duration _timeout = new Duration(seconds: 30); |
| 99 set timeout(Duration duration) { | 99 set timeout(Duration duration) { |
| 100 _timeout = duration; | 100 _timeout = duration; |
| 101 heartbeat(); | 101 heartbeat(); |
| 102 } | 102 } |
| 103 | 103 |
| 104 /// The number of out-of-band callbacks that have been registered with | |
| 105 /// [wrapAsync] but have yet to be called. | |
| 106 int _pendingCallbacks = 0; | |
| 107 | |
| 108 /// A completer that will be completed once [_pendingCallbacks] reaches zero. | |
| 109 /// This will only be non-`null` if [_awaitPendingCallbacks] has been called | |
| 110 /// while [_pendingCallbacks] is non-zero. | |
| 111 Completer _noPendingCallbacks; | |
| 112 | |
| 113 /// The timer for keeping track of task timeouts. This may be null. | 104 /// The timer for keeping track of task timeouts. This may be null. |
| 114 Timer _timeoutTimer; | 105 Timer _timeoutTimer; |
| 115 | 106 |
| 116 /// Creates a new schedule with empty task queues. | 107 /// Creates a new schedule with empty task queues. |
| 117 Schedule() { | 108 Schedule() { |
| 118 _tasks = new TaskQueue._("tasks", this); | 109 _tasks = new TaskQueue._("tasks", this); |
| 119 _onComplete = new TaskQueue._("onComplete", this); | 110 _onComplete = new TaskQueue._("onComplete", this); |
| 120 _onException = new TaskQueue._("onException", this); | 111 _onException = new TaskQueue._("onException", this); |
| 121 _currentQueue = _tasks; | 112 _currentQueue = _tasks; |
| 122 | 113 |
| (...skipping 84 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 207 "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" |
| 208 "${errorString()}"); | 199 "${errorString()}"); |
| 209 } | 200 } |
| 210 } | 201 } |
| 211 | 202 |
| 212 /// 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 |
| 213 /// 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 |
| 214 /// 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 |
| 215 /// out-of-band callbacks are properly handled by the scheduled test. | 206 /// out-of-band callbacks are properly handled by the scheduled test. |
| 216 /// | 207 /// |
| 208 /// [description] provides an optional description of the callback, which is | |
| 209 /// used when generating error messages. | |
| 210 /// | |
| 217 /// The top-level `wrapAsync` function should usually be used in preference to | 211 /// The top-level `wrapAsync` function should usually be used in preference to |
| 218 /// this in test code. | 212 /// this in test code. |
| 219 Function wrapAsync(fn(arg)) { | 213 Function wrapAsync(fn(arg), [String description]) { |
| 220 if (_state == ScheduleState.DONE) { | 214 if (_state == ScheduleState.DONE) { |
| 221 throw new StateError("wrapAsync called after the schedule has finished " | 215 throw new StateError("wrapAsync called after the schedule has finished " |
| 222 "running."); | 216 "running."); |
| 223 } | 217 } |
| 224 heartbeat(); | 218 heartbeat(); |
| 225 | 219 |
| 226 var queue = currentQueue; | 220 return currentQueue._wrapAsync(fn, description); |
| 227 // It's possible that the queue timed out before this. | |
| 228 bool _timedOut() => queue != currentQueue || _pendingCallbacks == 0; | |
| 229 | |
| 230 _pendingCallbacks++; | |
| 231 return (arg) { | |
| 232 try { | |
| 233 return fn(arg); | |
| 234 } catch (e, stackTrace) { | |
| 235 if (_timedOut()) { | |
| 236 _signalPostTimeoutError(e, stackTrace); | |
| 237 } else { | |
| 238 signalError(e, stackTrace); | |
| 239 } | |
| 240 } finally { | |
| 241 if (_timedOut()) return; | |
| 242 | |
| 243 _pendingCallbacks--; | |
| 244 if (_pendingCallbacks == 0 && _noPendingCallbacks != null) { | |
| 245 _noPendingCallbacks.complete(); | |
| 246 _noPendingCallbacks = null; | |
| 247 } | |
| 248 } | |
| 249 }; | |
| 250 } | 221 } |
| 251 | 222 |
| 252 /// Like [wrapAsync], this ensures that the current task queue waits for | 223 /// Like [wrapAsync], this ensures that the current task queue waits for |
| 253 /// out-of-band asynchronous code, and that errors raised in that code are | 224 /// out-of-band asynchronous code, and that errors raised in that code are |
| 254 /// handled correctly. However, [wrapFuture] wraps a [Future] chain rather | 225 /// handled correctly. However, [wrapFuture] wraps a [Future] chain rather |
| 255 /// than a single callback. | 226 /// than a single callback. |
| 256 /// | 227 /// |
| 257 /// The returned [Future] completes to the same value or error as [future]. | 228 /// The returned [Future] completes to the same value or error as [future]. |
| 258 /// | 229 /// |
| 230 /// [description] provides an optional description of the future, which is | |
| 231 /// used when generating error messages. | |
| 232 /// | |
| 259 /// The top-level `wrapFuture` function should usually be used in preference | 233 /// The top-level `wrapFuture` function should usually be used in preference |
| 260 /// to this in test code. | 234 /// to this in test code. |
| 261 Future wrapFuture(Future future) { | 235 Future wrapFuture(Future future, [String description]) { |
| 262 var doneCallback = wrapAsync((_) => null); | 236 var done = wrapAsync((fn) => fn(), description); |
| 263 done() => new Future.immediate(null).then(doneCallback); | |
| 264 | 237 |
| 265 future = future.then((result) { | 238 future = future.then((result) => done(() => result)).catchError((e) { |
| 266 done(); | 239 done(() { |
| 267 return result; | 240 throw e; |
| 268 }).catchError((e) { | 241 }); |
| 269 signalError(e); | 242 // wrapAsync will catch the first throw, so we throw [e] again so it |
| 270 done(); | 243 // propagates through the Future chain. |
| 271 throw e; | 244 throw e; |
| 272 }); | 245 }); |
| 273 | 246 |
| 274 // Don't top-level the error, since it's already been signaled to the | 247 // Don't top-level the error, since it's already been signaled to the |
| 275 // schedule. | 248 // schedule. |
| 276 future.catchError((_) => null); | 249 future.catchError((_) => null); |
| 277 | 250 |
| 278 return future; | 251 return future; |
| 279 } | 252 } |
| 280 | 253 |
| (...skipping 15 matching lines...) Expand all Loading... | |
| 296 } | 269 } |
| 297 | 270 |
| 298 /// Notifies the schedule that progress is being made on an asynchronous task. | 271 /// Notifies the schedule that progress is being made on an asynchronous task. |
| 299 /// This resets the timeout timer, and can be used in long-running tasks to | 272 /// This resets the timeout timer, and can be used in long-running tasks to |
| 300 /// keep them from timing out. | 273 /// keep them from timing out. |
| 301 void heartbeat() { | 274 void heartbeat() { |
| 302 if (_timeoutTimer != null) _timeoutTimer.cancel(); | 275 if (_timeoutTimer != null) _timeoutTimer.cancel(); |
| 303 if (_timeout == null) { | 276 if (_timeout == null) { |
| 304 _timeoutTimer = null; | 277 _timeoutTimer = null; |
| 305 } else { | 278 } else { |
| 306 _timeoutTimer = mock_clock.newTimer(_timeout, _signalTimeout); | 279 _timeoutTimer = mock_clock.newTimer(_timeout, () { |
| 280 _timeoutTimer = null; | |
| 281 currentQueue._signalTimeout(new ScheduleError.from(this, "The schedule " | |
| 282 "timed out after $_timeout of inactivity.")); | |
| 283 }); | |
| 307 } | 284 } |
| 308 } | 285 } |
| 309 | 286 |
| 310 /// The callback to run when the timeout timer fires. Notifies the current | |
| 311 /// queue that a timeout has occurred. | |
| 312 void _signalTimeout() { | |
| 313 // Reset the timer so that we can detect timeouts in the onException and | |
| 314 // onComplete queues. | |
| 315 _timeoutTimer = null; | |
| 316 | |
| 317 var error = new ScheduleError.from(this, "The schedule timed out after " | |
| 318 "$_timeout of inactivity."); | |
| 319 | |
| 320 _pendingCallbacks = 0; | |
| 321 if (_noPendingCallbacks != null) { | |
| 322 var noPendingCallbacks = _noPendingCallbacks; | |
| 323 _noPendingCallbacks = null; | |
| 324 noPendingCallbacks.completeError(error); | |
| 325 } else { | |
| 326 currentQueue._signalTimeout(error); | |
| 327 } | |
| 328 } | |
| 329 | |
| 330 /// Returns a [Future] that will complete once there are no pending | |
| 331 /// out-of-band callbacks. | |
| 332 Future _awaitNoPendingCallbacks() { | |
| 333 if (_pendingCallbacks == 0) return new Future.immediate(null); | |
| 334 if (_noPendingCallbacks == null) _noPendingCallbacks = new Completer(); | |
| 335 return _noPendingCallbacks.future; | |
| 336 } | |
| 337 | |
| 338 /// Register an error in the schedule's error list. This ensures that there | 287 /// Register an error in the schedule's error list. This ensures that there |
| 339 /// are no duplicate errors, and that all errors are wrapped in | 288 /// are no duplicate errors, and that all errors are wrapped in |
| 340 /// [ScheduleError]. | 289 /// [ScheduleError]. |
| 341 void _addError(error) { | 290 void _addError(error) { |
| 342 if (errors.contains(error)) return; | 291 if (errors.contains(error)) return; |
| 343 errors.add(new ScheduleError.from(this, error)); | 292 errors.add(new ScheduleError.from(this, error)); |
| 344 } | 293 } |
| 345 } | 294 } |
| 346 | 295 |
| 347 /// An enum of states for a [Schedule]. | 296 /// An enum of states for a [Schedule]. |
| (...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 381 | 330 |
| 382 /// An out-of-band error signaled by [_schedule]. If this is non-null, it | 331 /// An out-of-band error signaled by [_schedule]. If this is non-null, it |
| 383 /// indicates that the queue should stop as soon as possible and re-throw this | 332 /// indicates that the queue should stop as soon as possible and re-throw this |
| 384 /// error. | 333 /// error. |
| 385 ScheduleError _error; | 334 ScheduleError _error; |
| 386 | 335 |
| 387 /// The [SubstituteFuture] for the currently-running task in the queue, or | 336 /// The [SubstituteFuture] for the currently-running task in the queue, or |
| 388 /// null if no task is currently running. | 337 /// null if no task is currently running. |
| 389 SubstituteFuture _taskFuture; | 338 SubstituteFuture _taskFuture; |
| 390 | 339 |
| 340 /// The toal number of out-of-band callbacks that have been registered on | |
| 341 /// [this]. | |
| 342 int _totalCallbacks = 0; | |
| 343 | |
| 344 // TODO(nweiz): make this a read-only view when issue 8321 is fixed. | |
| 345 /// The descriptions of all callbacks that are blocking the completion of | |
| 346 /// [this]. | |
| 347 Collection<String> get pendingCallbacks => _pendingCallbacks; | |
| 348 final _pendingCallbacks = new Queue<String>(); | |
| 349 | |
| 350 /// A completer that will be completed once [_pendingCallbacks] becomes empty | |
| 351 /// after the queue finished running its tasks. | |
|
Bob Nystrom
2013/03/12 20:08:51
finished -> finishes.
nweiz
2013/03/12 20:56:51
Done.
| |
| 352 Future get _noPendingCallbacks => _noPendingCallbacksCompleter.future; | |
| 353 final Completer _noPendingCallbacksCompleter = new Completer(); | |
| 354 | |
| 391 /// A [Future] that completes when the tasks in [this] are all complete. If an | 355 /// 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 | 356 /// error occurs while running this queue, the returned [Future] will complete |
| 393 /// with that error. | 357 /// with that error. |
| 394 /// | 358 /// |
| 395 /// The returned [Future] can complete before outstanding out-of-band | 359 /// The returned [Future] can complete before outstanding out-of-band |
| 396 /// callbacks have finished running. | 360 /// callbacks have finished running. |
| 397 Future get onTasksComplete => _onTasksCompleteCompleter.future; | 361 Future get onTasksComplete => _onTasksCompleteCompleter.future; |
| 398 final _onTasksCompleteCompleter = new Completer(); | 362 final _onTasksCompleteCompleter = new Completer(); |
| 399 | 363 |
| 400 TaskQueue._(this.name, this._schedule) { | 364 TaskQueue._(this.name, this._schedule) { |
| 401 // Avoid top-leveling errors that are passed to onTasksComplete if there are | 365 // Avoid top-leveling errors that are passed to onTasksComplete if there are |
| 402 // no listeners. | 366 // no listeners. |
| 403 onTasksComplete.catchError((_) {}); | 367 onTasksComplete.catchError((_) {}); |
| 404 } | 368 } |
| 405 | 369 |
| 406 /// Whether this queue is currently running. | 370 /// Whether this queue is currently running. |
| 407 bool get isRunning => _schedule.state == ScheduleState.RUNNING && | 371 bool get isRunning => _schedule.state == ScheduleState.RUNNING && |
| 408 _schedule.currentQueue == this; | 372 _schedule.currentQueue == this; |
| 409 | 373 |
| 374 /// Whether this queue is running its tasks (as opposed to waiting for | |
| 375 /// out-of-band callbacks or not running at all). | |
| 376 bool get isRunningTasks => isRunning && _schedule.currentTask != null; | |
| 377 | |
| 410 /// Schedules a task, [fn], to run asynchronously as part of this queue. Tasks | 378 /// Schedules a task, [fn], to run asynchronously as part of this queue. Tasks |
| 411 /// will be run in the order they're scheduled. In [fn] returns a [Future], | 379 /// will be run in the order they're scheduled. In [fn] returns a [Future], |
| 412 /// tasks after it won't be run until that [Future] completes. | 380 /// tasks after it won't be run until that [Future] completes. |
| 413 /// | 381 /// |
| 414 /// The return value will be completed once the scheduled task has finished | 382 /// The return value will be completed once the scheduled task has finished |
| 415 /// running. Its return value is the same as the return value of [fn], or the | 383 /// running. Its return value is the same as the return value of [fn], or the |
| 416 /// value it completes to if it's a [Future]. | 384 /// value it completes to if it's a [Future]. |
| 417 /// | 385 /// |
| 418 /// If [description] is passed, it's used to describe the task for debugging | 386 /// If [description] is passed, it's used to describe the task for debugging |
| 419 /// purposes when an error occurs. | 387 /// purposes when an error occurs. |
| (...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 452 | 420 |
| 453 _taskFuture = new SubstituteFuture(task.fn()); | 421 _taskFuture = new SubstituteFuture(task.fn()); |
| 454 return _taskFuture.whenComplete(() { | 422 return _taskFuture.whenComplete(() { |
| 455 _taskFuture = null; | 423 _taskFuture = null; |
| 456 _schedule.heartbeat(); | 424 _schedule.heartbeat(); |
| 457 }).catchError((e) { | 425 }).catchError((e) { |
| 458 var error = new ScheduleError.from(_schedule, e); | 426 var error = new ScheduleError.from(_schedule, e); |
| 459 _signalError(error); | 427 _signalError(error); |
| 460 throw _error; | 428 throw _error; |
| 461 }); | 429 }); |
| 430 }).whenComplete(() { | |
| 431 _schedule._currentTask = null; | |
| 462 }).then((_) { | 432 }).then((_) { |
| 463 _onTasksCompleteCompleter.complete(); | 433 _onTasksCompleteCompleter.complete(); |
| 464 }).catchError((e) { | 434 }).catchError((e) { |
| 465 _onTasksCompleteCompleter.completeError(e); | 435 _onTasksCompleteCompleter.completeError(e); |
| 466 throw e; | 436 throw e; |
| 467 }).whenComplete(() { | 437 }).whenComplete(() { |
| 468 _schedule._currentTask = null; | 438 if (pendingCallbacks.isEmpty) return; |
| 469 return _schedule._awaitNoPendingCallbacks().catchError((e) { | 439 return _noPendingCallbacks.catchError((e) { |
| 470 // Signal the error rather than passing it through directly so that if a | 440 // Signal the error rather than passing it through directly so that if a |
| 471 // timeout happens after an in-task error, both are reported. | 441 // timeout happens after an in-task error, both are reported. |
| 472 _signalError(new ScheduleError.from(_schedule, e)); | 442 _signalError(new ScheduleError.from(_schedule, e)); |
| 473 }); | 443 }); |
| 474 }).whenComplete(() { | 444 }).whenComplete(() { |
| 475 _schedule.heartbeat(); | 445 _schedule.heartbeat(); |
| 476 // If the tasks were otherwise successful, make sure we throw any | 446 // 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 | 447 // out-of-band errors. If a task failed, make sure we throw the most |
| 478 // recent error. | 448 // recent error. |
| 479 if (_error != null) throw _error; | 449 if (_error != null) throw _error; |
| 480 }); | 450 }); |
| 481 } | 451 } |
| 482 | 452 |
| 453 /// Returns a function wrapping [fn] that pipes any errors into the schedule | |
| 454 /// chain. This will also block [this] from completing until the returned | |
| 455 /// function has been called. It's used to ensure that out-of-band callbacks | |
| 456 /// are properly handled by the scheduled test. | |
| 457 Function _wrapAsync(fn(arg), String description) { | |
| 458 assert(isRunning); | |
| 459 | |
| 460 // It's possible that the queue timed out before [fn] finished. | |
| 461 bool _timedOut() => | |
| 462 _schedule.currentQueue != this || pendingCallbacks.isEmpty; | |
| 463 | |
| 464 if (description == null) { | |
| 465 description = "Out-of-band operation #${_totalCallbacks}"; | |
| 466 } | |
| 467 _totalCallbacks++; | |
| 468 | |
| 469 _pendingCallbacks.add(description);; | |
|
Bob Nystrom
2013/03/12 20:08:51
;;
nweiz
2013/03/12 20:56:51
Done.
| |
| 470 return (arg) { | |
| 471 try { | |
| 472 return fn(arg); | |
| 473 } catch (e, stackTrace) { | |
| 474 var error = new ScheduleError.from( | |
| 475 _schedule, e, stackTrace: stackTrace); | |
| 476 if (_timedOut()) { | |
| 477 _schedule._signalPostTimeoutError(error); | |
| 478 } else { | |
| 479 _schedule.signalError(error); | |
| 480 } | |
| 481 } finally { | |
| 482 if (_timedOut()) return; | |
| 483 | |
| 484 _pendingCallbacks.remove(description); | |
| 485 if (_pendingCallbacks.isEmpty && !isRunningTasks) { | |
| 486 _noPendingCallbacksCompleter.complete(); | |
| 487 } | |
| 488 } | |
| 489 }; | |
| 490 } | |
| 491 | |
| 483 /// Signals that an out-of-band error has been detected and the queue should | 492 /// Signals that an out-of-band error has been detected and the queue should |
| 484 /// stop running as soon as possible. | 493 /// stop running as soon as possible. |
| 485 void _signalError(ScheduleError error) { | 494 void _signalError(ScheduleError error) { |
| 486 // If multiple errors are detected while a task is running, make sure the | 495 // If multiple errors are detected while a task is running, make sure the |
| 487 // earlier ones are recorded in the schedule. | 496 // earlier ones are recorded in the schedule. |
| 488 if (_error != null) _schedule._addError(_error); | 497 if (_error != null) _schedule._addError(_error); |
| 489 _error = error; | 498 _error = error; |
| 490 } | 499 } |
| 491 | 500 |
| 492 /// Notifies the queue that it has timed out and it needs to terminate | 501 /// Notifies the queue that it has timed out and it needs to terminate |
| 493 /// immediately with a timeout error. | 502 /// immediately with a timeout error. |
| 494 void _signalTimeout(ScheduleError error) { | 503 void _signalTimeout(ScheduleError error) { |
| 495 if (_taskFuture != null) { | 504 _pendingCallbacks.clear(); |
| 505 if (!isRunningTasks) { | |
| 506 _noPendingCallbacksCompleter.completeError(error); | |
| 507 } else if (_taskFuture != null) { | |
| 496 // Catch errors coming off the old task future, in case it completes after | 508 // Catch errors coming off the old task future, in case it completes after |
| 497 // timing out. | 509 // timing out. |
| 498 _taskFuture.substitute(new Future.immediateError(error)).catchError((e) { | 510 _taskFuture.substitute(new Future.immediateError(error)).catchError((e) { |
| 499 _schedule._signalPostTimeoutError(e); | 511 _schedule._signalPostTimeoutError(e); |
| 500 }); | 512 }); |
| 501 } else { | 513 } else { |
| 502 // This branch probably won't be reached, but it's conceivable that the | 514 // This branch probably won't be reached, but it's conceivable that the |
| 503 // event loop might get pumped when _taskFuture is null but we haven't yet | 515 // event loop might get pumped when _taskFuture is null but we haven't yet |
| 504 // called _awaitNoPendingCallbacks. | 516 // finished running all the tasks. |
| 505 _signalError(error); | 517 _signalError(error); |
| 506 } | 518 } |
| 507 } | 519 } |
| 508 | 520 |
| 509 String toString() => name; | 521 String toString() => name; |
| 510 | 522 |
| 511 /// Returns a detailed representation of the queue as a tree of tasks. If | 523 /// Returns a detailed representation of the queue as a tree of tasks. If |
| 512 /// [highlight] is passed, that task is specially highlighted. | 524 /// [highlight] is passed, that task is specially highlighted. |
| 513 /// | 525 /// |
| 514 /// [highlight] must be a task in this queue. | 526 /// [highlight] must be a task in this queue. |
| 515 String generateTree([Task highlight]) { | 527 String generateTree([Task highlight]) { |
| 516 assert(highlight == null || highlight.queue == this); | 528 assert(highlight == null || highlight.queue == this); |
| 517 return _contents.map((task) { | 529 return _contents.map((task) { |
| 518 var lines = task.toString().split("\n"); | 530 var lines = task.toString().split("\n"); |
| 519 var firstLine = task == highlight ? | 531 var firstLine = task == highlight ? |
| 520 "> ${lines.first}" : "* ${lines.first}"; | 532 "> ${lines.first}" : "* ${lines.first}"; |
| 521 lines = new List.from(lines.skip(1).map((line) => "| $line")); | 533 lines = new List.from(lines.skip(1).map((line) => "| $line")); |
| 522 lines.insertRange(0, 1, firstLine); | 534 lines.insertRange(0, 1, firstLine); |
| 523 return lines.join("\n"); | 535 return lines.join("\n"); |
| 524 }).join("\n"); | 536 }).join("\n"); |
| 525 } | 537 } |
| 526 } | 538 } |
| OLD | NEW |