1 /* 2 * Copyright (C) 2023 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 17 package com.android.adservices.service.common; 18 19 import static com.google.common.truth.Truth.assertThat; 20 21 import com.android.adservices.concurrency.AdServicesExecutors; 22 23 import com.google.common.util.concurrent.FluentFuture; 24 import com.google.common.util.concurrent.ListeningExecutorService; 25 26 import org.junit.Test; 27 28 import java.util.concurrent.CountDownLatch; 29 import java.util.concurrent.atomic.AtomicInteger; 30 import java.util.function.Supplier; 31 32 public class SingletonRunnerTest { 33 private final ListeningExecutorService mRunnerExecutor = 34 AdServicesExecutors.getBlockingExecutor(); 35 36 @Test testShouldRunTask()37 public void testShouldRunTask() throws Exception { 38 final AtomicInteger invocationCount = new AtomicInteger(0); 39 SingletonRunner<Integer> singletonRunner = 40 new SingletonRunner<>( 41 "Test task", 42 shouldStop -> 43 FluentFuture.from( 44 mRunnerExecutor.submit(invocationCount::incrementAndGet))); 45 singletonRunner.runSingleInstance().get(); 46 assertThat(invocationCount.get()).isEqualTo(1); 47 } 48 49 @Test testShouldReturnSameFutureIfTaskIsNotCompleted()50 public void testShouldReturnSameFutureIfTaskIsNotCompleted() throws Exception { 51 TestTaskRunner testTaskRunner = new TestTaskRunner(); 52 53 SingletonRunner<Integer> singletonRunner = new SingletonRunner<>("Test task", testTaskRunner); 54 assertThat(singletonRunner.runSingleInstance()) 55 .isSameInstanceAs(singletonRunner.runSingleInstance()); 56 } 57 58 @Test testShouldReturnDifferentFuturesIfTaskIsCompleted()59 public void testShouldReturnDifferentFuturesIfTaskIsCompleted() throws Exception { 60 TestTaskRunner testTaskRunner = new TestTaskRunner(); 61 62 SingletonRunner<Integer> singletonRunner = new SingletonRunner<>("Test task", testTaskRunner); 63 FluentFuture<Integer> firstCall = singletonRunner.runSingleInstance(); 64 testTaskRunner.mTestHasDecidedToStop.countDown(); 65 // Waiting for first run to complete 66 firstCall.get(); 67 68 assertThat(firstCall).isNotSameInstanceAs(singletonRunner.runSingleInstance()); 69 } 70 71 @Test testShouldRunOnce()72 public void testShouldRunOnce() throws Exception { 73 TestTaskRunner testTaskRunner = new TestTaskRunner(); 74 75 SingletonRunner<Integer> singletonRunner = new SingletonRunner<>("Test task", testTaskRunner); 76 FluentFuture<Integer> firstRunResult = singletonRunner.runSingleInstance(); 77 singletonRunner.runSingleInstance(); 78 testTaskRunner.mTestHasDecidedToStop.countDown(); 79 80 assertThat(firstRunResult.get()).isEqualTo(1); 81 assertThat(testTaskRunner.mInvocationCount.get()).isEqualTo(1); 82 } 83 84 @Test testShouldRunOnceMultipleStartStops()85 public void testShouldRunOnceMultipleStartStops() throws Exception { 86 TestTaskRunner testTaskRunner = new TestTaskRunner(); 87 SingletonRunner<Integer> singletonRunner = new SingletonRunner<>("Test task", testTaskRunner); 88 89 FluentFuture<Integer> firstRunResult = singletonRunner.runSingleInstance(); 90 FluentFuture<Integer> secondRunResult = singletonRunner.runSingleInstance(); 91 singletonRunner.stopWork(); 92 FluentFuture<Integer> thirdRunResult = singletonRunner.runSingleInstance(); 93 94 // The first task will stop only after this latch count has reached zero. 95 testTaskRunner.mTestHasDecidedToStop.countDown(); 96 97 assertThat(firstRunResult).isSameInstanceAs(secondRunResult); 98 assertThat(secondRunResult).isSameInstanceAs(thirdRunResult); 99 assertThat(firstRunResult.get()).isEqualTo(1); 100 assertThat(testTaskRunner.mInvocationCount.get()).isEqualTo(1); 101 } 102 103 @Test testShouldRunAgain()104 public void testShouldRunAgain() throws Exception { 105 TestTaskRunner testTaskRunner = new TestTaskRunner(); 106 SingletonRunner<Integer> singletonRunner = new SingletonRunner<>("Test task", testTaskRunner); 107 108 FluentFuture<Integer> firstRunResult = singletonRunner.runSingleInstance(); 109 testTaskRunner.mTestHasDecidedToStop.countDown(); 110 111 assertThat(firstRunResult.get()).isEqualTo(1); 112 113 // once the first runner completed the second can start 114 FluentFuture<Integer> secondRunResult = singletonRunner.runSingleInstance(); 115 116 // Same runnable invoked twice maintaining state 117 assertThat(secondRunResult.get()).isEqualTo(2); 118 } 119 120 @Test testShouldStopTask()121 public void testShouldStopTask() throws Exception { 122 final CountDownLatch testHasDecidedToStop = new CountDownLatch(1); 123 final AtomicInteger counter = new AtomicInteger(0); 124 SingletonRunner<Integer> singletonRunner = 125 new SingletonRunner<>( 126 "", 127 shouldStop -> 128 FluentFuture.from( 129 mRunnerExecutor.submit( 130 () -> { 131 counter.incrementAndGet(); 132 133 try { 134 testHasDecidedToStop.await(); 135 } catch (InterruptedException e) { 136 // Intentionally ignored 137 } 138 139 if (!shouldStop.get()) { 140 counter.incrementAndGet(); 141 } 142 143 return counter.get(); 144 }))); 145 FluentFuture<Integer> taskResult = singletonRunner.runSingleInstance(); 146 147 singletonRunner.stopWork(); 148 testHasDecidedToStop.countDown(); 149 150 assertThat(taskResult.get()).isEqualTo(1); 151 } 152 153 class TestTaskRunner implements SingletonRunner.InterruptableTaskRunner<Integer> { 154 final AtomicInteger mInvocationCount = new AtomicInteger(0); 155 final CountDownLatch mTestHasDecidedToStop = new CountDownLatch(1); 156 157 @Override run(Supplier<Boolean> stopFlagChecker)158 public FluentFuture<Integer> run(Supplier<Boolean> stopFlagChecker) { 159 return FluentFuture.from( 160 mRunnerExecutor.submit( 161 () -> { 162 mInvocationCount.incrementAndGet(); 163 try { 164 mTestHasDecidedToStop.await(); 165 } catch (InterruptedException e) { 166 // Ignored 167 } 168 return mInvocationCount.get(); 169 })); 170 } 171 } 172 } 173