From 936de94e1334bf97f83d0ec0ac39ebf48b6a7ee1 Mon Sep 17 00:00:00 2001 From: "James R. Perkins" Date: Tue, 16 Apr 2024 10:02:30 -0700 Subject: [PATCH 1/4] [WFARQ-167] Allow ServerSetupTask's to use resource injection. https://issues.redhat.com/browse/WFARQ-167 Signed-off-by: James R. Perkins --- .../container/ServerSetupObserver.java | 56 +++++++++++++++++-- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/common/src/main/java/org/jboss/as/arquillian/container/ServerSetupObserver.java b/common/src/main/java/org/jboss/as/arquillian/container/ServerSetupObserver.java index b9a4897b..03fd9c2d 100644 --- a/common/src/main/java/org/jboss/as/arquillian/container/ServerSetupObserver.java +++ b/common/src/main/java/org/jboss/as/arquillian/container/ServerSetupObserver.java @@ -18,7 +18,9 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.ArrayDeque; +import java.util.Collection; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; @@ -28,12 +30,19 @@ import org.jboss.arquillian.container.spi.Container; import org.jboss.arquillian.container.spi.client.deployment.DeploymentDescription; +import org.jboss.arquillian.container.spi.context.ContainerContext; import org.jboss.arquillian.container.spi.event.container.AfterUnDeploy; import org.jboss.arquillian.container.spi.event.container.BeforeDeploy; +import org.jboss.arquillian.core.api.Event; import org.jboss.arquillian.core.api.Instance; import org.jboss.arquillian.core.api.annotation.Inject; import org.jboss.arquillian.core.api.annotation.Observes; +import org.jboss.arquillian.core.spi.ServiceLoader; +import org.jboss.arquillian.test.spi.TestEnricher; import org.jboss.arquillian.test.spi.context.ClassContext; +import org.jboss.arquillian.test.spi.event.enrichment.AfterEnrichment; +import org.jboss.arquillian.test.spi.event.enrichment.BeforeEnrichment; +import org.jboss.arquillian.test.spi.event.enrichment.EnrichmentEvent; import org.jboss.arquillian.test.spi.event.suite.AfterClass; import org.jboss.arquillian.test.spi.event.suite.BeforeClass; import org.jboss.as.arquillian.api.ServerSetup; @@ -52,12 +61,21 @@ public class ServerSetupObserver { private static final Logger log = Logger.getLogger(ServerSetupObserver.class); + @Inject + private Instance containerContext; + @Inject private Instance managementClient; @Inject private Instance classContextInstance; + @Inject + private Instance serviceLoader; + + @Inject + private Event enrichmentEvent; + private final Map setupTasks = new HashMap<>(); private boolean afterClassRun = false; @@ -103,7 +121,8 @@ public synchronized void handleBeforeDeployment(@Observes BeforeDeploy event, Co } final ManagementClient client = managementClient.get(); - final ServerSetupTaskHolder holder = new ServerSetupTaskHolder(client); + final ServerSetupTaskHolder holder = new ServerSetupTaskHolder(client, container, containerContext.get(), serviceLoader, + enrichmentEvent); executeSetup(holder, setup, containerName, event.getDeployment()); } @@ -238,11 +257,22 @@ private static Class loadAssumptionFailureClass(String classname) { private final ManagementClient client; private final Deque setupTasks; private final Set deployments; - - private ServerSetupTaskHolder(final ManagementClient client) { + private final Container container; + private final ContainerContext containerContext; + private final Instance serviceLoader; + private final Event enrichmentEvent; + + private ServerSetupTaskHolder(final ManagementClient client, final Container container, + final ContainerContext containerContext, + final Instance serviceLoader, + final Event enrichmentEvent) { this.client = client; setupTasks = new ArrayDeque<>(); deployments = new HashSet<>(); + this.container = container; + this.containerContext = containerContext; + this.serviceLoader = serviceLoader; + this.enrichmentEvent = enrichmentEvent; } void setup(final ServerSetup setup, final String containerName) @@ -253,8 +283,9 @@ void setup(final ServerSetup setup, final String containerName) final Constructor ctor = clazz.getDeclaredConstructor(); ctor.setAccessible(true); final ServerSetupTask task = ctor.newInstance(); - setupTasks.add(task); try { + enrich(task, clazz.getMethod("setup", ManagementClient.class, String.class)); + setupTasks.add(task); task.setup(client, containerName); } catch (Throwable e) { // If this is one of the 'assumption failed' exceptions used in JUnit 4 or 5, throw it on @@ -273,6 +304,7 @@ public void tearDown(final String containerName) { ServerSetupTask task; while ((task = setupTasks.pollLast()) != null) { try { + enrich(task, task.getClass().getMethod("tearDown", ManagementClient.class, String.class)); task.tearDown(client, containerName); } catch (Throwable e) { // Unlike with setup, here we don't propagate assumption failures. @@ -303,10 +335,24 @@ private void rethrowFailedAssumptions(final Throwable t, final String containerN @SuppressWarnings("SameParameterValue") private void rethrowFailedAssumption(Throwable t, Class failureType) throws FailedAssumptionException { - if (failureType != null && t.getClass().isAssignableFrom(failureType)) { + if (failureType != null && failureType.isAssignableFrom(t.getClass())) { throw new FailedAssumptionException(t); } } + + private void enrich(final ServerSetupTask task, final Method method) { + try { + containerContext.activate(container.getName()); + enrichmentEvent.fire(new BeforeEnrichment(task, method)); + Collection testEnrichers = serviceLoader.get().all(TestEnricher.class); + for (TestEnricher enricher : testEnrichers) { + enricher.enrich(task); + } + enrichmentEvent.fire(new AfterEnrichment(task, method)); + } finally { + containerContext.deactivate(); + } + } } private static class FailedAssumptionException extends Exception { From d9649ae0463d5a9667e41b497aa20187b1d0b607 Mon Sep 17 00:00:00 2001 From: "James R. Perkins" Date: Tue, 16 Apr 2024 14:19:38 -0700 Subject: [PATCH 2/4] [WFARQ-169] Add helper methods to the ServerSetupTask for executing operations. https://issues.redhat.com/browse/WFARQ-169 Signed-off-by: James R. Perkins --- .../as/arquillian/api/ServerSetupTask.java | 82 +++++++++++++++++++ .../CommonContainerArchiveAppender.java | 50 +++++++++++ .../container/CommonContainerExtension.java | 2 + 3 files changed, 134 insertions(+) create mode 100644 common/src/main/java/org/jboss/as/arquillian/container/CommonContainerArchiveAppender.java diff --git a/common/src/main/java/org/jboss/as/arquillian/api/ServerSetupTask.java b/common/src/main/java/org/jboss/as/arquillian/api/ServerSetupTask.java index 638ab004..a9d117f7 100644 --- a/common/src/main/java/org/jboss/as/arquillian/api/ServerSetupTask.java +++ b/common/src/main/java/org/jboss/as/arquillian/api/ServerSetupTask.java @@ -16,7 +16,14 @@ package org.jboss.as.arquillian.api; +import java.io.IOException; +import java.util.function.Function; + import org.jboss.as.arquillian.container.ManagementClient; +import org.jboss.as.controller.client.Operation; +import org.jboss.as.controller.client.helpers.Operations; +import org.jboss.dmr.ModelNode; +import org.wildfly.plugin.tools.OperationExecutionException; /** * @@ -24,6 +31,7 @@ * * @author Stuart Douglas */ +@SuppressWarnings("unused") public interface ServerSetupTask { /** @@ -61,4 +69,78 @@ public interface ServerSetupTask { * @throws Exception if a failure occurs */ void tearDown(ManagementClient managementClient, String containerId) throws Exception; + + /** + * Executes an operation failing with a {@code RuntimeException} if the operation was not successful. + * + * @param client the client used to communicate with the server + * @param op the operation to execute + * + * @return the result from the operation + * + * @throws OperationExecutionException if the operation failed + * @throws IOException if an error occurs communicating with the server + */ + default ModelNode executeOperation(final ManagementClient client, final ModelNode op) throws IOException { + return executeOperation(client, op, (result) -> String.format("Failed to execute operation '%s': %s", op + .asString(), + Operations.getFailureDescription(result).asString())); + } + + /** + * Executes an operation failing with a {@code RuntimeException} if the operation was not successful. + * + * @param client the client used to communicate with the server + * @param op the operation to execute + * @param errorMessage a function which accepts the result as the argument and returns an error message for an + * unsuccessful operation + * + * @return the result from the operation + * + * @throws OperationExecutionException if the operation failed + * @throws IOException if an error occurs communicating with the server + */ + default ModelNode executeOperation(final ManagementClient client, final ModelNode op, + final Function errorMessage) throws IOException { + return executeOperation(client, Operation.Factory.create(op), errorMessage); + } + + /** + * Executes an operation failing with a {@code RuntimeException} if the operation was not successful. + * + * @param client the client used to communicate with the server + * @param op the operation to execute + * + * @return the result from the operation + * + * @throws OperationExecutionException if the operation failed + * @throws IOException if an error occurs communicating with the server + */ + default ModelNode executeOperation(final ManagementClient client, final Operation op) throws IOException { + return executeOperation(client, op, (result) -> String.format("Failed to execute operation '%s': %s", op.getOperation() + .asString(), + Operations.getFailureDescription(result).asString())); + } + + /** + * Executes an operation failing with a {@code RuntimeException} if the operation was not successful. + * + * @param client the client used to communicate with the server + * @param op the operation to execute + * @param errorMessage a function which accepts the result as the argument and returns an error message for an + * unsuccessful operation + * + * @return the result from the operation + * + * @throws OperationExecutionException if the operation failed + * @throws IOException if an error occurs communicating with the server + */ + default ModelNode executeOperation(final ManagementClient client, final Operation op, + final Function errorMessage) throws IOException { + final ModelNode result = client.getControllerClient().execute(op); + if (!Operations.isSuccessfulOutcome(result)) { + throw new OperationExecutionException(op, result); + } + return Operations.readResult(result); + } } diff --git a/common/src/main/java/org/jboss/as/arquillian/container/CommonContainerArchiveAppender.java b/common/src/main/java/org/jboss/as/arquillian/container/CommonContainerArchiveAppender.java new file mode 100644 index 00000000..b5ce0c84 --- /dev/null +++ b/common/src/main/java/org/jboss/as/arquillian/container/CommonContainerArchiveAppender.java @@ -0,0 +1,50 @@ +/* + * 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.container; + +import org.jboss.arquillian.container.test.spi.client.deployment.AuxiliaryArchiveAppender; +import org.jboss.as.arquillian.api.ServerSetup; +import org.jboss.as.arquillian.api.ServerSetupTask; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.wildfly.plugin.tools.OperationExecutionException; + +/** + * Creates a library to add to deployments for common container based dependencies for in-container tests. + * + * @author James R. Perkins + */ +public class CommonContainerArchiveAppender implements AuxiliaryArchiveAppender { + + @Override + public Archive createAuxiliaryArchive() { + return ShrinkWrap.create(JavaArchive.class, "wildfly-common-testencricher.jar") + // These two types are added to avoid exceptions with class loading for in-container tests. These + // shouldn't really be used for in-container tests. + .addClasses(ServerSetupTask.class, ServerSetup.class) + .addClasses(ManagementClient.class) + // Adds wildfly-plugin-tools, this exception itself is explicitly needed + .addPackages(true, OperationExecutionException.class.getPackage()) + .setManifest(new StringAsset("Manifest-Version: 1.0\n" + + "Dependencies: org.jboss.as.controller-client,org.jboss.dmr\n")); + } +} diff --git a/common/src/main/java/org/jboss/as/arquillian/container/CommonContainerExtension.java b/common/src/main/java/org/jboss/as/arquillian/container/CommonContainerExtension.java index 62c32ce4..7e92e4bf 100644 --- a/common/src/main/java/org/jboss/as/arquillian/container/CommonContainerExtension.java +++ b/common/src/main/java/org/jboss/as/arquillian/container/CommonContainerExtension.java @@ -16,6 +16,7 @@ package org.jboss.as.arquillian.container; import org.jboss.arquillian.container.spi.client.container.DeploymentExceptionTransformer; +import org.jboss.arquillian.container.test.spi.client.deployment.AuxiliaryArchiveAppender; import org.jboss.arquillian.core.spi.LoadableExtension; import org.jboss.arquillian.test.spi.TestEnricher; import org.jboss.arquillian.test.spi.enricher.resource.ResourceProvider; @@ -39,6 +40,7 @@ public void register(final ExtensionBuilder builder) { builder.service(ResourceProvider.class, ArchiveDeployerProvider.class); builder.service(ResourceProvider.class, ManagementClientProvider.class); builder.service(TestEnricher.class, ContainerResourceTestEnricher.class); + builder.service(AuxiliaryArchiveAppender.class, CommonContainerArchiveAppender.class); builder.observer(ServerSetupObserver.class); From 28677f456410ef4a4315259a2aecc9b7ef3e7510 Mon Sep 17 00:00:00 2001 From: "James R. Perkins" Date: Tue, 16 Apr 2024 15:49:07 -0700 Subject: [PATCH 3/4] [WFARQ-168] Re-throw exceptions for all errors thrown from a ServerSetupTask.setup. https://issues.redhat.com/browse/WFARQ-168 Signed-off-by: James R. Perkins --- .../as/arquillian/api/ServerSetupTask.java | 11 +- .../container/ServerSetupObserver.java | 112 +++--------- integration-tests/junit5-tests/pom.xml | 5 + .../server/setup/SetupTaskTestCase.java | 160 +++++++++++++++++ .../junit5/server/setup/SetupTaskTests.java | 162 ++++++++++++++++++ .../setup/SystemPropertyServerSetupTask.java | 60 +++++++ .../src/test/resources/arquillian.xml | 3 +- pom.xml | 5 + 8 files changed, 430 insertions(+), 88 deletions(-) create mode 100644 integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/server/setup/SetupTaskTestCase.java create mode 100644 integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/server/setup/SetupTaskTests.java create mode 100644 integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/server/setup/SystemPropertyServerSetupTask.java diff --git a/common/src/main/java/org/jboss/as/arquillian/api/ServerSetupTask.java b/common/src/main/java/org/jboss/as/arquillian/api/ServerSetupTask.java index a9d117f7..b84fecb3 100644 --- a/common/src/main/java/org/jboss/as/arquillian/api/ServerSetupTask.java +++ b/common/src/main/java/org/jboss/as/arquillian/api/ServerSetupTask.java @@ -39,20 +39,25 @@ public interface ServerSetupTask { * to the given container. *

* Note on exception handling: If an implementation of this method - * throws {@code org.junit.AssumptionViolatedException}, the implementation can assume - * the following: + * throws any exception, the implementation can assume the following: *

    *
  1. Any subsequent {@code ServerSetupTask}s {@link ServerSetup associated with test class} * will not be executed.
  2. *
  3. The deployment event that triggered the call to this method will be skipped.
  4. *
  5. The {@link #tearDown(ManagementClient, String) tearDown} method of the instance * that threw the exception will not be invoked. Therefore, implementations - * that throw {@code AssumptionViolatedException} should do so before altering any + * that throw {@code AssumptionViolatedException}, or any other exception, should do so before altering any * system state.
  6. *
  7. The {@link #tearDown(ManagementClient, String) tearDown} method for any * previously executed {@code ServerSetupTask}s {@link ServerSetup associated with test class} * will be invoked.
  8. *
+ *

+ * If any other exception is thrown, the {@link #tearDown(ManagementClient, String)} will be executed, including + * this implementations {@code tearDown()}, re-throwing the original exception. The original exception will have + * any other exceptions thrown in the {@code tearDown()} methods add as + * {@linkplain Throwable#addSuppressed(Throwable) suppressed} messages. + *

* * @param managementClient management client to use to interact with the container * @param containerId id of the container to which the deployment will be deployed diff --git a/common/src/main/java/org/jboss/as/arquillian/container/ServerSetupObserver.java b/common/src/main/java/org/jboss/as/arquillian/container/ServerSetupObserver.java index 03fd9c2d..76e15e50 100644 --- a/common/src/main/java/org/jboss/as/arquillian/container/ServerSetupObserver.java +++ b/common/src/main/java/org/jboss/as/arquillian/container/ServerSetupObserver.java @@ -17,7 +17,6 @@ package org.jboss.as.arquillian.container; import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayDeque; import java.util.Collection; @@ -121,8 +120,7 @@ public synchronized void handleBeforeDeployment(@Observes BeforeDeploy event, Co } final ManagementClient client = managementClient.get(); - final ServerSetupTaskHolder holder = new ServerSetupTaskHolder(client, container, containerContext.get(), serviceLoader, - enrichmentEvent); + final ServerSetupTaskHolder holder = new ServerSetupTaskHolder(client, container.getName()); executeSetup(holder, setup, containerName, event.getDeployment()); } @@ -184,14 +182,15 @@ public synchronized void handleAfterUndeploy(@Observes AfterUnDeploy afterDeploy private void executeSetup(final ServerSetupTaskHolder holder, ServerSetup setup, String containerName, DeploymentDescription deployment) - throws InvocationTargetException, NoSuchMethodException, IllegalAccessException, InstantiationException { + throws Exception { holder.deployments.add(deployment); setupTasks.put(containerName, holder); try { holder.setup(setup, containerName); - } catch (FailedAssumptionException fae) { + } catch (Throwable t) { + final Throwable toThrow = t; // We're going to throw on the underlying problem. But since that is going to // propagate to surefire and prevent further processing of the currently executing // test class, first we need to do cleanup work that's normally triggered by @@ -210,10 +209,13 @@ private void executeSetup(final ServerSetupTaskHolder holder, ServerSetup setup, // Tell the holder to do the normal tearDown holder.tearDown(containerName); - } catch (RuntimeException logged) { // just to be safe + } catch (Exception logged) { // just to be safe String className = failedSetup != null ? failedSetup.getClass().getName() : ServerSetupTask.class.getSimpleName(); - log.errorf(logged, "Failed tearing down ServerSetupTasks after a failed assumption in %s.setup()", className); + final String message = String + .format("Failed tearing down ServerSetupTasks after a failed assumption in %s.setup()", className); + toThrow.addSuppressed(new RuntimeException(message, logged)); + log.errorf(logged, message); } finally { // Clean out our own state changes we made before calling holder.setup. // Otherwise, later classes that use the same container may fail. @@ -222,77 +224,40 @@ private void executeSetup(final ServerSetupTaskHolder holder, ServerSetup setup, setupTasks.remove(containerName); } } - - throw fae.underlyingException; + if (toThrow instanceof Exception) { + throw (Exception) toThrow; + } + if (toThrow instanceof Error) { + throw (Error) toThrow; + } + // Throw the error as an assertion error to abort the testing + throw new AssertionError("Failed to invoke a ServerSetupTask.", toThrow); } - } - private static class ServerSetupTaskHolder { - - // Use reflection to access the various 'assumption' failure classes - // to avoid compile dependencies on JUnit 4 and 5 - private static final Class ASSUMPTION_VIOLATED_EXCEPTION = loadAssumptionFailureClass( - "org.junit.AssumptionViolatedException"); - - private static final Class TEST_ABORTED_EXCEPTION = loadAssumptionFailureClass( - "org.opentest4j.TestAbortedException"); - - private static final Class TESTNG_SKIPPED_EXCEPTION = loadAssumptionFailureClass( - "org.testng.SkipException"); - - @SuppressWarnings("SameParameterValue") - private static Class loadAssumptionFailureClass(String classname) { - Class result = null; - try { - result = ServerSetupObserver.class.getClassLoader().loadClass(classname); - } catch (ClassNotFoundException cnfe) { - log.debugf("%s is not available", classname); - } catch (Throwable t) { - log.warnf(t, "Failed to load %s", classname); - } - return result; - } + private class ServerSetupTaskHolder { private final ManagementClient client; private final Deque setupTasks; private final Set deployments; - private final Container container; - private final ContainerContext containerContext; - private final Instance serviceLoader; - private final Event enrichmentEvent; - - private ServerSetupTaskHolder(final ManagementClient client, final Container container, - final ContainerContext containerContext, - final Instance serviceLoader, - final Event enrichmentEvent) { + private final String containerName;; + + private ServerSetupTaskHolder(final ManagementClient client, final String containerName) { this.client = client; setupTasks = new ArrayDeque<>(); deployments = new HashSet<>(); - this.container = container; - this.containerContext = containerContext; - this.serviceLoader = serviceLoader; - this.enrichmentEvent = enrichmentEvent; + this.containerName = containerName; } - void setup(final ServerSetup setup, final String containerName) - throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, - FailedAssumptionException { + void setup(final ServerSetup setup, final String containerName) throws Throwable { final Class[] classes = setup.value(); for (Class clazz : classes) { final Constructor ctor = clazz.getDeclaredConstructor(); ctor.setAccessible(true); final ServerSetupTask task = ctor.newInstance(); - try { - enrich(task, clazz.getMethod("setup", ManagementClient.class, String.class)); - setupTasks.add(task); - task.setup(client, containerName); - } catch (Throwable e) { - // If this is one of the 'assumption failed' exceptions used in JUnit 4 or 5, throw it on - rethrowFailedAssumptions(e, containerName); - // Some other failure -- log it - log.errorf(e, "Setup failed during setup. Offending class '%s'", task); - } + enrich(task, clazz.getMethod("setup", ManagementClient.class, String.class)); + setupTasks.add(task); + task.setup(client, containerName); } } @@ -327,22 +292,9 @@ public String toString() { "]"; } - private void rethrowFailedAssumptions(final Throwable t, final String containerName) throws FailedAssumptionException { - rethrowFailedAssumption(t, ASSUMPTION_VIOLATED_EXCEPTION); - rethrowFailedAssumption(t, TEST_ABORTED_EXCEPTION); - rethrowFailedAssumption(t, TESTNG_SKIPPED_EXCEPTION); - } - - @SuppressWarnings("SameParameterValue") - private void rethrowFailedAssumption(Throwable t, Class failureType) throws FailedAssumptionException { - if (failureType != null && failureType.isAssignableFrom(t.getClass())) { - throw new FailedAssumptionException(t); - } - } - private void enrich(final ServerSetupTask task, final Method method) { try { - containerContext.activate(container.getName()); + containerContext.get().activate(containerName); enrichmentEvent.fire(new BeforeEnrichment(task, method)); Collection testEnrichers = serviceLoader.get().all(TestEnricher.class); for (TestEnricher enricher : testEnrichers) { @@ -350,16 +302,8 @@ private void enrich(final ServerSetupTask task, final Method method) { } enrichmentEvent.fire(new AfterEnrichment(task, method)); } finally { - containerContext.deactivate(); + containerContext.get().deactivate(); } } } - - private static class FailedAssumptionException extends Exception { - private final RuntimeException underlyingException; - - private FailedAssumptionException(Object underlying) { - this.underlyingException = (RuntimeException) underlying; - } - } } diff --git a/integration-tests/junit5-tests/pom.xml b/integration-tests/junit5-tests/pom.xml index 41ac961d..885b18b6 100644 --- a/integration-tests/junit5-tests/pom.xml +++ b/integration-tests/junit5-tests/pom.xml @@ -68,6 +68,11 @@ junit-platform-testkit test + + org.wildfly.arquillian + wildfly-arquillian-junit-api + test + org.wildfly.arquillian wildfly-arquillian-container-managed diff --git a/integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/server/setup/SetupTaskTestCase.java b/integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/server/setup/SetupTaskTestCase.java new file mode 100644 index 00000000..92764201 --- /dev/null +++ b/integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/server/setup/SetupTaskTestCase.java @@ -0,0 +1,160 @@ +/* + * 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.wildfly.arquillian.integration.test.junit5.server.setup; + +import java.io.IOException; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.as.arquillian.container.ManagementClient; +import org.jboss.as.controller.client.Operation; +import org.jboss.as.controller.client.helpers.ClientConstants; +import org.jboss.as.controller.client.helpers.Operations; +import org.jboss.as.controller.client.helpers.Operations.CompositeOperationBuilder; +import org.jboss.dmr.ModelNode; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.testkit.engine.EngineTestKit; +import org.junit.platform.testkit.engine.EventConditions; +import org.junit.platform.testkit.engine.TestExecutionResultConditions; +import org.wildfly.arquillian.junit.annotations.WildFlyArquillian; + +/** + * @author James R. Perkins + */ +@WildFlyArquillian +@RunAsClient +public class SetupTaskTestCase { + + @ArquillianResource + private ManagementClient client; + + @Deployment + public static WebArchive createDeployment() { + return ShrinkWrap.create(WebArchive.class, "setup-task-test.war") + .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml"); + } + + @BeforeEach + public void clearSystemProperties() throws Exception { + final CompositeOperationBuilder builder = CompositeOperationBuilder.create(); + for (ModelNode name : getSystemProperties()) { + final ModelNode address = Operations.createAddress("system-property", name.asString()); + builder.addStep(Operations.createRemoveOperation(address)); + } + executeOperation(builder.build()); + } + + @Test + public void successThenAssertionFail() throws Exception { + final var results = EngineTestKit.engine("junit-jupiter") + .selectors(DiscoverySelectors.selectClass(SetupTaskTests.SuccessThenAssertionFail.class)) + .execute(); + // No tests should have been executed + final var testEvents = results.testEvents(); + testEvents.assertThatEvents().isEmpty(); + // We should have one failure from the setup task + final var events = results.allEvents(); + events.assertStatistics((stats) -> stats.failed(1L)); + events.assertThatEvents() + .haveAtLeastOne(EventConditions.event( + EventConditions.finishedWithFailure(TestExecutionResultConditions.instanceOf(AssertionError.class)))); + assertOnlyProperties(SetupTaskTests.AssertionErrorSetupTask.PROPERTY_NAME); + } + + @Test + public void successThenRuntimeFail() throws Exception { + final var results = EngineTestKit.engine("junit-jupiter") + .selectors(DiscoverySelectors.selectClass(SetupTaskTests.SuccessThenRuntimeFail.class)) + .execute(); + // No tests should have been executed + final var testEvents = results.testEvents(); + testEvents.assertThatEvents().isEmpty(); + // We should have one failure from the setup task + final var events = results.allEvents(); + events.assertStatistics((stats) -> stats.failed(1L)); + events.assertThatEvents() + .haveAtLeastOne(EventConditions.event( + EventConditions.finishedWithFailure(TestExecutionResultConditions.instanceOf(RuntimeException.class)))); + assertOnlyProperties(SetupTaskTests.RuntimeExceptionSetupTask.PROPERTY_NAME); + } + + @Test + public void successAndAfter() throws Exception { + final var results = EngineTestKit.engine("junit-jupiter") + .selectors(DiscoverySelectors.selectMethod(SetupTaskTests.SuccessAndAfter.class, "systemPropertiesExist")) + .execute(); + // The test should have been successful + final var testEvents = results.testEvents(); + testEvents.assertThatEvents().haveExactly(1, EventConditions.finishedSuccessfully()); + // All properties should have been removed in the SetupServerTask.tearDown() + assertNoSystemProperties(); + } + + private void assertOnlyProperties(final String... names) throws IOException { + final Set expectedNames = Set.of(names); + final Set allProperties = getSystemProperties() + .stream() + .map(ModelNode::asString) + .collect(Collectors.toCollection(LinkedHashSet::new)); + Assertions.assertTrue(allProperties.containsAll(expectedNames), () -> String + .format("The following properties were expected in \"%s\", but not found; %s", allProperties, expectedNames)); + // Remove the expected properties + allProperties.removeAll(expectedNames); + Assertions.assertTrue(allProperties.isEmpty(), + () -> String.format("The following properties exist which should not exist: %s", allProperties)); + } + + private void assertNoSystemProperties() throws IOException { + final ModelNode op = Operations.createOperation("read-children-names"); + op.get(ClientConstants.CHILD_TYPE).set("system-property"); + final ModelNode result = executeOperation(op); + Assertions.assertTrue(result.asList() + .isEmpty(), () -> "Expected no system properties, found: " + result.asString()); + } + + private List getSystemProperties() throws IOException { + final ModelNode op = Operations.createOperation("read-children-names"); + op.get(ClientConstants.CHILD_TYPE).set("system-property"); + return executeOperation(op).asList(); + } + + private ModelNode executeOperation(final ModelNode op) throws IOException { + return executeOperation(Operation.Factory.create(op)); + } + + private ModelNode executeOperation(final Operation op) throws IOException { + final ModelNode result = client.getControllerClient().execute(op); + if (!Operations.isSuccessfulOutcome(result)) { + Assertions.fail("Operation has failed: " + Operations.getFailureDescription(result).asString()); + } + return Operations.readResult(result); + } +} diff --git a/integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/server/setup/SetupTaskTests.java b/integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/server/setup/SetupTaskTests.java new file mode 100644 index 00000000..947cf336 --- /dev/null +++ b/integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/server/setup/SetupTaskTests.java @@ -0,0 +1,162 @@ +/* + * 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.wildfly.arquillian.integration.test.junit5.server.setup; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.as.arquillian.api.ServerSetup; +import org.jboss.as.arquillian.api.ServerSetupTask; +import org.jboss.as.arquillian.container.ManagementClient; +import org.jboss.as.controller.client.helpers.ClientConstants; +import org.jboss.as.controller.client.helpers.Operations; +import org.jboss.dmr.ModelNode; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.wildfly.arquillian.junit.annotations.WildFlyArquillian; + +/** + * @author James R. Perkins + */ +@WildFlyArquillian +@RunAsClient +abstract class SetupTaskTests { + + public static class SuccessfulSetupTask extends SystemPropertyServerSetupTask implements ServerSetupTask { + public static final String PROPERTY_NAME = "wildfly.arquillian.test.success"; + + public SuccessfulSetupTask() { + super(Map.of(PROPERTY_NAME, "true")); + } + } + + public static class AfterSuccessfulSetupTask extends SystemPropertyServerSetupTask implements ServerSetupTask { + public static final String PROPERTY_NAME = "wildfly.arquillian.test.success.after"; + + public AfterSuccessfulSetupTask() { + super(Map.of(PROPERTY_NAME, "true")); + } + } + + public static class RuntimeExceptionSetupTask extends SystemPropertyServerSetupTask implements ServerSetupTask { + public static final String PROPERTY_NAME = "wildfly.arquillian.test.runtime.exception"; + + public RuntimeExceptionSetupTask() { + super(Map.of(PROPERTY_NAME, "true")); + } + + @Override + public void setup(final ManagementClient managementClient, final String containerId) throws Exception { + super.setup(managementClient, containerId); + throw new RuntimeException("RuntimeException failed on purpose"); + } + } + + public static class AssertionErrorSetupTask extends SystemPropertyServerSetupTask implements ServerSetupTask { + public static final String PROPERTY_NAME = "wildfly.arquillian.test.assertion.error"; + + public AssertionErrorSetupTask() { + super(Map.of(PROPERTY_NAME, "true")); + } + + @Override + public void setup(final ManagementClient managementClient, final String containerId) throws Exception { + super.setup(managementClient, containerId); + Assertions.fail("AssertionError failed on purpose"); + } + } + + @ArquillianResource + private ManagementClient client; + + @Deployment + public static WebArchive createDeployment() { + return ShrinkWrap.create(WebArchive.class, "inner-setup-task-tests.war") + .addClasses(SetupTaskTestCase.class, + SuccessfulSetupTask.class, + RuntimeExceptionSetupTask.class, + AssertionErrorSetupTask.class); + } + + @Test + public void failIfExecuted(final TestInfo testInfo) { + Assertions.fail(String.format("Test %s.%s should not have been executed.", + testInfo.getTestClass().map(Class::getName).orElse("Unknown"), + testInfo.getTestMethod().map(Method::getName).orElse("Unknown"))); + } + + @Test + public void systemPropertiesExist() throws Exception { + final Set properties = getSystemProperties() + .stream() + .map(ModelNode::asString) + .collect(Collectors.toCollection(TreeSet::new)); + Assertions.assertIterableEquals( + new TreeSet<>(Set.of(SuccessfulSetupTask.PROPERTY_NAME, AfterSuccessfulSetupTask.PROPERTY_NAME)), properties); + } + + private List getSystemProperties() throws IOException { + final ModelNode op = Operations.createOperation("read-children-names"); + op.get(ClientConstants.CHILD_TYPE).set("system-property"); + return executeOperation(op).asList(); + } + + private ModelNode executeOperation(final ModelNode op) throws IOException { + final ModelNode result = client.getControllerClient().execute(op); + if (!Operations.isSuccessfulOutcome(result)) { + Assertions.fail("Operation has failed: " + Operations.getFailureDescription(result).asString()); + } + return Operations.readResult(result); + } + + @ServerSetup({ + SuccessfulSetupTask.class, + RuntimeExceptionSetupTask.class, + AfterSuccessfulSetupTask.class + }) + public static class SuccessThenRuntimeFail extends SetupTaskTests { + } + + @ServerSetup({ + SuccessfulSetupTask.class, + AssertionErrorSetupTask.class, + AfterSuccessfulSetupTask.class + }) + public static class SuccessThenAssertionFail extends SetupTaskTests { + } + + @ServerSetup({ + SuccessfulSetupTask.class, + AfterSuccessfulSetupTask.class + }) + public static class SuccessAndAfter extends SetupTaskTests { + } +} diff --git a/integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/server/setup/SystemPropertyServerSetupTask.java b/integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/server/setup/SystemPropertyServerSetupTask.java new file mode 100644 index 00000000..58eadf58 --- /dev/null +++ b/integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/server/setup/SystemPropertyServerSetupTask.java @@ -0,0 +1,60 @@ +/* + * 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.wildfly.arquillian.integration.test.junit5.server.setup; + +import java.util.Map; + +import org.jboss.as.arquillian.api.ServerSetupTask; +import org.jboss.as.arquillian.container.ManagementClient; +import org.jboss.as.controller.client.helpers.Operations; +import org.jboss.dmr.ModelNode; + +/** + * @author James R. Perkins + */ +public class SystemPropertyServerSetupTask implements ServerSetupTask { + public final Map properties; + + public SystemPropertyServerSetupTask(final Map properties) { + this.properties = Map.copyOf(properties); + } + + @Override + public void setup(final ManagementClient managementClient, final String containerId) throws Exception { + final Operations.CompositeOperationBuilder builder = Operations.CompositeOperationBuilder.create(); + for (var entry : properties.entrySet()) { + final ModelNode address = Operations.createAddress("system-property", entry.getKey()); + final ModelNode op = Operations.createAddOperation(address); + op.get("value").set(entry.getValue()); + builder.addStep(op); + } + executeOperation(managementClient, builder.build()); + } + + @Override + public void tearDown(final ManagementClient managementClient, final String containerId) throws Exception { + final Operations.CompositeOperationBuilder builder = Operations.CompositeOperationBuilder.create(); + for (var entry : properties.entrySet()) { + final ModelNode address = Operations.createAddress("system-property", entry.getKey()); + builder.addStep(Operations.createRemoveOperation(address)); + } + executeOperation(managementClient, builder.build()); + } +} diff --git a/integration-tests/junit5-tests/src/test/resources/arquillian.xml b/integration-tests/junit5-tests/src/test/resources/arquillian.xml index c619b203..94b49d1a 100644 --- a/integration-tests/junit5-tests/src/test/resources/arquillian.xml +++ b/integration-tests/junit5-tests/src/test/resources/arquillian.xml @@ -22,7 +22,8 @@ ${jboss.home} ${debug.vm.args} ${jvm.args} - false + + true diff --git a/pom.xml b/pom.xml index 447f9aab..efd99976 100644 --- a/pom.xml +++ b/pom.xml @@ -592,6 +592,11 @@ wildfly-arquillian-container-remote ${project.version} + + org.wildfly.arquillian + wildfly-arquillian-junit-api + ${project.version} + org.wildfly.arquillian wildfly-arquillian-protocol-jmx From 5d6d5dffd1b6d8242486e0b3b5e0a0f1a87844bc Mon Sep 17 00:00:00 2001 From: "James R. Perkins" Date: Thu, 18 Apr 2024 16:38:31 -0700 Subject: [PATCH 4/4] [WFARQ-165] Upgrade wildfly-plugin-tools to 1.1.0.Final. Allow for injection of the ServerManager from the plugin tools. Add some server setup utilities and a DeploymentDescriptors utility. https://issues.redhat.com/browse/WFARQ-165 Signed-off-by: James R. Perkins --- .../container/domain/ArchiveDeployer.java | 9 +- common/pom.xml | 8 +- .../container/ArquillianServerManager.java | 129 ++++++ .../CommonContainerArchiveAppender.java | 3 + .../container/CommonContainerExtension.java | 4 + .../container/CommonDeployableContainer.java | 19 + .../CommonManagedDeployableContainer.java | 36 +- .../ContainerResourceTestEnricher.java | 10 +- .../container/ManagementClient.java | 13 +- .../container/ServerManagerProvider.java | 48 +++ .../setup/ConfigureLoggingSetupTask.java | 266 ++++++++++++ .../setup/ReloadServerSetupTask.java | 78 ++++ .../setup/SnapshotServerSetupTask.java | 158 ++++++++ container-managed/pom.xml | 4 + .../container/managed/AppClientWrapper.java | 4 +- .../test/junit5/InContainerTestAssertion.java | 49 +++ .../test/junit5/InContainerTestCase.java | 4 +- .../ServerManagerInjectionTestCase.java | 58 +++ .../setup/ReloadServerSetupTaskTestCase.java | 107 +++++ .../setup/SnapshotSetupTaskTestCase.java | 173 ++++++++ pom.xml | 22 +- wildfly-testing-tools/pom.xml | 55 +++ .../deployments/DeploymentDescriptors.java | 378 ++++++++++++++++++ .../tools/deployments/IndentingXmlWriter.java | 295 ++++++++++++++ .../DeploymentDescriptorsTest.java | 189 +++++++++ 25 files changed, 2064 insertions(+), 55 deletions(-) create mode 100644 common/src/main/java/org/jboss/as/arquillian/container/ArquillianServerManager.java create mode 100644 common/src/main/java/org/jboss/as/arquillian/container/ServerManagerProvider.java create mode 100644 common/src/main/java/org/jboss/as/arquillian/setup/ConfigureLoggingSetupTask.java create mode 100644 common/src/main/java/org/jboss/as/arquillian/setup/ReloadServerSetupTask.java create mode 100644 common/src/main/java/org/jboss/as/arquillian/setup/SnapshotServerSetupTask.java create mode 100644 integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/InContainerTestAssertion.java create mode 100644 integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/server/ServerManagerInjectionTestCase.java create mode 100644 integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/server/setup/ReloadServerSetupTaskTestCase.java create mode 100644 integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/server/setup/SnapshotSetupTaskTestCase.java create mode 100644 wildfly-testing-tools/pom.xml create mode 100644 wildfly-testing-tools/src/main/java/org/wildfly/testing/tools/deployments/DeploymentDescriptors.java create mode 100644 wildfly-testing-tools/src/main/java/org/wildfly/testing/tools/deployments/IndentingXmlWriter.java create mode 100644 wildfly-testing-tools/src/test/java/org/wildly/testing/tools/deployments/DeploymentDescriptorsTest.java diff --git a/common-domain/src/main/java/org/jboss/as/arquillian/container/domain/ArchiveDeployer.java b/common-domain/src/main/java/org/jboss/as/arquillian/container/domain/ArchiveDeployer.java index ebcbdfdb..36f42cc6 100644 --- a/common-domain/src/main/java/org/jboss/as/arquillian/container/domain/ArchiveDeployer.java +++ b/common-domain/src/main/java/org/jboss/as/arquillian/container/domain/ArchiveDeployer.java @@ -20,6 +20,7 @@ import java.util.Collections; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.Set; import java.util.concurrent.Future; @@ -39,7 +40,6 @@ import org.jboss.logging.Logger; import org.jboss.shrinkwrap.api.Archive; import org.jboss.shrinkwrap.api.exporter.ZipExporter; -import org.wildfly.common.Assert; import org.wildfly.plugin.tools.Deployment; import org.wildfly.plugin.tools.DeploymentManager; import org.wildfly.plugin.tools.DeploymentResult; @@ -56,7 +56,7 @@ * @author James R. Perkins * @since 17-Nov-2010 */ -@SuppressWarnings({ "WeakerAccess", "TypeMayBeWeakened", "DeprecatedIsStillUsed", "deprecation", "unused" }) +@SuppressWarnings({ "WeakerAccess", "TypeMayBeWeakened", "DeprecatedIsStillUsed", "unused" }) public class ArchiveDeployer { private static final Logger log = Logger.getLogger(ArchiveDeployer.class); @@ -77,8 +77,7 @@ public class ArchiveDeployer { */ @Deprecated public ArchiveDeployer(DomainDeploymentManager deploymentManager) { - Assert.checkNotNullParam("deploymentManager", deploymentManager); - this.deploymentManagerDeprecated = deploymentManager; + this.deploymentManagerDeprecated = Objects.requireNonNull(deploymentManager, "The deploymentManager cannot be null"); this.deploymentManager = null; } @@ -88,7 +87,7 @@ public ArchiveDeployer(DomainDeploymentManager deploymentManager) { * @param client the client used to communicate with the server */ public ArchiveDeployer(final ManagementClient client) { - Assert.checkNotNullParam("client", client); + Objects.requireNonNull(client, "The client cannot be null"); deploymentManagerDeprecated = null; this.deploymentManager = DeploymentManager.Factory.create(client.getControllerClient()); } diff --git a/common/pom.xml b/common/pom.xml index 19248d37..75f85ff9 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -65,6 +65,10 @@ org.jboss.remotingjmx remoting-jmx + + org.wildfly.arquillian + wildfly-testing-tools + org.wildfly.core wildfly-controller-client @@ -81,10 +85,6 @@ org.jboss.shrinkwrap.descriptors shrinkwrap-descriptors-impl-base - - org.wildfly.common - wildfly-common - diff --git a/common/src/main/java/org/jboss/as/arquillian/container/ArquillianServerManager.java b/common/src/main/java/org/jboss/as/arquillian/container/ArquillianServerManager.java new file mode 100644 index 00000000..4e2e5f66 --- /dev/null +++ b/common/src/main/java/org/jboss/as/arquillian/container/ArquillianServerManager.java @@ -0,0 +1,129 @@ +/* + * 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.container; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import org.jboss.as.controller.client.ModelControllerClient; +import org.jboss.dmr.ModelNode; +import org.wildfly.plugin.tools.ContainerDescription; +import org.wildfly.plugin.tools.DeploymentManager; +import org.wildfly.plugin.tools.server.ServerManager; + +/** + * A delegating implementation of a {@link ServerManager} which does not allow {@link #shutdown()} attempts. If either + * shutdown method is invoked, an {@link UnsupportedOperationException} will be thrown. + * + * @author James R. Perkins + */ +class ArquillianServerManager implements ServerManager { + + private final ServerManager delegate; + + ArquillianServerManager(ServerManager delegate) { + this.delegate = delegate; + } + + @Override + public ModelControllerClient client() { + return delegate.client(); + } + + @Override + public String serverState() { + return delegate.serverState(); + } + + @Override + public String launchType() { + return delegate.launchType(); + } + + @Override + public String takeSnapshot() throws IOException { + return delegate.takeSnapshot(); + } + + @Override + public ContainerDescription containerDescription() throws IOException { + return delegate.containerDescription(); + } + + @Override + public DeploymentManager deploymentManager() { + return delegate.deploymentManager(); + } + + @Override + public boolean isRunning() { + return delegate.isRunning(); + } + + @Override + public boolean waitFor(final long startupTimeout) throws InterruptedException { + return delegate.waitFor(startupTimeout); + } + + @Override + public boolean waitFor(final long startupTimeout, final TimeUnit unit) throws InterruptedException { + return delegate.waitFor(startupTimeout, unit); + } + + /** + * Throws an {@link UnsupportedOperationException} as the server is managed by Arquillian + * + * @throws UnsupportedOperationException as the server is managed by Arquillian + */ + @Override + public void shutdown() throws UnsupportedOperationException { + throw new UnsupportedOperationException("Cannot shutdown a server managed by Arquillian"); + } + + /** + * Throws an {@link UnsupportedOperationException} as the server is managed by Arquillian + * + * @throws UnsupportedOperationException s the server is managed by Arquillian + */ + @Override + public void shutdown(final long timeout) throws UnsupportedOperationException { + throw new UnsupportedOperationException("Cannot shutdown a server managed by Arquillian"); + } + + @Override + public void executeReload() throws IOException { + delegate.executeReload(); + } + + @Override + public void executeReload(final ModelNode reloadOp) throws IOException { + delegate.executeReload(reloadOp); + } + + @Override + public void reloadIfRequired() throws IOException { + delegate.reloadIfRequired(); + } + + @Override + public void reloadIfRequired(final long timeout, final TimeUnit unit) throws IOException { + delegate.reloadIfRequired(timeout, unit); + } +} diff --git a/common/src/main/java/org/jboss/as/arquillian/container/CommonContainerArchiveAppender.java b/common/src/main/java/org/jboss/as/arquillian/container/CommonContainerArchiveAppender.java index b5ce0c84..de9d3d17 100644 --- a/common/src/main/java/org/jboss/as/arquillian/container/CommonContainerArchiveAppender.java +++ b/common/src/main/java/org/jboss/as/arquillian/container/CommonContainerArchiveAppender.java @@ -22,6 +22,7 @@ import org.jboss.arquillian.container.test.spi.client.deployment.AuxiliaryArchiveAppender; import org.jboss.as.arquillian.api.ServerSetup; import org.jboss.as.arquillian.api.ServerSetupTask; +import org.jboss.as.arquillian.setup.ConfigureLoggingSetupTask; import org.jboss.shrinkwrap.api.Archive; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.asset.StringAsset; @@ -42,6 +43,8 @@ public Archive createAuxiliaryArchive() { // shouldn't really be used for in-container tests. .addClasses(ServerSetupTask.class, ServerSetup.class) .addClasses(ManagementClient.class) + // Add the setup task implementations + .addPackage(ConfigureLoggingSetupTask.class.getPackage()) // Adds wildfly-plugin-tools, this exception itself is explicitly needed .addPackages(true, OperationExecutionException.class.getPackage()) .setManifest(new StringAsset("Manifest-Version: 1.0\n" diff --git a/common/src/main/java/org/jboss/as/arquillian/container/CommonContainerExtension.java b/common/src/main/java/org/jboss/as/arquillian/container/CommonContainerExtension.java index 7e92e4bf..12c0763e 100644 --- a/common/src/main/java/org/jboss/as/arquillian/container/CommonContainerExtension.java +++ b/common/src/main/java/org/jboss/as/arquillian/container/CommonContainerExtension.java @@ -39,6 +39,10 @@ public void register(final ExtensionBuilder builder) { builder.service(DeploymentExceptionTransformer.class, ExceptionTransformer.class); builder.service(ResourceProvider.class, ArchiveDeployerProvider.class); builder.service(ResourceProvider.class, ManagementClientProvider.class); + // Set up the providers for client injection of a ServerManager. We will not support injection for in-container + // tests. The main reason for this is we likely shouldn't be managing a servers lifecycle from a deployment. In + // some cases it may not even work. + builder.service(ResourceProvider.class, ServerManagerProvider.class); builder.service(TestEnricher.class, ContainerResourceTestEnricher.class); builder.service(AuxiliaryArchiveAppender.class, CommonContainerArchiveAppender.class); diff --git a/common/src/main/java/org/jboss/as/arquillian/container/CommonDeployableContainer.java b/common/src/main/java/org/jboss/as/arquillian/container/CommonDeployableContainer.java index d73f0959..a2fe2889 100644 --- a/common/src/main/java/org/jboss/as/arquillian/container/CommonDeployableContainer.java +++ b/common/src/main/java/org/jboss/as/arquillian/container/CommonDeployableContainer.java @@ -46,6 +46,8 @@ import org.jboss.shrinkwrap.api.Archive; import org.jboss.shrinkwrap.descriptor.api.Descriptor; import org.wildfly.plugin.tools.ContainerDescription; +import org.wildfly.plugin.tools.server.ServerManager; +import org.wildfly.plugin.tools.server.StandaloneManager; /** * A JBossAS deployable container @@ -72,6 +74,11 @@ public abstract class CommonDeployableContainer jndiContext; + @Inject + @ContainerScoped + // Protected scope so the CommonManagedDeployableContainer can set it as well + protected InstanceProducer serverManagerProducer; + private final StandaloneDelegateProvider mccProvider = new StandaloneDelegateProvider(); private ManagementClient managementClient = null; private ContainerDescription containerDescription = null; @@ -122,6 +129,18 @@ public final void start() throws LifecycleException { } mccProvider.setDelegate(ModelControllerClient.Factory.create(clientConfigBuilder.build())); + // If we are not a CommonManagedDeployableContainer we still need the ServerManager + if (!(this instanceof CommonManagedDeployableContainer)) { + // Set up the server manager attempting to discover the process for monitoring purposes. We need the + // server manager regardless of whether we are in charge of the lifecycle or not. + final StandaloneManager serverManager = ServerManager.builder() + .client(getManagementClient().getControllerClient()) + // Note this won't work on Windows, but should work on other platforms + .process(ServerManager.findProcess().orElse(null)) + .standalone(); + serverManagerProducer.set(new ArquillianServerManager(serverManager)); + } + try { final Properties jndiProps = new Properties(); jndiProps.setProperty(Context.URL_PKG_PREFIXES, JBOSS_URL_PKG_PREFIX); diff --git a/common/src/main/java/org/jboss/as/arquillian/container/CommonManagedDeployableContainer.java b/common/src/main/java/org/jboss/as/arquillian/container/CommonManagedDeployableContainer.java index 7fa10948..96865722 100644 --- a/common/src/main/java/org/jboss/as/arquillian/container/CommonManagedDeployableContainer.java +++ b/common/src/main/java/org/jboss/as/arquillian/container/CommonManagedDeployableContainer.java @@ -17,7 +17,6 @@ import static org.wildfly.core.launcher.ProcessHelper.addShutdownHook; import static org.wildfly.core.launcher.ProcessHelper.destroyProcess; -import static org.wildfly.core.launcher.ProcessHelper.processHasDied; import java.io.IOException; import java.io.InputStream; @@ -33,6 +32,8 @@ import org.jboss.logging.Logger; import org.wildfly.core.launcher.CommandBuilder; import org.wildfly.core.launcher.Launcher; +import org.wildfly.plugin.tools.server.ServerManager; +import org.wildfly.plugin.tools.server.StandaloneManager; /** * A deployable container that manages a {@linkplain Process process}. @@ -57,6 +58,13 @@ protected void startInternal() throws LifecycleException { final T config = getContainerConfiguration(); if (isServerRunning(config)) { if (config.isAllowConnectingToRunningServer()) { + // Set up the server manager attempting to discover the process for monitoring purposes. We need the + // server manager regardless of whether we are in charge of the lifecycle or not. + final StandaloneManager serverManager = ServerManager.builder() + .client(getManagementClient().getControllerClient()) + .process(ServerManager.findProcess().orElse(null)) + .standalone(); + serverManagerProducer.set(new ArquillianServerManager(serverManager)); return; } else { failDueToRunning(config); @@ -73,33 +81,19 @@ protected void startInternal() throws LifecycleException { final Process process = Launcher.of(commandBuilder).setRedirectErrorStream(true).launch(); new Thread(new ConsoleConsumer(process, config.isOutputToConsole())).start(); shutdownThread = addShutdownHook(process); + final StandaloneManager serverManager = ServerManager.builder() + .client(getManagementClient().getControllerClient()) + .process(process) + .standalone(); long startupTimeout = config.getStartupTimeoutInSeconds(); - long timeout = startupTimeout * 1000; - boolean serverAvailable = false; - long sleep = 1000; - while (timeout > 0 && !serverAvailable) { - long before = System.currentTimeMillis(); - serverAvailable = getManagementClient().isServerInRunningState(); - timeout -= (System.currentTimeMillis() - before); - if (!serverAvailable) { - if (processHasDied(process)) { - final String msg = String.format( - "The java process starting the managed server exited unexpectedly with code [%d]", - process.exitValue()); - throw new LifecycleException(msg); - } - Thread.sleep(sleep); - timeout -= sleep; - sleep = Math.max(sleep / 2, 100); - } - } - if (!serverAvailable) { + if (!serverManager.waitFor(startupTimeout, TimeUnit.SECONDS)) { destroyProcess(process); throw new TimeoutException(String.format("Managed server was not started within [%d] s", startupTimeout)); } timeoutSupported = isOperationAttributeSupported("shutdown", "timeout"); this.process = process; + serverManagerProducer.set(new ArquillianServerManager(serverManager)); } catch (LifecycleException e) { throw e; diff --git a/common/src/main/java/org/jboss/as/arquillian/container/ContainerResourceTestEnricher.java b/common/src/main/java/org/jboss/as/arquillian/container/ContainerResourceTestEnricher.java index 31ea2700..d8e4ca05 100644 --- a/common/src/main/java/org/jboss/as/arquillian/container/ContainerResourceTestEnricher.java +++ b/common/src/main/java/org/jboss/as/arquillian/container/ContainerResourceTestEnricher.java @@ -34,6 +34,7 @@ import org.jboss.arquillian.core.api.annotation.Inject; import org.jboss.arquillian.test.spi.TestEnricher; import org.jboss.as.arquillian.api.ContainerResource; +import org.wildfly.plugin.tools.server.ServerManager; /** * Test enricher that allows for injection of remote JNDI context into @RunAsClient test cases. @@ -51,6 +52,9 @@ public class ContainerResourceTestEnricher implements TestEnricher { @Inject private Instance managementClient; + @Inject + private Instance serverManager; + /* * (non-Javadoc) * @@ -123,6 +127,8 @@ private Object lookup(Class type, ContainerResource resource, Annotation... q return lookupContext(type, resource, qualifiers); } else if (ManagementClient.class.isAssignableFrom(type)) { return managementClient.get(); + } else if (ServerManager.class.isAssignableFrom(type)) { + return serverManager.get(); } else { throw new RuntimeException("@ContainerResource an unknown type " + resource.value()); @@ -177,8 +183,4 @@ private Annotation[] filterAnnotations(Annotation[] annotations) { } return filtered.toArray(new Annotation[0]); } - - private interface ContainerResourceProvider { - Object lookup(Class type, ContainerResource resource, Annotation... qualifiers); - } } diff --git a/common/src/main/java/org/jboss/as/arquillian/container/ManagementClient.java b/common/src/main/java/org/jboss/as/arquillian/container/ManagementClient.java index 909fd97d..ce24cf28 100644 --- a/common/src/main/java/org/jboss/as/arquillian/container/ManagementClient.java +++ b/common/src/main/java/org/jboss/as/arquillian/container/ManagementClient.java @@ -44,6 +44,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -83,7 +84,6 @@ import org.jboss.dmr.ModelType; import org.jboss.dmr.Property; import org.jboss.logging.Logger; -import org.wildfly.common.Assert; /** * A helper class to join management related operations, like extract sub system ip/port (web/jmx) @@ -131,10 +131,7 @@ public class ManagementClient implements Closeable { public ManagementClient(ModelControllerClient client, final String mgmtAddress, final int managementPort, final String protocol) { - if (client == null) { - throw new IllegalArgumentException("Client must be specified"); - } - this.client = client; + this.client = Objects.requireNonNull(client, "Client must not be null"); this.mgmtAddress = mgmtAddress; this.mgmtPort = managementPort; this.mgmtProtocol = protocol; @@ -142,11 +139,7 @@ public ManagementClient(ModelControllerClient client, final String mgmtAddress, } public ManagementClient(ModelControllerClient client, final CommonContainerConfiguration config) { - if (client == null) { - throw new IllegalArgumentException("Client must be specified"); - } - Assert.checkNotNullParam("config", config); - this.client = client; + this.client = Objects.requireNonNull(client, "Client must not be null"); this.mgmtAddress = config.getManagementAddress(); this.mgmtPort = config.getManagementPort(); this.mgmtProtocol = config.getManagementProtocol(); diff --git a/common/src/main/java/org/jboss/as/arquillian/container/ServerManagerProvider.java b/common/src/main/java/org/jboss/as/arquillian/container/ServerManagerProvider.java new file mode 100644 index 00000000..a9b5111d --- /dev/null +++ b/common/src/main/java/org/jboss/as/arquillian/container/ServerManagerProvider.java @@ -0,0 +1,48 @@ +/* + * 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.container; + +import java.lang.annotation.Annotation; + +import org.jboss.arquillian.core.api.Instance; +import org.jboss.arquillian.core.api.annotation.Inject; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.wildfly.plugin.tools.server.ServerManager; + +/** + * A provider for {@link ArquillianResource} injection of a {@link ServerManager}. + * + * @author James R. Perkins + */ +public class ServerManagerProvider extends AbstractTargetsContainerProvider { + + @Inject + private Instance serverManager; + + @Override + public boolean canProvide(final Class type) { + return ServerManager.class.isAssignableFrom(type); + } + + @Override + public Object doLookup(final ArquillianResource resource, final Annotation... qualifiers) { + return serverManager.get(); + } +} diff --git a/common/src/main/java/org/jboss/as/arquillian/setup/ConfigureLoggingSetupTask.java b/common/src/main/java/org/jboss/as/arquillian/setup/ConfigureLoggingSetupTask.java new file mode 100644 index 00000000..c2514731 --- /dev/null +++ b/common/src/main/java/org/jboss/as/arquillian/setup/ConfigureLoggingSetupTask.java @@ -0,0 +1,266 @@ +/* + * 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.setup; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.jboss.as.arquillian.api.ServerSetupTask; +import org.jboss.as.arquillian.container.ManagementClient; +import org.jboss.as.controller.client.ModelControllerClient; +import org.jboss.as.controller.client.Operation; +import org.jboss.as.controller.client.helpers.Operations; +import org.jboss.as.controller.client.helpers.Operations.CompositeOperationBuilder; +import org.jboss.dmr.ModelNode; + +/** + * A setup task for configuring loggers for tests. + *

+ * You can define the log levels and logger names in two ways. The first is to pass a map of known logger levels with + * associated logger names to the {@linkplain ConfigureLoggingSetupTask#ConfigureLoggingSetupTask(Map) constructor}. + * The other is via a system property. + *

+ *

+ * To set the levels and logger names via a system property, use a key of {@code wildfly.logging.level.${level}} where + * {@code level} is one of the following: + *

    + *
  1. all
  2. + *
  3. trace
  4. + *
  5. debug
  6. + *
  7. info
  8. + *
  9. warn
  10. + *
  11. error
  12. + *
  13. off
  14. + *
+ * + * The value for each property is a comma delimited set of logger names. + *

+ * Example: + * + *

+ *       {@code -Dwildfly.logging.level.debug=org.wildfly.security,org.jboss.resteasy}
+ *   
+ *

+ *

+ * When using the constructor, the map should consist of a known log level as the key and loggers to be associated with + * that level as the value of the map. Example: + * + *

+ *     {@code
+ *          public class WildFlyLoggingSetupTask extends ConfigurationLoggingSetupTask {
+ *              public WildFlyLoggingSetupTask() {
+ *                  super(Map.of("DEBUG", Set.of("org.wildfly.core", "org.wildfly"}));
+ *              }
+ *          }
+ *     )
+ * 
+ * + * Note that when using the map constructor, you can still use the system property and the maps will be merged. + *

+ * + * @author James R. Perkins + */ +public class ConfigureLoggingSetupTask implements ServerSetupTask { + private final String handlerType; + private final String handlerName; + private final Map> logLevels; + private final BlockingDeque tearDownOps; + + /** + * Creates a new setup task which configures the {@code console-handler=CONSOLE} handler to allow all log levels. + * Then configures, either by modifying or adding, the loggers represented by the values from the system properties. + */ + public ConfigureLoggingSetupTask() { + this(Map.of()); + } + + /** + * Creates a new setup task which configures the handler to allow all log levels. Then configures, either by + * modifying or adding, the loggers represented by system properties. + * + * @param handlerType the handler type which should be modified to ensure it allows all log levels, if {@code null} + * {@code console-handler} will be used + * @param handlerName the name of the handler which should be modified to ensure it allows all log levels, if {@code null} + * {@code console-handler} will be used + */ + public ConfigureLoggingSetupTask(final String handlerType, final String handlerName) { + this(handlerType, handlerName, Map.of()); + } + + /** + * Creates a new setup task which configures the {@code console-handler=CONSOLE} handler to allow all log levels. + * Then configures, either by modifying or adding, the loggers represented by the values of the map passed in. The + * key of the map is the level desired for each logger. + *

+ * The map consists of levels as the key and a set of logger names as the value for each level. + *

+ * + * @param logLevels the map of levels and loggers + */ + public ConfigureLoggingSetupTask(final Map> logLevels) { + this(null, null, logLevels); + } + + /** + * Creates a new setup task which configures the handler to allow all log levels. Then configures, either by + * modifying or adding, the loggers represented by the values of the map passed in. The key of the map is the level + * desired for each logger. + *

+ * If the {@code handlerType} is {@code null} the value will be {@code console-handler}. If the {@code handlerName} + * is {@code null} the value used will be {@code CONSOLE}. + *

+ *

+ * The map consists of levels as the key and a set of logger names as the value for each level. + *

+ * + * @param handlerType the handler type which should be modified to ensure it allows all log levels, if {@code null} + * {@code console-handler} will be used + * @param handlerName the name of the handler which should be modified to ensure it allows all log levels, if {@code null} + * {@code console-handler} will be used + * @param logLevels the map of levels and loggers + */ + public ConfigureLoggingSetupTask(final String handlerType, final String handlerName, + final Map> logLevels) { + this.handlerType = handlerType == null ? "console-handler" : handlerType; + this.handlerName = handlerName == null ? "CONSOLE" : handlerName; + this.logLevels = createMap(logLevels); + this.tearDownOps = new LinkedBlockingDeque<>(); + } + + @Override + public void setup(final ManagementClient client, final String containerId) throws Exception { + final CompositeOperationBuilder builder = CompositeOperationBuilder.create(); + ModelNode address = Operations.createAddress("subsystem", "logging", handlerType, handlerName); + + // We need the current level to reset it when done + ModelNode currentValue = executeOp(client.getControllerClient(), + Operations.createReadAttributeOperation(address, "level")); + if (currentValue.isDefined()) { + tearDownOps.add(Operations.createWriteAttributeOperation(address, "level", currentValue.asString())); + } + + builder.addStep(Operations.createUndefineAttributeOperation(address, "level")); + for (Map.Entry> entry : logLevels.entrySet()) { + for (String logger : entry.getValue()) { + if (logger.isBlank()) { + address = Operations.createAddress("subsystem", "logging", "root-logger", "ROOT"); + } else { + address = Operations.createAddress("subsystem", "logging", "logger", logger); + } + builder.addStep(createLoggerOp(client.getControllerClient(), address, entry.getKey())); + } + } + executeOp(client.getControllerClient(), builder.build()); + } + + @Override + public void tearDown(final ManagementClient managementClient, final String containerId) throws Exception { + // Create a composite operation with all the tear-down operations + final CompositeOperationBuilder builder = CompositeOperationBuilder.create(); + ModelNode removeOp; + while ((removeOp = tearDownOps.pollFirst()) != null) { + builder.addStep(removeOp); + } + executeOp(managementClient.getControllerClient(), builder.build()); + } + + private ModelNode createLoggerOp(final ModelControllerClient client, final ModelNode address, final String level) + throws IOException { + // First check if the logger exists + final ModelNode op = Operations.createReadResourceOperation(address); + final ModelNode result = client.execute(op); + if (Operations.isSuccessfulOutcome(result)) { + // Get the current level from te result + final ModelNode loggerConfig = Operations.readResult(result); + if (loggerConfig.hasDefined("level")) { + tearDownOps.add(Operations.createWriteAttributeOperation(address, "level", loggerConfig.get("level") + .asString())); + } + return Operations.createWriteAttributeOperation(address, "level", level); + } + tearDownOps.add(Operations.createRemoveOperation(address)); + final ModelNode addOp = Operations.createAddOperation(address); + addOp.get("level").set(level); + return addOp; + } + + private ModelNode executeOp(final ModelControllerClient client, final ModelNode op) throws IOException { + return executeOp(client, Operation.Factory.create(op)); + } + + private ModelNode executeOp(final ModelControllerClient client, final Operation op) throws IOException { + final ModelNode result = client.execute(op); + if (!Operations.isSuccessfulOutcome(result)) { + throw new RuntimeException(Operations.getFailureDescription(result).asString()); + } + return Operations.readResult(result); + } + + private static Map> createMap(final Map> toMerge) { + // We only allow a known set of levels + final Map> logLevels = new HashMap<>(); + addLoggingConfig(logLevels, "all"); + addLoggingConfig(logLevels, "trace"); + addLoggingConfig(logLevels, "debug"); + addLoggingConfig(logLevels, "info"); + addLoggingConfig(logLevels, "warn"); + addLoggingConfig(logLevels, "error"); + addLoggingConfig(logLevels, "off"); + return Map.copyOf(merge(logLevels, toMerge)); + } + + private static void addLoggingConfig(final Map> map, final String level) { + final String value = System.getProperty("wildfly.logging.level." + level); + if (value != null) { + final Set names = Set.of(value.split(",")); + if (!names.isEmpty()) { + map.put(level.toUpperCase(Locale.ROOT), names); + } + } + } + + private static Map> merge(final Map> map1, final Map> map2) { + final Map> result = new HashMap<>(); + for (var entry : map1.entrySet()) { + result.put(entry.getKey().toUpperCase(Locale.ROOT), entry.getValue()); + } + for (final var entry : map2.entrySet()) { + if (entry.getValue().isEmpty()) { + continue; + } + final String key = entry.getKey().toUpperCase(Locale.ROOT); + if (result.containsKey(key)) { + result.put(key, + Stream.concat(result.get(key).stream(), entry.getValue().stream()) + .collect(Collectors.toSet())); + } else { + result.put(key, Set.copyOf(entry.getValue())); + } + } + return Map.copyOf(result); + } +} diff --git a/common/src/main/java/org/jboss/as/arquillian/setup/ReloadServerSetupTask.java b/common/src/main/java/org/jboss/as/arquillian/setup/ReloadServerSetupTask.java new file mode 100644 index 00000000..f16b4c8a --- /dev/null +++ b/common/src/main/java/org/jboss/as/arquillian/setup/ReloadServerSetupTask.java @@ -0,0 +1,78 @@ +/* + * 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.setup; + +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.as.arquillian.api.ServerSetupTask; +import org.jboss.as.arquillian.container.ManagementClient; +import org.wildfly.plugin.tools.server.ServerManager; + +/** + * @author James R. Perkins + */ +@SuppressWarnings({ "unused", "RedundantThrows" }) +public class ReloadServerSetupTask implements ServerSetupTask { + + @ArquillianResource + private ServerManager serverManager; + + @Override + public final void setup(final ManagementClient managementClient, final String containerId) throws Exception { + try { + doSetup(managementClient, containerId); + } finally { + serverManager.reloadIfRequired(); + } + } + + @Override + public final void tearDown(final ManagementClient managementClient, final String containerId) throws Exception { + try { + doTearDown(managementClient, containerId); + } finally { + serverManager.reloadIfRequired(); + } + } + + /** + * Execute any necessary setup work that needs to happen before the first deployment to the given container. + * + * @param client management client to use to interact with the container + * @param containerId id of the container to which the deployment will be deployed + * + * @throws Exception if a failure occurs + * @see #setup(ManagementClient, String) + */ + protected void doSetup(final ManagementClient client, final String containerId) throws Exception { + } + + /** + * Execute any tear down work that needs to happen after the last deployment associated + * with the given container has been undeployed. + * + * @param managementClient management client to use to interact with the container + * @param containerId id of the container to which the deployment will be deployed + * + * @throws Exception if a failure occurs + * @see #tearDown(ManagementClient, String) + */ + protected void doTearDown(final ManagementClient managementClient, final String containerId) throws Exception { + } +} diff --git a/common/src/main/java/org/jboss/as/arquillian/setup/SnapshotServerSetupTask.java b/common/src/main/java/org/jboss/as/arquillian/setup/SnapshotServerSetupTask.java new file mode 100644 index 00000000..80607f85 --- /dev/null +++ b/common/src/main/java/org/jboss/as/arquillian/setup/SnapshotServerSetupTask.java @@ -0,0 +1,158 @@ +/* + * 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.setup; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.as.arquillian.api.ServerSetupTask; +import org.jboss.as.arquillian.container.ManagementClient; +import org.jboss.as.controller.client.helpers.Operations; +import org.jboss.dmr.ModelNode; +import org.jboss.logging.Logger; +import org.wildfly.plugin.tools.server.ServerManager; + +/** + * A setup task which takes a snapshot of the current configuration. It then invokes the + * {@link #doSetup(ManagementClient, String)} which allows configuration of the running server. On + * {@link #tearDown(ManagementClient, String)} the snapshot server configuration is used to reload the server and + * overwrite the current configuration. + *

+ * This setup tasks should be the first setup tasks if used with other setup tasks. Otherwise, the snapshot will have + * changes from the previous setup tasks. + *

+ *

+ * Note that if during the setup the server gets in a state of {@code reload-required}, then the after the + * {@link #doSetup(ManagementClient, String)} is executed a reload will happen automatically. + *

+ *

+ * If the {@link #doSetup(ManagementClient, String)} fails, the {@link #tearDown(ManagementClient, String)} method will + * be invoked. + *

+ * + * @author James R. Perkins + */ +@SuppressWarnings({ "unused", "RedundantThrows" }) +public class SnapshotServerSetupTask implements ServerSetupTask { + private static final Logger LOGGER = Logger.getLogger(SnapshotServerSetupTask.class); + + private final Map snapshots = new ConcurrentHashMap<>(); + + @ArquillianResource + private ServerManager serverManager; + + @Override + public final void setup(final ManagementClient managementClient, final String containerId) throws Exception { + try { + final String fileName = serverManager.takeSnapshot(); + final AutoCloseable restorer = () -> { + final ModelNode op = Operations.createOperation("reload"); + op.get("server-config").set(fileName); + serverManager.executeReload(op); + serverManager.waitFor(timeout(), TimeUnit.SECONDS); + @SuppressWarnings("resource") + final ModelNode result1 = serverManager.client().execute(Operations.createOperation("write-config")); + if (!Operations.isSuccessfulOutcome(result1)) { + throw new RuntimeException( + "Failed to write config after restoring from snapshot " + Operations.getFailureDescription(result1) + .asString()); + } + }; + snapshots.put(containerId, restorer); + try { + doSetup(managementClient, containerId); + } catch (Throwable e) { + try { + restorer.close(); + } catch (Throwable t) { + LOGGER.warnf(t, "Failed to restore snapshot for %s: %s", getClass().getName(), fileName); + } + throw e; + } + } finally { + serverManager.reloadIfRequired(timeout(), TimeUnit.SECONDS); + } + } + + @Override + public final void tearDown(final ManagementClient managementClient, final String containerId) throws Exception { + try { + beforeRestore(managementClient, containerId); + } finally { + try { + final AutoCloseable snapshot = snapshots.remove(containerId); + if (snapshot != null) { + snapshot.close(); + } + } finally { + nonManagementCleanUp(); + } + } + } + + /** + * Execute any necessary setup work that needs to happen before the first deployment to the given container. + *

+ * If this method throws an exception, the {@link #tearDown(ManagementClient, String)} method will be invoked. + *

+ * + * @param managementClient management client to use to interact with the container + * @param containerId id of the container to which the deployment will be deployed + * + * @throws Exception if a failure occurs + * @see #setup(ManagementClient, String) + */ + protected void doSetup(final ManagementClient managementClient, final String containerId) throws Exception { + } + + /** + * Execute any necessary work required before the restore is completed. As an example removing a messaging queue + * which triggers removing the queue from a remote server. + * + * @param managementClient management client to use to interact with the container + * @param containerId id of the container to which the deployment will be deployed + * + * @throws Exception if a failure occurs + * @see #tearDown(ManagementClient, String) + */ + protected void beforeRestore(final ManagementClient managementClient, final String containerId) throws Exception { + } + + /** + * Allows for cleaning up resources that may have been created during the setup. This is always executed even if the + * {@link #doSetup(ManagementClient, String)} fails for some reason. + * + * @throws Exception if a failure occurs + */ + protected void nonManagementCleanUp() throws Exception { + } + + /** + * The number seconds to wait for the server to reload after the server configuration has been restored or if a + * reload was required in the {@link #doSetup(ManagementClient, String)}. + * + * @return the number of seconds to wait for a reload, the default is 10 seconds + */ + protected long timeout() { + return 10L; + } +} diff --git a/container-managed/pom.xml b/container-managed/pom.xml index a67d358a..266c3c44 100644 --- a/container-managed/pom.xml +++ b/container-managed/pom.xml @@ -34,6 +34,10 @@ org.wildfly.arquillian wildfly-arquillian-common
+ + org.wildfly.plugins + wildfly-plugin-tools + org.jboss.logging jboss-logging diff --git a/container-managed/src/main/java/org/jboss/as/arquillian/container/managed/AppClientWrapper.java b/container-managed/src/main/java/org/jboss/as/arquillian/container/managed/AppClientWrapper.java index 62c6c045..ef1032d1 100644 --- a/container-managed/src/main/java/org/jboss/as/arquillian/container/managed/AppClientWrapper.java +++ b/container-managed/src/main/java/org/jboss/as/arquillian/container/managed/AppClientWrapper.java @@ -35,7 +35,7 @@ import org.jboss.as.arquillian.container.ParameterUtils; import org.jboss.logging.Logger; -import org.wildfly.plugin.tools.ServerHelper; +import org.wildfly.plugin.tools.server.ServerManager; /** * A wrapper for an application client process. Allows interacting with the application client process. @@ -189,7 +189,7 @@ private List getAppClientCommand() { final String jbossHome = config.getJbossHome(); if (jbossHome == null) throw new IllegalArgumentException("jbossHome config property is not set."); - if (!ServerHelper.isValidHomeDirectory(jbossHome)) + if (!ServerManager.isValidHomeDirectory(jbossHome)) throw new IllegalArgumentException("Server directory from config jbossHome doesn't exist: " + jbossHome); final String archiveArg = String.format("%s#%s", archivePath, clientArchiveName); diff --git a/integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/InContainerTestAssertion.java b/integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/InContainerTestAssertion.java new file mode 100644 index 00000000..92601666 --- /dev/null +++ b/integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/InContainerTestAssertion.java @@ -0,0 +1,49 @@ +/* + * 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.wildfly.arquillian.integration.test.junit5; + +import java.security.AccessController; +import java.security.PrivilegedAction; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * @author James R. Perkins + */ +public interface InContainerTestAssertion { + + @Test + default void checkInContainer() { + final PrivilegedAction action = () -> { + final var caller = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .getCallerClass(); + // We should be a modular class loader and the name should be something line deployment.${archive} + Assertions.assertNotNull(caller.getClassLoader()); + Assertions.assertTrue(caller.getClassLoader().toString().contains("deployment.")); + return null; + }; + if (System.getSecurityManager() == null) { + action.run(); + } else { + AccessController.doPrivileged(action); + } + } +} diff --git a/integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/InContainerTestCase.java b/integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/InContainerTestCase.java index 39bf09eb..440dd018 100644 --- a/integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/InContainerTestCase.java +++ b/integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/InContainerTestCase.java @@ -45,7 +45,7 @@ */ @ExtendWith(ArquillianExtension.class) @ServerSetup(InContainerTestCase.SystemPropertyServerSetupTask.class) -public class InContainerTestCase { +public class InContainerTestCase implements InContainerTestAssertion { private static final Map PROPERTIES = new HashMap<>(); @@ -61,7 +61,7 @@ public class InContainerTestCase { @Deployment public static JavaArchive create() { return ShrinkWrap.create(JavaArchive.class) - .addClass(Greeter.class) + .addClasses(Greeter.class, InContainerTestAssertion.class) .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml"); } diff --git a/integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/server/ServerManagerInjectionTestCase.java b/integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/server/ServerManagerInjectionTestCase.java new file mode 100644 index 00000000..0879587b --- /dev/null +++ b/integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/server/ServerManagerInjectionTestCase.java @@ -0,0 +1,58 @@ +/* + * 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.wildfly.arquillian.integration.test.junit5.server; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.wildfly.arquillian.junit.annotations.WildFlyArquillian; +import org.wildfly.plugin.tools.server.ServerManager; + +/** + * @author James R. Perkins + */ +@WildFlyArquillian +@RunAsClient +public class ServerManagerInjectionTestCase { + + @ArquillianResource + private ServerManager serverManager; + + @Deployment + public static WebArchive deployment() { + return ShrinkWrap.create(WebArchive.class, "server-manager-injection.war") + .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml"); + } + + @Test + public void checkInjected() { + Assertions.assertNotNull(serverManager, "The server manager should have been injected."); + } + + @Test + public void checkRunning() { + Assertions.assertTrue(serverManager.isRunning(), "The server should be running"); + } +} diff --git a/integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/server/setup/ReloadServerSetupTaskTestCase.java b/integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/server/setup/ReloadServerSetupTaskTestCase.java new file mode 100644 index 00000000..a9f4c59a --- /dev/null +++ b/integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/server/setup/ReloadServerSetupTaskTestCase.java @@ -0,0 +1,107 @@ +/* + * 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.wildfly.arquillian.integration.test.junit5.server.setup; + +import java.io.IOException; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.as.arquillian.api.ContainerResource; +import org.jboss.as.arquillian.api.ServerSetup; +import org.jboss.as.arquillian.container.ManagementClient; +import org.jboss.as.arquillian.setup.ReloadServerSetupTask; +import org.jboss.as.controller.client.helpers.ClientConstants; +import org.jboss.as.controller.client.helpers.Operations; +import org.jboss.dmr.ModelNode; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.wildfly.arquillian.junit.annotations.WildFlyArquillian; +import org.wildfly.plugin.tools.server.ServerManager; + +/** + * @author James R. Perkins + */ +@WildFlyArquillian +@RunAsClient +@ServerSetup(ReloadServerSetupTaskTestCase.TestSetupTask.class) +public class ReloadServerSetupTaskTestCase { + + public static class TestSetupTask extends ReloadServerSetupTask { + private final ModelNode address = Operations.createAddress("subsystem", "remoting"); + private final String attributeName = "max-inbound-channels"; + private volatile int currentValue; + + @ContainerResource + private ServerManager serverManager; + + @Override + protected void doSetup(final ManagementClient client, final String containerId) throws Exception { + currentValue = executeOperation(client, Operations.createReadAttributeOperation(address, attributeName)).asInt(); + // Increase the current value which should put the server in a state of reload-required + executeOperation(client, Operations.createWriteAttributeOperation(address, attributeName, currentValue + 10)); + // Check the server state is in a reload required state + Assertions.assertEquals(ClientConstants.CONTROLLER_PROCESS_STATE_RELOAD_REQUIRED, serverManager.serverState()); + } + + @Override + protected void doTearDown(final ManagementClient managementClient, final String containerId) throws Exception { + // Reset the old value + executeOperation(managementClient, Operations.createWriteAttributeOperation(address, attributeName, currentValue)); + // Check the server state is in a reload required state + Assertions.assertEquals(ClientConstants.CONTROLLER_PROCESS_STATE_RELOAD_REQUIRED, serverManager.serverState()); + } + } + + @ContainerResource + private static ManagementClient client; + + @Deployment + public static WebArchive deployment() { + return ShrinkWrap.create(WebArchive.class) + .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml"); + } + + @AfterAll + public static void checkControllerState() throws Exception { + // Check the server state is in a reload required state + checkServerStateIsRunning(); + } + + @Test + public void checkServerReloaded() throws Exception { + // Check the server state is not in a reload required state + checkServerStateIsRunning(); + } + + private static void checkServerStateIsRunning() throws IOException { + final ModelNode op = Operations.createReadAttributeOperation(new ModelNode().setEmptyList(), "server-state"); + final ModelNode result = client.getControllerClient().execute(op); + if (Operations.isSuccessfulOutcome(result)) { + Assertions.assertEquals(ClientConstants.CONTROLLER_PROCESS_STATE_RUNNING, Operations.readResult(result) + .asString()); + } else { + Assertions.fail("Checking the server state failed: " + Operations.getFailureDescription(result).asString()); + } + } +} diff --git a/integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/server/setup/SnapshotSetupTaskTestCase.java b/integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/server/setup/SnapshotSetupTaskTestCase.java new file mode 100644 index 00000000..e8195217 --- /dev/null +++ b/integration-tests/junit5-tests/src/test/java/org/wildfly/arquillian/integration/test/junit5/server/setup/SnapshotSetupTaskTestCase.java @@ -0,0 +1,173 @@ +/* + * 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.wildfly.arquillian.integration.test.junit5.server.setup; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Supplier; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.as.arquillian.api.ServerSetup; +import org.jboss.as.arquillian.container.ManagementClient; +import org.jboss.as.arquillian.setup.SnapshotServerSetupTask; +import org.jboss.as.controller.client.helpers.Operations; +import org.jboss.dmr.ModelNode; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.testkit.engine.EngineTestKit; +import org.junit.platform.testkit.engine.EventConditions; +import org.junit.platform.testkit.engine.TestExecutionResultConditions; +import org.wildfly.arquillian.junit.annotations.WildFlyArquillian; + +/** + * @author James R. Perkins + */ +@WildFlyArquillian +@RunAsClient +public class SnapshotSetupTaskTestCase { + private static final String PROPERTY_NAME = SnapshotServerSetupTask.class.getName(); + private static final ModelNode ADDRESS = Operations.createAddress("system-property", PROPERTY_NAME); + private static final Path TEST_FILE = Path.of(System.getProperty("java.io.tmpdir"), "snapshot-test-file.txt"); + + @ArquillianResource + private ManagementClient client; + + @Deployment(testable = false) + public static WebArchive createDeployment() { + return ShrinkWrap.create(WebArchive.class, "snapshot-setup-task-test.war") + .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml"); + } + + @Test + public void checkSnapshotRestored() throws Exception { + final var results = EngineTestKit.engine("junit-jupiter") + .selectors(DiscoverySelectors.selectClass(SuccessfulSnapshotTests.class)) + .execute(); + // Two tests should have been executed successfully + final var testEvents = results.testEvents(); + testEvents.assertThatEvents().haveExactly(2, EventConditions.finishedSuccessfully()); + checkSystemProperty(client, false); + Assertions.assertTrue(Files.notExists(TEST_FILE), + "The test file should have been cleaned up in the nonManagementCleanup()"); + } + + @Test + public void checkSnapshotRestoredRollback() throws Exception { + final var results = EngineTestKit.engine("junit-jupiter") + .selectors(DiscoverySelectors.selectClass(RollbackSnapshotTests.class)) + .execute(); + // No tests should have been executed + final var testEvents = results.testEvents(); + testEvents.assertThatEvents().isEmpty(); + // We should have one failure from the setup task + final var events = results.allEvents(); + events.assertStatistics((stats) -> stats.failed(1L)); + events.assertThatEvents() + .haveAtLeastOne(EventConditions.event( + EventConditions.finishedWithFailure(TestExecutionResultConditions.instanceOf(AssertionError.class)))); + checkSystemProperty(client, false); + } + + private static void checkSystemProperty(final ManagementClient client, final boolean expectSuccess) throws IOException { + final ModelNode op = Operations.createReadAttributeOperation(ADDRESS, "value"); + final ModelNode result = client.getControllerClient().execute(op); + final Supplier msg; + if (expectSuccess) { + msg = () -> "Getting the system property failed: " + Operations.getFailureDescription(result) + .asString(); + } else { + msg = () -> String.format("Expected system property %s to not exist.", PROPERTY_NAME); + } + Assertions.assertTrue((expectSuccess == Operations.isSuccessfulOutcome(result)), msg); + } + + public static class TestSystemPropertySetupTask extends SnapshotServerSetupTask { + @Override + protected void doSetup(final ManagementClient managementClient, final String containerId) throws Exception { + final ModelNode op = Operations.createAddOperation(ADDRESS); + op.get("value").set("present"); + executeOperation(managementClient, op); + Files.createFile(TEST_FILE); + } + + @Override + protected void nonManagementCleanUp() throws Exception { + Files.delete(TEST_FILE); + } + } + + public static class ErrorSetupTask extends SnapshotServerSetupTask { + @Override + protected void doSetup(final ManagementClient managementClient, final String containerId) throws Exception { + final ModelNode op = Operations.createAddOperation(ADDRESS); + op.get("value").set("present"); + executeOperation(managementClient, op); + Files.createFile(TEST_FILE); + Assertions.fail("Failed on purpose"); + } + + @Override + protected void nonManagementCleanUp() throws Exception { + Files.delete(TEST_FILE); + } + } + + @WildFlyArquillian + @RunAsClient + public abstract static class SnapshotTests { + + @ArquillianResource + private ManagementClient client; + + @Deployment(testable = false) + public static WebArchive createDeployment() { + return ShrinkWrap.create(WebArchive.class, "inner-snapshot-setup-task-test.war") + .addClass(SnapshotServerSetupTask.class) + .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml"); + } + + @Test + public void systemPropertyExists() throws Exception { + checkSystemProperty(client, true); + } + + @Test + public void fileExists() { + Assertions.assertTrue(Files.exists(TEST_FILE), () -> "Expected test file to exist: " + TEST_FILE); + } + } + + @ServerSetup(TestSystemPropertySetupTask.class) + public static class SuccessfulSnapshotTests extends SnapshotTests { + + } + + @ServerSetup(ErrorSetupTask.class) + public static class RollbackSnapshotTests extends SnapshotTests { + + } +} diff --git a/pom.xml b/pom.xml index efd99976..5abba857 100644 --- a/pom.xml +++ b/pom.xml @@ -49,12 +49,12 @@ 3.1.0.Final 1.7.36 - 1.7.0.Final 11.0.0.Beta2 5.0.0.Final - 1.0.0.Beta4 + 1.1.0.Final + 1.2.6 2.0.0 4.0.54.Final 7.7.0 @@ -278,6 +278,7 @@ + wildfly-testing-tools common container-bootable container-embedded @@ -311,6 +312,13 @@ pom import + + org.jboss.shrinkwrap + shrinkwrap-bom + ${version.org.jboss.shrinkwrap} + pom + import + jakarta.ejb @@ -552,6 +560,11 @@ ${version.org.testng} + + org.wildfly.arquillian + wildfly-testing-tools + ${project.version} + org.wildfly.arquillian wildfly-arquillian-common @@ -607,11 +620,6 @@ wildfly-arquillian-testenricher-msc ${project.version} - - org.wildfly.common - wildfly-common - ${version.org.wildfly.common.wildfly-common} - org.wildfly.core wildfly-controller-client diff --git a/wildfly-testing-tools/pom.xml b/wildfly-testing-tools/pom.xml new file mode 100644 index 00000000..4ce96015 --- /dev/null +++ b/wildfly-testing-tools/pom.xml @@ -0,0 +1,55 @@ + + + + + 4.0.0 + + org.wildfly.arquillian + wildfly-arquillian-parent + 5.1.0.Beta2-SNAPSHOT + + + wildfly-testing-tools + + + + org.wildfly.core + wildfly-controller-client + + + org.wildfly.plugins + wildfly-plugin-tools + + + org.jboss.shrinkwrap + shrinkwrap-api + + + + + org.junit.jupiter + junit-jupiter + test + + + + \ No newline at end of file diff --git a/wildfly-testing-tools/src/main/java/org/wildfly/testing/tools/deployments/DeploymentDescriptors.java b/wildfly-testing-tools/src/main/java/org/wildfly/testing/tools/deployments/DeploymentDescriptors.java new file mode 100644 index 00000000..9898a0fc --- /dev/null +++ b/wildfly-testing-tools/src/main/java/org/wildfly/testing/tools/deployments/DeploymentDescriptors.java @@ -0,0 +1,378 @@ +/* + * 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.wildfly.testing.tools.deployments; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FilePermission; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.Permission; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.asset.Asset; +import org.jboss.shrinkwrap.api.asset.ByteArrayAsset; +import org.jboss.shrinkwrap.api.container.WebContainer; + +/** + * A utility to generate various deployment descriptors. + * + * @author James R. Perkins + */ +@SuppressWarnings("unused") +public class DeploymentDescriptors { + + private DeploymentDescriptors() { + } + + /** + * Adds a {@code jboss-deployment-structure.xml} file to a deployment with optional dependency additions or + * exclusions. + * + * @param archive the archive to add the {@code jboss-deployment-structure.xml} to + * @param addedModules the modules to add to an archive or an empty set + * @param excludedModules the modules to exclude from an archive or an empty set + * @param the archive type + * + * @return the archive + */ + public static & Archive> T addJBossDeploymentStructure(final T archive, + final Set addedModules, final Set excludedModules) { + return archive.addAsWebInfResource(createJBossDeploymentStructureAsset(addedModules, excludedModules), + "jboss-deployment-structure.xml"); + } + + /** + * Creates a {@code jboss-deployment-structure.xml} file with the optional dependency additions or exclusions. + * + * @param addedModules the modules to add or an empty set + * @param excludedModules the modules to exclude or an empty set + * + * @return a {@code jboss-deployment-structure.xml} asset + */ + public static Asset createJBossDeploymentStructureAsset(final Set addedModules, final Set excludedModules) { + return new ByteArrayAsset(createJBossDeploymentStructure(addedModules, excludedModules)); + } + + /** + * Creates a {@code jboss-deployment-structure.xml} file with the optional dependency additions or exclusions. + * + * @param addedModules the modules to add or an empty set + * @param excludedModules the modules to exclude or an empty set + * + * @return a {@code jboss-deployment-structure.xml} in a byte array + */ + public static byte[] createJBossDeploymentStructure(final Set addedModules, final Set excludedModules) { + XMLStreamWriter writer = null; + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + writer = createWriter(out); + + writer.writeStartDocument("utf-8", "1.0"); + writer.writeStartElement("jboss-deployment-structure"); + + writer.writeStartElement("deployment"); + + if (!addedModules.isEmpty()) { + writer.writeStartElement("dependencies"); + for (String module : addedModules) { + writer.writeStartElement("module"); + writer.writeAttribute("name", module); + writer.writeEndElement(); + } + writer.writeEndElement(); + } + if (!excludedModules.isEmpty()) { + writer.writeStartElement("exclusions"); + for (String module : addedModules) { + writer.writeStartElement("module"); + writer.writeAttribute("name", module); + writer.writeEndElement(); + } + writer.writeEndElement(); + } + + writer.writeEndElement(); + + writer.writeEndElement(); + writer.writeEndDocument(); + writer.flush(); + return out.toByteArray(); + } catch (IOException | XMLStreamException e) { + throw new RuntimeException("Failed to create the jboss-deployment-structure.xml file.", e); + } finally { + if (writer != null) + try { + writer.close(); + } catch (Exception ignore) { + + } + } + } + + /** + * Creates a {@code jboss-web.xml} with the context root provided. + * + * @param contextRoot the context root to use for the deployment + * + * @return a {@code jboss-web.xml} + */ + public static Asset createJBossWebContextRoot(final String contextRoot) { + return createJBossWebXmlAsset(Map.of("context-root", contextRoot)); + } + + /** + * Creates a {@code jboss-web.xml} with the security domain for the deployment. + * + * @param securityDomain the security domain to use for the deployment + * + * @return a {@code jboss-web.xml} + */ + public static Asset createJBossWebSecurityDomain(final String securityDomain) { + return createJBossWebXmlAsset(Map.of("security-domain", securityDomain)); + } + + /** + * Creates a {@code jboss-web.xml} with simple attributes. + * + * @param elements the elements to add where the key is the element name and the value is the elements value + * + * @return a {@code jboss-web.xml} + */ + public static Asset createJBossWebXmlAsset(final Map elements) { + return new ByteArrayAsset(createJBossWebXml(elements)); + } + + /** + * Creates a {@code jboss-web.xml} with simple attributes. + * + * @param elements the elements to add where the key is the element name and the value is the elements value + * + * @return a {@code jboss-web.xml} + */ + public static byte[] createJBossWebXml(final Map elements) { + XMLStreamWriter writer = null; + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + writer = createWriter(out); + + writer.writeStartDocument("utf-8", "1.0"); + writer.writeStartElement("jboss-web"); + + for (var element : elements.entrySet()) { + writer.writeStartElement(element.getKey()); + writer.writeCharacters(element.getValue()); + writer.writeEndElement(); + } + + writer.writeEndElement(); + writer.writeEndDocument(); + writer.flush(); + return out.toByteArray(); + } catch (IOException | XMLStreamException e) { + throw new RuntimeException("Failed to create the jboss-deployment-structure.xml file.", e); + } finally { + if (writer != null) + try { + writer.close(); + } catch (Exception ignore) { + + } + } + } + + /** + * Creates a new asset with the given contents for a {@code permissions.xml} file. + * + * @param permissions the permissions to add to the file + * + * @return an asset with the given contents for a {@code permissions.xml} file + */ + public static Asset createPermissionsXmlAsset(Permission... permissions) { + return new ByteArrayAsset(createPermissionsXml(permissions)); + } + + /** + * Creates a new asset with the given contents for a {@code permissions.xml} file. + * + * @param permissions the permissions to add to the file + * @param additionalPermissions any additional permissions to add to the file + * + * @return an asset with the given contents for a {@code permissions.xml} file + */ + public static Asset createPermissionsXmlAsset(final Iterable permissions, + final Permission... additionalPermissions) { + return new ByteArrayAsset(createPermissionsXml(permissions, additionalPermissions)); + } + + /** + * Creates a new asset with the given contents for a {@code permissions.xml} file. + * + * @param permissions the permissions to add to the file + * + * @return an asset with the given contents for a {@code permissions.xml} file + */ + public static Asset createPermissionsXmlAsset(final Iterable permissions) { + return new ByteArrayAsset(createPermissionsXml(permissions)); + } + + /** + * Creates a new asset with the given contents for a {@code permissions.xml} file. + * + * @param permissions the permissions to add to the file + * + * @return an asset with the given contents for a {@code permissions.xml} file + */ + public static byte[] createPermissionsXml(Permission... permissions) { + return createPermissionsXml(List.of(permissions)); + } + + /** + * Creates a new asset with the given contents for a {@code permissions.xml} file. + * + * @param permissions the permissions to add to the file + * @param additionalPermissions any additional permissions to add to the file + * + * @return an asset with the given contents for a {@code permissions.xml} file + */ + public static byte[] createPermissionsXml(final Iterable permissions, + final Permission... additionalPermissions) { + XMLStreamWriter writer = null; + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + writer = createWriter(out); + + writer.writeStartDocument("utf-8", "1.0"); + writer.writeStartElement("permissions"); + writer.writeNamespace(null, "https://jakarta.ee/xml/ns/jakartaee"); + writer.writeAttribute("version", "10"); + addPermissionXml(writer, permissions); + if (additionalPermissions != null && additionalPermissions.length > 0) { + addPermissionXml(writer, List.of(additionalPermissions)); + } + writer.writeEndElement(); + writer.writeEndDocument(); + writer.flush(); + return out.toByteArray(); + } catch (IOException | XMLStreamException e) { + throw new RuntimeException("Failed to create the permissions.xml file.", e); + } finally { + if (writer != null) + try { + writer.close(); + } catch (Exception ignore) { + + } + } + } + + /** + * This should only be used as a workaround for issues with API's where something like a + * {@link java.util.ServiceLoader} needs access to an implementation. + *

+ * Adds file permissions for every JAR in the modules directory. The {@code module.jar.path} system property + * must be set. + *

+ * + * @param moduleNames the module names to add file permissions for + * + * @return a collection of permissions required + */ + public static Collection addModuleFilePermission(final String... moduleNames) { + final String value = System.getProperty("module.jar.path"); + if (value == null || value.isBlank()) { + return Collections.emptySet(); + } + // Get the module path + final Path moduleDir = Path.of(value); + final Collection result = new ArrayList<>(); + for (String moduleName : moduleNames) { + final Path definedModuleDir = moduleDir.resolve(moduleName.replace('.', File.separatorChar)) + .resolve("main"); + // Find all the JAR's + try (Stream stream = Files.walk(definedModuleDir)) { + stream + .filter((path) -> path.getFileName().toString().endsWith(".jar")) + .map((path) -> new FilePermission(path.toAbsolutePath().toString(), "read")) + .forEach(result::add); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + return result; + } + + /** + * Creates the permissions required for the {@code java.io.tmpdir}. This adds permissions to read the directory, then + * adds permissions for all files and subdirectories of the temporary directory. The actions are used for the latter + * permission. + * + * @param actions the actions required for the temporary directory + * + * @return the permissions required + */ + public static Collection createTempDirPermission(final String actions) { + String tempDir = System.getProperty("java.io.tmpdir"); + // This should never happen, but it's a better error message than an NPE + if (tempDir.charAt(tempDir.length() - 1) != File.separatorChar) { + tempDir += File.separatorChar; + } + return List.of(new FilePermission(tempDir, "read"), new FilePermission(tempDir + "-", actions)); + } + + private static void addPermissionXml(final XMLStreamWriter writer, final Iterable permissions) + throws XMLStreamException { + for (Permission permission : permissions) { + writer.writeStartElement("permission"); + + writer.writeStartElement("class-name"); + writer.writeCharacters(permission.getClass().getName()); + writer.writeEndElement(); + + writer.writeStartElement("name"); + writer.writeCharacters(permission.getName()); + writer.writeEndElement(); + + final String actions = permission.getActions(); + if (actions != null && !actions.isEmpty()) { + writer.writeStartElement("actions"); + writer.writeCharacters(actions); + writer.writeEndElement(); + } + writer.writeEndElement(); + } + } + + private static XMLStreamWriter createWriter(final OutputStream out) throws XMLStreamException { + final XMLOutputFactory factory = XMLOutputFactory.newInstance(); + return new IndentingXmlWriter(factory.createXMLStreamWriter(out, "utf-8")); + } +} diff --git a/wildfly-testing-tools/src/main/java/org/wildfly/testing/tools/deployments/IndentingXmlWriter.java b/wildfly-testing-tools/src/main/java/org/wildfly/testing/tools/deployments/IndentingXmlWriter.java new file mode 100644 index 00000000..212036eb --- /dev/null +++ b/wildfly-testing-tools/src/main/java/org/wildfly/testing/tools/deployments/IndentingXmlWriter.java @@ -0,0 +1,295 @@ +/* + * 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.wildfly.testing.tools.deployments; + +import java.util.Iterator; +import java.util.stream.Stream; + +import javax.xml.namespace.NamespaceContext; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +/** + * @author James R. Perkins + */ +class IndentingXmlWriter implements XMLStreamWriter, XMLStreamConstants { + + private static final String SPACES = " "; + + private final XMLStreamWriter delegate; + private int index; + private int state = START_DOCUMENT; + private boolean indentEnd; + + IndentingXmlWriter(final XMLStreamWriter delegate) { + this.delegate = delegate; + index = 0; + indentEnd = false; + } + + private void indent() throws XMLStreamException { + final int index = this.index; + if (index > 0) { + for (int i = 0; i < index; i++) { + delegate.writeCharacters(SPACES); + } + } + + } + + private void newline() throws XMLStreamException { + delegate.writeCharacters(System.lineSeparator()); + } + + @Override + public void writeStartElement(final String localName) throws XMLStreamException { + newline(); + indent(); + delegate.writeStartElement(localName); + indentEnd = false; + state = START_ELEMENT; + index++; + } + + @Override + public void writeStartElement(final String namespaceURI, final String localName) throws XMLStreamException { + newline(); + indent(); + delegate.writeStartElement(namespaceURI, localName); + indentEnd = false; + state = START_ELEMENT; + index++; + } + + @Override + public void writeStartElement(final String prefix, final String localName, final String namespaceURI) + throws XMLStreamException { + newline(); + indent(); + delegate.writeStartElement(prefix, localName, namespaceURI); + indentEnd = false; + state = START_ELEMENT; + index++; + } + + @Override + public void writeEmptyElement(final String namespaceURI, final String localName) throws XMLStreamException { + newline(); + indent(); + delegate.writeEmptyElement(namespaceURI, localName); + state = END_ELEMENT; + } + + @Override + public void writeEmptyElement(final String prefix, final String localName, final String namespaceURI) + throws XMLStreamException { + newline(); + indent(); + delegate.writeEmptyElement(prefix, localName, namespaceURI); + state = END_ELEMENT; + } + + @Override + public void writeEmptyElement(final String localName) throws XMLStreamException { + newline(); + indent(); + delegate.writeEmptyElement(localName); + state = END_ELEMENT; + } + + @Override + public void writeEndElement() throws XMLStreamException { + index--; + if (state != CHARACTERS || indentEnd) { + newline(); + indent(); + indentEnd = false; + } + delegate.writeEndElement(); + state = END_ELEMENT; + } + + @Override + public void writeEndDocument() throws XMLStreamException { + delegate.writeEndDocument(); + state = END_DOCUMENT; + } + + @Override + public void close() throws XMLStreamException { + delegate.close(); + } + + @Override + public void flush() throws XMLStreamException { + delegate.flush(); + } + + @Override + public void writeAttribute(final String localName, final String value) throws XMLStreamException { + delegate.writeAttribute(localName, value); + } + + @Override + public void writeAttribute(final String prefix, final String namespaceURI, final String localName, final String value) + throws XMLStreamException { + delegate.writeAttribute(prefix, namespaceURI, localName, value); + } + + @Override + public void writeAttribute(final String namespaceURI, final String localName, final String value) + throws XMLStreamException { + delegate.writeAttribute(namespaceURI, localName, value); + } + + @Override + public void writeNamespace(final String prefix, final String namespaceURI) throws XMLStreamException { + delegate.writeNamespace(prefix, namespaceURI); + } + + @Override + public void writeDefaultNamespace(final String namespaceURI) throws XMLStreamException { + delegate.writeDefaultNamespace(namespaceURI); + } + + @Override + public void writeComment(final String data) throws XMLStreamException { + newline(); + indent(); + delegate.writeComment(data); + state = COMMENT; + } + + @Override + public void writeProcessingInstruction(final String target) throws XMLStreamException { + newline(); + indent(); + delegate.writeProcessingInstruction(target); + state = PROCESSING_INSTRUCTION; + } + + @Override + public void writeProcessingInstruction(final String target, final String data) throws XMLStreamException { + newline(); + indent(); + delegate.writeProcessingInstruction(target, data); + state = PROCESSING_INSTRUCTION; + } + + @Override + public void writeCData(final String data) throws XMLStreamException { + delegate.writeCData(data); + state = CDATA; + } + + @Override + public void writeDTD(final String dtd) throws XMLStreamException { + newline(); + indent(); + delegate.writeDTD(dtd); + state = DTD; + } + + @Override + public void writeEntityRef(final String name) throws XMLStreamException { + delegate.writeEntityRef(name); + state = ENTITY_REFERENCE; + } + + @Override + public void writeStartDocument() throws XMLStreamException { + delegate.writeStartDocument(); + newline(); + state = START_DOCUMENT; + } + + @Override + public void writeStartDocument(final String version) throws XMLStreamException { + delegate.writeStartDocument(version); + newline(); + state = START_DOCUMENT; + } + + @Override + public void writeStartDocument(final String encoding, final String version) throws XMLStreamException { + delegate.writeStartDocument(encoding, version); + newline(); + state = START_DOCUMENT; + } + + @Override + public void writeCharacters(final String text) throws XMLStreamException { + indentEnd = false; + boolean first = true; + final Iterator iterator = Stream.of(text.split("\n")).iterator(); + while (iterator.hasNext()) { + final String t = iterator.next(); + // On first iteration if more than one line is required, skip to a new line and indent + if (first && iterator.hasNext()) { + first = false; + newline(); + indent(); + } + delegate.writeCharacters(t); + if (iterator.hasNext()) { + newline(); + indent(); + indentEnd = true; + } + } + state = CHARACTERS; + } + + @Override + public void writeCharacters(final char[] text, final int start, final int len) throws XMLStreamException { + delegate.writeCharacters(text, start, len); + } + + @Override + public String getPrefix(final String uri) throws XMLStreamException { + return delegate.getPrefix(uri); + } + + @Override + public void setPrefix(final String prefix, final String uri) throws XMLStreamException { + delegate.setPrefix(prefix, uri); + } + + @Override + public void setDefaultNamespace(final String uri) throws XMLStreamException { + delegate.setDefaultNamespace(uri); + } + + @Override + public void setNamespaceContext(final NamespaceContext context) throws XMLStreamException { + delegate.setNamespaceContext(context); + } + + @Override + public NamespaceContext getNamespaceContext() { + return delegate.getNamespaceContext(); + } + + @Override + public Object getProperty(final String name) throws IllegalArgumentException { + return delegate.getProperty(name); + } +} diff --git a/wildfly-testing-tools/src/test/java/org/wildly/testing/tools/deployments/DeploymentDescriptorsTest.java b/wildfly-testing-tools/src/test/java/org/wildly/testing/tools/deployments/DeploymentDescriptorsTest.java new file mode 100644 index 00000000..97e15351 --- /dev/null +++ b/wildfly-testing-tools/src/test/java/org/wildly/testing/tools/deployments/DeploymentDescriptorsTest.java @@ -0,0 +1,189 @@ +/* + * 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.wildly.testing.tools.deployments; + +import java.io.InputStream; +import java.net.SocketPermission; +import java.security.Permission; +import java.util.Collection; +import java.util.Map; +import java.util.PropertyPermission; +import java.util.Set; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.wildfly.testing.tools.deployments.DeploymentDescriptors; + +/** + * @author James R. Perkins + */ +public class DeploymentDescriptorsTest { + + @Test + public void jbossWebSecurityDomain() throws Exception { + final String expected = generateJBossWebXml(Map.of("security-domain", "test")); + try (InputStream in = DeploymentDescriptors.createJBossWebSecurityDomain("test").openStream()) { + Assertions.assertEquals(expected, new String(in.readAllBytes())); + } + } + + @Test + public void jbossWebContextRoot() throws Exception { + final String expected = generateJBossWebXml(Map.of("context-root", "/test")); + try (InputStream in = DeploymentDescriptors.createJBossWebContextRoot("/test").openStream()) { + Assertions.assertEquals(expected, new String(in.readAllBytes())); + } + } + + @Test + public void jbossWebXml() throws Exception { + final var elements = Map.of("virtual-host", "localhost", "context-root", "/", "security-domain", "ApplicationDomain"); + final String expected = generateJBossWebXml(elements); + try (InputStream in = DeploymentDescriptors.createJBossWebXmlAsset(elements).openStream()) { + Assertions.assertEquals(expected, new String(in.readAllBytes())); + } + } + + @ParameterizedTest + @MethodSource("moduleArguments") + public void jbossDeploymentStructure(final Set addedModules, final Set excludedModules) { + final String expected = generateJBossDeploymentStructure(addedModules, excludedModules); + Assertions.assertEquals(expected, + new String(DeploymentDescriptors.createJBossDeploymentStructure(addedModules, excludedModules))); + } + + @ParameterizedTest + @MethodSource("permissions") + public void permissionsXml(final Set permissions) { + final String expected = generatePermissionsXml(permissions); + Assertions.assertEquals(expected, + new String(DeploymentDescriptors.createPermissionsXml(permissions))); + } + + static Stream moduleArguments() { + return Stream.of( + Arguments.of(Named.of("addedModules", Set.of("org.wildfly.arquillian", "org.wildfly.arquillian.test")), + Named.of("excludedModules", Set.of())), + Arguments.of(Named.of("addedModules", Set.of("org.wildfly.arquillian", "org.wildfly.arquillian.test")), + Named.of("excludedModules", Set.of("org.jboss.as.logging"))), + Arguments.of(Named.of("addedModules", Set.of()), + Named.of("excludedModules", Set.of("org.jboss.as.logging")))); + } + + static Stream permissions() { + return Stream.of( + Arguments.of(Set.of(new RuntimePermission("test.permissions", "action1"))), + Arguments.of(Set.of(new SocketPermission("localhost", "connect,resolve"), + new PropertyPermission("java.io.tmpdir", "read"), + new PropertyPermission("test.property", "read,write")))); + } + + private static String generateJBossWebXml(final Map elements) { + final StringBuilder xml = new StringBuilder() + .append("") + .append(System.lineSeparator()) + .append(System.lineSeparator()) + .append("") + .append(System.lineSeparator()); + for (var element : elements.entrySet()) { + xml.append(" <") + .append(element.getKey()) + .append('>') + .append(element.getValue()) + .append("") + .append(System.lineSeparator()); + } + xml.append(""); + return xml.toString(); + } + + private static String generateJBossDeploymentStructure(final Set addedModules, final Set excludedModules) { + final StringBuilder xml = new StringBuilder() + .append("") + .append(System.lineSeparator()) + .append(System.lineSeparator()) + .append("") + .append(System.lineSeparator()) + .append(" ") + .append(System.lineSeparator()); + if (!addedModules.isEmpty()) { + xml.append(" ") + .append(System.lineSeparator()); + for (String module : addedModules) { + xml.append(" ") + .append(System.lineSeparator()) + .append(" ") + .append(System.lineSeparator()); + } + xml.append(" ") + .append(System.lineSeparator()); + } + if (!excludedModules.isEmpty()) { + xml.append(" ") + .append(System.lineSeparator()); + for (String module : addedModules) { + xml.append(" ") + .append(System.lineSeparator()) + .append(" ") + .append(System.lineSeparator()); + } + xml.append(" ") + .append(System.lineSeparator()); + } + + xml.append(" ") + .append(System.lineSeparator()) + .append(""); + return xml.toString(); + } + + private static String generatePermissionsXml(final Collection permissions) { + final StringBuilder xml = new StringBuilder() + .append("") + .append(System.lineSeparator()) + .append(System.lineSeparator()) + .append("") + .append(System.lineSeparator()); + for (Permission permission : permissions) { + xml.append(" ") + .append(System.lineSeparator()) + .append(" ").append(permission.getClass().getName()).append("") + .append(System.lineSeparator()) + .append(" ").append(permission.getName()).append("") + .append(System.lineSeparator()); + final String actions = permission.getActions(); + if (actions != null && !actions.isEmpty()) { + xml.append(" ").append(actions).append("") + .append(System.lineSeparator()); + } + xml.append(" ") + .append(System.lineSeparator()); + } + xml.append(""); + return xml.toString(); + } +}