1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email;
18
19import android.os.Handler;
20import android.os.Message;
21import android.test.AndroidTestCase;
22import android.test.suitebuilder.annotation.SmallTest;
23
24import com.android.mail.utils.Clock;
25import com.android.mail.utils.Throttle;
26
27import java.util.Timer;
28import java.util.TimerTask;
29import java.util.concurrent.BlockingQueue;
30import java.util.concurrent.LinkedBlockingQueue;
31
32@SmallTest
33public class ThrottleTest extends AndroidTestCase {
34    private static final int MIN_TIMEOUT = 100;
35    private static final int MAX_TIMEOUT = 500;
36
37    private final CountingRunnable mRunnable = new CountingRunnable();
38    private final MockClock mClock = new MockClock();
39    private final MockTimer mTimer = new MockTimer(mClock);
40    private final Throttle mTarget = new Throttle("test", mRunnable, new CallItNowHandler(),
41            MIN_TIMEOUT, MAX_TIMEOUT, mClock, mTimer);
42
43    /**
44     * Advance the clock.
45     */
46    private void advanceClock(int milliseconds) {
47        mClock.advance(milliseconds);
48        mTimer.runExpiredTasks();
49    }
50
51    /**
52     * Gets two events.  They're far apart enough that the timeout won't be extended.
53     */
54    public void testSingleCalls() {
55        // T + 0
56        mTarget.onEvent();
57        advanceClock(0);
58        assertEquals(0, mRunnable.mCounter);
59
60        // T + 99
61        advanceClock(99);
62        assertEquals(0, mRunnable.mCounter); // Still not called
63
64        // T + 100
65        advanceClock(1);
66        assertEquals(1, mRunnable.mCounter); // Called
67
68        // T + 10100
69        advanceClock(10000);
70        assertEquals(1, mRunnable.mCounter);
71
72        // Do the same thing again.  Should work in the same way.
73
74        // T + 0
75        mTarget.onEvent();
76        advanceClock(0);
77        assertEquals(1, mRunnable.mCounter);
78
79        // T + 99
80        advanceClock(99);
81        assertEquals(1, mRunnable.mCounter); // Still not called
82
83        // T + 100
84        advanceClock(1);
85        assertEquals(2, mRunnable.mCounter); // Called
86
87        // T + 10100
88        advanceClock(10000);
89        assertEquals(2, mRunnable.mCounter);
90    }
91
92    /**
93     * Gets 5 events in a row in a short period.
94     *
95     * We only roughly check the consequence, as the detailed spec isn't really important.
96     * Here, we check if the timeout is extended, and the callback get called less than
97     * 5 times.
98     */
99    public void testMultiCalls() {
100        mTarget.onEvent();
101        advanceClock(1);
102        mTarget.onEvent();
103        advanceClock(1);
104        mTarget.onEvent();
105        advanceClock(1);
106        mTarget.onEvent();
107        advanceClock(1);
108        mTarget.onEvent();
109
110        // Timeout should be extended
111        assertTrue(mTarget.getTimeoutForTest() > 100);
112
113        // Shouldn't result in 5 callback calls.
114        advanceClock(2000);
115        assertTrue(mRunnable.mCounter < 5);
116    }
117
118    public void testUpdateTimeout() {
119        // Check initial value
120        assertEquals(100, mTarget.getTimeoutForTest());
121
122        // First call -- won't change the timeout
123        mTarget.updateTimeout();
124        assertEquals(100, mTarget.getTimeoutForTest());
125
126        // Call again in 10 ms -- will extend timeout.
127        mClock.advance(10);
128        mTarget.updateTimeout();
129        assertEquals(200, mTarget.getTimeoutForTest());
130
131        // Call again in TIMEOUT_EXTEND_INTERAVL ms -- will extend timeout.
132        mClock.advance(Throttle.TIMEOUT_EXTEND_INTERVAL);
133        mTarget.updateTimeout();
134        assertEquals(400, mTarget.getTimeoutForTest());
135
136        // Again -- timeout reaches max.
137        mClock.advance(Throttle.TIMEOUT_EXTEND_INTERVAL);
138        mTarget.updateTimeout();
139        assertEquals(500, mTarget.getTimeoutForTest());
140
141        // Call in TIMEOUT_EXTEND_INTERAVL + 1 ms -- timeout will get reset.
142        mClock.advance(Throttle.TIMEOUT_EXTEND_INTERVAL + 1);
143        mTarget.updateTimeout();
144        assertEquals(100, mTarget.getTimeoutForTest());
145    }
146
147    private static class CountingRunnable implements Runnable {
148        public int mCounter;
149
150        @Override
151        public void run() {
152            mCounter++;
153        }
154    }
155
156    /**
157     * Dummy {@link Handler} that executes {@link Runnable}s passed to {@link Handler#post}
158     * immediately on the current thread.
159     */
160    private static class CallItNowHandler extends Handler {
161        @Override
162        public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
163            msg.getCallback().run();
164            return true;
165        }
166    }
167
168    /**
169     * Substitute for {@link Timer} that works based on the provided {@link Clock}.
170     */
171    private static class MockTimer extends Timer {
172        private final Clock mClock;
173
174        private static class Entry {
175            public long mScheduledTime;
176            public TimerTask mTask;
177        }
178
179        private final BlockingQueue<Entry> mTasks = new LinkedBlockingQueue<Entry>();
180
181        public MockTimer(Clock clock) {
182            mClock = clock;
183        }
184
185        @Override
186        public void schedule(TimerTask task, long delay) {
187            if (delay == 0) {
188                task.run();
189            } else {
190                Entry e = new Entry();
191                e.mScheduledTime = mClock.getTime() + delay;
192                e.mTask = task;
193                mTasks.offer(e);
194            }
195        }
196
197        /**
198         * {@link MockTimer} can't know when the clock advances.  This method must be called
199         * whenever the (mock) current time changes.
200         */
201        public void runExpiredTasks() {
202            while (!mTasks.isEmpty()) {
203                Entry e = mTasks.peek();
204                if (e.mScheduledTime > mClock.getTime()) {
205                    break;
206                }
207                e.mTask.run();
208                mTasks.poll();
209            }
210        }
211    }
212}
213