Skip to content

Commit 9de4443

Browse files
Merge commit '378b48a37'
2 parents 89dd97b + 378b48a commit 9de4443

File tree

5 files changed

+207
-60
lines changed

5 files changed

+207
-60
lines changed

frontend/src/main/scala/bloop/bsp/BloopBspServices.scala

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ import ch.epfl.scala.bsp
2020
import ch.epfl.scala.bsp.BuildTargetIdentifier
2121
import ch.epfl.scala.bsp.MessageType
2222
import ch.epfl.scala.bsp.ShowMessageParams
23+
import ch.epfl.scala.bsp.CompileResult
24+
import ch.epfl.scala.bsp.StatusCode
25+
import ch.epfl.scala.bsp.Uri
26+
import ch.epfl.scala.bsp.endpoints
27+
import ch.epfl.scala.bsp.CompileResult
28+
import ch.epfl.scala.bsp.MessageType
29+
import ch.epfl.scala.bsp.ShowMessageParams
30+
import ch.epfl.scala.bsp.StatusCode
2331
import ch.epfl.scala.bsp.Uri
2432
import ch.epfl.scala.bsp.endpoints
2533
import ch.epfl.scala.debugadapter.DebugServer
@@ -689,10 +697,7 @@ final class BloopBspServices(
689697
}
690698

