Skip to content

Commit 5f985ad

Browse files
committed
Improve error reporting
1 parent ac56f3d commit 5f985ad

File tree

2 files changed

+91
-63
lines changed

2 files changed

+91
-63
lines changed
Lines changed: 58 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,35 @@
11
package dotty.tools.debug
22

33
import com.sun.jdi.Location
4-
import dotty.tools.io.JFile
4+
import dotty.tools.io.JPath
55
import dotty.tools.readLines
66

7+
import scala.annotation.tailrec
8+
79
/**
810
* A debug step and an associated assertion to validate the step.
911
* A sequence of DebugStepAssert is parsed from the check file in tests/debug
1012
*/
11-
private[debug] case class DebugStepAssert[T](step: DebugStep[T], assert: T => Unit)
13+
private[debug] case class DebugStepAssert[T](step: DebugStep[T], assert: T => Unit)(
14+
using val location: CheckFileLocation
15+
)
16+
17+
/** A location in the check file */
18+
private[debug] case class CheckFileLocation(checkFile: JPath, line: Int):
19+
override def toString: String = s"$checkFile:$line"
20+
21+
/** When a DebugStepAssert fails it throws a DebugStepException */
22+
private[debug] case class DebugStepException(message: String, location: CheckFileLocation) extends Exception
23+
24+
private[debug] enum DebugStep[T]:
25+
case Break(className: String, line: Int) extends DebugStep[Location]
26+
case Step extends DebugStep[Location]
27+
case Next extends DebugStep[Location]
28+
case Eval(expression: String) extends DebugStep[Either[String, String]]
1229

1330
private[debug] object DebugStepAssert:
1431
import DebugStep.*
15-
def parseCheckFile(checkFile: JFile): Seq[DebugStepAssert[?]] =
32+
def parseCheckFile(checkFile: JPath): Seq[DebugStepAssert[?]] =
1633
val sym = "[a-zA-Z0-9$.]+"
1734
val line = "\\d+"
1835
val trailing = s"\\s*(?:\\/\\/.*)?".r // empty or comment
@@ -24,13 +41,16 @@ private[debug] object DebugStepAssert:
2441
val result = "result (.*)".r
2542
val error = "error (.*)".r
2643
val multiLineError = s"error$trailing".r
44+
val allLines = readLines(checkFile.toFile)
2745

46+
@tailrec
2847
def loop(lines: List[String], acc: List[DebugStepAssert[?]]): List[DebugStepAssert[?]] =
48+
given location: CheckFileLocation = CheckFileLocation(checkFile, allLines.size - lines.size + 1)
2949
lines match
3050
case Nil => acc.reverse
3151
case break(className , lineStr) :: tail =>
32-
val line = lineStr.toInt
33-
val step = DebugStepAssert(Break(className, line), checkClassAndLine(className, line))
52+
val breakpointLine = lineStr.toInt
53+
val step = DebugStepAssert(Break(className, breakpointLine), checkClassAndLine(className, breakpointLine))
3454
loop(tail, step :: acc)
3555
case step(pattern) :: tail =>
3656
val step = DebugStepAssert(Step, checkLineOrMethod(pattern))
@@ -49,70 +69,70 @@ private[debug] object DebugStepAssert:
4969
val step = DebugStepAssert(Eval(expr), assertion)
5070
loop(tail2, step :: acc)
5171
case trailing() :: tail => loop(tail, acc)
52-
case invalid :: tail => throw new Exception(s"Cannot parse debug step: $invalid")
72+
case invalid :: tail =>
73+
throw new Exception(s"Cannot parse debug step: $invalid ($location)")
5374

5475
def parseEvalAssertion(lines: List[String]): (Either[String, String] => Unit, List[String]) =
76+
given location: CheckFileLocation = CheckFileLocation(checkFile, allLines.size - lines.size + 1)
5577
lines match
5678
case Nil => throw new Exception(s"Missing result or error")
79+
case trailing() :: tail => parseEvalAssertion(tail)
5780
case result(expected) :: tail => (checkResult(expected), tail)
5881
case error(expected) :: tail => (checkError(Seq(expected)), tail)
5982
case multiLineError() :: tail0 =>
6083
val (expected, tail1) = tail0.span(_.startsWith(" "))
6184
(checkError(expected.map(_.stripPrefix(" "))), tail1)
62-
case invalid :: _ => throw new Exception(s"Cannot parse as result or error: $invalid")
85+
case invalid :: _ =>
86+
throw new Exception(s"Cannot parse as result or error: $invalid ($location)")
6387

