Skip to content

Commit

Permalink
[WFARQ-168] Re-throw exceptions for all errors thrown from a ServerSe…
Browse files Browse the repository at this point in the history
…tupTask.setup.

https://issues.redhat.com/browse/WFARQ-168
Signed-off-by: James R. Perkins <[email protected]>
  • Loading branch information
jamezp committed Apr 17, 2024
1 parent d9649ae commit 28677f4
Show file tree
Hide file tree
Showing 8 changed files with 430 additions and 88 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,25 @@ public interface ServerSetupTask {
* to the given container.
* <p>
* <strong>Note on exception handling:</strong> 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:
* <ol>
* <li>Any subsequent {@code ServerSetupTask}s {@link ServerSetup associated with test class}
* <strong>will not</strong> be executed.</li>
* <li>The deployment event that triggered the call to this method will be skipped.</li>
* <li>The {@link #tearDown(ManagementClient, String) tearDown} method of the instance
* that threw the exception <strong>will not</strong> 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.</li>
* <li>The {@link #tearDown(ManagementClient, String) tearDown} method for any
* previously executed {@code ServerSetupTask}s {@link ServerSetup associated with test class}
* <strong>will</strong> be invoked.</li>
* </ol>
* <p>
* 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.
* </p>
*
* @param managementClient management client to use to interact with the container
* @param containerId id of the container to which the deployment will be deployed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}

Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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<ServerSetupTask> setupTasks;
private final Set<DeploymentDescription> deployments;
private final Container container;
private final ContainerContext containerContext;
private final Instance<ServiceLoader> serviceLoader;
private final Event<EnrichmentEvent> enrichmentEvent;

private ServerSetupTaskHolder(final ManagementClient client, final Container container,
final ContainerContext containerContext,
final Instance<ServiceLoader> serviceLoader,
final Event<EnrichmentEvent> 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);
}
}

Expand Down Expand Up @@ -327,39 +292,18 @@ 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<TestEnricher> testEnrichers = serviceLoader.get().all(TestEnricher.class);
for (TestEnricher enricher : testEnrichers) {
enricher.enrich(task);
}
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;
}
}
}
5 changes: 5 additions & 0 deletions integration-tests/junit5-tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@
<artifactId>junit-platform-testkit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wildfly.arquillian</groupId>
<artifactId>wildfly-arquillian-junit-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wildfly.arquillian</groupId>
<artifactId>wildfly-arquillian-container-managed</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <a href="mailto:[email protected]">James R. Perkins</a>
*/
@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<String> expectedNames = Set.of(names);
final Set<String> allProperties = getSystemProperties()
.stream()
.map(ModelNode::asString)
.collect(Collectors.toCollection(LinkedHashSet<String>::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<ModelNode> 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);
}
}
Loading

0 comments on commit 28677f4

Please sign in to comment.