@@ -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 ` */
58+ /** Running a `Test` class's main method from the specified `classpath ` */
5959 def runMain (classPath : String )(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())
0 commit comments