blob: 187887022cf44e17dbe1d5e1ed60c3eef0a5483d [file] [log] [blame]
/*
* Copyright (C) 2020 The Dagger Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dagger.hilt.android.internal.testing;
import android.app.Application;
import dagger.hilt.android.testing.OnComponentReadyRunner;
import dagger.hilt.android.testing.OnComponentReadyRunner.OnComponentReadyRunnerHolder;
import dagger.hilt.internal.GeneratedComponentManager;
import dagger.hilt.internal.Preconditions;
import java.lang.reflect.InvocationTargetException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.runner.Description;
/**
* Do not use except in Hilt generated code!
*
* <p>A manager for the creation of components that live in the test Application.
*/
public final class TestApplicationComponentManager
implements GeneratedComponentManager<Object>, OnComponentReadyRunnerHolder {
// This is a generated class that we always generate in a known location.
private static final String TEST_COMPONENT_DATA_SUPPLIER_IMPL =
"dagger.hilt.android.internal.testing.TestComponentDataSupplierImpl";
private final Application application;
private final Map<Class<?>, TestComponentData> testComponentDataSupplier;
private final AtomicReference<Object> component = new AtomicReference<>();
private final AtomicReference<Description> hasHiltTestRule = new AtomicReference<>();
private final Map<Class<?>, Object> registeredModules = new ConcurrentHashMap<>();
private final AtomicReference<Boolean> autoAddModuleEnabled = new AtomicReference<>();
private final AtomicReference<DelayedComponentState> delayedComponentState =
new AtomicReference<>(DelayedComponentState.NOT_DELAYED);
private volatile Object testInstance;
private volatile OnComponentReadyRunner onComponentReadyRunner = new OnComponentReadyRunner();
/**
* Represents the state of Component readiness. There are two valid transition sequences.
*
* <ul>
* <li>Typical test (no HiltAndroidRule#delayComponentReady): {@code NOT_DELAYED -> INJECTED}
* <li>Using HiltAndroidRule#delayComponentReady: {@code NOT_DELAYED -> COMPONENT_DELAYED ->
* COMPONENT_READY -> INJECTED}
* </ul>
*/
private enum DelayedComponentState {
// Valid transitions: COMPONENT_DELAYED, INJECTED
NOT_DELAYED,
// Valid transitions: COMPONENT_READY
COMPONENT_DELAYED,
// Valid transitions: INJECTED
COMPONENT_READY,
// Terminal state
INJECTED
}
public TestApplicationComponentManager(Application application) {
this.application = application;
try {
this.testComponentDataSupplier =
Class.forName(TEST_COMPONENT_DATA_SUPPLIER_IMPL)
.asSubclass(TestComponentDataSupplier.class)
.getDeclaredConstructor()
.newInstance()
.get();
} catch (ClassNotFoundException
| NoSuchMethodException
| IllegalAccessException
| InstantiationException
| InvocationTargetException e) {
throw new RuntimeException(
"Hilt classes generated from @HiltAndroidTest are missing. Check that you have annotated "
+ "your test class with @HiltAndroidTest and that the processor is running over your "
+ "test",
e);
}
}
@Override
public Object generatedComponent() {
if (component.get() == null) {
Preconditions.checkState(
hasHiltTestRule(),
"The component was not created. Check that you have added the HiltAndroidRule.");
if (!registeredModules.keySet().containsAll(requiredModules())) {
Set<Class<?>> difference = new HashSet<>(requiredModules());
difference.removeAll(registeredModules.keySet());
throw new IllegalStateException(
"The component was not created. Check that you have "
+ "registered all test modules:\n\tUnregistered: "
+ difference);
}
Preconditions.checkState(
bindValueReady(), "The test instance has not been set. Did you forget to call #bind()?");
throw new IllegalStateException(
"The component has not been created. "
+ "Check that you have called #inject()? Otherwise, "
+ "there is a race between injection and component creation. Make sure there is a "
+ "happens-before edge between the HiltAndroidRule/registering"
+ " all test modules and the first injection.");
}
return component.get();
}
@Override
public OnComponentReadyRunner getOnComponentReadyRunner() {
return onComponentReadyRunner;
}
/** For framework use only! This flag must be set before component creation. */
void setHasHiltTestRule(Description description) {
Preconditions.checkState(
// Some exempted tests set the test rule multiple times. Use CAS to avoid setting twice.
hasHiltTestRule.compareAndSet(null, description),
"The hasHiltTestRule flag has already been set!");
tryToCreateComponent();
}
void checkStateIsCleared() {
Preconditions.checkState(
component.get() == null,
"The Hilt component cannot be set before Hilt's test rule has run.");
Preconditions.checkState(
hasHiltTestRule.get() == null,
"The Hilt test rule cannot be set before Hilt's test rule has run.");
Preconditions.checkState(
autoAddModuleEnabled.get() == null,
"The Hilt autoAddModuleEnabled cannot be set before Hilt's test rule has run.");
Preconditions.checkState(
testInstance == null,
"The Hilt BindValue instance cannot be set before Hilt's test rule has run.");
Preconditions.checkState(
registeredModules.isEmpty(),
"The Hilt registered modules cannot be set before Hilt's test rule has run.");
Preconditions.checkState(
onComponentReadyRunner.isEmpty(),
"The Hilt onComponentReadyRunner cannot add listeners before Hilt's test rule has run.");
DelayedComponentState state = delayedComponentState.get();
switch (state) {
case NOT_DELAYED:
case COMPONENT_DELAYED:
// Expected
break;
case COMPONENT_READY:
throw new IllegalStateException("Called componentReady before test execution started");
case INJECTED:
throw new IllegalStateException("Called inject before test execution started");
}
}
void clearState() {
component.set(null);
hasHiltTestRule.set(null);
testInstance = null;
registeredModules.clear();
autoAddModuleEnabled.set(null);
delayedComponentState.set(DelayedComponentState.NOT_DELAYED);
onComponentReadyRunner = new OnComponentReadyRunner();
}
public Description getDescription() {
return hasHiltTestRule.get();
}
public Object getTestInstance() {
Preconditions.checkState(
testInstance != null,
"The test instance has not been set.");
return testInstance;
}
/** For framework use only! This method should be called when a required module is installed. */
public <T> void registerModule(Class<T> moduleClass, T module) {
Preconditions.checkNotNull(moduleClass);
Preconditions.checkState(
testComponentData().daggerRequiredModules().contains(moduleClass),
"Found unknown module class: %s",
moduleClass.getName());
if (requiredModules().contains(moduleClass)) {
Preconditions.checkState(
// Some exempted tests register modules multiple times.
!registeredModules.containsKey(moduleClass),
"Module is already registered: %s",
moduleClass.getName());
registeredModules.put(moduleClass, module);
tryToCreateComponent();
}
}
void delayComponentReady() {
switch (delayedComponentState.getAndSet(DelayedComponentState.COMPONENT_DELAYED)) {
case NOT_DELAYED:
// Expected
break;
case COMPONENT_DELAYED:
throw new IllegalStateException("Called delayComponentReady() twice");
case COMPONENT_READY:
throw new IllegalStateException("Called delayComponentReady() after componentReady()");
case INJECTED:
throw new IllegalStateException("Called delayComponentReady() after inject()");
}
}
void componentReady() {
switch (delayedComponentState.getAndSet(DelayedComponentState.COMPONENT_READY)) {
case NOT_DELAYED:
throw new IllegalStateException(
"Called componentReady(), even though delayComponentReady() was not used.");
case COMPONENT_DELAYED:
// Expected
break;
case COMPONENT_READY:
throw new IllegalStateException("Called componentReady() multiple times");
case INJECTED:
throw new IllegalStateException("Called componentReady() after inject()");
}
tryToCreateComponent();
}
void inject() {
switch (delayedComponentState.getAndSet(DelayedComponentState.INJECTED)) {
case NOT_DELAYED:
case COMPONENT_READY:
// Expected
break;
case COMPONENT_DELAYED:
throw new IllegalStateException("Called inject() before calling componentReady()");
case INJECTED:
throw new IllegalStateException("Called inject() multiple times");
}
Preconditions.checkNotNull(testInstance);
testInjector().injectTest(testInstance);
}
void verifyDelayedComponentWasMadeReady() {
Preconditions.checkState(
delayedComponentState.get() != DelayedComponentState.COMPONENT_DELAYED,
"Used delayComponentReady(), but never called componentReady()");
}
private void tryToCreateComponent() {
if (hasHiltTestRule()
&& registeredModules.keySet().containsAll(requiredModules())
&& bindValueReady()
&& delayedComponentReady()) {
Preconditions.checkState(
autoAddModuleEnabled.get() != null,
"Component cannot be created before autoAddModuleEnabled is set.");
Preconditions.checkState(
component.compareAndSet(
null,
componentSupplier().get(registeredModules, testInstance, autoAddModuleEnabled.get())),
"Tried to create the component more than once! "
+ "There is a race between registering the HiltAndroidRule and registering"
+ " all test modules. Make sure there is a happens-before edge between the two.");
onComponentReadyRunner.setComponentManager((GeneratedComponentManager) application);
}
}
void setTestInstance(Object testInstance) {
Preconditions.checkNotNull(testInstance);
Preconditions.checkState(this.testInstance == null, "The test instance was already set!");
this.testInstance = testInstance;
}
void setAutoAddModule(boolean autoAddModule) {
Preconditions.checkState(
autoAddModuleEnabled.get() == null, "autoAddModuleEnabled is already set!");
autoAddModuleEnabled.set(autoAddModule);
}
private Set<Class<?>> requiredModules() {
return autoAddModuleEnabled.get()
? testComponentData().hiltRequiredModules()
: testComponentData().daggerRequiredModules();
}
private boolean waitForBindValue() {
return testComponentData().waitForBindValue();
}
private TestInjector<Object> testInjector() {
return testComponentData().testInjector();
}
private TestComponentData.ComponentSupplier componentSupplier() {
return testComponentData().componentSupplier();
}
private TestComponentData testComponentData() {
return testComponentDataSupplier.get(testClass());
}
private Class<?> testClass() {
Preconditions.checkState(
hasHiltTestRule(),
"Test must have an HiltAndroidRule.");
return hasHiltTestRule.get().getTestClass();
}
private boolean bindValueReady() {
return !waitForBindValue() || testInstance != null;
}
private boolean delayedComponentReady() {
return delayedComponentState.get() != DelayedComponentState.COMPONENT_DELAYED;
}
private boolean hasHiltTestRule() {
return hasHiltTestRule.get() != null;
}
}