Refine and test SBT BSP target

- Pass source dirs and current list of files
  - Align display name and URI
This commit is contained in:
Jason Zaugg 2021-06-30 11:19:24 +10:00
parent 0bd736be2a
commit fe046476b1
7 changed files with 140 additions and 35 deletions

View File

@ -144,7 +144,9 @@ final case class PluginData(
resolvers: Option[Vector[Resolver]],
report: Option[UpdateReport],
scalacOptions: Seq[String],
unmanagedSourceDirectories: Seq[File],
unmanagedSources: Seq[File],
managedSourceDirectories: Seq[File],
managedSources: Seq[File]
) {
val classpath: Seq[Attributed[File]] = definitionClasspath ++ dependencyClasspath
@ -152,7 +154,7 @@ final case class PluginData(
object PluginData {
private[sbt] def apply(dependencyClasspath: Def.Classpath): PluginData =
PluginData(dependencyClasspath, Nil, None, None, Nil, Nil, Nil)
PluginData(dependencyClasspath, Nil, None, None, Nil, Nil, Nil, Nil, Nil)
}
object EvaluateTask {

View File

@ -1164,16 +1164,20 @@ private[sbt] object Load {
val prod = (Configurations.Runtime / exportedProducts).value
val cp = (Configurations.Runtime / fullClasspath).value
val opts = (Configurations.Compile / scalacOptions).value
val managedSrcs = (Configurations.Compile / managedSources).value
val unmanagedSrcDirs = (Configurations.Compile / unmanagedSourceDirectories).value
val unmanagedSrcs = (Configurations.Compile / unmanagedSources).value
val managedSrcDirs = (Configurations.Compile / managedSourceDirectories).value
val managedSrcs = (Configurations.Compile / managedSources).value
PluginData(
removeEntries(cp, prod),
prod,
Some(fullResolvers.value.toVector),
Some(update.value),
opts,
unmanagedSrcDirs,
unmanagedSrcs,
managedSrcs
managedSrcDirs,
managedSrcs,
)
},
scalacOptions += "-Wconf:cat=unused-nowarn:s",
@ -1229,7 +1233,7 @@ private[sbt] object Load {
loadPluginDefinition(
dir,
config,
PluginData(config.globalPluginClasspath, Nil, None, None, Nil, Nil, Nil)
PluginData(config.globalPluginClasspath, Nil, None, None, Nil, Nil, Nil, Nil, Nil)
)
def buildPlugins(dir: File, s: State, config: LoadBuildConfiguration): LoadedPlugins =
@ -1423,6 +1427,8 @@ final case class LoadBuildConfiguration(
Some(data.updateReport),
Nil,
Nil,
Nil,
Nil,
Nil
)
case None => PluginData(globalPluginClasspath)

View File

@ -132,15 +132,21 @@ object BuildServerProtocol {
val base = loadedBuildUnit.localBase
val sbtFiles = configurationSources(base)
val pluginData = loadedBuildUnit.unit.plugins.pluginData
val unmanagedSources = pluginData.unmanagedSources.map(
f => SourceItem(f.toURI, SourceItemKind.File, generated = false)
)
val managedSources = pluginData.managedSources.map(
f => SourceItem(f.toURI, SourceItemKind.File, generated = true)
)
val sbtFilesItems =
sbtFiles.map(f => SourceItem(f.toURI, SourceItemKind.File, generated = false))
SourcesItem(id, (unmanagedSources ++ managedSources ++ sbtFilesItems).toVector)
val all = Vector.newBuilder[SourceItem]
def add(fs: Seq[File], sourceItemKind: Int, generated: Boolean): Unit = {
fs.foreach(f => all += (SourceItem(f.toURI, sourceItemKind, generated = generated)))
}
all += (SourceItem(
loadedBuildUnit.unit.plugins.base.toURI,
SourceItemKind.Directory,
generated = false
))
add(pluginData.unmanagedSourceDirectories, SourceItemKind.Directory, generated = false)
add(pluginData.unmanagedSources, SourceItemKind.File, generated = false)
add(pluginData.managedSourceDirectories, SourceItemKind.Directory, generated = true)
add(pluginData.managedSources, SourceItemKind.File, generated = true)
add(sbtFiles, SourceItemKind.File, generated = false)
SourcesItem(id, all.result())
}
val result = SourcesResult((items ++ buildItems).toVector)
s.respondEvent(result)
@ -499,13 +505,17 @@ object BuildServerProtocol {
scope.project.toOption match {
case Some(ProjectRef(buildUri, _)) =>
val loadedBuildUnit = loadedBuild.units(buildUri)
buildsMap.getOrElseUpdate(toId(loadedBuildUnit), new mutable.ListBuffer) += targetId
buildsMap.getOrElseUpdate(
toSbtTargetId(loadedBuildUnit),
new mutable.ListBuffer
) += targetId
}
targetId -> scope
}
val buildMap = if (bspSbtEnabled.value) {
for (loadedBuildUnit <- loadedBuild.units.values) yield {
toId(loadedBuildUnit) -> loadedBuildUnit
val rootProjectId = loadedBuildUnit.root
toSbtTargetId(loadedBuildUnit) -> loadedBuildUnit
}
} else {
Nil
@ -557,7 +567,6 @@ object BuildServerProtocol {
buildTargetIdentifier: BuildTargetIdentifier,
buildFor: Seq[BuildTargetIdentifier]
): Def.Initialize[Task[BuildTarget]] = Def.task {
val structure = buildStructure.value
val scalaProvider = appConfiguration.value.provider().scalaProvider()
appConfiguration.value.provider().mainClasspath()
val scalaJars = scalaProvider.jars()
@ -579,9 +588,7 @@ object BuildServerProtocol {
BuildTarget(
buildTargetIdentifier,
// naming convention still seems like the only way to get IntelliJ to import this correctly
// https://github.com/JetBrains/intellij-scala/blob/a54c2a7c157236f35957049cbfd8c10587c9e60c/scala/scala-impl/src/org/jetbrains/sbt/language/SbtFileImpl.scala#L82-L84
structure.rootProject(loadedUnit.unit.uri) + "-build",
toSbtTargetIdName(loadedUnit),
projectStandard(loadedUnit.unit.localBase).toURI,
Vector(),
BuildTargetCapabilities(canCompile = false, canTest = false, canRun = false),
@ -828,14 +835,19 @@ object BuildServerProtocol {
)
}
private val SbtBuildSuffix = "#sbt-build"
private def toId(ref: LoadedBuildUnit): BuildTargetIdentifier = {
// naming convention still seems like the only reliable way to get IntelliJ to import this correctly
// https://github.com/JetBrains/intellij-scala/blob/a54c2a7c157236f35957049cbfd8c10587c9e60c/scala/scala-impl/src/org/jetbrains/sbt/language/SbtFileImpl.scala#L82-L84
private def toSbtTargetIdName(ref: LoadedBuildUnit): String = {
ref.root + "-build"
}
private def toSbtTargetId(ref: LoadedBuildUnit): BuildTargetIdentifier = {
val name = toSbtTargetIdName(ref)
val build = ref.unit.uri
val sanitized = build.toString.indexOf("#") match {
case i if i > 0 => build.toString.take(i)
case _ => build.toString
}
BuildTargetIdentifier(new URI(sanitized + SbtBuildSuffix))
BuildTargetIdentifier(new URI(sanitized + "#" + name))
}
private def toId(ref: ProjectReference, config: Configuration): BuildTargetIdentifier =
ref match {
@ -879,7 +891,9 @@ object BuildServerProtocol {
}
def warnIfBuildsNonEmpty(method: String, log: Logger): Unit = {
if (builds.nonEmpty)
log.warn(s"$method is a no-op for build.sbt targets: ${builds.keys.mkString("[", ",", "]")}")
log.warn(
s"$method is a no-op for build.sbt targets: ${builds.keys.mkString("[", ",", "]")}"
)
}
}
}

View File

@ -7,10 +7,18 @@
package testpkg
import sbt.internal.bsp.SourcesResult
import java.io.File
import sbt.internal.bsp.WorkspaceBuildTargetsResult
import scala.concurrent.duration._
// starts svr using server-test/buildserver and perform custom server tests
object BuildServerTest extends AbstractServerTest {
import sbt.internal.bsp.codec.JsonProtocol._
override val testDirectory: String = "buildserver"
test("build/initialize") { _ =>
@ -26,12 +34,12 @@ object BuildServerTest extends AbstractServerTest {
"""{ "jsonrpc": "2.0", "id": "16", "method": "workspace/buildTargets", "params": {} }"""
)
assert(processing("workspace/buildTargets"))
assert {
svr.waitForString(10.seconds) { s =>
(s contains """"id":"16"""") &&
(s contains """"displayName":"util"""")
}
}
val result = svr.waitFor[WorkspaceBuildTargetsResult](10.seconds)
val utilTarget = result.targets.find(_.displayName.contains("util")).get
assert(utilTarget.id.uri.toString.endsWith("#util/Compile"))
val buildServerBuildTarget =
result.targets.find(_.displayName.contains("buildserver-build")).get
assert(buildServerBuildTarget.id.uri.toString.endsWith("#buildserver-build"))
}
test("buildTarget/sources") { _ =>
@ -42,10 +50,33 @@ object BuildServerTest extends AbstractServerTest {
|} }""".stripMargin
)
assert(processing("buildTarget/sources"))
assert(svr.waitForString(10.seconds) { s =>
(s contains """"id":"24"""") &&
(s contains "util/src/main/scala")
})
val s = svr.waitFor[SourcesResult](10.seconds)
val sources = s.items.head.sources.map(_.uri)
assert(sources.contains(new File(svr.baseDirectory, "util/src/main/scala").toURI))
}
test("buildTarget/sources SBT") { _ =>
val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#buildserver-build"
svr.sendJsonRpc(
s"""{ "jsonrpc": "2.0", "id": "25", "method": "buildTarget/sources", "params": {
| "targets": [{ "uri": "$x" }]
|} }""".stripMargin
)
assert(processing("buildTarget/sources"))
val s = svr.waitFor[SourcesResult](10.seconds)
val sources = s.items.head.sources.map(_.uri).sorted
val expectedSources = Vector(
"build.sbt",
"project/",
"project/A.scala",
"project/src/main/java",
"project/src/main/scala-2",
"project/src/main/scala-2.12",
"project/src/main/scala-sbt-1.0",
"project/src/main/scala/",
"project/src/main/scala/B.scala",
"project/target/scala-2.12/sbt-1.0/src_managed/main"
).map(rel => new File(svr.baseDirectory.getAbsoluteFile, rel).toURI).sorted
assert(sources == expectedSources)
}
test("buildTarget/compile") { _ =>

View File

@ -12,17 +12,18 @@ import java.net.Socket
import java.nio.file.{ Files, Path }
import java.util.concurrent.{ LinkedBlockingQueue, TimeUnit }
import java.util.concurrent.atomic.AtomicBoolean
import verify._
import sbt.{ ForkOptions, OutputStrategy, RunFromSourceMain }
import sbt.io.IO
import sbt.io.syntax._
import sbt.protocol.ClientSocket
import sjsonnew.JsonReader
import sjsonnew.support.scalajson.unsafe.{ Converter, Parser }
import scala.annotation.tailrec
import scala.concurrent._
import scala.concurrent.duration._
import scala.util.{ Success, Try }
import scala.util.{ Failure, Success, Try }
trait AbstractServerTest extends TestSuite[Unit] {
private var temp: File = _
@ -293,6 +294,57 @@ case class TestServer(
}
impl()
}
final def waitFor[T: JsonReader](duration: FiniteDuration): T = {
val deadline = duration.fromNow
var lastEx: Throwable = null
@tailrec def impl(): T =
lines.poll(deadline.timeLeft.toMillis, TimeUnit.MILLISECONDS) match {
case null =>
if (lastEx != null) throw lastEx
else throw new TimeoutException
case s =>
Parser
.parseFromString(s)
.flatMap(
jvalue =>
Converter.fromJson[T](
jvalue.toStandard
.asInstanceOf[sjsonnew.shaded.scalajson.ast.JObject]
.value("result")
.toUnsafe
)
) match {
case Success(value) =>
value
case Failure(exception) =>
if (deadline.isOverdue) {
val ex = new TimeoutException()
ex.initCause(exception)
throw ex
} else {
lastEx = exception
impl()
}
}
}
impl()
}
final def waitForResponse(duration: FiniteDuration, id: Int): String = {
val deadline = duration.fromNow
@tailrec def impl(): String =
lines.poll(deadline.timeLeft.toMillis, TimeUnit.MILLISECONDS) match {
case null =>
throw new TimeoutException()
case s =>
val s1 = s
val correctId = s1.contains("\"id\":\"" + id + "\"")
if (!correctId && !deadline.isOverdue) impl()
else if (deadline.isOverdue)
throw new TimeoutException()
else s
}
impl()
}
final def neverReceive(duration: FiniteDuration)(f: String => Boolean): Boolean = {
val deadline = duration.fromNow