1 // Copyright 2019 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.base.task; 6 7 import static org.junit.Assert.assertEquals; 8 import static org.junit.Assert.assertFalse; 9 import static org.junit.Assert.assertNull; 10 import static org.junit.Assert.assertTrue; 11 12 import androidx.test.filters.SmallTest; 13 14 import org.junit.After; 15 import org.junit.Assert; 16 import org.junit.Before; 17 import org.junit.Rule; 18 import org.junit.Test; 19 import org.junit.rules.ExpectedException; 20 import org.junit.runner.RunWith; 21 import org.robolectric.Robolectric; 22 import org.robolectric.android.util.concurrent.RoboExecutorService; 23 import org.robolectric.annotation.Config; 24 import org.robolectric.annotation.LooperMode; 25 import org.robolectric.shadows.ShadowLog; 26 import org.robolectric.util.Scheduler; 27 28 import org.chromium.base.Log; 29 import org.chromium.base.task.AsyncTask.Status; 30 import org.chromium.base.test.BaseRobolectricTestRunner; 31 32 import java.util.concurrent.CancellationException; 33 import java.util.concurrent.LinkedBlockingQueue; 34 import java.util.concurrent.TimeUnit; 35 36 /** Tests for {@link AsyncTask}. */ 37 @RunWith(BaseRobolectricTestRunner.class) 38 @Config(manifest = Config.NONE) 39 @LooperMode(LooperMode.Mode.LEGACY) 40 public class AsyncTaskThreadTest { 41 private static final String TAG = "AsyncTaskThreadTest"; 42 private static final boolean DEBUG = false; 43 44 private static class BlockAndGetFeedDataTask extends AsyncTask<Boolean> { 45 private final LinkedBlockingQueue<Boolean> mIncomingQueue = 46 new LinkedBlockingQueue<Boolean>(); 47 private final LinkedBlockingQueue<Boolean> mOutgoingQueue = 48 new LinkedBlockingQueue<Boolean>(); 49 private final LinkedBlockingQueue<Boolean> mInterruptedExceptionQueue = 50 new LinkedBlockingQueue<Boolean>(); 51 private Boolean mPostExecuteResult; 52 53 @Override doInBackground()54 protected Boolean doInBackground() { 55 if (DEBUG) Log.i(TAG, "doInBackground"); 56 mOutgoingQueue.add(true); 57 return blockAndGetFeedData(); 58 } 59 60 @Override onPostExecute(Boolean result)61 protected void onPostExecute(Boolean result) { 62 if (DEBUG) Log.i(TAG, "onPostExecute: " + result); 63 mPostExecuteResult = result; 64 } 65 feedData(Boolean data)66 public void feedData(Boolean data) { 67 mIncomingQueue.add(data); 68 } 69 blockAndGetFeedData()70 private Boolean blockAndGetFeedData() { 71 try { 72 return mIncomingQueue.poll(3, TimeUnit.SECONDS); 73 } catch (InterruptedException e) { 74 if (DEBUG) Log.i(TAG, "InterruptedException"); 75 mInterruptedExceptionQueue.add(true); 76 return false; 77 } 78 } 79 blockUntilDoInBackgroundStarts()80 public void blockUntilDoInBackgroundStarts() throws Exception { 81 mOutgoingQueue.poll(3, TimeUnit.SECONDS); 82 } 83 getPostExecuteResult()84 public Boolean getPostExecuteResult() { 85 return mPostExecuteResult; 86 } 87 getInterruptedExceptionQueue()88 public LinkedBlockingQueue<Boolean> getInterruptedExceptionQueue() { 89 return mInterruptedExceptionQueue; 90 } 91 } 92 93 private final BlockAndGetFeedDataTask mTask = new BlockAndGetFeedDataTask(); 94 private final RoboExecutorService mRoboExecutorService = new RoboExecutorService(); 95 private final Scheduler mBackgroundScheduler = Robolectric.getBackgroundThreadScheduler(); 96 97 @Rule public ExpectedException thrown = ExpectedException.none(); 98 AsyncTaskThreadTest()99 public AsyncTaskThreadTest() { 100 if (DEBUG) ShadowLog.stream = System.out; 101 } 102 103 @Before setUp()104 public void setUp() { 105 mBackgroundScheduler.pause(); 106 assertEquals(Status.PENDING, mTask.getStatus()); 107 } 108 109 @After tearDown()110 public void tearDown() { 111 // No unexpected interrupted exception. 112 assertNull(mTask.getInterruptedExceptionQueue().poll()); 113 Assert.assertTrue(mRoboExecutorService.shutdownNow().isEmpty()); 114 } 115 116 @Test 117 @SmallTest testCancel_ReturnsFalseOnceTaskFinishes()118 public void testCancel_ReturnsFalseOnceTaskFinishes() throws Exception { 119 // This test requires robo executor service such that we can run 120 // one background task. 121 mTask.executeOnExecutor(mRoboExecutorService); 122 123 // Ensure that the background thread is not blocked. 124 mTask.feedData(true); 125 126 mBackgroundScheduler.runOneTask(); 127 128 // Cannot cancel. The task is already run. 129 assertFalse(mTask.cancel(/* mayInterruptIfRunning= */ false)); 130 assertTrue(mTask.get()); 131 assertEquals(Boolean.TRUE, mTask.getPostExecuteResult()); 132 133 // Note: This is somewhat counter-intuitive since cancel() failed. 134 assertTrue(mTask.isCancelled()); 135 assertEquals(Status.FINISHED, mTask.getStatus()); 136 } 137 138 @Test 139 @SmallTest testCancel_InPreExecute()140 public void testCancel_InPreExecute() throws Exception { 141 // Note that background loop is paused. 142 mTask.executeOnExecutor(mRoboExecutorService); 143 144 // Ensure that the background thread is not blocked. 145 mTask.feedData(true); 146 147 // cancel() can still return true 148 assertTrue(mTask.cancel(false)); 149 150 mBackgroundScheduler.runOneTask(); 151 152 try { 153 assertTrue(mTask.get()); 154 Assert.fail(); 155 } catch (CancellationException e) { 156 // expected 157 } 158 159 assertTrue(mTask.isCancelled()); 160 assertEquals(Status.FINISHED, mTask.getStatus()); 161 } 162 163 @Test 164 @SmallTest testCancel_CanReturnTrueEvenAfterTaskStarts()165 public void testCancel_CanReturnTrueEvenAfterTaskStarts() throws Exception { 166 mTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 167 168 // Wait until the task is started. Note that data is not yet fed. 169 mTask.blockUntilDoInBackgroundStarts(); 170 assertEquals(Status.RUNNING, mTask.getStatus()); 171 172 // This reflects FutureTask#cancel() behavior. Note that the task is 173 // started but cancel can still return true. 174 assertTrue(mTask.cancel(/* mayInterruptIfRunning= */ false)); 175 176 // Continue the task. 177 mTask.feedData(true); 178 179 // get() will raise an exception although the task is started. 180 try { 181 mTask.get(); 182 Assert.fail(); 183 } catch (CancellationException e) { 184 // expected 185 } 186 assertNull(mTask.getPostExecuteResult()); // onPostExecute did not run. 187 188 assertTrue(mTask.isCancelled()); 189 assertEquals(Status.RUNNING, mTask.getStatus()); 190 } 191 192 @Test 193 @SmallTest testCancel_MayInterrupt_ReturnsFalseOnceTaskFinishes()194 public void testCancel_MayInterrupt_ReturnsFalseOnceTaskFinishes() throws Exception { 195 // This test requires robo executor service such that we can run 196 // one background task. 197 mTask.executeOnExecutor(mRoboExecutorService); 198 199 // Ensure that the background thread is not blocked. 200 mTask.feedData(true); 201 202 mBackgroundScheduler.runOneTask(); 203 204 // Cannot cancel. The task is already run. 205 assertFalse(mTask.cancel(/* mayInterruptIfRunning= */ true)); 206 assertTrue(mTask.get()); 207 assertEquals(Boolean.TRUE, mTask.getPostExecuteResult()); 208 209 // Note: This is somewhat counter-intuitive since cancel() failed. 210 assertTrue(mTask.isCancelled()); 211 212 assertEquals(Status.FINISHED, mTask.getStatus()); 213 } 214 215 @Test 216 @SmallTest testCancel_MayInterrupt_TaskIsInterrupted()217 public void testCancel_MayInterrupt_TaskIsInterrupted() throws Exception { 218 mTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 219 220 // Wait until the task is started. Note that data is not yet fed. 221 mTask.blockUntilDoInBackgroundStarts(); 222 223 assertEquals(Status.RUNNING, mTask.getStatus()); 224 225 // Cancel and interrupt the current task. 226 assertTrue(mTask.cancel(/* mayInterruptIfRunning= */ true)); 227 228 // Do not feed data here because task may finish before it gets interrupted. 229 230 // get() will raise an exception although the task is started. 231 try { 232 mTask.get(); 233 Assert.fail(); 234 } catch (CancellationException e) { 235 // expected 236 } 237 assertNull(mTask.getPostExecuteResult()); // onPostExecute did not run. 238 // Task was interrupted. 239 // Note: interruption is raised and handled in the background thread, so we need to 240 // wait here. 241 assertEquals(Boolean.TRUE, mTask.getInterruptedExceptionQueue().poll(3, TimeUnit.SECONDS)); 242 243 assertTrue(mTask.isCancelled()); 244 assertEquals(Status.RUNNING, mTask.getStatus()); 245 } 246 247 @Test 248 @SmallTest testExecuteTwiceRaisesException()249 public void testExecuteTwiceRaisesException() throws Exception { 250 mTask.executeOnExecutor(mRoboExecutorService); 251 // Note that background loop is paused. 252 try { 253 // A second run should cause an exception. 254 mTask.executeOnExecutor(mRoboExecutorService); 255 Assert.fail(); 256 } catch (IllegalStateException e) { 257 // expected 258 } 259 mBackgroundScheduler.runOneTask(); // ensure to pass tearDown check 260 } 261 } 262