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

Allow the use of QuarkusTestResourceLifecycleManager with @TestProfile #10368

Merged
Merged
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 @@ -3,12 +3,15 @@
import static org.hamcrest.Matchers.is;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.QuarkusTestProfile;
import io.quarkus.test.junit.TestProfile;
Expand All @@ -35,6 +38,12 @@ public void testPortTakesEffect() {
Assertions.assertEquals(7777, RestAssured.port);
}

@Test
public void testTestResourceState() {
// 155 means that the TestResource was started but hasn't yet stopped
Assertions.assertEquals(155, DummyTestResource.state.get());
}

public static class MyProfile implements QuarkusTestProfile {

@Override
Expand All @@ -46,5 +55,41 @@ public Map<String, String> getConfigOverrides() {
public Set<Class<?>> getEnabledAlternatives() {
return Collections.singleton(BonjourService.class);
}

@Override
public List<TestResourceEntry> testResources() {
return Collections
.singletonList(new TestResourceEntry(DummyTestResource.class, Collections.singletonMap("num", "100")));
}
}

/**
* This only used to ensure that the TestResource has been handled correctly by the QuarkusTestExtension
*/
public static class DummyTestResource implements QuarkusTestResourceLifecycleManager {

public static final AtomicInteger state = new AtomicInteger(0);
public static final int START_DELTA = 55;

private Integer numArg;

@Override
public void init(Map<String, String> initArgs) {
numArg = Integer.parseInt(initArgs.get("num"));
state.set(numArg);
}

@Override
public Map<String, String> start() {
state.addAndGet(START_DELTA);
return Collections.emptyMap();
}

@Override
public void stop() {
if (state.get() != (numArg + START_DELTA)) {
throw new IllegalStateException("TestResource state was not properly handled");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ public class TestResourceManager implements Closeable {
private boolean started = false;

public TestResourceManager(Class<?> testClass) {
testResourceEntries = getTestResources(testClass);
this(testClass, Collections.emptyList());
}

public TestResourceManager(Class<?> testClass, List<TestResourceClassEntry> additionalTestResources) {
testResourceEntries = getTestResources(testClass, additionalTestResources);
}

public void init() {
Expand Down Expand Up @@ -101,19 +105,18 @@ public void close() {
}
}

@SuppressWarnings("unchecked")
private List<TestResourceEntry> getTestResources(Class<?> testClass) {
private List<TestResourceEntry> getTestResources(Class<?> testClass, List<TestResourceClassEntry> additionalTestResources) {
IndexView index = TestClassIndexer.readIndex(testClass);

List<TestResourceEntry> testResourceEntries = new ArrayList<>();

// we need to keep track of duplicate entries to make sure we don't start the same resource
// multiple times even if there are multiple same @QuarkusTestResource annotations
Set<TestResourceClassEntry> alreadyAddedEntries = new HashSet<>();
Set<TestResourceClassEntry> uniqueEntries = new HashSet<>();
for (AnnotationInstance annotation : findQuarkusTestResourceInstances(index)) {
try {
Class<? extends QuarkusTestResourceLifecycleManager> testResourceClass = (Class<? extends QuarkusTestResourceLifecycleManager>) Class
.forName(annotation.value().asString(), true, Thread.currentThread().getContextClassLoader());
Class<? extends QuarkusTestResourceLifecycleManager> testResourceClass = loadTestResourceClassFromTCCL(
annotation.value().asString());

AnnotationValue argsAnnotationValue = annotation.value("initArgs");
Map<String, String> args;
Expand All @@ -127,16 +130,22 @@ private List<TestResourceEntry> getTestResources(Class<?> testClass) {
}
}

TestResourceClassEntry testResourceClassEntry = new TestResourceClassEntry(testResourceClass, args);
if (alreadyAddedEntries.contains(testResourceClassEntry)) {
continue;
}
alreadyAddedEntries.add(testResourceClassEntry);
uniqueEntries.add(new TestResourceClassEntry(testResourceClass, args));
} catch (IllegalArgumentException | SecurityException e) {
throw new RuntimeException("Unable to instantiate the test resource " + annotation.value().asString());
}
}

uniqueEntries.addAll(additionalTestResources);

for (TestResourceClassEntry entry : uniqueEntries) {
Class<? extends QuarkusTestResourceLifecycleManager> testResourceClass = entry.clazz;
Map<String, String> args = entry.args;
try {
testResourceEntries.add(new TestResourceEntry(testResourceClass.getConstructor().newInstance(), args));
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
throw new RuntimeException("Unable to instantiate the test resource " + annotation.value().asString());
throw new RuntimeException("Unable to instantiate the test resource " + testResourceClass.getName());
}
}

Expand All @@ -158,6 +167,16 @@ public int compare(TestResourceEntry o1, TestResourceEntry o2) {
return testResourceEntries;
}

@SuppressWarnings("unchecked")
private Class<? extends QuarkusTestResourceLifecycleManager> loadTestResourceClassFromTCCL(String className) {
try {
return (Class<? extends QuarkusTestResourceLifecycleManager>) Class
.forName(className, true, Thread.currentThread().getContextClassLoader());
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}

private Collection<AnnotationInstance> findQuarkusTestResourceInstances(IndexView index) {
Set<AnnotationInstance> testResourceAnnotations = new HashSet<>(index
.getAnnotations(DotName.createSimple(QuarkusTestResource.class.getName())));
Expand All @@ -168,29 +187,7 @@ private Collection<AnnotationInstance> findQuarkusTestResourceInstances(IndexVie
return testResourceAnnotations;
}

private static class TestResourceEntry {
private final QuarkusTestResourceLifecycleManager testResource;
private final Map<String, String> args;

public TestResourceEntry(QuarkusTestResourceLifecycleManager testResource) {
this(testResource, Collections.emptyMap());
}

public TestResourceEntry(QuarkusTestResourceLifecycleManager testResource, Map<String, String> args) {
this.testResource = testResource;
this.args = args;
}

public QuarkusTestResourceLifecycleManager getTestResource() {
return testResource;
}

public Map<String, String> getArgs() {
return args;
}
}

private static class TestResourceClassEntry {
public static class TestResourceClassEntry {
private Class<? extends QuarkusTestResourceLifecycleManager> clazz;
private Map<String, String> args;

Expand All @@ -216,4 +213,26 @@ public int hashCode() {
}
}

private static class TestResourceEntry {
private final QuarkusTestResourceLifecycleManager testResource;
private final Map<String, String> args;

public TestResourceEntry(QuarkusTestResourceLifecycleManager testResource) {
this(testResource, Collections.emptyMap());
}

public TestResourceEntry(QuarkusTestResourceLifecycleManager testResource, Map<String, String> args) {
this.testResource = testResource;
this.args = args;
}

public QuarkusTestResourceLifecycleManager getTestResource() {
return testResource;
}

public Map<String, String> getArgs() {
return args;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.nio.file.Paths;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -132,8 +133,9 @@ private ExtensionState doJavaStart(ExtensionContext context, Class<? extends Qua
final QuarkusBootstrap.Builder runnerBuilder = QuarkusBootstrap.builder()
.setIsolateDeployment(true)
.setMode(QuarkusBootstrap.Mode.TEST);
QuarkusTestProfile profileInstance = null;
if (profile != null) {
QuarkusTestProfile profileInstance = profile.newInstance();
profileInstance = profile.newInstance();
Map<String, String> additional = new HashMap<>(profileInstance.getConfigOverrides());
if (!profileInstance.getEnabledAlternatives().isEmpty()) {
additional.put("quarkus.arc.selected-alternatives", profileInstance.getEnabledAlternatives().stream()
Expand Down Expand Up @@ -188,8 +190,9 @@ private ExtensionState doJavaStart(ExtensionContext context, Class<? extends Qua

//must be done after the TCCL has been set
testResourceManager = (Closeable) startupAction.getClassLoader().loadClass(TestResourceManager.class.getName())
.getConstructor(Class.class)
.newInstance(requiredTestClass);
.getConstructor(Class.class, List.class)
.newInstance(requiredTestClass,
getAdditionalTestResources(profileInstance, startupAction.getClassLoader()));
testResourceManager.getClass().getMethod("init").invoke(testResourceManager);
testResourceManager.getClass().getMethod("start").invoke(testResourceManager);

Expand Down Expand Up @@ -253,6 +256,37 @@ public void run() {
}
}

/**
* Since {@link TestResourceManager} is loaded from the ClassLoader passed in as an argument,
* we need to convert the user input {@link QuarkusTestProfile.TestResourceEntry} into instances of
* {@link TestResourceManager.TestResourceClassEntry}
* that are loaded from that ClassLoader
*/
private List<Object> getAdditionalTestResources(
QuarkusTestProfile profileInstance, ClassLoader classLoader) {
if ((profileInstance == null) || profileInstance.testResources().isEmpty()) {
return Collections.emptyList();
}

try {
Constructor<?> testResourceClassEntryConstructor = Class
.forName(TestResourceManager.TestResourceClassEntry.class.getName(), true, classLoader)
.getConstructor(Class.class, Map.class);

List<QuarkusTestProfile.TestResourceEntry> testResources = profileInstance.testResources();
List<Object> result = new ArrayList<>(testResources.size());
for (QuarkusTestProfile.TestResourceEntry testResource : testResources) {
Object instance = testResourceClassEntryConstructor.newInstance(
Class.forName(testResource.getClazz().getName(), true, classLoader), testResource.getArgs());
result.add(instance);
}

return result;
} catch (Exception e) {
throw new IllegalStateException("Unable to handle profile " + profileInstance.getClass());
}
}

// keep it super simple for now, but we might need multiple strategies in the future
private void populateDeepCloneField(StartupAction startupAction) {
deepClone = new XStreamDeepClone(startupAction.getClassLoader());
Expand Down Expand Up @@ -538,6 +572,7 @@ public void interceptTestTemplateMethod(Invocation<Void> invocation, ReflectiveI
invocation.skip();
}

@SuppressWarnings("unchecked")
@Override
public <T> T interceptTestFactoryMethod(Invocation<T> invocation,
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package io.quarkus.test.junit;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;

/**
* Defines a 'test profile'. Tests run under a test profile
* will have different configuration options to other tests.
Expand Down Expand Up @@ -39,4 +42,30 @@ default Set<Class<?>> getEnabledAlternatives() {
default String getConfigProfile() {
return null;
}

/**
* {@link QuarkusTestResourceLifecycleManager} classes (along with their init params) to be used from this
* specific test profile
*/
default List<TestResourceEntry> testResources() {
return Collections.emptyList();
}

final class TestResourceEntry {
private Class<? extends QuarkusTestResourceLifecycleManager> clazz;
private Map<String, String> args;

public TestResourceEntry(Class<? extends QuarkusTestResourceLifecycleManager> clazz, Map<String, String> args) {
this.clazz = clazz;
this.args = args;
}

public Class<? extends QuarkusTestResourceLifecycleManager> getClazz() {
return clazz;
}

public Map<String, String> getArgs() {
return args;
}
}
}