From 50cf74cc67aecfaccaeae991e1af63bd97ccba3e Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Thu, 17 Sep 2020 15:58:14 +0200 Subject: [PATCH 1/2] Add BSP `buildTarget/scalaTestClasses` endpoint --- main/src/main/scala/sbt/Keys.scala | 2 + .../internal/server/BuildServerProtocol.scala | 32 ++++++++++++- .../sbt/internal/bsp/RunResult.scala | 2 +- .../internal/bsp/ScalaMainClassesParams.scala | 1 + .../internal/bsp/ScalaTestClassesItem.scala | 40 ++++++++++++++++ .../internal/bsp/ScalaTestClassesParams.scala | 46 +++++++++++++++++++ .../internal/bsp/ScalaTestClassesResult.scala | 41 +++++++++++++++++ .../sbt/internal/bsp/codec/JsonProtocol.scala | 3 ++ .../codec/ScalaTestClassesItemFormats.scala | 29 ++++++++++++ .../codec/ScalaTestClassesParamsFormats.scala | 29 ++++++++++++ .../codec/ScalaTestClassesResultFormats.scala | 29 ++++++++++++ protocol/src/main/contraband/bsp.contra | 31 ++++++++++++- .../src/server-test/buildserver/build.sbt | 3 ++ .../foo/src/main/scala/foo/FooMain.scala | 4 +- .../foo/src/test/scala/foo/FooTest.scala | 9 ++++ .../test/scala/testpkg/BuildServerTest.scala | 14 ++++++ 16 files changed, 311 insertions(+), 4 deletions(-) create mode 100644 protocol/src/main/contraband-scala/sbt/internal/bsp/ScalaTestClassesItem.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/bsp/ScalaTestClassesParams.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/bsp/ScalaTestClassesResult.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/bsp/codec/ScalaTestClassesItemFormats.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/bsp/codec/ScalaTestClassesParamsFormats.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/bsp/codec/ScalaTestClassesResultFormats.scala create mode 100644 server-test/src/server-test/buildserver/foo/src/test/scala/foo/FooTest.scala diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index ebf62d8f8..1b22f3188 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -401,6 +401,8 @@ object Keys { val bspBuildTargetRun = inputKey[Unit]("Corresponds to buildTarget/run request").withRank(DTask) val bspBuildTargetScalacOptions = inputKey[Unit]("").withRank(DTask) val bspBuildTargetScalacOptionsItem = taskKey[ScalacOptionsItem]("").withRank(DTask) + val bspScalaTestClasses = inputKey[Unit]("Corresponds to buildTarget/scalaTestClasses request").withRank(DTask) + val bspScalaTestClassesItem = taskKey[ScalaTestClassesItem]("").withRank(DTask) val bspScalaMainClasses = inputKey[Unit]("Corresponds to buildTarget/scalaMainClasses request").withRank(DTask) val bspScalaMainClassesItem = taskKey[ScalaMainClassesItem]("").withRank(DTask) diff --git a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala index 4065d760c..b43157e56 100644 --- a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala @@ -142,6 +142,17 @@ object BuildServerProtocol { } }.evaluated, bspBuildTargetScalacOptions / aggregate := false, + bspScalaTestClasses := Def.inputTaskDyn { + val s = state.value + val workspace = bspWorkspace.value + val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri))) + val filter = ScopeFilter.in(targets.map(workspace)) + Def.task { + val items = bspScalaTestClassesItem.all(filter).value + val result = ScalaTestClassesResult(items.toVector, None) + s.respondEvent(result) + } + }.evaluated, bspScalaMainClasses := Def.inputTaskDyn { val s = state.value val workspace = bspWorkspace.value @@ -152,7 +163,8 @@ object BuildServerProtocol { val result = ScalaMainClassesResult(items.toVector, None) s.respondEvent(result) } - }.evaluated + }.evaluated, + bspScalaMainClasses / aggregate := false ) // This will be scoped to Compile, Test, IntegrationTest etc @@ -180,6 +192,7 @@ object BuildServerProtocol { bspBuildTargetRun := bspRunTask.evaluated, bspBuildTargetScalacOptionsItem := scalacOptionsTask.value, bspInternalDependencyConfigurations := internalDependencyConfigurationsSetting.value, + bspScalaTestClassesItem := scalaTestClassesTask.value, bspScalaMainClassesItem := scalaMainClassesTask.value ) @@ -267,6 +280,12 @@ object BuildServerProtocol { val command = Keys.bspBuildTargetScalacOptions.key val _ = callback.appendExec(s"$command $targets", Some(r.id)) + case r: JsonRpcRequestMessage if r.method == "buildTarget/scalaTestClasses" => + val param = Converter.fromJson[ScalaTestClassesParams](json(r)).get + val targets = param.targets.map(_.uri).mkString(" ") + val command = Keys.bspScalaTestClasses.key + val _ = callback.appendExec(s"$command $targets", Some(r.id)) + case r: JsonRpcRequestMessage if r.method == "buildTarget/scalaMainClasses" => val param = Converter.fromJson[ScalaMainClassesParams](json(r)).get val targets = param.targets.map(_.uri).mkString(" ") @@ -524,6 +543,17 @@ object BuildServerProtocol { } } + private def scalaTestClassesTask: Initialize[Task[ScalaTestClassesItem]] = Def.task { + val testClasses = Keys.definedTests.?.value + .getOrElse(Seq.empty) + .map(_.name) + .toVector + ScalaTestClassesItem( + bspTargetIdentifier.value, + testClasses + ) + } + private def scalaMainClassesTask: Initialize[Task[ScalaMainClassesItem]] = Def.task { val jvmOptions = Keys.javaOptions.value.toVector val mainClasses = Keys.discoveredMainClasses.value.map( diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/RunResult.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/RunResult.scala index 33583b0ed..89f807cd3 100644 --- a/protocol/src/main/contraband-scala/sbt/internal/bsp/RunResult.scala +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/RunResult.scala @@ -7,7 +7,7 @@ package sbt.internal.bsp /** * Run Result * @param originId An optional request id to know the origin of this report. - * @param statusCode A status code fore the execution. + * @param statusCode A status code for the execution. */ final class RunResult private ( val originId: Option[String], diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/ScalaMainClassesParams.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/ScalaMainClassesParams.scala index 766cfed1d..3a3c10173 100644 --- a/protocol/src/main/contraband-scala/sbt/internal/bsp/ScalaMainClassesParams.scala +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/ScalaMainClassesParams.scala @@ -5,6 +5,7 @@ // DO NOT EDIT MANUALLY package sbt.internal.bsp /** + * Scala Main Class Request * The build target main classes request is sent from the client to the server * to query for the list of main classes that can be fed as arguments to buildTarget/run. * @param originId An optional number uniquely identifying a client request. diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/ScalaTestClassesItem.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/ScalaTestClassesItem.scala new file mode 100644 index 000000000..087404198 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/ScalaTestClassesItem.scala @@ -0,0 +1,40 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.bsp +/** + * @param target The build target that contains the test classes. + * @param classes The fully qualified names of the test classes in this target + */ +final class ScalaTestClassesItem private ( + val target: sbt.internal.bsp.BuildTargetIdentifier, + val classes: Vector[String]) extends Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: ScalaTestClassesItem => (this.target == x.target) && (this.classes == x.classes) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (37 * (17 + "sbt.internal.bsp.ScalaTestClassesItem".##) + target.##) + classes.##) + } + override def toString: String = { + "ScalaTestClassesItem(" + target + ", " + classes + ")" + } + private[this] def copy(target: sbt.internal.bsp.BuildTargetIdentifier = target, classes: Vector[String] = classes): ScalaTestClassesItem = { + new ScalaTestClassesItem(target, classes) + } + def withTarget(target: sbt.internal.bsp.BuildTargetIdentifier): ScalaTestClassesItem = { + copy(target = target) + } + def withClasses(classes: Vector[String]): ScalaTestClassesItem = { + copy(classes = classes) + } +} +object ScalaTestClassesItem { + + def apply(target: sbt.internal.bsp.BuildTargetIdentifier, classes: Vector[String]): ScalaTestClassesItem = new ScalaTestClassesItem(target, classes) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/ScalaTestClassesParams.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/ScalaTestClassesParams.scala new file mode 100644 index 000000000..7903fba21 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/ScalaTestClassesParams.scala @@ -0,0 +1,46 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.bsp +/** + * Scala Test Class Request + * The build target scala test options request is sent from the client to the server + * to query for the list of fully qualified names of test clases in a given list of targets. + * @param originId An optional number uniquely identifying a client request. + */ +final class ScalaTestClassesParams private ( + val targets: Vector[sbt.internal.bsp.BuildTargetIdentifier], + val originId: Option[String]) extends Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: ScalaTestClassesParams => (this.targets == x.targets) && (this.originId == x.originId) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (37 * (17 + "sbt.internal.bsp.ScalaTestClassesParams".##) + targets.##) + originId.##) + } + override def toString: String = { + "ScalaTestClassesParams(" + targets + ", " + originId + ")" + } + private[this] def copy(targets: Vector[sbt.internal.bsp.BuildTargetIdentifier] = targets, originId: Option[String] = originId): ScalaTestClassesParams = { + new ScalaTestClassesParams(targets, originId) + } + def withTargets(targets: Vector[sbt.internal.bsp.BuildTargetIdentifier]): ScalaTestClassesParams = { + copy(targets = targets) + } + def withOriginId(originId: Option[String]): ScalaTestClassesParams = { + copy(originId = originId) + } + def withOriginId(originId: String): ScalaTestClassesParams = { + copy(originId = Option(originId)) + } +} +object ScalaTestClassesParams { + + def apply(targets: Vector[sbt.internal.bsp.BuildTargetIdentifier], originId: Option[String]): ScalaTestClassesParams = new ScalaTestClassesParams(targets, originId) + def apply(targets: Vector[sbt.internal.bsp.BuildTargetIdentifier], originId: String): ScalaTestClassesParams = new ScalaTestClassesParams(targets, Option(originId)) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/ScalaTestClassesResult.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/ScalaTestClassesResult.scala new file mode 100644 index 000000000..8c84ec00e --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/ScalaTestClassesResult.scala @@ -0,0 +1,41 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.bsp +/** @param originId An optional id of the request that triggered this result. */ +final class ScalaTestClassesResult private ( + val items: Vector[sbt.internal.bsp.ScalaTestClassesItem], + val originId: Option[String]) extends Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: ScalaTestClassesResult => (this.items == x.items) && (this.originId == x.originId) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (37 * (17 + "sbt.internal.bsp.ScalaTestClassesResult".##) + items.##) + originId.##) + } + override def toString: String = { + "ScalaTestClassesResult(" + items + ", " + originId + ")" + } + private[this] def copy(items: Vector[sbt.internal.bsp.ScalaTestClassesItem] = items, originId: Option[String] = originId): ScalaTestClassesResult = { + new ScalaTestClassesResult(items, originId) + } + def withItems(items: Vector[sbt.internal.bsp.ScalaTestClassesItem]): ScalaTestClassesResult = { + copy(items = items) + } + def withOriginId(originId: Option[String]): ScalaTestClassesResult = { + copy(originId = originId) + } + def withOriginId(originId: String): ScalaTestClassesResult = { + copy(originId = Option(originId)) + } +} +object ScalaTestClassesResult { + + def apply(items: Vector[sbt.internal.bsp.ScalaTestClassesItem], originId: Option[String]): ScalaTestClassesResult = new ScalaTestClassesResult(items, originId) + def apply(items: Vector[sbt.internal.bsp.ScalaTestClassesItem], originId: String): ScalaTestClassesResult = new ScalaTestClassesResult(items, Option(originId)) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/JsonProtocol.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/JsonProtocol.scala index 0cd9acee0..0a2e3fffa 100644 --- a/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/JsonProtocol.scala +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/JsonProtocol.scala @@ -44,6 +44,9 @@ trait JsonProtocol extends sjsonnew.BasicJsonProtocol with sbt.internal.bsp.codec.ScalacOptionsResultFormats with sbt.internal.bsp.codec.BspConnectionDetailsFormats with sbt.internal.bsp.codec.MetalsMetadataFormats + with sbt.internal.bsp.codec.ScalaTestClassesParamsFormats + with sbt.internal.bsp.codec.ScalaTestClassesItemFormats + with sbt.internal.bsp.codec.ScalaTestClassesResultFormats with sbt.internal.bsp.codec.ScalaMainClassesParamsFormats with sbt.internal.bsp.codec.ScalaMainClassFormats with sbt.internal.bsp.codec.ScalaMainClassesItemFormats diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/ScalaTestClassesItemFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/ScalaTestClassesItemFormats.scala new file mode 100644 index 000000000..a079301b3 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/ScalaTestClassesItemFormats.scala @@ -0,0 +1,29 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.bsp.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait ScalaTestClassesItemFormats { self: sbt.internal.bsp.codec.BuildTargetIdentifierFormats with sjsonnew.BasicJsonProtocol => +implicit lazy val ScalaTestClassesItemFormat: JsonFormat[sbt.internal.bsp.ScalaTestClassesItem] = new JsonFormat[sbt.internal.bsp.ScalaTestClassesItem] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.bsp.ScalaTestClassesItem = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + val target = unbuilder.readField[sbt.internal.bsp.BuildTargetIdentifier]("target") + val classes = unbuilder.readField[Vector[String]]("classes") + unbuilder.endObject() + sbt.internal.bsp.ScalaTestClassesItem(target, classes) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.bsp.ScalaTestClassesItem, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("target", obj.target) + builder.addField("classes", obj.classes) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/ScalaTestClassesParamsFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/ScalaTestClassesParamsFormats.scala new file mode 100644 index 000000000..8a3cad4ae --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/ScalaTestClassesParamsFormats.scala @@ -0,0 +1,29 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.bsp.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait ScalaTestClassesParamsFormats { self: sbt.internal.bsp.codec.BuildTargetIdentifierFormats with sjsonnew.BasicJsonProtocol => +implicit lazy val ScalaTestClassesParamsFormat: JsonFormat[sbt.internal.bsp.ScalaTestClassesParams] = new JsonFormat[sbt.internal.bsp.ScalaTestClassesParams] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.bsp.ScalaTestClassesParams = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + val targets = unbuilder.readField[Vector[sbt.internal.bsp.BuildTargetIdentifier]]("targets") + val originId = unbuilder.readField[Option[String]]("originId") + unbuilder.endObject() + sbt.internal.bsp.ScalaTestClassesParams(targets, originId) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.bsp.ScalaTestClassesParams, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("targets", obj.targets) + builder.addField("originId", obj.originId) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/ScalaTestClassesResultFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/ScalaTestClassesResultFormats.scala new file mode 100644 index 000000000..ab51a7bb2 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/ScalaTestClassesResultFormats.scala @@ -0,0 +1,29 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.bsp.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait ScalaTestClassesResultFormats { self: sbt.internal.bsp.codec.ScalaTestClassesItemFormats with sjsonnew.BasicJsonProtocol => +implicit lazy val ScalaTestClassesResultFormat: JsonFormat[sbt.internal.bsp.ScalaTestClassesResult] = new JsonFormat[sbt.internal.bsp.ScalaTestClassesResult] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.bsp.ScalaTestClassesResult = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + val items = unbuilder.readField[Vector[sbt.internal.bsp.ScalaTestClassesItem]]("items") + val originId = unbuilder.readField[Option[String]]("originId") + unbuilder.endObject() + sbt.internal.bsp.ScalaTestClassesResult(items, originId) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.bsp.ScalaTestClassesResult, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("items", obj.items) + builder.addField("originId", obj.originId) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband/bsp.contra b/protocol/src/main/contraband/bsp.contra index c4b184e27..ca7031cd7 100644 --- a/protocol/src/main/contraband/bsp.contra +++ b/protocol/src/main/contraband/bsp.contra @@ -371,6 +371,9 @@ type CompileReport { time: Int } + + + ## Run Request ## The run request is sent from the client to the server to run a build target. ## The server communicates during the initialize handshake whether this method is supported or not. @@ -399,7 +402,7 @@ type RunResult { ## An optional request id to know the origin of this report. originId: String - ## A status code fore the execution. + ## A status code for the execution. statusCode: Int! } @@ -507,6 +510,32 @@ type MetalsMetadata { supportedScalaVersions: [String] } +## Scala Test Class Request +## The build target scala test options request is sent from the client to the server +## to query for the list of fully qualified names of test clases in a given list of targets. +type ScalaTestClassesParams { + targets: [sbt.internal.bsp.BuildTargetIdentifier] + + ## An optional number uniquely identifying a client request. + originId: String +} + +type ScalaTestClassesResult { + items: [sbt.internal.bsp.ScalaTestClassesItem] + + ## An optional id of the request that triggered this result. + originId: String +} + +type ScalaTestClassesItem { + ## The build target that contains the test classes. + target: sbt.internal.bsp.BuildTargetIdentifier! + + ## The fully qualified names of the test classes in this target + classes: [String] +} + +## Scala Main Class Request ## The build target main classes request is sent from the client to the server ## to query for the list of main classes that can be fed as arguments to buildTarget/run. type ScalaMainClassesParams { diff --git a/server-test/src/server-test/buildserver/build.sbt b/server-test/src/server-test/buildserver/build.sbt index 2bc71ff1e..fbbfc556b 100644 --- a/server-test/src/server-test/buildserver/build.sbt +++ b/server-test/src/server-test/buildserver/build.sbt @@ -6,6 +6,9 @@ lazy val root = (project in file(".")) .aggregate(foo, util) lazy val foo = project.in(file("foo")) + .settings( + libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.8" % "test", + ) .dependsOn(util) lazy val util = project diff --git a/server-test/src/server-test/buildserver/foo/src/main/scala/foo/FooMain.scala b/server-test/src/server-test/buildserver/foo/src/main/scala/foo/FooMain.scala index 9c5a63b10..29209deca 100644 --- a/server-test/src/server-test/buildserver/foo/src/main/scala/foo/FooMain.scala +++ b/server-test/src/server-test/buildserver/foo/src/main/scala/foo/FooMain.scala @@ -1,5 +1,7 @@ package foo object FooMain extends App { - println("Hello World!") + val message = "Hello World!" + + println(message) } diff --git a/server-test/src/server-test/buildserver/foo/src/test/scala/foo/FooTest.scala b/server-test/src/server-test/buildserver/foo/src/test/scala/foo/FooTest.scala new file mode 100644 index 000000000..8e230be99 --- /dev/null +++ b/server-test/src/server-test/buildserver/foo/src/test/scala/foo/FooTest.scala @@ -0,0 +1,9 @@ +package foo + +import org.scalatest.FreeSpec + +class FooTest extends FreeSpec { + "test message" in { + assert(FooMain.message == "Hello World!") + } +} \ No newline at end of file diff --git a/server-test/src/test/scala/testpkg/BuildServerTest.scala b/server-test/src/test/scala/testpkg/BuildServerTest.scala index 1bef9cd82..6ee1a26e6 100644 --- a/server-test/src/test/scala/testpkg/BuildServerTest.scala +++ b/server-test/src/test/scala/testpkg/BuildServerTest.scala @@ -120,6 +120,20 @@ object BuildServerTest extends AbstractServerTest { }) } + test("buildTarget/scalaTestClasses") { _ => + val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#foo/Test" + svr.sendJsonRpc( + s"""{ "jsonrpc": "2.0", "id": "18", "method": "buildTarget/scalaTestClasses", "params": { + | "targets": [{ "uri": "$x" }] + |} }""".stripMargin + ) + assert(svr.waitForString(10.seconds) { s => + println(s) + (s contains """"id":"18"""") && + (s contains """"classes":["foo.FooTest"]""") + }) + } + def initializeRequest(): Unit = { svr.sendJsonRpc( """{ "jsonrpc": "2.0", "id": "10", "method": "build/initialize", From f5753f763c7e3dcbe12a8f43d0333252da9e1d15 Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Mon, 21 Sep 2020 12:16:31 +0200 Subject: [PATCH 2/2] Add BSP buildTarget/test endpoint --- main/src/main/scala/sbt/Keys.scala | 1 + .../internal/server/BuildServerProtocol.scala | 60 +++++++++++++++- .../bsp/BuildServerCapabilities.scala | 22 ++++-- .../sbt/internal/bsp/ScalaTestParams.scala | 38 ++++++++++ .../sbt/internal/bsp/TestParams.scala | 70 +++++++++++++++++++ .../sbt/internal/bsp/TestProvider.scala | 32 +++++++++ .../sbt/internal/bsp/TestResult.scala | 45 ++++++++++++ .../BuildServerCapabilitiesFormats.scala | 6 +- .../sbt/internal/bsp/codec/JsonProtocol.scala | 6 +- .../bsp/codec/ScalaTestParamsFormats.scala | 27 +++++++ .../bsp/codec/TestParamsFormats.scala | 35 ++++++++++ .../bsp/codec/TestProviderFormats.scala | 27 +++++++ .../bsp/codec/TestResultFormats.scala | 29 ++++++++ protocol/src/main/contraband/bsp.contra | 50 ++++++++++++- .../foo/src/main/scala/foo/FooMain.scala | 2 +- .../foo/src/test/scala/foo/FailingTest.scala | 9 +++ .../foo/src/test/scala/foo/FooTest.scala | 2 +- .../test/scala/testpkg/BuildServerTest.scala | 39 ++++++++++- 18 files changed, 483 insertions(+), 17 deletions(-) create mode 100644 protocol/src/main/contraband-scala/sbt/internal/bsp/ScalaTestParams.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/bsp/TestParams.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/bsp/TestProvider.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/bsp/TestResult.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/bsp/codec/ScalaTestParamsFormats.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/bsp/codec/TestParamsFormats.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/bsp/codec/TestProviderFormats.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/bsp/codec/TestResultFormats.scala create mode 100644 server-test/src/server-test/buildserver/foo/src/test/scala/foo/FailingTest.scala diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 1b22f3188..6f21cd0ae 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -398,6 +398,7 @@ object Keys { val bspBuildTargetDependencySourcesItem = taskKey[DependencySourcesItem]("").withRank(DTask) val bspBuildTargetCompile = inputKey[Unit]("").withRank(DTask) val bspBuildTargetCompileItem = taskKey[Int]("").withRank(DTask) + val bspBuildTargetTest = inputKey[Unit]("Corresponds to buildTarget/test request").withRank(DTask) val bspBuildTargetRun = inputKey[Unit]("Corresponds to buildTarget/run request").withRank(DTask) val bspBuildTargetScalacOptions = inputKey[Unit]("").withRank(DTask) val bspBuildTargetScalacOptionsItem = taskKey[ScalacOptionsItem]("").withRank(DTask) diff --git a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala index b43157e56..268bb2151 100644 --- a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala @@ -30,12 +30,15 @@ import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter, Parser => import scala.util.{ Failure, Success, Try } import scala.util.control.NonFatal +import sbt.Project._ +import sbt.std.TaskExtra object BuildServerProtocol { import sbt.internal.bsp.codec.JsonProtocol._ private val capabilities = BuildServerCapabilities( CompileProvider(BuildServerConnection.languages), + TestProvider(BuildServerConnection.languages), RunProvider(BuildServerConnection.languages), dependencySourcesProvider = true, canReload = true @@ -130,6 +133,8 @@ object BuildServerProtocol { } }.evaluated, bspBuildTargetCompile / aggregate := false, + bspBuildTargetTest := bspTestTask.evaluated, + bspBuildTargetTest / aggregate := false, bspBuildTargetScalacOptions := Def.inputTaskDyn { val s = state.value val workspace = bspWorkspace.value @@ -255,6 +260,11 @@ object BuildServerProtocol { val command = Keys.bspBuildTargetCompile.key val _ = callback.appendExec(s"$command $targets", Some(r.id)) + case r: JsonRpcRequestMessage if r.method == "buildTarget/test" => + val task = bspBuildTargetTest.key + val paramStr = CompactPrinter(json(r)) + val _ = callback.appendExec(s"$task $paramStr", Some(r.id)) + case r if r.method == "buildTarget/run" => val paramJson = json(r) val param = Converter.fromJson[RunParams](json(r)).get @@ -388,7 +398,7 @@ object BuildServerProtocol { config <- configs if dep != thisProjectRef || config.name != thisConfig.name } yield Keys.bspTargetIdentifier.in(dep, config) - val capabilities = BuildTargetCapabilities(canCompile = true, canTest = false, canRun = false) + val capabilities = BuildTargetCapabilities(canCompile = true, canTest = true, canRun = true) val tags = BuildTargetTag.fromConfig(configuration.name) Def.task { BuildTarget( @@ -441,7 +451,6 @@ object BuildServerProtocol { } private def bspCompileTask: Def.Initialize[Task[Int]] = Def.task { - import sbt.Project._ Keys.compile.result.value match { case Value(_) => StatusCode.Success case Inc(_) => @@ -496,6 +505,53 @@ object BuildServerProtocol { runMainClassTask(mainClass, runParams.originId) } + private def bspTestTask: Def.Initialize[InputTask[Unit]] = Def.inputTaskDyn { + val testParams = jsonParser + .map(_.flatMap(json => Converter.fromJson[TestParams](json))) + .parsed + .get + val workspace = bspWorkspace.value + + val resultTask: Def.Initialize[Task[Result[Seq[Unit]]]] = testParams.dataKind match { + case Some("scala-test") => + val data = testParams.data.getOrElse(JNull) + val items = Converter.fromJson[ScalaTestParams](data) match { + case Failure(e) => + throw LangServerError(ErrorCodes.ParseError, e.getMessage) + case Success(value) => value.testClasses + } + val testTasks: Seq[Def.Initialize[Task[Unit]]] = items.map { item => + val scope = workspace(item.target) + item.classes.toList match { + case Nil => Def.task(()) + case classes => + (scope / testOnly).toTask(" " + classes.mkString(" ")) + } + } + testTasks.joinWith(ts => TaskExtra.joinTasks(ts).join).result + + case Some(dataKind) => + throw LangServerError( + ErrorCodes.InvalidParams, + s"Unexpected data of kind '$dataKind', 'scala-main-class' is expected" + ) + + case None => + // run allTests in testParams.targets + val filter = ScopeFilter.in(testParams.targets.map(workspace)) + test.all(filter).result + } + + Def.task { + val state = Keys.state.value + val statusCode = resultTask.value match { + case Value(_) => StatusCode.Success + case Inc(_) => StatusCode.Error + } + val _ = state.respondEvent(TestResult(testParams.originId, statusCode)) + } + } + private def runMainClassTask(mainClass: ScalaMainClass, originId: Option[String]) = Def.task { val state = Keys.state.value val logger = Keys.streams.value.log diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/BuildServerCapabilities.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/BuildServerCapabilities.scala index b65e97a4e..518fe0e12 100644 --- a/protocol/src/main/contraband-scala/sbt/internal/bsp/BuildServerCapabilities.scala +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/BuildServerCapabilities.scala @@ -6,12 +6,14 @@ package sbt.internal.bsp /** * @param compileProvider The languages the server supports compilation via method buildTarget/compile. + * @param testProvider The languages the server supports test execution via method buildTarget/test * @param dependencySourcesProvider The server provides sources for library dependencies via method buildTarget/dependencySources * @param canReload Reloading the workspace state through workspace/reload is supported */ final class BuildServerCapabilities private ( val compileProvider: Option[sbt.internal.bsp.CompileProvider], + val testProvider: Option[sbt.internal.bsp.TestProvider], val runProvider: Option[sbt.internal.bsp.RunProvider], val dependencySourcesProvider: Option[Boolean], val canReload: Option[Boolean]) extends Serializable { @@ -19,17 +21,17 @@ final class BuildServerCapabilities private ( override def equals(o: Any): Boolean = o match { - case x: BuildServerCapabilities => (this.compileProvider == x.compileProvider) && (this.runProvider == x.runProvider) && (this.dependencySourcesProvider == x.dependencySourcesProvider) && (this.canReload == x.canReload) + case x: BuildServerCapabilities => (this.compileProvider == x.compileProvider) && (this.testProvider == x.testProvider) && (this.runProvider == x.runProvider) && (this.dependencySourcesProvider == x.dependencySourcesProvider) && (this.canReload == x.canReload) case _ => false } override def hashCode: Int = { - 37 * (37 * (37 * (37 * (37 * (17 + "sbt.internal.bsp.BuildServerCapabilities".##) + compileProvider.##) + runProvider.##) + dependencySourcesProvider.##) + canReload.##) + 37 * (37 * (37 * (37 * (37 * (37 * (17 + "sbt.internal.bsp.BuildServerCapabilities".##) + compileProvider.##) + testProvider.##) + runProvider.##) + dependencySourcesProvider.##) + canReload.##) } override def toString: String = { - "BuildServerCapabilities(" + compileProvider + ", " + runProvider + ", " + dependencySourcesProvider + ", " + canReload + ")" + "BuildServerCapabilities(" + compileProvider + ", " + testProvider + ", " + runProvider + ", " + dependencySourcesProvider + ", " + canReload + ")" } - private[this] def copy(compileProvider: Option[sbt.internal.bsp.CompileProvider] = compileProvider, runProvider: Option[sbt.internal.bsp.RunProvider] = runProvider, dependencySourcesProvider: Option[Boolean] = dependencySourcesProvider, canReload: Option[Boolean] = canReload): BuildServerCapabilities = { - new BuildServerCapabilities(compileProvider, runProvider, dependencySourcesProvider, canReload) + private[this] def copy(compileProvider: Option[sbt.internal.bsp.CompileProvider] = compileProvider, testProvider: Option[sbt.internal.bsp.TestProvider] = testProvider, runProvider: Option[sbt.internal.bsp.RunProvider] = runProvider, dependencySourcesProvider: Option[Boolean] = dependencySourcesProvider, canReload: Option[Boolean] = canReload): BuildServerCapabilities = { + new BuildServerCapabilities(compileProvider, testProvider, runProvider, dependencySourcesProvider, canReload) } def withCompileProvider(compileProvider: Option[sbt.internal.bsp.CompileProvider]): BuildServerCapabilities = { copy(compileProvider = compileProvider) @@ -37,6 +39,12 @@ final class BuildServerCapabilities private ( def withCompileProvider(compileProvider: sbt.internal.bsp.CompileProvider): BuildServerCapabilities = { copy(compileProvider = Option(compileProvider)) } + def withTestProvider(testProvider: Option[sbt.internal.bsp.TestProvider]): BuildServerCapabilities = { + copy(testProvider = testProvider) + } + def withTestProvider(testProvider: sbt.internal.bsp.TestProvider): BuildServerCapabilities = { + copy(testProvider = Option(testProvider)) + } def withRunProvider(runProvider: Option[sbt.internal.bsp.RunProvider]): BuildServerCapabilities = { copy(runProvider = runProvider) } @@ -58,6 +66,6 @@ final class BuildServerCapabilities private ( } object BuildServerCapabilities { - def apply(compileProvider: Option[sbt.internal.bsp.CompileProvider], runProvider: Option[sbt.internal.bsp.RunProvider], dependencySourcesProvider: Option[Boolean], canReload: Option[Boolean]): BuildServerCapabilities = new BuildServerCapabilities(compileProvider, runProvider, dependencySourcesProvider, canReload) - def apply(compileProvider: sbt.internal.bsp.CompileProvider, runProvider: sbt.internal.bsp.RunProvider, dependencySourcesProvider: Boolean, canReload: Boolean): BuildServerCapabilities = new BuildServerCapabilities(Option(compileProvider), Option(runProvider), Option(dependencySourcesProvider), Option(canReload)) + def apply(compileProvider: Option[sbt.internal.bsp.CompileProvider], testProvider: Option[sbt.internal.bsp.TestProvider], runProvider: Option[sbt.internal.bsp.RunProvider], dependencySourcesProvider: Option[Boolean], canReload: Option[Boolean]): BuildServerCapabilities = new BuildServerCapabilities(compileProvider, testProvider, runProvider, dependencySourcesProvider, canReload) + def apply(compileProvider: sbt.internal.bsp.CompileProvider, testProvider: sbt.internal.bsp.TestProvider, runProvider: sbt.internal.bsp.RunProvider, dependencySourcesProvider: Boolean, canReload: Boolean): BuildServerCapabilities = new BuildServerCapabilities(Option(compileProvider), Option(testProvider), Option(runProvider), Option(dependencySourcesProvider), Option(canReload)) } diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/ScalaTestParams.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/ScalaTestParams.scala new file mode 100644 index 000000000..387c9102c --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/ScalaTestParams.scala @@ -0,0 +1,38 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.bsp +/** + * Scala Test Params + * ScalaTestParams contains scala-specific metadata for testing Scala targets. + * This metadata is embedded in the data field of the buildTarget/test request + * when the dataKind field contains "scala-test". + */ +final class ScalaTestParams private ( + val testClasses: Vector[sbt.internal.bsp.ScalaTestClassesItem]) extends Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: ScalaTestParams => (this.testClasses == x.testClasses) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (17 + "sbt.internal.bsp.ScalaTestParams".##) + testClasses.##) + } + override def toString: String = { + "ScalaTestParams(" + testClasses + ")" + } + private[this] def copy(testClasses: Vector[sbt.internal.bsp.ScalaTestClassesItem] = testClasses): ScalaTestParams = { + new ScalaTestParams(testClasses) + } + def withTestClasses(testClasses: Vector[sbt.internal.bsp.ScalaTestClassesItem]): ScalaTestParams = { + copy(testClasses = testClasses) + } +} +object ScalaTestParams { + + def apply(testClasses: Vector[sbt.internal.bsp.ScalaTestClassesItem]): ScalaTestParams = new ScalaTestParams(testClasses) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/TestParams.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/TestParams.scala new file mode 100644 index 000000000..551d110c5 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/TestParams.scala @@ -0,0 +1,70 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.bsp +/** + * Test Request + * The test build target request is sent from the client to the server to test the given list of build targets. + * The server communicates during the initialize handshake whether this method is supported or not. + * @param targets A sequence of build targets to test. + * @param originId An option identifier generated by the client to identify this request. + The server may include this id in triggered notifications or responses. + * @param arguments Optional arguments to the test execution engine. + * @param dataKind Kind of data to expect in the `data` field. + If this field is not set, the kind of data is not specified. + * @param data Language-specific metadata for this test execution. + */ +final class TestParams private ( + val targets: Vector[sbt.internal.bsp.BuildTargetIdentifier], + val originId: Option[String], + val arguments: Vector[String], + val dataKind: Option[String], + val data: Option[sjsonnew.shaded.scalajson.ast.unsafe.JValue]) extends Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: TestParams => (this.targets == x.targets) && (this.originId == x.originId) && (this.arguments == x.arguments) && (this.dataKind == x.dataKind) && (this.data == x.data) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (37 * (37 * (37 * (37 * (17 + "sbt.internal.bsp.TestParams".##) + targets.##) + originId.##) + arguments.##) + dataKind.##) + data.##) + } + override def toString: String = { + "TestParams(" + targets + ", " + originId + ", " + arguments + ", " + dataKind + ", " + data + ")" + } + private[this] def copy(targets: Vector[sbt.internal.bsp.BuildTargetIdentifier] = targets, originId: Option[String] = originId, arguments: Vector[String] = arguments, dataKind: Option[String] = dataKind, data: Option[sjsonnew.shaded.scalajson.ast.unsafe.JValue] = data): TestParams = { + new TestParams(targets, originId, arguments, dataKind, data) + } + def withTargets(targets: Vector[sbt.internal.bsp.BuildTargetIdentifier]): TestParams = { + copy(targets = targets) + } + def withOriginId(originId: Option[String]): TestParams = { + copy(originId = originId) + } + def withOriginId(originId: String): TestParams = { + copy(originId = Option(originId)) + } + def withArguments(arguments: Vector[String]): TestParams = { + copy(arguments = arguments) + } + def withDataKind(dataKind: Option[String]): TestParams = { + copy(dataKind = dataKind) + } + def withDataKind(dataKind: String): TestParams = { + copy(dataKind = Option(dataKind)) + } + def withData(data: Option[sjsonnew.shaded.scalajson.ast.unsafe.JValue]): TestParams = { + copy(data = data) + } + def withData(data: sjsonnew.shaded.scalajson.ast.unsafe.JValue): TestParams = { + copy(data = Option(data)) + } +} +object TestParams { + + def apply(targets: Vector[sbt.internal.bsp.BuildTargetIdentifier], originId: Option[String], arguments: Vector[String], dataKind: Option[String], data: Option[sjsonnew.shaded.scalajson.ast.unsafe.JValue]): TestParams = new TestParams(targets, originId, arguments, dataKind, data) + def apply(targets: Vector[sbt.internal.bsp.BuildTargetIdentifier], originId: String, arguments: Vector[String], dataKind: String, data: sjsonnew.shaded.scalajson.ast.unsafe.JValue): TestParams = new TestParams(targets, Option(originId), arguments, Option(dataKind), Option(data)) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/TestProvider.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/TestProvider.scala new file mode 100644 index 000000000..84a15804c --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/TestProvider.scala @@ -0,0 +1,32 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.bsp +final class TestProvider private ( + val languageIds: Vector[String]) extends Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: TestProvider => (this.languageIds == x.languageIds) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (17 + "sbt.internal.bsp.TestProvider".##) + languageIds.##) + } + override def toString: String = { + "TestProvider(" + languageIds + ")" + } + private[this] def copy(languageIds: Vector[String] = languageIds): TestProvider = { + new TestProvider(languageIds) + } + def withLanguageIds(languageIds: Vector[String]): TestProvider = { + copy(languageIds = languageIds) + } +} +object TestProvider { + + def apply(languageIds: Vector[String]): TestProvider = new TestProvider(languageIds) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/TestResult.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/TestResult.scala new file mode 100644 index 000000000..87235b0db --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/TestResult.scala @@ -0,0 +1,45 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.bsp +/** + * Test Result + * @param originId An optional request id to know the origin of this report. + * @param statusCode A status code for the execution. + */ +final class TestResult private ( + val originId: Option[String], + val statusCode: Int) extends Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: TestResult => (this.originId == x.originId) && (this.statusCode == x.statusCode) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (37 * (17 + "sbt.internal.bsp.TestResult".##) + originId.##) + statusCode.##) + } + override def toString: String = { + "TestResult(" + originId + ", " + statusCode + ")" + } + private[this] def copy(originId: Option[String] = originId, statusCode: Int = statusCode): TestResult = { + new TestResult(originId, statusCode) + } + def withOriginId(originId: Option[String]): TestResult = { + copy(originId = originId) + } + def withOriginId(originId: String): TestResult = { + copy(originId = Option(originId)) + } + def withStatusCode(statusCode: Int): TestResult = { + copy(statusCode = statusCode) + } +} +object TestResult { + + def apply(originId: Option[String], statusCode: Int): TestResult = new TestResult(originId, statusCode) + def apply(originId: String, statusCode: Int): TestResult = new TestResult(Option(originId), statusCode) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/BuildServerCapabilitiesFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/BuildServerCapabilitiesFormats.scala index 7864480af..37211376a 100644 --- a/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/BuildServerCapabilitiesFormats.scala +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/BuildServerCapabilitiesFormats.scala @@ -5,18 +5,19 @@ // DO NOT EDIT MANUALLY package sbt.internal.bsp.codec import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } -trait BuildServerCapabilitiesFormats { self: sbt.internal.bsp.codec.CompileProviderFormats with sbt.internal.bsp.codec.RunProviderFormats with sjsonnew.BasicJsonProtocol => +trait BuildServerCapabilitiesFormats { self: sbt.internal.bsp.codec.CompileProviderFormats with sbt.internal.bsp.codec.TestProviderFormats with sbt.internal.bsp.codec.RunProviderFormats with sjsonnew.BasicJsonProtocol => implicit lazy val BuildServerCapabilitiesFormat: JsonFormat[sbt.internal.bsp.BuildServerCapabilities] = new JsonFormat[sbt.internal.bsp.BuildServerCapabilities] { override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.bsp.BuildServerCapabilities = { __jsOpt match { case Some(__js) => unbuilder.beginObject(__js) val compileProvider = unbuilder.readField[Option[sbt.internal.bsp.CompileProvider]]("compileProvider") + val testProvider = unbuilder.readField[Option[sbt.internal.bsp.TestProvider]]("testProvider") val runProvider = unbuilder.readField[Option[sbt.internal.bsp.RunProvider]]("runProvider") val dependencySourcesProvider = unbuilder.readField[Option[Boolean]]("dependencySourcesProvider") val canReload = unbuilder.readField[Option[Boolean]]("canReload") unbuilder.endObject() - sbt.internal.bsp.BuildServerCapabilities(compileProvider, runProvider, dependencySourcesProvider, canReload) + sbt.internal.bsp.BuildServerCapabilities(compileProvider, testProvider, runProvider, dependencySourcesProvider, canReload) case None => deserializationError("Expected JsObject but found None") } @@ -24,6 +25,7 @@ implicit lazy val BuildServerCapabilitiesFormat: JsonFormat[sbt.internal.bsp.Bui override def write[J](obj: sbt.internal.bsp.BuildServerCapabilities, builder: Builder[J]): Unit = { builder.beginObject() builder.addField("compileProvider", obj.compileProvider) + builder.addField("testProvider", obj.testProvider) builder.addField("runProvider", obj.runProvider) builder.addField("dependencySourcesProvider", obj.dependencySourcesProvider) builder.addField("canReload", obj.canReload) diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/JsonProtocol.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/JsonProtocol.scala index 0a2e3fffa..8de3f325f 100644 --- a/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/JsonProtocol.scala +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/JsonProtocol.scala @@ -17,6 +17,7 @@ trait JsonProtocol extends sjsonnew.BasicJsonProtocol with sbt.internal.bsp.codec.BuildClientCapabilitiesFormats with sbt.internal.bsp.codec.InitializeBuildParamsFormats with sbt.internal.bsp.codec.CompileProviderFormats + with sbt.internal.bsp.codec.TestProviderFormats with sbt.internal.bsp.codec.RunProviderFormats with sbt.internal.bsp.codec.BuildServerCapabilitiesFormats with sbt.internal.bsp.codec.InitializeBuildResultFormats @@ -35,6 +36,8 @@ trait JsonProtocol extends sjsonnew.BasicJsonProtocol with sbt.internal.bsp.codec.BspCompileResultFormats with sbt.internal.bsp.codec.CompileTaskFormats with sbt.internal.bsp.codec.CompileReportFormats + with sbt.internal.bsp.codec.TestParamsFormats + with sbt.internal.bsp.codec.TestResultFormats with sbt.internal.bsp.codec.RunParamsFormats with sbt.internal.bsp.codec.RunResultFormats with sbt.internal.bsp.codec.ScalaBuildTargetFormats @@ -44,8 +47,9 @@ trait JsonProtocol extends sjsonnew.BasicJsonProtocol with sbt.internal.bsp.codec.ScalacOptionsResultFormats with sbt.internal.bsp.codec.BspConnectionDetailsFormats with sbt.internal.bsp.codec.MetalsMetadataFormats - with sbt.internal.bsp.codec.ScalaTestClassesParamsFormats with sbt.internal.bsp.codec.ScalaTestClassesItemFormats + with sbt.internal.bsp.codec.ScalaTestParamsFormats + with sbt.internal.bsp.codec.ScalaTestClassesParamsFormats with sbt.internal.bsp.codec.ScalaTestClassesResultFormats with sbt.internal.bsp.codec.ScalaMainClassesParamsFormats with sbt.internal.bsp.codec.ScalaMainClassFormats diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/ScalaTestParamsFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/ScalaTestParamsFormats.scala new file mode 100644 index 000000000..3837db231 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/ScalaTestParamsFormats.scala @@ -0,0 +1,27 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.bsp.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait ScalaTestParamsFormats { self: sbt.internal.bsp.codec.ScalaTestClassesItemFormats with sjsonnew.BasicJsonProtocol => +implicit lazy val ScalaTestParamsFormat: JsonFormat[sbt.internal.bsp.ScalaTestParams] = new JsonFormat[sbt.internal.bsp.ScalaTestParams] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.bsp.ScalaTestParams = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + val testClasses = unbuilder.readField[Vector[sbt.internal.bsp.ScalaTestClassesItem]]("testClasses") + unbuilder.endObject() + sbt.internal.bsp.ScalaTestParams(testClasses) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.bsp.ScalaTestParams, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("testClasses", obj.testClasses) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/TestParamsFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/TestParamsFormats.scala new file mode 100644 index 000000000..7e07dd188 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/TestParamsFormats.scala @@ -0,0 +1,35 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.bsp.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait TestParamsFormats { self: sbt.internal.bsp.codec.BuildTargetIdentifierFormats with sbt.internal.util.codec.JValueFormats with sjsonnew.BasicJsonProtocol => +implicit lazy val TestParamsFormat: JsonFormat[sbt.internal.bsp.TestParams] = new JsonFormat[sbt.internal.bsp.TestParams] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.bsp.TestParams = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + val targets = unbuilder.readField[Vector[sbt.internal.bsp.BuildTargetIdentifier]]("targets") + val originId = unbuilder.readField[Option[String]]("originId") + val arguments = unbuilder.readField[Vector[String]]("arguments") + val dataKind = unbuilder.readField[Option[String]]("dataKind") + val data = unbuilder.readField[Option[sjsonnew.shaded.scalajson.ast.unsafe.JValue]]("data") + unbuilder.endObject() + sbt.internal.bsp.TestParams(targets, originId, arguments, dataKind, data) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.bsp.TestParams, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("targets", obj.targets) + builder.addField("originId", obj.originId) + builder.addField("arguments", obj.arguments) + builder.addField("dataKind", obj.dataKind) + builder.addField("data", obj.data) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/TestProviderFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/TestProviderFormats.scala new file mode 100644 index 000000000..4a36c57c1 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/TestProviderFormats.scala @@ -0,0 +1,27 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.bsp.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait TestProviderFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val TestProviderFormat: JsonFormat[sbt.internal.bsp.TestProvider] = new JsonFormat[sbt.internal.bsp.TestProvider] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.bsp.TestProvider = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + val languageIds = unbuilder.readField[Vector[String]]("languageIds") + unbuilder.endObject() + sbt.internal.bsp.TestProvider(languageIds) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.bsp.TestProvider, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("languageIds", obj.languageIds) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/TestResultFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/TestResultFormats.scala new file mode 100644 index 000000000..2b6d99abd --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/TestResultFormats.scala @@ -0,0 +1,29 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.bsp.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait TestResultFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val TestResultFormat: JsonFormat[sbt.internal.bsp.TestResult] = new JsonFormat[sbt.internal.bsp.TestResult] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.bsp.TestResult = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + val originId = unbuilder.readField[Option[String]]("originId") + val statusCode = unbuilder.readField[Int]("statusCode") + unbuilder.endObject() + sbt.internal.bsp.TestResult(originId, statusCode) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.bsp.TestResult, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("originId", obj.originId) + builder.addField("statusCode", obj.statusCode) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband/bsp.contra b/protocol/src/main/contraband/bsp.contra index ca7031cd7..1d18e7f61 100644 --- a/protocol/src/main/contraband/bsp.contra +++ b/protocol/src/main/contraband/bsp.contra @@ -168,8 +168,8 @@ type BuildServerCapabilities { ## The languages the server supports compilation via method buildTarget/compile. compileProvider: sbt.internal.bsp.CompileProvider - # The languages the server supports test execution via method buildTarget/test - # testProvider: TestProvider + ## The languages the server supports test execution via method buildTarget/test + testProvider: sbt.internal.bsp.TestProvider # The languages the server supports run via method buildTarget/run runProvider: sbt.internal.bsp.RunProvider @@ -198,6 +198,10 @@ type CompileProvider { languageIds: [String] } +type TestProvider { + languageIds: [String] +} + type RunProvider { languageIds: [String] } @@ -371,8 +375,42 @@ type CompileReport { time: Int } +## Test Request +## The test build target request is sent from the client to the server to test the given list of build targets. +## The server communicates during the initialize handshake whether this method is supported or not. +type TestParams { + ## A sequence of build targets to test. + targets: [sbt.internal.bsp.BuildTargetIdentifier] + ## An option identifier generated by the client to identify this request. + ## The server may include this id in triggered notifications or responses. + originId: String + ## Optional arguments to the test execution engine. + arguments: [String] + + ## Kind of data to expect in the `data` field. + ## If this field is not set, the kind of data is not specified. + dataKind: String + + ## Language-specific metadata for this test execution. + data: sjsonnew.shaded.scalajson.ast.unsafe.JValue +} + +## Test Result +type TestResult { + ## An optional request id to know the origin of this report. + originId: String + + ## A status code for the execution. + statusCode: Int! + + # Kind of data to expect in the `data` field. + # If this field is not set, the kind of data is not specified. + # dataKind: String + + # data: sjsonnew.shaded.scalajson.ast.unsafe.JValue +} ## Run Request ## The run request is sent from the client to the server to run a build target. @@ -510,6 +548,14 @@ type MetalsMetadata { supportedScalaVersions: [String] } +## Scala Test Params +## ScalaTestParams contains scala-specific metadata for testing Scala targets. +## This metadata is embedded in the data field of the buildTarget/test request +## when the dataKind field contains "scala-test". +type ScalaTestParams { + testClasses: [sbt.internal.bsp.ScalaTestClassesItem] +} + ## Scala Test Class Request ## The build target scala test options request is sent from the client to the server ## to query for the list of fully qualified names of test clases in a given list of targets. diff --git a/server-test/src/server-test/buildserver/foo/src/main/scala/foo/FooMain.scala b/server-test/src/server-test/buildserver/foo/src/main/scala/foo/FooMain.scala index 29209deca..348be2497 100644 --- a/server-test/src/server-test/buildserver/foo/src/main/scala/foo/FooMain.scala +++ b/server-test/src/server-test/buildserver/foo/src/main/scala/foo/FooMain.scala @@ -1,7 +1,7 @@ package foo object FooMain extends App { - val message = "Hello World!" + lazy val message = "Hello World!" println(message) } diff --git a/server-test/src/server-test/buildserver/foo/src/test/scala/foo/FailingTest.scala b/server-test/src/server-test/buildserver/foo/src/test/scala/foo/FailingTest.scala new file mode 100644 index 000000000..886d3d6aa --- /dev/null +++ b/server-test/src/server-test/buildserver/foo/src/test/scala/foo/FailingTest.scala @@ -0,0 +1,9 @@ +package foo + +import org.scalatest.FreeSpec + +class FailingTest extends FreeSpec { + "it should fail" in { + throw new Exception("Test failed") + } +} \ No newline at end of file diff --git a/server-test/src/server-test/buildserver/foo/src/test/scala/foo/FooTest.scala b/server-test/src/server-test/buildserver/foo/src/test/scala/foo/FooTest.scala index 8e230be99..1874da8c1 100644 --- a/server-test/src/server-test/buildserver/foo/src/test/scala/foo/FooTest.scala +++ b/server-test/src/server-test/buildserver/foo/src/test/scala/foo/FooTest.scala @@ -2,7 +2,7 @@ package foo import org.scalatest.FreeSpec -class FooTest extends FreeSpec { +class FooTest extends FreeSpec { "test message" in { assert(FooMain.message == "Hello World!") } diff --git a/server-test/src/test/scala/testpkg/BuildServerTest.scala b/server-test/src/test/scala/testpkg/BuildServerTest.scala index 6ee1a26e6..c6d9c75ec 100644 --- a/server-test/src/test/scala/testpkg/BuildServerTest.scala +++ b/server-test/src/test/scala/testpkg/BuildServerTest.scala @@ -130,7 +130,44 @@ object BuildServerTest extends AbstractServerTest { assert(svr.waitForString(10.seconds) { s => println(s) (s contains """"id":"18"""") && - (s contains """"classes":["foo.FooTest"]""") + (s contains """"classes":["foo.FailingTest","foo.FooTest"]""") + }) + } + + test("buildTarget/test: run all tests") { _ => + val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#foo/Test" + svr.sendJsonRpc( + s"""{ "jsonrpc": "2.0", "id": "19", "method": "buildTarget/test", "params": { + | "targets": [{ "uri": "$x" }] + |} }""".stripMargin + ) + assert(svr.waitForString(10.seconds) { s => + println(s) + (s contains """"id":"19"""") && + (s contains """"statusCode":2""") + }) + } + + test("buildTarget/test: run one test class") { _ => + val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#foo/Test" + svr.sendJsonRpc( + s"""{ "jsonrpc": "2.0", "id": "20", "method": "buildTarget/test", "params": { + | "targets": [{ "uri": "$x" }], + | "dataKind": "scala-test", + | "data": { + | "testClasses": [ + | { + | "target": { "uri": "$x" }, + | "classes": ["foo.FooTest"] + | } + | ] + | } + |} }""".stripMargin + ) + assert(svr.waitForString(10.seconds) { s => + println(s) + (s contains """"id":"20"""") && + (s contains """"statusCode":1""") }) }