diff options
-rwxr-xr-x | build.xml | 6 | ||||
-rw-r--r-- | src/compiler/scala/tools/nsc/util/StackTracing.scala | 76 | ||||
-rw-r--r-- | src/compiler/scala/tools/nsc/util/package.scala | 18 | ||||
-rw-r--r-- | src/partest-extras/scala/tools/partest/ReplTest.scala | 26 | ||||
-rw-r--r-- | src/repl/scala/tools/nsc/interpreter/IMain.scala | 4 | ||||
-rw-r--r-- | test/files/run/repl-trim-stack-trace.scala | 11 | ||||
-rw-r--r-- | test/junit/scala/tools/nsc/util/StackTraceTest.scala | 159 |
7 files changed, 285 insertions, 15 deletions
@@ -845,7 +845,11 @@ TODO: <target name="docs.clean"> <clean build="docs"/> <delete dir="${build.dir}/manmaker" includeemptydirs="yes" quiet="yes" failonerror="no"/> </target> <target name="dist.clean"> <delete dir="${dists.dir}" includeemptydirs="yes" quiet="yes" failonerror="no"/> </target> - <target name="all.clean" depends="locker.clean, docs.clean"> <clean build="sbt"/> <clean build="osgi"/> </target> + <target name="junit.clean"> <clean build="junit"/> </target> + + <target name="all.clean" depends="locker.clean, docs.clean, junit.clean"> + <clean build="sbt"/> <clean build="osgi"/> + </target> <!-- Used by the scala-installer script --> <target name="allallclean" depends="all.clean, dist.clean"/> 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/partest-extras/scala/tools/partest/ReplTest.scala b/src/partest-extras/scala/tools/partest/ReplTest.scala index 7cc2dd39a9..38662c34b5 100644 --- a/src/partest-extras/scala/tools/partest/ReplTest.scala +++ b/src/partest-extras/scala/tools/partest/ReplTest.scala @@ -30,14 +30,30 @@ 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 { + /** 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") } } 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..0f4a43bc85 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,16 @@ f: Nothing scala> f java.lang.Exception at .f(<console>:7) + ... 69 elided scala> """ + // 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 } 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) + } + */ + } +} |