Index: tests/lib/async/zone_timer_task_test.dart |
diff --git a/tests/lib/async/zone_timer_task_test.dart b/tests/lib/async/zone_timer_task_test.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..310f7ca510a5b6fa5ecf0ac24247e712a11240aa |
--- /dev/null |
+++ b/tests/lib/async/zone_timer_task_test.dart |
@@ -0,0 +1,515 @@ |
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file |
+// for details. All rights reserved. Use of this source code is governed by a |
+// BSD-style license that can be found in the LICENSE file. |
+ |
+// Tests timer tasks. |
+ |
+import 'package:expect/expect.dart'; |
+import 'package:async_helper/async_helper.dart'; |
+import 'dart:async'; |
+import 'dart:collection'; |
+ |
+class MyTimerSpecification implements SingleShotTimerTaskSpecification { |
+ final Function callback; |
+ final Duration duration; |
+ |
+ MyTimerSpecification(this.callback, this.duration); |
+ |
+ bool get isOneShot => true; |
+ String get name => "test.timer-override"; |
+} |
+ |
+class MyPeriodicTimerSpecification implements PeriodicTimerTaskSpecification { |
+ final Function callback; |
+ final Duration duration; |
+ |
+ MyPeriodicTimerSpecification(this.callback, this.duration); |
+ |
+ bool get isOneShot => true; |
+ String get name => "test.periodic-timer-override"; |
+} |
+ |
+/// Makes sure things are working in a simple setting. |
+/// No interceptions, changes, ... |
+Future testTimerTask() { |
+ List log = []; |
+ |
+ var testCompleter = new Completer(); |
+ asyncStart(); |
+ |
+ int taskIdCounter = 0; |
+ |
+ Object createTaskHandler(Zone self, ZoneDelegate parent, Zone zone, |
+ TaskCreate create, TaskSpecification specification) { |
+ var taskMap = self['taskMap']; |
+ var taskIdMap = self['taskIdMap']; |
+ if (specification is SingleShotTimerTaskSpecification) { |
+ log.add("create enter " |
+ "zone: ${self['name']} " |
+ "spec-duration: ${specification.duration} " |
+ "spec-oneshot?: ${specification.isOneShot}"); |
+ var result = parent.createTask(zone, create, specification); |
+ taskMap[result] = specification; |
+ taskIdMap[specification] = taskIdCounter++; |
+ log.add("create leave"); |
+ return result; |
+ } else if (specification is PeriodicTimerTaskSpecification) { |
+ log.add("create enter " |
+ "zone: ${self['name']} " |
+ "spec-duration: ${specification.duration} " |
+ "spec-oneshot?: ${specification.isOneShot}"); |
+ var result = parent.createTask(zone, create, specification); |
+ taskMap[result] = specification; |
+ taskIdMap[specification] = taskIdCounter++; |
+ log.add("create leave"); |
+ return result; |
+ } |
+ return parent.createTask(zone, create, specification); |
+ } |
+ |
+ void runTaskHandler(Zone self, ZoneDelegate parent, Zone zone, TaskRun run, |
+ Object task, Object arg) { |
+ var taskMap = self['taskMap']; |
+ var taskIdMap = self['taskIdMap']; |
+ if (taskMap.containsKey(task)) { |
+ var spec = taskMap[task]; |
+ log.add("run enter " |
+ "zone: ${self['name']} " |
+ "task-id: ${taskIdMap[spec]} " |
+ "arg: $arg"); |
+ parent.runTask(zone, run, task, arg); |
+ log.add("run leave"); |
+ return; |
+ } |
+ parent.runTask(zone, run, task, arg); |
+ } |
+ |
+ runZoned(() async { |
+ var completer0 = new Completer(); |
+ Timer.run(() { |
+ completer0.complete("done"); |
+ }); |
+ await completer0.future; |
+ |
+ Expect.listEquals([ |
+ 'create enter zone: custom zone spec-duration: 0:00:00.000000 ' |
+ 'spec-oneshot?: true', |
+ 'create leave', |
+ 'run enter zone: custom zone task-id: 0 arg: null', |
+ 'run leave' |
+ ], log); |
+ log.clear(); |
+ |
+ var completer1 = new Completer(); |
+ var counter1 = 0; |
+ new Timer.periodic(const Duration(milliseconds: 5), (Timer timer) { |
+ if (counter1++ > 1) { |
+ timer.cancel(); |
+ completer1.complete("done"); |
+ } |
+ }); |
+ await completer1.future; |
+ |
+ Expect.listEquals([ |
+ 'create enter zone: custom zone spec-duration: 0:00:00.005000 ' |
+ 'spec-oneshot?: false', |
+ 'create leave', |
+ 'run enter zone: custom zone task-id: 1 arg: null', |
+ 'run leave', |
+ 'run enter zone: custom zone task-id: 1 arg: null', |
+ 'run leave', |
+ 'run enter zone: custom zone task-id: 1 arg: null', |
+ 'run leave' |
+ ], log); |
+ log.clear(); |
+ |
+ testCompleter.complete("done"); |
+ asyncEnd(); |
+ }, |
+ zoneValues: {'name': 'custom zone', 'taskMap': {}, 'taskIdMap': {}}, |
+ zoneSpecification: new ZoneSpecification( |
+ createTask: createTaskHandler, |
+ runTask: runTaskHandler)); |
+ |
+ return testCompleter.future; |
+} |
+ |
+/// More complicated zone, that intercepts... |
+Future testTimerTask2() { |
+ List log = []; |
+ |
+ var testCompleter = new Completer(); |
+ asyncStart(); |
+ |
+ int taskIdCounter = 0; |
+ |
+ Object createTaskHandler(Zone self, ZoneDelegate parent, Zone zone, |
+ TaskCreate create, TaskSpecification specification) { |
+ var taskMap = self['taskMap']; |
+ var taskIdMap = self['taskIdMap']; |
+ if (specification is SingleShotTimerTaskSpecification) { |
+ log.add("create enter " |
+ "zone: ${self['name']} " |
+ "spec-duration: ${specification.duration} " |
+ "spec-oneshot?: ${specification.isOneShot}"); |
+ var mySpec = new MyTimerSpecification(specification.callback, |
+ specification.duration + const Duration(milliseconds: 2)); |
+ var result = parent.createTask(zone, create, mySpec); |
+ taskMap[result] = specification; |
+ taskIdMap[specification] = taskIdCounter++; |
+ log.add("create leave"); |
+ return result; |
+ } else if (specification is PeriodicTimerTaskSpecification) { |
+ log.add("create enter " |
+ "zone: ${self['name']} " |
+ "spec-duration: ${specification.duration} " |
+ "spec-oneshot?: ${specification.isOneShot}"); |
+ var mySpec = new MyPeriodicTimerSpecification(specification.callback, |
+ specification.duration + const Duration(milliseconds: 2)); |
+ var result = parent.createTask(zone, create, specification); |
+ taskMap[result] = specification; |
+ taskIdMap[specification] = taskIdCounter++; |
+ log.add("create leave"); |
+ return result; |
+ } |
+ return parent.createTask(zone, create, specification); |
+ } |
+ |
+ void runTaskHandler(Zone self, ZoneDelegate parent, Zone zone, TaskRun run, |
+ Object task, Object arg) { |
+ var taskMap = self['taskMap']; |
+ var taskIdMap = self['taskIdMap']; |
+ if (taskMap.containsKey(task)) { |
+ var spec = taskMap[task]; |
+ log.add("run enter " |
+ "zone: ${self['name']} " |
+ "task-id: ${taskIdMap[spec]} " |
+ "arg: $arg"); |
+ parent.runTask(zone, run, task, arg); |
+ log.add("run leave"); |
+ return; |
+ } |
+ parent.runTask(zone, run, task, arg); |
+ } |
+ |
+ runZoned(() async { |
+ var completer0 = new Completer(); |
+ Timer.run(() { |
+ completer0.complete("done"); |
+ }); |
+ await completer0.future; |
+ |
+ // No visible change (except for the zone name) in the log, compared to the |
+ // simple invocations. |
+ Expect.listEquals([ |
+ 'create enter zone: outer-zone spec-duration: 0:00:00.000000 ' |
+ 'spec-oneshot?: true', |
+ 'create leave', |
+ 'run enter zone: outer-zone task-id: 0 arg: null', |
+ 'run leave' |
+ ], log); |
+ log.clear(); |
+ |
+ var completer1 = new Completer(); |
+ var counter1 = 0; |
+ new Timer.periodic(const Duration(milliseconds: 5), (Timer timer) { |
+ if (counter1++ > 1) { |
+ timer.cancel(); |
+ completer1.complete("done"); |
+ } |
+ }); |
+ await completer1.future; |
+ |
+ // No visible change (except for the zone nome) in the log, compared to the |
+ // simple invocations. |
+ Expect.listEquals([ |
+ 'create enter zone: outer-zone spec-duration: 0:00:00.005000 ' |
+ 'spec-oneshot?: false', |
+ 'create leave', |
+ 'run enter zone: outer-zone task-id: 1 arg: null', |
+ 'run leave', |
+ 'run enter zone: outer-zone task-id: 1 arg: null', |
+ 'run leave', |
+ 'run enter zone: outer-zone task-id: 1 arg: null', |
+ 'run leave' |
+ ], log); |
+ log.clear(); |
+ |
+ var nestedCompleter = new Completer(); |
+ |
+ runZoned(() async { |
+ var completer0 = new Completer(); |
+ Timer.run(() { |
+ completer0.complete("done"); |
+ }); |
+ await completer0.future; |
+ |
+ // The outer zone sees the duration change of the inner zone. |
+ Expect.listEquals([ |
+ 'create enter zone: inner-zone spec-duration: 0:00:00.000000 ' |
+ 'spec-oneshot?: true', |
+ 'create enter zone: outer-zone spec-duration: 0:00:00.002000 ' |
+ 'spec-oneshot?: true', |
+ 'create leave', |
+ 'create leave', |
+ 'run enter zone: inner-zone task-id: 3 arg: null', |
+ 'run enter zone: outer-zone task-id: 2 arg: null', |
+ 'run leave', |
+ 'run leave' |
+ ], log); |
+ log.clear(); |
+ |
+ var completer1 = new Completer(); |
+ var counter1 = 0; |
+ new Timer.periodic(const Duration(milliseconds: 5), (Timer timer) { |
+ if (counter1++ > 1) { |
+ timer.cancel(); |
+ completer1.complete("done"); |
+ } |
+ }); |
+ await completer1.future; |
+ |
+ // The outer zone sees the duration change of the inner zone. |
+ Expect.listEquals([ |
+ 'create enter zone: inner-zone spec-duration: 0:00:00.005000 ' |
+ 'spec-oneshot?: false', |
+ 'create enter zone: outer-zone spec-duration: 0:00:00.005000 ' |
+ 'spec-oneshot?: false', |
+ 'create leave', |
+ 'create leave', |
+ 'run enter zone: inner-zone task-id: 5 arg: null', |
+ 'run enter zone: outer-zone task-id: 4 arg: null', |
+ 'run leave', |
+ 'run leave', |
+ 'run enter zone: inner-zone task-id: 5 arg: null', |
+ 'run enter zone: outer-zone task-id: 4 arg: null', |
+ 'run leave', |
+ 'run leave', |
+ 'run enter zone: inner-zone task-id: 5 arg: null', |
+ 'run enter zone: outer-zone task-id: 4 arg: null', |
+ 'run leave', |
+ 'run leave' |
+ ], log); |
+ log.clear(); |
+ |
+ nestedCompleter.complete("done"); |
+ }, |
+ zoneValues: {'name': 'inner-zone', 'taskMap': {}, 'taskIdMap': {}}, |
+ zoneSpecification: new ZoneSpecification( |
+ createTask: createTaskHandler, |
+ runTask: runTaskHandler)); |
+ |
+ await nestedCompleter.future; |
+ testCompleter.complete("done"); |
+ asyncEnd(); |
+ }, |
+ zoneValues: {'name': 'outer-zone', 'taskMap': {}, 'taskIdMap': {}}, |
+ zoneSpecification: new ZoneSpecification( |
+ createTask: createTaskHandler, |
+ runTask: runTaskHandler)); |
+ |
+ return testCompleter.future; |
+} |
+ |
+class TimerEntry { |
+ final int time; |
+ final SimulatedTimer timer; |
+ |
+ TimerEntry(this.time, this.timer); |
+} |
+ |
+class SimulatedTimer implements Timer { |
+ static int _idCounter = 0; |
+ |
+ Zone _zone; |
+ final int _id = _idCounter++; |
+ final Duration _duration; |
+ final Function _callback; |
+ final bool _isPeriodic; |
+ bool _isActive = true; |
+ |
+ SimulatedTimer(this._zone, this._duration, this._callback, this._isPeriodic); |
+ |
+ bool get isActive => _isActive; |
+ |
+ void cancel() { |
+ _isActive = false; |
+ } |
+ |
+ void _run() { |
+ if (!isActive) return; |
+ _zone.runTask(_runTimer, this, null); |
+ } |
+ |
+ static void _runTimer(SimulatedTimer timer, _) { |
+ if (timer._isPeriodic) { |
+ timer._callback(timer); |
+ } else { |
+ timer._callback(); |
+ } |
+ } |
+} |
+ |
+testSimulatedTimer() { |
+ List log = []; |
+ |
+ var currentTime = 0; |
+ // Using a simple list as queue. Not very efficient, but the test has only |
+ // very few timers running at the same time. |
+ var queue = new DoubleLinkedQueue<TimerEntry>(); |
+ |
+ // Schedules the given callback at now + duration. |
+ void schedule(int scheduledTime, SimulatedTimer timer) { |
+ log.add("scheduling timer ${timer._id} for $scheduledTime"); |
+ if (queue.isEmpty) { |
+ queue.add(new TimerEntry(scheduledTime, timer)); |
+ } else { |
+ DoubleLinkedQueueEntry current = queue.firstEntry(); |
+ while (current != null) { |
+ if (current.element.time <= scheduledTime) { |
+ current = current.nextEntry(); |
+ } else { |
+ current.prepend(new TimerEntry(scheduledTime, timer)); |
+ break; |
+ } |
+ } |
+ if (current == null) { |
+ queue.add(new TimerEntry(scheduledTime, timer)); |
+ } |
+ } |
+ } |
+ |
+ void runQueue() { |
+ while (queue.isNotEmpty) { |
+ var item = queue.removeFirst(); |
+ // If multiple callbacks were scheduled at the same time, increment the |
+ // current time instead of staying at the same time. |
+ currentTime = item.time > currentTime ? item.time : currentTime + 1; |
+ SimulatedTimer timer = item.timer; |
+ log.add("running timer ${timer._id} at $currentTime " |
+ "(active?: ${timer.isActive})"); |
+ if (!timer.isActive) continue; |
+ if (timer._isPeriodic) { |
+ schedule(currentTime + timer._duration.inMilliseconds, timer); |
+ } |
+ item.timer._run(); |
+ } |
+ } |
+ |
+ SimulatedTimer createSimulatedOneShotTimer( |
+ SingleShotTimerTaskSpecification spec, Zone zone) { |
+ var timer = new SimulatedTimer(zone, spec.duration, spec.callback, false); |
+ schedule(currentTime + spec.duration.inMilliseconds, timer); |
+ return timer; |
+ } |
+ |
+ SimulatedTimer createSimulatedPeriodicTimer( |
+ PeriodicTimerTaskSpecification spec, Zone zone) { |
+ var timer = new SimulatedTimer(zone, spec.duration, spec.callback, true); |
+ schedule(currentTime + spec.duration.inMilliseconds, timer); |
+ return timer; |
+ } |
+ |
+ Object createSimulatedTaskHandler(Zone self, ZoneDelegate parent, Zone zone, |
+ TaskCreate create, TaskSpecification specification) { |
+ var taskMap = self['taskMap']; |
+ var taskIdMap = self['taskIdMap']; |
+ if (specification is SingleShotTimerTaskSpecification) { |
+ log.add("create enter " |
+ "zone: ${self['name']} " |
+ "spec-duration: ${specification.duration} " |
+ "spec-oneshot?: ${specification.isOneShot}"); |
+ var result = |
+ parent.createTask(zone, createSimulatedOneShotTimer, specification); |
+ log.add("create leave"); |
+ return result; |
+ } |
+ if (specification is PeriodicTimerTaskSpecification) { |
+ log.add("create enter " |
+ "zone: ${self['name']} " |
+ "spec-duration: ${specification.duration} " |
+ "spec-oneshot?: ${specification.isOneShot}"); |
+ var result = |
+ parent.createTask(zone, createSimulatedPeriodicTimer, specification); |
+ log.add("create leave"); |
+ return result; |
+ } |
+ return parent.createTask(zone, create, specification); |
+ } |
+ |
+ runZoned(() { |
+ Timer.run(() { |
+ log.add("running Timer.run"); |
+ }); |
+ |
+ var timer0; |
+ |
+ new Timer(const Duration(milliseconds: 10), () { |
+ log.add("running Timer(10)"); |
+ timer0.cancel(); |
+ log.add("canceled timer0"); |
+ }); |
+ |
+ timer0 = new Timer(const Duration(milliseconds: 15), () { |
+ log.add("running Timer(15)"); |
+ }); |
+ |
+ var counter1 = 0; |
+ new Timer.periodic(const Duration(milliseconds: 5), (Timer timer) { |
+ log.add("running periodic timer $counter1"); |
+ if (counter1++ > 1) { |
+ timer.cancel(); |
+ } |
+ }); |
+ }, |
+ zoneSpecification: |
+ new ZoneSpecification(createTask: createSimulatedTaskHandler)); |
+ |
+ runQueue(); |
+ |
+ Expect.listEquals([ |
+ 'create enter zone: null spec-duration: 0:00:00.000000 spec-oneshot?: true', |
+ 'scheduling timer 0 for 0', |
+ 'create leave', |
+ 'create enter zone: null spec-duration: 0:00:00.010000 spec-oneshot?: true', |
+ 'scheduling timer 1 for 10', |
+ 'create leave', |
+ 'create enter zone: null spec-duration: 0:00:00.015000 spec-oneshot?: true', |
+ 'scheduling timer 2 for 15', |
+ 'create leave', |
+ 'create enter zone: null spec-duration: 0:00:00.005000 ' |
+ 'spec-oneshot?: false', |
+ 'scheduling timer 3 for 5', |
+ 'create leave', |
+ 'running timer 0 at 1 (active?: true)', |
+ 'running Timer.run', |
+ 'running timer 3 at 5 (active?: true)', |
+ 'scheduling timer 3 for 10', |
+ 'running periodic timer 0', |
+ 'running timer 1 at 10 (active?: true)', |
+ 'running Timer(10)', |
+ 'canceled timer0', |
+ 'running timer 3 at 11 (active?: true)', |
+ 'scheduling timer 3 for 16', |
+ 'running periodic timer 1', |
+ 'running timer 2 at 15 (active?: false)', |
+ 'running timer 3 at 16 (active?: true)', |
+ 'scheduling timer 3 for 21', |
+ 'running periodic timer 2', |
+ 'running timer 3 at 21 (active?: false)' |
+ ], log); |
+ log.clear(); |
+} |
+ |
+runTests() async { |
+ await testTimerTask(); |
+ await testTimerTask2(); |
+ testSimulatedTimer(); |
+} |
+ |
+main() { |
+ asyncStart(); |
+ runTests().then((_) { |
+ asyncEnd(); |
+ }); |
+} |