Last active
January 30, 2017 10:04
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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