/** * Copyright 2009 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.waveprotocol.wave.client.scheduler.testing; import org.waveprotocol.wave.client.scheduler.TimerService; import org.waveprotocol.wave.client.scheduler.Scheduler.IncrementalTask; import org.waveprotocol.wave.client.scheduler.Scheduler.Schedulable; import org.waveprotocol.wave.client.scheduler.Scheduler.Task; import java.util.Iterator; import java.util.PriorityQueue; /** * TimerService to be used during testing, can call scheduled services without * sleeping. * */ public class FakeTimerService implements TimerService { private int currentTime = 0; private int instanceCount = 0; /** * Maps absolute start time (currentTime + minimumTime) to a Task that should * run at or after that time. */ private final PriorityQueue<TimedTask> tasks = new PriorityQueue<TimedTask>(); private double startTime = System.currentTimeMillis(); /** A {@link IncrementalTask} along with the time when it should run. */ private class TimedTask implements Comparable<TimedTask> { private final IncrementalTask task; private final int time; private final int interval; private final int instanceNumber; // compareTo() tiebreaker for fair scheduling for interval==0 /** * A task that is to be run at the specified time, and then repeatedly each * time the specified interval has passed. * * @param time earliest time when the task should run * @param task The task to run * @param interval delay before the next time the task is to be run. Must be * {@literal >= 0}. If {@link IncrementalTask#execute()} returns * false, the task is not rerun again; Clients of this class must make * sure that this eventually happens for tasks with an interval 0, as * otherwise {@link FakeTimerService#tick(int)} will repeat the task * indefinitely and never return. * @throws IllegalArgumentException if time or interval < 0 */ TimedTask(int time, IncrementalTask task, int interval) { this.task = task; this.time = time; this.interval = interval; if (task == null) { throw new NullPointerException("null task"); } if (time < 0) { throw new IllegalArgumentException("Expected time >= 0, got " + time); } if (interval < 0) { throw new IllegalArgumentException("Expected interval >= 0, got " + interval); } this.instanceNumber = instanceCount++; } /** A task that is to be run only once, at or after the specified time. */ TimedTask(int time, Task task) { // Note: Even though we specify an interval of 1 here, the task is never // re-run because {@code TaskAsIncrementalTask.execute()} returns false. this(time, new TaskAsIncrementalTask(task), 1); } /** The task to run at or after time {@link #getTime()}. */ public IncrementalTask getTask() { return task; } /** The earliest time at which {@link #getTask()} should run. */ public int getTime() { return time; } /** * A TimedTask representing the next time the underlying task should be * excuted (if {@link IncrementalTask#execute()} returns true). */ TimedTask nextExecution() { return new TimedTask(time + interval, task, interval); } /** * {@inheritDoc} * * This order is inconsistent with equals. */ @Override public int compareTo(TimedTask that) { int result = this.time - that.time; if (result == 0) { // This way, when a task with delay 0 is rescheduled, it will be scheduled past all other // pending tasks at the same time due to its higher instance number. Thus, when there are // several tasks at the same time with delay 0, they will be scheduled fairly. result = this.instanceNumber - that.instanceNumber; } return result; } /** {@inheritDoc} */ @Override public boolean equals(Object obj) { if (!(obj instanceof TimedTask)) { return false; } else { TimedTask that = (TimedTask) obj; return this.time == that.time && this.task.equals(that.task); } } /** {@inheritDoc} */ @Override public int hashCode() { return time + 31 * task.hashCode(); } /** {@inheritDoc} */ @Override public String toString() { return "TimedTask[time=" + time + ", task=" + task + "]"; } } /** * Adapter that views a {@link Task} as an {@link IncrementalTask} which * returns false from {@link #execute} and therefore runs only once. */ private static final class TaskAsIncrementalTask implements IncrementalTask { private final Task task; /** Views the specified task as an IncrementalTask. */ public TaskAsIncrementalTask(Task task) { this.task = task; if (task == null) { throw new NullPointerException("null task"); } } /** The underlying task. */ final Task getTask() { return task; } /** {@link Task#execute() Executes} the wrapped Task and returns false. */ @Override public boolean execute() { task.execute(); return false; } /** {@inheritDoc} */ @Override public boolean equals(Object obj) { if (!(obj instanceof TaskAsIncrementalTask)) { return false; } else { TaskAsIncrementalTask that = (TaskAsIncrementalTask) obj; return this.task.equals(that.task); } } /** {@inheritDoc} */ @Override public int hashCode() { return task.hashCode() * 31 + 17; } /** {@inheritDoc} */ @Override public String toString() { return "TaskAsIncrementalTask[" + task + "]"; } } @Override public void schedule(Task task) { cancel(task); scheduleDelayed(task, 0); } @Override public void schedule(IncrementalTask process) { scheduleRepeating(process, 0, 0); } @Override public void scheduleDelayed(Task task, int minimumTime) { cancel(task); tasks.add(new TimedTask(currentTime + minimumTime, task)); } @Override public void scheduleDelayed(IncrementalTask process, int minimumTime) { cancel(process); scheduleRepeating(process, minimumTime, 1); } @Override public void scheduleRepeating(IncrementalTask process, int minimumTime, int interval) { cancel(process); tasks.add(new TimedTask(currentTime + minimumTime, process, interval)); } private boolean findOrCancel(Schedulable job, boolean remove) { IncrementalTask incrementalTask; if (job instanceof IncrementalTask) { incrementalTask = (IncrementalTask) job; } else if (job instanceof Task) { incrementalTask = new TaskAsIncrementalTask((Task) job); } else { throw new IllegalArgumentException("cancel: Unknown schedulable type: " + job.getClass()); } Iterator<TimedTask> iterator = tasks.iterator(); boolean found = false; while (iterator.hasNext()) { if (iterator.next().getTask().equals(incrementalTask)) { if (remove) { iterator.remove(); found = true; } else { return true; } } } return found; } @Override public void cancel(Schedulable job) { findOrCancel(job, true); } @Override public boolean isScheduled(Schedulable job) { return findOrCancel(job, false); } @Override public int elapsedMillis() { return currentTime; } @Override public double currentTimeMillis() { return startTime + currentTime; } /** * Sets startTime of this TimerService to given time. * * @param startTime New value for startTime to take. */ public void setStartTime(double startTime) { this.startTime = startTime; } /** * Advances specified millis, running all tasks in between. * * @param millisToAdvance Number of milliseconds to advance the timer. */ public void tick(int millisToAdvance) { currentTime += millisToAdvance; while (!tasks.isEmpty() && tasks.peek().getTime() <= currentTime) { TimedTask timedTask = tasks.poll(); IncrementalTask task = timedTask.getTask(); boolean doReschedule = task.execute(); if (doReschedule) { // Note: If the next execution is at or before currentTime, the task // will be re-executed before this tick() call returns. tasks.add(timedTask.nextExecution()); } } } public int countTasksScheduled() { return tasks.size(); } }