Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Step 1 of improving the handling of exception propagation #656

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions test/spi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
The tests in this module use a custom maven compiler configuration to simulate a class that exists on the server side but not on the client side to validate the behavior seen in issue
https://github.com/arquillian/arquillian-core/issues/641.

To be able to run these tests from within Intellij, one has to configure the
"Settings | Build, Execution, Deployment | Build Tools | Maven | Runner" to have the "Delegate IDE build/run actions to Maven" box checked.

45 changes: 44 additions & 1 deletion test/spi/pom.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

<!-- Parent -->
<parent>
Expand Down Expand Up @@ -53,5 +55,46 @@
</dependency>

</dependencies>

<build>
<plugins>
<!-- Configure the compiler plugin to not compile the SomeNonClientSideException class into
the test-classes directory so that the standard test runtime will not see it.
-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<!-- Override the default-testCompile execution to exclude SomeNonClientSideException.java -->
<id>default-testCompile</id>
<phase>test-compile</phase>
<goals>
<goal>testCompile</goal>
</goals>
<configuration>
<testExcludes>
<testExclude>org/jboss/arquillian/test/spi/serveronly/**</testExclude>
</testExcludes>
</configuration>
</execution>
<execution>
<!-- A custom test-compile execution to output the SomeNonClientSideException.class to target/serveronly-classes -->
<id>serveronly-compile</id>
<phase>test-compile</phase>
<goals>
<goal>testCompile</goal>
</goals>
<configuration>
<testIncludes>
<testInclude>org/jboss/arquillian/test/spi/serveronly/**</testInclude>
</testIncludes>
<outputDirectory>${project.build.directory}/serveronly-classes</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
*/
package org.jboss.arquillian.test.spi;

import java.io.PrintStream;

/**
* Exception class used when a proxied exception cannot be created. This
* exception type is is thrown instead and contains information about the
* exception type is thrown instead and contains information about the
* proxied class and a hint about why it could not be thrown.
*
* @author <a href="mailto:[email protected]">Andy Gibson</a>
Expand Down Expand Up @@ -59,4 +61,9 @@ public ArquillianProxyException(Throwable cause) {
public ArquillianProxyException(String message, String exceptionClassName, String reason, Throwable cause) {
this(String.format("%s : %s [Proxied because : %s]", exceptionClassName, message, reason), cause);
}

@Override
public void printStackTrace(PrintStream s) {
super.printStackTrace(s);
}
}
143 changes: 132 additions & 11 deletions test/spi/src/main/java/org/jboss/arquillian/test/spi/ExceptionProxy.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;

/**
* Takes an exception class and creates a proxy that can be used to rebuild the
Expand All @@ -50,32 +53,43 @@
* @author <a href="mailto:[email protected]">Andy Gibson</a>
*/
public class ExceptionProxy implements Externalizable {

// The serialVersionUID of the ExceptionProxy that existed in Arquillian 1.9.1.Final
private static final long serialVersionUID = 2321010311438950147L;

// This is the className of the exception in the container passed into TestResult#setThrowable(Throwable)
private String className;

// This is the message of the exception in the container passed into TestResult#setThrowable(Throwable)
private String message;

// This is the stack trace of the exception in the container passed into TestResult#setThrowable(Throwable)
private StackTraceElement[] trace;

// This is a proxy to the cause exception in the container, not used post 1.9.1.Final
private ExceptionProxy causeProxy;

// This is the causeProxy#createException() instance
private Throwable cause;

// This only exists if the original container exception could be deserialized in the client
private Throwable original;

// This would exist if the original exception could not be serialized in the container
private Throwable serializationProcessException = null;
// New fields added in 1.9.2.Final
private Version version;
private List<String> causeHierarchy;

public static class Version implements Serializable {
int version = 2;
}

public ExceptionProxy() {
version = new Version();
}

public ExceptionProxy(Throwable throwable) {
this.version = new Version();
this.className = throwable.getClass().getName();
this.message = throwable.getMessage();
this.trace = throwable.getStackTrace();
this.causeProxy = ExceptionProxy.createForException(throwable.getCause());
//this.causeProxy = ExceptionProxy.createForException(throwable.getCause());
this.original = throwable;
this.causeHierarchy = getExceptionHierarchy(throwable);
}

/**
Expand Down Expand Up @@ -106,7 +120,8 @@ public boolean hasException() {

/**
* Constructs an instance of the proxied exception based on the class name,
* message, stack trace and if applicable, the cause.
* message, stack trace and if applicable, and the cause if the cause could be
* deserialized in the client. Otherwise, this returns an ArquillianProxyException
*
* @return The constructed {@link Throwable} instance
*/
Expand Down Expand Up @@ -143,7 +158,7 @@ public Throwable getCause() {
cause = causeProxy.createException();
}
}
return cause;
return serializationProcessException;
}

