diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchiveProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchiveProcessor.java index 162daf43e3aaf..c03e0d3280d86 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchiveProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchiveProcessor.java @@ -22,10 +22,12 @@ import org.jboss.jandex.Indexer; import org.jboss.logging.Logger; +import io.quarkus.arc.processor.AnnotationsTransformer; import io.quarkus.arc.processor.BeanArchives; import io.quarkus.arc.processor.BeanDefiningAnnotation; import io.quarkus.arc.processor.BeanDeployment; import io.quarkus.arc.processor.DotNames; +import io.quarkus.arc.runtime.AdditionalBean; import io.quarkus.deployment.ApplicationArchive; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -51,7 +53,8 @@ public BeanArchiveIndexBuildItem build(ArcConfig config, ApplicationArchivesBuil CustomScopeAnnotationsBuildItem customScopes, List excludeDependencyBuildItems, List beanArchivePredicates, List knownCompatibleBeanArchives, - BuildCompatibleExtensionsBuildItem buildCompatibleExtensions) + BuildCompatibleExtensionsBuildItem buildCompatibleExtensions, + BuildProducer annotationsTransformations) throws Exception { // First build an index from application archives @@ -69,6 +72,20 @@ public BeanArchiveIndexBuildItem build(ArcConfig config, ApplicationArchivesBuil Set additionalBeansFromExtensions = new HashSet<>(); buildCompatibleExtensions.entrypoint.runDiscovery(applicationIndex, additionalBeansFromExtensions); additionalBeanClasses.addAll(additionalBeansFromExtensions); + annotationsTransformations.produce(new AnnotationsTransformerBuildItem(new AnnotationsTransformer() { + @Override + public boolean appliesTo(Kind kind) { + return kind == Kind.CLASS; + } + + @Override + public void transform(TransformationContext ctx) { + if (additionalBeansFromExtensions.contains(ctx.getTarget().asClass().name().toString())) { + // make all the `@Discovery`-registered classes beans + ctx.transform().add(AdditionalBean.class).done(); + } + } + })); // Build the index for additional beans and generated bean classes Set additionalIndex = new HashSet<>(); @@ -131,13 +148,22 @@ private IndexView buildApplicationIndex(ArcConfig config, ApplicationArchivesBui .map(bda -> new BeanDefiningAnnotation(bda.getName(), bda.getDefaultScope())) .collect(Collectors.toList()), stereotypes); beanDefiningAnnotations.addAll(customScopes.getCustomScopeNames()); - // Also include archives that are not bean archives but contain qualifiers or interceptor bindings + // Also include archives that are not bean archives but contain scopes, qualifiers or interceptor bindings + beanDefiningAnnotations.add(DotNames.SCOPE); + beanDefiningAnnotations.add(DotNames.NORMAL_SCOPE); beanDefiningAnnotations.add(DotNames.QUALIFIER); beanDefiningAnnotations.add(DotNames.INTERCEPTOR_BINDING); + boolean rootIsAlwaysBeanArchive = !config.strictCompatibility; + Collection candidateArchives = applicationArchivesBuildItem.getApplicationArchives(); + if (!rootIsAlwaysBeanArchive) { + candidateArchives = new ArrayList<>(candidateArchives); + candidateArchives.add(applicationArchivesBuildItem.getRootArchive()); + } + List indexes = new ArrayList<>(); - for (ApplicationArchive archive : applicationArchivesBuildItem.getApplicationArchives()) { + for (ApplicationArchive archive : candidateArchives) { if (isApplicationArchiveExcluded(config, excludeDependencyBuildItems, archive)) { continue; } @@ -151,7 +177,9 @@ private IndexView buildApplicationIndex(ArcConfig config, ApplicationArchivesBui indexes.add(index); } } - indexes.add(applicationArchivesBuildItem.getRootArchive().getIndex()); + if (rootIsAlwaysBeanArchive) { + indexes.add(applicationArchivesBuildItem.getRootArchive().getIndex()); + } return CompositeIndex.create(indexes); } diff --git a/independent-projects/arc/pom.xml b/independent-projects/arc/pom.xml index 720a01b64e47c..b21c8b33c0bdc 100644 --- a/independent-projects/arc/pom.xml +++ b/independent-projects/arc/pom.xml @@ -60,7 +60,7 @@ 1.7.0.Final 2.0.1 - 4.0.9 + 4.0.10 4.13.2 3.11.0 diff --git a/independent-projects/arc/tcks/arquillian/src/main/java/io/quarkus/arc/arquillian/ArcDeployableContainer.java b/independent-projects/arc/tcks/arquillian/src/main/java/io/quarkus/arc/arquillian/ArcDeployableContainer.java index 25a3ee26e9fc1..d011791f2282f 100644 --- a/independent-projects/arc/tcks/arquillian/src/main/java/io/quarkus/arc/arquillian/ArcDeployableContainer.java +++ b/independent-projects/arc/tcks/arquillian/src/main/java/io/quarkus/arc/arquillian/ArcDeployableContainer.java @@ -106,11 +106,13 @@ private Object findTest(ArcContainer arc, Class testClass) { return instance.get(); } - // fallback for generic test classes, whose set of bean types does not contain a `Class` - // but a `ParameterizedType` instead - for (InstanceHandle handle : arc.listAll(Object.class)) { - if (testClass.equals(handle.getBean().getBeanClass())) { - return handle.get(); + if (testClass.getTypeParameters().length > 0) { + // fallback for generic test classes, whose set of bean types does not contain a `Class` + // but a `ParameterizedType` instead + for (InstanceHandle handle : arc.listAll(Object.class)) { + if (testClass.equals(handle.getBean().getBeanClass())) { + return handle.get(); + } } } diff --git a/independent-projects/arc/tcks/cdi-tck-runner/pom.xml b/independent-projects/arc/tcks/cdi-tck-runner/pom.xml index 9cf87fd791c06..ccbc8f951ca31 100644 --- a/independent-projects/arc/tcks/cdi-tck-runner/pom.xml +++ b/independent-projects/arc/tcks/cdi-tck-runner/pom.xml @@ -74,6 +74,7 @@ src/test/resources/testng.xml + true ${project.build.directory}/porting-pkg false diff --git a/independent-projects/arc/tcks/cdi-tck-runner/src/test/resources/META-INF/cdi-tck.properties b/independent-projects/arc/tcks/cdi-tck-runner/src/test/resources/META-INF/cdi-tck.properties deleted file mode 100644 index c38de38628e08..0000000000000 --- a/independent-projects/arc/tcks/cdi-tck-runner/src/test/resources/META-INF/cdi-tck.properties +++ /dev/null @@ -1 +0,0 @@ -org.jboss.cdi.tck.cdiLiteMode=true diff --git a/independent-projects/arc/tcks/cdi-tck-runner/src/test/resources/testng.xml b/independent-projects/arc/tcks/cdi-tck-runner/src/test/resources/testng.xml index 87e459298438e..04d01134aff6c 100644 --- a/independent-projects/arc/tcks/cdi-tck-runner/src/test/resources/testng.xml +++ b/independent-projects/arc/tcks/cdi-tck-runner/src/test/resources/testng.xml @@ -20,87 +20,14 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tcks/jakarta-atinject/pom.xml b/tcks/jakarta-atinject/pom.xml new file mode 100644 index 0000000000000..ae1bf0a694d24 --- /dev/null +++ b/tcks/jakarta-atinject/pom.xml @@ -0,0 +1,88 @@ + + + quarkus-tck-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-tck-jakarta-atinjecft + Quarkus - TCK - Jakarta AtInject + + + 2.0.1 + 4.13.2 + + + + + io.quarkus + quarkus-arquillian + + + io.quarkus + quarkus-arc + + + + jakarta.inject + jakarta.inject-tck + ${atinject-tck.version} + + + jakarta.inject + jakarta.inject-api + + + junit + junit + + + + + junit + junit + ${junit4.version} + + + org.jboss.arquillian.junit + arquillian-junit-container + + + + + io.quarkus + quarkus-arc-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + false + true + false + + false + + + + + + diff --git a/tcks/jakarta-atinject/src/test/java/io/quarkus/tck/atinject/AtInjectTckExtension.java b/tcks/jakarta-atinject/src/test/java/io/quarkus/tck/atinject/AtInjectTckExtension.java new file mode 100644 index 0000000000000..d0c7753c8983b --- /dev/null +++ b/tcks/jakarta-atinject/src/test/java/io/quarkus/tck/atinject/AtInjectTckExtension.java @@ -0,0 +1,53 @@ +package io.quarkus.tck.atinject; + +import jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension; +import jakarta.enterprise.inject.build.compatible.spi.ClassConfig; +import jakarta.enterprise.inject.build.compatible.spi.Discovery; +import jakarta.enterprise.inject.build.compatible.spi.Enhancement; +import jakarta.enterprise.inject.build.compatible.spi.MetaAnnotations; +import jakarta.enterprise.inject.build.compatible.spi.ScannedClasses; +import jakarta.enterprise.inject.literal.NamedLiteral; + +import org.atinject.tck.auto.Convertible; +import org.atinject.tck.auto.Drivers; +import org.atinject.tck.auto.DriversSeat; +import org.atinject.tck.auto.FuelTank; +import org.atinject.tck.auto.Seat; +import org.atinject.tck.auto.Tire; +import org.atinject.tck.auto.V8Engine; +import org.atinject.tck.auto.accessories.Cupholder; +import org.atinject.tck.auto.accessories.SpareTire; + +public class AtInjectTckExtension implements BuildCompatibleExtension { + @Discovery + public void discovery(ScannedClasses scan, MetaAnnotations meta) { + scan.add(Convertible.class.getName()); + scan.add(DriversSeat.class.getName()); + scan.add(FuelTank.class.getName()); + scan.add(Seat.class.getName()); + scan.add(Tire.class.getName()); + scan.add(V8Engine.class.getName()); + + scan.add(Cupholder.class.getName()); + scan.add(SpareTire.class.getName()); + } + + @Enhancement(types = Convertible.class) + public void convertible(ClassConfig clazz) { + clazz.fields() + .stream() + .filter(it -> "spareTire".equals(it.info().name())) + .forEach(it -> it.addAnnotation(Spare.class)); + } + + @Enhancement(types = DriversSeat.class) + public void driversSeat(ClassConfig clazz) { + clazz.addAnnotation(Drivers.class); + } + + @Enhancement(types = SpareTire.class) + public void spareTire(ClassConfig clazz) { + clazz.addAnnotation(NamedLiteral.of("spare")) + .addAnnotation(Spare.class); + } +} diff --git a/tcks/jakarta-atinject/src/test/java/io/quarkus/tck/atinject/AtInjectTest.java b/tcks/jakarta-atinject/src/test/java/io/quarkus/tck/atinject/AtInjectTest.java new file mode 100644 index 0000000000000..5f9753f116646 --- /dev/null +++ b/tcks/jakarta-atinject/src/test/java/io/quarkus/tck/atinject/AtInjectTest.java @@ -0,0 +1,61 @@ +package io.quarkus.tck.atinject; + +import static org.junit.Assert.assertTrue; + +import java.util.Enumeration; + +import jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension; + +import org.atinject.tck.Tck; +import org.atinject.tck.auto.Car; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.Test; +import org.junit.runner.RunWith; + +import io.quarkus.arc.Arc; + +@RunWith(Arquillian.class) +public class AtInjectTest { + @Deployment + public static Archive deployment() { + return ShrinkWrap.create(JavaArchive.class) + .addPackages(true, Tck.class.getPackage()) + .addClasses(AtInjectTest.class, AtInjectTckExtension.class, Spare.class) + .addAsServiceProvider(BuildCompatibleExtension.class, AtInjectTckExtension.class) + .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml"); + } + + @Test + public void test() { + Car instance = Arc.container().instance(Car.class).get(); + + junit.framework.Test test = Tck.testsFor(instance, /* supportsStatic */ false, /* supportsPrivate */ true); + junit.framework.TestResult result = new junit.framework.TestResult(); + test.run(result); + + // this is ugly and doesn't report failures properly, but I don't see a better way + if (!result.wasSuccessful()) { + int failuresCount = 0; + Enumeration failures = result.failures(); + while (failures.hasMoreElements()) { + System.out.println(failures.nextElement()); + failuresCount++; + } + + int errorsCount = 0; + Enumeration errors = result.errors(); + while (errors.hasMoreElements()) { + System.out.println(errors.nextElement()); + errorsCount++; + } + System.out.println("Total " + failuresCount + " failures and " + errorsCount + " errors"); + } + + assertTrue(result.wasSuccessful()); + } +} diff --git a/tcks/jakarta-atinject/src/test/java/io/quarkus/tck/atinject/Spare.java b/tcks/jakarta-atinject/src/test/java/io/quarkus/tck/atinject/Spare.java new file mode 100644 index 0000000000000..188f152fb441d --- /dev/null +++ b/tcks/jakarta-atinject/src/test/java/io/quarkus/tck/atinject/Spare.java @@ -0,0 +1,11 @@ +package io.quarkus.tck.atinject; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import jakarta.inject.Qualifier; + +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface Spare { +} diff --git a/tcks/jakarta-cdi-lang-model/pom.xml b/tcks/jakarta-cdi-lang-model/pom.xml new file mode 100644 index 0000000000000..ace4cae236926 --- /dev/null +++ b/tcks/jakarta-cdi-lang-model/pom.xml @@ -0,0 +1,80 @@ + + + quarkus-tck-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-tck-jakarta-cdi-lang-model + Quarkus - TCK - Jakarta CDI Lang Model + + + 4.0.10 + 4.13.2 + + + + + io.quarkus + quarkus-arquillian + + + io.quarkus + quarkus-arc + + + + jakarta.enterprise + cdi-tck-lang-model + ${cdi-tck.version} + + + junit + junit + ${junit4.version} + + + org.jboss.arquillian.junit + arquillian-junit-container + + + + + io.quarkus + quarkus-arc-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + false + true + false + + + false + + + + + + diff --git a/tcks/jakarta-cdi-lang-model/src/test/java/io/quarkus/tck/cdi/lang/model/LangModelTckExtension.java b/tcks/jakarta-cdi-lang-model/src/test/java/io/quarkus/tck/cdi/lang/model/LangModelTckExtension.java new file mode 100644 index 0000000000000..5861c91ec6742 --- /dev/null +++ b/tcks/jakarta-cdi-lang-model/src/test/java/io/quarkus/tck/cdi/lang/model/LangModelTckExtension.java @@ -0,0 +1,23 @@ +package io.quarkus.tck.cdi.lang.model; + +import jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension; +import jakarta.enterprise.inject.build.compatible.spi.Discovery; +import jakarta.enterprise.inject.build.compatible.spi.Enhancement; +import jakarta.enterprise.inject.build.compatible.spi.ScannedClasses; +import jakarta.enterprise.lang.model.declarations.ClassInfo; + +import org.jboss.cdi.lang.model.tck.LangModelVerifier; + +public class LangModelTckExtension implements BuildCompatibleExtension { + @Discovery + public void addClass(ScannedClasses scan) { + // `LangModelVerifier` has no bean defining annotation + // and isn't discovered in annotated discovery + scan.add(LangModelVerifier.class.getName()); + } + + @Enhancement(types = LangModelVerifier.class) + public void run(ClassInfo clazz) { + LangModelVerifier.verify(clazz); + } +} diff --git a/tcks/jakarta-cdi-lang-model/src/test/java/io/quarkus/tck/cdi/lang/model/LangModelTest.java b/tcks/jakarta-cdi-lang-model/src/test/java/io/quarkus/tck/cdi/lang/model/LangModelTest.java new file mode 100644 index 0000000000000..14c8c8a831079 --- /dev/null +++ b/tcks/jakarta-cdi-lang-model/src/test/java/io/quarkus/tck/cdi/lang/model/LangModelTest.java @@ -0,0 +1,31 @@ +package io.quarkus.tck.cdi.lang.model; + +import jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.cdi.lang.model.tck.LangModelVerifier; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(Arquillian.class) +public class LangModelTest { + @Deployment + public static Archive deployment() { + return ShrinkWrap.create(JavaArchive.class) + .addPackage(LangModelVerifier.class.getPackage()) + .addClasses(LangModelTest.class, LangModelTckExtension.class) + .addAsServiceProvider(BuildCompatibleExtension.class, LangModelTckExtension.class) + .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml"); + } + + @Test + public void test() { + // the test itself runs in LangModelTckExtension + // and if it fails, deployment fails + } +} diff --git a/tcks/jakarta-cdi/pom.xml b/tcks/jakarta-cdi/pom.xml new file mode 100644 index 0000000000000..db576b37d02c9 --- /dev/null +++ b/tcks/jakarta-cdi/pom.xml @@ -0,0 +1,99 @@ + + + quarkus-tck-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-tck-jakarta-cdi + Quarkus - TCK - Jakarta CDI + + + 4.0.10 + + + + + io.quarkus + quarkus-arquillian + + + io.quarkus + quarkus-arc + + + io.quarkus.arc + arc-cdi-tck-porting-pkg + ${project.version} + + + jakarta.enterprise + cdi-tck-core-impl + ${cdi-tck.version} + + + + + io.quarkus + quarkus-arc-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-porting-pkg + generate-test-resources + + copy-dependencies + + + io.quarkus.arc + arc-cdi-tck-porting-pkg + ${project.build.directory}/porting-pkg + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + true + ${project.build.directory}/porting-pkg + false + true + false + + + jakarta.enterprise:cdi-tck-core-impl + + + src/test/resources/testng.xml + + false + + + + + + diff --git a/tcks/jakarta-cdi/src/test/resources/testng.xml b/tcks/jakarta-cdi/src/test/resources/testng.xml new file mode 100644 index 0000000000000..04d01134aff6c --- /dev/null +++ b/tcks/jakarta-cdi/src/test/resources/testng.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tcks/microprofile-context-propagation/src/main/java/io/quarkus/arquillian/ArquillianBeforeAfterEnricher.java b/tcks/microprofile-context-propagation/src/main/java/io/quarkus/arquillian/ArquillianBeforeAfterEnricher.java deleted file mode 100644 index a9a2949286bbe..0000000000000 --- a/tcks/microprofile-context-propagation/src/main/java/io/quarkus/arquillian/ArquillianBeforeAfterEnricher.java +++ /dev/null @@ -1,49 +0,0 @@ -package io.quarkus.arquillian; - -import org.jboss.arquillian.container.spi.context.annotation.DeploymentScoped; -import org.jboss.arquillian.core.api.InstanceProducer; -import org.jboss.arquillian.core.api.annotation.Inject; -import org.jboss.arquillian.core.api.annotation.Observes; - -import io.quarkus.arc.Arc; - -/** - * Activates request context before test runs and shuts it down afterwards - */ -public class ArquillianBeforeAfterEnricher { - - private static final String ERROR_MSG = "Arc container is not running, cannot activate CDI contexts!"; - - @Inject - @DeploymentScoped - private InstanceProducer appClassloader; - - public void on(@Observes(precedence = -100) org.jboss.arquillian.test.spi.event.suite.Before event) throws Throwable { - //we are outside the runtime class loader, so we don't have direct access to the container - Class arcClz = appClassloader.get().loadClass(Arc.class.getName()); - Object container = arcClz.getMethod("container").invoke(null); - if (container != null) { - boolean running = (boolean) container.getClass().getMethod("isRunning").invoke(container); - if (running) { - Object context = container.getClass().getMethod("requestContext").invoke(container); - context.getClass().getMethod("activate").invoke(context); - } else { - throw new IllegalStateException(ERROR_MSG); - } - } - } - - public void on(@Observes(precedence = 100) org.jboss.arquillian.test.spi.event.suite.After event) throws Throwable { - Class arcClz = appClassloader.get().loadClass(Arc.class.getName()); - Object container = arcClz.getMethod("container").invoke(null); - if (container != null) { - boolean running = (boolean) container.getClass().getMethod("isRunning").invoke(container); - if (running) { - Object context = container.getClass().getMethod("requestContext").invoke(container); - context.getClass().getMethod("terminate").invoke(context); - } else { - throw new IllegalStateException(ERROR_MSG); - } - } - } -} diff --git a/tcks/microprofile-context-propagation/src/main/java/io/quarkus/arquillian/QuarkusArquillianLifecycleExtension.java b/tcks/microprofile-context-propagation/src/main/java/io/quarkus/arquillian/QuarkusArquillianLifecycleExtension.java deleted file mode 100644 index cf3362278045e..0000000000000 --- a/tcks/microprofile-context-propagation/src/main/java/io/quarkus/arquillian/QuarkusArquillianLifecycleExtension.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.quarkus.arquillian; - -import org.jboss.arquillian.core.spi.LoadableExtension; - -public class QuarkusArquillianLifecycleExtension implements LoadableExtension { - - @Override - public void register(ExtensionBuilder builder) { - // for MP CP TCK only, we want to register this observer which activated CDI contexts - builder.observer(ArquillianBeforeAfterEnricher.class); - } -} diff --git a/tcks/microprofile-context-propagation/src/main/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension b/tcks/microprofile-context-propagation/src/main/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension deleted file mode 100644 index f0a66d5d92b63..0000000000000 --- a/tcks/microprofile-context-propagation/src/main/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension +++ /dev/null @@ -1 +0,0 @@ -io.quarkus.arquillian.QuarkusArquillianLifecycleExtension \ No newline at end of file diff --git a/tcks/pom.xml b/tcks/pom.xml index 37c78b0af8dc2..cab353810baae 100644 --- a/tcks/pom.xml +++ b/tcks/pom.xml @@ -97,6 +97,9 @@ + jakarta-atinject + jakarta-cdi + jakarta-cdi-lang-model microprofile-config microprofile-context-propagation microprofile-fault-tolerance diff --git a/test-framework/arquillian/src/main/java/io/quarkus/arquillian/ClassLoaderExceptionTransformer.java b/test-framework/arquillian/src/main/java/io/quarkus/arquillian/ClassLoaderExceptionTransformer.java index f9934b6b8db40..bd00146bebc55 100644 --- a/test-framework/arquillian/src/main/java/io/quarkus/arquillian/ClassLoaderExceptionTransformer.java +++ b/test-framework/arquillian/src/main/java/io/quarkus/arquillian/ClassLoaderExceptionTransformer.java @@ -16,7 +16,7 @@ public class ClassLoaderExceptionTransformer { @Inject @DeploymentScoped - Instance classLoaderInstance; + Instance deployment; @Inject Instance testResultInstance; @@ -29,7 +29,7 @@ public void transform(@Observes(precedence = -1000) Test event) { try { if (res.getClass().getClassLoader() != null && res.getClass().getClassLoader() != getClass().getClassLoader()) { - if (res.getClass() == classLoaderInstance.get().loadClass(res.getClass().getName())) { + if (res.getClass() == deployment.get().getAppClassLoader().loadClass(res.getClass().getName())) { ByteArrayOutputStream out = new ByteArrayOutputStream(); ObjectOutputStream oo = new ObjectOutputStream(out); oo.writeObject(res); diff --git a/test-framework/arquillian/src/main/java/io/quarkus/arquillian/InjectionEnricher.java b/test-framework/arquillian/src/main/java/io/quarkus/arquillian/InjectionEnricher.java index 79c816d4c59f6..3044d4e6b6624 100644 --- a/test-framework/arquillian/src/main/java/io/quarkus/arquillian/InjectionEnricher.java +++ b/test-framework/arquillian/src/main/java/io/quarkus/arquillian/InjectionEnricher.java @@ -15,6 +15,7 @@ import jakarta.enterprise.inject.spi.BeanManager; import org.jboss.arquillian.container.spi.context.annotation.DeploymentScoped; +import org.jboss.arquillian.core.api.Instance; import org.jboss.arquillian.core.api.InstanceProducer; import org.jboss.arquillian.core.api.annotation.Inject; import org.jboss.arquillian.test.spi.TestEnricher; @@ -37,7 +38,7 @@ public class InjectionEnricher implements TestEnricher { @Inject @DeploymentScoped - private InstanceProducer appClassloader; + private Instance deployment; @Override public void enrich(Object testCase) { @@ -50,7 +51,9 @@ public Object[] resolve(Method method) { ClassLoader old = Thread.currentThread().getContextClassLoader(); try { CreationContextHolder holder = getCreationalContext(); - ClassLoader cl = appClassloader.get() != null ? appClassloader.get() : getClass().getClassLoader(); + ClassLoader cl = deployment.get() != null && deployment.get().hasAppClassLoader() + ? deployment.get().getAppClassLoader() + : getClass().getClassLoader(); Thread.currentThread().setContextClassLoader(cl); Class c = cl.loadClass(IsolatedEnricher.class.getName()); BiFunction function = (BiFunction) c @@ -67,7 +70,9 @@ public Object[] resolve(Method method) { private CreationContextHolder getCreationalContext() { try { - ClassLoader cl = appClassloader.get() != null ? appClassloader.get() : getClass().getClassLoader(); + ClassLoader cl = deployment.get() != null && deployment.get().hasAppClassLoader() + ? deployment.get().getAppClassLoader() + : getClass().getClassLoader(); Class c = cl.loadClass(IsolatedCreationContextCreator.class.getName()); Supplier> supplier = (Supplier>) c .getDeclaredConstructor().newInstance(); diff --git a/test-framework/arquillian/src/main/java/io/quarkus/arquillian/QuarkusBeforeAfterLifecycle.java b/test-framework/arquillian/src/main/java/io/quarkus/arquillian/QuarkusBeforeAfterLifecycle.java index 2a6343ff69bfc..647f8c5cd150d 100644 --- a/test-framework/arquillian/src/main/java/io/quarkus/arquillian/QuarkusBeforeAfterLifecycle.java +++ b/test-framework/arquillian/src/main/java/io/quarkus/arquillian/QuarkusBeforeAfterLifecycle.java @@ -5,7 +5,7 @@ import org.jboss.arquillian.container.spi.context.annotation.DeploymentScoped; import org.jboss.arquillian.container.test.api.RunAsClient; -import org.jboss.arquillian.core.api.InstanceProducer; +import org.jboss.arquillian.core.api.Instance; import org.jboss.arquillian.core.api.annotation.Inject; import org.jboss.arquillian.core.api.annotation.Observes; @@ -25,7 +25,7 @@ public class QuarkusBeforeAfterLifecycle { @Inject @DeploymentScoped - private InstanceProducer appClassloader; + private Instance deployment; public void on(@Observes(precedence = DEFAULT_PRECEDENCE) org.jboss.arquillian.test.spi.event.suite.Before event) throws Throwable { @@ -97,7 +97,9 @@ private void invokeCallbacks(String methodName, String junitOrTestNgCallbackClas throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException, ClassNotFoundException { ClassLoader old = Thread.currentThread().getContextClassLoader(); - ClassLoader cl = appClassloader.get() != null ? appClassloader.get() : old; + ClassLoader cl = deployment.get() != null && deployment.get().hasAppClassLoader() + ? deployment.get().getAppClassLoader() + : old; try { Thread.currentThread().setContextClassLoader(cl); diff --git a/test-framework/arquillian/src/main/java/io/quarkus/arquillian/QuarkusDeployableContainer.java b/test-framework/arquillian/src/main/java/io/quarkus/arquillian/QuarkusDeployableContainer.java index 2c5f3ebbb5aeb..4f41a5dd06c35 100644 --- a/test-framework/arquillian/src/main/java/io/quarkus/arquillian/QuarkusDeployableContainer.java +++ b/test-framework/arquillian/src/main/java/io/quarkus/arquillian/QuarkusDeployableContainer.java @@ -59,16 +59,12 @@ public class QuarkusDeployableContainer implements DeployableContainer runningApp; + private InstanceProducer deployment; @Inject @DeploymentScoped private InstanceProducer deploymentLocation; - @Inject - @DeploymentScoped - private InstanceProducer appClassloader; - @Inject private Instance testClass; @@ -191,8 +187,7 @@ public void execute(BuildContext context) { AugmentAction augmentAction = new AugmentActionImpl(curatedApplication, customizers); StartupAction app = augmentAction.createInitialRuntimeApplication(); RunningQuarkusApplication runningQuarkusApplication = app.run(); - appClassloader.set(runningQuarkusApplication.getClassLoader()); - runningApp.set(runningQuarkusApplication); + deployment.set(new QuarkusDeployment(runningQuarkusApplication)); Thread.currentThread().setContextClassLoader(runningQuarkusApplication.getClassLoader()); // Instantiate the real test instance testInstance = TestInstantiator.instantiateTest(testJavaClass, runningQuarkusApplication.getClassLoader()); @@ -241,7 +236,7 @@ public void execute(BuildContext context) { ProtocolMetaData metadata = new ProtocolMetaData(); //TODO: fix this - String testUri = TestHTTPResourceManager.getUri(runningApp.get()); + String testUri = TestHTTPResourceManager.getUri(deployment.get().getRunningApp()); System.setProperty("test.url", testUri); URI uri = URI.create(testUri); @@ -255,42 +250,9 @@ public void execute(BuildContext context) { @Override public void undeploy(Archive archive) throws DeploymentException { try { - RunningQuarkusApplication runner = runningApp.get(); - if (runner != null) { - Thread.currentThread().setContextClassLoader(runningApp.get().getClassLoader()); - } - testInstance = null; - Path location = deploymentLocation.get(); - if (location != null) { - try { - Files.walkFileTree(location, new FileVisitor() { - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - Files.delete(file); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { - Files.delete(dir); - return FileVisitResult.CONTINUE; - } - }); - } catch (IOException e) { - LOGGER.warn("Unable to delete the deployment dir: " + location, e); - } - } + RunningQuarkusApplication runner = deployment.get().getRunningApp(); if (runner != null) { + Thread.currentThread().setContextClassLoader(runner.getClassLoader()); try { runner.close(); } catch (Exception e) { @@ -300,6 +262,38 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOEx } finally { Thread.currentThread().setContextClassLoader(old); } + deployment.get().cleanup(); + + Path location = deploymentLocation.get(); + if (location != null) { + try { + Files.walkFileTree(location, new FileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + LOGGER.warn("Unable to delete the deployment dir: " + location, e); + } + } } @Override diff --git a/test-framework/arquillian/src/main/java/io/quarkus/arquillian/QuarkusDeployment.java b/test-framework/arquillian/src/main/java/io/quarkus/arquillian/QuarkusDeployment.java new file mode 100644 index 0000000000000..7b267da97d774 --- /dev/null +++ b/test-framework/arquillian/src/main/java/io/quarkus/arquillian/QuarkusDeployment.java @@ -0,0 +1,39 @@ +package io.quarkus.arquillian; + +import io.quarkus.bootstrap.app.RunningQuarkusApplication; + +/** + * This class is a workaround for Arquillian holding onto deployment-scoped (and container-scoped) + * objects until its end of life. That allows undeploying and redeploying without losing contextual + * data, but is also technically a memory leak. If the deployment-scoped objects are large (such as + * the {@code RunningQuarkusApplication}), this memory leak becomes very visible very soon. This + * Quarkus Arquillian adapter is only used to run TCKs for Jakarta and MicroProfile specifications, + * which don't need this capability. Also, the {@code RunningQuarkusApplication} is unusable after + * it is closed, so it makes no sense to keep it around. Hence, {@link QuarkusDeployableContainer} + * may safely {@link #cleanup()} this object after a deployment is undeployed, making the Quarkus + * application unreachable and GC-able. Arquillian will keep piling up empty objects, which is still + * a memory leak, but one that doesn't hurt as much. + */ +class QuarkusDeployment { + private RunningQuarkusApplication runningApp; + + QuarkusDeployment(RunningQuarkusApplication runningApp) { + this.runningApp = runningApp; + } + + RunningQuarkusApplication getRunningApp() { + return runningApp; + } + + ClassLoader getAppClassLoader() { + return runningApp.getClassLoader(); + } + + boolean hasAppClassLoader() { + return runningApp != null && runningApp.getClassLoader() != null; + } + + void cleanup() { + runningApp = null; + } +} diff --git a/test-framework/arquillian/src/main/java/io/quarkus/arquillian/QuarkusProtocol.java b/test-framework/arquillian/src/main/java/io/quarkus/arquillian/QuarkusProtocol.java index afd7b8381156c..17d238b8cc89b 100644 --- a/test-framework/arquillian/src/main/java/io/quarkus/arquillian/QuarkusProtocol.java +++ b/test-framework/arquillian/src/main/java/io/quarkus/arquillian/QuarkusProtocol.java @@ -59,7 +59,7 @@ static class QuarkusMethodExecutor implements ContainerMethodExecutor { Instance testResult; @Inject - Instance classLoaderInstance; + Instance deployment; @Override public TestResult invoke(TestMethodExecutor testMethodExecutor) { @@ -70,7 +70,7 @@ public TestResult invoke(TestMethodExecutor testMethodExecutor) { public void invoke(Object... parameters) throws Throwable { ClassLoader loader = Thread.currentThread().getContextClassLoader(); try { - Thread.currentThread().setContextClassLoader(classLoaderInstance.get()); + Thread.currentThread().setContextClassLoader(deployment.get().getAppClassLoader()); Object actualTestInstance = QuarkusDeployableContainer.testInstance; Method actualMethod = null; diff --git a/test-framework/arquillian/src/main/java/io/quarkus/arquillian/RequestContextLifecycle.java b/test-framework/arquillian/src/main/java/io/quarkus/arquillian/RequestContextLifecycle.java index a3ebcea8ff28d..49f99b2604e14 100644 --- a/test-framework/arquillian/src/main/java/io/quarkus/arquillian/RequestContextLifecycle.java +++ b/test-framework/arquillian/src/main/java/io/quarkus/arquillian/RequestContextLifecycle.java @@ -1,7 +1,7 @@ package io.quarkus.arquillian; import org.jboss.arquillian.container.spi.context.annotation.DeploymentScoped; -import org.jboss.arquillian.core.api.InstanceProducer; +import org.jboss.arquillian.core.api.Instance; import org.jboss.arquillian.core.api.annotation.Inject; import org.jboss.arquillian.core.api.annotation.Observes; import org.jboss.arquillian.test.spi.event.suite.After; @@ -21,12 +21,13 @@ public class RequestContextLifecycle { @Inject @DeploymentScoped - private InstanceProducer appClassloader; + private Instance deployment; public void on(@Observes(precedence = DEFAULT_PRECEDENCE) Before event) throws Throwable { //we are outside the runtime class loader, so we don't have direct access to the container - ClassLoader classLoader = appClassloader.get(); - if (classLoader != null) { + QuarkusDeployment deployment = this.deployment.get(); + if (deployment != null && deployment.hasAppClassLoader()) { + ClassLoader classLoader = deployment.getAppClassLoader(); Class arcClz = classLoader.loadClass(Arc.class.getName()); Object container = arcClz.getMethod("container").invoke(null); if (container != null) { @@ -42,8 +43,9 @@ public void on(@Observes(precedence = DEFAULT_PRECEDENCE) Before event) throws T public void on(@Observes(precedence = DEFAULT_PRECEDENCE) After event) throws Throwable { //we are outside the runtime class loader, so we don't have direct access to the container - ClassLoader classLoader = appClassloader.get(); - if (classLoader != null) { + QuarkusDeployment deployment = this.deployment.get(); + if (deployment != null && deployment.hasAppClassLoader()) { + ClassLoader classLoader = deployment.getAppClassLoader(); Class arcClz = classLoader.loadClass(Arc.class.getName()); Object container = arcClz.getMethod("container").invoke(null); if (container != null) { diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/TestInstantiator.java b/test-framework/common/src/main/java/io/quarkus/test/common/TestInstantiator.java index 59215a6250827..9351c1fbd86b7 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/TestInstantiator.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/TestInstantiator.java @@ -1,9 +1,11 @@ package io.quarkus.test.common; -import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Method; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.spi.CDI; + import org.jboss.logging.Logger; public class TestInstantiator { @@ -15,21 +17,10 @@ public static Object instantiateTest(Class testClass, ClassLoader classLoader try { Class actualTestClass = Class.forName(testClass.getName(), true, Thread.currentThread().getContextClassLoader()); - Class cdi = Thread.currentThread().getContextClassLoader().loadClass("jakarta.enterprise.inject.spi.CDI"); - Object instance = cdi.getMethod("current").invoke(null); - Method selectMethod = cdi.getMethod("select", Class.class, Annotation[].class); - Object cdiInstance = selectMethod.invoke(instance, actualTestClass, new Annotation[0]); - return selectMethod.getReturnType().getMethod("get").invoke(cdiInstance); - // BeanManager bm = CDI.current().getBeanManager(); - // Set> beans = bm.getBeans(testClass); - // Set> nonSubClasses = new HashSet<>(); - // for (Bean i : beans) { - // if (i.getBeanClass() == testClass) { - // nonSubClasses.add(i); - // } - // } - // Bean bean = bm.resolve(nonSubClasses); - // return bm.getReference(bean, testClass, bm.createCreationalContext(bean)); + Class delegate = Thread.currentThread().getContextClassLoader() + .loadClass("io.quarkus.test.common.TestInstantiator$Delegate"); + Method instantiate = delegate.getMethod("instantiate", Class.class); + return instantiate.invoke(null, actualTestClass); } catch (Exception e) { log.warn("Failed to initialize test as a CDI bean, falling back to direct initialization", e); try { @@ -41,4 +32,27 @@ public static Object instantiateTest(Class testClass, ClassLoader classLoader } } } + + // this class shall be loaded by the Quarkus CL + public static class Delegate { + public static Object instantiate(Class clazz) { + CDI cdi = CDI.current(); + Instance instance = cdi.select(clazz); + if (instance.isResolvable()) { + return instance.get(); + } + + if (clazz.getTypeParameters().length > 0) { + // fallback for generic test classes, whose set of bean types + // does not contain a `Class` but a `ParameterizedType` instead + for (Instance.Handle handle : cdi.select(Object.class).handles()) { + if (clazz.equals(handle.getBean().getBeanClass())) { + return handle.get(); + } + } + } + + throw new IllegalStateException("No bean: " + clazz); + } + } }