Skip to content

Commit 1378e1a

Browse files
committed
Add debugMain method
1 parent afef97c commit 1378e1a

File tree

4 files changed

+99
-82
lines changed

4 files changed

+99
-82
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
@@ -919,7 +919,7 @@ trait ParallelTesting extends RunnerOrchestration { self =>
919919

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

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

Lines changed: 81 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,23 @@ trait RunnerOrchestration {
5151
/** Open JDI connection for testing the debugger */
5252
def debugMode: Boolean = false
5353

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

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

@@ -69,13 +82,22 @@ trait RunnerOrchestration {
6982
* it died
7083
*/
7184
private class RunnerMonitor {
85+
/** Did add hook to kill the child VMs? */
86+
private val didAddCleanupCallback = new AtomicBoolean(false)
7287

7388
def runMain(classPath: String)(implicit summaryReport: SummaryReporting): Status =
7489
withRunner(_.runMain(classPath))
7590

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

80102
/** Checks if `process` is still alive
81103
*
@@ -88,88 +110,78 @@ trait RunnerOrchestration {
88110
catch { case _: IllegalThreadStateException => true }
89111

90112
/** Destroys the underlying process and kills IO streams */
91-
def kill(): Unit = {
113+
def kill(): Unit =
92114
if (process ne null) process.destroy()
93115
process = null
94-
childStdout = null
95-
childStdin = null
96-
}
97-
98-
/** Did add hook to kill the child VMs? */
99-
private val didAddCleanupCallback = new AtomicBoolean(false)
100116

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

125145
// Create a future reading the object:
126-
val readOutput = Future {
146+
Future:
127147
val sb = new StringBuilder
128148

129-
if (childStdout eq null)
130-
childStdout = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))
131-
132-
var childOutput: String = childStdout.readLine()
149+
var childOutput: String = process.stdout.readLine()
133150

134151
// Discard all messages until the test starts
135152
while (childOutput != ChildJVMMain.MessageStart && childOutput != null)
136-
childOutput = childStdout.readLine()
137-
childOutput = childStdout.readLine()
153+
childOutput = process.stdout.readLine()
154+
childOutput = process.stdout.readLine()
138155

139-
while (childOutput != ChildJVMMain.MessageEnd && childOutput != null) {
156+
while childOutput != ChildJVMMain.MessageEnd && childOutput != null do
140157
sb.append(childOutput).append(System.lineSeparator)
141-
childOutput = childStdout.readLine()
142-
}
158+
childOutput = process.stdout.readLine()
143159

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

165-
// A Java process and its JDI port for debugging, if debugMode is enabled.
166-
private class RunnerProcess(p: Process, val port: Option[Int]):
167-
export p.*
174+
// Makes the encapsulating RunnerMonitor spawn a new runner
175+
private def respawn(): Unit =
176+
process.destroy()
177+
process = null
178+
process = createProcess()
179+
end Runner
168180

169181
/** Create a process which has the classpath of the `ChildJVMMain` and the
170182
* scala library.
171183
*/
172-
private def createProcess: RunnerProcess = {
184+
private def createProcess(): RunnerProcess = {
173185
val url = classOf[ChildJVMMain].getProtectionDomain.getCodeSource.getLocation
174186
val cp = Paths.get(url.toURI).toString + JFile.pathSeparator + Properties.scalaLibrary
175187
val javaBin = Paths.get(sys.props("java.home"), "bin", "java").toString
@@ -212,12 +224,15 @@ trait RunnerOrchestration {
212224
notify()
213225
}
214226

215-
private def withRunner[T](op: Runner => T): T = {
227+
private def withRunner[T](op: Runner => T)(using summaryReport: SummaryReporting): T =
228+
// If for some reason the test runner (i.e. sbt) doesn't kill the VM,
229+
// we need to clean up ourselves.
230+
if didAddCleanupCallback.compareAndSet(false, true) then
231+
summaryReport.addCleanup(() => killAll())
216232
val runner = getRunner()
217233
val result = op(runner)
218234
freeRunner(runner)
219235
result
220-
}
221236

222237
def killAll(): Unit = {
223238
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)