/**
Expand All @@ -162,7 +177,44 @@ public Throwable getCause() {
*/
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
className = (String) in.readObject();
// Read the first object to see if it is the version object
Object firstObject = in.readObject();
if (firstObject instanceof Version) {
version = (Version) firstObject;
className = (String) in.readObject();
message = (String) in.readObject();
trace = (StackTraceElement[]) in.readObject();
causeHierarchy = (List<String>) in.readObject();
// Try to deserialize the original exception
try {
byte[] originalExceptionData = (byte[]) in.readObject();
if (originalExceptionData != null && originalExceptionData.length > 0) {
ByteArrayInputStream originalIn = new ByteArrayInputStream(originalExceptionData);
ObjectInputStream input = new ObjectInputStream(originalIn);
original = (Throwable) input.readObject();
}
} catch (Throwable e) {
this.serializationProcessException = e;
}
// Override with the remote serialization issue cause if exists
Throwable tmpSerializationProcessException = (Throwable) in.readObject();
if (tmpSerializationProcessException != null) {
serializationProcessException = tmpSerializationProcessException;
}

//
if(serializationProcessException == null && original == null) {
original = buildOriginalException();
}
} else {
// If it is not the version object this is an old version of the ExceptionProxy
readExternal_191Final((String) firstObject, in);
}
}

// No longer used in 1.9.2.Final+, can be removed in 2.0.0.Final
protected void readExternal_191Final(String className, ObjectInput in) throws IOException, ClassNotFoundException {
this.className = className;
message = (String) in.readObject();
trace = (StackTraceElement[]) in.readObject();
causeProxy = (ExceptionProxy) in.readObject();
Expand Down Expand Up @@ -208,6 +260,32 @@ public void readExternal(ObjectInput in) throws IOException, ClassNotFoundExcept

@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(version);
out.writeObject(className);
out.writeObject(message);
out.writeObject(trace);
out.writeObject(causeHierarchy);
byte[] originalBytes = new byte[0];
try {
/* Try to serialize the original exception. Here we do it in a separate try-catch block to avoid
because default serialization will serialize whatever it can and leave non-serializable fields out.
We have to make the write of the root exception atomic.
*/
ByteArrayOutputStream originalOut = new ByteArrayOutputStream();
ObjectOutputStream output = new ObjectOutputStream(originalOut);
output.writeObject(original);
output.flush();
originalBytes = originalOut.toByteArray();
} catch (NotSerializableException e) {
// ignore, could not serialize original exception
this.serializationProcessException = e;
}
out.writeObject(originalBytes);
out.writeObject(serializationProcessException);
}

