From 2fc528e0887b46a8dea403c1f8620ca8967c4b42 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Sat, 24 Aug 2013 04:20:44 -0700 Subject: 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(:9) at .rewrapperer(:10) ... 32 elided Caused by: java.lang.RuntimeException: java.lang.RuntimeException: Point of failure at .wrapper(:8) ... 34 more Caused by: java.lang.RuntimeException: Point of failure at .sample(: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 ``` --- .../scala/tools/nsc/util/StackTracing.scala | 76 ++++++++++ src/compiler/scala/tools/nsc/util/package.scala | 18 ++- src/repl/scala/tools/nsc/interpreter/IMain.scala | 4 +- test/files/run/repl-trim-stack-trace.scala | 8 ++ .../scala/tools/nsc/util/StackTraceTest.scala | 159 +++++++++++++++++++++ 5 files changed, 256 insertions(+), 9 deletions(-) create mode 100644 src/compiler/scala/tools/nsc/util/StackTracing.scala create mode 100644 test/junit/scala/tools/nsc/util/StackTraceTest.scala 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(:7) + ... 69 elided scala> def f = throw new Exception("") f: Nothing @@ -20,6 +21,7 @@ f: Nothing scala> f java.lang.Exception: at .f(:7) + ... 69 elided scala> def f = throw new Exception f: Nothing @@ -27,7 +29,13 @@ f: Nothing scala> f java.lang.Exception at .f(: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) + } + */ + } +} -- cgit v1.2.3 From c88e8be97777bd7bf1c8392a83f17e1583de2ffd Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Mon, 26 Aug 2013 03:52:58 -0700 Subject: Target junit.clean to clean junit artifacts And all.clean will also junit.clean. --- build.xml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/build.xml b/build.xml index c65a3531ee..4a2456eebf 100755 --- a/build.xml +++ b/build.xml @@ -845,7 +845,11 @@ TODO: - + + + + + -- cgit v1.2.3 From 534ced4885e75d89025d569cc2cbc2da8ed45cab Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Mon, 2 Sep 2013 07:56:03 -0700 Subject: SI-7781 Improve test and add comment The test should normalize the elided message, not strip it. (Thanks qerub; I was frustrated with kitteh's turnaround that night, hence unwilling to improve once it passed.) --- src/partest-extras/scala/tools/partest/ReplTest.scala | 5 +++++ test/files/run/repl-trim-stack-trace.scala | 13 ++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/partest-extras/scala/tools/partest/ReplTest.scala b/src/partest-extras/scala/tools/partest/ReplTest.scala index 7cc2dd39a9..54d1d04a02 100644 --- a/src/partest-extras/scala/tools/partest/ReplTest.scala +++ b/src/partest-extras/scala/tools/partest/ReplTest.scala @@ -30,6 +30,11 @@ abstract class ReplTest extends DirectTest { def show() = eval() foreach println } +/** Run a REPL test from a session transcript. + * The `session` should be a triple-quoted String starting + * with the `Type in expressions` message and ending + * after the final `prompt`, including the last space. + */ abstract class SessionTest extends ReplTest { def session: String override final def code = expected filter (_.startsWith(prompt)) map (_.drop(prompt.length)) mkString "\n" diff --git a/test/files/run/repl-trim-stack-trace.scala b/test/files/run/repl-trim-stack-trace.scala index db42b37fdd..0f4a43bc85 100644 --- a/test/files/run/repl-trim-stack-trace.scala +++ b/test/files/run/repl-trim-stack-trace.scala @@ -33,9 +33,12 @@ java.lang.Exception 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 + // normalize the "elided" lines because the frame count depends on test context + lazy val elided = """(\s+\.{3} )\d+( elided)""".r + def normalize(line: String) = line match { + case elided(ellipsis, suffix) => s"$ellipsis???$suffix" + case s => s + } + override def eval() = super.eval() map normalize + override def expected = super.expected map normalize } -- cgit v1.2.3 From 20b7ae6b8839be6593d1a3ca31ac938bb3431f25 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Mon, 2 Sep 2013 08:55:49 -0700 Subject: SI-7781 Comments to SessionTest Also, it would be nice if code and expected results were calculated lazily. That would allow tests with infinite code but which terminate on various conditions. --- .../scala/tools/partest/ReplTest.scala | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/partest-extras/scala/tools/partest/ReplTest.scala b/src/partest-extras/scala/tools/partest/ReplTest.scala index 54d1d04a02..38662c34b5 100644 --- a/src/partest-extras/scala/tools/partest/ReplTest.scala +++ b/src/partest-extras/scala/tools/partest/ReplTest.scala @@ -36,13 +36,24 @@ abstract class ReplTest extends DirectTest { * after the final `prompt`, including the last space. */ abstract class SessionTest extends ReplTest { + /** Session transcript, as a triple-quoted, multiline, marginalized string. */ def session: String - override final def code = expected filter (_.startsWith(prompt)) map (_.drop(prompt.length)) mkString "\n" - def expected = session.stripMargin.lines.toList + + /** Expected output, as an iterator. */ + def expected = session.stripMargin.lines + + /** Code is the command list culled from the session (or the expected session output). + * Would be nicer if code were lazy lines. + */ + override final def code = expected filter (_ startsWith prompt) map (_ drop prompt.length) mkString "\n" + final def prompt = "scala> " + + /** Default test is to compare expected and actual output and emit the diff on a failed comparison. */ override def show() = { - val out = eval().toList - if (out.size != expected.size) Console println s"Expected ${expected.size} lines, got ${out.size}" - if (out != expected) Console print nest.FileManager.compareContents(expected, out, "expected", "actual") + val evaled = eval().toList + val wanted = expected.toList + if (evaled.size != wanted.size) Console println s"Expected ${wanted.size} lines, got ${evaled.size}" + if (evaled != wanted) Console print nest.FileManager.compareContents(wanted, evaled, "expected", "actual") } } -- cgit v1.2.3