1 // Copyright 2020 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 org.junit.After; 8 import org.junit.Assert; 9 import org.junit.Before; 10 import org.junit.Rule; 11 import org.junit.Test; 12 import org.junit.runner.RunWith; 13 import org.robolectric.annotation.Config; 14 15 import org.chromium.base.test.BaseRobolectricTestRunner; 16 import org.chromium.base.test.util.JniMocker; 17 18 import java.util.List; 19 import java.util.concurrent.CountDownLatch; 20 import java.util.concurrent.Executor; 21 import java.util.concurrent.ExecutorService; 22 import java.util.concurrent.Executors; 23 import java.util.concurrent.TimeUnit; 24 import java.util.concurrent.atomic.AtomicInteger; 25 26 /** 27 * Unit tests for {@link SequencedTaskRunnerImpl} that focuses on the transition/migration that 28 * happens as native initializes. 29 */ 30 @RunWith(BaseRobolectricTestRunner.class) 31 @Config(manifest = Config.NONE) 32 public class SequencedTaskRunnerTaskMigrationTest { 33 @Rule public JniMocker mMocker = new JniMocker(); 34 35 // It might be tempting to use fake executor similar to Robolectric's scheduler that is driven 36 // from the test's main thread. Unfortunately this approach means that only two states of the 37 // TaskRunner are observable: the posted task resides in the internal queue or the task is 38 // removed from the queue and has its execution completed. The tricky case is the another state: 39 // the task is already removed but is not yet completed. This can only be modelled with real 40 // concurrency. 41 private final ExecutorService mConcurrentExecutor = Executors.newCachedThreadPool(); 42 43 @Before setUp()44 public void setUp() throws Exception { 45 PostTask.setPrenativeThreadPoolExecutorForTesting(mConcurrentExecutor); 46 } 47 48 @After tearDown()49 public void tearDown() throws Exception { 50 PostTask.resetPrenativeThreadPoolExecutorForTesting(); 51 // Ensure that no stuck threads left behind. 52 List<Runnable> queuedRunnables = mConcurrentExecutor.shutdownNow(); 53 Assert.assertTrue("Some task is stuck in thread pool queue", queuedRunnables.isEmpty()); 54 // Termination will be immediate if tests aren't broken. Generous timeout prevents test 55 // from being stuck forever. 56 Assert.assertTrue( 57 "Some task is stuck in thread pool", 58 mConcurrentExecutor.awaitTermination(10, TimeUnit.SECONDS)); 59 } 60 61 @Test nativeRunnerShouldNotExecuteTasksIfJavaThreadIsWorking()62 public void nativeRunnerShouldNotExecuteTasksIfJavaThreadIsWorking() { 63 Executor noopExecutor = runnable -> {}; 64 FakeTaskRunnerImplNatives fakeTaskRunnerNatives = 65 new FakeTaskRunnerImplNatives(noopExecutor); 66 mMocker.mock(TaskRunnerImplJni.TEST_HOOKS, fakeTaskRunnerNatives); 67 BlockingTask preNativeTask = new BlockingTask(); 68 SequencedTaskRunnerImpl taskRunner = new SequencedTaskRunnerImpl(TaskTraits.USER_VISIBLE); 69 70 taskRunner.postTask(preNativeTask); 71 // Dummy task that is planned to be executed on native pool. 72 taskRunner.postTask(() -> {}); 73 74 // Ensure that first task is running on pre-native thread pool: avoid race between 75 // starting the task and requesting native task runner's init. 76 preNativeTask.awaitTaskStarted(); 77 taskRunner.initNativeTaskRunner(); 78 79 Assert.assertFalse( 80 "Native task should not start before java task completion", 81 fakeTaskRunnerNatives.hasReceivedTasks()); 82 } 83 84 @Test pendingTasksShouldBeExecutedOnNativeRunnerAfterInit()85 public void pendingTasksShouldBeExecutedOnNativeRunnerAfterInit() { 86 FakeTaskRunnerImplNatives fakeTaskRunnerNatives = 87 new FakeTaskRunnerImplNatives(mConcurrentExecutor); 88 mMocker.mock(TaskRunnerImplJni.TEST_HOOKS, fakeTaskRunnerNatives); 89 BlockingTask preNativeTask = new BlockingTask(); 90 AwaitableTask nativeTask = new AwaitableTask(); 91 SequencedTaskRunnerImpl taskRunner = new SequencedTaskRunnerImpl(TaskTraits.USER_VISIBLE); 92 93 taskRunner.postTask(preNativeTask); 94 taskRunner.postTask(nativeTask); 95 96 // Ensure that first task is running on pre-native thread pool: avoid race between 97 // starting the task and requesting native task runner's init. 98 preNativeTask.awaitTaskStarted(); 99 taskRunner.initNativeTaskRunner(); 100 // Allow pre-native task to complete. Second task is going to be run on native pool because 101 // native task runner is available. 102 preNativeTask.allowComplete(); 103 104 // Wait for second task to be started: avoid race between submitting task to the native task 105 // runner and checking the state of the latter in assertion below. 106 nativeTask.awaitTaskStarted(); 107 108 Assert.assertTrue( 109 "Second task should run on the native pool", 110 fakeTaskRunnerNatives.hasReceivedTasks()); 111 } 112 113 @Test taskPostedAfterNativeInitShouldRunInNativePool()114 public void taskPostedAfterNativeInitShouldRunInNativePool() { 115 FakeTaskRunnerImplNatives fakeTaskRunnerNatives = 116 new FakeTaskRunnerImplNatives(mConcurrentExecutor); 117 mMocker.mock(TaskRunnerImplJni.TEST_HOOKS, fakeTaskRunnerNatives); 118 119 SequencedTaskRunnerImpl taskRunner = new SequencedTaskRunnerImpl(TaskTraits.USER_VISIBLE); 120 taskRunner.initNativeTaskRunner(); 121 122 AwaitableTask nativeTask = new AwaitableTask(); 123 taskRunner.postTask(nativeTask); 124 125 // Wait for the task to be started: avoid race between submitting task to the native task 126 // runner and checking the state of the latter in assertion below. 127 nativeTask.awaitTaskStarted(); 128 Assert.assertTrue( 129 "Task should run on the native pool", fakeTaskRunnerNatives.hasReceivedTasks()); 130 } 131 awaitNoInterruptedException(CountDownLatch taskLatch)132 private static void awaitNoInterruptedException(CountDownLatch taskLatch) { 133 try { 134 // Generous timeout prevents test from being stuck forever. Actual delay is going to 135 // be a few milliseconds. 136 Assert.assertTrue( 137 "Timed out waiting for latch to count down", 138 taskLatch.await(10, TimeUnit.SECONDS)); 139 } catch (InterruptedException e) { 140 Thread.currentThread().interrupt(); 141 } 142 } 143 144 private static class AwaitableTask implements Runnable { 145 private final CountDownLatch mTaskStartedLatch = new CountDownLatch(1); 146 147 @Override run()148 public void run() { 149 mTaskStartedLatch.countDown(); 150 } 151 awaitTaskStarted()152 public void awaitTaskStarted() { 153 awaitNoInterruptedException(mTaskStartedLatch); 154 } 155 } 156 157 private static class BlockingTask extends AwaitableTask { 158 private final CountDownLatch mTaskAllowedToComplete = new CountDownLatch(1); 159 160 @Override run()161 public void run() { 162 super.run(); 163 awaitNoInterruptedException(mTaskAllowedToComplete); 164 } 165 allowComplete()166 public void allowComplete() { 167 mTaskAllowedToComplete.countDown(); 168 } 169 } 170 171 private static class FakeTaskRunnerImplNatives implements TaskRunnerImpl.Natives { 172 private final AtomicInteger mReceivedTasksCount = new AtomicInteger(); 173 private final Executor mExecutor; 174 FakeTaskRunnerImplNatives(Executor executor)175 public FakeTaskRunnerImplNatives(Executor executor) { 176 mExecutor = executor; 177 } 178 179 @Override init(int taskRunnerType, int taskTraits)180 public long init(int taskRunnerType, int taskTraits) { 181 return 1; 182 } 183 184 @Override destroy(long nativeTaskRunnerAndroid)185 public void destroy(long nativeTaskRunnerAndroid) {} 186 187 @Override postDelayedTask( long nativeTaskRunnerAndroid, Runnable task, long delay, String runnableClassName)188 public void postDelayedTask( 189 long nativeTaskRunnerAndroid, Runnable task, long delay, String runnableClassName) { 190 mReceivedTasksCount.incrementAndGet(); 191 mExecutor.execute(task); 192 } 193 194 @Override belongsToCurrentThread(long nativeTaskRunnerAndroid)195 public boolean belongsToCurrentThread(long nativeTaskRunnerAndroid) { 196 return false; 197 } 198 hasReceivedTasks()199 public boolean hasReceivedTasks() { 200 return mReceivedTasksCount.get() > 0; 201 } 202 } 203 } 204