/*
* 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 dagger.hilt.internal.TestSingletonComponentManager;
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!
*
*
A manager for the creation of components that live in the test Application.
*/
public final class TestApplicationComponentManager
implements TestSingletonComponentManager, OnComponentReadyRunnerHolder {
private final Object earlyComponentLock = new Object();
private volatile Object earlyComponent = null;
private final Object testComponentDataLock = new Object();
private volatile TestComponentData testComponentData;
private final Application application;
private final AtomicReference component = new AtomicReference<>();
private final AtomicReference hasHiltTestRule = new AtomicReference<>();
// TODO(bcorso): Consider using a lock here rather than ConcurrentHashMap to avoid b/37042460.
private final Map, Object> registeredModules = new ConcurrentHashMap<>();
private final AtomicReference autoAddModuleEnabled = new AtomicReference<>();
private final AtomicReference 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.
*
*
* Typical test (no HiltAndroidRule#delayComponentReady): {@code NOT_DELAYED -> INJECTED}
* Using HiltAndroidRule#delayComponentReady: {@code NOT_DELAYED -> COMPONENT_DELAYED ->
* COMPONENT_READY -> INJECTED}
*
*/
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;
}
@Override
public Object earlySingletonComponent() {
if (earlyComponent == null) {
synchronized (earlyComponentLock) {
if (earlyComponent == null) {
earlyComponent = EarlySingletonComponentCreator.createComponent();
}
}
}
return earlyComponent;
}
@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> 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(
testComponentData == null,
"The testComponentData 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;
testComponentData = 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 void registerModule(Class 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> requiredModules() {
return autoAddModuleEnabled.get()
? testComponentData().hiltRequiredModules()
: testComponentData().daggerRequiredModules();
}
private boolean waitForBindValue() {
return testComponentData().waitForBindValue();
}
private TestInjector testInjector() {
return testComponentData().testInjector();
}
private TestComponentData.ComponentSupplier componentSupplier() {
return testComponentData().componentSupplier();
}
private TestComponentData testComponentData() {
if (testComponentData == null) {
synchronized (testComponentDataLock) {
if (testComponentData == null) {
testComponentData = TestComponentDataSupplier.get(testClass());
}
}
}
return testComponentData;
}
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;
}
}