FakeAsyncTaskExecutor.java revision 94b10b530c0fc297e2974e57e094c500d3ee6003
1/*
2 * Copyright (C) 2011 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.dialer.util;
18
19import android.app.Instrumentation;
20import android.os.AsyncTask;
21
22import com.android.contacts.util.AsyncTaskExecutor;
23import com.android.contacts.util.AsyncTaskExecutors;
24import com.google.common.collect.Lists;
25
26import junit.framework.Assert;
27
28import java.util.Iterator;
29import java.util.List;
30import java.util.concurrent.CountDownLatch;
31import java.util.concurrent.Executor;
32import java.util.concurrent.TimeUnit;
33
34import javax.annotation.concurrent.GuardedBy;
35import javax.annotation.concurrent.ThreadSafe;
36
37/**
38 * Test implementation of AsyncTaskExecutor.
39 * <p>
40 * This class is thread-safe. As per the contract of the AsyncTaskExecutor, the submit methods must
41 * be called from the main ui thread, however the other public methods may be called from any thread
42 * (most commonly the test thread).
43 * <p>
44 * Tasks submitted to this executor will not be run immediately. Rather they will be stored in a
45 * list of submitted tasks, where they can be examined. They can also be run on-demand using the run
46 * methods, so that different ordering of AsyncTask execution can be simulated.
47 * <p>
48 * The onPreExecute method of the submitted AsyncTask will be called synchronously during the
49 * call to {@link #submit(Object, AsyncTask, Object...)}.
50 */
51@ThreadSafe
52public class FakeAsyncTaskExecutor implements AsyncTaskExecutor {
53    private static final long DEFAULT_TIMEOUT_MS = 10000;
54
55    /** The maximum length of time in ms to wait for tasks to execute during tests. */
56    private final long mTimeoutMs = DEFAULT_TIMEOUT_MS;
57
58    private final Object mLock = new Object();
59    @GuardedBy("mLock") private final List<SubmittedTask> mSubmittedTasks = Lists.newArrayList();
60
61    private final DelayedExecutor mBlockingExecutor = new DelayedExecutor();
62    private final Instrumentation mInstrumentation;
63
64    /** Create a fake AsyncTaskExecutor for use in unit tests. */
65    public FakeAsyncTaskExecutor(Instrumentation instrumentation) {
66        Assert.assertNotNull(instrumentation);
67        mInstrumentation = instrumentation;
68    }
69
70    /** Encapsulates an async task with the params and identifier it was submitted with. */
71    public interface SubmittedTask {
72        Runnable getRunnable();
73        Object getIdentifier();
74        AsyncTask<?, ?, ?> getAsyncTask();
75    }
76
77    private static final class SubmittedTaskImpl implements SubmittedTask {
78        private final Object mIdentifier;
79        private final Runnable mRunnable;
80        private final AsyncTask<?, ?, ?> mAsyncTask;
81
82        public SubmittedTaskImpl(Object identifier, Runnable runnable,
83                AsyncTask<?, ?, ?> asyncTask) {
84            mIdentifier = identifier;
85            mRunnable = runnable;
86            mAsyncTask = asyncTask;
87        }
88
89        @Override
90        public Object getIdentifier() {
91            return mIdentifier;
92        }
93
94        @Override
95        public Runnable getRunnable() {
96            return mRunnable;
97        }
98
99        @Override
100        public AsyncTask<?, ?, ?> getAsyncTask() {
101            return mAsyncTask;
102        }
103
104        @Override
105        public String toString() {
106            return "SubmittedTaskImpl [mIdentifier=" + mIdentifier + "]";
107        }
108    }
109
110    private class DelayedExecutor implements Executor {
111        private final Object mNextLock = new Object();
112        @GuardedBy("mNextLock") private Object mNextIdentifier;
113        @GuardedBy("mNextLock") private AsyncTask<?, ?, ?> mNextTask;
114
115        @Override
116        public void execute(Runnable command) {
117            synchronized (mNextLock) {
118                Assert.assertNotNull(mNextTask);
119                mSubmittedTasks.add(new SubmittedTaskImpl(mNextIdentifier,
120                        command, mNextTask));
121                mNextIdentifier = null;
122                mNextTask = null;
123            }
124        }
125
126        public <T> AsyncTask<T, ?, ?> submit(Object identifier,
127                AsyncTask<T, ?, ?> task, T... params) {
128            synchronized (mNextLock) {
129                Assert.assertNull(mNextIdentifier);
130                Assert.assertNull(mNextTask);
131                mNextIdentifier = identifier;
132                Assert.assertNotNull("Already had a valid task.\n"
133                        + "Are you calling AsyncTaskExecutor.submit(...) from within the "
134                        + "onPreExecute() method of another task being submitted?\n"
135                        + "Sorry!  Not that's not supported.", task);
136                mNextTask = task;
137            }
138            return task.executeOnExecutor(this, params);
139        }
140    }
141
142    @Override
143    public <T> AsyncTask<T, ?, ?> submit(Object identifier, AsyncTask<T, ?, ?> task, T... params) {
144        AsyncTaskExecutors.checkCalledFromUiThread();
145        return mBlockingExecutor.submit(identifier, task, params);
146    }
147
148    /**
149     * Runs a single task matching the given identifier.
150     * <p>
151     * Removes the matching task from the list of submitted tasks, then runs it. The executor used
152     * to execute this async task will be a same-thread executor.
153     * <p>
154     * Fails if there was not exactly one task matching the given identifier.
155     * <p>
156     * This method blocks until the AsyncTask has completely finished executing.
157     */
158    public void runTask(Object identifier) throws InterruptedException {
159        List<SubmittedTask> tasks = getSubmittedTasksByIdentifier(identifier, true);
160        Assert.assertEquals("Expected one task " + identifier + ", got " + tasks, 1, tasks.size());
161        runTask(tasks.get(0));
162    }
163
164    /**
165     * Runs all tasks whose identifier matches the given identifier.
166     * <p>
167     * Removes all matching tasks from the list of submitted tasks, and runs them. The executor used
168     * to execute these async tasks will be a same-thread executor.
169     * <p>
170     * Fails if there were no tasks matching the given identifier.
171     * <p>
172     * This method blocks until the AsyncTask objects have completely finished executing.
173     */
174    public void runAllTasks(Object identifier) throws InterruptedException {
175        List<SubmittedTask> tasks = getSubmittedTasksByIdentifier(identifier, true);
176        Assert.assertTrue("There were no tasks with identifier " + identifier, tasks.size() > 0);
177        for (SubmittedTask task : tasks) {
178            runTask(task);
179        }
180    }
181
182    /**
183     * Executes a single {@link SubmittedTask}.
184     * <p>
185     * Blocks until the task has completed running.
186     */
187    private <T> void runTask(final SubmittedTask submittedTask) throws InterruptedException {
188        submittedTask.getRunnable().run();
189        // Block until the onPostExecute or onCancelled has finished.
190        // Unfortunately we can't be sure when the AsyncTask will have posted its result handling
191        // code to the main ui thread, the best we can do is wait for the Status to be FINISHED.
192        final CountDownLatch latch = new CountDownLatch(1);
193        class AsyncTaskHasFinishedRunnable implements Runnable {
194            @Override
195            public void run() {
196                if (submittedTask.getAsyncTask().getStatus() == AsyncTask.Status.FINISHED) {
197                    latch.countDown();
198                } else {
199                    mInstrumentation.waitForIdle(this);
200                }
201            }
202        }
203        mInstrumentation.waitForIdle(new AsyncTaskHasFinishedRunnable());
204        Assert.assertTrue(latch.await(mTimeoutMs, TimeUnit.MILLISECONDS));
205    }
206
207    private List<SubmittedTask> getSubmittedTasksByIdentifier(
208            Object identifier, boolean remove) {
209        Assert.assertNotNull(identifier);
210        List<SubmittedTask> results = Lists.newArrayList();
211        synchronized (mLock) {
212            Iterator<SubmittedTask> iter = mSubmittedTasks.iterator();
213            while (iter.hasNext()) {
214                SubmittedTask task = iter.next();
215                if (identifier.equals(task.getIdentifier())) {
216                    results.add(task);
217                    iter.remove();
218                }
219            }
220        }
221        return results;
222    }
223
224    /** Get a factory that will return this instance - useful for testing. */
225    public AsyncTaskExecutors.AsyncTaskExecutorFactory getFactory() {
226        return new AsyncTaskExecutors.AsyncTaskExecutorFactory() {
227            @Override
228            public AsyncTaskExecutor createAsyncTaskExeuctor() {
229                return FakeAsyncTaskExecutor.this;
230            }
231        };
232    }
233}
234