Skip to content

Instantly share code, notes, and snippets.

@ndemengel
Last active January 30, 2017 10:04
Show Gist options
  • Save ndemengel/7d2f612dc2bfcee6e941f71900c6a1bc to your computer and use it in GitHub Desktop.
Save ndemengel/7d2f612dc2bfcee6e941f71900c6a1bc to your computer and use it in GitHub Desktop.
RecomposedThrowable, for when you want to test a very specific error handling case but all you have is a stack trace from your production logs
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.util.Arrays.stream;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableList;
import static java.util.Optional.ofNullable;
/**
* For when you want to test a very specific error handling case but all you
* have is a stack trace from your production logs.
* <pre>
* RecomposedThrowable recomposed = RecomposedThrowable.fromStackTraceString(myStackTraceAsAString);
* // one can just query `recomposed`, or make it a real Throwable:
* Throwable imitation = t.toThrowable();
* // ...
* </pre>
*/
public final class RecomposedThrowable {
private static final String JAVA_METHOD_NAME = "\\p{javaJavaIdentifierPart}+";
private static final String JAVA_FULLY_QUALIFIED_CLASS_NAME = "(?:" + JAVA_METHOD_NAME + "\\.)*\\p{javaJavaIdentifierPart}+";
private static final String LINE_NUMBER = "\\d+";
private static final String JAVA_FILE_NAME = "\\p{javaJavaIdentifierPart}+\\.java";
private static final Pattern STACK_TRACE_LINE = Pattern.compile("\tat (" + JAVA_FULLY_QUALIFIED_CLASS_NAME + ")\\.(" + JAVA_METHOD_NAME + ")" +
"\\((?:Native Method|(" + JAVA_FILE_NAME + ")(?::(" + LINE_NUMBER + "))?)\\)");
private static final Pattern DISCARDED_TRACES_LINE = Pattern.compile("\t... (\\d+) more");
private final String fullyQualifiedName;
private final String message;
private final List<StackTraceElement> stackTrace;
private final RecomposedThrowable cause;
private RecomposedThrowable(String fullyQualifiedName, String message, List<StackTraceElement> stackTrace, RecomposedThrowable cause) {
this.fullyQualifiedName = fullyQualifiedName;
this.message = message;
this.stackTrace = unmodifiableList(stackTrace);
this.cause = cause;
}
public String getFullyQualifiedName() {
return fullyQualifiedName;
}
public Optional<RecomposedThrowable> getCause() {
return ofNullable(cause);
}
public Optional<String> getMessage() {
return ofNullable(message);
}
public List<StackTraceElement> getStackTrace() {
return stackTrace;
}
public StackTraceElement[] getStackTraceAsArray() {
return stackTrace.toArray(new StackTraceElement[stackTrace.size()]);
}
public static RecomposedThrowable fromStackTraceString(String stackTraceString) {
return recomposeThrowable(stackTraceString, new LastTrace());
}
private static RecomposedThrowable recomposeThrowable(String stackTraceString, LastTrace lastTrace) {
String causeMarker = "\nCaused by: ";
int causeIdx = stackTraceString.indexOf(causeMarker);
String throwableStackTrace = stackTraceString.substring(0, causeIdx == -1 ? stackTraceString.length() : causeIdx + 1);
String[] throwableStackTraceLines = throwableStackTrace.split("\n");
String throwableName = readThrowableFullyQualifiedName(throwableStackTraceLines[0]);
String throwableMessage = readThrowableMessage(throwableStackTraceLines[0]);
List<StackTraceElement> stackTrace = readStackTrace(throwableStackTraceLines, lastTrace);
RecomposedThrowable recomposedCause = null;
if (causeIdx != -1) {
String causeStackTraceString = stackTraceString.substring(causeIdx + causeMarker.length());
recomposedCause = recomposeThrowable(causeStackTraceString, lastTrace);
}
return new RecomposedThrowable(throwableName, throwableMessage, stackTrace, recomposedCause);
}
private static String readThrowableFullyQualifiedName(String firstStackTraceLine) {
return firstStackTraceLine.split(":")[0];
}
private static String readThrowableMessage(String firstStackTraceLine) {
int idx = firstStackTraceLine.indexOf(": ");
if (idx == -1) {
return null;
}
return firstStackTraceLine.substring(idx + 2);
}
private static List<StackTraceElement> readStackTrace(String[] throwableStackTraceLines, LastTrace lastTrace) {
List<StackTraceElement> elements = new ArrayList<>();
stream(throwableStackTraceLines).forEach(line -> {
final Matcher regularLineMatcher = STACK_TRACE_LINE.matcher(line);
if (regularLineMatcher.matches()) {
elements.add(toStackTraceElement(regularLineMatcher));
return;
}
final Matcher discardedLineMatcher = DISCARDED_TRACES_LINE.matcher(line);
if (discardedLineMatcher.matches()) {
int numberOfDiscardedElements = Integer.parseInt(discardedLineMatcher.group(1));
elements.addAll(lastTrace.findDiscardedElements(numberOfDiscardedElements));
}
});
lastTrace.set(elements);
return elements;
}
private static StackTraceElement toStackTraceElement(Matcher matcher) {
// native method
if (matcher.group(3) == null) {
return new StackTraceElement(matcher.group(1), matcher.group(2), "NativeMethodAccessorImpl.java", -2);
}
// line number unavavailable
if (matcher.group(4) == null) {
return new StackTraceElement(matcher.group(1), matcher.group(2), matcher.group(3), -1);
}
// all details available
return new StackTraceElement(matcher.group(1), matcher.group(2), matcher.group(3), Integer.parseInt(matcher.group(4)));
}
public ThrowableImitation toThrowable() {
return new ThrowableImitation(this);
}
public <T extends Throwable> T toThrowable(Function<String, T> throwableFactory) {
T throwable = throwableFactory.apply(message);
throwable.setStackTrace(getStackTraceAsArray());
return throwable;
}
public <T extends Throwable> T toThrowable(BiFunction<String, Throwable, T> throwableFactory) {
T throwable = throwableFactory.apply(message, getCause().map(RecomposedThrowable::toThrowable).orElse(null));
throwable.setStackTrace(getStackTraceAsArray());
return throwable;
}
private static class LastTrace {
private List<StackTraceElement> lastTrace;
public void set(List<StackTraceElement> trace) {
this.lastTrace = trace;
}
public List<StackTraceElement> findDiscardedElements(int numberOfDiscardedElements) {
if (lastTrace.size() < numberOfDiscardedElements) {
return emptyList();
}
int startIdx = lastTrace.size() - numberOfDiscardedElements;
return lastTrace.subList(startIdx, lastTrace.size());
}
}
public static class ThrowableImitation extends Throwable {
private static final long serialVersionUID = 8951306043536043146L;
private final RecomposedThrowable source;
private ThrowableImitation(RecomposedThrowable source) {
super(
source.getMessage().orElse(null),
source.getCause().map(RecomposedThrowable::toThrowable).orElse(null)
);
setStackTrace(source.getStackTraceAsArray());
this.source = source;
}
@Override
public synchronized ThrowableImitation getCause() {
return (ThrowableImitation) super.getCause();
}
public String getRealThrowableName() {
return source.getFullyQualifiedName();
}
}
}
import com.hopwork.watson.RecomposedThrowable.ThrowableImitation;
import org.junit.Test;
import java.io.PrintWriter;
import java.io.StringWriter;
import static org.assertj.core.api.Assertions.assertThat;
@SuppressWarnings("OptionalGetWithoutIsPresent")
public class RecomposedThrowableTest {
@Test
public void should_handle_simple_throwable() throws Exception {
// given
Throwable simpleThrowable = new RuntimeException();
String stackTraceAsString = getStackTraceAsString(simpleThrowable);
// when
RecomposedThrowable recomposedThrowable = RecomposedThrowable.fromStackTraceString(stackTraceAsString);
// then
assertThat(recomposedThrowable.getFullyQualifiedName()).isEqualTo("java.lang.RuntimeException");
assertThat(recomposedThrowable.getMessage()).isNotPresent();
assertThat(recomposedThrowable.getStackTraceAsArray()).isEqualTo(simpleThrowable.getStackTrace());
assertThat(recomposedThrowable.getCause()).isNotPresent();
}
@Test
public void should_handle_throwable_with_message() throws Exception {
// given
Throwable throwableWithMessage = new RuntimeException("some message");
String stackTraceAsString = getStackTraceAsString(throwableWithMessage);
// when
RecomposedThrowable recomposedThrowable = RecomposedThrowable.fromStackTraceString(stackTraceAsString);
// then
assertThat(recomposedThrowable.getFullyQualifiedName()).isEqualTo("java.lang.RuntimeException");
assertThat(recomposedThrowable.getMessage()).contains("some message");
assertThat(recomposedThrowable.getStackTraceAsArray()).isEqualTo(throwableWithMessage.getStackTrace());
assertThat(recomposedThrowable.getCause()).isNotPresent();
}
@Test
public void should_handle_throwable_with_cause() throws Exception {
// given
Throwable throwableWithCause = createThrowableWithSpecificTrace(createCauseWithSpecificTrace());
String stackTraceAsString = getStackTraceAsString(throwableWithCause);
// when
RecomposedThrowable recomposedThrowable = RecomposedThrowable.fromStackTraceString(stackTraceAsString);
// then
assertThat(recomposedThrowable.getFullyQualifiedName()).isEqualTo("java.lang.RuntimeException");
assertThat(recomposedThrowable.getStackTraceAsArray()).isEqualTo(throwableWithCause.getStackTrace());
assertThat(recomposedThrowable.getCause()).isPresent();
assertThat(recomposedThrowable.getCause().get().getFullyQualifiedName()).isEqualTo("java.lang.Error");
assertThat(recomposedThrowable.getCause().get().getStackTraceAsArray()).isEqualTo(throwableWithCause.getCause().getStackTrace());
}
@Test
public void should_handle_throwable_with_cause_chain() throws Exception {
// given
Throwable throwableWithCauseChain = createThrowableWithSpecificTrace(createCauseWithCause());
String stackTraceAsString = getStackTraceAsString(throwableWithCauseChain);
// when
RecomposedThrowable recomposedThrowable = RecomposedThrowable.fromStackTraceString(stackTraceAsString);
// then
assertThat(recomposedThrowable.getFullyQualifiedName()).isEqualTo("java.lang.RuntimeException");
assertThat(recomposedThrowable.getStackTraceAsArray()).isEqualTo(throwableWithCauseChain.getStackTrace());
assertThat(recomposedThrowable.getCause()).isPresent();
assertThat(recomposedThrowable.getCause().get().getMessage()).contains("intermediate cause");
assertThat(recomposedThrowable.getCause().get().getFullyQualifiedName()).isEqualTo("java.lang.IllegalStateException");
assertThat(recomposedThrowable.getCause().get().getStackTraceAsArray()).isEqualTo(throwableWithCauseChain.getCause().getStackTrace());
assertThat(recomposedThrowable.getCause().get().getCause()).isPresent();
assertThat(recomposedThrowable.getCause().get().getCause().get().getFullyQualifiedName()).isEqualTo("java.lang.Error");
assertThat(recomposedThrowable.getCause().get().getCause().get().getStackTraceAsArray()).isEqualTo(throwableWithCauseChain.getCause().getCause().getStackTrace());
}
@Test
public void should_handle_throwable_with_cause_and_messages() throws Exception {
// given
Throwable throwableWithCauseAndMessages = new RuntimeException("some message", new RuntimeException("cause message"));
String stackTraceAsString = getStackTraceAsString(throwableWithCauseAndMessages);
// when
RecomposedThrowable recomposedThrowable = RecomposedThrowable.fromStackTraceString(stackTraceAsString);
// then
assertThat(recomposedThrowable.getMessage()).contains("some message");
assertThat(recomposedThrowable.getStackTraceAsArray()).isEqualTo(throwableWithCauseAndMessages.getStackTrace());
assertThat(recomposedThrowable.getCause()).isPresent();
assertThat(recomposedThrowable.getCause().get().getMessage()).contains("cause message");
assertThat(recomposedThrowable.getCause().get().getStackTraceAsArray()).isEqualTo(throwableWithCauseAndMessages.getCause().getStackTrace());
}
@Test
public void should_ignore_suppressed_exceptions() throws Exception {
// given
Throwable cause = createCauseWithSpecificTrace();
cause.addSuppressed(createSuppressedThrowable("sup1"));
Throwable throwableWithCause = createThrowableWithSpecificTrace(cause);
throwableWithCause.addSuppressed(createSuppressedThrowable("sup2"));
throwableWithCause.printStackTrace();
String stackTraceAsString = getStackTraceAsString(throwableWithCause);
// when
RecomposedThrowable recomposedThrowable = RecomposedThrowable.fromStackTraceString(stackTraceAsString);
// then
assertThat(recomposedThrowable.getFullyQualifiedName()).isEqualTo("java.lang.RuntimeException");
assertThat(recomposedThrowable.getStackTraceAsArray()).isEqualTo(throwableWithCause.getStackTrace());
assertThat(recomposedThrowable.getCause()).isPresent();
assertThat(recomposedThrowable.getCause().get().getFullyQualifiedName()).isEqualTo("java.lang.Error");
assertThat(recomposedThrowable.getCause().get().getStackTraceAsArray()).isEqualTo(throwableWithCause.getCause().getStackTrace());
}
@Test
public void should_create_an_imitation_of_original_throwable() throws Exception {
// given
Throwable throwableWithCauseAndMessages = new RuntimeException("some message", new Error("cause message"));
String stackTraceAsString = getStackTraceAsString(throwableWithCauseAndMessages);
// when
RecomposedThrowable recomposedThrowable = RecomposedThrowable.fromStackTraceString(stackTraceAsString);
ThrowableImitation fakeThrowable = recomposedThrowable.toThrowable();
// then
assertThat(fakeThrowable.getRealThrowableName()).isEqualTo("java.lang.RuntimeException");
assertThat(fakeThrowable.getMessage()).isEqualTo("some message");
assertThat(fakeThrowable.getStackTrace()).isEqualTo(throwableWithCauseAndMessages.getStackTrace());
assertThat(fakeThrowable.getCause().getRealThrowableName()).isEqualTo("java.lang.Error");
assertThat(fakeThrowable.getCause().getMessage()).isEqualTo("cause message");
assertThat(fakeThrowable.getCause().getStackTrace()).isEqualTo(throwableWithCauseAndMessages.getCause().getStackTrace());
}
private Throwable createSuppressedThrowable(String message) {
return new IllegalArgumentException(message);
}
public Throwable createThrowableWithSpecificTrace(Throwable cause) {
return createThrowable(cause);
}
public Throwable createThrowable(Throwable cause) {
return new RuntimeException(cause);
}
private Throwable createCauseWithCause() {
Throwable subCause = createCauseWithSpecificTrace();
return createIntermediateCauseWithSpecificTrace(subCause);
}
private Throwable createIntermediateCauseWithSpecificTrace(Throwable subCause) {
return createIntermediateCause(subCause);
}
private IllegalStateException createIntermediateCause(Throwable subCause) {
return new IllegalStateException("intermediate cause", subCause);
}
private Throwable createCauseWithSpecificTrace() {
return createCause();
}
private Throwable createCause() {
return new Error();
}
private static String getStackTraceAsString(Throwable throwable) {
@SuppressWarnings("resource") StringWriter writer = new StringWriter();
throwable.printStackTrace(new PrintWriter(writer));
return writer.toString();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment