forked from square/dagger
-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add EarlyTestEntryPoints to allow entry points to be called in tests …
…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
Showing
52 changed files
with
1,799 additions
and
78 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.