@@ -51,10 +51,23 @@ trait RunnerOrchestration {
51
51
/** Open JDI connection for testing the debugger */
52
52
def debugMode : Boolean = false
53
53
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 =
56
56
monitor.runMain(classPath)
57
57
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
+
58
71
/** Kill all processes */
59
72
def cleanup () = monitor.killAll()
60
73
@@ -69,13 +82,22 @@ trait RunnerOrchestration {
69
82
* it died
70
83
*/
71
84
private class RunnerMonitor {
85
+ /** Did add hook to kill the child VMs? */
86
+ private val didAddCleanupCallback = new AtomicBoolean (false )
72
87
73
88
def runMain (classPath : String )(implicit summaryReport : SummaryReporting ): Status =
74
89
withRunner(_.runMain(classPath))
75
90
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 ):
79
101
80
102
/** Checks if `process` is still alive
81
103
*
@@ -88,88 +110,78 @@ trait RunnerOrchestration {
88
110
catch { case _ : IllegalThreadStateException => true }
89
111
90
112
/** Destroys the underlying process and kills IO streams */
91
- def kill (): Unit = {
113
+ def kill (): Unit =
92
114
if (process ne null ) process.destroy()
93
115
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 )
100
116
101
117
/** 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 ] =
122
142
// pass file to running process
123
- childStdin .println(classPath)
143
+ process.stdin .println(classPath)
124
144
125
145
// Create a future reading the object:
126
- val readOutput = Future {
146
+ Future :
127
147
val sb = new StringBuilder
128
148
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()
133
150
134
151
// Discard all messages until the test starts
135
152
while (childOutput != ChildJVMMain .MessageStart && childOutput != null )
136
- childOutput = childStdout .readLine()
137
- childOutput = childStdout .readLine()
153
+ childOutput = process.stdout .readLine()
154
+ childOutput = process.stdout .readLine()
138
155
139
- while ( childOutput != ChildJVMMain .MessageEnd && childOutput != null ) {
156
+ while childOutput != ChildJVMMain .MessageEnd && childOutput != null do
140
157
sb.append(childOutput).append(System .lineSeparator)
141
- childOutput = childStdout.readLine()
142
- }
158
+ childOutput = process.stdout.readLine()
143
159
144
160
if (process.isAlive() && childOutput != null ) Success (sb.toString)
145
161
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
161
172
status
162
- }
163
- }
164
173
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
168
180
169
181
/** Create a process which has the classpath of the `ChildJVMMain` and the
170
182
* scala library.
171
183
*/
172
- private def createProcess : RunnerProcess = {
184
+ private def createProcess () : RunnerProcess = {
173
185
val url = classOf [ChildJVMMain ].getProtectionDomain.getCodeSource.getLocation
174
186
val cp = Paths .get(url.toURI).toString + JFile .pathSeparator + Properties .scalaLibrary
175
187
val javaBin = Paths .get(sys.props(" java.home" ), " bin" , " java" ).toString
@@ -212,12 +224,15 @@ trait RunnerOrchestration {
212
224
notify()
213
225
}
214
226
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())
216
232
val runner = getRunner()
217
233
val result = op(runner)
218
234
freeRunner(runner)
219
235
result
220
- }
221
236
222
237
def killAll (): Unit = {
223
238
freeRunners.foreach(_.kill())
0 commit comments