Skip to content

Commit 5044f1c

Browse files
committed
Merge pull request scala#2877 from som-snytt/issue/repl-stack-trunc
SI-7781 REPL stack trunc shows cause
2 parents a8c0527 + 20b7ae6 commit 5044f1c

File tree

7 files changed

+285
-15
lines changed

7 files changed

+285
-15
lines changed

build.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -845,7 +845,11 @@ TODO:
845845
<target name="docs.clean"> <clean build="docs"/> <delete dir="${build.dir}/manmaker" includeemptydirs="yes" quiet="yes" failonerror="no"/> </target>
846846
<target name="dist.clean"> <delete dir="${dists.dir}" includeemptydirs="yes" quiet="yes" failonerror="no"/> </target>
847847

848-
<target name="all.clean" depends="locker.clean, docs.clean"> <clean build="sbt"/> <clean build="osgi"/> </target>
848+
<target name="junit.clean"> <clean build="junit"/> </target>
849+
850+
<target name="all.clean" depends="locker.clean, docs.clean, junit.clean">
851+
<clean build="sbt"/> <clean build="osgi"/>
852+
</target>
849853

850854
<!-- Used by the scala-installer script -->
851855
<target name="allallclean" depends="all.clean, dist.clean"/>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/* NSC -- new Scala compiler
2+
* Copyright 2005-2013 LAMP/EPFL
3+
*/
4+
5+
package scala.tools.nsc.util
6+
7+
private[util] trait StackTracing extends Any {
8+
9+
/** Format a stack trace, returning the prefix consisting of frames that satisfy
10+
* a given predicate.
11+
* The format is similar to the typical case described in the JavaDoc
12+
* for [[java.lang.Throwable#printStackTrace]].
13+
* If a stack trace is truncated, it will be followed by a line of the form
14+
* `... 3 elided`, by analogy to the lines `... 3 more` which indicate
15+
* shared stack trace segments.
16+
* @param e the exception
17+
* @param p the predicate to select the prefix
18+
*/
19+
def stackTracePrefixString(e: Throwable)(p: StackTraceElement => Boolean): String = {
20+
import collection.mutable.{ ArrayBuffer, ListBuffer }
21+
import compat.Platform.EOL
22+
import util.Properties.isJavaAtLeast
23+
24+
val sb = ListBuffer.empty[String]
25+
26+
type TraceRelation = String
27+
val Self = new TraceRelation("")
28+
val CausedBy = new TraceRelation("Caused by: ")
29+
val Suppressed = new TraceRelation("Suppressed: ")
30+
31+
val suppressable = isJavaAtLeast("1.7")
32+
33+
def clazz(e: Throwable) = e.getClass.getName
34+
def because(e: Throwable): String = e.getCause match { case null => null ; case c => header(c) }
35+
def msg(e: Throwable): String = e.getMessage match { case null => because(e) ; case s => s }
36+
def txt(e: Throwable): String = msg(e) match { case null => "" ; case s => s": $s" }
37+
def header(e: Throwable): String = s"${clazz(e)}${txt(e)}"
38+
39+
val indent = "\u0020\u0020"
40+
41+
val seen = new ArrayBuffer[Throwable](16)
42+
def unseen(t: Throwable) = {
43+
def inSeen = seen exists (_ eq t)
44+
val interesting = (t != null) && !inSeen
45+
if (interesting) seen += t
46+
interesting
47+
}
48+
49+
def print(e: Throwable, r: TraceRelation, share: Array[StackTraceElement], indents: Int): Unit = if (unseen(e)) {
50+
val trace = e.getStackTrace
51+
val frames = (
52+
if (share.nonEmpty) {
53+
val spare = share.reverseIterator
54+
val trimmed = trace.reverse dropWhile (spare.hasNext && spare.next == _)
55+
trimmed.reverse
56+
} else trace
57+
)
58+
val prefix = frames takeWhile p
59+
val margin = indent * indents
60+
val indented = margin + indent
61+
sb append s"${margin}${r}${header(e)}"
62+
prefix foreach (f => sb append s"${indented}at $f")
63+
if (frames.size < trace.size) sb append s"$indented... ${trace.size - frames.size} more"
64+
if (r == Self && prefix.size < frames.size) sb append s"$indented... ${frames.size - prefix.size} elided"
65+
print(e.getCause, CausedBy, trace, indents)
66+
if (suppressable) {
67+
import scala.language.reflectiveCalls
68+
type Suppressing = { def getSuppressed(): Array[Throwable] }
69+
for (s <- e.asInstanceOf[Suppressing].getSuppressed) print(s, Suppressed, frames, indents + 1)
70+
}
71+
}
72+
print(e, Self, share = Array.empty, indents = 0)
73+
74+
sb mkString EOL
75+
}
76+
}

