diff --git a/src/main/java/org/jboss/logmanager/formatters/Formatters.java b/src/main/java/org/jboss/logmanager/formatters/Formatters.java index 4a5a3500..d33c1ab4 100644 --- a/src/main/java/org/jboss/logmanager/formatters/Formatters.java +++ b/src/main/java/org/jboss/logmanager/formatters/Formatters.java @@ -21,29 +21,22 @@ import static java.lang.Math.max; import static java.lang.Math.min; -import static java.lang.System.getSecurityManager; -import static java.lang.Thread.currentThread; import static java.security.AccessController.doPrivileged; import java.io.PrintWriter; import java.net.InetAddress; -import java.net.URL; import java.net.UnknownHostException; import java.security.AccessController; -import java.security.CodeSource; import java.security.PrivilegedAction; -import java.security.ProtectionDomain; import java.text.SimpleDateFormat; import java.util.ArrayDeque; import java.util.Collections; import java.util.Date; import java.util.Deque; import java.util.HashMap; -import java.util.IdentityHashMap; import java.util.Locale; import java.util.Map; import java.util.Properties; -import java.util.Set; import java.util.TimeZone; import java.util.logging.Formatter; import java.util.logging.Level; @@ -632,7 +625,6 @@ public static FormatStep exceptionFormatStep(final boolean leftJustify, final in * @return the format step */ public static FormatStep exceptionFormatStep(final boolean leftJustify, final int minimumWidth, final boolean truncateBeginning, final int maximumWidth, final String argument, final boolean extended) { - final ThreadLocal entered = new ThreadLocal<>() ; return new JustifyingFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth) { public void renderRaw(final StringBuilder builder, final ExtLogRecord record) { doPrivileged(new PrivilegedAction() { @@ -646,300 +638,15 @@ public Void run() { } catch (NumberFormatException ignore) { } } - final Map cache = extended ? new HashMap() : null; - renderStackTrace(builder, t, cache, extended, depth); + StackTraceFormatter.renderStackTrace(builder, t, extended, depth); } return null; } }); } - - private void renderStackTrace(final StringBuilder builder, final Throwable t, final Map cache, final boolean extended, final int depth) { - builder.append(": ").append(t).append(NEW_LINE); - final StackTraceElement[] stackTrace = t.getStackTrace(); - if (extended) { - for (StackTraceElement element : stackTrace) { - renderExtended(builder, element, cache); - } - } else { - for (StackTraceElement element : stackTrace) { - renderTrivial(builder, element); - } - } - // Use the identity of the throwable to determine uniqueness - final Set seen = Collections.newSetFromMap(new IdentityHashMap()); - seen.add(t); - - // Render suppressed if desired - if (depth != 0) { - renderSuppressed(builder, t, t.getSuppressed(), cache, extended, depth, 0, seen); - } - - // Render the cause - final Throwable cause = t.getCause(); - if (cause != null) { - renderCause(builder, t, cause, cache, extended, depth, 0, seen); - } - } - - private void renderStackTrace(final StringBuilder builder, final String text, final Throwable parent, final Throwable child, - final Map cache, final boolean extended, final int depth, final int count, final Set seen) { - // Add the child to the seen list, the parent should already have been added - seen.add(child); - - final StackTraceElement[] causeStack = child.getStackTrace(); - final StackTraceElement[] currentStack = parent.getStackTrace(); - - int m = causeStack.length - 1; - int n = currentStack.length - 1; - - // Walk the stacks backwards from the end, until we find an element that is different - while (m >= 0 && n >= 0 && causeStack[m].equals(currentStack[n])) { - m--; n--; - } - int framesInCommon = causeStack.length - 1 - m; - - indent(builder, count); - builder.append(text).append(child).append(NEW_LINE); - - if (extended) { - for (int i=0; i <= m; i++) { - // Add the prefix for each line - indent(builder, count); - renderExtended(builder, causeStack[i], cache); - } - } else { - for (int i=0; i <= m; i++) { - // Add the prefix for each line - indent(builder, count); - renderTrivial(builder, causeStack[i]); - } - } - if (framesInCommon != 0) { - indent(builder, count); - builder.append("\t... ").append(framesInCommon).append(" more").append(NEW_LINE); - } - - if (depth != 0) { - renderSuppressed(builder, child, child.getSuppressed(), cache, extended, depth, count, seen); - } - - // Recurse if we have a cause - final Throwable ourCause = child.getCause(); - if (ourCause != null) { - renderCause(builder, child, ourCause, cache, extended, depth, count, seen); - } - } - - private void renderCause(final StringBuilder builder, final Throwable t, final Throwable cause, final Map cache, - final boolean extended, final int depth, final int count, final Set seen) { - renderStackTrace(builder, "Caused by: ", t, cause, cache, extended, depth, count, seen); - } - - private void renderSuppressed(final StringBuilder builder, final Throwable t, final Throwable[] suppressed, final Map cache, - final boolean extended, final int depth, final int count, final Set seen) { - if (suppressed != null && (depth < 0 || depth > count)) { - for (Throwable s : suppressed) { - if (seen.contains(s)) { - builder.append("\t[CIRCULAR REFERENCE:").append(s).append(']').append(NEW_LINE); - } else { - renderStackTrace(builder, "Suppressed: ", t, s, cache, extended, depth, (count + 1), seen); - } - } - } - } - - private void indent(final StringBuilder builder, final int count) { - for (int i = 0; i < count; i++) { - builder.append('\t'); - } - } - - private void renderTrivial(final StringBuilder builder, final StackTraceElement element) { - builder.append("\tat ").append(element).append(NEW_LINE); - } - - private void renderExtended(final StringBuilder builder, final StackTraceElement element, final Map cache) { - builder.append("\tat ").append(element); - final String className = element.getClassName(); - final String cached; - if ((cached = cache.get(className)) != null) { - builder.append(cached).append(NEW_LINE); - return; - } - final int dotIdx = className.lastIndexOf('.'); - if (dotIdx == -1) { - builder.append(NEW_LINE); - return; - } - final String packageName = className.substring(0, dotIdx); - - // try to guess the real Class object - final Class exceptionClass = guessClass(className); - - // now try to guess the real Package object - Package exceptionPackage = null; - if (exceptionClass != null) { - exceptionPackage = exceptionClass.getPackage(); - } - if (exceptionPackage == null) try { - exceptionPackage = Package.getPackage(packageName); - } catch (Throwable t) { - // ignore - } - - // now try to extract the version from the Package - String packageVersion = null; - if (exceptionPackage != null) { - try { - packageVersion = exceptionPackage.getImplementationVersion(); - } catch (Throwable t) { - // ignore - } - if (packageVersion == null) try { - packageVersion = exceptionPackage.getSpecificationVersion(); - } catch (Throwable t) { - // ignore - } - } - - // now try to find the originating resource of the class - URL resource = null; - final SecurityManager sm = getSecurityManager(); - final String classResourceName = className.replace('.', '/') + ".class"; - if (exceptionClass != null) { - try { - if (sm == null) { - final ProtectionDomain protectionDomain = exceptionClass.getProtectionDomain(); - if (protectionDomain != null) { - final CodeSource codeSource = protectionDomain.getCodeSource(); - if (codeSource != null) { - resource = codeSource.getLocation(); - } - } - } else { - resource = doPrivileged(new PrivilegedAction() { - public URL run() { - final ProtectionDomain protectionDomain = exceptionClass.getProtectionDomain(); - if (protectionDomain != null) { - final CodeSource codeSource = protectionDomain.getCodeSource(); - if (codeSource != null) { - return codeSource.getLocation(); - } - } - return null; - } - }); - } - } catch (Throwable t) { - // ignore - } - if (resource == null) try { - final ClassLoader exceptionClassLoader = exceptionClass.getClassLoader(); - if (sm == null) { - resource = exceptionClassLoader == null ? ClassLoader.getSystemResource(classResourceName) : exceptionClassLoader.getResource(classResourceName); - } else { - resource = doPrivileged(new PrivilegedAction() { - public URL run() { - return exceptionClassLoader == null ? ClassLoader.getSystemResource(classResourceName) : exceptionClassLoader.getResource(classResourceName); - } - }); - } - } catch (Throwable t) { - // ignore - } - } - - // now try to extract the JAR name from the resource URL - String jarName = getJarName(resource, classResourceName); - - // finally, render the mess - boolean started = false; - final StringBuilder tagBuilder = new StringBuilder(); - if (jarName != null) { - started = true; - tagBuilder.append(" [").append(jarName).append(':'); - } - if (packageVersion != null) { - if (! started) { - tagBuilder.append(" [:"); - started = true; - } - tagBuilder.append(packageVersion); - } - if (started) { - tagBuilder.append(']'); - final String tag = tagBuilder.toString(); - cache.put(className, tag); - builder.append(tag); - } else { - cache.put(className, ""); - } - builder.append(NEW_LINE); - } - - private Class guessClass(final String name) { - if (entered.get() != null) return null; - entered.set(Boolean.TRUE); - try { - try { - final ClassLoader tccl = currentThread().getContextClassLoader(); - if (tccl != null) return Class.forName(name, false, tccl); - } catch (ClassNotFoundException e) { - // ok, try something else... - } - try { - return Class.forName(name); - } catch (ClassNotFoundException e) { - // ok, try something else... - } - return Class.forName(name, false, null); - } catch (Throwable t) { - return null; - } finally { - entered.remove(); - } - } }; } - static String getJarName(URL resource, String classResourceName) { - if (resource == null) { - return null; - } - - final String path = resource.getPath(); - final String protocol = resource.getProtocol(); - - if ("jar".equals(protocol)) { - // the last path segment before "!/" should be the JAR name - final int sepIdx = path.lastIndexOf("!/"); - if (sepIdx != -1) { - // hit! - final String firstPart = path.substring(0, sepIdx); - // now find the last file separator before the JAR separator - final int lsIdx = Math.max(firstPart.lastIndexOf('/'), firstPart.lastIndexOf('\\')); - return firstPart.substring(lsIdx + 1); - } - } else if ("module".equals(protocol)) { - return resource.getPath(); - } - - // OK, that would have been too easy. Next let's just grab the last piece before the class name - for (int endIdx = path.lastIndexOf(classResourceName); endIdx >= 0; endIdx--) { - char ch = path.charAt(endIdx); - if (ch == '/' || ch == '\\' || ch == '?') { - String firstPart = path.substring(0, endIdx); - int lsIdx = Math.max(firstPart.lastIndexOf('/'), firstPart.lastIndexOf('\\')); - return firstPart.substring(lsIdx + 1); - } - } - - // OK, just use the last segment - final int endIdx = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')); - return path.substring(endIdx + 1); - } - /** * Create a format step which emits the log message resource key (if any) with the given justification rules. * diff --git a/src/main/java/org/jboss/logmanager/formatters/StackTraceFormatter.java b/src/main/java/org/jboss/logmanager/formatters/StackTraceFormatter.java new file mode 100644 index 00000000..7de111eb --- /dev/null +++ b/src/main/java/org/jboss/logmanager/formatters/StackTraceFormatter.java @@ -0,0 +1,362 @@ +/* + * JBoss, Home of Professional Open Source. + * + * Copyright 2017 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.logmanager.formatters; + +import static java.lang.System.getSecurityManager; +import static java.lang.Thread.currentThread; +import static java.security.AccessController.doPrivileged; + +import java.net.URL; +import java.security.CodeSource; +import java.security.PrivilegedAction; +import java.security.ProtectionDomain; +import java.util.Collections; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; + +/** + * Formatter used to format the stack trace of an exception. + * + * @author James R. Perkins + */ +class StackTraceFormatter { + private static final String CAUSED_BY_CAPTION = "Caused by: "; + private static final String SUPPRESSED_CAPTION = "Suppressed: "; + // Used to guard against reentry when attempting to guess the class name + private static final ThreadLocal ENTERED = new ThreadLocal<>(); + + private final Set seen = Collections.newSetFromMap(new IdentityHashMap()); + private final StringBuilder builder; + private final int suppressedDepth; + private final boolean extended; + private final Map cache; + private int suppressedCount; + + private StackTraceFormatter(final StringBuilder builder, final int suppressedDepth, final boolean extended) { + this.builder = builder; + this.suppressedDepth = suppressedDepth; + this.extended = extended; + cache = extended ? new HashMap() : null; + } + + /** + * Writes the stack trace into the builder. + * + * @param builder the string builder ot append the stack trace to + * @param t the throwable to render + * @param extended {@code true} if the stack trace should attempt to resolve the JAR the class is in, otherwise + * {@code false} + * @param suppressedDepth the number of suppressed messages to include + */ + static void renderStackTrace(final StringBuilder builder, final Throwable t, final boolean extended, final int suppressedDepth) { + new StackTraceFormatter(builder, suppressedDepth, extended).renderStackTrace(t); + } + + private void renderStackTrace(final Throwable t) { + // Reset the suppression count + suppressedCount = 0; + // Write the exception message + builder.append(": ").append(t); + newLine(); + + // Write the stack trace for this message + final StackTraceElement[] stackTrace = t.getStackTrace(); + for (StackTraceElement element : stackTrace) { + if (extended) { + renderExtended("", element); + } else { + renderTrivial("", element); + } + } + + // Write any suppressed messages, if required + if (suppressedDepth != 0) { + for (Throwable se : t.getSuppressed()) { + if (suppressedDepth < 0 || suppressedDepth > suppressedCount++) { + renderStackTrace(stackTrace, se, SUPPRESSED_CAPTION, "\t"); + } + } + } + + // Print cause if there is one + final Throwable ourCause = t.getCause(); + if (ourCause != null) { + renderStackTrace(stackTrace, ourCause, CAUSED_BY_CAPTION, ""); + } + } + + private void renderStackTrace(final StackTraceElement[] parentStack, final Throwable child, final String caption, final String prefix) { + if (seen.contains(child)) { + builder.append("\t[CIRCULAR REFERENCE:") + .append(child) + .append(']'); + newLine(); + } else { + seen.add(child); + // Find the unique frames suppressing duplicates + final StackTraceElement[] causeStack = child.getStackTrace(); + int m = causeStack.length - 1; + int n = parentStack.length - 1; + while (m >= 0 && n >= 0 && causeStack[m].equals(parentStack[n])) { + m--; + n--; + } + final int framesInCommon = causeStack.length - 1 - m; + + // Print our stack trace + builder.append(prefix) + .append(caption) + .append(child); + newLine(); + for (int i = 0; i <= m; i++) { + if (extended) { + renderExtended(prefix, causeStack[i]); + } else { + renderTrivial(prefix, causeStack[i]); + } + } + if (framesInCommon != 0) { + builder.append(prefix) + .append("\t... ") + .append(framesInCommon) + .append(" more"); + newLine(); + } + + // Print suppressed exceptions, if any + if (suppressedDepth != 0) { + for (Throwable se : child.getSuppressed()) { + if (suppressedDepth < 0 || suppressedDepth > suppressedCount++) { + renderStackTrace(causeStack, se, SUPPRESSED_CAPTION, prefix + "\t"); + } + } + } + + // Print cause, if any + Throwable ourCause = child.getCause(); + if (ourCause != null) { + renderStackTrace(causeStack, ourCause, CAUSED_BY_CAPTION, prefix); + } + } + } + + private void renderTrivial(final String prefix, final StackTraceElement element) { + builder.append(prefix) + .append("\tat ") + .append(element); + newLine(); + } + + private void renderExtended(final String prefix, final StackTraceElement element) { + builder.append(prefix) + .append("\tat ") + .append(element); + final String className = element.getClassName(); + final String cached; + if ((cached = cache.get(className)) != null) { + builder.append(cached); + newLine(); + return; + } + final int dotIdx = className.lastIndexOf('.'); + if (dotIdx == -1) { + newLine(); + return; + } + final String packageName = className.substring(0, dotIdx); + + // try to guess the real Class object + final Class exceptionClass = guessClass(className); + + // now try to guess the real Package object + Package exceptionPackage = null; + if (exceptionClass != null) { + exceptionPackage = exceptionClass.getPackage(); + } + if (exceptionPackage == null) try { + exceptionPackage = Package.getPackage(packageName); + } catch (Throwable t) { + // ignore + } + + // now try to extract the version from the Package + String packageVersion = null; + if (exceptionPackage != null) { + try { + packageVersion = exceptionPackage.getImplementationVersion(); + } catch (Throwable t) { + // ignore + } + if (packageVersion == null) try { + packageVersion = exceptionPackage.getSpecificationVersion(); + } catch (Throwable t) { + // ignore + } + } + + // now try to find the originating resource of the class + URL resource = null; + final SecurityManager sm = getSecurityManager(); + final String classResourceName = className.replace('.', '/') + ".class"; + if (exceptionClass != null) { + try { + if (sm == null) { + final ProtectionDomain protectionDomain = exceptionClass.getProtectionDomain(); + if (protectionDomain != null) { + final CodeSource codeSource = protectionDomain.getCodeSource(); + if (codeSource != null) { + resource = codeSource.getLocation(); + } + } + } else { + resource = doPrivileged(new PrivilegedAction() { + public URL run() { + final ProtectionDomain protectionDomain = exceptionClass.getProtectionDomain(); + if (protectionDomain != null) { + final CodeSource codeSource = protectionDomain.getCodeSource(); + if (codeSource != null) { + return codeSource.getLocation(); + } + } + return null; + } + }); + } + } catch (Throwable t) { + // ignore + } + if (resource == null) try { + final ClassLoader exceptionClassLoader = exceptionClass.getClassLoader(); + if (sm == null) { + resource = exceptionClassLoader == null ? ClassLoader.getSystemResource(classResourceName) : exceptionClassLoader.getResource(classResourceName); + } else { + resource = doPrivileged(new PrivilegedAction() { + public URL run() { + return exceptionClassLoader == null ? ClassLoader.getSystemResource(classResourceName) : exceptionClassLoader.getResource(classResourceName); + } + }); + } + } catch (Throwable t) { + // ignore + } + } + + // now try to extract the JAR name from the resource URL + final String jarName = getJarName(resource, classResourceName); + + // finally, render the mess + boolean started = false; + final StringBuilder tagBuilder = new StringBuilder(); + if (jarName != null) { + started = true; + tagBuilder.append(" [").append(jarName).append(':'); + } + if (packageVersion != null) { + if (!started) { + tagBuilder.append(" [:"); + started = true; + } + tagBuilder.append(packageVersion); + } + if (started) { + tagBuilder.append(']'); + final String tag = tagBuilder.toString(); + cache.put(className, tag); + builder.append(tag); + } else { + cache.put(className, ""); + } + newLine(); + } + + private void newLine() { + builder.append(System.lineSeparator()); + } + + private static Class guessClass(final String name) { + if (ENTERED.get() != null) return null; + ENTERED.set(Boolean.TRUE); + try { + try { + final ClassLoader tccl = currentThread().getContextClassLoader(); + if (tccl != null) return Class.forName(name, false, tccl); + } catch (ClassNotFoundException e) { + // ok, try something else... + } + try { + return Class.forName(name); + } catch (ClassNotFoundException e) { + // ok, try something else... + } + return Class.forName(name, false, null); + } catch (Throwable t) { + return null; + } finally { + ENTERED.remove(); + } + } + + /** + * Attempts to parse the JAR name from the resource URL. + * + * @param resource the URL for the resource + * @param classResourceName the name of the name of the class within the resource + * + * @return the name of the JAR or module + */ + static String getJarName(URL resource, String classResourceName) { + if (resource == null) { + return null; + } + + final String path = resource.getPath(); + final String protocol = resource.getProtocol(); + + if ("jar".equals(protocol)) { + // the last path segment before "!/" should be the JAR name + final int sepIdx = path.lastIndexOf("!/"); + if (sepIdx != -1) { + // hit! + final String firstPart = path.substring(0, sepIdx); + // now find the last file separator before the JAR separator + final int lsIdx = Math.max(firstPart.lastIndexOf('/'), firstPart.lastIndexOf('\\')); + return firstPart.substring(lsIdx + 1); + } + } else if ("module".equals(protocol)) { + return resource.getPath(); + } + + // OK, that would have been too easy. Next let's just grab the last piece before the class name + for (int endIdx = path.lastIndexOf(classResourceName); endIdx >= 0; endIdx--) { + char ch = path.charAt(endIdx); + if (ch == '/' || ch == '\\' || ch == '?') { + String firstPart = path.substring(0, endIdx); + int lsIdx = Math.max(firstPart.lastIndexOf('/'), firstPart.lastIndexOf('\\')); + return firstPart.substring(lsIdx + 1); + } + } + + // OK, just use the last segment + final int endIdx = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')); + return path.substring(endIdx + 1); + } +} diff --git a/src/test/java/org/jboss/logmanager/formatters/FormattersTests.java b/src/test/java/org/jboss/logmanager/formatters/FormattersTests.java index 1f6a5475..7d32d235 100644 --- a/src/test/java/org/jboss/logmanager/formatters/FormattersTests.java +++ b/src/test/java/org/jboss/logmanager/formatters/FormattersTests.java @@ -36,7 +36,7 @@ public void getJarName_jar() throws MalformedURLException { String classResourceName = "org/alib/foo/Bar.class"; URL resource = new URL(null, "jar:file:/D:/Java/SomeDir/alib-3.14.jar!/" + classResourceName, new DummyURLStreamHandler()); - assertEquals(Formatters.getJarName(resource, classResourceName), "alib-3.14.jar"); + assertEquals(StackTraceFormatter.getJarName(resource, classResourceName), "alib-3.14.jar"); } @Test @@ -44,7 +44,7 @@ public void getJarName_module() throws MalformedURLException { String classResourceName = "org/blib/foo/Bar.class"; URL resource = new URL(null, "module:blib-3.14.jar", new DummyURLStreamHandler()); - assertEquals(Formatters.getJarName(resource, classResourceName), "blib-3.14.jar"); + assertEquals(StackTraceFormatter.getJarName(resource, classResourceName), "blib-3.14.jar"); } @Test @@ -52,7 +52,7 @@ public void getJarName_vfs() throws MalformedURLException { String classResourceName = "org/clib/foo/Bar.class"; URL resource = new URL(null, "vfs:/D:/Java/jboss-as-7.1.1.Final/standalone/deployments/myapp.war/WEB-INF/lib/clib-3.1.4.jar", new DummyURLStreamHandler()); - assertEquals(Formatters.getJarName(resource, classResourceName), "clib-3.1.4.jar"); + assertEquals(StackTraceFormatter.getJarName(resource, classResourceName), "clib-3.1.4.jar"); } @Test @@ -60,7 +60,7 @@ public void getJarName_other1() throws MalformedURLException { String classResourceName = "org/dlib/foo/Bar.class"; URL resource = new URL(null, "foo://bar/" + classResourceName, new DummyURLStreamHandler()); - assertEquals(Formatters.getJarName(resource, classResourceName), ""); + assertEquals(StackTraceFormatter.getJarName(resource, classResourceName), ""); } @Test @@ -68,7 +68,7 @@ public void getJarName_other2() throws MalformedURLException { String classResourceName = "org/elib/foo/Bar.class"; URL resource = new URL(null, "foo://bar/quux/" + classResourceName, new DummyURLStreamHandler()); - assertEquals(Formatters.getJarName(resource, classResourceName), "quux"); + assertEquals(StackTraceFormatter.getJarName(resource, classResourceName), "quux"); } @Test @@ -76,7 +76,7 @@ public void getJarName_other3() throws MalformedURLException { String classResourceName = "org/flib/foo/Bar.class"; URL resource = new URL(null, "foo://bar/baz/quux/" + classResourceName, new DummyURLStreamHandler()); - assertEquals(Formatters.getJarName(resource, classResourceName), "quux"); + assertEquals(StackTraceFormatter.getJarName(resource, classResourceName), "quux"); } } diff --git a/src/test/java/org/jboss/logmanager/formatters/PatternFormatterTests.java b/src/test/java/org/jboss/logmanager/formatters/PatternFormatterTests.java index a99d2c2e..b42ee546 100644 --- a/src/test/java/org/jboss/logmanager/formatters/PatternFormatterTests.java +++ b/src/test/java/org/jboss/logmanager/formatters/PatternFormatterTests.java @@ -238,7 +238,7 @@ public void extendedThrowable() throws Exception { Assert.assertTrue(formatted.contains("cause")); Assert.assertTrue(formatted.contains("level1")); Assert.assertTrue(formatted.contains("suppressedLevel1")); - Assert.assertTrue(formatted.contains("suppressedLevel1a")); + Assert.assertFalse(formatted.contains("suppressedLevel1a")); Assert.assertFalse(formatted.contains("suppressedLevel2")); // Add a circular reference to the cause. This should test both that the caused suppressed exceptions are being diff --git a/src/test/java/org/jboss/logmanager/formatters/StackTraceFormatterTests.java b/src/test/java/org/jboss/logmanager/formatters/StackTraceFormatterTests.java new file mode 100644 index 00000000..d7f2facd --- /dev/null +++ b/src/test/java/org/jboss/logmanager/formatters/StackTraceFormatterTests.java @@ -0,0 +1,199 @@ +/* + * JBoss, Home of Professional Open Source. + * + * Copyright 2017 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.logmanager.formatters; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import org.junit.Assert; +import org.junit.Test; + +/** + * @author James R. Perkins + */ +public class StackTraceFormatterTests { + + @Test + public void compareSimpleStackTrace() { + final RuntimeException e = new RuntimeException(); + final StringWriter writer = new StringWriter(); + e.printStackTrace(new PrintWriter(writer)); + + final StringBuilder sb = new StringBuilder(); + StackTraceFormatter.renderStackTrace(sb, e, false, -1); + + Assert.assertEquals(writer.toString(), sanitize(sb.toString())); + } + + @Test + public void compareCauseStackTrace() { + final RuntimeException e = new RuntimeException("Test Exception", new IllegalStateException("Cause")); + + final StringWriter writer = new StringWriter(); + e.printStackTrace(new PrintWriter(writer)); + + final StringBuilder sb = new StringBuilder(); + StackTraceFormatter.renderStackTrace(sb, e, false, -1); + + Assert.assertEquals(writer.toString(), sanitize(sb.toString())); + } + + @Test + public void compareSuppressedAndCauseStackTrace() { + final RuntimeException r1 = new RuntimeException("Exception 1"); + final RuntimeException r2 = new RuntimeException("Exception 2", r1); + final RuntimeException r3 = new RuntimeException("Exception 3", r2); + + final RuntimeException cause = new RuntimeException("This is the cause", r1); + cause.addSuppressed(r2); + cause.addSuppressed(r3); + + final StringWriter writer = new StringWriter(); + cause.printStackTrace(new PrintWriter(writer)); + + final StringBuilder sb = new StringBuilder(); + StackTraceFormatter.renderStackTrace(sb, cause, false, -1); + + Assert.assertEquals(writer.toString(), sanitize(sb.toString())); + } + + @Test + public void compareNestedSuppressedStackTrace() { + final RuntimeException r1 = new RuntimeException("Exception 1"); + final RuntimeException r2 = new RuntimeException("Exception 2", r1); + final RuntimeException r3 = new RuntimeException("Exception 3", r2); + final IllegalStateException nested = new IllegalStateException("Nested 1"); + nested.addSuppressed(new RuntimeException("Nested 1a")); + r3.addSuppressed(nested); + r3.addSuppressed(new IllegalStateException("Nested 2")); + + final RuntimeException cause = new RuntimeException("This is the cause", r1); + cause.addSuppressed(r2); + cause.addSuppressed(r3); + + final StringWriter writer = new StringWriter(); + cause.printStackTrace(new PrintWriter(writer)); + + final StringBuilder sb = new StringBuilder(); + StackTraceFormatter.renderStackTrace(sb, cause, false, -1); + + Assert.assertEquals(writer.toString(), sanitize(sb.toString())); + } + + @Test + public void compareMultiNestedSuppressedStackTrace() { + final Throwable cause = createMultiNestedCause(); + + final StringWriter writer = new StringWriter(); + cause.printStackTrace(new PrintWriter(writer)); + + final StringBuilder sb = new StringBuilder(); + StackTraceFormatter.renderStackTrace(sb, cause, false, -1); + + Assert.assertEquals(writer.toString(), sanitize(sb.toString())); + } + + @Test + public void compareMultiNestedSuppressedAndNestedCauseStackTrace() { + final Throwable rootCause = createMultiNestedCause(); + final RuntimeException cause = new RuntimeException("This is the parent", rootCause); + + final StringWriter writer = new StringWriter(); + cause.printStackTrace(new PrintWriter(writer)); + + final StringBuilder sb = new StringBuilder(); + StackTraceFormatter.renderStackTrace(sb, cause, false, -1); + + Assert.assertEquals(writer.toString(), sanitize(sb.toString())); + } + + @Test + public void testNestedSuppressStackTraceDepth() { + // Test that all messages exist + testDepth(-1); + // Now test up to 11 messages + for (int i = 0; i < 12; i++) { + testDepth(i); + } + } + + private void testDepth(final int depth) { + final Throwable cause = createMultiNestedCause(); + + final StringBuilder sb = new StringBuilder(); + StackTraceFormatter.renderStackTrace(sb, cause, false, depth); + + String msg = sb.toString(); + + // Check the buffer for suppressed messages, should only have Suppressed 1 + checkMessage(msg, "Suppressed 1", depth > 0, depth); + checkMessage(msg, "Nested 1", depth > 1, depth); + checkMessage(msg, "Nested 1a", depth > 2, depth); + checkMessage(msg, "Nested 1-2", depth > 3, depth); + checkMessage(msg, "Suppressed 2", depth > 4, depth); + checkMessage(msg, "Nested 2", depth > 5, depth); + checkMessage(msg, "Nested 2a", depth > 6, depth); + checkMessage(msg, "Nested 2-2", depth > 7, depth); + checkMessage(msg, "Suppressed 3", depth > 8, depth); + checkMessage(msg, "Nested 3", depth > 9, depth); + checkMessage(msg, "Nested 3a", depth > 10, depth); + checkMessage(msg, "Nested 3-2", depth > 11, depth); + } + + private void checkMessage(final String msg, final String text, final boolean shouldExist, final int depth) { + final boolean test = (shouldExist || depth < 0); + Assert.assertEquals(String.format("Depth %d should %s contained \"%s\": %s", depth, (test ? "have" : "not have"), text, msg), msg.contains(text), test); + } + + private Throwable createMultiNestedCause() { + final RuntimeException suppressed1 = new RuntimeException("Suppressed 1"); + final IllegalStateException nested1 = new IllegalStateException("Nested 1"); + nested1.addSuppressed(new RuntimeException("Nested 1a")); + suppressed1.addSuppressed(nested1); + suppressed1.addSuppressed(new IllegalStateException("Nested 1-2")); + + + final RuntimeException suppressed2 = new RuntimeException("Suppressed 2"); + final IllegalStateException nested2 = new IllegalStateException("Nested 2"); + nested2.addSuppressed(new RuntimeException("Nested 2a")); + suppressed2.addSuppressed(nested2); + suppressed2.addSuppressed(new IllegalStateException("Nested 2-2")); + + + final RuntimeException suppressed3 = new RuntimeException("Suppressed 3"); + final IllegalStateException nested3 = new IllegalStateException("Nested 3"); + nested3.addSuppressed(new RuntimeException("Nested 3a")); + suppressed3.addSuppressed(nested3); + suppressed3.addSuppressed(new IllegalStateException("Nested 3-2")); + + final RuntimeException cause = new RuntimeException("This is the cause"); + cause.addSuppressed(suppressed1); + cause.addSuppressed(suppressed2); + cause.addSuppressed(suppressed3); + return cause; + } + + private static String sanitize(final String s) { + if (s.startsWith(": ")) { + return s.substring(2); + } + return s; + } +}