• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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