• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.hilt.android.internal.testing;
18 
19 import android.app.Application;
20 import dagger.hilt.android.testing.OnComponentReadyRunner;
21 import dagger.hilt.android.testing.OnComponentReadyRunner.OnComponentReadyRunnerHolder;
22 import dagger.hilt.internal.GeneratedComponentManager;
23 import dagger.hilt.internal.Preconditions;
24 import java.lang.reflect.InvocationTargetException;
25 import java.util.HashSet;
26 import java.util.Map;
27 import java.util.Set;
28 import java.util.concurrent.ConcurrentHashMap;
29 import java.util.concurrent.atomic.AtomicReference;
30 import org.junit.runner.Description;
31 
32 /**
33  * Do not use except in Hilt generated code!
34  *
35  * <p>A manager for the creation of components that live in the test Application.
36  */
37 public final class TestApplicationComponentManager
38     implements GeneratedComponentManager<Object>, OnComponentReadyRunnerHolder {
39 
40   // This is a generated class that we always generate in a known location.
41   private static final String TEST_COMPONENT_DATA_SUPPLIER_IMPL =
42       "dagger.hilt.android.internal.testing.TestComponentDataSupplierImpl";
43 
44   private final Application application;
45   private final Map<Class<?>, TestComponentData> testComponentDataSupplier;
46 
47   private final AtomicReference<Object> component = new AtomicReference<>();
48   private final AtomicReference<Description> hasHiltTestRule = new AtomicReference<>();
49   private final Map<Class<?>, Object> registeredModules = new ConcurrentHashMap<>();
50   private final AtomicReference<Boolean> autoAddModuleEnabled = new AtomicReference<>();
51   private final AtomicReference<DelayedComponentState> delayedComponentState =
52       new AtomicReference<>(DelayedComponentState.NOT_DELAYED);
53   private volatile Object testInstance;
54   private volatile OnComponentReadyRunner onComponentReadyRunner = new OnComponentReadyRunner();
55 
56   /**
57    * Represents the state of Component readiness. There are two valid transition sequences.
58    *
59    * <ul>
60    *   <li>Typical test (no HiltAndroidRule#delayComponentReady): {@code NOT_DELAYED -> INJECTED}
61    *   <li>Using HiltAndroidRule#delayComponentReady: {@code NOT_DELAYED -> COMPONENT_DELAYED ->
62    *       COMPONENT_READY -> INJECTED}
63    * </ul>
64    */
65   private enum DelayedComponentState {
66     // Valid transitions: COMPONENT_DELAYED, INJECTED
67     NOT_DELAYED,
68     // Valid transitions: COMPONENT_READY
69     COMPONENT_DELAYED,
70     // Valid transitions: INJECTED
71     COMPONENT_READY,
72     // Terminal state
73     INJECTED
74   }
75 
TestApplicationComponentManager(Application application)76   public TestApplicationComponentManager(Application application) {
77     this.application = application;
78     try {
79       this.testComponentDataSupplier =
80           Class.forName(TEST_COMPONENT_DATA_SUPPLIER_IMPL)
81               .asSubclass(TestComponentDataSupplier.class)
82               .getDeclaredConstructor()
83               .newInstance()
84               .get();
85     } catch (ClassNotFoundException
86         | NoSuchMethodException
87         | IllegalAccessException
88         | InstantiationException
89         | InvocationTargetException e) {
90       throw new RuntimeException(
91           "Hilt classes generated from @HiltAndroidTest are missing. Check that you have annotated "
92               + "your test class with @HiltAndroidTest and that the processor is running over your "
93               + "test",
94           e);
95     }
96   }
97 
98   @Override
generatedComponent()99   public Object generatedComponent() {
100     if (component.get() == null) {
101       Preconditions.checkState(
102           hasHiltTestRule(),
103       "The component was not created. Check that you have added the HiltAndroidRule.");
104       if (!registeredModules.keySet().containsAll(requiredModules())) {
105         Set<Class<?>> difference = new HashSet<>(requiredModules());
106         difference.removeAll(registeredModules.keySet());
107         throw new IllegalStateException(
108             "The component was not created. Check that you have "
109                 + "registered all test modules:\n\tUnregistered: "
110                 + difference);
111       }
112       Preconditions.checkState(
113           bindValueReady(), "The test instance has not been set. Did you forget to call #bind()?");
114       throw new IllegalStateException(
115           "The component has not been created. "
116               + "Check that you have called #inject()? Otherwise, "
117               + "there is a race between injection and component creation. Make sure there is a "
118               + "happens-before edge between the HiltAndroidRule/registering"
119               + " all test modules and the first injection.");
120     }
121     return component.get();
122   }
123 
124   @Override
getOnComponentReadyRunner()125   public OnComponentReadyRunner getOnComponentReadyRunner() {
126     return onComponentReadyRunner;
127   }
128 
129   /** For framework use only! This flag must be set before component creation. */
setHasHiltTestRule(Description description)130   void setHasHiltTestRule(Description description) {
131     Preconditions.checkState(
132         // Some exempted tests set the test rule multiple times. Use CAS to avoid setting twice.
133         hasHiltTestRule.compareAndSet(null, description),
134         "The hasHiltTestRule flag has already been set!");
135     tryToCreateComponent();
136   }
137 
checkStateIsCleared()138   void checkStateIsCleared() {
139     Preconditions.checkState(
140         component.get() == null,
141         "The Hilt component cannot be set before Hilt's test rule has run.");
142     Preconditions.checkState(
143         hasHiltTestRule.get() == null,
144         "The Hilt test rule cannot be set before Hilt's test rule has run.");
145     Preconditions.checkState(
146         autoAddModuleEnabled.get() == null,
147         "The Hilt autoAddModuleEnabled cannot be set before Hilt's test rule has run.");
148     Preconditions.checkState(
149         testInstance == null,
150         "The Hilt BindValue instance cannot be set before Hilt's test rule has run.");
151     Preconditions.checkState(
152         registeredModules.isEmpty(),
153         "The Hilt registered modules cannot be set before Hilt's test rule has run.");
154     Preconditions.checkState(
155         onComponentReadyRunner.isEmpty(),
156         "The Hilt onComponentReadyRunner cannot add listeners before Hilt's test rule has run.");
157     DelayedComponentState state = delayedComponentState.get();
158     switch (state) {
159       case NOT_DELAYED:
160       case COMPONENT_DELAYED:
161         // Expected
162         break;
163       case COMPONENT_READY:
164         throw new IllegalStateException("Called componentReady before test execution started");
165       case INJECTED:
166         throw new IllegalStateException("Called inject before test execution started");
167     }
168   }
169 
clearState()170   void clearState() {
171     component.set(null);
172     hasHiltTestRule.set(null);
173     testInstance = null;
174     registeredModules.clear();
175     autoAddModuleEnabled.set(null);
176     delayedComponentState.set(DelayedComponentState.NOT_DELAYED);
177     onComponentReadyRunner = new OnComponentReadyRunner();
178   }
179 
getDescription()180   public Description getDescription() {
181     return hasHiltTestRule.get();
182   }
183 
getTestInstance()184   public Object getTestInstance() {
185     Preconditions.checkState(
186         testInstance != null,
187         "The test instance has not been set.");
188     return testInstance;
189   }
190 
191   /** For framework use only! This method should be called when a required module is installed. */
registerModule(Class<T> moduleClass, T module)192   public <T> void registerModule(Class<T> moduleClass, T module) {
193     Preconditions.checkNotNull(moduleClass);
194     Preconditions.checkState(
195         testComponentData().daggerRequiredModules().contains(moduleClass),
196         "Found unknown module class: %s",
197         moduleClass.getName());
198     if (requiredModules().contains(moduleClass)) {
199       Preconditions.checkState(
200           // Some exempted tests register modules multiple times.
201           !registeredModules.containsKey(moduleClass),
202           "Module is already registered: %s",
203           moduleClass.getName());
204 
205       registeredModules.put(moduleClass, module);
206       tryToCreateComponent();
207     }
208   }
209 
delayComponentReady()210   void delayComponentReady() {
211     switch (delayedComponentState.getAndSet(DelayedComponentState.COMPONENT_DELAYED)) {
212       case NOT_DELAYED:
213         // Expected
214         break;
215       case COMPONENT_DELAYED:
216         throw new IllegalStateException("Called delayComponentReady() twice");
217       case COMPONENT_READY:
218         throw new IllegalStateException("Called delayComponentReady() after componentReady()");
219       case INJECTED:
220         throw new IllegalStateException("Called delayComponentReady() after inject()");
221     }
222   }
223 
componentReady()224   void componentReady() {
225     switch (delayedComponentState.getAndSet(DelayedComponentState.COMPONENT_READY)) {
226       case NOT_DELAYED:
227         throw new IllegalStateException(
228             "Called componentReady(), even though delayComponentReady() was not used.");
229       case COMPONENT_DELAYED:
230         // Expected
231         break;
232       case COMPONENT_READY:
233         throw new IllegalStateException("Called componentReady() multiple times");
234       case INJECTED:
235         throw new IllegalStateException("Called componentReady() after inject()");
236     }
237     tryToCreateComponent();
238   }
239 
inject()240   void inject() {
241     switch (delayedComponentState.getAndSet(DelayedComponentState.INJECTED)) {
242       case NOT_DELAYED:
243       case COMPONENT_READY:
244         // Expected
245         break;
246       case COMPONENT_DELAYED:
247         throw new IllegalStateException("Called inject() before calling componentReady()");
248       case INJECTED:
249         throw new IllegalStateException("Called inject() multiple times");
250     }
251     Preconditions.checkNotNull(testInstance);
252     testInjector().injectTest(testInstance);
253   }
254 
verifyDelayedComponentWasMadeReady()255   void verifyDelayedComponentWasMadeReady() {
256     Preconditions.checkState(
257         delayedComponentState.get() != DelayedComponentState.COMPONENT_DELAYED,
258         "Used delayComponentReady(), but never called componentReady()");
259   }
260 
tryToCreateComponent()261   private void tryToCreateComponent() {
262     if (hasHiltTestRule()
263         && registeredModules.keySet().containsAll(requiredModules())
264         && bindValueReady()
265         && delayedComponentReady()) {
266       Preconditions.checkState(
267           autoAddModuleEnabled.get() !=  null,
268           "Component cannot be created before autoAddModuleEnabled is set.");
269       Preconditions.checkState(
270           component.compareAndSet(
271               null,
272               componentSupplier().get(registeredModules, testInstance, autoAddModuleEnabled.get())),
273           "Tried to create the component more than once! "
274               + "There is a race between registering the HiltAndroidRule and registering"
275               + " all test modules. Make sure there is a happens-before edge between the two.");
276       onComponentReadyRunner.setComponentManager((GeneratedComponentManager) application);
277     }
278   }
279 
setTestInstance(Object testInstance)280   void setTestInstance(Object testInstance) {
281     Preconditions.checkNotNull(testInstance);
282     Preconditions.checkState(this.testInstance == null, "The test instance was already set!");
283     this.testInstance = testInstance;
284   }
285 
setAutoAddModule(boolean autoAddModule)286   void setAutoAddModule(boolean autoAddModule) {
287     Preconditions.checkState(
288         autoAddModuleEnabled.get() == null, "autoAddModuleEnabled is already set!");
289     autoAddModuleEnabled.set(autoAddModule);
290   }
291 
requiredModules()292   private Set<Class<?>> requiredModules() {
293     return autoAddModuleEnabled.get()
294         ? testComponentData().hiltRequiredModules()
295         : testComponentData().daggerRequiredModules();
296   }
297 
waitForBindValue()298   private boolean waitForBindValue() {
299     return testComponentData().waitForBindValue();
300   }
301 
testInjector()302   private TestInjector<Object> testInjector() {
303     return testComponentData().testInjector();
304   }
305 
componentSupplier()306   private TestComponentData.ComponentSupplier componentSupplier() {
307     return testComponentData().componentSupplier();
308   }
309 
testComponentData()310   private TestComponentData testComponentData() {
311     return testComponentDataSupplier.get(testClass());
312   }
313 
testClass()314   private Class<?> testClass() {
315     Preconditions.checkState(
316         hasHiltTestRule(),
317     "Test must have an HiltAndroidRule.");
318     return hasHiltTestRule.get().getTestClass();
319   }
320 
bindValueReady()321   private boolean bindValueReady() {
322     return !waitForBindValue() || testInstance != null;
323   }
324 
delayedComponentReady()325   private boolean delayedComponentReady() {
326     return delayedComponentState.get() != DelayedComponentState.COMPONENT_DELAYED;
327   }
328 
hasHiltTestRule()329   private boolean hasHiltTestRule() {
330     return hasHiltTestRule.get() != null;
331   }
332 }
333