Skip to content

Commit df36c69

Browse files
committed
Add debugMain method
1 parent ee7f7c7 commit df36c69

File tree

4 files changed

+97
-81
lines changed

4 files changed

+97
-81
lines changed

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

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,19 @@ object DebugTests extends ParallelTesting:
4141

4242
private def verifyDebug(dir: JFile, testSource: TestSource, warnings: Int, reporters: Seq[TestReporter], logger: LoggedRunnable) =
4343
if Properties.testsNoRun then addNoRunWarning()
44-
else runMain(testSource.runClassPath) match
45-
case Success(output) => ()
46-
case Failure(output) =>
47-
if output == "" then
48-
echo(s"Test '${testSource.title}' failed with no output")
49-
else
50-
echo(s"Test '${testSource.title}' failed with output:")
51-
echo(output)
52-
failTestSource(testSource)
53-
case Timeout =>
54-
echo("failed because test " + testSource.title + " timed out")
55-
failTestSource(testSource, TimeoutFailure(testSource.title))
44+
else
45+
val status = debugMain(testSource.runClassPath)(_.launch())
46+
status match
47+
case Success(output) => ()
48+
case Failure(output) =>
49+
if output == "" then
50+
echo(s"Test '${testSource.title}' failed with no output")
51+
else
52+
echo(s"Test '${testSource.title}' failed with output:")
53+
echo(output)
54+
failTestSource(testSource)
55+
case Timeout =>
56+
echo("failed because test " + testSource.title + " timed out")
57+
failTestSource(testSource, TimeoutFailure(testSource.title))
5658

5759
end DebugTest

