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: 356184068
  • Loading branch information
bcorso authored and Dagger Team committed Feb 24, 2021
1 parent 608e6cc commit 8f98ee9
Show file tree
Hide file tree
Showing 52 changed files with 1,799 additions and 78 deletions.
1 change: 1 addition & 0 deletions BUILD
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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_TEST_ENTRY_POINT = "dagger.hilt.android.EarlyTestEntryPoint";

/**
* 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) {
// @EarlyTestEntryPoint 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_TEST_ENTRY_POINT),
"Interface, %s, annotated with @EarlyTestEntryPoint should be called with "
+ "EarlyTestEntryPoints.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
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,39 @@ android_library(
],
)

android_library(
name = "early_test_entry_point",
srcs = [
"EarlyTestEntryPoint.java",
"EarlyTestEntryPoints.java",
],
exported_plugins = [
"//java/dagger/hilt/android/processor/internal/earlytestentrypoint: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
92 changes: 92 additions & 0 deletions java/dagger/hilt/android/EarlyTestEntryPoint.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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 an {@link dagger.hilt.EntryPoint} usage may be called before the
* singleton component is available in a Hilt test.
*
* <p>Warning: Please consider all of the caveats (listed at the bottom) when using this annotation.
*
* <h1>Background:
*
* <p>In a {@link dagger.hilt.android.HiltAndroidApp Hilt application}, the singleton component's
* lifetime is scoped to the {@link android.app.Application} instance, which allows the entry point
* usages to be called at any point in the applications lifetime.
*
* <p>However, in a {@link dagger.hilt.android.testing.HiltAndroidTest Hilt test}, the singleton
* component's lifetime is scoped to the lifetime of a single test case, which is typically much
* shorter than the test application's lifetime (which can span multiple test cases and even
* multiple test classes).
*
* <p>Thus, while calling an entry point from {@link android.app.Application#onCreate()} will work
* fine in a Hilt application, the same entry point usage in a Hilt test will fail because the
* singleton component for the test case cannot be created yet.
*
* <h1>{@code @EarlyTestEntryPoints}
*
* <p>When using {@link EarlyTestEntryPoint}, the annotated entry point will be installed into a
* new component that has the lifetime of the test application. As normal, each test case will still
* have its own singleton component instance that lasts the lifetime of the test case.
*
* <p>{@link EarlyTestEntryPoint} does not have any effect in a Hilt application, (i.e. Hilt code
* generated for {@link dagger.hilt.android.HiltAndroidApp}), it only affects Hilt tests (i.e. Hilt
* code generated for {@link dagger.hilt.android.testing.HiltAndroidTest}).
*
* <h1>Example:
*
* <pre>{@code
* @EarlyTestEntryPoint
* @EntryPoint
* @InstallIn(SingletonComponent.class)
* interface FooEntryPoint {
* Foo getFoo();
* }
*
* // EarlyTestEntryPoints.get() must be used with entry points annotated with @EarlyTestEntryPoint
* // This entry point can now be called at any point during a test, e.g. in Application.onCreate().
* Foo foo = EarlyTestEntryPoints.get(appContext, FooEntryPoint.class).getFoo();
* }</pre>
*
* <h1>Caveats:
*
* <p>The component used with `EarlyTestEntryPoints` does not share any state with the singleton
* component used for a given test case. Even `@Singleton` scoped bindings will ***not*** be shared.
*
* <p>The component used with `EarlyTestEntryPoints` does not have access to any test-specific
* bindings (i.e. bindings created within a specific test class such as [`@BindValue`] or a
* [nested modules]).
*
* <p>Finally, the component used with `EarlyTestEntryPoints` lives for the lifetime of the
* application, so it can leak state across multiple test cases if each test case is not run within
* a separate application instance.
*
* <h1>Best practices:
*
* <p>Avoid using {@link EarlyTestEntryPoint} on entry points that depend on mutable scoped bindings
* (either directly or transitively). Such cases can lead to confusion since the mutable state is
* not shared between the two components.
*/
@Retention(RUNTIME) // Needs to be runtime for checks in EntryPoints and EarlyTestEntryPoints.
@Target(ElementType.TYPE)
public @interface EarlyTestEntryPoint {}
78 changes: 78 additions & 0 deletions java/dagger/hilt/android/EarlyTestEntryPoints.java
Original file line number Diff line number Diff line change
@@ -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 EarlyTestEntryPoint}. */
public final class EarlyTestEntryPoints {

/**
* 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 EarlyTestEntryPoint}. 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(
hasAnnotation(entryPoint, EarlyTestEntryPoint.class),
"%s should be called with EntryPoints.get() rather than EarlyTestEntryPoints.get()",
entryPoint.getCanonicalName());
Object earlyComponent =
((TestSingletonComponentManager) componentManager).earlyTestSingletonComponent();
return entryPoint.cast(earlyComponent);
}

// @EarlyTestEntryPoint 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 hasAnnotation(
Class<?> clazz, Class<? extends Annotation> annotationClazz) {
for (Annotation annotation : clazz.getAnnotations()) {
if (annotation.annotationType().equals(annotationClazz)) {
return true;
}
}
return false;
}

private EarlyTestEntryPoints() {}
}
11 changes: 11 additions & 0 deletions java/dagger/hilt/android/internal/testing/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,24 @@ android_library(
],
)

android_library(
name = "early_test_singleton_component_creator",
testonly = 1,
srcs = ["EarlyTestSingletonComponentCreator.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 comments on commit 8f98ee9

Please sign in to comment.