64-
loop(readLines(checkFile), Nil)
88+
loop(allLines, Nil)
6589
end parseCheckFile
6690

67-
private def checkClassAndLine(className: String, line: Int)(location: Location): Unit =
68-
assert(className == location.declaringType.name, s"obtained ${location.declaringType.name}, expected ${className}")
69-
checkLine(line)(location)
91+
private def checkClassAndLine(className: String, breakpointLine: Int)(using CheckFileLocation)(location: Location): Unit =
92+
debugStepAssertEquals(location.declaringType.name, className)
93+
checkLine(breakpointLine)(location)
7094

71-
private def checkLineOrMethod(pattern: String): Location => Unit =
95+
private def checkLineOrMethod(pattern: String)(using CheckFileLocation): Location => Unit =
7296
if "(\\d+)".r.matches(pattern) then checkLine(pattern.toInt) else checkMethod(pattern)
7397

74-
private def checkLine(line: Int)(location: Location): Unit =
75-
assert(location.lineNumber == line, s"obtained ${location.lineNumber}, expected $line")
98+
private def checkLine(expected: Int)(using CheckFileLocation)(location: Location): Unit =
99+
debugStepAssertEquals(location.lineNumber, expected)
76100

77-
private def checkMethod(methodName: String)(location: Location): Unit = assert(methodName == location.method.name)
101+
private def checkMethod(expected: String)(using CheckFileLocation)(location: Location): Unit =
102+
debugStepAssertEquals(location.method.name, expected)
78103

79-
private def checkResult(expected: String)(obtained: Either[String, String]): Unit =
104+
private def checkResult(expected: String)(using CheckFileLocation)(obtained: Either[String, String]): Unit =
80105
obtained match
81106
case Left(obtained) =>
82-
val message =
107+
debugStepFailed(
83108
s"""|Evaluation failed:
84109
|${obtained.replace("\n", "\n|")}""".stripMargin
85-
throw new AssertionError(message)
86-
case Right(obtained) =>
87-
val message =
88-
s"""|Expected: $expected
89-
|Obtained: $obtained""".stripMargin
90-
assert(expected.r.matches(obtained.toString), message)
110+
)
111+
case Right(obtained) => debugStepAssertEquals(obtained, expected)
91112

92-
private def checkError(expected: Seq[String])(obtained: Either[String, String]): Unit =
113+
private def checkError(expected: Seq[String])(using CheckFileLocation)(obtained: Either[String, String]): Unit =
93114
obtained match
94115
case Left(obtained) =>
95-
val message =
116+
debugStepAssert(
117+
expected.forall(e => e.r.findFirstMatchIn(obtained).isDefined),
96118
s"""|Expected:
97-
|${expected.mkString("\n")}
119+
|${expected.mkString("\n|")}
98120
|Obtained:
99121
|${obtained.replace("\n", "\n|")}""".stripMargin
100-
assert(expected.forall(e => e.r.findFirstMatchIn(obtained).isDefined), message)
122+
)
101123
case Right(obtained) =>
102-
val message =
124+
debugStepFailed(
103125
s"""|Evaluation succeeded but failure expected.
104126
|Obtained: $obtained
105127
|""".stripMargin
106-
throw new AssertionError(message)
107-
108-
109-
end DebugStepAssert
110-
111-
private[debug] enum DebugStep[T]:
112-
case Break(className: String, line: Int) extends DebugStep[Location]
113-
case Step extends DebugStep[Location]
114-
case Next extends DebugStep[Location]
115-
case Eval(expression: String) extends DebugStep[Either[String, String]]
128+
)
116129

130+
private def debugStepAssertEquals[T](obtained: T, expected: T)(using CheckFileLocation): Unit =
131+
debugStepAssert(obtained == expected, s"Obtained $obtained, Expected: $expected")
117132

133+
private def debugStepAssert(assertion: Boolean, message: String)(using CheckFileLocation): Unit =
134+
if !assertion then debugStepFailed(message)
118135

