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

Testing: allow bytecode transformations in QuarkusComponentTest #35473

Closed
Show file tree
Hide file tree
Changes from all 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 @@ -126,6 +126,10 @@ private BeanProcessor(Builder builder) {
this.injectionPointAnnotationsPredicate = Predicate.not(DotNames.DEPRECATED::equals);
}

public String getName() {
return name;
}

public ContextRegistrar.RegistrationContext registerCustomContexts() {
return beanDeployment.registerCustomContexts(contextRegistrars);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
package io.quarkus.test.component;

import jakarta.inject.Singleton;
import jakarta.enterprise.context.ApplicationScoped;

import org.eclipse.microprofile.config.inject.ConfigProperty;

@Singleton
public class ComponentFoo {
// using normal scope so that client proxy is required, so the class must:
// - not be `final`
// - not have non-`private` `final` methods
// - not have a `private` constructor
// all these rules are deliberately broken to trigger ArC bytecode transformation
@ApplicationScoped
public final class ComponentFoo {

@ConfigProperty(name = "bar", defaultValue = "baz")
String bar;

String ping() {
private ComponentFoo() {
}

final String ping() {
return bar;
}

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

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;

import io.quarkus.arc.InterceptorCreator;
Expand All @@ -11,6 +16,12 @@ public class InterceptorMethodCreator implements InterceptorCreator {

static final String CREATE_KEY = "createKey";

private static final AtomicInteger idGenerator = new AtomicInteger();

// filled in the original CL, used to register interceptor methods in the extra CL
private static final Map<String, String[]> interceptorMethods = new HashMap<>();

// filled in the extra CL, used to actually invoke interceptor methods
private static final Map<String, Function<SyntheticCreationalContext<?>, InterceptFunction>> createFunctions = new HashMap<>();

@Override
Expand All @@ -25,12 +36,74 @@ public InterceptFunction create(SyntheticCreationalContext<Object> context) {
throw new IllegalStateException("Create function not found: " + createKey);
}

static void registerCreate(String key, Function<SyntheticCreationalContext<?>, InterceptFunction> create) {
createFunctions.put(key, create);
// called in the original CL, fills `interceptorMethods`
static String preregister(Class<?> testClass, Method interceptorMethod) {
String key = "io_quarkus_test_component_InterceptorMethodCreator_" + idGenerator.incrementAndGet();
String[] descriptor = new String[3 + interceptorMethod.getParameterCount()];
descriptor[0] = testClass.getName();
descriptor[1] = interceptorMethod.getDeclaringClass().getName();
descriptor[2] = interceptorMethod.getName();
for (int i = 0; i < interceptorMethod.getParameterCount(); i++) {
descriptor[3 + i] = interceptorMethod.getParameterTypes()[i].getName();
}
interceptorMethods.put(key, descriptor);
return key;
}

static Map<String, String[]> preregistered() {
return interceptorMethods;
}

// called in the extra CL, fills `createFunctions`
static void register(Map<String, String[]> methods, Deque<?> testInstanceStack) throws ReflectiveOperationException {
for (Map.Entry<String, String[]> entry : methods.entrySet()) {
String key = entry.getKey();
String[] descriptor = entry.getValue();
Class<?> testClass = Class.forName(descriptor[0]);
Class<?> declaringClass = Class.forName(descriptor[1]);
String methodName = descriptor[2];
int params = descriptor.length - 3;
Class<?>[] parameterTypes = new Class<?>[params];
for (int i = 0; i < params; i++) {
parameterTypes[i] = Class.forName(descriptor[3 + i]);
}
Method method = declaringClass.getDeclaredMethod(methodName, parameterTypes);
boolean isStatic = Modifier.isStatic(method.getModifiers());

Function<SyntheticCreationalContext<?>, InterceptFunction> fun = ctx -> {
return ic -> {
Object instance = null;
if (!isStatic) {
for (Object testInstanceData : testInstanceStack) {
// the objects on the stack are instances of `TestInstance` in the original CL,
// need to obtain the test instance (which in turn comes from the extra CL) reflectively
Field field = testInstanceData.getClass().getDeclaredField("testInstance");
field.setAccessible(true);
Object testInstance = field.get(testInstanceData);
if (testInstance.getClass().equals(testClass)) {
instance = testInstance;
break;
}
}
if (instance == null) {
throw new IllegalStateException("Test instance not available");
}
}
if (!method.canAccess(instance)) {
method.setAccessible(true);
}
return method.invoke(instance, ic);
};
};

createFunctions.put(key, fun);
}
}

static void clear() {
interceptorMethods.clear();
createFunctions.clear();
idGenerator.set(0);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;

import org.jboss.logging.Logger;
Expand All @@ -11,10 +12,11 @@
import io.quarkus.arc.SyntheticCreationalContext;

public class MockBeanCreator implements BeanCreator<Object> {
private static final Logger LOG = Logger.getLogger(MockBeanCreator.class);

static final String CREATE_KEY = "createKey";

private static final Logger LOG = Logger.getLogger(MockBeanCreator.class);
private static final AtomicInteger idGenerator = new AtomicInteger();

private static final Map<String, Function<SyntheticCreationalContext<?>, ?>> createFunctions = new HashMap<>();

Expand All @@ -34,12 +36,15 @@ public Object create(SyntheticCreationalContext<Object> context) {
return Mockito.mock(implementationClass);
}

static void registerCreate(String key, Function<SyntheticCreationalContext<?>, ?> create) {
static String registerCreate(Function<SyntheticCreationalContext<?>, ?> create) {
String key = "io_quarkus_test_component_MockBeanCreator_" + idGenerator.incrementAndGet();
createFunctions.put(key, create);
return key;
}

static void clear() {
createFunctions.clear();
idGenerator.set(0);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,94 @@

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.URL;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Map;
import java.util.Objects;

import io.quarkus.arc.ComponentsProvider;
import io.quarkus.arc.ResourceReferenceProvider;

class QuarkusComponentTestClassLoader extends ClassLoader {
static {
ClassLoader.registerAsParallelCapable();
}

private final Map<String, byte[]> localClasses; // generated and transformed classes
private final File componentsProviderFile;
private final File resourceReferenceProviderFile;

public QuarkusComponentTestClassLoader(ClassLoader parent, File componentsProviderFile,
File resourceReferenceProviderFile) {
public QuarkusComponentTestClassLoader(ClassLoader parent, Map<String, byte[]> localClasses,
File componentsProviderFile) {
super(parent);

this.localClasses = localClasses;
this.componentsProviderFile = Objects.requireNonNull(componentsProviderFile);
this.resourceReferenceProviderFile = resourceReferenceProviderFile;
}

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = findLoadedClass(name);
if (clazz != null) {
return clazz;
}

byte[] bytecode = null;
if (localClasses != null) {
bytecode = localClasses.get(name);
}
if (bytecode == null && !mustDelegateToParent(name)) {
String path = name.replace('.', '/') + ".class";
try (InputStream in = getParent().getResourceAsStream(path)) {
if (in != null) {
bytecode = in.readAllBytes();
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
if (bytecode != null) {
clazz = defineClass(name, bytecode, 0, bytecode.length);
if (resolve) {
resolveClass(clazz);
}
return clazz;
}

return super.loadClass(name, resolve);
}
}

private static boolean mustDelegateToParent(String name) {
return name.startsWith("java.")
|| name.startsWith("jdk.")
|| name.startsWith("javax.")
|| name.startsWith("sun.")
|| name.startsWith("com.sun.")
|| name.startsWith("org.ietf.jgss.")
|| name.startsWith("org.w3c.")
|| name.startsWith("org.xml.")
|| name.startsWith("org.jcp.xml.")
|| name.equals("io.quarkus.dev.testing.TracingHandler");
}

@Override
public Enumeration<URL> getResources(String name) throws IOException {
if (("META-INF/services/" + ComponentsProvider.class.getName()).equals(name)) {
// return URL that points to the correct components provider
return Collections.enumeration(Collections.singleton(componentsProviderFile.toURI()
.toURL()));
} else if (resourceReferenceProviderFile != null
&& ("META-INF/services/" + ResourceReferenceProvider.class.getName()).equals(name)) {
return Collections.enumeration(Collections.singleton(resourceReferenceProviderFile.toURI()
.toURL()));
if (componentsProviderFile != null
&& ("META-INF/services/" + ComponentsProvider.class.getName()).equals(name)) {
return Collections.enumeration(Collections.singleton(componentsProviderFile.toURI().toURL()));
}
return super.getResources(name);
}

public static QuarkusComponentTestClassLoader inTCCL() {
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
if (tccl instanceof QuarkusComponentTestClassLoader) {
return (QuarkusComponentTestClassLoader) tccl;
}
throw new IllegalStateException("TCCL is not QuarkusComponentTestClassLoader, the `@RegisterExtension` field"
+ " of type `QuarkusComponentTestExtension` must be `static`");
}
}
Loading
Loading