From bafa48eb40eadb2b6dee8917b4fc135577b0ddce Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Tue, 7 Mar 2023 12:27:41 +0100 Subject: [PATCH 1/5] ArC: improve equality of generated annotation literals Apparently, a lot of people use the construct `new AnnotationLiteral() {}` to create an annotation literal for a memberless annotation. This is wrong, because the result doesn't implement the annotation interface, as specified by the contract of the `Annotation.equals()` method: > Returns true if the specified object represents an annotation that is logically > equivalent to this one. In other words, returns true if the specified object > is an instance of the same annotation interface as this instance, all of whose > members are equal to the corresponding member of this annotation, as defined below: > [...] Yet, this occurs both in ArC tests and in the CDI TCK. This commit therefore improves the generated `equals()` method of ArC annotation literals for this particular case. If the annotation type has no members, the generated `equals()` method no longer verifies that the other object's class implements the annotation interface. Instead, it verifies that the other object's class implements `java.lang.annotation.Annotation` and that the `annotationType()` classes are equal. This is also what CDI's `AnnotationLiteral` does. --- .../processor/AnnotationLiteralGenerator.java | 25 +++++++-- .../AnnotationLiteralProcessorTest.java | 56 ++++++++++++++++++- 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/AnnotationLiteralGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/AnnotationLiteralGenerator.java index 4e4f7ef2e8c31..e34b0139c192f 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/AnnotationLiteralGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/AnnotationLiteralGenerator.java @@ -5,6 +5,7 @@ import static org.objectweb.asm.Opcodes.ACC_PUBLIC; import static org.objectweb.asm.Opcodes.ACC_STATIC; +import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -315,14 +316,28 @@ private static void generateEquals(ClassCreator clazz, AnnotationLiteralClassInf equals.ifReferencesNotEqual(equals.getThis(), equals.getMethodParam(0)) .falseBranch().returnBoolean(true); - equals.ifTrue(equals.instanceOf(equals.getMethodParam(0), literal.annotationClass.name().toString())) - .falseBranch().returnBoolean(false); - if (literal.annotationMembers().isEmpty()) { - // short-circuit for memberless annotations - equals.returnBoolean(true); + // special case for memberless annotations + // + // a lot of people apparently use the construct `new AnnotationLiteral() {}` + // to create an annotation literal for a memberless annotation, which is wrong, because + // the result doesn't implement the annotation interface + // + // yet, we handle that case here by doing what `AnnotationLiteral` does: instead of + // checking that the other object is an instance of the same annotation interface, + // as specified by the `Annotation.equals()` contract, we check that it implements + // the `Annotation` interface and have the same `annotationType()` + equals.ifTrue(equals.instanceOf(equals.getMethodParam(0), Annotation.class)) + .falseBranch().returnBoolean(false); + ResultHandle thisAnnType = equals.loadClass(literal.annotationClass); + ResultHandle otherAnnType = equals.invokeInterfaceMethod(MethodDescriptor.ofMethod(Annotation.class, + "annotationType", Class.class), equals.getMethodParam(0)); + equals.returnValue(Gizmo.equals(equals, thisAnnType, otherAnnType)); } + equals.ifTrue(equals.instanceOf(equals.getMethodParam(0), literal.annotationClass.name().toString())) + .falseBranch().returnBoolean(false); + ResultHandle other = equals.checkCast(equals.getMethodParam(0), literal.annotationClass.name().toString()); for (MethodInfo annotationMember : literal.annotationMembers()) { diff --git a/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/AnnotationLiteralProcessorTest.java b/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/AnnotationLiteralProcessorTest.java index 8f1685614a581..f19dab68360a1 100644 --- a/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/AnnotationLiteralProcessorTest.java +++ b/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/AnnotationLiteralProcessorTest.java @@ -36,6 +36,12 @@ public enum SimpleEnum { public @interface ClassRetainedAnnotation { } + @Retention(RetentionPolicy.RUNTIME) + public @interface MemberlessAnnotation { + class Literal extends AnnotationLiteral implements MemberlessAnnotation { + } + } + @Retention(RetentionPolicy.RUNTIME) public @interface SimpleAnnotation { String value(); @@ -287,7 +293,7 @@ public SimpleAnnotation[] nestedArray() { private final IndexView index; public AnnotationLiteralProcessorTest() throws IOException { - index = Index.of(SimpleEnum.class, SimpleAnnotation.class, ComplexAnnotation.class); + index = Index.of(SimpleEnum.class, MemberlessAnnotation.class, SimpleAnnotation.class, ComplexAnnotation.class); } @Test @@ -332,6 +338,54 @@ public void test() throws ReflectiveOperationException { annotation.toString()); } + @Test + public void memberless() throws ReflectiveOperationException { + AnnotationLiteralProcessor literals = new AnnotationLiteralProcessor(index, ignored -> true); + + TestClassLoader cl = new TestClassLoader(getClass().getClassLoader()); + try (ClassCreator creator = ClassCreator.builder().classOutput(cl).className(generatedClass).build()) { + MethodCreator method = creator.getMethodCreator("get", MemberlessAnnotation.class) + .setModifiers(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC); + ResultHandle annotation = literals.create(method, index.getClassByName(MemberlessAnnotation.class), + AnnotationInstance.builder(MemberlessAnnotation.class).build()); + method.returnValue(annotation); + } + + Collection resources = new AnnotationLiteralGenerator(false) + .generate(literals.getCache(), Collections.emptySet()); + for (ResourceOutput.Resource resource : resources) { + if (resource.getType() == ResourceOutput.Resource.Type.JAVA_CLASS) { + cl.write(resource.getName(), resource.getData()); + } else { + throw new IllegalStateException("Unexpected " + resource.getType() + " " + resource.getName()); + } + } + + Class clazz = cl.loadClass(generatedClass); + MemberlessAnnotation annotation = (MemberlessAnnotation) clazz.getMethod("get").invoke(null); + + assertTrue(annotation instanceof AbstractAnnotationLiteral); + AbstractAnnotationLiteral annotationLiteral = (AbstractAnnotationLiteral) annotation; + assertEquals(annotation.annotationType(), annotationLiteral.annotationType()); + + AnnotationLiteral correctLiteral = new MemberlessAnnotation.Literal(); + AnnotationLiteral incorrectLiteral = new AnnotationLiteral<>() { + }; + + // verify both ways, to ensure our generated classes interop correctly with `AnnotationLiteral` + assertEquals(correctLiteral, annotation); + assertEquals(annotation, correctLiteral); + + assertEquals(incorrectLiteral, annotation); + assertEquals(annotation, incorrectLiteral); + + assertEquals(correctLiteral.hashCode(), annotation.hashCode()); + assertEquals(incorrectLiteral.hashCode(), annotation.hashCode()); + + assertEquals("@io.quarkus.arc.processor.AnnotationLiteralProcessorTest$MemberlessAnnotation()", + annotation.toString()); + } + @Test public void missingAnnotationClass() { AnnotationLiteralProcessor literals = new AnnotationLiteralProcessor(index, ignored -> true); From 2b0e9b434fdd0abf469f6b64ca51c04afbb1e4fa Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Tue, 7 Mar 2023 12:27:52 +0100 Subject: [PATCH 2/5] ArC: add Arquillian adapter --- independent-projects/arc/arquillian/pom.xml | 48 ++++ .../arquillian/ArcContainerConfiguration.java | 10 + .../arquillian/ArcDeployableContainer.java | 171 +++++++++++ .../quarkus/arc/arquillian/ArcExtension.java | 20 ++ .../quarkus/arc/arquillian/ArcProtocol.java | 259 +++++++++++++++++ .../arquillian/ArcProtocolConfiguration.java | 6 + .../quarkus/arc/arquillian/BeanArchive.java | 88 ++++++ .../io/quarkus/arc/arquillian/Deployer.java | 272 ++++++++++++++++++ .../arc/arquillian/DeploymentClassLoader.java | 74 +++++ .../quarkus/arc/arquillian/DeploymentDir.java | 25 ++ .../io/quarkus/arc/arquillian/ExtraBean.java | 11 + .../arc/arquillian/utils/Archives.java | 37 +++ .../arc/arquillian/utils/ClassLoading.java | 40 +++ .../arc/arquillian/utils/Directories.java | 41 +++ .../quarkus/arc/arquillian/utils/Hacks.java | 12 + ...boss.arquillian.core.spi.LoadableExtension | 1 + independent-projects/arc/pom.xml | 4 + 17 files changed, 1119 insertions(+) create mode 100644 independent-projects/arc/arquillian/pom.xml create mode 100644 independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/ArcContainerConfiguration.java create mode 100644 independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/ArcDeployableContainer.java create mode 100644 independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/ArcExtension.java create mode 100644 independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/ArcProtocol.java create mode 100644 independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/ArcProtocolConfiguration.java create mode 100644 independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/BeanArchive.java create mode 100644 independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/Deployer.java create mode 100644 independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/DeploymentClassLoader.java create mode 100644 independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/DeploymentDir.java create mode 100644 independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/ExtraBean.java create mode 100644 independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/utils/Archives.java create mode 100644 independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/utils/ClassLoading.java create mode 100644 independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/utils/Directories.java create mode 100644 independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/utils/Hacks.java create mode 100644 independent-projects/arc/arquillian/src/main/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension diff --git a/independent-projects/arc/arquillian/pom.xml b/independent-projects/arc/arquillian/pom.xml new file mode 100644 index 0000000000000..38043e4e86ad0 --- /dev/null +++ b/independent-projects/arc/arquillian/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + + io.quarkus.arc + arc-parent + 999-SNAPSHOT + + + arc-arquillian + ArC - Arquillian Container + + + + + org.jboss.arquillian + arquillian-bom + ${version.arquillian} + pom + import + + + + + + + io.quarkus.arc + arc-processor + + + + org.jboss.arquillian.container + arquillian-container-spi + + + org.jboss.arquillian.container + arquillian-container-test-spi + + + org.jboss.arquillian.container + arquillian-container-test-impl-base + + + + diff --git a/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/ArcContainerConfiguration.java b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/ArcContainerConfiguration.java new file mode 100644 index 0000000000000..28c3e245147cf --- /dev/null +++ b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/ArcContainerConfiguration.java @@ -0,0 +1,10 @@ +package io.quarkus.arc.arquillian; + +import org.jboss.arquillian.container.spi.ConfigurationException; +import org.jboss.arquillian.container.spi.client.container.ContainerConfiguration; + +public class ArcContainerConfiguration implements ContainerConfiguration { + @Override + public void validate() throws ConfigurationException { + } +} diff --git a/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/ArcDeployableContainer.java b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/ArcDeployableContainer.java new file mode 100644 index 0000000000000..fef060e773718 --- /dev/null +++ b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/ArcDeployableContainer.java @@ -0,0 +1,171 @@ +package io.quarkus.arc.arquillian; + +import java.io.File; +import java.io.IOException; + +import jakarta.enterprise.event.Shutdown; +import jakarta.enterprise.event.Startup; + +import org.jboss.arquillian.container.spi.client.container.DeployableContainer; +import org.jboss.arquillian.container.spi.client.container.DeploymentException; +import org.jboss.arquillian.container.spi.client.protocol.ProtocolDescription; +import org.jboss.arquillian.container.spi.client.protocol.metadata.ProtocolMetaData; +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.TestClass; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.exporter.ZipExporter; +import org.jboss.shrinkwrap.descriptor.api.Descriptor; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.InjectableInstance; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.arc.arquillian.utils.ClassLoading; +import io.quarkus.arc.arquillian.utils.Directories; + +public class ArcDeployableContainer implements DeployableContainer { + @Inject + @DeploymentScoped + private InstanceProducer deploymentDir; + + @Inject + @DeploymentScoped + private InstanceProducer deploymentClassLoader; + + @Inject + @DeploymentScoped + private InstanceProducer runningArc; + + @Inject + private Instance testClass; + + static Object testInstance; + + @Override + public Class getConfigurationClass() { + return ArcContainerConfiguration.class; + } + + @Override + public void setup(ArcContainerConfiguration configuration) { + } + + @Override + public ProtocolDescription getDefaultProtocol() { + return new ProtocolDescription("ArC"); + } + + @Override + public ProtocolMetaData deploy(Archive archive) throws DeploymentException { + if (System.getProperty("saveArchive") != null) { + File file = new File(archive.getName()); + archive.as(ZipExporter.class).exportTo(file); + System.out.println("Archive for test " + testClass.get().getName() + " saved in: " + file.getAbsolutePath()); + } + + if (testClass.get() == null) { + throw new IllegalStateException("Test class not available"); + } + String testClassName = testClass.get().getName(); + + ClassLoader old = Thread.currentThread().getContextClassLoader(); + try { + DeploymentDir deploymentDir = new DeploymentDir(); + this.deploymentDir.set(deploymentDir); + + DeploymentClassLoader deploymentClassLoader = new Deployer(archive, deploymentDir, testClassName).deploy(); + this.deploymentClassLoader.set(deploymentClassLoader); + + Thread.currentThread().setContextClassLoader(deploymentClassLoader); + + ArcContainer arcContainer = Arc.initialize(); + runningArc.set(arcContainer); + arcContainer.beanManager().getEvent().fire(new Startup()); + + Class actualTestClass = Class.forName(testClassName, true, deploymentClassLoader); + testInstance = findTest(arcContainer, actualTestClass); + } catch (Throwable t) { + // clone the exception into the correct class loader + Throwable nt = ClassLoading.cloneExceptionIntoSystemCL(t); + throw new DeploymentException("Unable to start ArC", nt); + } finally { + Thread.currentThread().setContextClassLoader(old); + } + + return new ProtocolMetaData(); + } + + private Object findTest(ArcContainer arc, Class testClass) { + InjectableInstance instance = arc.select(testClass); + if (instance.isResolvable()) { + 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(); + } + } + + throw new IllegalStateException("No bean: " + testClass); + } + + @Override + public void undeploy(Archive archive) throws DeploymentException { + ClassLoader old = Thread.currentThread().getContextClassLoader(); + try { + DeploymentClassLoader deploymentClassLoader = this.deploymentClassLoader.get(); + + ArcContainer arcContainer = runningArc.get(); + if (arcContainer != null) { + Thread.currentThread().setContextClassLoader(deploymentClassLoader); + arcContainer.beanManager().getEvent().fire(new Shutdown()); + Arc.shutdown(); + } + testInstance = null; + + try { + deploymentClassLoader.close(); + } catch (IOException e) { + throw new DeploymentException("Failed to close deployment classloader", e); + } + + DeploymentDir deploymentDir = this.deploymentDir.get(); + if (deploymentDir != null) { + if (System.getProperty("retainDeployment") == null) { + Directories.deleteDirectory(deploymentDir.root); + } else { + System.out.println("Deployment for test " + testClass.get().getName() + + " retained in: " + deploymentDir.root); + } + } + } finally { + Thread.currentThread().setContextClassLoader(old); + } + } + + @Override + public void deploy(Descriptor descriptor) { + throw new UnsupportedOperationException(); + + } + + @Override + public void undeploy(Descriptor descriptor) { + throw new UnsupportedOperationException(); + + } + + @Override + public void start() { + } + + @Override + public void stop() { + } +} diff --git a/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/ArcExtension.java b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/ArcExtension.java new file mode 100644 index 0000000000000..993a57b7894d2 --- /dev/null +++ b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/ArcExtension.java @@ -0,0 +1,20 @@ +package io.quarkus.arc.arquillian; + +import org.jboss.arquillian.container.spi.client.container.DeployableContainer; +import org.jboss.arquillian.container.test.spi.client.protocol.Protocol; +import org.jboss.arquillian.core.spi.LoadableExtension; + +import io.quarkus.arc.arquillian.utils.Hacks; + +public class ArcExtension implements LoadableExtension { + // this is called early enough + static { + Hacks.preventFileHandleLeaks(); + } + + @Override + public void register(ExtensionBuilder builder) { + builder.service(DeployableContainer.class, ArcDeployableContainer.class); + builder.service(Protocol.class, ArcProtocol.class); + } +} diff --git a/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/ArcProtocol.java b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/ArcProtocol.java new file mode 100644 index 0000000000000..aca146f784141 --- /dev/null +++ b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/ArcProtocol.java @@ -0,0 +1,259 @@ +package io.quarkus.arc.arquillian; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.spi.Annotated; +import jakarta.enterprise.inject.spi.AnnotatedCallable; +import jakarta.enterprise.inject.spi.AnnotatedParameter; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.InjectionPoint; + +import org.jboss.arquillian.container.spi.client.protocol.ProtocolDescription; +import org.jboss.arquillian.container.spi.client.protocol.metadata.ProtocolMetaData; +import org.jboss.arquillian.container.test.impl.client.protocol.local.LocalDeploymentPackager; +import org.jboss.arquillian.container.test.impl.execution.event.LocalExecutionEvent; +import org.jboss.arquillian.container.test.spi.ContainerMethodExecutor; +import org.jboss.arquillian.container.test.spi.client.deployment.DeploymentPackager; +import org.jboss.arquillian.container.test.spi.client.protocol.Protocol; +import org.jboss.arquillian.container.test.spi.command.CommandCallback; +import org.jboss.arquillian.core.api.Event; +import org.jboss.arquillian.core.api.Injector; +import org.jboss.arquillian.core.api.Instance; +import org.jboss.arquillian.core.api.annotation.Inject; +import org.jboss.arquillian.test.spi.TestMethodExecutor; +import org.jboss.arquillian.test.spi.TestResult; + +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.arquillian.utils.ClassLoading; + +public class ArcProtocol implements Protocol { + @Inject + Instance injector; + + @Override + public Class getProtocolConfigurationClass() { + return ArcProtocolConfiguration.class; + } + + @Override + public ProtocolDescription getDescription() { + return new ProtocolDescription("ArC"); + } + + @Override + public DeploymentPackager getPackager() { + return new LocalDeploymentPackager(); + } + + @Override + public ContainerMethodExecutor getExecutor(ArcProtocolConfiguration protocolConfiguration, ProtocolMetaData metaData, + CommandCallback callback) { + return injector.get().inject(new ArcMethodExecutor()); + } + + static class ArcMethodExecutor implements ContainerMethodExecutor { + @Inject + Event event; + + @Inject + Instance testResult; + + @Inject + Instance deploymentClassLoader; + + @Inject + Instance runningArc; + + @Override + public TestResult invoke(TestMethodExecutor testMethodExecutor) { + event.fire(new LocalExecutionEvent(new TestMethodExecutor() { + private final ArcContainer arc = runningArc.get(); + + @Override + public String getMethodName() { + return testMethodExecutor.getMethod().getName(); + } + + @Override + public Method getMethod() { + return testMethodExecutor.getMethod(); + } + + @Override + public Object getInstance() { + return ArcDeployableContainer.testInstance; + } + + @Override + public void invoke(Object... ignored) throws Throwable { + ClassLoader old = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(deploymentClassLoader.get()); + + arc.requestContext().activate(); + + Object actualTestInstance = ArcDeployableContainer.testInstance; + + Method actualMethod = null; + try { + actualMethod = actualTestInstance.getClass().getMethod(getMethod().getName(), + ClassLoading.convertToTCCL(getMethod().getParameterTypes())); + } catch (NoSuchMethodException e) { + actualMethod = actualTestInstance.getClass().getDeclaredMethod(getMethod().getName(), + ClassLoading.convertToTCCL(getMethod().getParameterTypes())); + actualMethod.setAccessible(true); + } + + AtomicReference> contextToDestroy = new AtomicReference<>(); + Object[] parameters = lookupParameters(actualMethod, contextToDestroy); + + try { + actualMethod.invoke(actualTestInstance, parameters); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + throw ClassLoading.cloneExceptionIntoSystemCL(cause); + } finally { + CreationalContext creationalContext = contextToDestroy.get(); + if (creationalContext != null) { + creationalContext.release(); + } + } + } finally { + arc.requestContext().terminate(); + + Thread.currentThread().setContextClassLoader(old); + } + } + + private Object[] lookupParameters(Method method, AtomicReference> context) { + Parameter[] parameters = method.getParameters(); + Object[] result = new Object[parameters.length]; + + boolean hasNonArquillianDataProvider = false; + for (Annotation annotation : method.getAnnotations()) { + if (annotation.annotationType().getName().equals("org.testng.annotations.Test")) { + try { + Method dataProviderMember = annotation.annotationType().getDeclaredMethod("dataProvider"); + String value = dataProviderMember.invoke(annotation).toString(); + hasNonArquillianDataProvider = !value.equals("") && !value.equals("ARQUILLIAN_DATA_PROVIDER"); + break; + } catch (ReflectiveOperationException ignored) { + } + } + } + if (hasNonArquillianDataProvider) { + return result; + } + + BeanManager beanManager = arc.beanManager(); + CreationalContext creationalContext = beanManager.createCreationalContext(null); + context.set(creationalContext); + for (int i = 0; i < parameters.length; i++) { + result[i] = beanManager.getInjectableReference(new FakeInjectionPoint<>(parameters[i], + beanManager), creationalContext); + } + return result; + } + })); + + return testResult.get(); + } + } + + private static class FakeInjectionPoint implements InjectionPoint { + private final Parameter parameter; + private final BeanManager beanManager; + + FakeInjectionPoint(Parameter parameter, BeanManager beanManager) { + this.parameter = parameter; + this.beanManager = beanManager; + } + + public Type getType() { + return parameter.getParameterizedType(); + } + + public Set getQualifiers() { + Set qualifiers = new HashSet<>(); + for (Annotation annotation : parameter.getAnnotations()) { + if (beanManager.isQualifier(annotation.annotationType())) { + qualifiers.add(annotation); + } + } + return qualifiers; + } + + public Bean getBean() { + Set> beans = beanManager.getBeans(getType(), getQualifiers().toArray(new Annotation[0])); + return beanManager.resolve(beans); + } + + public Member getMember() { + return parameter.getDeclaringExecutable(); + } + + public Annotated getAnnotated() { + return new FakeAnnotatedParameter(); + } + + public boolean isDelegate() { + return false; + } + + public boolean isTransient() { + return false; + } + + class FakeAnnotatedParameter implements AnnotatedParameter { + @Override + public int getPosition() { + throw new UnsupportedOperationException(); // not used anywhere + } + + @Override + public AnnotatedCallable getDeclaringCallable() { + throw new UnsupportedOperationException(); // not used anywhere + } + + @Override + public Type getBaseType() { + return FakeInjectionPoint.this.getType(); + } + + @Override + public Set getTypeClosure() { + return Set.of(getBaseType(), Object.class); + } + + @Override + public T getAnnotation(Class annotationType) { + for (Annotation annotation : parameter.getAnnotations()) { + if (annotation.annotationType() == annotationType) { + return annotationType.cast(annotation); + } + } + return null; + } + + @Override + public Set getAnnotations() { + return Set.of(parameter.getAnnotations()); + } + + @Override + public boolean isAnnotationPresent(Class annotationType) { + return getAnnotation(annotationType) != null; + } + } + } +} diff --git a/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/ArcProtocolConfiguration.java b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/ArcProtocolConfiguration.java new file mode 100644 index 0000000000000..a2ec73eed8e64 --- /dev/null +++ b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/ArcProtocolConfiguration.java @@ -0,0 +1,6 @@ +package io.quarkus.arc.arquillian; + +import org.jboss.arquillian.container.test.spi.client.protocol.ProtocolConfiguration; + +public class ArcProtocolConfiguration implements ProtocolConfiguration { +} diff --git a/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/BeanArchive.java b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/BeanArchive.java new file mode 100644 index 0000000000000..04a1e597ff7b7 --- /dev/null +++ b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/BeanArchive.java @@ -0,0 +1,88 @@ +package io.quarkus.arc.arquillian; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.ArchivePath; +import org.jboss.shrinkwrap.api.Filters; +import org.jboss.shrinkwrap.api.Node; +import org.jboss.shrinkwrap.api.asset.Asset; + +final class BeanArchive { + final Set classes; // values are "com/example/MyClass.class" + + private BeanArchive(Set classes) { + this.classes = classes; + } + + static BeanArchive detect(Archive archive) throws IOException { + Map beansXmlMap = archive.getContent(Filters.include(".*/beans.xml")); + if (beansXmlMap.isEmpty()) { + return null; + } + if (beansXmlMap.size() > 1) { + throw new IllegalStateException("Archive contains multiple beans.xml files: " + archive); + } + + Map.Entry beansXmlEntry = beansXmlMap.entrySet().iterator().next(); + if (!isBeanArchive(beansXmlEntry.getValue().getAsset())) { + return null; + } + + String classesPrefix; + + String beansXmlPath = beansXmlEntry.getKey().get(); + if (beansXmlPath.endsWith("/META-INF/beans.xml")) { + classesPrefix = beansXmlPath.replace("/META-INF/beans.xml", "/"); + } else if (beansXmlPath.endsWith("/WEB-INF/beans.xml")) { + classesPrefix = beansXmlPath.replace("/WEB-INF/beans.xml", "/WEB-INF/classes/"); + } else { + throw new IllegalStateException("Invalid beans.xml location: " + beansXmlPath); + } + + Set beanClasses = new HashSet<>(); + String classesPrefixPattern = Pattern.quote(classesPrefix); + for (ArchivePath path : archive.getContent(Filters.include("^" + classesPrefixPattern + ".*\\.class$")).keySet()) { + beanClasses.add(path.get().replaceFirst(classesPrefixPattern, "")); + } + + return new BeanArchive(beanClasses); + } + + private static boolean isBeanArchive(Asset beansXml) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (InputStream in = beansXml.openStream()) { + in.transferTo(bytes); + } + String content = bytes.toString(StandardCharsets.UTF_8); + + if (content.trim().isEmpty()) { + return true; + } + + if (content.contains("bean-discovery-mode='annotated'") + || content.contains("bean-discovery-mode=\"annotated\"")) { + return true; + } + + if (content.contains("bean-discovery-mode='none'") + || content.contains("bean-discovery-mode=\"none\"")) { + return false; + } + + if (content.contains("bean-discovery-mode='all'") + || content.contains("bean-discovery-mode=\"all\"")) { + throw new IllegalStateException("Bean discovery mode of 'all' not supported in CDI Lite"); + } + + // bean discovery mode not present, defaults to `annotated` + return true; + } +} diff --git a/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/Deployer.java b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/Deployer.java new file mode 100644 index 0000000000000..d016b322adc19 --- /dev/null +++ b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/Deployer.java @@ -0,0 +1,272 @@ +package io.quarkus.arc.arquillian; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import jakarta.enterprise.inject.Stereotype; +import jakarta.inject.Qualifier; +import jakarta.interceptor.InterceptorBinding; + +import org.jboss.arquillian.container.spi.client.container.DeploymentException; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.DotName; +import org.jboss.jandex.Index; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.Indexer; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.ArchivePath; +import org.jboss.shrinkwrap.api.Filters; +import org.jboss.shrinkwrap.api.Node; +import org.jboss.shrinkwrap.api.asset.ArchiveAsset; +import org.jboss.shrinkwrap.api.asset.Asset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.jboss.shrinkwrap.api.spec.WebArchive; + +import io.quarkus.arc.arquillian.utils.Archives; +import io.quarkus.arc.processor.AnnotationsTransformer; +import io.quarkus.arc.processor.BeanArchives; +import io.quarkus.arc.processor.BeanDefiningAnnotation; +import io.quarkus.arc.processor.BeanProcessor; +import io.quarkus.arc.processor.bcextensions.ExtensionsEntryPoint; + +final class Deployer { + private final Archive deploymentArchive; + private final DeploymentDir deploymentDir; + private final String testClass; + + private final List beanArchives = new ArrayList<>(); + + Deployer(Archive deploymentArchive, DeploymentDir deploymentDir, String testClass) { + this.deploymentArchive = deploymentArchive; + this.deploymentDir = deploymentDir; + this.testClass = testClass; + } + + DeploymentClassLoader deploy() throws DeploymentException { + try { + if (deploymentArchive instanceof JavaArchive) { + explodeJar(); + } else if (deploymentArchive instanceof WebArchive) { + explodeWar(); + } else { + throw new DeploymentException("Unknown archive type: " + deploymentArchive); + } + + generate(); + + return new DeploymentClassLoader(deploymentDir); + } catch (IOException | ExecutionException | InterruptedException e) { + throw new DeploymentException("Deployment failed", e); + } + } + + private void explodeJar() throws IOException { + Archives.explode(deploymentArchive, "/", deploymentDir.appClasses); + + BeanArchive beanArchive = BeanArchive.detect(deploymentArchive); + if (beanArchive != null) { + beanArchives.add(beanArchive); + } + } + + private void explodeWar() throws IOException { + Archives.explode(deploymentArchive, "/WEB-INF/classes/", deploymentDir.appClasses); + + BeanArchive beanArchive = BeanArchive.detect(deploymentArchive); + if (beanArchive != null) { + beanArchives.add(beanArchive); + } + + Map libs = deploymentArchive.getContent(Filters.include("^/WEB-INF/lib/.*\\.jar$")); + for (Map.Entry entry : libs.entrySet()) { + String path = entry.getKey().get(); + Asset asset = entry.getValue().getAsset(); + + String jarFile = path.replace("/WEB-INF/lib/", ""); + Path jarFilePath = deploymentDir.appLibraries.resolve(jarFile); + Archives.copy(asset, jarFilePath); + + if (asset instanceof ArchiveAsset) { + BeanArchive nestedBeanArchive = BeanArchive.detect(((ArchiveAsset) asset).getArchive()); + if (nestedBeanArchive != null) { + beanArchives.add(nestedBeanArchive); + } + } + } + } + + private void generate() throws IOException, ExecutionException, InterruptedException { + Index applicationIndex = buildApplicationIndex(); + + try (Closeable ignored = withDeploymentClassLoader()) { + ExtensionsEntryPoint buildCompatibleExtensions = new ExtensionsEntryPoint(); + Set additionalClasses = new HashSet<>(); + buildCompatibleExtensions.runDiscovery(applicationIndex, additionalClasses); + + IndexView beanArchiveIndex = buildImmutableBeanArchiveIndex(applicationIndex, additionalClasses); + + BeanProcessor beanProcessor = BeanProcessor.builder() + .setName(deploymentDir.root.getFileName().toString()) + .setImmutableBeanArchiveIndex(beanArchiveIndex) + .setComputingBeanArchiveIndex(BeanArchives.buildComputingBeanArchiveIndex( + Thread.currentThread().getContextClassLoader(), new ConcurrentHashMap<>(), + beanArchiveIndex)) + .setApplicationIndex(applicationIndex) + .setBuildCompatibleExtensions(buildCompatibleExtensions) + .setAdditionalBeanDefiningAnnotations(Set.of(new BeanDefiningAnnotation( + DotName.createSimple(ExtraBean.class.getName()), null))) + .addAnnotationTransformer(new AnnotationsTransformer() { + @Override + public boolean appliesTo(AnnotationTarget.Kind kind) { + return kind == AnnotationTarget.Kind.CLASS; + } + + @Override + public void transform(TransformationContext ctx) { + if (testClass.equals(ctx.getTarget().asClass().name().toString())) { + // make the test class a bean + ctx.transform().add(ExtraBean.class).done(); + } + if (additionalClasses.contains(ctx.getTarget().asClass().name().toString())) { + // make all the `@Discovery`-registered classes beans + ctx.transform().add(ExtraBean.class).done(); + } + } + }) + .setOutput(resource -> { + switch (resource.getType()) { + case JAVA_CLASS: + resource.writeTo(deploymentDir.generatedClasses.toFile()); + break; + case SERVICE_PROVIDER: + resource.writeTo(deploymentDir.generatedServices.toFile()); + break; + default: + throw new IllegalArgumentException("Unknown resource type " + resource.getType()); + } + }) + .build(); + beanProcessor.process(); + } + } + + private Index buildApplicationIndex() throws IOException { + Indexer indexer = new Indexer(); + try (Stream appClasses = Files.walk(deploymentDir.appClasses)) { + List classFiles = appClasses.filter(it -> it.toString().endsWith(".class")).collect(Collectors.toList()); + for (Path classFile : classFiles) { + try (InputStream in = Files.newInputStream(classFile)) { + indexer.index(in); + } + } + } + try (Stream appLibraries = Files.walk(deploymentDir.appLibraries)) { + List jarFiles = appLibraries.filter(it -> it.toString().endsWith(".jar")).collect(Collectors.toList()); + for (Path jarFile : jarFiles) { + try (JarFile jar = new JarFile(jarFile.toFile())) { + Enumeration entries = jar.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + if (entry.getName().endsWith(".class")) { + try (InputStream in = jar.getInputStream(entry)) { + indexer.index(in); + } + } + } + } + } + } + return indexer.complete(); + } + + private IndexView buildImmutableBeanArchiveIndex(Index applicationIndex, Set additionalClasses) throws IOException { + Indexer indexer = new Indexer(); + + Set seen = new HashSet<>(); + + // 1. classes in bean archives + for (BeanArchive beanArchive : beanArchives) { + for (String classFile : beanArchive.classes) { + indexFromTCCL(indexer, classFile); + seen.add(classFile); + } + } + + // 2. additional classes added through build compatible extensions + for (String additionalClass : additionalClasses) { + String classFile = additionalClass.replace('.', '/') + ".class"; + if (seen.contains(classFile)) { + continue; + } + indexFromTCCL(indexer, classFile); + seen.add(classFile); + } + + // 3. test class + { + String classFile = testClass.replace('.', '/') + ".class"; + if (!seen.contains(classFile)) { + indexFromTCCL(indexer, classFile); + seen.add(classFile); + } + } + + // 4. CDI-related annotations (qualifiers, interceptor bindings, stereotypes) + // CDI recognizes them even if they come from an archive that is not a bean archive + Set> metaAnnotations = Set.of(Qualifier.class, InterceptorBinding.class, Stereotype.class); + for (Class metaAnnotation : metaAnnotations) { + DotName metaAnnotationName = DotName.createSimple(metaAnnotation.getName()); + for (AnnotationInstance annotation : applicationIndex.getAnnotations(metaAnnotationName)) { + if (annotation.target().kind().equals(AnnotationTarget.Kind.CLASS)) { + String annotationClass = annotation.target().asClass().name().toString(); + String classFile = annotationClass.replace('.', '/') + ".class"; + if (seen.contains(classFile)) { + continue; + } + indexFromTCCL(indexer, classFile); + seen.add(classFile); + } + } + } + + return BeanArchives.buildImmutableBeanArchiveIndex(indexer.complete()); + } + + private void indexFromTCCL(Indexer indexer, String classFile) throws IOException { + ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + try (InputStream in = tccl.getResourceAsStream(classFile)) { + indexer.index(in); + } + } + + private Closeable withDeploymentClassLoader() throws IOException { + ClassLoader oldCl = Thread.currentThread().getContextClassLoader(); + DeploymentClassLoader newCl = new DeploymentClassLoader(deploymentDir); + + Thread.currentThread().setContextClassLoader(newCl); + return new Closeable() { + @Override + public void close() throws IOException { + Thread.currentThread().setContextClassLoader(oldCl); + newCl.close(); + } + }; + } +} diff --git a/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/DeploymentClassLoader.java b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/DeploymentClassLoader.java new file mode 100644 index 0000000000000..be6449f4274b4 --- /dev/null +++ b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/DeploymentClassLoader.java @@ -0,0 +1,74 @@ +package io.quarkus.arc.arquillian; + +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.quarkus.arc.ComponentsProvider; + +final class DeploymentClassLoader extends URLClassLoader { + static { + ClassLoader.registerAsParallelCapable(); + } + + private final DeploymentDir deploymentDir; + + DeploymentClassLoader(DeploymentDir deploymentDir) throws IOException { + super(findUrls(deploymentDir)); + this.deploymentDir = deploymentDir; + setDefaultAssertionStatus(true); + } + + private static URL[] findUrls(DeploymentDir deploymentDir) throws IOException { + List result = new ArrayList<>(); + + result.add(deploymentDir.appClasses.toUri().toURL()); + result.add(deploymentDir.generatedClasses.toUri().toURL()); + + try (Stream stream = Files.walk(deploymentDir.appLibraries)) { + List jars = stream.filter(p -> p.toString().endsWith(".jar")).collect(Collectors.toList()); + for (Path jar : jars) { + result.add(jar.toUri().toURL()); + } + } + + return result.toArray(new URL[0]); + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + synchronized (getClassLoadingLock(name)) { + Class clazz = findLoadedClass(name); + if (clazz != null) { + return clazz; + } + + try { + clazz = findClass(name); + if (resolve) { + resolveClass(clazz); + } + return clazz; + } catch (ClassNotFoundException ignored) { + return super.loadClass(name, resolve); + } + } + } + + @Override + public Enumeration getResources(String name) throws IOException { + if (("META-INF/services/" + ComponentsProvider.class.getName()).equals(name)) { + URL url = deploymentDir.generatedServices.resolve(ComponentsProvider.class.getName()).toUri().toURL(); + return Collections.enumeration(Collections.singleton(url)); + } + return super.getResources(name); + } +} diff --git a/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/DeploymentDir.java b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/DeploymentDir.java new file mode 100644 index 0000000000000..391b2b68a7de2 --- /dev/null +++ b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/DeploymentDir.java @@ -0,0 +1,25 @@ +package io.quarkus.arc.arquillian; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +final class DeploymentDir { + final Path root; + + final Path appClasses; + final Path appLibraries; + + final Path generatedClasses; + final Path generatedServices; + + DeploymentDir() throws IOException { + this.root = Files.createTempDirectory("ArcArquillian"); + + this.appClasses = Files.createDirectories(root.resolve("app").resolve("classes")); + this.appLibraries = Files.createDirectories(root.resolve("app").resolve("libraries")); + + this.generatedClasses = Files.createDirectories(root.resolve("generated").resolve("classes")); + this.generatedServices = Files.createDirectories(root.resolve("generated").resolve("services")); + } +} diff --git a/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/ExtraBean.java b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/ExtraBean.java new file mode 100644 index 0000000000000..72be26f40f9de --- /dev/null +++ b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/ExtraBean.java @@ -0,0 +1,11 @@ +package io.quarkus.arc.arquillian; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface ExtraBean { +} diff --git a/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/utils/Archives.java b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/utils/Archives.java new file mode 100644 index 0000000000000..0fabbcce7c59c --- /dev/null +++ b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/utils/Archives.java @@ -0,0 +1,37 @@ +package io.quarkus.arc.arquillian.utils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.regex.Pattern; + +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.ArchivePath; +import org.jboss.shrinkwrap.api.Filters; +import org.jboss.shrinkwrap.api.Node; +import org.jboss.shrinkwrap.api.asset.Asset; + +public class Archives { + public static void explode(Archive archive, String prefix, Path targetPath) throws IOException { + String prefixPattern = "^" + Pattern.quote(prefix); + Map files = archive.getContent(Filters.include(prefixPattern + ".*")); + for (Map.Entry entry : files.entrySet()) { + Asset asset = entry.getValue().getAsset(); + if (asset == null) { + continue; + } + + String path = entry.getKey().get().replaceFirst(prefixPattern, ""); + copy(asset, targetPath.resolve(path)); + } + } + + public static void copy(Asset asset, Path targetPath) throws IOException { + Files.createDirectories(targetPath.getParent()); // make sure the directory exists + try (InputStream in = asset.openStream()) { + Files.copy(in, targetPath); + } + } +} diff --git a/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/utils/ClassLoading.java b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/utils/ClassLoading.java new file mode 100644 index 0000000000000..4fabc68ca04e5 --- /dev/null +++ b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/utils/ClassLoading.java @@ -0,0 +1,40 @@ +package io.quarkus.arc.arquillian.utils; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +public class ClassLoading { + public static Class[] convertToTCCL(Class[] classes) throws ClassNotFoundException { + return convertToCL(classes, Thread.currentThread().getContextClassLoader()); + } + + public static Class[] convertToCL(Class[] classes, ClassLoader classLoader) throws ClassNotFoundException { + Class[] result = new Class[classes.length]; + for (int i = 0; i < classes.length; i++) { + if (classes[i].getClassLoader() != classLoader) { + result[i] = classLoader.loadClass(classes[i].getName()); + } else { + result[i] = classes[i]; + } + } + return result; + } + + public static Throwable cloneExceptionIntoSystemCL(Throwable exception) { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ObjectOutputStream serializer = new ObjectOutputStream(out); + serializer.writeObject(exception); + serializer.close(); + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + ObjectInputStream deserializer = new ObjectInputStream(in); + return (Throwable) deserializer.readObject(); + } catch (IOException | ClassNotFoundException e) { + // shrug + return exception; + } + } +} diff --git a/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/utils/Directories.java b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/utils/Directories.java new file mode 100644 index 0000000000000..d15eafd7998fe --- /dev/null +++ b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/utils/Directories.java @@ -0,0 +1,41 @@ +package io.quarkus.arc.arquillian.utils; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; + +public class Directories { + public static void deleteDirectory(Path dir) { + try { + Files.walkFileTree(dir, new FileVisitor<>() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { + 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) { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/utils/Hacks.java b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/utils/Hacks.java new file mode 100644 index 0000000000000..b7f456a8b2517 --- /dev/null +++ b/independent-projects/arc/arquillian/src/main/java/io/quarkus/arc/arquillian/utils/Hacks.java @@ -0,0 +1,12 @@ +package io.quarkus.arc.arquillian.utils; + +import java.net.URLConnection; + +public class Hacks { + public static void preventFileHandleLeaks() { + // `JarURLConnection` is known to leak file handles with caching enabled, + // which occurs in `URLClassLoader` and in CDI TCK's `PropertiesBasedConfigurationBuilder` + // and causes failures during directory deletion on Windows + URLConnection.setDefaultUseCaches("jar", false); + } +} diff --git a/independent-projects/arc/arquillian/src/main/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension b/independent-projects/arc/arquillian/src/main/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension new file mode 100644 index 0000000000000..c72ef1dcf9caf --- /dev/null +++ b/independent-projects/arc/arquillian/src/main/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension @@ -0,0 +1 @@ +io.quarkus.arc.arquillian.ArcExtension \ No newline at end of file diff --git a/independent-projects/arc/pom.xml b/independent-projects/arc/pom.xml index 3528926818c62..1d4553bfc95ce 100644 --- a/independent-projects/arc/pom.xml +++ b/independent-projects/arc/pom.xml @@ -54,6 +54,8 @@ 3.0.0 2.1.0 + 1.7.0.Alpha14 + 3.2.1 3.0.0-M9 1.6.8 @@ -63,6 +65,8 @@ runtime processor tests + + arquillian From 39bb9423d0ac31ddd9bac42552a7aa7193134279 Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Tue, 7 Mar 2023 12:28:03 +0100 Subject: [PATCH 3/5] ArC: add CDI TCK runner The TCK runner contains a sizeable exclude list. It will shrink over time. --- .../arc/cdi-tck-porting-pkg/pom.xml | 28 + .../io/quarkus/arc/tck/porting/BeansImpl.java | 22 + .../quarkus/arc/tck/porting/ContextsImpl.java | 64 ++ .../io/quarkus/arc/tck/porting/ELImpl.java | 24 + .../resources/META-INF/cdi-tck.properties | 3 + .../arc/cdi-tck-runner/pom.xml | 84 +++ .../resources/META-INF/cdi-tck.properties | 1 + .../src/test/resources/testng.xml | 566 ++++++++++++++++++ independent-projects/arc/pom.xml | 15 + 9 files changed, 807 insertions(+) create mode 100644 independent-projects/arc/cdi-tck-porting-pkg/pom.xml create mode 100644 independent-projects/arc/cdi-tck-porting-pkg/src/main/java/io/quarkus/arc/tck/porting/BeansImpl.java create mode 100644 independent-projects/arc/cdi-tck-porting-pkg/src/main/java/io/quarkus/arc/tck/porting/ContextsImpl.java create mode 100644 independent-projects/arc/cdi-tck-porting-pkg/src/main/java/io/quarkus/arc/tck/porting/ELImpl.java create mode 100644 independent-projects/arc/cdi-tck-porting-pkg/src/main/resources/META-INF/cdi-tck.properties create mode 100644 independent-projects/arc/cdi-tck-runner/pom.xml create mode 100644 independent-projects/arc/cdi-tck-runner/src/test/resources/META-INF/cdi-tck.properties create mode 100644 independent-projects/arc/cdi-tck-runner/src/test/resources/testng.xml diff --git a/independent-projects/arc/cdi-tck-porting-pkg/pom.xml b/independent-projects/arc/cdi-tck-porting-pkg/pom.xml new file mode 100644 index 0000000000000..41d39dfd05bf2 --- /dev/null +++ b/independent-projects/arc/cdi-tck-porting-pkg/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + + io.quarkus.arc + arc-parent + 999-SNAPSHOT + + + arc-cdi-tck-porting-pkg + ArC - CDI TCK Porting Package + + + + io.quarkus.arc + arc + + + jakarta.enterprise + cdi-tck-api + ${version.cdi-tck} + + + + diff --git a/independent-projects/arc/cdi-tck-porting-pkg/src/main/java/io/quarkus/arc/tck/porting/BeansImpl.java b/independent-projects/arc/cdi-tck-porting-pkg/src/main/java/io/quarkus/arc/tck/porting/BeansImpl.java new file mode 100644 index 0000000000000..7da9826681dff --- /dev/null +++ b/independent-projects/arc/cdi-tck-porting-pkg/src/main/java/io/quarkus/arc/tck/porting/BeansImpl.java @@ -0,0 +1,22 @@ +package io.quarkus.arc.tck.porting; + +import org.jboss.cdi.tck.spi.Beans; + +import io.quarkus.arc.ClientProxy; + +public class BeansImpl implements Beans { + @Override + public boolean isProxy(Object o) { + return o instanceof ClientProxy; + } + + @Override + public byte[] passivate(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public Object activate(byte[] bytes) { + throw new UnsupportedOperationException(); + } +} diff --git a/independent-projects/arc/cdi-tck-porting-pkg/src/main/java/io/quarkus/arc/tck/porting/ContextsImpl.java b/independent-projects/arc/cdi-tck-porting-pkg/src/main/java/io/quarkus/arc/tck/porting/ContextsImpl.java new file mode 100644 index 0000000000000..615d79dbd1f8f --- /dev/null +++ b/independent-projects/arc/cdi-tck-porting-pkg/src/main/java/io/quarkus/arc/tck/porting/ContextsImpl.java @@ -0,0 +1,64 @@ +package io.quarkus.arc.tck.porting; + +import java.lang.annotation.Annotation; + +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.context.spi.Context; +import jakarta.enterprise.context.spi.Contextual; +import jakarta.enterprise.context.spi.CreationalContext; + +import org.jboss.cdi.tck.spi.Contexts; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.InjectableContext; +import io.quarkus.arc.ManagedContext; + +public class ContextsImpl implements Contexts { + @Override + public void setActive(Context context) { + ((ManagedContext) context).activate(); + } + + @Override + public void setInactive(Context context) { + ((ManagedContext) context).deactivate(); + } + + @Override + public Context getRequestContext() { + return Arc.container().requestContext(); + } + + @Override + public Context getDependentContext() { + // ArC doesn't have a context object for the dependent context, let's fake it here for now + // TODO we'll likely have to implement this in ArC properly + + return new Context() { + @Override + public Class getScope() { + return Dependent.class; + } + + @Override + public T get(Contextual contextual, CreationalContext creationalContext) { + throw new UnsupportedOperationException(); + } + + @Override + public T get(Contextual contextual) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isActive() { + return true; + } + }; + } + + @Override + public void destroyContext(Context context) { + ((InjectableContext) context).destroy(); + } +} diff --git a/independent-projects/arc/cdi-tck-porting-pkg/src/main/java/io/quarkus/arc/tck/porting/ELImpl.java b/independent-projects/arc/cdi-tck-porting-pkg/src/main/java/io/quarkus/arc/tck/porting/ELImpl.java new file mode 100644 index 0000000000000..bdd7a6505a2b2 --- /dev/null +++ b/independent-projects/arc/cdi-tck-porting-pkg/src/main/java/io/quarkus/arc/tck/porting/ELImpl.java @@ -0,0 +1,24 @@ +package io.quarkus.arc.tck.porting; + +import jakarta.el.ELContext; +import jakarta.enterprise.inject.spi.BeanManager; + +import org.jboss.cdi.tck.spi.EL; + +public class ELImpl implements EL { + @Override + public T evaluateValueExpression(BeanManager beanManager, String expression, Class expectedType) { + throw new UnsupportedOperationException(); + } + + @Override + public T evaluateMethodExpression(BeanManager beanManager, String expression, Class expectedType, + Class[] expectedParamTypes, Object[] expectedParams) { + throw new UnsupportedOperationException(); + } + + @Override + public ELContext createELContext(BeanManager beanManager) { + throw new UnsupportedOperationException(); + } +} diff --git a/independent-projects/arc/cdi-tck-porting-pkg/src/main/resources/META-INF/cdi-tck.properties b/independent-projects/arc/cdi-tck-porting-pkg/src/main/resources/META-INF/cdi-tck.properties new file mode 100644 index 0000000000000..ace25ed7227d9 --- /dev/null +++ b/independent-projects/arc/cdi-tck-porting-pkg/src/main/resources/META-INF/cdi-tck.properties @@ -0,0 +1,3 @@ +org.jboss.cdi.tck.spi.Beans=io.quarkus.arc.tck.porting.BeansImpl +org.jboss.cdi.tck.spi.Contexts=io.quarkus.arc.tck.porting.ContextsImpl +org.jboss.cdi.tck.spi.EL=io.quarkus.arc.tck.porting.ELImpl diff --git a/independent-projects/arc/cdi-tck-runner/pom.xml b/independent-projects/arc/cdi-tck-runner/pom.xml new file mode 100644 index 0000000000000..3ffebd5061095 --- /dev/null +++ b/independent-projects/arc/cdi-tck-runner/pom.xml @@ -0,0 +1,84 @@ + + + 4.0.0 + + + io.quarkus.arc + arc-parent + 999-SNAPSHOT + + + arc-cdi-tck-runner + ArC - CDI TCK Runner + + + + + org.jboss.arquillian + arquillian-bom + ${version.arquillian} + pom + import + + + + + + + io.quarkus.arc + arc-arquillian + + + io.quarkus.arc + arc-cdi-tck-porting-pkg + + + jakarta.enterprise + cdi-tck-core-impl + ${version.cdi-tck} + + + + + + + + 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 + + + jakarta.enterprise:cdi-tck-core-impl + + + src/test/resources/testng.xml + + + ${project.build.directory}/porting-pkg + + false + + + + + + diff --git a/independent-projects/arc/cdi-tck-runner/src/test/resources/META-INF/cdi-tck.properties b/independent-projects/arc/cdi-tck-runner/src/test/resources/META-INF/cdi-tck.properties new file mode 100644 index 0000000000000..c38de38628e08 --- /dev/null +++ b/independent-projects/arc/cdi-tck-runner/src/test/resources/META-INF/cdi-tck.properties @@ -0,0 +1 @@ +org.jboss.cdi.tck.cdiLiteMode=true diff --git a/independent-projects/arc/cdi-tck-runner/src/test/resources/testng.xml b/independent-projects/arc/cdi-tck-runner/src/test/resources/testng.xml new file mode 100644 index 0000000000000..850d21280384c --- /dev/null +++ b/independent-projects/arc/cdi-tck-runner/src/test/resources/testng.xml @@ -0,0 +1,566 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/independent-projects/arc/pom.xml b/independent-projects/arc/pom.xml index 1d4553bfc95ce..7faa8f1b58922 100644 --- a/independent-projects/arc/pom.xml +++ b/independent-projects/arc/pom.xml @@ -55,6 +55,7 @@ 2.1.0 1.7.0.Alpha14 + 4.0.7 3.2.1 3.0.0-M9 @@ -67,6 +68,8 @@ tests arquillian + cdi-tck-porting-pkg + cdi-tck-runner @@ -85,6 +88,18 @@ ${project.version} + + io.quarkus.arc + arc-arquillian + ${project.version} + + + + io.quarkus.arc + arc-cdi-tck-porting-pkg + ${project.version} + + jakarta.enterprise jakarta.enterprise.cdi-api From 27d45bf984d00c8cc968586cde75042a3c0a0f41 Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Tue, 7 Mar 2023 12:28:32 +0100 Subject: [PATCH 4/5] ArC: add AtInject TCK runner --- .../arc/atinject-tck-runner/pom.xml | 59 ++++++++++++++++++ .../quarkus/arc/tck/AtInjectTckExtension.java | 53 ++++++++++++++++ .../java/io/quarkus/arc/tck/AtInjectTest.java | 61 +++++++++++++++++++ .../test/java/io/quarkus/arc/tck/Spare.java | 11 ++++ independent-projects/arc/pom.xml | 3 + 5 files changed, 187 insertions(+) create mode 100644 independent-projects/arc/atinject-tck-runner/pom.xml create mode 100644 independent-projects/arc/atinject-tck-runner/src/test/java/io/quarkus/arc/tck/AtInjectTckExtension.java create mode 100644 independent-projects/arc/atinject-tck-runner/src/test/java/io/quarkus/arc/tck/AtInjectTest.java create mode 100644 independent-projects/arc/atinject-tck-runner/src/test/java/io/quarkus/arc/tck/Spare.java diff --git a/independent-projects/arc/atinject-tck-runner/pom.xml b/independent-projects/arc/atinject-tck-runner/pom.xml new file mode 100644 index 0000000000000..523b38fd401ee --- /dev/null +++ b/independent-projects/arc/atinject-tck-runner/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + + io.quarkus.arc + arc-parent + 999-SNAPSHOT + + + arc-atinject-tck-runner + ArC - AtInject TCK Runner + + + + + org.jboss.arquillian + arquillian-bom + ${version.arquillian} + pom + import + + + + + + + io.quarkus.arc + arc-arquillian + + + jakarta.inject + jakarta.inject-tck + ${version.atinject-tck} + + + jakarta.inject + jakarta.inject-api + + + junit + junit + + + + + junit + junit + ${version.junit4} + + + org.jboss.arquillian.junit + arquillian-junit-container + + + + diff --git a/independent-projects/arc/atinject-tck-runner/src/test/java/io/quarkus/arc/tck/AtInjectTckExtension.java b/independent-projects/arc/atinject-tck-runner/src/test/java/io/quarkus/arc/tck/AtInjectTckExtension.java new file mode 100644 index 0000000000000..31c773784cbda --- /dev/null +++ b/independent-projects/arc/atinject-tck-runner/src/test/java/io/quarkus/arc/tck/AtInjectTckExtension.java @@ -0,0 +1,53 @@ +package io.quarkus.arc.tck; + +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/independent-projects/arc/atinject-tck-runner/src/test/java/io/quarkus/arc/tck/AtInjectTest.java b/independent-projects/arc/atinject-tck-runner/src/test/java/io/quarkus/arc/tck/AtInjectTest.java new file mode 100644 index 0000000000000..de7f451baa78a --- /dev/null +++ b/independent-projects/arc/atinject-tck-runner/src/test/java/io/quarkus/arc/tck/AtInjectTest.java @@ -0,0 +1,61 @@ +package io.quarkus.arc.tck; + +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/independent-projects/arc/atinject-tck-runner/src/test/java/io/quarkus/arc/tck/Spare.java b/independent-projects/arc/atinject-tck-runner/src/test/java/io/quarkus/arc/tck/Spare.java new file mode 100644 index 0000000000000..3e312b195e9d7 --- /dev/null +++ b/independent-projects/arc/atinject-tck-runner/src/test/java/io/quarkus/arc/tck/Spare.java @@ -0,0 +1,11 @@ +package io.quarkus.arc.tck; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import jakarta.inject.Qualifier; + +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface Spare { +} diff --git a/independent-projects/arc/pom.xml b/independent-projects/arc/pom.xml index 7faa8f1b58922..78e5cc037b318 100644 --- a/independent-projects/arc/pom.xml +++ b/independent-projects/arc/pom.xml @@ -55,7 +55,9 @@ 2.1.0 1.7.0.Alpha14 + 2.0.1 4.0.7 + 4.13.2 3.2.1 3.0.0-M9 @@ -68,6 +70,7 @@ tests arquillian + atinject-tck-runner cdi-tck-porting-pkg cdi-tck-runner From 603ecfa8ba5c0f7b70823f1fba43012d89fdc093 Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Tue, 7 Mar 2023 12:28:50 +0100 Subject: [PATCH 5/5] ArC: add Lang Model TCK runner --- .../arc/lang-model-tck-runner/pom.xml | 49 +++++++++++++++++++ .../arc/tck/LangModelTckExtension.java | 23 +++++++++ .../io/quarkus/arc/tck/LangModelTest.java | 31 ++++++++++++ independent-projects/arc/pom.xml | 1 + 4 files changed, 104 insertions(+) create mode 100644 independent-projects/arc/lang-model-tck-runner/pom.xml create mode 100644 independent-projects/arc/lang-model-tck-runner/src/test/java/io/quarkus/arc/tck/LangModelTckExtension.java create mode 100644 independent-projects/arc/lang-model-tck-runner/src/test/java/io/quarkus/arc/tck/LangModelTest.java diff --git a/independent-projects/arc/lang-model-tck-runner/pom.xml b/independent-projects/arc/lang-model-tck-runner/pom.xml new file mode 100644 index 0000000000000..12e4ffcf82156 --- /dev/null +++ b/independent-projects/arc/lang-model-tck-runner/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + + io.quarkus.arc + arc-parent + 999-SNAPSHOT + + + arc-lang-model-tck-runner + ArC - Lang Model TCK Runner + + + + + org.jboss.arquillian + arquillian-bom + ${version.arquillian} + pom + import + + + + + + + io.quarkus.arc + arc-arquillian + + + jakarta.enterprise + cdi-tck-lang-model + ${version.cdi-tck} + + + junit + junit + ${version.junit4} + + + org.jboss.arquillian.junit + arquillian-junit-container + + + + diff --git a/independent-projects/arc/lang-model-tck-runner/src/test/java/io/quarkus/arc/tck/LangModelTckExtension.java b/independent-projects/arc/lang-model-tck-runner/src/test/java/io/quarkus/arc/tck/LangModelTckExtension.java new file mode 100644 index 0000000000000..65cb300e57df5 --- /dev/null +++ b/independent-projects/arc/lang-model-tck-runner/src/test/java/io/quarkus/arc/tck/LangModelTckExtension.java @@ -0,0 +1,23 @@ +package io.quarkus.arc.tck; + +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/independent-projects/arc/lang-model-tck-runner/src/test/java/io/quarkus/arc/tck/LangModelTest.java b/independent-projects/arc/lang-model-tck-runner/src/test/java/io/quarkus/arc/tck/LangModelTest.java new file mode 100644 index 0000000000000..8396e8c7ae335 --- /dev/null +++ b/independent-projects/arc/lang-model-tck-runner/src/test/java/io/quarkus/arc/tck/LangModelTest.java @@ -0,0 +1,31 @@ +package io.quarkus.arc.tck; + +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/independent-projects/arc/pom.xml b/independent-projects/arc/pom.xml index 78e5cc037b318..def6ade66fc5b 100644 --- a/independent-projects/arc/pom.xml +++ b/independent-projects/arc/pom.xml @@ -73,6 +73,7 @@ atinject-tck-runner cdi-tck-porting-pkg cdi-tck-runner + lang-model-tck-runner