Skip to content

Commit 33e706f

Browse files
committed
Port stack formatter
1 parent d8bac4e commit 33e706f

File tree

2 files changed

+152
-0
lines changed

2 files changed

+152
-0
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Scala (https://www.scala-lang.org)
3+
*
4+
* Copyright EPFL and Lightbend, Inc.
5+
*
6+
* Licensed under Apache License 2.0
7+
* (http://www.apache.org/licenses/LICENSE-2.0).
8+
*
9+
* See the NOTICE file distributed with this work for
10+
* additional information regarding copyright ownership.
11+
*/
12+
13+
package dotty.tools.dotc.util
14+
15+
import collection.mutable, mutable.ListBuffer
16+
import scala.util.chaining.given
17+
import java.lang.System.lineSeparator
18+
19+
object StackTraceOps:
20+
21+
extension (t: Throwable)
22+
23+
/** Format a stack trace, taking the prefix span satisfying a predicate.
24+
*
25+
* The format is similar to the typical case described in the Javadoc
26+
* for [[java.lang.Throwable#printStackTrace()*]].
27+
* If a stack trace is truncated, it will be followed by a line of the form
28+
* `... 3 elided`, by analogy to the lines `... 3 more` which indicate
29+
* shared stack trace segments.
30+
* @param e the exception
31+
* @param p the predicate to select the prefix
32+
*/
33+
def formatStackTracePrefix(p: StackTraceElement => Boolean): String =
34+
35+
type TraceRelation = String
36+
val Self = new TraceRelation("")
37+
val CausedBy = new TraceRelation("Caused by: ")
38+
val Suppressed = new TraceRelation("Suppressed: ")
39+
40+
def header(e: Throwable): String =
41+
def because = e.getCause match { case null => null ; case c => header(c) }
42+
def msg = e.getMessage match { case null => because ; case s => s }
43+
def txt = msg match { case null => "" ; case s => s": $s" }
44+
s"${e.getClass.getName}$txt"
45+
46+
val seen = mutable.Set.empty[Throwable]
47+
def unseen(e: Throwable): Boolean = (e != null && !seen(e)).tap(if _ then seen += e)
48+
49+
val lines = ListBuffer.empty[String]
50+
51+
// format the stack trace, skipping the shared trace
52+
def print(e: Throwable, r: TraceRelation, share: Array[StackTraceElement], indents: Int): Unit = if unseen(e) then
53+
val trace = e.getStackTrace
54+
val frames = if share.isEmpty then trace else
55+
val spare = share.reverseIterator
56+
val trimmed = trace.reverse.dropWhile(spare.hasNext && spare.next() == _)
57+
trimmed.reverse
58+
val prefix = frames.takeWhile(p)
59+
val margin = " " * indents
60+
lines += s"${margin}${r}${header(e)}"
61+
prefix.foreach(frame => lines += s"$margin at $frame")
62+
63+
val traceFramesLenDiff = trace.length - frames.length
64+
val framesPrefixLenDiff = frames.length - prefix.length
65+
if traceFramesLenDiff > 0 then
66+
if framesPrefixLenDiff > 0 then lines += s"$margin ... $framesPrefixLenDiff elided and $traceFramesLenDiff more"
67+
else lines += s"$margin ... $traceFramesLenDiff more"
68+
else if framesPrefixLenDiff > 0 then lines += s"$margin ... $framesPrefixLenDiff elided"
69+
70+
print(e.getCause, CausedBy, trace, indents)
71+
e.getSuppressed.foreach(print(_, Suppressed, frames, indents + 1))
72+
end print
73+
74+
print(t, Self, share = Array.empty, indents = 0)
75+
lines.mkString(lineSeparator)
76+
end formatStackTracePrefix
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
2+
package dotty.tools.dotc.util
3+
4+
import scala.util.{Failure, Success, Try}
5+
import scala.util.chaining.given
6+
7+
import org.junit.Assert.{assertEquals, assertTrue}
8+
import org.junit.Test
9+
10+
class StackTraceTest:
11+
val CausedBy = "Caused by: "
12+
val Suppressed = "Suppressed: "
13+
14+
// throws
15+
def sample = throw new RuntimeException("Point of failure")
16+
def sampler: String = sample
17+
18+
// repackage with message
19+
def resample: String = try sample catch case e: Throwable => throw new RuntimeException("resample", e)
20+
def resampler: String = resample
21+
22+
// simple wrapper
23+
def wrapper: String = try sample catch case e: Throwable => throw new RuntimeException(e)
24+
// another onion skin
25+
def rewrapper: String = try wrapper catch case e: Throwable => throw new RuntimeException(e)
26+
def rewrapperer: String = rewrapper
27+
28+
// circular cause
29+
def insane: String = try sample catch case e: Throwable => throw new RuntimeException(e).tap(e.initCause)
30+
def insaner: String = insane
31+
32+
def repressed: String = try sample catch case e: Throwable => throw new RuntimeException("My problem").tap(_.addSuppressed(e))
33+
def represser: String = repressed
34+
35+
// evaluating s should throw, p trims stack trace, t is the test of resulting trace string
36+
def probe(s: => String)(p: StackTraceElement => Boolean)(t: String => Unit): Unit =
37+
import StackTraceOps.formatStackTracePrefix
38+
Try(s).recover { case e => e.formatStackTracePrefix(p) } match
39+
case Success(s) => t(s)
40+
case Failure(e) => throw e
41+
42+
@Test def showsAllTrace() =
43+
probe(sampler)(_ => true)(s => assertTrue(s.linesIterator.length > 5))
44+
45+
// summary + one frame + elision
46+
@Test def showsOnlyPrefix() =
47+
probe(sample)(_.getMethodName == "sample")(s => assertEquals(3, s.linesIterator.length))
48+
49+
// summary + one frame + elision, caused by + one frame + elision
50+
@Test def showsCause() = probe(resampler)(_.getMethodName != "resampler") { s =>
51+
val res = s.linesIterator.toList
52+
assertEquals(6, res.length)
53+
assertTrue(res.exists(_.startsWith(CausedBy)))
54+
}
55+
56+
// summary + one frame + elision times three
57+
@Test def showsWrappedExceptions() = probe(rewrapperer)(_.getMethodName != "rewrapperer") { s =>
58+
val res = s.linesIterator.toList
59+
assertEquals(9, res.length)
60+
assertTrue(res.exists(_.startsWith(CausedBy)))
61+
assertEquals(2, res.collect { case s if s.startsWith(CausedBy) => s }.size)
62+
}
63+
64+
// summary + one frame + elision times two with extra frame
65+
@Test def dontBlowOnCycle() = probe(insaner)(_.getMethodName != "insaner") { s =>
66+
val res = s.linesIterator.toList
67+
assertEquals(6, res.length)
68+
assertTrue(res.exists(_.startsWith(CausedBy)))
69+
}
70+
71+
@Test def showsSuppressed() = probe(represser)(_.getMethodName != "represser") { s =>
72+
val res = s.linesIterator.toList
73+
assertEquals(6, res.length)
74+
assertTrue(res.exists(_.trim.startsWith(Suppressed)))
75+
}
76+
end StackTraceTest

0 commit comments

Comments
 (0)