summaryrefslogtreecommitdiff
path: root/tests/src/com/android/dialer/util/FakeAsyncTaskExecutor.java
blob: 064587e4b58d1aad1383e87193f3108364b6773b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * 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 com.android.dialer.util;

import android.app.Instrumentation;
import android.os.AsyncTask;

import com.android.contacts.util.AsyncTaskExecutor;
import com.android.contacts.util.AsyncTaskExecutors;
import com.google.common.collect.Lists;

import junit.framework.Assert;

import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;

import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;

/**
 * Test implementation of AsyncTaskExecutor.
 * <p>
 * This class is thread-safe. As per the contract of the AsyncTaskExecutor, the submit methods must
 * be called from the main ui thread, however the other public methods may be called from any thread
 * (most commonly the test thread).
 * <p>
 * Tasks submitted to this executor will not be run immediately. Rather they will be stored in a
 * list of submitted tasks, where they can be examined. They can also be run on-demand using the run
 * methods, so that different ordering of AsyncTask execution can be simulated.
 * <p>
 * The onPreExecute method of the submitted AsyncTask will be called synchronously during the
 * call to {@link #submit(Object, AsyncTask, Object...)}.
 */
@ThreadSafe
public class FakeAsyncTaskExecutor implements AsyncTaskExecutor {
    private static final long DEFAULT_TIMEOUT_MS = 10000;

    /** The maximum length of time in ms to wait for tasks to execute during tests. */
    private final long mTimeoutMs = DEFAULT_TIMEOUT_MS;

    private final Object mLock = new Object();
    @GuardedBy("mLock") private final List<SubmittedTask> mSubmittedTasks = Lists.newArrayList();

    private final DelayedExecutor mBlockingExecutor = new DelayedExecutor();
    private final Instrumentation mInstrumentation;

    /** Create a fake AsyncTaskExecutor for use in unit tests. */
    public FakeAsyncTaskExecutor(Instrumentation instrumentation) {
        Assert.assertNotNull(instrumentation);
        mInstrumentation = instrumentation;
    }

    /** Encapsulates an async task with the params and identifier it was submitted with. */
    public interface SubmittedTask {
        Runnable getRunnable();
        Object getIdentifier();
        AsyncTask<?, ?, ?> getAsyncTask();
    }

    private static final class SubmittedTaskImpl implements SubmittedTask {
        private final Object mIdentifier;
        private final Runnable mRunnable;
        private final AsyncTask<?, ?, ?> mAsyncTask;

        public SubmittedTaskImpl(Object identifier, Runnable runnable,
                AsyncTask<?, ?, ?> asyncTask) {
            mIdentifier = identifier;
            mRunnable = runnable;
            mAsyncTask = asyncTask;
        }

        @Override
        public Object getIdentifier() {
            return mIdentifier;
        }

        @Override
        public Runnable getRunnable() {
            return mRunnable;
        }

        @Override
        public AsyncTask<?, ?, ?> getAsyncTask() {
            return mAsyncTask;
        }

        @Override
        public String toString() {
            return "SubmittedTaskImpl [mIdentifier=" + mIdentifier + "]";
        }
    }

    private class DelayedExecutor implements Executor {
        private final Object mNextLock = new Object();
        @GuardedBy("mNextLock") private Object mNextIdentifier;
        @GuardedBy("mNextLock") private AsyncTask<?, ?, ?> mNextTask;

        @Override
        public void execute(Runnable command) {
            synchronized (mNextLock) {
                Assert.assertNotNull(mNextTask);
                mSubmittedTasks.add(new SubmittedTaskImpl(mNextIdentifier,
                        command, mNextTask));
                mNextIdentifier = null;
                mNextTask = null;
            }
        }

