Skip to content

Commit

Permalink
Add EarlyTestEntryPoints to allow entry points to be called in tests …
Browse files Browse the repository at this point in the history
…before the test instance is instantiated.

See #2016

RELNOTES=Add EarlyTestEntryPoints to allow entry points to be called in tests before the test instance is instantiated.
PiperOrigin-RevId: 359350904
  • Loading branch information
bcorso authored and Dagger Team committed Feb 24, 2021
1 parent 608e6cc commit 289f59f
Show file tree
Hide file tree
Showing 52 changed files with 1,758 additions and 78 deletions.
1 change: 1 addition & 0 deletions BUILD
Expand Up @@ -105,6 +105,7 @@ android_library(
"@maven//:org_robolectric_shadows_framework", # For ActivityController
"@maven//:androidx_lifecycle_lifecycle_common", # For Lifecycle.State
"@maven//:androidx_activity_activity", # For ComponentActivity
"@maven//:androidx_test_core", # For ApplicationProvider
"@maven//:androidx_test_ext_junit",
"@maven//:org_robolectric_annotations",
"@maven//:org_robolectric_robolectric",
Expand Down
3 changes: 3 additions & 0 deletions java/dagger/hilt/BUILD
Expand Up @@ -51,6 +51,8 @@ java_library(
":package_info",
"//java/dagger/hilt/internal:component_manager",
"//java/dagger/hilt/internal:generated_component",
"//java/dagger/hilt/internal:preconditions",
"//java/dagger/hilt/internal:test_singleton_component",
"@google_bazel_common//third_party/java/jsr305_annotations",
],
)
Expand Down Expand Up @@ -210,6 +212,7 @@ gen_maven_artifact(
"//java/dagger/hilt/internal:component_manager",
"//java/dagger/hilt/internal:generated_component",
"//java/dagger/hilt/internal:preconditions",
"//java/dagger/hilt/internal:test_singleton_component",
"//java/dagger/hilt/internal:unsafe_casts",
"//java/dagger/hilt/internal/aliasof",
"//java/dagger/hilt/internal/definecomponent",
Expand Down
27 changes: 25 additions & 2 deletions java/dagger/hilt/EntryPoints.java
Expand Up @@ -18,10 +18,14 @@

import dagger.hilt.internal.GeneratedComponent;
import dagger.hilt.internal.GeneratedComponentManager;
import dagger.hilt.internal.Preconditions;
import dagger.hilt.internal.TestSingletonComponent;
import java.lang.annotation.Annotation;
import javax.annotation.Nonnull;

/** Static utility methods for accessing objects through entry points. */
public final class EntryPoints {
private static final String EARLY_ENTRY_POINT = "dagger.hilt.android.EarlyEntryPoint";

/**
* Returns the entry point interface given a component or component manager. Note that this
Expand All @@ -39,11 +43,20 @@ public final class EntryPoints {
@Nonnull
public static <T> T get(Object component, Class<T> entryPoint) {
if (component instanceof GeneratedComponent) {
if (component instanceof TestSingletonComponent) {
// @EarlyEntryPoint only has an effect in test environment, so we shouldn't fail in
// non-test cases. In addition, some of the validation requires the use of reflection, which
// we don't want to do in non-test cases anyway.
Preconditions.checkState(
!hasAnnotationReflection(entryPoint, EARLY_ENTRY_POINT),
"Interface, %s, annotated with @EarlyEntryPoint should be called with "
+ "EarlyEntryPoints.get() rather than EntryPoints.get()",
entryPoint.getCanonicalName());
}
// Unsafe cast. There is no way for this method to know that the correct component was used.
return entryPoint.cast(component);
} else if (component instanceof GeneratedComponentManager) {
// Unsafe cast. There is no way for this method to know that the correct component was used.
return entryPoint.cast(((GeneratedComponentManager<?>) component).generatedComponent());
return get(((GeneratedComponentManager<?>) component).generatedComponent(), entryPoint);
} else {
throw new IllegalStateException(
String.format(
Expand All @@ -52,5 +65,15 @@ public static <T> T get(Object component, Class<T> entryPoint) {
}
}

// Note: This method uses reflection but it should only be called in test environments.
private static boolean hasAnnotationReflection(Class<?> clazz, String annotationName) {
for (Annotation annotation : clazz.getAnnotations()) {
if (annotation.annotationType().getCanonicalName().contentEquals(annotationName)) {
return true;
}
}
return false;
}

private EntryPoints() {}
}
38 changes: 30 additions & 8 deletions java/dagger/hilt/android/BUILD
Expand Up @@ -114,11 +114,39 @@ android_library(
],
)

android_library(
name = "early_test_entry_point",
srcs = [
"EarlyEntryPoint.java",
"EarlyEntryPoints.java",
],
exported_plugins = [
"//java/dagger/hilt/android/processor/internal/earlyentrypoint:processor",
],
deps = [
":package_info",
"//java/dagger/hilt:entry_point",
"//java/dagger/hilt/internal:component_manager",
"//java/dagger/hilt/internal:preconditions",
"//java/dagger/hilt/internal:test_singleton_component_manager",
"@google_bazel_common//third_party/java/jsr305_annotations",
],
)

java_library(
name = "package_info",
srcs = ["package-info.java"],
deps = [
"@google_bazel_common//third_party/java/jsr305_annotations",
],
)

android_library(
name = "artifact-lib",
tags = ["maven_coordinates=com.google.dagger:hilt-android:" + POM_VERSION_ALPHA],
exports = [
":android_entry_point",
":early_test_entry_point",
":entry_point_accessors",
":hilt_android_app",
":package_info",
Expand All @@ -128,14 +156,6 @@ android_library(
],
)

java_library(
name = "package_info",
srcs = ["package-info.java"],
deps = [
"@google_bazel_common//third_party/java/jsr305_annotations",
],
)

gen_maven_artifact(
name = "artifact",
artifact_coordinates = "com.google.dagger:hilt-android:" + POM_VERSION_ALPHA,
Expand All @@ -146,6 +166,7 @@ gen_maven_artifact(
"//java/dagger/hilt/android:activity_retained_lifecycle",
"//java/dagger/hilt/android:android_entry_point",
"//java/dagger/hilt/android:hilt_android_app",
"//java/dagger/hilt/android:early_test_entry_point",
"//java/dagger/hilt/android:package_info",
"//java/dagger/hilt/android/components",
"//java/dagger/hilt/android/components:view_model_component",
Expand All @@ -168,6 +189,7 @@ gen_maven_artifact(
"//java/dagger/hilt/android/scopes:package_info",
"//java/dagger/hilt/internal:component_entry_point",
"//java/dagger/hilt/internal:generated_entry_point",
"//java/dagger/hilt/internal:test_singleton_component_manager",
],
artifact_target_maven_deps = [
"androidx.activity:activity",
Expand Down
55 changes: 55 additions & 0 deletions java/dagger/hilt/android/EarlyEntryPoint.java
@@ -0,0 +1,55 @@
/*
* Copyright (C) 2021 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;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

/**
* An escape hatch for when a Hilt entry point usage needs to be called before the singleton
* component is available in a Hilt test.
*
* <p>Warning: Please see documentation for more details:
* https://dagger.dev/hilt/early-entry-point
*
* <h1>Usage:
*
* <p>To use, annotate an existing entry point with {@link EarlyEntryPoint} as shown below:
*
* <pre>{@code
* @EarlyEntryPoint
* @EntryPoint
* @InstallIn(SingletonComponent.class)
* interface FooEntryPoint {
* Foo getFoo();
* }
* }</pre>
*
* <p>Then, replace any usages of {@link EntryPoints} with {@link EarlyEntryPoints}, as shown below:
*
* <pre>{@code
* // EarlyEntryPoints.get() must be used with entry points annotated with @EarlyEntryPoint
* // This entry point can now be called at any point during a test, e.g. in Application.onCreate().
* Foo foo = EarlyEntryPoints.get(appContext, FooEntryPoint.class).getFoo();
* }</pre>
*/
@Retention(RUNTIME) // Needs to be runtime for checks in EntryPoints and EarlyEntryPoints.
@Target(ElementType.TYPE)
public @interface EarlyEntryPoint {}
78 changes: 78 additions & 0 deletions java/dagger/hilt/android/EarlyEntryPoints.java
@@ -0,0 +1,78 @@
/*
* Copyright (C) 2021 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;

import android.content.Context;
import dagger.hilt.EntryPoints;
import dagger.hilt.internal.GeneratedComponentManagerHolder;
import dagger.hilt.internal.Preconditions;
import dagger.hilt.internal.TestSingletonComponentManager;
import java.lang.annotation.Annotation;
import javax.annotation.Nonnull;

/** Static utility methods for accessing entry points annotated with {@link EarlyEntryPoint}. */
public final class EarlyEntryPoints {

/**
* Returns the early entry point interface given a component manager holder. Note that this
* performs an unsafe cast and so callers should be sure that the given component/component
* manager matches the early entry point interface that is given.
*
* @param applicationContext The application context.
* @param entryPoint The interface marked with {@link EarlyEntryPoint}. The {@link
* dagger.hilt.InstallIn} annotation on this entry point should match the component argument
* above.
*/
// Note that the input is not statically declared to be a Component or ComponentManager to make
// this method easier to use, since most code will use this with an Application or Context type.
@Nonnull
public static <T> T get(Context applicationContext, Class<T> entryPoint) {
applicationContext = applicationContext.getApplicationContext();
Preconditions.checkState(
applicationContext instanceof GeneratedComponentManagerHolder,
"Expected application context to implement GeneratedComponentManagerHolder. "
+ "Check that you're passing in an application context that uses Hilt.");
Object componentManager =
((GeneratedComponentManagerHolder) applicationContext).componentManager();
if (componentManager instanceof TestSingletonComponentManager) {
Preconditions.checkState(
hasAnnotationReflection(entryPoint, EarlyEntryPoint.class),
"%s should be called with EntryPoints.get() rather than EarlyEntryPoints.get()",
entryPoint.getCanonicalName());
Object earlyComponent =
((TestSingletonComponentManager) componentManager).earlySingletonComponent();
return entryPoint.cast(earlyComponent);
}

// @EarlyEntryPoint only has an effect in test environment, so if this is not a test we
// delegate to EntryPoints.
return EntryPoints.get(applicationContext, entryPoint);
}

// Note: This method uses reflection but it should only be called in test environments.
private static boolean hasAnnotationReflection(
Class<?> clazz, Class<? extends Annotation> annotationClazz) {
for (Annotation annotation : clazz.getAnnotations()) {
if (annotation.annotationType().equals(annotationClazz)) {
return true;
}
}
return false;
}

private EarlyEntryPoints() {}
}
11 changes: 11 additions & 0 deletions java/dagger/hilt/android/internal/testing/BUILD
Expand Up @@ -36,16 +36,24 @@ android_library(
],
)

android_library(
name = "early_test_singleton_component_creator",
testonly = 1,
srcs = ["EarlySingletonComponentCreator.java"],
)

android_library(
name = "test_application_component_manager",
testonly = 1,
srcs = ["TestApplicationComponentManager.java"],
deps = [
":early_test_singleton_component_creator",
":test_component_data",
":test_injector",
"//java/dagger/hilt/android/testing:on_component_ready_runner",
"//java/dagger/hilt/internal:component_manager",
"//java/dagger/hilt/internal:preconditions",
"//java/dagger/hilt/internal:test_singleton_component_manager",
"@maven//:junit_junit",
],
)
Expand All @@ -67,6 +75,9 @@ android_library(
name = "test_application_component_manager_holder",
testonly = 1,
srcs = ["TestApplicationComponentManagerHolder.java"],
deps = [
"//java/dagger/hilt/internal:component_manager",
],
)

android_library(
Expand Down
@@ -0,0 +1,48 @@
/*
* Copyright (C) 2021 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 java.lang.reflect.InvocationTargetException;

/** Creates a test's early component. */
public abstract class EarlySingletonComponentCreator {
private static final String EARLY_SINGLETON_COMPONENT_CREATOR_IMPL =
"dagger.hilt.android.internal.testing.EarlySingletonComponentCreatorImpl";

static Object createComponent() {
try {
return Class.forName(EARLY_SINGLETON_COMPONENT_CREATOR_IMPL)
.asSubclass(EarlySingletonComponentCreator.class)
.getDeclaredConstructor()
.newInstance()
.create();
} catch (ClassNotFoundException
| NoSuchMethodException
| IllegalAccessException
| InstantiationException
| InvocationTargetException e) {
throw new RuntimeException(
"The EarlyComponent was requested but does not exist. Check that you have annotated "
+ "your test class with @HiltAndroidTest and that the processor is running over your "
+ "test.",
e);
}
}

/** Creates the early test component. */
abstract Object create();
}

0 comments on commit 289f59f

Please sign in to comment.