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 extends ServerSetupTask> 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:
*
*
Any subsequent {@code ServerSetupTask}s {@link ServerSetup associated with test class}
* will not be executed.
*
The deployment event that triggered the call to this method will be skipped.
*
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.
*
The {@link #tearDown(ManagementClient, String) tearDown} method for any
* previously executed {@code ServerSetupTask}s {@link ServerSetup associated with test class}
* will be invoked.
*
+ *
+ * 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 extends ServerSetupTask>[] classes = setup.value();
for (Class extends ServerSetupTask> clazz : classes) {
final Constructor extends ServerSetupTask> 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-testkittest
+
+ org.wildfly.arquillian
+ wildfly-arquillian-junit-api
+ test
+ org.wildfly.arquillianwildfly-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.arquillianwildfly-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.remotingjmxremoting-jmx
+
+ org.wildfly.arquillian
+ wildfly-testing-tools
+ org.wildfly.corewildfly-controller-client
@@ -81,10 +85,6 @@
org.jboss.shrinkwrap.descriptorsshrinkwrap-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:
+ *
+ *
all
+ *
trace
+ *
debug
+ *
info
+ *
warn
+ *
error
+ *
off
+ *
+ *
+ * The value for each property is a comma delimited set of logger names.
+ *
+ * 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:
+ *
+ *
+ *
+ * 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.arquillianwildfly-arquillian-common
+
+ org.wildfly.plugins
+ wildfly-plugin-tools
+ org.jboss.loggingjboss-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
+
+ 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.arquillianwildfly-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.corewildfly-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 extends Permission> 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 extends 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
+ *
+ * @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 extends Permission> 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 extends Permission> 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(element.getKey())
+ .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();
+ }
+}