        public <T> AsyncTask<T, ?, ?> submit(Object identifier,
                AsyncTask<T, ?, ?> task, T... params) {
            synchronized (mNextLock) {
                Assert.assertNull(mNextIdentifier);
                Assert.assertNull(mNextTask);
                mNextIdentifier = identifier;
                Assert.assertNotNull("Already had a valid task.\n"
                        + "Are you calling AsyncTaskExecutor.submit(...) from within the "
                        + "onPreExecute() method of another task being submitted?\n"
                        + "Sorry!  Not that's not supported.", task);
                mNextTask = task;
            }
            return task.executeOnExecutor(this, params);
        }
    }

    @Override
    public <T> AsyncTask<T, ?, ?> submit(Object identifier, AsyncTask<T, ?, ?> task, T... params) {
        AsyncTaskExecutors.checkCalledFromUiThread();
        return mBlockingExecutor.submit(identifier, task, params);
    }

    /**
     * Runs a single task matching the given identifier.
     * <p>
     * Removes the matching task from the list of submitted tasks, then runs it. The executor used
     * to execute this async task will be a same-thread executor.
     * <p>
     * Fails if there was not exactly one task matching the given identifier.
     * <p>
     * This method blocks until the AsyncTask has completely finished executing.
     */
    public void runTask(Object identifier) throws InterruptedException {
        List<SubmittedTask> tasks = getSubmittedTasksByIdentifier(identifier, true);
        Assert.assertEquals("Expected one task " + identifier + ", got " + tasks, 1, tasks.size());
        runTask(tasks.get(0));
    }

    /**
     * Runs all tasks whose identifier matches the given identifier.
     * <p>
     * Removes all matching tasks from the list of submitted tasks, and runs them. The executor used
     * to execute these async tasks will be a same-thread executor.
     * <p>
     * Fails if there were no tasks matching the given identifier.
     * <p>
     * This method blocks until the AsyncTask objects have completely finished executing.
     */
    public void runAllTasks(Object identifier) throws InterruptedException {
        List<SubmittedTask> tasks = getSubmittedTasksByIdentifier(identifier, true);
        Assert.assertTrue("There were no tasks with identifier " + identifier, tasks.size() > 0);
        for (SubmittedTask task : tasks) {
            runTask(task);
        }
    }

    /**
     * Executes a single {@link SubmittedTask}.
     * <p>
     * Blocks until the task has completed running.
     */
    private <T> void runTask(final SubmittedTask submittedTask) throws InterruptedException {
        submittedTask.getRunnable().run();
        // Block until the onPostExecute or onCancelled has finished.
        // Unfortunately we can't be sure when the AsyncTask will have posted its result handling
        // code to the main ui thread, the best we can do is wait for the Status to be FINISHED.
        final CountDownLatch latch = new CountDownLatch(1);
        class AsyncTaskHasFinishedRunnable implements Runnable {
            @Override
            public void run() {
                if (submittedTask.getAsyncTask().getStatus() == AsyncTask.Status.FINISHED) {
                    latch.countDown();
                } else {
                    mInstrumentation.waitForIdle(this);
                }
            }
        }
        mInstrumentation.waitForIdle(new AsyncTaskHasFinishedRunnable());
        Assert.assertTrue(latch.await(mTimeoutMs, TimeUnit.MILLISECONDS));
    }

    private List<SubmittedTask> getSubmittedTasksByIdentifier(
            Object identifier, boolean remove) {
        Assert.assertNotNull(identifier);
        List<SubmittedTask> results = Lists.newArrayList();
        synchronized (mLock) {
            Iterator<SubmittedTask> iter = mSubmittedTasks.iterator();
            while (iter.hasNext()) {
                SubmittedTask task = iter.next();
                if (identifier.equals(task.getIdentifier())) {
                    results.add(task);
                    iter.remove();
                }
            }
        }
        return results;
    }

    /** Get a factory that will return this instance - useful for testing. */
    public AsyncTaskExecutors.AsyncTaskExecutorFactory getFactory() {
        return new AsyncTaskExecutors.AsyncTaskExecutorFactory() {
            @Override
            public AsyncTaskExecutor createAsyncTaskExeuctor() {
                return FakeAsyncTaskExecutor.this;
            }
        };
    }
}