compiler/test/dotty/tools/vulpix/ParallelTesting.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -929,7 +929,7 @@ trait ParallelTesting extends RunnerOrchestration { self =>
929929

930930
private def verifyOutput(checkFile: Option[JFile], dir: JFile, testSource: TestSource, warnings: Int, reporters: Seq[TestReporter], logger: LoggedRunnable) = {
931931
if Properties.testsNoRun then addNoRunWarning()
932-
else runMain(testSource.runClassPath) match {
932+
else runMain(testSource.runClassPath, testSource.allToolArgs) match {
933933
case Success(output) => checkFile match {
934934
case Some(file) if file.exists => diffTest(testSource, file, output.linesIterator.toList, reporters, logger)
935935
case _ =>

compiler/test/dotty/tools/vulpix/RunnerOrchestration.scala

Lines changed: 79 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,23 @@ trait RunnerOrchestration {
5555
/** Open JDI connection for testing the debugger */
5656
def debugMode: Boolean = false
5757

58-
/** Running a `Test` class's main method from the specified `dir` */
59-
def runMain(classPath: String)(implicit summaryReport: SummaryReporting): Status =
58+
/** Running a `Test` class's main method from the specified `classpath` */
59+
def runMain(classPath: String, toolArgs: ToolArgs)(implicit summaryReport: SummaryReporting): Status =
6060
monitor.runMain(classPath)
6161

62+
trait Debuggee:
63+
// the jdi port to connect the debugger
64+
def jdiPort: Int
65+
// start the main method in the background
66+
def launch(): Unit
67+
68+
/** Provide a Debuggee for debugging the Test class's main method
69+
* @param f the debugging flow: set breakpoints, launch main class, pause, step, evaluate etc
70+
*/
71+
def debugMain(classPath: String)(f: Debuggee => Unit)(implicit summaryReport: SummaryReporting): Status =
72+
assert(debugMode, "debugMode is disabled")
73+
monitor.debugMain(classPath)(f)
74+
6275
/** Kill all processes */
6376
def cleanup() = monitor.killAll()
6477

@@ -73,13 +86,22 @@ trait RunnerOrchestration {
7386
* it died
7487
*/
7588
private class RunnerMonitor {
89+
/** Did add hook to kill the child VMs? */
90+
private val didAddCleanupCallback = new AtomicBoolean(false)
7691

7792
def runMain(classPath: String)(implicit summaryReport: SummaryReporting): Status =
7893
withRunner(_.runMain(classPath))
7994

80-
private class Runner(private var process: RunnerProcess) {
81-
private var childStdout: BufferedReader = uninitialized
82-
private var childStdin: PrintStream = uninitialized
95+
def debugMain(classPath: String)(f: Debuggee => Unit)(implicit summaryReport: SummaryReporting): Status =
96+
withRunner(_.debugMain(classPath)(f))
97+
98+
// A JVM process and its JDI port for debugging, if debugMode is enabled.
99+
private class RunnerProcess(p: Process, val jdiPort: Option[Int]):
100+
val stdout = new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))
101+
val stdin = new PrintStream(p.getOutputStream(), /* autoFlush = */ true)
102+
export p.{exitValue, isAlive, destroy}
103+
104+
private class Runner(private var process: RunnerProcess):
83105

84106
/** Checks if `process` is still alive
85107
*
@@ -92,83 +114,72 @@ trait RunnerOrchestration {
92114
catch { case _: IllegalThreadStateException => true }
93115

94116
/** Destroys the underlying process and kills IO streams */
95-
def kill(): Unit = {
117+
def kill(): Unit =
96118
if (process ne null) process.destroy()
97119
process = null
98-
childStdout = null
99-
childStdin = null
100-
}
101-
102-
/** Did add hook to kill the child VMs? */
103-
private val didAddCleanupCallback = new AtomicBoolean(false)
104120

105121
/** Blocks less than `maxDuration` while running `Test.main` from `dir` */
106-
def runMain(classPath: String)(implicit summaryReport: SummaryReporting): Status = {
107-
if (didAddCleanupCallback.compareAndSet(false, true)) {
108-
// If for some reason the test runner (i.e. sbt) doesn't kill the VM, we
109-
// need to clean up ourselves.
110-
summaryReport.addCleanup(() => killAll())
111-
}
112-
assert(process ne null,
113-
"Runner was killed and then reused without setting a new process")
114-
115-
// Makes the encapsulating RunnerMonitor spawn a new runner
116-
def respawn(): Unit = {
117-
process.destroy()
118-
process = createProcess
119-
childStdout = null
120-
childStdin = null
121-
}
122-
123-
if (childStdin eq null)
124-
childStdin = new PrintStream(process.getOutputStream(), /* autoFlush = */ true)
125-
122+
def runMain(classPath: String): Status =
123+
assert(process ne null, "Runner was killed and then reused without setting a new process")
124+
awaitStatusOrRespawn(startMain(classPath))
125+
126+
def debugMain(classPath: String)(f: Debuggee => Unit): Status =
127+
assert(process ne null, "Runner was killed and then reused without setting a new process")
128+
assert(process.jdiPort.isDefined, "Runner has not been started in debug mode")
129+
130+
var mainFuture: Future[Status] = null
131+
val debuggee = new Debuggee:
132+
def jdiPort: Int = process.jdiPort.get
133+
def launch(): Unit =
134+
mainFuture = startMain(classPath)
135+
136+
try f(debuggee)
137+
catch case debugFailure: Throwable =>
138+
if mainFuture != null then awaitStatusOrRespawn(mainFuture)
139+
throw debugFailure
140+
141+
assert(mainFuture ne null, "main method not started by debugger")
142+
awaitStatusOrRespawn(mainFuture)
143+
end debugMain
144+
145+
private def startMain(classPath: String): Future[Status] =
126146
// pass file to running process
127-
childStdin.println(classPath)
147+
process.stdin.println(classPath)
128148

129149
// Create a future reading the object:
130-
val readOutput = Future {
150+
Future:
131151
val sb = new StringBuilder
132152

133-
if (childStdout eq null)
134-
childStdout = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))
135-
136-
var childOutput: String = childStdout.readLine()
153+
var childOutput: String = process.stdout.readLine()
137154

138155
// Discard all messages until the test starts
139156
while (childOutput != ChildJVMMain.MessageStart && childOutput != null)
140-
childOutput = childStdout.readLine()
141-
childOutput = childStdout.readLine()
157+
childOutput = process.stdout.readLine()
158+
childOutput = process.stdout.readLine()
142159

143-
while (childOutput != ChildJVMMain.MessageEnd && childOutput != null) {
160+
while childOutput != ChildJVMMain.MessageEnd && childOutput != null do
144161
sb.append(childOutput).append(System.lineSeparator)
145-
childOutput = childStdout.readLine()
146-
}
162+
childOutput = process.stdout.readLine()
147163

148164
if (process.isAlive() && childOutput != null) Success(sb.toString)
149165
else Failure(sb.toString)
150-
}
151-
152-
// Await result for `maxDuration` and then timout and destroy the
153-
// process:
154-
val status =
155-
try Await.result(readOutput, maxDuration)
156-
catch { case _: TimeoutException => Timeout }
157-
158-
// Handle failure of the VM:
159-
status match {
160-
case _: Success if safeMode => respawn()
161-
case _: Success => // no need to respawn sub process
162-
case _: Failure => respawn()
163-
case Timeout => respawn()
164-
}
166+
end startMain
167+
168+
// wait status of the main class execution, respawn if failure or timeout
169+
private def awaitStatusOrRespawn(future: Future[Status]): Status =
170+
val status = try Await.result(future, maxDuration)
171+
catch case _: TimeoutException => Timeout
172+
// handle failures
173+
status match
174+
case _: Success if !safeMode => () // no need to respawn
175+
case _ => respawn() // safeMode, failure or timeout
165176
status
166-
}
167-
}
168177

169-
// A Java process and its JDI port for debugging, if debugMode is enabled.
170-
private class RunnerProcess(p: Process, val port: Option[Int]):
171-
export p.*
178+
// Makes the encapsulating RunnerMonitor spawn a new runner
179+
private def respawn(): Unit =
180+
process.destroy()
181+
process = createProcess
182+
end Runner
172183

173184
/** Create a process which has the classpath of the `ChildJVMMain` and the
174185
* scala library.
@@ -216,12 +227,15 @@ trait RunnerOrchestration {
216227
notify()
217228
}
218229

219-
private def withRunner[T](op: Runner => T): T = {
230+
private def withRunner[T](op: Runner => T)(using summaryReport: SummaryReporting): T =
231+
// If for some reason the test runner (i.e. sbt) doesn't kill the VM,
232+
// we need to clean up ourselves.
233+
if didAddCleanupCallback.compareAndSet(false, true) then
234+
summaryReport.addCleanup(() => killAll())
220235
val runner = getRunner()
221236
val result = op(runner)
222237
freeRunner(runner)
223238
result
224-
}
225239

226240
def killAll(): Unit = {
227241
freeRunners.foreach(_.kill())

sjs-compiler-tests/test/scala/dotty/tools/dotc/ScalaJSCompilationTests.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package dotty
22
package tools
33
package dotc
44

5-
import org.junit.{ Test, BeforeClass, AfterClass }
5+
import org.junit.{ Test => JUnitTest, BeforeClass, AfterClass }
66
import org.junit.experimental.categories.Category
77

88
import scala.concurrent.duration._
@@ -28,7 +28,7 @@ class ScalaJSCompilationTests extends ParallelTesting {
2828

2929
// Negative tests ------------------------------------------------------------
3030

31-
@Test def negScalaJS: Unit = {
31+
@JUnitTest def negScalaJS: Unit = {
3232
implicit val testGroup: TestGroup = TestGroup("negScalaJS")
3333
aggregateTests(
3434
compileFilesInDir("tests/neg-scalajs", scalaJSOptions),
@@ -58,7 +58,7 @@ class ScalaJSCompilationTests extends ParallelTesting {
5858
Failure(writer.toString())
5959
end runMain
6060

61-
@Test def runScalaJS: Unit = {
61+
@JUnitTest def runScalaJS: Unit = {
6262
implicit val testGroup: TestGroup = TestGroup("runScalaJS")
6363
aggregateTests(
6464
compileFilesInDir("tests/run", scalaJSOptions),

0 commit comments

Comments
 (0)