src/compiler/scala/tools/nsc/util/package.scala

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ package tools
88
package nsc
99

1010
import java.io.{ OutputStream, PrintStream, ByteArrayOutputStream, PrintWriter, StringWriter }
11-
import scala.compat.Platform.EOL
1211

1312
package object util {
1413

@@ -79,12 +78,17 @@ package object util {
7978
s"$clazz$msg @ $frame"
8079
}
8180

82-
def stackTracePrefixString(ex: Throwable)(p: StackTraceElement => Boolean): String = {
83-
val frames = ex.getStackTrace takeWhile p map (" at " + _)
84-
val msg = ex.getMessage match { case null => "" ; case s => s": $s" }
85-
val clazz = ex.getClass.getName
86-
87-
s"$clazz$msg" +: frames mkString EOL
81+
implicit class StackTraceOps(val e: Throwable) extends AnyVal with StackTracing {
82+
/** Format the stack trace, returning the prefix consisting of frames that satisfy
83+
* a given predicate.
84+
* The format is similar to the typical case described in the JavaDoc
85+
* for [[java.lang.Throwable#printStackTrace]].
86+
* If a stack trace is truncated, it will be followed by a line of the form
87+
* `... 3 elided`, by analogy to the lines `... 3 more` which indicate
88+
* shared stack trace segments.
89+
* @param p the predicate to select the prefix
90+
*/
91+
def stackTracePrefixString(p: StackTraceElement => Boolean): String = stackTracePrefixString(e)(p)
8892
}
8993

9094
lazy val trace = new SimpleTracer(System.out)

src/partest-extras/scala/tools/partest/ReplTest.scala

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,30 @@ abstract class ReplTest extends DirectTest {
3030
def show() = eval() foreach println
3131
}
3232

33+
/** Run a REPL test from a session transcript.
34+
* The `session` should be a triple-quoted String starting
35+
* with the `Type in expressions` message and ending
36+
* after the final `prompt`, including the last space.
37+
*/
3338
abstract class SessionTest extends ReplTest {
39+
/** Session transcript, as a triple-quoted, multiline, marginalized string. */
3440
def session: String
35-
override final def code = expected filter (_.startsWith(prompt)) map (_.drop(prompt.length)) mkString "\n"
36-
def expected = session.stripMargin.lines.toList
41+
42+
/** Expected output, as an iterator. */
43+
def expected = session.stripMargin.lines
44+
45+
/** Code is the command list culled from the session (or the expected session output).
46+
* Would be nicer if code were lazy lines.
47+
*/
48+
override final def code = expected filter (_ startsWith prompt) map (_ drop prompt.length) mkString "\n"
49+
3750
final def prompt = "scala> "
51+
52+
/** Default test is to compare expected and actual output and emit the diff on a failed comparison. */
3853
override def show() = {
39-
val out = eval().toList
40-
if (out.size != expected.size) Console println s"Expected ${expected.size} lines, got ${out.size}"
41-
if (out != expected) Console print nest.FileManager.compareContents(expected, out, "expected", "actual")
54+
val evaled = eval().toList
55+
val wanted = expected.toList
56+
if (evaled.size != wanted.size) Console println s"Expected ${wanted.size} lines, got ${evaled.size}"
57+
if (evaled != wanted) Console print nest.FileManager.compareContents(wanted, evaled, "expected", "actual")
4258
}
4359
}

src/repl/scala/tools/nsc/interpreter/IMain.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import scala.reflect.internal.util.{ BatchSourceFile, SourceFile }
2222
import scala.tools.util.PathResolver
2323
import scala.tools.nsc.io.AbstractFile
2424
import scala.tools.nsc.typechecker.{ TypeStrings, StructuredTypeStrings }
25-
import scala.tools.nsc.util.{ ScalaClassLoader, stringFromWriter, stackTracePrefixString }
25+
import scala.tools.nsc.util.{ ScalaClassLoader, stringFromWriter, StackTraceOps }
2626
import scala.tools.nsc.util.Exceptional.unwrap
2727

2828
import javax.script.{AbstractScriptEngine, Bindings, ScriptContext, ScriptEngine, ScriptEngineFactory, ScriptException, CompiledScript, Compilable}
@@ -726,7 +726,7 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set
726726
def isWrapperInit(x: StackTraceElement) = cond(x.getClassName) {
727727
case classNameRegex() if x.getMethodName == nme.CONSTRUCTOR.decoded => true
728728
}
729-
val stackTrace = util.stackTracePrefixString(unwrapped)(!isWrapperInit(_))
729+
val stackTrace = unwrapped stackTracePrefixString (!isWrapperInit(_))
730730

731731
withLastExceptionLock[String]({
732732
directBind[Throwable]("lastException", unwrapped)(StdReplTags.tagOfThrowable, classTag[Throwable])

test/files/run/repl-trim-stack-trace.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,32 @@ f: Nothing
1313
scala> f
1414
java.lang.Exception: Uh-oh
1515
at .f(<console>:7)
16+
... 69 elided
1617
1718
scala> def f = throw new Exception("")
1819
f: Nothing
1920
2021
scala> f
2122
java.lang.Exception:
2223
at .f(<console>:7)
24+
... 69 elided
2325
2426
scala> def f = throw new Exception
2527
f: Nothing
2628
2729
scala> f
2830
java.lang.Exception
2931
at .f(<console>:7)
32+
... 69 elided
3033
3134
scala> """
3235

36+
// normalize the "elided" lines because the frame count depends on test context
37+
lazy val elided = """(\s+\.{3} )\d+( elided)""".r
38+
def normalize(line: String) = line match {
39+
case elided(ellipsis, suffix) => s"$ellipsis???$suffix"
40+
case s => s
41+
}
42+
override def eval() = super.eval() map normalize
43+
override def expected = super.expected map normalize
3344
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
2+
package scala.tools.nsc.util
3+
4+
import scala.language.reflectiveCalls
5+
import scala.util._
6+
import PartialFunction.cond
7+
import Properties.isJavaAtLeast
8+
9+
import org.junit.Assert._
10+
import org.junit.Test
11+
import org.junit.runner.RunWith
12+
import org.junit.runners.JUnit4
13+
14+
trait Expecting {
15+
/*
16+
import org.expecty.Expecty
17+
final val expect = new Expecty
18+
*/
19+
}
20+
21+
22+
@RunWith(classOf[JUnit4])
23+
class StackTraceTest extends Expecting {
24+
// formerly an enum
25+
val CausedBy = "Caused by: "
26+
val Suppressed = "Suppressed: "
27+
28+
// throws
29+
def sample = throw new RuntimeException("Point of failure")
30+
def sampler: String = sample
31+
32+
// repackage with message
33+
def resample: String = try { sample } catch { case e: Throwable => throw new RuntimeException("resample", e) }
34+
def resampler: String = resample
35+
36+
// simple wrapper
37+
def wrapper: String = try { sample } catch { case e: Throwable => throw new RuntimeException(e) }
38+
// another onion skin
39+
def rewrapper: String = try { wrapper } catch { case e: Throwable => throw new RuntimeException(e) }
40+
def rewrapperer: String = rewrapper
41+
42+
// only an insane wretch would do this
43+
def insane: String = try { sample } catch {
44+
case e: Throwable =>
45+
val t = new RuntimeException(e)
46+
e initCause t
47+
throw t
48+
}
49+
def insaner: String = insane
50+
51+
/** Java 7 */
52+
val suppressable = isJavaAtLeast("1.7")
53+
type Suppressing = { def addSuppressed(t: Throwable): Unit }
54+
55+
def repressed: String = try { sample } catch {
56+
case e: Throwable =>
57+
val t = new RuntimeException("My problem")
58+
if (suppressable) {
59+
t.asInstanceOf[Suppressing] addSuppressed e
60+
}
61+
throw t
62+
}
63+
def represser: String = repressed
64+
65+
// evaluating s should throw, p trims stack trace, t is the test of resulting trace string
66+
def probe(s: =>String)(p: StackTraceElement => Boolean)(t: String => Unit): Unit = {
67+
Try(s) recover { case e => e stackTracePrefixString p } match {
68+
case Success(s) => t(s)
69+
case Failure(e) => throw e
70+
}
71+
}
72+
73+
@Test def showsAllTrace() {
74+
probe(sampler)(_ => true) { s =>
75+
val res = s.lines.toList
76+
/*
77+
expect {
78+
res.length > 5 // many lines
79+
// these expectations may be framework-specific
80+
//s contains "sbt.TestFramework"
81+
//res.last contains "java.lang.Thread"
82+
}
83+
*/
84+
assert (res.length > 5)
85+
}
86+
}
87+
@Test def showsOnlyPrefix() = probe(sample)(_.getMethodName == "sample") { s =>
88+
val res = s.lines.toList
89+
/*
90+
expect {
91+
res.length == 3 // summary + one frame + elision
92+
}
93+
*/
94+
assert (res.length == 3)
95+
}
96+
@Test def showsCause() = probe(resampler)(_.getMethodName != "resampler") { s =>
97+
val res = s.lines.toList
98+
/*
99+
expect {
100+
res.length == 6 // summary + one frame + elision, caused by + one frame + elision
101+
res exists (_ startsWith CausedBy.toString)
102+
}
103+
*/
104+
assert (res.length == 6)
105+
assert (res exists (_ startsWith CausedBy.toString))
106+
}
107+
@Test def showsWrappedExceptions() = probe(rewrapperer)(_.getMethodName != "rewrapperer") { s =>
108+
val res = s.lines.toList
109+
/*
110+
expect {
111+
res.length == 9 // summary + one frame + elision times three
112+
res exists (_ startsWith CausedBy.toString)
113+
(res collect {
114+
case s if s startsWith CausedBy.toString => s
115+
}).size == 2
116+
}
117+
*/
118+
assert (res.length == 9)
119+
assert (res exists (_ startsWith CausedBy.toString))
120+
assert ((res collect {
121+
case s if s startsWith CausedBy.toString => s
122+
}).size == 2)
123+
}
124+
@Test def dontBlowOnCycle() = probe(insaner)(_.getMethodName != "insaner") { s =>
125+
val res = s.lines.toList
126+
/*
127+
expect {
128+
res.length == 7 // summary + one frame + elision times two with extra frame
129+
res exists (_ startsWith CausedBy.toString)
130+
}
131+
*/
132+
assert (res.length == 7)
133+
assert (res exists (_ startsWith CausedBy.toString))
134+
}
135+
136+
/** Java 7, but shouldn't bomb on Java 6.
137+
*
138+
java.lang.RuntimeException: My problem
139+
at scala.tools.nsc.util.StackTraceTest.repressed(StackTraceTest.scala:56)
140+
... 27 elided
141+
Suppressed: java.lang.RuntimeException: Point of failure
142+
at scala.tools.nsc.util.StackTraceTest.sample(StackTraceTest.scala:29)
143+
at scala.tools.nsc.util.StackTraceTest.repressed(StackTraceTest.scala:54)
144+
... 27 more
145+
*/
146+
@Test def showsSuppressed() = probe(represser)(_.getMethodName != "represser") { s =>
147+
val res = s.lines.toList
148+
if (suppressable) {
149+
assert (res.length == 7)
150+
assert (res exists (_.trim startsWith Suppressed.toString))
151+
}
152+
/*
153+
expect {
154+
res.length == 7
155+
res exists (_ startsWith " " + Suppressed.toString)
156+
}
157+
*/
158+
}
159+
}

0 commit comments

Comments
 (0)