Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cached injector for common modules #3

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static java.util.stream.Collectors.toSet;
import static org.junit.platform.commons.support.AnnotationSupport.findRepeatableAnnotations;
import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation;
Litemn marked this conversation as resolved.
Show resolved Hide resolved

import com.google.common.collect.Iterables;
import com.google.common.reflect.TypeToken;
Expand All @@ -25,6 +26,8 @@
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Stream;
import javax.inject.Qualifier;
import org.junit.jupiter.api.extension.ExtensionContext;
Expand All @@ -39,6 +42,8 @@ public final class GuiceExtension implements TestInstancePostProcessor, Paramete
private static final Namespace NAMESPACE =
Namespace.create("name", "falgout", "jeffrey", "testing", "junit", "guice");

private static final ConcurrentMap<Set<? extends Class<?>>, Injector> INJECTOR_CACHE = new ConcurrentHashMap<>();

public GuiceExtension() {}

@Override
Expand Down Expand Up @@ -79,10 +84,44 @@ private static Injector createInjector(ExtensionContext context)
InvocationTargetException {
Optional<Injector> parentInjector = getParentInjector(context);
List<? extends Module> modules = getNewModules(context);

if (isSharedInjector(context)) {
return createCachedInjector(modules, parentInjector);
}
return parentInjector
.map(injector -> injector.createChildInjector(modules))
.orElseGet(() -> Guice.createInjector(modules));
.map(injector -> injector.createChildInjector(modules))
Litemn marked this conversation as resolved.
Show resolved Hide resolved
.orElseGet(() -> Guice.createInjector(modules));
}

private static Injector createCachedInjector(List<? extends Module> modules,
Optional<Injector> parentInjector) {
Set<? extends Class<?>> modulClass = modules.stream().map(Object::getClass).collect(toSet());
Litemn marked this conversation as resolved.
Show resolved Hide resolved
Optional<Injector> cachedInjector = getCachedInjector(modulClass);
if (cachedInjector.isPresent()) {
Litemn marked this conversation as resolved.
Show resolved Hide resolved
return cachedInjector.get();
} else {
Injector createdInjector = parentInjector
.map(injector -> injector.createChildInjector(modules))
Litemn marked this conversation as resolved.
Show resolved Hide resolved
.orElseGet(() -> Guice.createInjector(modules));
if (!modulClass.isEmpty()) {
INJECTOR_CACHE.put(modulClass, createdInjector);
Litemn marked this conversation as resolved.
Show resolved Hide resolved
}
return createdInjector;
}
}

private static boolean isSharedInjector(ExtensionContext context) {
if (!context.getElement().isPresent()) {
return false;
}
AnnotatedElement element = context.getElement().get();
return findAnnotation(element, SharedInjectors.class).isPresent();
}

private static Optional<Injector> getCachedInjector(Set<? extends Class<?>> modulClasses) {
if (!modulClasses.isEmpty() && INJECTOR_CACHE.containsKey(modulClasses)) {
return Optional.of(INJECTOR_CACHE.get(modulClasses));
}
return Optional.empty();
}

private static Optional<Injector> getParentInjector(ExtensionContext context)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package name.falgout.jeffrey.testing.junit.guice;

import org.junit.jupiter.api.extension.ExtendWith;

import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Documented
@Retention(RUNTIME)
@Target({TYPE, METHOD})
@Inherited
@ExtendWith(GuiceExtension.class)
public @interface SharedInjectors {
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

import com.google.inject.AbstractModule;
import com.google.inject.Injector;
import com.google.inject.Key;
import java.lang.reflect.InvocationTargetException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import javax.inject.Inject;
import name.falgout.jeffrey.testing.junit.guice.GuiceExtensionTest.TestModule;
import name.falgout.jeffrey.testing.junit.testing.ExpectFailure;
Expand Down Expand Up @@ -168,6 +171,58 @@ void doesNotHaveInjectConstructor(NotInjectable.Arg arg) {}
void cannotBeInjected(NotInjectable notInjectable) {}
}

@Nested
@IncludeModule(CachedModule.class)
@SharedInjectors
class FirstCachedInjectorTest {

@Test
void firstTest(long i) {
assertEquals(1, i);
}
@Test
void secondTest(long i) {
assertEquals(1, i);
}
}

@Nested
@IncludeModule(CachedModule.class)
@SharedInjectors
class SecondCachedInjectorTest {

@Test
void firstTest(long i) {
assertEquals(1, i);
}
@Test
void secondTest(long i) {
assertEquals(1, i);
}
}

@Nested
@IncludeModule(NonCachedModule.class)
class FirstNonCachedInjectorTest {
@Test
void test(long i) {
long expectedValue = NonCachedModule.SECOND_EXECUTED.get() ? 2 : 1;
assertEquals(expectedValue, i);
NonCachedModule.FIRST_EXECUTED.set(true);
}
}

@Nested
@IncludeModule(NonCachedModule.class)
class SecondNonCachedInjectorTest {
@Test
void test(long i) {
long expectedValue = NonCachedModule.FIRST_EXECUTED.get() ? 2 : 1;
assertEquals(expectedValue, i);
NonCachedModule.SECOND_EXECUTED.set(true);
}
}

static final class FooBarExtension implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext,
Expand Down Expand Up @@ -250,4 +305,24 @@ private static class Arg {
private Arg(String s) {}
}
}

static final class CachedModule extends AbstractModule {
static final AtomicLong ATOMIC_LONG = new AtomicLong(0);
@Override
protected void configure() {
bind(long.class).toInstance(ATOMIC_LONG.incrementAndGet());
}
}

static final class NonCachedModule extends AbstractModule {
static final AtomicLong ATOMIC_LONG = new AtomicLong(0);
//depend on which test executed first
static final AtomicBoolean FIRST_EXECUTED = new AtomicBoolean(false);
static final AtomicBoolean SECOND_EXECUTED = new AtomicBoolean(false);

@Override
protected void configure() {
bind(long.class).toInstance(ATOMIC_LONG.incrementAndGet());
}
}
}