diff --git a/protocol-jmx/src/main/java/org/jboss/as/arquillian/protocol/jmx/JMXProtocolPackager.java b/protocol-jmx/src/main/java/org/jboss/as/arquillian/protocol/jmx/JMXProtocolPackager.java index 592177a7..f7da2c1d 100644 --- a/protocol-jmx/src/main/java/org/jboss/as/arquillian/protocol/jmx/JMXProtocolPackager.java +++ b/protocol-jmx/src/main/java/org/jboss/as/arquillian/protocol/jmx/JMXProtocolPackager.java @@ -122,6 +122,7 @@ public Archive generateDeployment(TestDeployment testDeployment, } } addModulesManifestDependencies(appArchive); + TestDescription.addTestDescription(testDeployment); archiveHolder.addPreparedDeployment(testDeployment.getDeploymentName()); return appArchive; } @@ -135,7 +136,7 @@ private JavaArchive generateArquillianServiceArchive(Collection> auxA archive.addPackage(AbstractJMXProtocol.class.getPackage()); // add the classes required for server setup archive.addClasses(ServerSetup.class, ServerSetupTask.class, ManagementClient.class, Authentication.class, - NetworkUtils.class); + NetworkUtils.class, TestDescription.class); final Set archiveDependencies = new LinkedHashSet(); archiveDependencies.add(ModuleIdentifier.create("org.jboss.as.jmx")); diff --git a/protocol-jmx/src/main/java/org/jboss/as/arquillian/protocol/jmx/TestDescription.java b/protocol-jmx/src/main/java/org/jboss/as/arquillian/protocol/jmx/TestDescription.java new file mode 100644 index 00000000..d9dddbe7 --- /dev/null +++ b/protocol-jmx/src/main/java/org/jboss/as/arquillian/protocol/jmx/TestDescription.java @@ -0,0 +1,197 @@ +/* + * JBoss, Home of Professional Open Source. + * + * Copyright 2024 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jboss.as.arquillian.protocol.jmx; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Properties; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.spi.TestDeployment; +import org.jboss.as.server.deployment.Attachments; +import org.jboss.as.server.deployment.DeploymentUnit; +import org.jboss.as.server.deployment.module.ResourceRoot; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.ArchivePath; +import org.jboss.shrinkwrap.api.Filter; +import org.jboss.shrinkwrap.api.Node; +import org.jboss.shrinkwrap.api.asset.ArchiveAsset; +import org.jboss.shrinkwrap.api.asset.ByteArrayAsset; +import org.jboss.shrinkwrap.api.spec.EnterpriseArchive; +import org.jboss.vfs.VirtualFile; + +/** + * A simple definition describing a test deployment. + * + * @author James R. Perkins + */ +public class TestDescription { + + private static final String PATH = "/META-INF/test-description.properties"; + private static final String TARGET_CONTAINER = "org.jboss.as.arquillian.protocol.jmx.target.container"; + private static final String ARQ_DEPLOYMENT_NAME = "org.jboss.as.arquillian.protocol.jmx.arq.deployment.name"; + private static final String DEPLOYMENT_NAME = "org.jboss.as.arquillian.protocol.jmx.deployment.name"; + private static final Filter ROOT_FILTER = (p) -> p.getParent() == null || p.getParent().get().equals("/"); + + private final Properties properties; + + private TestDescription(final Properties properties) { + this.properties = properties; + } + + /** + * Gets the test description from the deployment. + * + * @param deploymentUnit the deployment unit + * + * @return the test description from the deployment + */ + public static TestDescription from(final DeploymentUnit deploymentUnit) { + // Get the properties + final ResourceRoot resourceRoot = deploymentUnit.getAttachment(Attachments.DEPLOYMENT_ROOT); + final VirtualFile testDescription = resourceRoot.getRoot().getChild(TestDescription.PATH); + final Properties properties = new Properties(); + if (testDescription != null && testDescription.exists()) { + try (InputStream in = testDescription.openStream()) { + properties.load(new InputStreamReader(in, StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + return new TestDescription(properties); + } + + /** + * Creates a test description based on the test deployment and attaches the description to the deployment for later + * usage. For EAR's this attaches the configuration to each module in the EAR. + * + * @param testDeployment the test deployment to gather information from + */ + public static void addTestDescription(final TestDeployment testDeployment) { + String targetContainer = null; + String arqDeploymentName = null; + if (testDeployment.getTargetDescription() != null) { + targetContainer = testDeployment.getTargetDescription().getName(); + } + if (testDeployment.getDeploymentName() != null) { + arqDeploymentName = testDeployment.getDeploymentName(); + } + final Archive archive = testDeployment.getApplicationArchive(); + if (archive instanceof EnterpriseArchive) { + // We need to update EAR's modules separately + final EnterpriseArchive ear = (EnterpriseArchive) archive; + final Map modules = ear + .getContent(ROOT_FILTER); + for (Node module : modules.values()) { + if (module.getAsset() instanceof ArchiveAsset) { + final Archive moduleArchive = ((ArchiveAsset) module.getAsset()).getArchive(); + addTestDescription(moduleArchive, targetContainer, arqDeploymentName); + } + } + } + // Always add a description for the current archive + addTestDescription(archive, targetContainer, arqDeploymentName); + } + + /** + * The container the test and deployment target. + * + * @return the optional name of the target container for the test + */ + public Optional targetContainer() { + return Optional.ofNullable(properties.getProperty(TARGET_CONTAINER)); + } + + /** + * The deployments name. This will be the name of the deployed archive. + * + * @return the deployments name + */ + public String deploymentName() { + return properties.getProperty(DEPLOYMENT_NAME); + } + + /** + * The name of the deployment relevant to Arquillian. This is the value from {@link Deployment#name()} + * + * @return the optional name of the arquillian deployment + */ + public Optional arquillianDeploymentName() { + return Optional.ofNullable(properties.getProperty(ARQ_DEPLOYMENT_NAME)); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof TestDescription)) { + return false; + } + + final TestDescription other = (TestDescription) o; + return Objects.equals(targetContainer(), other.targetContainer()) + && Objects.equals(deploymentName(), other.deploymentName()) + && Objects.equals(arquillianDeploymentName(), other.arquillianDeploymentName()); + } + + @Override + public int hashCode() { + return Objects.hash(targetContainer(), deploymentName(), arquillianDeploymentName()); + } + + @Override + public String toString() { + return "TestDescription [targetContainer=" + targetContainer() + ", deploymentName=" + deploymentName() + + ", arquillianDeploymentName=" + arquillianDeploymentName() + "]"; + } + + private static void addTestDescription(final Archive archive, final String targetContainer, + final String arqDeploymentName) { + try { + final Properties properties = new Properties(); + if (archive.contains(TestDescription.PATH)) { + try (InputStream in = archive.delete(TestDescription.PATH).getAsset().openStream()) { + properties.load(new InputStreamReader(in, StandardCharsets.UTF_8)); + } + } + if (targetContainer != null) { + properties.put(TestDescription.TARGET_CONTAINER, targetContainer); + } + if (arqDeploymentName != null) { + properties.put(TestDescription.ARQ_DEPLOYMENT_NAME, arqDeploymentName); + } + properties.put(TestDescription.DEPLOYMENT_NAME, archive.getName()); + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + properties.store(out, null); + archive.add(new ByteArrayAsset(out.toByteArray()), TestDescription.PATH); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/protocol-jmx/src/main/java/org/jboss/as/arquillian/service/ArquillianConfig.java b/protocol-jmx/src/main/java/org/jboss/as/arquillian/service/ArquillianConfig.java index 7742a263..639db5e2 100644 --- a/protocol-jmx/src/main/java/org/jboss/as/arquillian/service/ArquillianConfig.java +++ b/protocol-jmx/src/main/java/org/jboss/as/arquillian/service/ArquillianConfig.java @@ -18,7 +18,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -26,6 +25,7 @@ import org.jboss.arquillian.container.test.spi.util.ServiceLoader; import org.jboss.arquillian.testenricher.msc.ServiceTargetAssociation; +import org.jboss.as.arquillian.protocol.jmx.TestDescription; import org.jboss.as.server.deployment.Attachments; import org.jboss.as.server.deployment.DeploymentUnit; import org.jboss.modules.Module; @@ -49,13 +49,13 @@ public final class ArquillianConfig implements Service { private final Supplier arquillianServiceSupplier; private final Supplier deploymentUnitSupplier; private final ServiceName serviceName; - private final Map testClasses = new LinkedHashMap<>(); + private final Map testClasses; - ArquillianConfig(final ServiceName serviceName, final Map testClasses, + ArquillianConfig(final ServiceName serviceName, final Map testClasses, final Supplier arquillianServiceSupplier, final Supplier deploymentUnitSupplier) { this.serviceName = serviceName; - this.testClasses.putAll(testClasses); + this.testClasses = Map.copyOf(testClasses); this.arquillianServiceSupplier = arquillianServiceSupplier; this.deploymentUnitSupplier = deploymentUnitSupplier; for (ArquillianConfigServiceCustomizer customizer : ServiceLoader.load(ArquillianConfigServiceCustomizer.class)) { @@ -101,8 +101,10 @@ boolean supports(String className) { * @return {@code true} if this config supports a test class with the given classname and method name */ boolean supports(String className, String methodName) { - TestClassMethods methods = testClasses.get(className); - return methods != null && methods.supportsMethod(methodName); + final TestClassInfo testClassInfo = testClasses.get(className); + return testClassInfo != null && (getDeploymentUnit().getName() + .equals(testClassInfo.testDescription.deploymentName()) + && testClassInfo.supportsMethod(methodName)); } Class loadClass(String className) throws ClassNotFoundException { @@ -143,24 +145,25 @@ public String toString() { return "ArquillianConfig[service=" + sname + ",unit=" + uname + ",tests=" + testClasses + "]"; } - static final class TestClassMethods { - - static final TestClassMethods ALL_METHODS = new TestClassMethods(); + static final class TestClassInfo { private final boolean allMethods; + private final TestDescription testDescription; private final Set methods; - private TestClassMethods() { + TestClassInfo(final TestDescription testDescription) { this.allMethods = true; + this.testDescription = testDescription; this.methods = Collections.emptySet(); } - TestClassMethods(Set methods) { + TestClassInfo(final TestDescription testDescription, final Set methods) { this.allMethods = false; + this.testDescription = testDescription; this.methods = new HashSet<>(methods); } - private boolean supportsMethod(String methodName) { + private boolean supportsMethod(final String methodName) { return allMethods || methods.contains(methodName); } } diff --git a/protocol-jmx/src/main/java/org/jboss/as/arquillian/service/ArquillianConfigBuilder.java b/protocol-jmx/src/main/java/org/jboss/as/arquillian/service/ArquillianConfigBuilder.java index ac223de5..cf574991 100644 --- a/protocol-jmx/src/main/java/org/jboss/as/arquillian/service/ArquillianConfigBuilder.java +++ b/protocol-jmx/src/main/java/org/jboss/as/arquillian/service/ArquillianConfigBuilder.java @@ -23,6 +23,7 @@ import java.util.Map; import java.util.Set; +import org.jboss.as.arquillian.protocol.jmx.TestDescription; import org.jboss.as.server.deployment.AttachmentKey; import org.jboss.as.server.deployment.Attachments; import org.jboss.as.server.deployment.DeploymentUnit; @@ -61,15 +62,15 @@ class ArquillianConfigBuilder { private static final DotName OPERATE_ON_DEPLOYMENT = DotName .createSimple("org.jboss.arquillian.container.test.api.OperateOnDeployment"); - private static final AttachmentKey> CLASSES = AttachmentKey + private static final AttachmentKey> CLASSES = AttachmentKey .create(Map.class); ArquillianConfigBuilder() { } - static Map getClasses(final DeploymentUnit depUnit) { + static Map getClasses(final DeploymentUnit depUnit) { // Get Test Class Names - final Map testClasses = depUnit.getAttachment(CLASSES); + final Map testClasses = depUnit.getAttachment(CLASSES); return testClasses == null || testClasses.isEmpty() ? null : testClasses; } @@ -108,37 +109,57 @@ static void handleParseAnnotations(final DeploymentUnit deploymentUnit) { final Set testNgTests = compositeIndex.getAllKnownSubclasses(testNGClassName); // Get Test Class Names - final Map testClasses = new LinkedHashMap<>(); + final Map testClasses = new LinkedHashMap<>(); + final TestDescription testDescription = TestDescription.from(deploymentUnit); // JUnit for (AnnotationInstance instance : runWithList) { final AnnotationTarget target = instance.target(); if (target instanceof ClassInfo) { final ClassInfo classInfo = (ClassInfo) target; final String testClassName = classInfo.name().toString(); - testClasses.put(testClassName, getTestMethods(classInfo, deploymentUnit.getName())); + testClasses.put(testClassName, + getTestMethods(compositeIndex, classInfo, testDescription)); } } // TestNG for (final ClassInfo classInfo : testNgTests) { - testClasses.put(classInfo.name().toString(), getTestMethods(classInfo, deploymentUnit.getName())); + testClasses.put(classInfo.name().toString(), getTestMethods(compositeIndex, classInfo, testDescription)); } deploymentUnit.putAttachment(CLASSES, testClasses); } - private static ArquillianConfig.TestClassMethods getTestMethods(ClassInfo classInfo, String deployment) { + private static ArquillianConfig.TestClassInfo getTestMethods(final CompositeIndex compositeIndex, final ClassInfo classInfo, + final TestDescription testDescription) { + // Record all methods which can operate on this deployment. + final Set methods = new HashSet<>(); + final String deploymentName = testDescription.arquillianDeploymentName().orElse(null); + findAllMethods(compositeIndex, classInfo, deploymentName, methods); + return new ArquillianConfig.TestClassInfo(testDescription, Set.copyOf(methods)); + } - List instances = classInfo.annotations(OPERATE_ON_DEPLOYMENT); - if (instances.isEmpty()) { - return ArquillianConfig.TestClassMethods.ALL_METHODS; + private static void findAllMethods(final CompositeIndex compositeIndex, final ClassInfo classInfo, + final String deploymentName, final Set methods) { + if (classInfo == null) { + return; } - - Set methods = new HashSet<>(); - for (AnnotationInstance instance : instances) { - if (deployment.equals(instance.value().asString()) - && instance.target().kind() == AnnotationTarget.Kind.METHOD) { - methods.add(instance.target().asMethod().name()); + classInfo.methods().forEach(methodInfo -> { + // If the @OperateOnDeployment method is present, it must match the test descriptions deployment + if (methodInfo.hasAnnotation(OPERATE_ON_DEPLOYMENT)) { + final AnnotationInstance annotation = methodInfo.annotation(OPERATE_ON_DEPLOYMENT); + if (annotation.value().asString().equals(deploymentName)) { + methods.add(methodInfo.name()); + } + } else { + // No @OperateOnDeployment annotation present on the method, we have to assume it's okay to run for + // this test description. + methods.add(methodInfo.name()); } + }); + if (classInfo.superName() != null && !classInfo.superName().toString().equals(Object.class.getName())) { + findAllMethods(compositeIndex, compositeIndex.getClassByName(classInfo.superName()), deploymentName, methods); } - return new ArquillianConfig.TestClassMethods(methods); + // Interfaces can have default methods, we'll check those too + classInfo.interfaceNames() + .forEach(name -> findAllMethods(compositeIndex, compositeIndex.getClassByName(name), deploymentName, methods)); } } diff --git a/protocol-jmx/src/main/java/org/jboss/as/arquillian/service/ArquillianService.java b/protocol-jmx/src/main/java/org/jboss/as/arquillian/service/ArquillianService.java index 925b618b..17f64810 100644 --- a/protocol-jmx/src/main/java/org/jboss/as/arquillian/service/ArquillianService.java +++ b/protocol-jmx/src/main/java/org/jboss/as/arquillian/service/ArquillianService.java @@ -88,7 +88,7 @@ public synchronized void start(final StartContext context) throws StartException arquillianServiceConsumer.accept(this); final MBeanServer mbeanServer = mBeanServerSupplier.get(); try { - jmxTestRunner = new ExtendedJMXTestRunner(); + jmxTestRunner = new ExtendedJMXTestRunner(new ThreadLocal<>()); jmxTestRunner.registerMBean(mbeanServer); } catch (Throwable t) { throw new StartException("Failed to start Arquillian Test Runner", t); @@ -133,6 +133,10 @@ private ArquillianConfig getArquillianConfig(final String className, final long private ArquillianConfig getArquillianConfig(final String className, String methodName, final long timeout) { synchronized (deployedTests) { + if (methodName == null && deployedTests.size() > 1) { + log.warn( + "An attempt was made to lookup an Arquillian configuration with more than one deployed test. This may result in unexpected behavior."); + } log.debugf("Getting Arquillian config for: %s", className); for (ArquillianConfig arqConfig : deployedTests) { // A test class with methods annotated with @OperateOnDeployment may be packaged in multiple @@ -170,18 +174,21 @@ private ArquillianConfig getArquillianConfig(final String className, String meth } private class ExtendedJMXTestRunner extends JMXTestRunner { + private final ThreadLocal configHolder; - ExtendedJMXTestRunner() { - super(new ExtendedTestClassLoader()); + ExtendedJMXTestRunner(final ThreadLocal configHolder) { + super(new ExtendedTestClassLoader(configHolder)); + this.configHolder = configHolder; } @Override public byte[] runTestMethod(final String className, final String methodName, Map protocolProps) { // Setup the ContextManager - ArquillianConfig config = getArquillianConfig(className, methodName, 30000L); + final ArquillianConfig config = getArquillianConfig(className, methodName, 30000L); Map properties = Collections.singletonMap(TEST_CLASS_PROPERTY, className); ContextManager contextManager = setupContextManager(config, properties); try { + configHolder.set(config); ClassLoader runWithClassLoader = ClassLoader.getSystemClassLoader(); if (Boolean.parseBoolean(protocolProps.get(ExtendedJMXProtocolConfiguration.PROPERTY_ENABLE_TCCL))) { DeploymentUnit depUnit = config.getDeploymentUnit(); @@ -197,6 +204,7 @@ public byte[] runTestMethod(final String className, final String methodName, Map WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(tccl); } } finally { + configHolder.remove(); if (contextManager != null) { contextManager.teardown(properties); } @@ -208,7 +216,11 @@ protected TestResult doRunTestMethod(TestRunner runner, Class testClass, Stri Map protocolProps) { ClassLoader runWithClassLoader = ClassLoader.getSystemClassLoader(); if (Boolean.parseBoolean(protocolProps.get(ExtendedJMXProtocolConfiguration.PROPERTY_ENABLE_TCCL))) { - ArquillianConfig config = getArquillianConfig(testClass.getName(), methodName, 30000L); + ArquillianConfig config = configHolder.get(); + // This should not happen, but in this case we'll be safe + if (config == null) { + config = getArquillianConfig(testClass.getName(), methodName, 30000L); + } DeploymentUnit depUnit = config.getDeploymentUnit(); Module module = depUnit.getAttachment(Attachments.MODULE); if (module != null) { @@ -237,13 +249,21 @@ private ContextManager setupContextManager(final ArquillianConfig config, final } class ExtendedTestClassLoader implements JMXTestRunner.TestClassLoader { + private final ThreadLocal configHolder; + + ExtendedTestClassLoader(final ThreadLocal configHolder) { + this.configHolder = configHolder; + } @Override public Class loadTestClass(final String className) throws ClassNotFoundException { - - final ArquillianConfig arqConfig = getArquillianConfig(className, -1); - if (arqConfig == null) - throw new ClassNotFoundException("No Arquillian config found for: " + className); + // We first attempt to check the thread local, if not set we will make an attempt to look it up + ArquillianConfig arqConfig = configHolder.get(); + if (arqConfig == null) { + arqConfig = getArquillianConfig(className, -1); + if (arqConfig == null) + throw new ClassNotFoundException("No Arquillian config found for: " + className); + } return arqConfig.loadClass(className); } @@ -277,7 +297,7 @@ public void handleEvent(final ServiceController controller, final LifecycleEv ServiceName parentName = serviceName.getParent(); ServiceController parentController = controller.getServiceContainer().getService(parentName); DeploymentUnit depUnit = (DeploymentUnit) parentController.getValue(); // TODO: eliminate deprecated API usage - Map testClasses = ArquillianConfigBuilder.getClasses(depUnit); + Map testClasses = ArquillianConfigBuilder.getClasses(depUnit); if (testClasses != null) { String duName = ArquillianConfigBuilder.getName(depUnit); ServiceName arqConfigSN = ServiceName.JBOSS.append("arquillian", "config", duName);