@@ -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())
0 commit comments