136+
private def debugStepFailed(message: String)(using location: CheckFileLocation): Unit =
137+
throw DebugStepException(message, location)
138+
end DebugStepAssert

compiler/test/dotty/tools/debug/DebugTests.scala

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -45,32 +45,26 @@ object DebugTests extends ParallelTesting:
4545
private def verifyDebug(dir: JFile, testSource: TestSource, warnings: Int, reporters: Seq[TestReporter], logger: LoggedRunnable) =
4646
if Properties.testsNoRun then addNoRunWarning()
4747
else
48-
val checkFile = testSource.checkFile.getOrElse(throw new Exception("Missing check file"))
48+
val checkFile = testSource.checkFile.getOrElse(throw new Exception("Missing check file")).toPath
4949
val debugSteps = DebugStepAssert.parseCheckFile(checkFile)
50-
val expressionEvaluator = ExpressionEvaluator(testSource.sourceFiles, testSource.flags, testSource.runClassPath, testSource.outDir)
51-
val status = debugMain(testSource.runClassPath): debuggee =>
52-
val debugger = Debugger(debuggee.jdiPort, expressionEvaluator, maxDuration/* , verbose = true */)
53-
// configure the breakpoints before starting the debuggee
54-
val breakpoints = debugSteps.map(_.step).collect { case b: DebugStep.Break => b }
55-
for b <- breakpoints do debugger.configureBreakpoint(b.className, b.line)
56-
try
57-
debuggee.launch()
58-
playDebugSteps(debugger, debugSteps/* , verbose = true */)
59-
finally
60-
// stop debugger to let debuggee terminate its execution
61-
debugger.dispose()
62-
status match
63-
case Success(output) => ()
64-
case Failure(output) =>
65-
if output == "" then
66-
echo(s"Test '${testSource.title}' failed with no output")
67-
else
68-
echo(s"Test '${testSource.title}' failed with output:")
69-
echo(output)
70-
failTestSource(testSource)
71-
case Timeout =>
72-
echo("failed because test " + testSource.title + " timed out")
73-
failTestSource(testSource, TimeoutFailure(testSource.title))
50+
val expressionEvaluator =
51+
ExpressionEvaluator(testSource.sourceFiles, testSource.flags, testSource.runClassPath, testSource.outDir)
52+
try
53+
val status = debugMain(testSource.runClassPath): debuggee =>
54+
val debugger = Debugger(debuggee.jdiPort, expressionEvaluator, maxDuration/* , verbose = true */)
55+
// configure the breakpoints before starting the debuggee
56+
val breakpoints = debugSteps.map(_.step).collect { case b: DebugStep.Break => b }
57+
for b <- breakpoints do debugger.configureBreakpoint(b.className, b.line)
58+
try
59+
debuggee.launch()
60+
playDebugSteps(debugger, debugSteps/* , verbose = true */)
61+
finally
62+
// stop debugger to let debuggee terminate its execution
63+
debugger.dispose()
64+
reportDebuggeeStatus(testSource, status)
65+
catch case DebugStepException(message, location) =>
66+
echo(s"\nDebug step failed: $location\n" + message)
67+
failTestSource(testSource)
7468
end verifyDebug
7569

7670
private def playDebugSteps(debugger: Debugger, steps: Seq[DebugStepAssert[?]], verbose: Boolean = false): Unit =
@@ -111,4 +105,18 @@ object DebugTests extends ParallelTesting:
111105
if verbose then println(s"eval $expr $result")
112106
assert(result)
113107
end playDebugSteps
108+
109+
private def reportDebuggeeStatus(testSource: TestSource, status: Status): Unit =
110+
status match
111+
case Success(output) => ()
112+
case Failure(output) =>
113+
if output == "" then
114+
echo(s"Test '${testSource.title}' failed with no output")
115+
else
116+
echo(s"Test '${testSource.title}' failed with output:")
117+
echo(output)
118+
failTestSource(testSource)
119+
case Timeout =>
120+
echo("failed because test " + testSource.title + " timed out")
121+
failTestSource(testSource, TimeoutFailure(testSource.title))
114122
end DebugTest

0 commit comments

Comments
 (0)