691699
def test(params: bsp.TestParams): BspEndpointResponse[bsp.TestResult] = {
692-
def test(
693-
project: Project,
694-
state: State
695-
): Task[State] = {
700+
def test(project: Project, state: State): Task[Tasks.TestRuns] = {
696701
val testFilter = TestInternals.parseFilters(Nil) // Don't support test only for now
697702
val handler = new LoggingEventHandler(state.logger)
698703
Tasks.test(
@@ -717,25 +722,35 @@ final class BloopBspServices(
717722
val args = params.arguments.getOrElse(Nil)
718723
val logger = logger0.asBspServerVerbose
719724
compileProjects(mappings, state, args, originId, logger).flatMap {
720-
case (newState, compileResult) =>
721-
compileResult match {
722-
case Right(_) =>
723-
val sequentialTestExecution = mappings.foldLeft(Task.now(newState)) {
724-
case (taskState, (_, p)) => taskState.flatMap(state => test(p, state))
725-
}
726-
727-
sequentialTestExecution.materialize.map(_.toEither).map {
728-
case Right(newState) =>
725+
case (newState, Right(CompileResult(_, StatusCode.Ok, _, _))) =>
726+
val sequentialTestExecution: Task[Seq[Tasks.TestRuns]] =
727+
Task.sequence(mappings.map { case (_, p) => test(p, state) })
728+
729+
sequentialTestExecution.materialize.map {
730+
case Success(testRunsSeq) =>
731+
testRunsSeq.reduceOption(_ ++ _) match {
732+
case None =>
729733
(newState, Right(bsp.TestResult(originId, bsp.StatusCode.Ok, None, None)))
730-
case Left(e) =>
731-
//(newState, Right(bsp.TestResult(None, bsp.StatusCode.Error, None)))
732-
val errorMessage =
733-
Response.internalError(s"Failed test execution: ${e.getMessage}")
734-
(newState, Left(errorMessage))
734+
case Some(testRuns) =>
735+
val status = testRuns.status
736+
val bspStatus =
737+
if (status == ExitStatus.Ok) bsp.StatusCode.Ok else bsp.StatusCode.Error
738+
(
739+
newState.mergeStatus(status),
740+
Right(bsp.TestResult(originId, bspStatus, None, None))
741+
)
735742
}
736-
737-
case Left(error) => Task.now((newState, Left(error)))
743+
case Failure(e) =>
744+
val errorMessage =
745+
Response.internalError(s"Failed test execution: ${e.getMessage}")
746+
(newState, Left(errorMessage))
738747
}
748+
749+
case (newState, Right(CompileResult(_, errorCode, _, _))) =>
750+
Task.now((newState, Right(bsp.TestResult(originId, errorCode, None, None))))
751+
752+
case (newState, Left(error)) =>
753+
Task.now((newState, Left(error)))
739754
}
740755
}
741756
}

frontend/src/main/scala/bloop/engine/Interpreter.scala

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -362,16 +362,18 @@ object Interpreter {
362362

363363
val handler = new LoggingEventHandler(state.logger)
364364

365-
Tasks.test(
366-
state,
367-
projectsToTest,
368-
cmd.args,
369-
testFilter,
370-
ScalaTestSuites.empty,
371-
handler,
372-
cmd.parallel,
373-
RunMode.Normal
374-
)
365+
Tasks
366+
.test(
367+
state,
368+
projectsToTest,
369+
cmd.args,
370+
testFilter,
371+
ScalaTestSuites.empty,
372+
handler,
373+
cmd.parallel,
374+
RunMode.Normal
375+
)
376+
.map(testRuns => state.mergeStatus(testRuns.status))
375377
}
376378
}
377379

frontend/src/main/scala/bloop/engine/tasks/Tasks.scala

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,22 @@ object Tasks {
9393
state
9494
}
9595

96+
case class TestRun(project: Project, exitCode: Int, results: List[TestSuiteEvent.Results]) {
97+
def testsFailed: Boolean =
98+
results.exists(_.events.exists(e => TestFailedStatus.contains(e.status())))
99+
def processFailed: Boolean =
100+
exitCode != 0
101+
def failed: Option[ExitStatus] =
102+
if (processFailed || testsFailed) Some(ExitStatus.TestExecutionError) else None
103+
}
104+
105+
case class TestRuns(runs: List[TestRun]) {
106+
def ++(other: TestRuns): TestRuns =
107+
TestRuns(runs ++ other.runs)
108+
def status: ExitStatus =
109+
runs.view.flatMap(_.failed).headOption.getOrElse(ExitStatus.Ok)
110+
}
111+
96112
/**
97113
* Run the tests for all projects in `projectsToTest`.
98114
*
@@ -112,53 +128,45 @@ object Tasks {
112128
testEventHandler: BloopTestSuiteEventHandler,
113129
runInParallel: Boolean = false,
114130
mode: RunMode
115-
): Task[State] = {
116-
import state.logger
117-
implicit val logContext: DebugFilter = DebugFilter.Test
131+
): Task[TestRuns] = {
118132

119-
var failure = false
120133
val testTasks = projectsToTest.map { project =>
121-
/* Intercept test failures to set the correct error code */
122-
val failureHandler = new LoggingEventHandler(state.logger) {
134+
// note: mutable state here, to collect all `TestSuiteEvent.Results` produced while running tests
135+
// it should be fine, since it's scoped to this particular evaluation of `TestTask.runTestSuites`
136+
val resultsBuilder = List.newBuilder[TestSuiteEvent.Results]
137+
val handleAndStore = new LoggingEventHandler(state.logger) {
123138
override def report(): Unit = testEventHandler.report()
124139
override def handle(event: TestSuiteEvent): Unit = {
125140
testEventHandler.handle(event)
126141
event match {
127-
case TestSuiteEvent.Results(_, ev)
128-
if ev.exists(e => TestFailedStatus.contains(e.status())) =>
129-
failure = true
142+
case x: TestSuiteEvent.Results => resultsBuilder += x
130143
case _ => ()
131144
}
132145
}
133146
}
134147

135148
val cwd = project.workingDirectory
136-
TestTask.runTestSuites(
137-
state,
138-
project,
139-
cwd,
140-
userTestOptions,
141-
testFilter,
142-
testClasses,
143-
failureHandler,
144-
mode
145-
)
149+
TestTask
150+
.runTestSuites(
151+
state,
152+
project,
153+
cwd,
154+
userTestOptions,
155+
testFilter,
156+
testClasses,
157+
handleAndStore,
158+
mode
159+
)
160+
.map { exitCode => TestRun(project, exitCode, resultsBuilder.result()) }
146161
}
147162

148-
val runAll: List[Task[Int]] => Task[List[Int]] =
149-
if (runInParallel) {
150-
Task.gather
151-
} else {
152-
Task.sequence
153-
}
163+
def runAll[A]: List[Task[A]] => Task[List[A]] =
164+
if (runInParallel) Task.gather else Task.sequence
154165

155-
runAll(testTasks).map { exitCodes =>
166+
runAll(testTasks).map { testRuns =>
156167
// When the test execution is over report no matter what the result is
157168
testEventHandler.report()
158-
logger.debug(s"Test suites failed: $failure")
159-
val isOk = !failure && exitCodes.forall(_ == 0)
160-
if (isOk) state.mergeStatus(ExitStatus.Ok)
161-
else state.copy(status = ExitStatus.TestExecutionError)
169+
TestRuns(testRuns)
162170
}
163171
}
164172

frontend/src/test/scala/bloop/bsp/BspBaseSuite.scala

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,54 @@ abstract class BspBaseSuite extends BaseSuite with BspClientTest {
199199
}
200200
}
201201

202+
def testTask(
203+
project: TestProject,
204+
originId: Option[String] = None,
205+
arguments: Option[List[String]] = None,
206+
dataKind: Option[String] = None,
207+
data: Option[RawJson] = None,
208+
clearDiagnostics: Boolean = true
209+
): Task[ManagedBspTestState] = {
210+
runAfterTargets(project) { target =>
211+
// Handle internal state before sending test request
212+
if (clearDiagnostics) diagnostics.clear()
213+
currentCompileIteration.increment(1)
214+
val params = bsp.TestParams(List(target), originId, arguments, dataKind, data)
215+
216+
rpcRequest(BuildTarget.test, params).flatMap { r =>
217+
// `headL` returns latest saved state from bsp because source is behavior subject
218+
Task
219+
.liftMonixTaskUncancellable(
220+
serverStates.headL
221+
)
222+
.map { state =>
223+
new ManagedBspTestState(
224+
state,
225+
r.statusCode,
226+
currentCompileIteration,
227+
diagnostics,
228+
client0,
229+
serverStates
230+
)
231+
}
232+
}
233+
}
234+
}
235+
236+
def test(
237+
project: TestProject,
238+
originId: Option[String] = None,
239+
arguments: Option[List[String]] = None,
240+
dataKind: Option[String] = None,
241+
data: Option[RawJson] = None,
242+
clearDiagnostics: Boolean = true,
243+
timeout: Long = 5
244+
): ManagedBspTestState = {
245+
val task = testTask(project, originId, arguments, dataKind, data, clearDiagnostics)
246+
247+
TestUtil.await(FiniteDuration(timeout, "s"))(task)
248+
}
249+
202250
def cleanTask(project: TestProject): Task[ManagedBspTestState] = {
203251
runAfterTargets(project) { target =>
204252
rpcRequest(BuildTarget.cleanCache, bsp.CleanCacheParams(List(target))).flatMap { r =>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package bloop.bsp
2+
3+
import bloop.cli.BspProtocol
4+
import bloop.cli.ExitStatus
5+
import bloop.io.AbsolutePath
6+
import bloop.logging.RecordingLogger
7+
import bloop.util.TestProject
8+
import bloop.util.TestUtil
9+
import bloop.internal.build.BuildTestInfo
10+
import bloop.ScalaInstance
11+
12+
object TcpBspTestSpec extends BspTestSpec(BspProtocol.Tcp)
13+
object LocalBspTestSpec extends BspTestSpec(BspProtocol.Local)
14+
15+
class BspTestSpec(override val protocol: BspProtocol) extends BspBaseSuite {
16+
17+
val junitJars = BuildTestInfo.junitTestJars.map(AbsolutePath.apply)
18+
private val scalaVersion = "2.12.15"
19+
20+
test("bsp test succeeds") {
21+
TestUtil.withinWorkspace { workspace =>
22+
val sources = List(
23+
"""/main/scala/FooTest.scala
24+
|class FooTest {
25+
| @org.junit.Test def foo(): Unit = org.junit.Assert.assertTrue(true)
26+
|}
27+
""".stripMargin
28+
)
29+
30+
val logger = new RecordingLogger(ansiCodesSupported = false)
31+
32+
val compilerJars = ScalaInstance
33+
.resolve("org.scala-lang", "scala-compiler", scalaVersion, logger)
34+
.allJars
35+
.map(AbsolutePath.apply)
36+
37+
val A =
38+
TestProject(workspace, "a", sources, enableTests = true, jars = compilerJars ++ junitJars)
39+
loadBspState(workspace, List(A), logger) { state =>
40+
val compiled = state.compile(A)
41+
val testResult = compiled.toTestState.test(A)
42+
assertExitStatus(testResult, ExitStatus.Ok)
43+
}
44+
}
45+
}
46+
47+
test("bsp test fails") {
48+
TestUtil.withinWorkspace { workspace =>
49+
val sources = List(
50+
"""/FooTest.scala
51+
|class FooTest {
52+
| @org.junit.Test def foo(): Unit = {
53+
| org.junit.Assert.fail()
54+
| }
55+
|}
56+
""".stripMargin
57+
)
58+
59+
val logger = new RecordingLogger(ansiCodesSupported = false)
60+
val compilerJars = ScalaInstance
61+
.resolve("org.scala-lang", "scala-compiler", scalaVersion, logger)
62+
.allJars
63+
.map(AbsolutePath.apply)
64+
65+
val A =
66+
TestProject(workspace, "a", sources, enableTests = true, jars = compilerJars ++ junitJars)
67+
loadBspState(workspace, List(A), logger) { state =>
68+
val compiled = state.compile(A)
69+
val testResult = compiled.toTestState.test(A)
70+
assertExitStatus(testResult, ExitStatus.TestExecutionError)
71+
}
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)