summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSom Snytt <som.snytt@gmail.com>2013-08-24 04:20:44 -0700
committerSom Snytt <som.snytt@gmail.com>2013-09-02 18:59:15 -0700
commit2fc528e0887b46a8dea403c1f8620ca8967c4b42 (patch)
tree578b50bc3cb03aeeae6d1359074f3d26b4af44cf
parenta8c05274f738943ae58ecefda4b012b9daf5d8dc (diff)
downloadscala-2fc528e0887b46a8dea403c1f8620ca8967c4b42.tar.gz
scala-2fc528e0887b46a8dea403c1f8620ca8967c4b42.tar.bz2
scala-2fc528e0887b46a8dea403c1f8620ca8967c4b42.zip
SI-7781 REPL stack trunc shows cause
The handy stack trace truncation in REPL doesn't show cause like a regular trace. This commit fixes that and also adds the usual indicator for truncation, viz, "... 33 more". The example from the ticket produces: ``` scala> rewrapperer java.lang.RuntimeException: java.lang.RuntimeException: java.lang.RuntimeException: Point of failure at .rewrapper(<console>:9) at .rewrapperer(<console>:10) ... 32 elided Caused by: java.lang.RuntimeException: java.lang.RuntimeException: Point of failure at .wrapper(<console>:8) ... 34 more Caused by: java.lang.RuntimeException: Point of failure at .sample(<console>:7) ... 35 more ``` Suppressed exceptions on Java 7 are handled reflectively. ``` java.lang.RuntimeException: My problem at scala.tools.nsc.util.StackTraceTest.repressed(StackTraceTest.scala:56) ... 27 elided Suppressed: java.lang.RuntimeException: Point of failure at scala.tools.nsc.util.StackTraceTest.sample(StackTraceTest.scala:29) at scala.tools.nsc.util.StackTraceTest.repressed(StackTraceTest.scala:54) ... 27 more ```
-rw-r--r--src/compiler/scala/tools/nsc/util/StackTracing.scala76
-rw-r--r--src/compiler/scala/tools/nsc/util/package.scala18
-rw-r--r--src/repl/scala/tools/nsc/interpreter/IMain.scala4
-rw-r--r--test/files/run/repl-trim-stack-trace.scala8
-rw-r--r--test/junit/scala/tools/nsc/util/StackTraceTest.scala159
5 files changed, 256 insertions, 9 deletions
diff --git a/src/compiler/scala/tools/nsc/util/StackTracing.scala b/src/compiler/scala/tools/nsc/util/StackTracing.scala
new file mode 100644
index 0000000000..fa4fe29f28
--- /dev/null
+++ b/src/compiler/scala/tools/nsc/util/StackTracing.scala
@@ -0,0 +1,76 @@
+/* NSC -- new Scala compiler
+ * Copyright 2005-2013 LAMP/EPFL
+ */
+
+package scala.tools.nsc.util
+
+private[util] trait StackTracing extends Any {
+
+ /** Format a stack trace, returning the prefix consisting of frames that satisfy
+ * a given predicate.
+ * The format is similar to the typical case described in the JavaDoc
+ * for [[java.lang.Throwable#printStackTrace]].
+ * If a stack trace is truncated, it will be followed by a line of the form
+ * `... 3 elided`, by analogy to the lines `... 3 more` which indicate
+ * shared stack trace segments.
+ * @param e the exception
+ * @param p the predicate to select the prefix
+ */
+ def stackTracePrefixString(e: Throwable)(p: StackTraceElement => Boolean): String = {
+ import collection.mutable.{ ArrayBuffer, ListBuffer }
+ import compat.Platform.EOL
+ import util.Properties.isJavaAtLeast
+
+ val sb = ListBuffer.empty[String]
+
+ type TraceRelation = String
+ val Self = new TraceRelation("")
+ val CausedBy = new TraceRelation("Caused by: ")
+ val Suppressed = new TraceRelation("Suppressed: ")
+
+ val suppressable = isJavaAtLeast("1.7")
+
+ def clazz(e: Throwable) = e.getClass.getName
+ def because(e: Throwable): String = e.getCause match { case null => null ; case c => header(c) }
+ def msg(e: Throwable): String = e.getMessage match { case null => because(e) ; case s => s }
+ def txt(e: Throwable): String = msg(e) match { case null => "" ; case s => s": $s" }
+ def header(e: Throwable): String = s"${clazz(e)}${txt(e)}"
+
+ val indent = "\u0020\u0020"
+
+ val seen = new ArrayBuffer[Throwable](16)
+ def unseen(t: Throwable) = {
+ def inSeen = seen exists (_ eq t)
+ val interesting = (t != null) && !inSeen
+ if (interesting) seen += t
+ interesting
+ }
+
+ def print(e: Throwable, r: TraceRelation, share: Array[StackTraceElement], indents: Int): Unit = if (unseen(e)) {
+ val trace = e.getStackTrace
+ val frames = (
+ if (share.nonEmpty) {
+ val spare = share.reverseIterator
+ val trimmed = trace.reverse dropWhile (spare.hasNext && spare.next == _)
+ trimmed.reverse
+ } else trace
+ )
+ val prefix = frames takeWhile p
+ val margin = indent * indents
+ val indented = margin + indent
+ sb append s"${margin}${r}${header(e)}"
+ prefix foreach (f => sb append s"${indented}at $f")
+ if (frames.size < trace.size) sb append s"$indented... ${trace.size - frames.size} more"
+ if (r == Self && prefix.size < frames.size) sb append s"$indented... ${frames.size - prefix.size} elided"
+ print(e.getCause, CausedBy, trace, indents)
+ if (suppressable) {
+ import scala.language.reflectiveCalls
+ type Suppressing = { def getSuppressed(): Array[Throwable] }
+ for (s <- e.asInstanceOf[Suppressing].getSuppressed) print(s, Suppressed, frames, indents + 1)
+ }
+ }
+ print(e, Self, share = Array.empty, indents = 0)
+
+ sb mkString EOL
+ }
+}
diff --git a/src/compiler/scala/tools/nsc/util/package.scala b/src/compiler/scala/tools/nsc/util/package.scala
index ea3c9d8dde..72a4bbf5c0 100644
--- a/src/compiler/scala/tools/nsc/util/package.scala
+++ b/src/compiler/scala/tools/nsc/util/package.scala
@@ -8,7 +8,6 @@ package tools
package nsc
import java.io.{ OutputStream, PrintStream, ByteArrayOutputStream, PrintWriter, StringWriter }
-import scala.compat.Platform.EOL
package object util {
@@ -79,12 +78,17 @@ package object util {
s"$clazz$msg @ $frame"
}
- def stackTracePrefixString(ex: Throwable)(p: StackTraceElement => Boolean): String = {
- val frames = ex.getStackTrace takeWhile p map (" at " + _)
- val msg = ex.getMessage match { case null => "" ; case s => s": $s" }
- val clazz = ex.getClass.getName
-
- s"$clazz$msg" +: frames mkString EOL
+ implicit class StackTraceOps(val e: Throwable) extends AnyVal with StackTracing {
+ /** Format the stack trace, returning the prefix consisting of frames that satisfy
+ * a given predicate.
+ * The format is similar to the typical case described in the JavaDoc
+ * for [[java.lang.Throwable#printStackTrace]].
+ * If a stack trace is truncated, it will be followed by a line of the form
+ * `... 3 elided`, by analogy to the lines `... 3 more` which indicate
+ * shared stack trace segments.
+ * @param p the predicate to select the prefix
+ */
+ def stackTracePrefixString(p: StackTraceElement => Boolean): String = stackTracePrefixString(e)(p)
}
lazy val trace = new SimpleTracer(System.out)
diff --git a/src/repl/scala/tools/nsc/interpreter/IMain.scala b/src/repl/scala/tools/nsc/interpreter/IMain.scala
index 3eafa563bc..ee4ff59498 100644
--- a/src/repl/scala/tools/nsc/interpreter/IMain.scala
+++ b/src/repl/scala/tools/nsc/interpreter/IMain.scala
@@ -22,7 +22,7 @@ import scala.reflect.internal.util.{ BatchSourceFile, SourceFile }
import scala.tools.util.PathResolver
import scala.tools.nsc.io.AbstractFile
import scala.tools.nsc.typechecker.{ TypeStrings, StructuredTypeStrings }
-import scala.tools.nsc.util.{ ScalaClassLoader, stringFromWriter, stackTracePrefixString }
+import scala.tools.nsc.util.{ ScalaClassLoader, stringFromWriter, StackTraceOps }
import scala.tools.nsc.util.Exceptional.unwrap
import javax.script.{AbstractScriptEngine, Bindings, ScriptContext, ScriptEngine, ScriptEngineFactory, ScriptException, CompiledScript, Compilable}
@@ -726,7 +726,7 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set
def isWrapperInit(x: StackTraceElement) = cond(x.getClassName) {
case classNameRegex() if x.getMethodName == nme.CONSTRUCTOR.decoded => true
}
- val stackTrace = util.stackTracePrefixString(unwrapped)(!isWrapperInit(_))
+ val stackTrace = unwrapped stackTracePrefixString (!isWrapperInit(_))
withLastExceptionLock[String]({
directBind[Throwable]("lastException", unwrapped)(StdReplTags.tagOfThrowable, classTag[Throwable])
diff --git a/test/files/run/repl-trim-stack-trace.scala b/test/files/run/repl-trim-stack-trace.scala
index bbf46f2f19..db42b37fdd 100644
--- a/test/files/run/repl-trim-stack-trace.scala
+++ b/test/files/run/repl-trim-stack-trace.scala
@@ -13,6 +13,7 @@ f: Nothing
scala> f
java.lang.Exception: Uh-oh
at .f(<console>:7)
+ ... 69 elided
scala> def f = throw new Exception("")
f: Nothing
@@ -20,6 +21,7 @@ f: Nothing
scala> f
java.lang.Exception:
at .f(<console>:7)
+ ... 69 elided
scala> def f = throw new Exception
f: Nothing
@@ -27,7 +29,13 @@ f: Nothing
scala> f
java.lang.Exception
at .f(<console>:7)
+ ... 69 elided
scala> """
+ // remove the "elided" lines because the frame count is variable
+ lazy val elided = """\s+\.{3} (?:\d+) elided""".r
+ def filtered(lines: Seq[String]) = lines filter { case elided() => false ; case _ => true }
+ override def eval() = filtered(super.eval().toSeq).iterator
+ override def expected = filtered(super.expected).toList
}
diff --git a/test/junit/scala/tools/nsc/util/StackTraceTest.scala b/test/junit/scala/tools/nsc/util/StackTraceTest.scala
new file mode 100644
index 0000000000..e7654244c5
--- /dev/null
+++ b/test/junit/scala/tools/nsc/util/StackTraceTest.scala
@@ -0,0 +1,159 @@
+
+package scala.tools.nsc.util
+
+import scala.language.reflectiveCalls
+import scala.util._
+import PartialFunction.cond
+import Properties.isJavaAtLeast
+
+import org.junit.Assert._
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+trait Expecting {
+ /*
+ import org.expecty.Expecty
+ final val expect = new Expecty
+ */
+}
+
+
+@RunWith(classOf[JUnit4])
+class StackTraceTest extends Expecting {
+ // formerly an enum
+ val CausedBy = "Caused by: "
+ val Suppressed = "Suppressed: "
+
+ // throws
+ def sample = throw new RuntimeException("Point of failure")
+ def sampler: String = sample
+
+ // repackage with message
+ def resample: String = try { sample } catch { case e: Throwable => throw new RuntimeException("resample", e) }
+ def resampler: String = resample
+
+ // simple wrapper
+ def wrapper: String = try { sample } catch { case e: Throwable => throw new RuntimeException(e) }
+ // another onion skin
+ def rewrapper: String = try { wrapper } catch { case e: Throwable => throw new RuntimeException(e) }
+ def rewrapperer: String = rewrapper
+
+ // only an insane wretch would do this
+ def insane: String = try { sample } catch {
+ case e: Throwable =>
+ val t = new RuntimeException(e)
+ e initCause t
+ throw t
+ }
+ def insaner: String = insane
+
+ /** Java 7 */
+ val suppressable = isJavaAtLeast("1.7")
+ type Suppressing = { def addSuppressed(t: Throwable): Unit }
+
+ def repressed: String = try { sample } catch {
+ case e: Throwable =>
+ val t = new RuntimeException("My problem")
+ if (suppressable) {
+ t.asInstanceOf[Suppressing] addSuppressed e
+ }
+ throw t
+ }
+ def represser: String = repressed
+
+ // evaluating s should throw, p trims stack trace, t is the test of resulting trace string
+ def probe(s: =>String)(p: StackTraceElement => Boolean)(t: String => Unit): Unit = {
+ Try(s) recover { case e => e stackTracePrefixString p } match {
+ case Success(s) => t(s)
+ case Failure(e) => throw e
+ }
+ }
+
+ @Test def showsAllTrace() {
+ probe(sampler)(_ => true) { s =>
+ val res = s.lines.toList
+ /*
+ expect {
+ res.length > 5 // many lines
+ // these expectations may be framework-specific
+ //s contains "sbt.TestFramework"
+ //res.last contains "java.lang.Thread"
+ }
+ */
+ assert (res.length > 5)
+ }
+ }
+ @Test def showsOnlyPrefix() = probe(sample)(_.getMethodName == "sample") { s =>
+ val res = s.lines.toList
+ /*
+ expect {
+ res.length == 3 // summary + one frame + elision
+ }
+ */
+ assert (res.length == 3)
+ }
+ @Test def showsCause() = probe(resampler)(_.getMethodName != "resampler") { s =>
+ val res = s.lines.toList
+ /*
+ expect {
+ res.length == 6 // summary + one frame + elision, caused by + one frame + elision
+ res exists (_ startsWith CausedBy.toString)
+ }
+ */
+ assert (res.length == 6)
+ assert (res exists (_ startsWith CausedBy.toString))
+ }
+ @Test def showsWrappedExceptions() = probe(rewrapperer)(_.getMethodName != "rewrapperer") { s =>
+ val res = s.lines.toList
+ /*
+ expect {
+ res.length == 9 // summary + one frame + elision times three
+ res exists (_ startsWith CausedBy.toString)
+ (res collect {
+ case s if s startsWith CausedBy.toString => s
+ }).size == 2
+ }
+ */
+ assert (res.length == 9)
+ assert (res exists (_ startsWith CausedBy.toString))
+ assert ((res collect {
+ case s if s startsWith CausedBy.toString => s
+ }).size == 2)
+ }
+ @Test def dontBlowOnCycle() = probe(insaner)(_.getMethodName != "insaner") { s =>
+ val res = s.lines.toList
+ /*
+ expect {
+ res.length == 7 // summary + one frame + elision times two with extra frame
+ res exists (_ startsWith CausedBy.toString)
+ }
+ */
+ assert (res.length == 7)
+ assert (res exists (_ startsWith CausedBy.toString))
+ }
+
+ /** Java 7, but shouldn't bomb on Java 6.
+ *
+java.lang.RuntimeException: My problem
+ at scala.tools.nsc.util.StackTraceTest.repressed(StackTraceTest.scala:56)
+ ... 27 elided
+ Suppressed: java.lang.RuntimeException: Point of failure
+ at scala.tools.nsc.util.StackTraceTest.sample(StackTraceTest.scala:29)
+ at scala.tools.nsc.util.StackTraceTest.repressed(StackTraceTest.scala:54)
+ ... 27 more
+ */
+ @Test def showsSuppressed() = probe(represser)(_.getMethodName != "represser") { s =>
+ val res = s.lines.toList
+ if (suppressable) {
+ assert (res.length == 7)
+ assert (res exists (_.trim startsWith Suppressed.toString))
+ }
+ /*
+ expect {
+ res.length == 7
+ res exists (_ startsWith " " + Suppressed.toString)
+ }
+ */
+ }
+}