1 /* 2 * Copyright (C) 2015 The Dagger Authors. 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 dagger.functional.cycle; 18 19 import static com.google.common.truth.Truth.assertThat; 20 import static java.lang.Thread.State.BLOCKED; 21 import static java.lang.Thread.State.WAITING; 22 import static java.lang.annotation.RetentionPolicy.RUNTIME; 23 import static org.junit.Assert.fail; 24 25 import com.google.common.util.concurrent.SettableFuture; 26 import com.google.common.util.concurrent.Uninterruptibles; 27 import dagger.Component; 28 import dagger.Module; 29 import dagger.Provides; 30 import java.lang.annotation.Retention; 31 import java.util.ArrayList; 32 import java.util.Collections; 33 import java.util.List; 34 import java.util.concurrent.CountDownLatch; 35 import java.util.concurrent.ExecutionException; 36 import java.util.concurrent.atomic.AtomicInteger; 37 import javax.inject.Provider; 38 import javax.inject.Qualifier; 39 import javax.inject.Singleton; 40 import org.junit.Test; 41 import org.junit.runner.RunWith; 42 import org.junit.runners.JUnit4; 43 44 @RunWith(JUnit4.class) 45 public class DoubleCheckCycleTest { 46 // TODO(b/77916397): Migrate remaining tests in DoubleCheckTest to functional tests in this class. 47 48 /** A qualifier for a reentrant scoped binding. */ 49 @Retention(RUNTIME) 50 @Qualifier 51 @interface Reentrant {} 52 53 /** A module to be overridden in each test. */ 54 @Module 55 static class OverrideModule { 56 @Provides 57 @Singleton provideObject()58 Object provideObject() { 59 throw new IllegalStateException("This method should be overridden in tests"); 60 } 61 62 @Provides 63 @Singleton 64 @Reentrant provideReentrantObject(@eentrant Provider<Object> provider)65 Object provideReentrantObject(@Reentrant Provider<Object> provider) { 66 throw new IllegalStateException("This method should be overridden in tests"); 67 } 68 } 69 70 @Singleton 71 @Component(modules = OverrideModule.class) 72 interface TestComponent { getObject()73 Object getObject(); getReentrantObject()74 @Reentrant Object getReentrantObject(); 75 } 76 77 @Test testNonReentrant()78 public void testNonReentrant() { 79 AtomicInteger callCount = new AtomicInteger(0); 80 81 // Provides a non-reentrant binding. The provides method should only be called once. 82 DoubleCheckCycleTest.TestComponent component = 83 DaggerDoubleCheckCycleTest_TestComponent.builder() 84 .overrideModule( 85 new OverrideModule() { 86 @Override Object provideObject() { 87 callCount.getAndIncrement(); 88 return new Object(); 89 } 90 }) 91 .build(); 92 93 assertThat(callCount.get()).isEqualTo(0); 94 Object first = component.getObject(); 95 assertThat(callCount.get()).isEqualTo(1); 96 Object second = component.getObject(); 97 assertThat(callCount.get()).isEqualTo(1); 98 assertThat(first).isSameInstanceAs(second); 99 } 100 101 @Test testReentrant()102 public void testReentrant() { 103 AtomicInteger callCount = new AtomicInteger(0); 104 105 // Provides a reentrant binding. Even though it's scoped, the provides method is called twice. 106 // In this case, we allow it since the same instance is returned on the second call. 107 DoubleCheckCycleTest.TestComponent component = 108 DaggerDoubleCheckCycleTest_TestComponent.builder() 109 .overrideModule( 110 new OverrideModule() { 111 @Override Object provideReentrantObject(Provider<Object> provider) { 112 if (callCount.incrementAndGet() == 1) { 113 return provider.get(); 114 } 115 return new Object(); 116 } 117 }) 118 .build(); 119 120 assertThat(callCount.get()).isEqualTo(0); 121 Object first = component.getReentrantObject(); 122 assertThat(callCount.get()).isEqualTo(2); 123 Object second = component.getReentrantObject(); 124 assertThat(callCount.get()).isEqualTo(2); 125 assertThat(first).isSameInstanceAs(second); 126 } 127 128 @Test testFailingReentrant()129 public void testFailingReentrant() { 130 AtomicInteger callCount = new AtomicInteger(0); 131 132 // Provides a failing reentrant binding. Even though it's scoped, the provides method is called 133 // twice. In this case we throw an exception since a different instance is provided on the 134 // second call. 135 DoubleCheckCycleTest.TestComponent component = 136 DaggerDoubleCheckCycleTest_TestComponent.builder() 137 .overrideModule( 138 new OverrideModule() { 139 @Override Object provideReentrantObject(Provider<Object> provider) { 140 if (callCount.incrementAndGet() == 1) { 141 provider.get(); 142 return new Object(); 143 } 144 return new Object(); 145 } 146 }) 147 .build(); 148 149 assertThat(callCount.get()).isEqualTo(0); 150 try { 151 component.getReentrantObject(); 152 fail("Expected IllegalStateException"); 153 } catch (IllegalStateException e) { 154 assertThat(e).hasMessageThat().contains("Scoped provider was invoked recursively"); 155 } 156 assertThat(callCount.get()).isEqualTo(2); 157 } 158 159 @Test(timeout = 5000) 160 testGetFromMultipleThreads()161 public void testGetFromMultipleThreads() throws Exception { 162 AtomicInteger callCount = new AtomicInteger(0); 163 AtomicInteger requestCount = new AtomicInteger(0); 164 SettableFuture<Object> future = SettableFuture.create(); 165 166 // Provides a non-reentrant binding. In this case, we return a SettableFuture so that we can 167 // control when the provides method returns. 168 DoubleCheckCycleTest.TestComponent component = 169 DaggerDoubleCheckCycleTest_TestComponent.builder() 170 .overrideModule( 171 new OverrideModule() { 172 @Override 173 Object provideObject() { 174 callCount.incrementAndGet(); 175 try { 176 return Uninterruptibles.getUninterruptibly(future); 177 } catch (ExecutionException e) { 178 throw new RuntimeException(e); 179 } 180 } 181 }) 182 .build(); 183 184 185 int numThreads = 10; 186 CountDownLatch remainingTasks = new CountDownLatch(numThreads); 187 List<Thread> tasks = new ArrayList<>(numThreads); 188 List<Object> values = Collections.synchronizedList(new ArrayList<>(numThreads)); 189 190 // Set up multiple threads that call component.getObject(). 191 for (int i = 0; i < numThreads; i++) { 192 tasks.add( 193 new Thread( 194 () -> { 195 requestCount.incrementAndGet(); 196 values.add(component.getObject()); 197 remainingTasks.countDown(); 198 })); 199 } 200 201 // Check initial conditions 202 assertThat(remainingTasks.getCount()).isEqualTo(10); 203 assertThat(requestCount.get()).isEqualTo(0); 204 assertThat(callCount.get()).isEqualTo(0); 205 assertThat(values).isEmpty(); 206 207 // Start all threads 208 tasks.forEach(Thread::start); 209 210 // Wait for all threads to wait/block. 211 long waiting = 0; 212 while (waiting != numThreads) { 213 waiting = 214 tasks.stream() 215 .map(Thread::getState) 216 .filter(state -> state == WAITING || state == BLOCKED) 217 .count(); 218 } 219 220 // Check the intermediate state conditions. 221 // * All 10 threads should have requested the binding, but none should have finished. 222 // * Only 1 thread should have reached the provides method. 223 // * None of the threads should have set a value (since they are waiting for future to be set). 224 assertThat(remainingTasks.getCount()).isEqualTo(10); 225 assertThat(requestCount.get()).isEqualTo(10); 226 assertThat(callCount.get()).isEqualTo(1); 227 assertThat(values).isEmpty(); 228 229 // Set the future and wait on all remaining threads to finish. 230 Object futureValue = new Object(); 231 future.set(futureValue); 232 remainingTasks.await(); 233 234 // Check the final state conditions. 235 // All values should be set now, and they should all be equal to the same instance. 236 assertThat(remainingTasks.getCount()).isEqualTo(0); 237 assertThat(requestCount.get()).isEqualTo(10); 238 assertThat(callCount.get()).isEqualTo(1); 239 assertThat(values).isEqualTo(Collections.nCopies(numThreads, futureValue)); 240 } 241 } 242