Bug 304219 - Formatting in Java Stack Trace Console should support more
keywords
Apart from unit test for existing formatting it add support for the
following keywords:
- Caused by:
- Suppressed:
- ... n more
- [CYCLIC REFERENCE:x]
Dependent if a 'Caused by' is the cause for the main or a suppressed
exception its indentation can differ. Java Stack Trace Console format
will not break those specific indentations.
Change-Id: Ida5507354fd30788975fcf88158a57dde4f464d1
Signed-off-by: Paul Pazderski <paul-eclipse@ppazderski.de>
diff --git a/org.eclipse.jdt.debug.tests/console tests/org/eclipse/jdt/debug/tests/console/JavaStackTraceConsoleTest.java b/org.eclipse.jdt.debug.tests/console tests/org/eclipse/jdt/debug/tests/console/JavaStackTraceConsoleTest.java
index d11b319..924dcda 100644
--- a/org.eclipse.jdt.debug.tests/console tests/org/eclipse/jdt/debug/tests/console/JavaStackTraceConsoleTest.java
+++ b/org.eclipse.jdt.debug.tests/console tests/org/eclipse/jdt/debug/tests/console/JavaStackTraceConsoleTest.java
@@ -1,5 +1,5 @@
/*******************************************************************************
- * Copyright (c) 2014, 2019 SAP SE and others.
+ * Copyright (c) 2014, 2020 SAP SE and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
@@ -12,10 +12,12 @@
* SAP SE - initial API and implementation
* Paul Pazderski - Bug 546900: Tests to check initial console content and content persistence
* Paul Pazderski - Bug 343023: Tests for 'clear initial content on first edit'
+ * Paul Pazderski - Bug 304219: Tests for formatting
*******************************************************************************/
package org.eclipse.jdt.debug.tests.console;
import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertNotEquals;
import java.io.IOException;
import java.nio.file.Files;
@@ -24,6 +26,8 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jdt.debug.tests.AbstractDebugTest;
@@ -35,6 +39,7 @@
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ST;
@@ -57,6 +62,9 @@
*/
public class JavaStackTraceConsoleTest extends AbstractDebugTest {
+ private final static Pattern LEFT_INDENT = Pattern.compile("^[ \\t]*");
+ private final static Pattern RIGHT_INDENT = Pattern.compile("\\s+$");
+
private final JavaStackTraceConsoleFactory fConsoleFactory = new JavaStackTraceConsoleFactory();
private JavaStackTraceConsole fConsole;
@@ -269,6 +277,223 @@
});
}
+ /** Test formatting of a plain simple stack trace. */
+ public void testFormatSimple() {
+ IDocument doc = consoleDocumentFormatted("java.lang.AssertionError: expected:5 but was:7\n\n"
+ + "at org.junit.Ass\nert.fail(Assert.java:88) \n" + "at\norg.junit. \nAssert.failNotEquals(Assert.java:834)\n"
+ + "at org.junit.Assert.assertEquals(Assert.java:118)\n" + "at \norg.junit.Assert.assertEquals\n(Assert.java:144)");
+ assertEquals("java.lang.AssertionError: expected:5 but was:7", getLine(doc, 0));
+ assertEquals("at org.junit.Assert.fail(Assert.java:88)", getLine(doc, 1).trim());
+ assertEquals("at org.junit.Assert.failNotEquals(Assert.java:834)", getLine(doc, 2).trim());
+ assertEquals("at org.junit.Assert.assertEquals(Assert.java:118)", getLine(doc, 3).trim());
+ assertEquals("at org.junit.Assert.assertEquals(Assert.java:144)", getLine(doc, 4).trim());
+ checkIndentationConsistency(doc, 0);
+ }
+
+ /** Test formatting of a stack trace including thread name. */
+ public void testFormatThreadName() {
+ IDocument doc = consoleDocumentFormatted("Exception in thread \"ma\nin\" java.lang.NullPointerException\n"
+ + "at \nStacktrace.main(Stacktrace.java:4)");
+ assertEquals("Exception in thread \"main\" java.lang.NullPointerException", getLine(doc, 0));
+ assertEquals("at Stacktrace.main(Stacktrace.java:4)", getLine(doc, 1).trim());
+ checkIndentationConsistency(doc, 0);
+ }
+
+ /** Test formatting with some less common method names. */
+ public void testFormatUncommonMethods() {
+ IDocument doc = consoleDocumentFormatted("Stack Trace\n" + " at org.eclipse.core.runtime.SafeRunner.run\n(SafeRunner.java:43)\n"
+ + " at org.eclipse.ui.internal.JFaceUtil.lambda$0(JFaceUtil.java:47)\n"
+ + " at org.eclipse.ui.internal.JFaceUtil$$Lambda$107/0x00000 \n008013c5c40.run(Unknown Source)\n"
+ + "\tat org.eclipse.jface.util.SafeRunnable.run(SafeRunnable.java:174)\n"
+ + " at java.base@12/java.lang.reflect.Method.invoke(Method.java:567)\n"
+ + " \n at app/\n/org.eclipse.equinox.launcher.Main.main(Main.java:1438)");
+ assertEquals("Stack Trace", getLine(doc, 0));
+ assertEquals("at org.eclipse.core.runtime.SafeRunner.run(SafeRunner.java:43)", getLine(doc, 1).trim());
+ assertEquals("at org.eclipse.ui.internal.JFaceUtil.lambda$0(JFaceUtil.java:47)", getLine(doc, 2).trim());
+ assertEquals("at org.eclipse.ui.internal.JFaceUtil$$Lambda$107/0x00000008013c5c40.run(Unknown Source)", getLine(doc, 3).trim());
+ assertEquals("at org.eclipse.jface.util.SafeRunnable.run(SafeRunnable.java:174)", getLine(doc, 4).trim());
+ assertEquals("at java.base@12/java.lang.reflect.Method.invoke(Method.java:567)", getLine(doc, 5).trim());
+ assertEquals("at app//org.eclipse.equinox.launcher.Main.main(Main.java:1438)", getLine(doc, 6).trim());
+ checkIndentationConsistency(doc, 0);
+ }
+
+ /** Test formatting with a 'locked' entry. */
+ public void testFormatLocked() {
+ IDocument doc = consoleDocumentFormatted("java.lang.Thread.State: RUNNABLE\n"
+ + " at java.net.PlainSocketImpl.socketAccept(Native Method)\n\n\n"
+ + "at java.net.PlainSocketImpl\n.accept(PlainSocketImpl.java:408)\n" + "\t - locked <0x911d3c30> (a java.net.SocksSocketImpl)\n"
+ + " at java.net.ServerSocket.implAccept(ServerSocket.java:462)\n" + "at java.net.ServerSocket.accept(ServerSocket.java:430)");
+ assertEquals("java.lang.Thread.State: RUNNABLE", getLine(doc, 0));
+ assertEquals("at java.net.PlainSocketImpl.socketAccept(Native Method)", getLine(doc, 1).trim());
+ assertEquals("at java.net.PlainSocketImpl.accept(PlainSocketImpl.java:408)", getLine(doc, 2).trim());
+ assertEquals("- locked <0x911d3c30> (a java.net.SocksSocketImpl)", getLine(doc, 3).trim());
+ assertEquals("at java.net.ServerSocket.implAccept(ServerSocket.java:462)", getLine(doc, 4).trim());
+ assertEquals("at java.net.ServerSocket.accept(ServerSocket.java:430)", getLine(doc, 5).trim());
+ checkIndentationConsistency(doc, 0);
+ }
+
+ /** Test formatting with a ... more entry. */
+ public void testFormatMore() {
+ // additional this one is missing the 'header' line and starting with an 'at' line
+ IDocument doc = consoleDocumentFormatted(" at org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager.preparePersistenceUnitInfos(DefaultPersistenceUnitManager.java:470)\n"
+ + " at org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager.afterPropertiesSet(DefaultPersistenceUnitManager.java:424)\n"
+ + "at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:310)\n"
+ + " at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:318)\n\n"
+ + " \t\t at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1633)\n"
+ + " \t \tat org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1570)\n"
+ + " ... 53 more\n" + "");
+ assertEquals("at org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager.preparePersistenceUnitInfos(DefaultPersistenceUnitManager.java:470)", getLine(doc, 1).trim());
+ assertEquals("at org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager.afterPropertiesSet(DefaultPersistenceUnitManager.java:424)", getLine(doc, 2).trim());
+ assertEquals("at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:310)", getLine(doc, 3).trim());
+ assertEquals("at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:318)", getLine(doc, 4).trim());
+ assertEquals("at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1633)", getLine(doc, 5).trim());
+ assertEquals("at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1570)", getLine(doc, 6).trim());
+ assertEquals("... 53 more", getLine(doc, 7).trim());
+ checkIndentationConsistency(doc, 0);
+ }
+
+ /** Test formatting stack trace with cause. */
+ public void testFormatCause() {
+ IDocument doc = consoleDocumentFormatted("HighLevelException:\n LowLevelException\n" + "\tat Junk.a(Junk.java:13)\n"
+ + " at Junk.main(Junk.java:4)\n" + " Caused by: LowLevelException\n" + " at Junk.e(Junk.java:30)\n"
+ + " at Junk.d\n(Junk.java:27)\n" + "at Junk.c(Junk.java:21)");
+ assertEquals("HighLevelException: LowLevelException", getLine(doc, 0));
+ assertEquals("at Junk.a(Junk.java:13)", getLine(doc, 1).trim());
+ assertEquals("at Junk.main(Junk.java:4)", getLine(doc, 2).trim());
+ assertEquals("Caused by: LowLevelException", getLine(doc, 3));
+ assertEquals("at Junk.e(Junk.java:30)", getLine(doc, 4).trim());
+ assertEquals("at Junk.d(Junk.java:27)", getLine(doc, 5).trim());
+ assertEquals("at Junk.c(Junk.java:21)", getLine(doc, 6).trim());
+ checkIndentationConsistency(doc, 0);
+
+ // nested causes
+ doc = consoleDocumentFormatted("HighLevelException:\t MidLevelException:\n LowLevelException\n" + "\tat Junk.a(Junk.java:13)\n"
+ + " at Junk.main(Junk.java:4)\n" + " Caused by: MidLevelException: LowLevelException\n" + " at Junk.c(Junk.java:23)\n"
+ + " at Junk.b(Junk.java:17)\n" + " at Junk.a(Junk.java:11)\n" + "... 1 more\n" + " Caused by: LowLevelException\n"
+ + " at Junk.e(Junk.java:30)\n" + "at Junk.d(Junk.java:27)\n" + " at Junk.c(Junk.java:21)\n" + " ... 3 more\n");
+ assertEquals("HighLevelException: MidLevelException: LowLevelException", getLine(doc, 0));
+ assertEquals("at Junk.a(Junk.java:13)", getLine(doc, 1).trim());
+ assertEquals("at Junk.main(Junk.java:4)", getLine(doc, 2).trim());
+ assertEquals("Caused by: MidLevelException: LowLevelException", getLine(doc, 3));
+ assertEquals("at Junk.c(Junk.java:23)", getLine(doc, 4).trim());
+ assertEquals("at Junk.b(Junk.java:17)", getLine(doc, 5).trim());
+ assertEquals("at Junk.a(Junk.java:11)", getLine(doc, 6).trim());
+ assertEquals("... 1 more", getLine(doc, 7).trim());
+ assertEquals("Caused by: LowLevelException", getLine(doc, 8));
+ assertEquals("at Junk.e(Junk.java:30)", getLine(doc, 9).trim());
+ assertEquals("at Junk.d(Junk.java:27)", getLine(doc, 10).trim());
+ assertEquals("at Junk.c(Junk.java:21)", getLine(doc, 11).trim());
+ assertEquals("... 3 more", getLine(doc, 12).trim());
+ checkIndentationConsistency(doc, 0);
+ }
+
+ /** Test formatting stack trace with suppressed exceptions. */
+ public void testFormatSuppressed() {
+ IDocument doc = consoleDocumentFormatted("Exception in thread \"main\" java.lang.Exception: Something happened\n" + "at Foo.bar(Native)\n"
+ + " at Foo.main(Foo.java:5)\n" + " Suppressed: Resource$CloseFailException: Resource ID = 0\n"
+ + " at Resource.close(Resource\n.java:26)\n" + " at Foo.bar(Foo.java)\n" + " ... 1 more\n" + "");
+ assertEquals("Exception in thread \"main\" java.lang.Exception: Something happened", getLine(doc, 0));
+ assertEquals("at Foo.bar(Native)", getLine(doc, 1).trim());
+ assertEquals("at Foo.main(Foo.java:5)", getLine(doc, 2).trim());
+ assertEquals("Suppressed: Resource$CloseFailException: Resource ID = 0", getLine(doc, 3).trim());
+ assertEquals("at Resource.close(Resource.java:26)", getLine(doc, 4).trim());
+ assertEquals("at Foo.bar(Foo.java)", getLine(doc, 5).trim());
+ assertEquals("... 1 more", getLine(doc, 6).trim());
+ checkIndentationConsistency(doc, 0);
+
+ // multiple suppressed
+ doc = consoleDocumentFormatted("Exception in thread \"main\" java.lang.Exception: Main block\n" + " at Foo3.main(Foo3.java:7)\n"
+ + " Suppressed: Resource$CloseFailException: Resource ID = 2\n" + " at Resource.close(Resource.java:26)\n"
+ + " \t\tat Foo3.main(Foo3.java:5)\n" + "Suppressed: Resource$CloseFailException: Resource ID = 1\n"
+ + " at Resource.close(Resource.java:26)\n" + " at Foo3.main(Foo3.java:5)\n" + "");
+ assertEquals("Exception in thread \"main\" java.lang.Exception: Main block", getLine(doc, 0));
+ assertEquals("at Foo3.main(Foo3.java:7)", getLine(doc, 1).trim());
+ assertEquals("Suppressed: Resource$CloseFailException: Resource ID = 2", getLine(doc, 2).trim());
+ assertEquals("at Resource.close(Resource.java:26)", getLine(doc, 3).trim());
+ assertEquals("at Foo3.main(Foo3.java:5)", getLine(doc, 4).trim());
+ assertEquals("Suppressed: Resource$CloseFailException: Resource ID = 1", getLine(doc, 5).trim());
+ assertEquals("at Resource.close(Resource.java:26)", getLine(doc, 6).trim());
+ assertEquals("at Foo3.main(Foo3.java:5)", getLine(doc, 7).trim());
+ checkIndentationConsistency(doc, 0);
+ }
+
+ /** Test formatting stack trace with mixture of cause and suppressed. */
+ public void testFormatSuppressedWithCause() {
+ // exception with suppressed and cause
+ IDocument doc = consoleDocumentFormatted("Exception in thread \"main\" java.lang.Exception: Main block\n" + " at Foo3.main(Foo3.java:7)\n"
+ + " Suppressed: Resource$CloseFailException: Resource ID = 1\n" + " at Resource.close(Resource.java:26)\n"
+ + " at Foo3.main(Foo3.java:5)\n" + "Caused by: java.lang.Exception: I did it\n" + " at Foo3.main(Foo3.java:8)\n");
+ assertEquals("Exception in thread \"main\" java.lang.Exception: Main block", getLine(doc, 0));
+ assertEquals("at Foo3.main(Foo3.java:7)", getLine(doc, 1).trim());
+ assertEquals("Suppressed: Resource$CloseFailException: Resource ID = 1", getLine(doc, 2).trim());
+ assertEquals("at Resource.close(Resource.java:26)", getLine(doc, 3).trim());
+ assertEquals("at Foo3.main(Foo3.java:5)", getLine(doc, 4).trim());
+ assertEquals("Caused by: java.lang.Exception: I did it", getLine(doc, 5).trim());
+ assertEquals("at Foo3.main(Foo3.java:8)", getLine(doc, 6).trim());
+ checkIndentationConsistency(doc, 0);
+ // Additional indentation check. Since cause is linked to primary exception it must be less indented as suppressed stuff.
+ assertEquals(getLineIndentation(getLine(doc, 0)), getLineIndentation(getLine(doc, 5)));
+ assertEquals(getLineIndentation(getLine(doc, 1)), getLineIndentation(getLine(doc, 6)));
+ assertTrue(getLineIndentation(getLine(doc, 3)) > getLineIndentation(getLine(doc, 5)));
+ assertTrue(getLineIndentation(getLine(doc, 3)) > getLineIndentation(getLine(doc, 6)));
+
+ // exception with suppressed and cause for the suppressed
+ doc = consoleDocumentFormatted("Exception in thread \"main\" java.lang.Exception: Main block\n" + " at Foo4.main(Foo4.java:6)\n"
+ + " Suppressed: Resource2$CloseFailException: Resource ID = 1\n" + " at Resource2.close(Resource2.java:20)\n"
+ + " at Foo4.main(Foo4.java:5)\n" + " Caused by: java.lang.Exception: Rats, you caught me\n"
+ + " at Resource2$CloseFailException.<init>(Resource2.java:45)\n" + " ... 2 more\n");
+ assertEquals("Exception in thread \"main\" java.lang.Exception: Main block", getLine(doc, 0));
+ assertEquals("at Foo4.main(Foo4.java:6)", getLine(doc, 1).trim());
+ assertEquals("Suppressed: Resource2$CloseFailException: Resource ID = 1", getLine(doc, 2).trim());
+ assertEquals("at Resource2.close(Resource2.java:20)", getLine(doc, 3).trim());
+ assertEquals("at Foo4.main(Foo4.java:5)", getLine(doc, 4).trim());
+ assertEquals("Caused by: java.lang.Exception: Rats, you caught me", getLine(doc, 5).trim());
+ assertEquals("at Resource2$CloseFailException.<init>(Resource2.java:45)", getLine(doc, 6).trim());
+ assertEquals("... 2 more", getLine(doc, 7).trim());
+ checkIndentationConsistency(doc, 0);
+ // Additional indentation check. Since cause is linked to suppressed exception it must be greater indented as primary exception stuff.
+ assertNotEquals(getLineIndentation(getLine(doc, 0)), getLineIndentation(getLine(doc, 5)));
+ assertEquals(getLineIndentation(getLine(doc, 2)), getLineIndentation(getLine(doc, 5)));
+ assertNotEquals(getLineIndentation(getLine(doc, 1)), getLineIndentation(getLine(doc, 6)));
+ assertEquals(getLineIndentation(getLine(doc, 3)), getLineIndentation(getLine(doc, 6)));
+ assertEquals(getLineIndentation(getLine(doc, 4)), getLineIndentation(getLine(doc, 7)));
+ assertTrue(getLineIndentation(getLine(doc, 5)) > getLineIndentation(getLine(doc, 0)));
+ assertTrue(getLineIndentation(getLine(doc, 6)) > getLineIndentation(getLine(doc, 1)));
+ }
+
+ /** Test formatting the rare [CIRCULAR REFERENCE:...] entry. */
+ public void testFormatCircular() {
+ IDocument doc = consoleDocumentFormatted("Exception in thread \"main\" Stacktrace$BadException\n"
+ + "at Stacktrace.main\n(Stacktrace.java:4)\n" + " Caused by: Stacktrace$BadExceptionCompanion: Stacktrace$BadException\n"
+ + " at Stacktrace$BadException.<init>(Stacktrace.java:10)\n" + " ... 1 more\n"
+ + " [CIRCULAR REFERENCE:Stacktrace$BadException]");
+ assertEquals("Exception in thread \"main\" Stacktrace$BadException", getLine(doc, 0));
+ assertEquals("at Stacktrace.main(Stacktrace.java:4)", getLine(doc, 1).trim());
+ assertEquals("Caused by: Stacktrace$BadExceptionCompanion: Stacktrace$BadException", getLine(doc, 2));
+ assertEquals("at Stacktrace$BadException.<init>(Stacktrace.java:10)", getLine(doc, 3).trim());
+ assertEquals("... 1 more", getLine(doc, 4).trim());
+ assertEquals("[CIRCULAR REFERENCE:Stacktrace$BadException]", getLine(doc, 5).trim());
+ checkIndentationConsistency(doc, 0);
+ }
+
+ /** Test formatting stack trace from an ant execution. (output mixed with ant prefixes) */
+ public void testFormatAnt() {
+ IDocument doc = consoleDocumentFormatted("[java] !ENTRY org.eclipse.debug.core 4 120 2005-01-11 03:02:30.321\n"
+ + " [java] !MESSAGE An exception occurred while dispatching debug events.\n" + " [java] !STACK 0\n"
+ + " [java] java.lang.NullPointerException\n" + " [java] at \n"
+ + "org.eclipse.debug.internal.ui.views.console.ProcessConsole.closeStreams\n" + "(ProcessConsole.java:364)\n" + " [java] at \n"
+ + "org.eclipse.debug.internal.ui.views.console.ProcessConsole.handleDebugEvents\n" + "(ProcessConsole.java:438)\n"
+ + " [java] at org.eclipse.debug.core.DebugPlugin$EventNotifier.run\n" + "(DebugPlugin.java:1043)");
+ assertEquals("[java] !ENTRY org.eclipse.debug.core 4 120 2005-01-11 03:02:30.321", getLine(doc, 0));
+ assertEquals("[java] !MESSAGE An exception occurred while dispatching debug events.", getLine(doc, 1));
+ assertEquals("[java] !STACK 0", getLine(doc, 2));
+ assertEquals("[java] java.lang.NullPointerException", getLine(doc, 3));
+ assertEquals("at org.eclipse.debug.internal.ui.views.console.ProcessConsole.closeStreams(ProcessConsole.java:364)", getLine(doc, 4).replace("[java]", "").trim());
+ assertEquals("at org.eclipse.debug.internal.ui.views.console.ProcessConsole.handleDebugEvents(ProcessConsole.java:438)", getLine(doc, 5).replace("[java]", "").trim());
+ assertEquals("at org.eclipse.debug.core.DebugPlugin$EventNotifier.run(DebugPlugin.java:1043)", getLine(doc, 6).replace("[java]", "").trim());
+ checkIndentationConsistency(doc, 3);
+ }
+
private IDocument consoleDocumentWithText(String text) throws InterruptedException {
IDocument document = fConsole.getDocument();
document.set(text);
@@ -277,6 +502,90 @@
return document;
}
+ private String getLine(IDocument doc, int line) {
+ IRegion lineInfo;
+ try {
+ lineInfo = doc.getLineInformation(line);
+ return doc.get(lineInfo.getOffset(), lineInfo.getLength());
+ } catch (BadLocationException ex) {
+ return null;
+ }
+ }
+
+ /**
+ * Do some tests on the stack trace indentation. No hardcoded valued just some general assumptions.
+ *
+ * @param doc
+ * document to test
+ * @param startLine
+ * first line to check
+ */
+ private void checkIndentationConsistency(IDocument doc, int startLine) {
+ boolean firstSuppress = true;
+ int lastIndent = -1;
+ // Remember how the next line's indentation can differ from the previous.
+ // -1 -> less indented
+ // 0 -> equal
+ // 1 -> more indented
+ int allowedIndentChange = 1;
+ for (int i = startLine, lineCount = doc.getNumberOfLines(); i < lineCount; i++) {
+ String line = getLine(doc, i);
+ line = line.replaceFirst("^\\[[^\\s\\]]+\\] ", ""); // remove and prefix if any
+ if (i != 0) { // first line can be empty
+ assertNotEquals("Empty line " + i, "", line);
+ }
+ assertFalse("Trailing whitespace in line " + i, RIGHT_INDENT.matcher(line).find());
+
+ boolean causedBy = line.trim().startsWith("Caused by: ");
+ boolean suppressed = line.trim().startsWith("Suppressed: ");
+ if (causedBy || (suppressed && !firstSuppress)) {
+ allowedIndentChange = -1;
+ }
+
+ int lineIndent = getLineIndentation(line);
+ if (allowedIndentChange < 0) {
+ assertTrue("Wrong indented line " + i + ": " + lastIndent + " > " + lineIndent, lastIndent > lineIndent);
+ } else if (allowedIndentChange == 0) {
+ assertEquals("Mixed indentation in line " + i, lastIndent, lineIndent);
+ } else if (allowedIndentChange > 0) {
+ assertTrue("Wrong indented line " + i + ": " + lastIndent + " < " + lineIndent, lastIndent < lineIndent);
+ }
+ lastIndent = lineIndent;
+ allowedIndentChange = 0;
+ if (causedBy || suppressed || i == startLine) {
+ allowedIndentChange = 1;
+ }
+ firstSuppress &= !suppressed;
+ }
+ }
+
+ private int getLineIndentation(String line) {
+ int tabSize = 4;
+ String indent = "";
+ Matcher m = LEFT_INDENT.matcher(line);
+ if (m.find()) {
+ indent = m.group();
+ }
+ int tabCount = indent.length() - indent.replace("\t", "").length();
+ return indent.length() + (tabSize - 1) * tabCount;
+ }
+
+ /**
+ * Set given text, invoke formatting and wait until finished.
+ *
+ * @param text
+ * new console text
+ * @return the consoles document
+ */
+ private IDocument consoleDocumentFormatted(String text) {
+ IDocument document = fConsole.getDocument();
+ document.set(text);
+ fConsole.format();
+ // wait for document being formatted
+ TestUtil.waitForJobs(getName(), 30, 1000);
+ return document;
+ }
+
private String[] linkTextsAtPositions(int... offsets) throws BadLocationException {
IDocument document = fConsole.getDocument();
diff --git a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/console/JavaStackTraceConsole.java b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/console/JavaStackTraceConsole.java
index 6ab52b4..5b80944 100644
--- a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/console/JavaStackTraceConsole.java
+++ b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/console/JavaStackTraceConsole.java
@@ -1,5 +1,5 @@
/*******************************************************************************
- * Copyright (c) 2000, 2019 IBM Corporation and others.
+ * Copyright (c) 2000, 2020 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
@@ -12,6 +12,7 @@
* IBM Corporation - initial API and implementation
* Paul Pazderski - Bug 546900: Fix IO handling in JavaStacktraceConsole
* Paul Pazderski - Bug 343023: Clear the initial stack trace console message on first edit
+ * Paul Pazderski - Bug 304219: Recognize more typical stack trace keywords for formatting
*******************************************************************************/
package org.eclipse.jdt.internal.debug.ui.console;
@@ -21,6 +22,8 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
import java.util.StringTokenizer;
import org.eclipse.core.runtime.IProgressMonitor;
@@ -80,6 +83,10 @@
public final static String CONSOLE_TYPE = "javaStackTraceConsole"; //$NON-NLS-1$
public final static String FILE_NAME = JDIDebugUIPlugin.getDefault().getStateLocation().toOSString() + File.separator + "stackTraceConsole.txt"; //$NON-NLS-1$
+ private static final String NL = "\n"; //$NON-NLS-1$
+ private static final String INDENT_STR = " "; //$NON-NLS-1$
+ private static final int INDENT_WIDTH = 4;
+
private JavaStackTraceConsolePartitioner partitioner = new JavaStackTraceConsolePartitioner();
private IPropertyChangeListener propertyListener = new IPropertyChangeListener() {
@Override
@@ -234,98 +241,136 @@
}
- /**
- * Underlying format operation
- * @param trace the stack trace to format
- * @return the formatted stack trace for this console
- */
- private String format(String trace) {
- StringTokenizer tokenizer = new StringTokenizer(trace, " \t\n\r\f", true); //$NON-NLS-1$
- StringBuilder formattedTrace = new StringBuilder();
+ /**
+ * Underlying format operation
+ *
+ * @param trace
+ * the stack trace to format
+ * @return the formatted stack trace for this console
+ */
+ private String format(String trace) {
+ StringTokenizer tokenizer = new StringTokenizer(trace, " \t\n\r\f", true); //$NON-NLS-1$
+ StringBuilder formattedTrace = new StringBuilder(trace.length());
- boolean insideAt = false;
- boolean newLine = true;
- int pendingSpaces = 0;
- boolean antTrace = false;
+ boolean insideAt = false;
+ boolean newLine = true;
+ int pendingSpaces = 0;
+ boolean antTrace = false;
+ int depth = 1;
+ // Block depth map is used to find the most likely indentation for a Caused.
+ // In combination with Suppressed the correct indentation can be ambiguous.
+ // Map has indentation in number of spaces of a previous block as key and formated
+ // indentation depth used for this block as value.
+ Map<Integer, Integer> blockDepth = new HashMap<>(3);
- while (tokenizer.hasMoreTokens()) {
- String token = tokenizer.nextToken();
- if (token.length() == 0)
- {
+ while (tokenizer.hasMoreTokens()) {
+ String token = tokenizer.nextToken();
+ if (token.isEmpty()) {
continue; // paranoid
}
- char c = token.charAt(0);
- // handle delimiters
- switch (c) {
- case ' ':
- if (newLine) {
- pendingSpaces++;
- } else {
- pendingSpaces = 1;
- }
- continue;
- case '\t':
- if (newLine) {
- pendingSpaces += 4;
- } else {
- pendingSpaces = 1;
- }
- continue;
- case '\n':
- case '\r':
- case '\f':
- if (insideAt) {
- pendingSpaces = 1;
- } else {
- pendingSpaces = 0;
- newLine = true;
- }
- continue;
- }
- // consider newlines only before token starting with char '\"' or
- // token "at" or "-".
- if (newLine || antTrace) {
- if (c == '\"') { // leading thread name, e.g. "Worker-124"
- // prio=5
- formattedTrace.append("\n\n"); //$NON-NLS-1$ print 2 lines to break between threads
- } else if ("-".equals(token)) { //$NON-NLS-1$ - locked ...
- formattedTrace.append("\n"); //$NON-NLS-1$
- formattedTrace.append(" "); //$NON-NLS-1$
- formattedTrace.append(token);
- pendingSpaces = 0;
- continue;
- } else if ("at".equals(token)) { //$NON-NLS-1$ at ...
- if (!antTrace) {
- formattedTrace.append("\n"); //$NON-NLS-1$
- formattedTrace.append(" "); //$NON-NLS-1$
- } else {
- formattedTrace.append(' ');
- }
- insideAt = true;
- formattedTrace.append(token);
- pendingSpaces = 0;
- continue;
- } else if (c == '[') {
- if(antTrace) {
- formattedTrace.append("\n"); //$NON-NLS-1$
- }
- formattedTrace.append(token);
- pendingSpaces = 0;
- newLine = false;
- antTrace = true;
- continue;
- }
- newLine = false;
- }
- if (pendingSpaces > 0) {
- for (int i = 0; i < pendingSpaces; i++) {
- formattedTrace.append(' ');
- }
- pendingSpaces = 0;
- }
- formattedTrace.append(token);
- insideAt = false;
- }
- return formattedTrace.toString();
- }
+ char c = token.charAt(0);
+ // handle delimiters
+ switch (c) {
+ case ' ':
+ if (newLine) {
+ pendingSpaces++;
+ } else {
+ pendingSpaces = 1;
+ }
+ continue;
+ case '\t':
+ if (newLine) {
+ pendingSpaces += INDENT_WIDTH;
+ } else {
+ pendingSpaces = 1;
+ }
+ continue;
+ case '\n':
+ case '\r':
+ case '\f':
+ if (insideAt) {
+ pendingSpaces = 1;
+ } else {
+ pendingSpaces = 0;
+ newLine = true;
+ }
+ continue;
+ }
+ // consider newlines only before token starting with char '\"' or
+ // token "at", "-", "...", "Caused by:", "Suppressed:" and "[CIRCULAR".
+ if (newLine) {
+ if (c == '\"') { // leading thread name, e.g. "Worker-124" prio=5
+ formattedTrace.append(NL + NL); // print 2 lines to break between threads
+ } else if (c == '-' // - locked <address>
+ || "...".equals(token)) { //$NON-NLS-1$ ... xx more
+ applyIndentedToken(formattedTrace, depth, token, antTrace);
+ pendingSpaces = 0;
+ continue;
+ } else if ("at".equals(token)) { //$NON-NLS-1$ at method
+ insideAt = true;
+ applyIndentedToken(formattedTrace, depth, token, antTrace);
+ pendingSpaces = 0;
+ continue;
+ } else if (c == '[') {
+ if ("[CIRCULAR".equals(token)) { //$NON-NLS-1$ [CIRCULAR REFERENCE:toString()]
+ applyIndentedToken(formattedTrace, depth, token, antTrace);
+ pendingSpaces = 0;
+ } else {
+ if (antTrace) {
+ formattedTrace.append(NL);
+ }
+ formattedTrace.append(token);
+ pendingSpaces = 0;
+ antTrace = true;
+ }
+ continue;
+ } else if ("Caused".equals(token)) { //$NON-NLS-1$ Caused by: reason
+ // Guess depth for Cause block. This can be interpreted as if the Caused
+ // block is moved to the left until it aligns with a previous Suppressed
+ // block or hit the line begin.
+ depth = 0;
+ for (Map.Entry<Integer, Integer> block : blockDepth.entrySet()) {
+ if (block.getKey() <= pendingSpaces && block.getValue() > depth) {
+ depth = block.getValue();
+ }
+ }
+ applyIndentedToken(formattedTrace, depth, token, antTrace);
+ depth++;
+ pendingSpaces = 0;
+ continue;
+ } else if ("Suppressed:".equals(token)) { //$NON-NLS-1$ Suppressed: reason
+ if (depth >= 2) {
+ depth--;
+ }
+ blockDepth.put(pendingSpaces, depth);
+ applyIndentedToken(formattedTrace, depth, token, antTrace);
+ depth = 2;
+ pendingSpaces = 0;
+ continue;
+ }
+ newLine = false;
+ }
+ if (pendingSpaces > 0) {
+ for (int i = 0; i < pendingSpaces; i++) {
+ formattedTrace.append(' ');
+ }
+ pendingSpaces = 0;
+ }
+ formattedTrace.append(token);
+ insideAt = false;
+ }
+ return formattedTrace.toString();
+ }
+
+ private void applyIndentedToken(StringBuilder formattedTrace, int depth, String token, boolean antTrace) {
+ if (antTrace) {
+ formattedTrace.append(' ');
+ } else {
+ formattedTrace.append(NL);
+ }
+ for (int i = 0; i < depth; i++) {
+ formattedTrace.append(INDENT_STR);
+ }
+ formattedTrace.append(token);
+ }
}