/
SbtDebuggee.scala
201 lines (178 loc) · 6.93 KB
/
SbtDebuggee.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
package ch.epfl.scala.debugadapter.sbtplugin.internal
import ch.epfl.scala.debugadapter.*
import ch.epfl.scala.debugadapter.sbtplugin.{AnnotatedFingerscan, SubclassFingerscan}
import sbt.Tests.Cleanup
import sbt.internal.bsp.BuildTargetIdentifier
import sbt.io.IO
import sbt.testing.*
import sbt.{ForkConfiguration, ForkMain, ForkOptions, ForkTags, TestDefinition, TestFramework}
import java.io.{ObjectInputStream, ObjectOutputStream}
import java.net.{ServerSocket, Socket}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.control.NonFatal
import ch.epfl.scala.debugadapter.testing.TestSuiteEvent
import io.reactivex.Observable
import java.io.Closeable
private[debugadapter] sealed trait SbtDebuggee extends Debuggee {
val logger: LoggerAdapter
val classesToUpdate: Observable[Seq[String]]
override def observeClassUpdates(onClassUpdate: Seq[String] => Unit): Closeable = {
val subscription = classesToUpdate.subscribe(onClassUpdate(_))
() => if (!subscription.isDisposed) subscription.dispose
}
}
private[debugadapter] final class MainClassDebuggee(
target: BuildTargetIdentifier,
val scalaVersion: ScalaVersion,
forkOptions: ForkOptions,
val modules: Seq[Module],
val libraries: Seq[Library],
val unmanagedEntries: Seq[UnmanagedEntry],
val javaRuntime: Option[JavaRuntime],
override val classesToUpdate: Observable[Seq[String]],
mainClass: String,
args: Seq[String],
val logger: LoggerAdapter
)(implicit ec: ExecutionContext)
extends SbtDebuggee {
override def name: String =
s"${getClass.getSimpleName}(${target.uri}, $mainClass)"
override def run(listener: DebuggeeListener): CancelableFuture[Unit] =
DebuggeeProcess.start(forkOptions, classPath, mainClass, args, listener, logger)
}
private[debugadapter] final class TestSuitesDebuggee(
target: BuildTargetIdentifier,
val scalaVersion: ScalaVersion,
forkOptions: ForkOptions,
val modules: Seq[Module],
val libraries: Seq[Library],
val unmanagedEntries: Seq[UnmanagedEntry],
val javaRuntime: Option[JavaRuntime],
override val classesToUpdate: Observable[Seq[String]],
cleanups: Seq[Cleanup],
parallel: Boolean,
runners: Map[TestFramework, Runner],
tests: Seq[TestDefinition],
val logger: LoggerAdapter
)(implicit executionContext: ExecutionContext)
extends SbtDebuggee {
override def name: String =
s"${getClass.getSimpleName}(${target.uri}, [${tests.mkString(", ")}])"
override def run(listener: DebuggeeListener): CancelableFuture[Unit] = {
val eventHandler = new SbtTestSuiteEventHandler(listener)
@annotation.tailrec
def receiveLogs(is: ObjectInputStream, os: ObjectOutputStream): Unit = {
is.readObject() match {
case Array(ForkTags.Error, s: String) =>
eventHandler.handle(TestSuiteEvent.Error(s))
receiveLogs(is, os)
case Array(ForkTags.Warn, s: String) =>
eventHandler.handle(TestSuiteEvent.Warn(s))
receiveLogs(is, os)
case Array(ForkTags.Info, s: String) =>
eventHandler.handle(TestSuiteEvent.Info(s))
receiveLogs(is, os)
case Array(ForkTags.Debug, s: String) =>
eventHandler.handle(TestSuiteEvent.Debug(s))
receiveLogs(is, os)
case t: Throwable =>
eventHandler.handle(TestSuiteEvent.Trace(t))
receiveLogs(is, os)
case Array(testSuite: String, events: Array[Event]) =>
eventHandler.handle(TestSuiteEvent.Results(testSuite, events.toList))
receiveLogs(is, os)
case ForkTags.Done =>
eventHandler.handle(TestSuiteEvent.Done)
os.writeObject(ForkTags.Done)
os.flush()
}
}
object Acceptor extends Thread {
val server = new ServerSocket(0)
var socket: Socket = _
var os: ObjectOutputStream = _
var is: ObjectInputStream = _
override def run(): Unit = {
try {
socket = server.accept()
os = new ObjectOutputStream(socket.getOutputStream)
is = new ObjectInputStream(socket.getInputStream)
// Must flush the header that the constructor writes
// otherwise the ObjectInputStream on the other end may block indefinitely
os.flush()
val config = new ForkConfiguration(true, parallel)
os.writeObject(config)
val taskdefs = tests.map { t =>
val forkFingerprint = t.fingerprint match {
case s: SubclassFingerprint => new SubclassFingerscan(s)
case a: AnnotatedFingerprint => new AnnotatedFingerscan(a)
case f => sys.error("Unknown fingerprint type: " + f.getClass)
}
new TaskDef(
t.name,
forkFingerprint,
t.explicitlySpecified,
t.selectors
)
}
os.writeObject(taskdefs.toArray)
os.writeInt(runners.size)
for ((testFramework, mainRunner) <- runners) {
os.writeObject(testFramework.implClassNames.toArray)
os.writeObject(mainRunner.args)
os.writeObject(mainRunner.remoteArgs)
}
os.flush()
receiveLogs(is, os)
} catch {
case NonFatal(e) => close()
}
}
def close(): Unit = {
if (os != null) os.close()
if (socket != null) socket.close()
server.close()
}
}
Acceptor.start()
val mainClass = classOf[ForkMain].getCanonicalName
val args = Seq(Acceptor.server.getLocalPort.toString)
val fullClasspath = classPath ++ Seq(
IO.classLocationPath[ForkMain], // test-agent
IO.classLocationPath[Framework], // test-interface
IO.classLocationPath[SubclassFingerscan] // sbt-plugin
)
val process = DebuggeeProcess.start(forkOptions, fullClasspath, mainClass, args, listener, logger)
process.future.onComplete { _ =>
Acceptor.close()
try {
// the setup happens during the execution of the test
// the cleanup can crash if it needs the sbt streams (which is already closed)
val dummyLoader = getClass.getClassLoader
cleanups.foreach(_.cleanup(dummyLoader))
} catch {
case NonFatal(cause) =>
logger.warn(s"Failed to cleanup the tests")
logger.trace(cause)
}
}
process
}
}
private[debugadapter] final class AttachRemoteDebuggee(
target: BuildTargetIdentifier,
val scalaVersion: ScalaVersion,
val modules: Seq[Module],
val libraries: Seq[Library],
val unmanagedEntries: Seq[UnmanagedEntry],
val javaRuntime: Option[JavaRuntime],
override val classesToUpdate: Observable[Seq[String]],
val logger: LoggerAdapter
) extends SbtDebuggee {
override def name: String = s"${getClass.getSimpleName}(${target.uri})"
override def run(listener: DebuggeeListener): CancelableFuture[Unit] =
new CancelableFuture[Unit] {
override def future: Future[Unit] = Future.successful(())
override def cancel(): Unit = ()
}
}