/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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;
import com.google.gwt.junit.client.GWTTestCase;
import org.waveprotocol.wave.client.scheduler.Scheduler.IncrementalTask;
import org.waveprotocol.wave.client.scheduler.Scheduler.Listener;
import org.waveprotocol.wave.client.scheduler.Scheduler.Priority;
import org.waveprotocol.wave.client.scheduler.Scheduler.Schedulable;
import org.waveprotocol.wave.client.scheduler.testing.FakeSimpleTimer;
import org.waveprotocol.wave.model.util.Pair;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
/**
* Test case for BrowserBackedScheduler
*
* Does not just test the interface, also checks that its use of the timer is
* limited, and that the data structures are clean when no tasks are remaining,
* etc.
*
* @author danilatos@google.com (Daniel Danilatos)
*/
public class BrowserBackedSchedulerGwtTest extends GWTTestCase {
/** Usable priorities (excludes {@link Priority#INTERNAL_SUPPRESS}) */
private static final Collection<Priority> priorities =
EnumSet.complementOf(EnumSet.of(Priority.INTERNAL_SUPPRESS));
private abstract class HasProgress {
HasProgress prev;
public abstract boolean hasProgressed();
void checkOrder() {
if (prev != null) {
assertTrue("Tasks executed out of order", prev.hasProgressed());
}
}
}
private class FakeTask extends HasProgress implements Scheduler.Task {
private final int durationMillis;
private boolean wasExecuted = false;
public FakeTask(int durationMillis) {
this.durationMillis = durationMillis;
}
public void execute() {
assertTrue("Task executed more than once", !wasExecuted);
wasExecuted = true;
providedTimer.tick(durationMillis);
}
@Override
public boolean hasProgressed() {
return wasExecuted;
}
@Override
public String toString() {
return "FakeTask " + durationMillis + " ms";
}
}
private class FakeProcess extends HasProgress implements
Scheduler.IncrementalTask {
private final int[] jobDurations;
int current = 0;
public FakeProcess(int... jobDurations) {
this.jobDurations = jobDurations;
}
public boolean execute() {
assertTrue("Completed process was executed again", !hasFinished());
providedTimer.tick(jobDurations[current]);
current++;
return !hasFinished();
}
@Override
public boolean hasProgressed() {
return current > 0;
}
public boolean hasFinished() {
return current >= jobDurations.length;
}
}
private class IntervalProcess implements Scheduler.IncrementalTask {
int expectedNextTime = 0;
final int interval;
public IntervalProcess(int firstTime, int interval) {
expectedNextTime = firstTime;
this.interval = interval;
}
public boolean execute() {
assertEquals(expectedNextTime, providedTimer.getTime(), 0.01);
expectedNextTime += interval;
return true;
}
}
private final SimpleTimer.Factory provider = new SimpleTimer.Factory() {
public SimpleTimer create(Runnable runnable) {
assert providedTimer == null
: "Not expecting to provide more than one timer at a time";
return providedTimer = new FakeSimpleTimer(runnable);
}
};
private FakeSimpleTimer providedTimer;
private BrowserBackedScheduler scheduler;
private List<FakeTask> tasks;
private ArrayList<Pair<Schedulable, Integer>> executedJob;
public void testIsScheduledReportsScheduledState() {
FakeTask task1 = new FakeTask(1);
FakeTask task2 = new FakeTask(1);
FakeTask task3 = new FakeTask(1);
scheduler.schedule(Priority.LOW, task1);
scheduler.scheduleDelayed(Priority.LOW, task3, 20);
assertTrue(scheduler.isScheduled(task1));
assertFalse(scheduler.isScheduled(task2));
assertTrue(scheduler.isScheduled(task3));
providedTimer.trigger(10);
assertFalse(scheduler.isScheduled(task1));
assertFalse(scheduler.isScheduled(task2));
assertTrue(scheduler.isScheduled(task3));
providedTimer.trigger(20);
assertFalse(scheduler.isScheduled(task1));
assertFalse(scheduler.isScheduled(task2));
assertFalse(scheduler.isScheduled(task3));
}
public void testIsScheduledReportsFalseForRunningTask() {
scheduler.schedule(Priority.LOW, new Scheduler.Task() {
public void execute() {
assertFalse(scheduler.isScheduled(this));
}
});
scheduler.scheduleDelayed(Priority.LOW, new Scheduler.Task() {
public void execute() {
assertFalse(scheduler.isScheduled(this));
}
}, 10);
providedTimer.trigger(20);
}
public void testAllCriticalTasksRun() {
scheduleTasks(4, Priority.CRITICAL, 5);
providedTimer.trigger();
assertTasksRun(true, 0, 3);
assertEmpty(true);
}
public void testNonCriticalTasksChunked() {
scheduleTasks(5, Priority.HIGH, 4);
providedTimer.trigger();
assertTasksRun(true, 0, 2);
assertTasksRun(false, 3, 4);
providedTimer.trigger();
assertTasksRun(true, 0, 4);
assertEmpty(true);
}
public void testTasksPrioritised() {
scheduleTasks(5, Priority.LOW, 4);
scheduleTasks(5, Priority.HIGH, 4);
providedTimer.trigger();
assertTasksRun(false, 0, 4);
assertTasksRun(true, 5, 7);
assertTasksRun(false, 8, 9);
providedTimer.trigger();
assertTasksRun(true, 5, 9);
assertTasksRun(true, 0, 0);
assertTasksRun(false, 1, 4);
providedTimer.trigger();
assertTasksRun(true, 1, 3);
assertTasksRun(false, 4, 4);
providedTimer.trigger();
assertTasksRun(true, 0, 9);
assertEmpty(true);
}
public void testTasksScheduledOneAtATime() {
for (Priority p : priorities) {
FakeTask task = new FakeTask(10);
scheduler.schedule(p, task);
scheduler.schedule(p, task); // Should be a no-op
providedTimer.trigger();
assertTrue("Task was not run at all", task.wasExecuted);
}
assertEmpty(true);
}
public void testRescheduledTasksIncreasePriority() {
FakeTask task = new FakeTask(1);
FakeTask longLow = new FakeTask(20);
FakeTask shortLow = new FakeTask(1);
scheduler.schedule(Priority.LOW, longLow);
scheduler.schedule(Priority.LOW, shortLow);
scheduler.schedule(Priority.LOW, task);
scheduler.schedule(Priority.HIGH, task); // Should upgrade priority
providedTimer.trigger();
assertTrue("Task might not have been upgraded", task.wasExecuted);
assertTrue("Low priority task out of order", !shortLow.wasExecuted);
assertEmpty(false);
trigger(3);
assertEmpty(true);
}
public void testRescheduledTasksDecreasePriority() {
FakeTask task = new FakeTask(1);
FakeTask longLow = new FakeTask(20);
FakeTask shortLow = new FakeTask(1);
scheduler.schedule(Priority.LOW, longLow);
scheduler.schedule(Priority.LOW, shortLow);
scheduler.schedule(Priority.HIGH, task);
scheduler.schedule(Priority.LOW, task); // Should replace priority
providedTimer.trigger();
assertFalse("Task might not have been downgraded", task.wasExecuted);
assertTrue("Low priority task out of order", !shortLow.wasExecuted);
assertEmpty(false);
trigger(3);
assertEmpty(true);
}
public void testProcessesInterleave() {
final FakeProcess[] lastProc = new FakeProcess[1];
class InterleavingProc extends FakeProcess {
public InterleavingProc(int... jobTimes) {
super(jobTimes);
}
@Override
public boolean execute() {
assertTrue("Did not interleave", lastProc[0] != this);
lastProc[0] = this;
return super.execute();
}
}
FakeProcess proc1 = new InterleavingProc(5, 5, 5);
FakeProcess proc2 = new InterleavingProc(5, 5, 5);
scheduler.schedule(Priority.LOW, proc1);
scheduler.schedule(Priority.LOW, proc2);
scheduler.setTimeSlice(20);
providedTimer.trigger();
assertTrue(proc1.hasProgressed() && proc2.hasProgressed());
assertEmpty(false);
providedTimer.trigger();
assertEmpty(true);
assertTrue(proc1.hasFinished() && proc2.hasFinished());
}
public void testCancelling() {
FakeProcess process1 = new FakeProcess(10, 10, 10, 10, 10, 10);
FakeProcess process2 = new FakeProcess(10, 10, 10, 10, 10, 10);
FakeTask task1 = new FakeTask(1);
FakeTask task2 = new FakeTask(1);
scheduler.schedule(Priority.HIGH, process1);
scheduler.schedule(Priority.LOW, task1);
scheduler.schedule(Priority.LOW, task2);
scheduler.schedule(Priority.LOW, process2);
providedTimer.trigger();
assertTrue(process1.hasProgressed());
assertFalse(process1.hasFinished() || process2.hasProgressed());
assertFalse(task1.wasExecuted || task2.wasExecuted);
assertEmpty(false);
scheduler.cancel(task1);
scheduler.cancel(task2);
scheduler.cancel(process1);
scheduler.cancel(process2);
assertEmpty(true);
scheduler.schedule(Priority.LOW, task2);
providedTimer.trigger();
assertTrue(task2.wasExecuted);
assertFalse(process1.hasFinished() || process2.hasProgressed());
assertFalse(task1.wasExecuted);
assertEmpty(true);
}
public void testCancellingAnIncrementalTaskDuringExecutionIgnoresContinueValue() {
IncrementalTask mock = new IncrementalTask() {
boolean cancelled;
@Override
public boolean execute() {
if (cancelled) {
fail("Cancelled task was not removed from scheduler");
}
scheduler.cancel(this);
cancelled = true;
return true;
}
};
scheduler.scheduleRepeating(Priority.LOW, mock, 0, 50);
providedTimer.trigger();
for (int i = 0; i < 50; i++) {
providedTimer.trigger(1);
}
assertEmpty(true);
}
public void testRepeatingJob() {
Scheduler.IncrementalTask p = new IntervalProcess(0, 50);
scheduler.scheduleRepeating(Priority.LOW, p, 0, 50);
providedTimer.trigger();
for (int i = 0; i < 200; i++) {
providedTimer.trigger(1);
}
assertEmpty(false);
scheduler.cancel(p);
assertEmpty(true);
}
public void testCoincidingRepeatingJobs() {
for (int i = 0; i < 10; i++) {
Scheduler.IncrementalTask p = new IntervalProcess(0, 50);
scheduler.scheduleRepeating(Priority.LOW, p, 0, 50);
}
providedTimer.trigger();
while (providedTimer.getTime() < 200) {
providedTimer.trigger(1);
}
assertEmpty(false);
}
public void testDelayedProcessBehavesNormallyOnceStarted() {
FakeProcess p = new FakeProcess(5, 5, 5, 5);
scheduler.scheduleDelayed(Priority.LOW, p, 10);
commonTestDelayedProcessBehavesNormallyOnceStarted(p);
}
public void testDelayedZeroIntervalProcessBehavesNormallyOnceStarted() {
FakeProcess p = new FakeProcess(5, 5, 5, 5);
scheduler.scheduleRepeating(Priority.LOW, p, 10, 0);
commonTestDelayedProcessBehavesNormallyOnceStarted(p);
}
private void commonTestDelayedProcessBehavesNormallyOnceStarted(FakeProcess p) {
providedTimer.trigger(9);
assertFalse(p.hasProgressed());
providedTimer.trigger(1);
assertTrue(p.hasProgressed() && !p.hasFinished());
providedTimer.trigger();
assertTrue(p.hasFinished());
assertEmpty(true);
}
/**
* Test that missed out runs of a repeating job are dropped
*/
public void testRepeatingJobNotOwedExtraUnitsWhenStarved() {
FakeProcess p = new FakeProcess(1, 1, 1, 1);
scheduler.scheduleRepeating(Priority.LOW, p, 0, 5);
for (int i = 0; i < 4; i++) {
assertEmpty(false);
providedTimer.trigger(100);
assertTrue(p.current == i + 1);
}
assertTrue(p.hasFinished());
assertEmpty(true);
}
public void testRescheduleDelayedJobResetsTimer() {
FakeTask t = new FakeTask(5);
scheduler.scheduleDelayed(Priority.LOW, t, 50);
providedTimer.trigger(40); // @40, scheduled @50
assertEquals(50, providedTimer.getScheduledTime(), 0.001);
scheduler.scheduleDelayed(Priority.LOW, t, 50);
providedTimer.trigger(40); // @80, scheduled @90
assertFalse(t.wasExecuted);
assertEquals(90, providedTimer.getScheduledTime(), 0.001);
providedTimer.trigger(50); // Past @90
assertTrue(t.wasExecuted);
assertEmpty(true);
}
public void testRepeatingJobs() {
final int[] intervals = new int[] { 30, 40, 70 };
for (int i : intervals) {
Scheduler.IncrementalTask p = new IntervalProcess(0, i);
scheduler.scheduleRepeating(Priority.LOW, p, 0, i);
}
assertEmpty(false);
for (int i = 0; i < 500; i++) {
providedTimer.trigger();
providedTimer.tick(1);
}
}
public void testDelayedJobs() {
FakeTask task1 = new FakeTask(1);
FakeTask task2 = new FakeTask(1);
FakeTask task3 = new FakeTask(1);
FakeProcess process1 = new FakeProcess(5, 5, 5, 5, 5, 5);
FakeProcess process2 = new FakeProcess(5, 5, 5, 5, 5, 5);
// Schedule a bunch of delayed jobs and check the use of
// the timer is optimised
scheduler.scheduleDelayed(Priority.HIGH, task1, 50);
assertEquals(50, providedTimer.getScheduledTime(), 0.1);
scheduler.scheduleDelayed(Priority.LOW, task2, 60);
assertEquals(50, providedTimer.getScheduledTime(), 0.1);
scheduler.scheduleDelayed(Priority.LOW, task3, 40);
assertEquals(40, providedTimer.getScheduledTime(), 0.1);
scheduler.scheduleRepeating(Priority.LOW, process1, 50, 20);
assertEquals(40, providedTimer.getScheduledTime(), 0.1);
scheduler.scheduleRepeating(Priority.HIGH, process2, 30, 20);
assertEquals(30, providedTimer.getScheduledTime(), 0.1);
task3.prev = process2;
task1.prev = task3;
process1.prev = task1;
task2.prev = process1;
while (providedTimer.getTime() < 100) {
providedTimer.trigger(2);
}
assertTrue(task1.wasExecuted && task2.wasExecuted && task3.wasExecuted);
assertTrue(process1.hasProgressed() && process2.hasProgressed());
assertFalse(process1.hasFinished() && process2.hasFinished());
assertEmpty(false);
while (providedTimer.getTime() < 300) {
providedTimer.trigger(2);
}
assertEmpty(true);
}
public void testTaskTakingTooLong() {
// Make slow task 500ms
scheduleTask(Priority.LOW, 500);
providedTimer.trigger();
assertTasksRun(true, 0, 0);
assertEmpty(true);
// Check we have gotten call back for task too slow
assertEquals(1, executedJob.size());
assertEquals("FakeTask 500 ms", executedJob.get(0).first.toString());
assertEquals(new Integer(500), executedJob.get(0).second);
}
// HELPERS
@Override
protected void gwtSetUp() throws Exception {
providedTimer = null;
scheduler = new BrowserBackedScheduler(provider, Controller.NOOP);
scheduler.setTimeSlice(10);
tasks = new ArrayList<FakeTask>();
executedJob = new ArrayList<Pair<Schedulable, Integer>>();
scheduler.addListener(new Listener() {
@Override
public void onJobExecuted(Schedulable task, int timeSpent) {
if (timeSpent > 100) {
executedJob.add(new Pair<Schedulable, Integer>(task, timeSpent));
}
}
});
}
protected int scheduleTask(Priority priority, int duration) {
FakeTask task = new FakeTask(duration);
int index = tasks.size();
scheduler.schedule(priority, task);
tasks.add(task);
return index;
}
protected int scheduleTasks(int qty, Priority priority, int duration) {
int last = -1;
for (int i = 0; i < qty; i++) {
last = scheduleTask(priority, duration);
}
return last;
}
protected void assertTasksRun(boolean wereRun, int startIndexInclusive,
int endIndexInclusive) {
for (int i = startIndexInclusive; i <= endIndexInclusive; i++) {
boolean actual = tasks.get(i).wasExecuted;
assertEquals("Expected task " + i + " was" + (actual ? "" : " not")
+ " run", wereRun, actual);
}
}
protected void assertEmpty(boolean empty) {
assertTrue("Should " + (empty ? "" : "not ") + "be empty: "
+ scheduler.toString(), scheduler.debugIsClear() ^ !empty);
double time = providedTimer.getScheduledTime();
assertTrue("Incorrect leftover timer scheduling: " + time,
empty ? time == Double.MAX_VALUE : time < Double.MAX_VALUE);
}
protected void trigger(int numTimes) {
for (int i = 0; i < numTimes; i++) {
providedTimer.trigger();
}
}
/** {@inheritDoc} */
@Override
public String getModuleName() {
return "org.waveprotocol.wave.client.scheduler.tests";
}
}