diff --git a/integration-tests/main/src/test/java/io/quarkus/it/main/GreetingProfileTestCase.java b/integration-tests/main/src/test/java/io/quarkus/it/main/GreetingProfileTestCase.java index ed4115bd2740d3..7c4d9abf010a65 100644 --- a/integration-tests/main/src/test/java/io/quarkus/it/main/GreetingProfileTestCase.java +++ b/integration-tests/main/src/test/java/io/quarkus/it/main/GreetingProfileTestCase.java @@ -5,10 +5,12 @@ import java.util.Collections; 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; @@ -35,6 +37,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 @@ -46,5 +54,40 @@ public Map getConfigOverrides() { public Set> getEnabledAlternatives() { return Collections.singleton(BonjourService.class); } + + @Override + public Map, Map> testResources() { + return Collections.singletonMap(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 initArgs) { + numArg = Integer.parseInt(initArgs.get("num")); + state.set(numArg); + } + + @Override + public Map 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"); + } + } } } diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java b/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java index 8051002d82e808..73091000cb4fe2 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java @@ -27,7 +27,12 @@ public class TestResourceManager implements Closeable { private boolean started = false; public TestResourceManager(Class testClass) { - testResourceEntries = getTestResources(testClass); + this(testClass, Collections.emptyMap()); + } + + public TestResourceManager(Class testClass, + Map, Map> additionalTestResources) { + testResourceEntries = getTestResources(testClass, additionalTestResources); } public void init() { @@ -102,18 +107,19 @@ public void close() { } @SuppressWarnings("unchecked") - private List getTestResources(Class testClass) { + private List getTestResources(Class testClass, + Map, Map> additionalTestResources) { IndexView index = TestClassIndexer.readIndex(testClass); List 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 alreadyAddedEntries = new HashSet<>(); + Set uniqueEntries = new HashSet<>(); for (AnnotationInstance annotation : findQuarkusTestResourceInstances(index)) { try { - Class testResourceClass = (Class) Class - .forName(annotation.value().asString(), true, Thread.currentThread().getContextClassLoader()); + Class testResourceClass = loadTestResourceClassFromTCCL( + annotation.value().asString()); AnnotationValue argsAnnotationValue = annotation.value("initArgs"); Map args; @@ -128,15 +134,38 @@ private List getTestResources(Class testClass) { } TestResourceClassEntry testResourceClassEntry = new TestResourceClassEntry(testResourceClass, args); - if (alreadyAddedEntries.contains(testResourceClassEntry)) { + if (uniqueEntries.contains(testResourceClassEntry)) { + continue; + } + uniqueEntries.add(testResourceClassEntry); + } catch (IllegalArgumentException | SecurityException e) { + throw new RuntimeException("Unable to instantiate the test resource " + annotation.value().asString()); + } + } + + for (Map.Entry, Map> entry : additionalTestResources + .entrySet()) { + String testResourceClassName = entry.getKey().getName(); + try { + TestResourceClassEntry entryFromCorrectCL = new TestResourceClassEntry( + loadTestResourceClassFromTCCL(testResourceClassName), entry.getValue()); + if (uniqueEntries.contains(entryFromCorrectCL)) { continue; } - alreadyAddedEntries.add(testResourceClassEntry); + uniqueEntries.add(entryFromCorrectCL); + } catch (IllegalArgumentException | SecurityException e) { + throw new RuntimeException("Unable to instantiate the test resource " + testResourceClassName); + } + } + for (TestResourceClassEntry entry : uniqueEntries) { + Class testResourceClass = entry.clazz; + Map 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()); } } @@ -158,6 +187,16 @@ public int compare(TestResourceEntry o1, TestResourceEntry o2) { return testResourceEntries; } + @SuppressWarnings("unchecked") + private Class loadTestResourceClassFromTCCL(String className) { + try { + return (Class) Class + .forName(className, true, Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + private Collection findQuarkusTestResourceInstances(IndexView index) { Set testResourceAnnotations = new HashSet<>(index .getAnnotations(DotName.createSimple(QuarkusTestResource.class.getName()))); @@ -168,29 +207,7 @@ private Collection findQuarkusTestResourceInstances(IndexVie return testResourceAnnotations; } - private static class TestResourceEntry { - private final QuarkusTestResourceLifecycleManager testResource; - private final Map args; - - public TestResourceEntry(QuarkusTestResourceLifecycleManager testResource) { - this(testResource, Collections.emptyMap()); - } - - public TestResourceEntry(QuarkusTestResourceLifecycleManager testResource, Map args) { - this.testResource = testResource; - this.args = args; - } - - public QuarkusTestResourceLifecycleManager getTestResource() { - return testResource; - } - - public Map getArgs() { - return args; - } - } - - private static class TestResourceClassEntry { + public static class TestResourceClassEntry { private Class clazz; private Map args; @@ -216,4 +233,26 @@ public int hashCode() { } } + private static class TestResourceEntry { + private final QuarkusTestResourceLifecycleManager testResource; + private final Map args; + + public TestResourceEntry(QuarkusTestResourceLifecycleManager testResource) { + this(testResource, Collections.emptyMap()); + } + + public TestResourceEntry(QuarkusTestResourceLifecycleManager testResource, Map args) { + this.testResource = testResource; + this.args = args; + } + + public QuarkusTestResourceLifecycleManager getTestResource() { + return testResource; + } + + public Map getArgs() { + return args; + } + } + } diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java index 3f1d334eb10995..8da54ba6c31097 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java @@ -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; @@ -63,6 +64,7 @@ import io.quarkus.runtime.configuration.ProfileManager; import io.quarkus.test.common.PathTestHelper; import io.quarkus.test.common.PropertyTestUtil; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; import io.quarkus.test.common.RestAssuredURLManager; import io.quarkus.test.common.TestClassIndexer; import io.quarkus.test.common.TestResourceManager; @@ -132,8 +134,9 @@ private ExtensionState doJavaStart(ExtensionContext context, Class additional = new HashMap<>(profileInstance.getConfigOverrides()); if (!profileInstance.getEnabledAlternatives().isEmpty()) { additional.put("quarkus.arc.selected-alternatives", profileInstance.getEnabledAlternatives().stream() @@ -188,8 +191,8 @@ private ExtensionState doJavaStart(ExtensionContext context, Class, Map> getAdditionalTestResourceEntries( + QuarkusTestProfile profileInstance) { + if (profileInstance == null) { + return Collections.emptyMap(); + } + + Map, Map> testResources = profileInstance + .testResources(); + if (testResources.isEmpty()) { + return Collections.emptyMap(); + } + + return testResources; + } + // 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()); diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestProfile.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestProfile.java index f370cc0f929f67..f6f093f6679dae 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestProfile.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestProfile.java @@ -4,6 +4,8 @@ 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. @@ -39,4 +41,12 @@ default Set> getEnabledAlternatives() { default String getConfigProfile() { return null; } + + /** + * {@link QuarkusTestResourceLifecycleManager} classes (along with their init params) to be used from this + * specific test profile + */ + default Map, Map> testResources() { + return Collections.emptyMap(); + } }