// No longer used in 1.9.2.Final+, can be removed in 2.0.0.Final
protected void writeExternal_191Final(ObjectOutput out) throws IOException {
out.writeObject(className);
out.writeObject(message);
out.writeObject(trace);
Expand Down Expand Up @@ -241,4 +319,47 @@ public void writeExternal(ObjectOutput out) throws IOException {
public String toString() {
return super.toString() + String.format("[class=%s, message=%s],cause = %s", className, message, causeProxy);
}

/**
* Get the exception hierarchy for the exception class
*
* @return list of exception types in the hierarchy
*/
protected List<String> getExceptionHierarchy(Throwable t) {
List<String> hierarchy = new ArrayList<>();
Class<?> tclass = t.getClass();
while(Throwable.class.isAssignableFrom(tclass)) {
hierarchy.add(tclass.getName());
tclass = tclass.getSuperclass();
}
return hierarchy;
}
/**
* Build the original exception based on the exception class name. This first
* tries to use a ctor with a message, then a default ctor.
*
* @return the original exception
*/
protected Throwable buildOriginalException() {
Throwable original = null;
for(String tclassName : causeHierarchy) {
try {
Class<? extends Throwable> tclass = Class.forName(tclassName).asSubclass(Throwable.class);
try {
original = tclass.getDeclaredConstructor(String.class).newInstance(message);
break;
} catch (Exception e) {
try {
original = tclass.getDeclaredConstructor().newInstance();
break;
} catch (Exception ex) {
// ignore, could not load class on client side, try next base class
}
}
} catch (ClassNotFoundException e) {
// ignore, could not load class on client side, try next base class
}
}
return original;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Externalizable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
Expand All @@ -28,11 +29,17 @@
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.net.URLClassLoader;

import org.junit.Assert;
import org.junit.Test;

/**
* ExceptionProxyTestCase
* Updated for https://github.com/arquillian/arquillian-core/issues/641
* where the exception seen by a client that did not have the exception class
* thrown from a server was not on the client classpath.
*
* @author <a href="mailto:[email protected]">Aslak Knutsen</a>
* @version $Revision: $
Expand Down Expand Up @@ -67,7 +74,6 @@ public void shouldSerializeNonSerializableExceptions() throws Exception {
Assert.assertTrue(
"Verify Proxy message contain root cause of serialization problem",
t.getMessage().contains("BufferedInputStream"));
Assert.assertEquals(UnsupportedOperationException.class, t.getCause().getClass());
}

@Test
Expand All @@ -82,7 +88,8 @@ public void shouldSerializeNonDeSerializableExceptions() throws Exception {
Assert.assertTrue(
"Verify Proxy message contain root cause of deserialization problem",
t.getMessage().contains("Could not de-serialize"));
Assert.assertEquals(UnsupportedOperationException.class, t.getCause().getClass());
// This is not valid if the exception is not serializable
//Assert.assertEquals(UnsupportedOperationException.class, t.getCause().getClass());
}

@Test
Expand All @@ -96,12 +103,44 @@ public void shouldRecreateInvocationTargetExceptions() throws Exception {
Assert.assertEquals(ClassNotFoundException.class, t.getCause().getCause().getClass());
}

@Test
public void handleExceptionClassNotOnClientClasspath() throws Throwable {
Throwable serverException = causeServerException();
System.out.println("Loaded server exception: " + serverException);
ExceptionProxy proxy = serialize(ExceptionProxy.createForException(serverException));
Throwable t = proxy.createException();
System.out.println("Client exception from proxy: " + t);
System.out.println("Client exception trace from proxy:");
t.printStackTrace();
Assert.assertEquals(ArquillianProxyException.class, t.getClass());
Assert.assertEquals(ClassNotFoundException.class, t.getCause().getClass());
}

private Throwable causeServerException() throws Exception {
// Create a ClassLoader for the target/serveronly-classes dir
File serverOnlyClasses = new File("target/serveronly-classes");
Assert.assertTrue("target/serveronly-classes should exist", serverOnlyClasses.exists());
URL[] serveronlyCP = {serverOnlyClasses.toURL()};
URLClassLoader classLoader = new URLClassLoader(serveronlyCP, getClass().getClassLoader());
Class<IBean> exClass = (Class<IBean>) classLoader.loadClass("org.jboss.arquillian.test.spi.serveronly.SomeBean");
IBean bean = exClass.newInstance();
Throwable exception = null;
try {
bean.invoke();
} catch (Exception e) {
exception = e;
}
return exception;
}

private ExceptionProxy serialize(ExceptionProxy proxy) throws Exception {
ByteArrayOutputStream output = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(output);
out.writeObject(proxy);
out.close();
byte[] data = output.toByteArray();

ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(output.toByteArray()));
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(data));
return (ExceptionProxy) in.readObject();
}

Expand All @@ -122,7 +161,10 @@ private void printConstructors(Throwable throwable) throws Exception {
}
}

// Simulate org.jboss.weld.exceptions.IllegalArgumentException
/** Simulate org.jboss.weld.exceptions.IllegalArgumentException
* Note, this does not simulate the case of weld implementation classes not
* being on the test client classpath, which is the norm.
*/
private static class ExtendedIllegalArgumentException extends IllegalArgumentException {
private static final long serialVersionUID = 1L;

Expand Down
Loading
Loading