@@ -55,10 +55,23 @@ trait RunnerOrchestration {
55
55
/** Open JDI connection for testing the debugger */
56
56
def debugMode : Boolean = false
57
57
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 =
60
60
monitor.runMain(classPath)
61
61
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
+
62
75
/** Kill all processes */
63
76
def cleanup () = monitor.killAll()
64
77
@@ -73,13 +86,22 @@ trait RunnerOrchestration {
73
86
* it died
74
87
*/
75
88
private class RunnerMonitor {
89
+ /** Did add hook to kill the child VMs? */
90
+ private val didAddCleanupCallback = new AtomicBoolean (false )
76
91
77
92
def runMain (classPath : String )(implicit summaryReport : SummaryReporting ): Status =
78
93
withRunner(_.runMain(classPath))
79
94
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 ):
83
105
84
106
/** Checks if `process` is still alive
85
107
*
@@ -92,83 +114,72 @@ trait RunnerOrchestration {
92
114
catch { case _ : IllegalThreadStateException => true }
93
115
94
116
/** Destroys the underlying process and kills IO streams */
95
- def kill (): Unit = {
117
+ def kill (): Unit =
96
118
if (process ne null ) process.destroy()
97
119
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 )
104
120
105
121
/** 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 ] =
126
146
// pass file to running process
127
- childStdin .println(classPath)
147
+ process.stdin .println(classPath)
128
148
129
149
// Create a future reading the object:
130
- val readOutput = Future {
150
+ Future :
131
151
val sb = new StringBuilder
132
152
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()
137
154
138
155
// Discard all messages until the test starts
139
156
while (childOutput != ChildJVMMain .MessageStart && childOutput != null )
140
- childOutput = childStdout .readLine()
141
- childOutput = childStdout .readLine()
157
+ childOutput = process.stdout .readLine()
158
+ childOutput = process.stdout .readLine()
142
159
143
- while ( childOutput != ChildJVMMain .MessageEnd && childOutput != null ) {
160
+ while childOutput != ChildJVMMain .MessageEnd && childOutput != null do
144
161
sb.append(childOutput).append(System .lineSeparator)
145
- childOutput = childStdout.readLine()
146
- }
162
+ childOutput = process.stdout.readLine()
147
163
148
164
if (process.isAlive() && childOutput != null ) Success (sb.toString)
149
165
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
165
176
status
166
- }
167
- }
168
177
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
172
183
173
184
/** Create a process which has the classpath of the `ChildJVMMain` and the
174
185
* scala library.
@@ -216,12 +227,15 @@ trait RunnerOrchestration {
216
227
notify()
217
228
}
218
229
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())
220
235
val runner = getRunner()
221
236
val result = op(runner)
222
237
freeRunner(runner)
223
238
result
224
- }
225
239
226
240
def killAll (): Unit = {
227
241
freeRunners.foreach(_.kill())
0 commit comments