Skip to content

Commit

Permalink
Merge pull request #33927 from mkouba/quarkuscomponenttest-alllist-su…
Browse files Browse the repository at this point in the history
…pport

QuarkusComponentTest - add support for `@All` List injection points
  • Loading branch information
mkouba committed Jun 10, 2023
2 parents 5b5294e + edf4dce commit 3a154fd
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,16 @@ public interface ArcContainer {
*/
<T> List<InstanceHandle<T>> listAll(TypeLiteral<T> type, Annotation... qualifiers);

/**
*
* @param <X>
* @param type
* @param qualifiers
* @return the list of handles for the disambiguated beans
* @see #listAll(Class, Annotation...)
*/
<X> List<InstanceHandle<X>> listAll(Type type, Annotation... qualifiers);

/**
* Returns true if Arc container is running.
* This can be used as a quick check to determine CDI availability in Quarkus.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,13 +354,17 @@ public <T> InjectableInstance<T> select(TypeLiteral<T> type, Annotation... quali

@Override
public <T> List<InstanceHandle<T>> listAll(Class<T> type, Annotation... qualifiers) {
return Instances.listOfHandles(CurrentInjectionPointProvider.EMPTY_SUPPLIER, type, Set.of(qualifiers),
new CreationalContextImpl<>(null));
return listAll((Type) type, qualifiers);
}

@Override
public <T> List<InstanceHandle<T>> listAll(TypeLiteral<T> type, Annotation... qualifiers) {
return Instances.listOfHandles(CurrentInjectionPointProvider.EMPTY_SUPPLIER, type.getType(), Set.of(qualifiers),
return listAll(type.getType(), qualifiers);
}

@Override
public <X> List<InstanceHandle<X>> listAll(Type type, Annotation... qualifiers) {
return Instances.listOfHandles(CurrentInjectionPointProvider.EMPTY_SUPPLIER, type, Set.of(qualifiers),
new CreationalContextImpl<>(null));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import io.quarkus.arc.impl.HierarchyDiscovery;
import io.quarkus.arc.processor.Annotations;
import io.quarkus.arc.processor.BeanResolver;
import io.quarkus.arc.processor.InjectionPointInfo;
import io.quarkus.arc.processor.Types;

class MockBeanConfiguratorImpl<T> implements MockBeanConfigurator<T> {
Expand Down Expand Up @@ -169,9 +168,9 @@ public QuarkusComponentTestExtension register() {
return test;
}

boolean matches(BeanResolver beanResolver, InjectionPointInfo injectionPoint) {
return matchesType(injectionPoint.getRequiredType(), beanResolver)
&& hasQualifiers(injectionPoint.getRequiredQualifiers(), beanResolver);
boolean matches(BeanResolver beanResolver, org.jboss.jandex.Type requiredType, Set<AnnotationInstance> qualifiers) {
return matchesType(requiredType, beanResolver)
&& hasQualifiers(qualifiers, beanResolver);
}

boolean matchesType(org.jboss.jandex.Type requiredType, BeanResolver beanResolver) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -59,6 +60,7 @@
import org.junit.jupiter.api.extension.TestInstancePostProcessor;
import org.junit.jupiter.api.extension.TestInstancePreDestroyCallback;

import io.quarkus.arc.All;
import io.quarkus.arc.Arc;
import io.quarkus.arc.ArcContainer;
import io.quarkus.arc.ComponentsProvider;
Expand All @@ -78,6 +80,7 @@
import io.quarkus.arc.processor.BuiltinBean;
import io.quarkus.arc.processor.BytecodeTransformer;
import io.quarkus.arc.processor.ContextRegistrar;
import io.quarkus.arc.processor.DotNames;
import io.quarkus.arc.processor.InjectionPointInfo;
import io.quarkus.arc.processor.InjectionPointInfo.TypeAndQualifiers;
import io.quarkus.arc.processor.ResourceOutput;
Expand Down Expand Up @@ -553,7 +556,7 @@ public void register(RegistrationContext context) {
long start = System.nanoTime();
List<BeanInfo> beans = context.beans().collect();
BeanDeployment beanDeployment = context.get(Key.DEPLOYMENT);
List<InjectionPointInfo> unsatisfied = new ArrayList<>();
Set<TypeAndQualifiers> unsatisfiedInjectionPoints = new HashSet<>();
boolean configInjectionPoint = false;
Set<TypeAndQualifiers> configPropertyInjectionPoints = new HashSet<>();
DotName configDotName = DotName.createSimple(Config.class);
Expand All @@ -564,10 +567,11 @@ public void register(RegistrationContext context) {
// - find unsatisfied injection points
for (InjectionPointInfo injectionPoint : context.getInjectionPoints()) {
BuiltinBean builtin = BuiltinBean.resolve(injectionPoint);
if (builtin != null && builtin != BuiltinBean.INSTANCE) {
if (builtin != null && builtin != BuiltinBean.INSTANCE && builtin != BuiltinBean.LIST) {
continue;
}
if (injectionPoint.getRequiredType().name().equals(configDotName)) {
if (injectionPoint.getRequiredType().name().equals(configDotName)
&& injectionPoint.hasDefaultedQualifier()) {
configInjectionPoint = true;
continue;
}
Expand All @@ -576,38 +580,31 @@ public void register(RegistrationContext context) {
injectionPoint.getRequiredQualifiers()));
continue;
}
if (isSatisfied(injectionPoint, beans, beanDeployment)) {
Type requiredType = injectionPoint.getRequiredType();
Set<AnnotationInstance> qualifiers = injectionPoint.getRequiredQualifiers();
if (builtin == BuiltinBean.LIST) {
// @All List<Delta> -> Delta
requiredType = requiredType.asParameterizedType().arguments().get(0);
qualifiers = new HashSet<>(qualifiers);
qualifiers.removeIf(q -> q.name().equals(DotNames.ALL));
}
if (isSatisfied(requiredType, qualifiers, injectionPoint, beans, beanDeployment)) {
continue;
}
unsatisfied.add(injectionPoint);
LOG.debugf("Unsatisfied injection point found: %s", injectionPoint.getTargetInfo());
}

Set<TypeAndQualifiers> mockableInjectionPoints = new HashSet<>();

for (Iterator<InjectionPointInfo> it = unsatisfied.iterator(); it.hasNext();) {
InjectionPointInfo injectionPoint = it.next();

Type requiredType = injectionPoint.getRequiredType();
if (requiredType.kind() == Kind.PRIMITIVE || requiredType.kind() == Kind.ARRAY) {
continue;
throw new IllegalStateException(
"Found an unmockable unsatisfied injection point: " + injectionPoint.getTargetInfo());
}
it.remove();
mockableInjectionPoints.add(new TypeAndQualifiers(injectionPoint.getRequiredType(),
injectionPoint.getRequiredQualifiers()));
}

if (!unsatisfied.isEmpty()) {
throw new IllegalStateException("Found unmockable unsatisfied injection points:\n\t - " + unsatisfied
.stream().map(InjectionPointInfo::getTargetInfo).collect(Collectors.joining("\n\t - ")));
unsatisfiedInjectionPoints.add(new TypeAndQualifiers(requiredType, qualifiers));
LOG.debugf("Unsatisfied injection point found: %s", injectionPoint.getTargetInfo());
}

for (TypeAndQualifiers typeAndQualifiers : mockableInjectionPoints) {
ClassInfo implementationClass = computingIndex.getClassByName(typeAndQualifiers.type.name());
for (TypeAndQualifiers unsatisfied : unsatisfiedInjectionPoints) {
ClassInfo implementationClass = computingIndex.getClassByName(unsatisfied.type.name());
BeanConfigurator<Object> configurator = context.configure(implementationClass.name())
.scope(Singleton.class)
.addType(typeAndQualifiers.type);
typeAndQualifiers.qualifiers.forEach(configurator::addQualifier);
.addType(unsatisfied.type);
unsatisfied.qualifiers.forEach(configurator::addQualifier);
configurator.param("implementationClass", implementationClass)
.creator(MockBeanCreator.class)
.defaultBean()
Expand Down Expand Up @@ -637,7 +634,7 @@ public void register(RegistrationContext context) {

LOG.debugf("Test injection points analyzed in %s ms [found: %s, mocked: %s]",
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start), context.getInjectionPoints().size(),
mockableInjectionPoints.size());
unsatisfiedInjectionPoints.size());
}
});

Expand Down Expand Up @@ -725,16 +722,18 @@ private void indexAnnotatedElement(Indexer indexer, AnnotatedElement element) th
}
}

private boolean isSatisfied(InjectionPointInfo injectionPoint, Iterable<BeanInfo> beans, BeanDeployment beanDeployment) {
private boolean isSatisfied(Type requiredType, Set<AnnotationInstance> qualifiers, InjectionPointInfo injectionPoint,
Iterable<BeanInfo> beans,
BeanDeployment beanDeployment) {
for (BeanInfo bean : beans) {
if (Beans.matches(bean, injectionPoint.getRequiredType(), injectionPoint.getRequiredQualifiers())) {
if (Beans.matches(bean, requiredType, qualifiers)) {
LOG.debugf("Injection point %s satisfied by %s", injectionPoint.getTargetInfo(),
bean.toString());
return true;
}
}
for (MockBeanConfiguratorImpl<?> mock : mockConfigurators) {
if (mock.matches(beanDeployment.getBeanResolver(), injectionPoint)) {
if (mock.matches(beanDeployment.getBeanResolver(), requiredType, qualifiers)) {
LOG.debugf("Injection point %s satisfied by %s", injectionPoint.getTargetInfo(),
mock);
return true;
Expand Down Expand Up @@ -775,38 +774,62 @@ private List<FieldInjector> injectFields(Class<?> testClass, Object testInstance
static class FieldInjector {

private final Field field;
private final InstanceHandle<?> handle;
private final List<InstanceHandle<?>> unsetHandles;

public FieldInjector(Field field, Object testInstance) throws Exception {
ArcContainer container = Arc.container();
BeanManager beanManager = container.beanManager();

this.field = field;
this.handle = container.instance(field.getGenericType(), getQualifiers(field, beanManager));

if (field.isAnnotationPresent(Inject.class)) {
if (handle.getBean().getKind() == io.quarkus.arc.InjectableBean.Kind.SYNTHETIC) {
throw new IllegalStateException(String
.format("The injected field %s expects a real component; but obtained: %s", field,
handle.getBean()));
ArcContainer container = Arc.container();
BeanManager beanManager = container.beanManager();
java.lang.reflect.Type requiredType = field.getGenericType();
Annotation[] qualifiers = getQualifiers(field, beanManager);

Object injectedInstance;

if (qualifiers.length > 0 && Arrays.stream(qualifiers).anyMatch(All.Literal.INSTANCE::equals)) {
// Special handling for @Injec @All List
if (isListRequiredType(requiredType)) {
List<InstanceHandle<Object>> handles = container.listAll(requiredType, qualifiers);
if (isTypeArgumentInstanceHandle(requiredType)) {
injectedInstance = handles;
} else {
injectedInstance = handles.stream().map(InstanceHandle::get).collect(Collectors.toUnmodifiableList());
}
unsetHandles = cast(handles);
} else {
throw new IllegalStateException("Invalid injection point type: " + field);
}
} else {
if (handle.getBean().getKind() != io.quarkus.arc.InjectableBean.Kind.SYNTHETIC) {
throw new IllegalStateException(String
.format("The injected field %s expects a mocked bean; but obtained: %s", field, handle.getBean()));
InstanceHandle<?> handle = container.instance(requiredType, qualifiers);
if (field.isAnnotationPresent(Inject.class)) {
if (handle.getBean().getKind() == io.quarkus.arc.InjectableBean.Kind.SYNTHETIC) {
throw new IllegalStateException(String
.format("The injected field %s expects a real component; but obtained: %s", field,
handle.getBean()));
}
} else {
if (handle.getBean().getKind() != io.quarkus.arc.InjectableBean.Kind.SYNTHETIC) {
throw new IllegalStateException(String
.format("The injected field %s expects a mocked bean; but obtained: %s", field,
handle.getBean()));
}
}
injectedInstance = handle.get();
unsetHandles = List.of(handle);
}

field.setAccessible(true);
field.set(testInstance, handle.get());
field.set(testInstance, injectedInstance);
}

void unset(Object testInstance) throws Exception {
if (handle.getBean() != null && handle.getBean().getScope().equals(Dependent.class)) {
try {
handle.destroy();
} catch (Exception e) {
LOG.errorf(e, "Unable to destroy the injected %s", handle.getBean());
for (InstanceHandle<?> handle : unsetHandles) {
if (handle.getBean() != null && handle.getBean().getScope().equals(Dependent.class)) {
try {
handle.destroy();
} catch (Exception e) {
LOG.errorf(e, "Unable to destroy the injected %s", handle.getBean());
}
}
}
field.setAccessible(true);
Expand All @@ -824,6 +847,23 @@ private Class<? extends Annotation> loadInjectMock() {
}
}

private static boolean isListRequiredType(java.lang.reflect.Type type) {
if (type instanceof ParameterizedType) {
final ParameterizedType parameterizedType = (ParameterizedType) type;
return List.class.equals(parameterizedType.getRawType());
}
return false;
}

private static boolean isTypeArgumentInstanceHandle(java.lang.reflect.Type type) {
// List<String> -> String
java.lang.reflect.Type typeArgument = ((ParameterizedType) type).getActualTypeArguments()[0];
if (typeArgument instanceof ParameterizedType) {
return ((ParameterizedType) typeArgument).getRawType().equals(InstanceHandle.class);
}
return false;
}

private boolean resolvesToBuiltinBean(Class<?> rawType) {
return Instance.class.isAssignableFrom(rawType) || Event.class.equals(rawType) || BeanManager.class.equals(rawType);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,6 @@ public class Bravo {
Charlie charlie;

public String ping() {
try {
Thread.sleep(7l);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return charlie.ping();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.quarkus.test.component.declarative;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

import java.util.List;

import jakarta.inject.Inject;
import jakarta.inject.Singleton;

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import io.quarkus.arc.All;
import io.quarkus.test.component.ConfigureMock;
import io.quarkus.test.component.QuarkusComponentTest;
import io.quarkus.test.component.beans.Bravo;
import io.quarkus.test.component.beans.Delta;
import io.quarkus.test.component.beans.SimpleQualifier;

@QuarkusComponentTest
public class ListAllMockTest {

@Inject
ListAllComponent component;

@ConfigureMock
Delta delta;

@ConfigureMock
@SimpleQualifier
Bravo bravo;

@Test
public void testMock() {
Mockito.when(delta.ping()).thenReturn(false);
Mockito.when(bravo.ping()).thenReturn("ok");
assertFalse(component.ping());
assertEquals(1, component.bravos.size());
assertEquals("ok", component.bravos.get(0).ping());
}

@Singleton
static class ListAllComponent {

@All
List<Delta> deltas;

@All
@SimpleQualifier
List<Bravo> bravos;

boolean ping() {
return deltas.get(0).ping();
}

}
}

0 comments on commit 3a154fd

Please sign in to comment.