Merge pull request #7417 from adpi2/build-server-source-mapping

[1.10.x] Use sourcePositionMappers to report BSP diagnostics
This commit is contained in:
adpi2 2023-10-25 16:23:02 +02:00 committed by GitHub
commit 3e205d04b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 293 additions and 296 deletions

View File

@ -33,7 +33,6 @@ import sjsonnew.shaded.scalajson.ast.unsafe.{ JNull, JValue }
import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter, Parser => JsonParser } import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter, Parser => JsonParser }
import xsbti.CompileFailed import xsbti.CompileFailed
import java.nio.file.Path
import java.io.File import java.io.File
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import scala.collection.mutable import scala.collection.mutable
@ -44,6 +43,8 @@ import scala.util.{ Failure, Success, Try }
import scala.annotation.nowarn import scala.annotation.nowarn
import sbt.testing.Framework import sbt.testing.Framework
import scala.collection.immutable.ListSet import scala.collection.immutable.ListSet
import xsbti.VirtualFileRef
import java.util.concurrent.atomic.AtomicReference
object BuildServerProtocol { object BuildServerProtocol {
import sbt.internal.bsp.codec.JsonProtocol._ import sbt.internal.bsp.codec.JsonProtocol._
@ -329,11 +330,13 @@ object BuildServerProtocol {
val underlying = (Keys.compile / compilerReporter).value val underlying = (Keys.compile / compilerReporter).value
val logger = streams.value.log val logger = streams.value.log
val meta = isMetaBuild.value val meta = isMetaBuild.value
val spms = sourcePositionMappers.value
if (bspEnabled.value) { if (bspEnabled.value) {
new BuildServerReporterImpl( new BuildServerReporterImpl(
targetId, targetId,
bspCompileStateInstance, bspCompileStateInstance,
converter, converter,
Defaults.foldMappers(spms, reportAbsolutePath.value, fileConverter.value),
meta, meta,
logger, logger,
underlying underlying
@ -1064,11 +1067,14 @@ object BuildServerProtocol {
private[server] final class BspCompileState { private[server] final class BspCompileState {
/** /**
* keeps track of problems in given file so BSP reporter * keeps track of problems in a given file in a map of virtual source file to text documents.
* can omit unnecessary diagnostics updates * In most cases the only text document is the source file. In case of source generation,
* e.g. Twirl, the text documents are the input files, e.g. the Twirl files.
* We use the sourcePositionMappers to build this map.
*/ */
val hasAnyProblems: java.util.Set[Path] = val problemsBySourceFiles
java.util.concurrent.ConcurrentHashMap.newKeySet[Path] : AtomicReference[Map[VirtualFileRef, Vector[TextDocumentIdentifier]]] =
new AtomicReference(Map.empty)
/** /**
* keeps track of those projects that were compiled at * keeps track of those projects that were compiled at
@ -1076,6 +1082,6 @@ object BuildServerProtocol {
* are compiled for the first time. * are compiled for the first time.
* see: https://github.com/scalacenter/bloop/issues/726 * see: https://github.com/scalacenter/bloop/issues/726
*/ */
val compiledAtLeastOnce: AtomicBoolean = new AtomicBoolean(false) val isFirstReport: AtomicBoolean = new AtomicBoolean(true)
} }
} }

View File

@ -8,8 +8,6 @@
package sbt.internal.server package sbt.internal.server
import java.nio.file.Path
import sbt.StandardMain import sbt.StandardMain
import sbt.internal.bsp._ import sbt.internal.bsp._
import sbt.internal.util.ManagedLogger import sbt.internal.util.ManagedLogger
@ -28,6 +26,9 @@ import xsbti.{
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
import scala.collection.mutable import scala.collection.mutable
/**
Provides methods for sending success and failure reports and publishing diagnostics.
*/
sealed trait BuildServerReporter extends Reporter { sealed trait BuildServerReporter extends Reporter {
private final val sigFilesWritten = "[sig files written]" private final val sigFilesWritten = "[sig files written]"
private final val pureExpression = "a pure expression does nothing in statement position" private final val pureExpression = "a pure expression does nothing in statement position"
@ -71,10 +72,16 @@ sealed trait BuildServerReporter extends Reporter {
override def comment(pos: XPosition, msg: String): Unit = underlying.comment(pos, msg) override def comment(pos: XPosition, msg: String): Unit = underlying.comment(pos, msg)
} }
/**
* @param bspCompileState what has already been reported in previous compilation.
* @param sourcePositionMapper a function that maps an xsbti.Position from the generated source
* (the Scala file) to the input file of the generator (e.g. Twirl file)
*/
final class BuildServerReporterImpl( final class BuildServerReporterImpl(
buildTarget: BuildTargetIdentifier, buildTarget: BuildTargetIdentifier,
bspCompileState: BspCompileState, bspCompileState: BspCompileState,
converter: FileConverter, converter: FileConverter,
sourcePositionMapper: xsbti.Position => xsbti.Position,
protected override val isMetaBuild: Boolean, protected override val isMetaBuild: Boolean,
protected override val logger: ManagedLogger, protected override val logger: ManagedLogger,
protected override val underlying: Reporter protected override val underlying: Reporter
@ -83,13 +90,13 @@ final class BuildServerReporterImpl(
import sbt.internal.inc.JavaInterfaceUtil._ import sbt.internal.inc.JavaInterfaceUtil._
private lazy val exchange = StandardMain.exchange private lazy val exchange = StandardMain.exchange
private val problemsByFile = mutable.Map[Path, Vector[Diagnostic]]() private val problemsByFile = mutable.Map[VirtualFileRef, Vector[Problem]]()
// sometimes the compiler returns a fake position such as <macro> // sometimes the compiler returns a fake position such as <macro>
// on Windows, this causes InvalidPathException (see #5994 and #6720) // on Windows, this causes InvalidPathException (see #5994 and #6720)
private def toSafePath(ref: VirtualFileRef): Option[Path] = private def toDocument(ref: VirtualFileRef): Option[TextDocumentIdentifier] =
if (ref.id().contains("<")) None if (ref.id().contains("<")) None
else Some(converter.toPath(ref)) else Some(TextDocumentIdentifier(converter.toPath(ref).toUri))
/** /**
* Send diagnostics from the compilation to the client. * Send diagnostics from the compilation to the client.
@ -97,65 +104,43 @@ final class BuildServerReporterImpl(
* *
* @param analysis current compile analysis * @param analysis current compile analysis
*/ */
override def sendSuccessReport( override def sendSuccessReport(analysis: CompileAnalysis): Unit = {
analysis: CompileAnalysis, for ((source, infos) <- analysis.readSourceInfos.getAllSourceInfos.asScala) {
): Unit = { val problems = infos.getReportedProblems.toVector
val shouldReportAllProblems = !bspCompileState.compiledAtLeastOnce.getAndSet(true) sendReport(source, problems)
for { }
(source, infos) <- analysis.readSourceInfos.getAllSourceInfos.asScala notifyFirstReport()
filePath <- toSafePath(source) }
} {
// clear problems for current file
val hadProblems = bspCompileState.hasAnyProblems.remove(filePath)
val reportedProblems = infos.getReportedProblems.toVector override def sendFailureReport(sources: Array[VirtualFile]): Unit = {
val diagnostics = reportedProblems.map(toDiagnostic) for (source <- sources) {
val problems = problemsByFile.getOrElse(source, Vector.empty)
// publish diagnostics if: sendReport(source, problems)
// 1. file had any problems previously - we might want to update them with new ones
// 2. file has fresh problems - we might want to update old ones
// 3. build project is compiled first time - shouldReportAllProblems is set
val shouldPublish = hadProblems || diagnostics.nonEmpty || shouldReportAllProblems
// file can have some warnings
if (diagnostics.nonEmpty) {
bspCompileState.hasAnyProblems.add(filePath)
}
if (shouldPublish) {
val params = PublishDiagnosticsParams(
textDocument = TextDocumentIdentifier(filePath.toUri),
buildTarget,
originId = None,
diagnostics.toVector,
reset = true
)
exchange.notifyEvent("build/publishDiagnostics", params)
}
} }
} }
override def sendFailureReport(sources: Array[VirtualFile]): Unit = {
val shouldReportAllProblems = !bspCompileState.compiledAtLeastOnce.get
for {
source <- sources
filePath <- toSafePath(source)
} {
val diagnostics = problemsByFile.getOrElse(filePath, Vector.empty)
val hadProblems = bspCompileState.hasAnyProblems.remove(filePath) private def sendReport(source: VirtualFileRef, problems: Vector[Problem]): Unit = {
val shouldPublish = hadProblems || diagnostics.nonEmpty || shouldReportAllProblems val oldDocuments = getAndClearPreviousDocuments(source)
// mark file as file with problems // publish diagnostics if:
if (diagnostics.nonEmpty) { // 1. file had any problems previously: update them with new ones
bspCompileState.hasAnyProblems.add(filePath) // 2. file has fresh problems: report them
} // 3. build project is compiled for the first time: send success report
if (oldDocuments.nonEmpty || problems.nonEmpty || isFirstReport) {
val diagsByDocuments = problems
.flatMap(mapProblemToDiagnostic)
.groupBy { case (document, _) => document }
.mapValues(_.map { case (_, diag) => diag })
updateNewDocuments(source, diagsByDocuments.keys.toVector)
if (shouldPublish) { // send a report for the new documents, the old ones and the source file
(diagsByDocuments.keySet ++ oldDocuments ++ toDocument(source)).foreach { document =>
val diags = diagsByDocuments.getOrElse(document, Vector.empty)
val params = PublishDiagnosticsParams( val params = PublishDiagnosticsParams(
textDocument = TextDocumentIdentifier(filePath.toUri), document,
buildTarget, buildTarget,
originId = None, originId = None,
diagnostics, diags,
reset = true reset = true
) )
exchange.notifyEvent("build/publishDiagnostics", params) exchange.notifyEvent("build/publishDiagnostics", params)
@ -166,12 +151,13 @@ final class BuildServerReporterImpl(
protected override def publishDiagnostic(problem: Problem): Unit = { protected override def publishDiagnostic(problem: Problem): Unit = {
for { for {
id <- problem.position.sourcePath.toOption id <- problem.position.sourcePath.toOption
filePath <- toSafePath(VirtualFileRef.of(id)) (document, diagnostic) <- mapProblemToDiagnostic(problem)
} { } {
val diagnostic = toDiagnostic(problem) val fileRef = VirtualFileRef.of(id)
problemsByFile(filePath) = problemsByFile.getOrElse(filePath, Vector.empty) :+ diagnostic problemsByFile(fileRef) = problemsByFile.getOrElse(fileRef, Vector.empty) :+ problem
val params = PublishDiagnosticsParams( val params = PublishDiagnosticsParams(
TextDocumentIdentifier(filePath.toUri), document,
buildTarget, buildTarget,
originId = None, originId = None,
Vector(diagnostic), Vector(diagnostic),
@ -181,13 +167,51 @@ final class BuildServerReporterImpl(
} }
} }
private def toRange(pos: XPosition): Range = { private def getAndClearPreviousDocuments(source: VirtualFileRef): Seq[TextDocumentIdentifier] =
val startLineOpt = pos.startLine.toOption.map(_.toLong - 1) bspCompileState.problemsBySourceFiles.getAndUpdate(_ - source).getOrElse(source, Seq.empty)
val startColumnOpt = pos.startColumn.toOption.map(_.toLong)
val endLineOpt = pos.endLine.toOption.map(_.toLong - 1) private def updateNewDocuments(
val endColumnOpt = pos.endColumn.toOption.map(_.toLong) source: VirtualFileRef,
val lineOpt = pos.line.toOption.map(_.toLong - 1) documents: Vector[TextDocumentIdentifier]
val columnOpt = pos.pointer.toOption.map(_.toLong) ): Unit = {
val _ = bspCompileState.problemsBySourceFiles.updateAndGet(_ + (source -> documents))
}
private def isFirstReport: Boolean = bspCompileState.isFirstReport.get
private def notifyFirstReport(): Unit = {
val _ = bspCompileState.isFirstReport.set(false)
}
/**
* Map a given problem, in a Scala source file, to a Diagnostic in an user-facing source file.
* E.g. if the source file is generated from Twirl, the diagnostic will be reported to the Twirl file.
*/
private def mapProblemToDiagnostic(
problem: Problem
): Option[(TextDocumentIdentifier, Diagnostic)] = {
val mappedPosition = sourcePositionMapper(problem.position)
for {
mappedSource <- mappedPosition.sourcePath.toOption
document <- toDocument(VirtualFileRef.of(mappedSource))
} yield {
val diagnostic = Diagnostic(
toRange(mappedPosition),
Option(toDiagnosticSeverity(problem.severity)),
problem.diagnosticCode().toOption.map(_.code),
Option("sbt"),
problem.message
)
(document, diagnostic)
}
}
private def toRange(position: xsbti.Position): Range = {
val startLineOpt = position.startLine.toOption.map(_.toLong - 1)
val startColumnOpt = position.startColumn.toOption.map(_.toLong)
val endLineOpt = position.endLine.toOption.map(_.toLong - 1)
val endColumnOpt = position.endColumn.toOption.map(_.toLong)
val lineOpt = position.line.toOption.map(_.toLong - 1)
val columnOpt = position.pointer.toOption.map(_.toLong)
def toPosition(lineOpt: Option[Long], columnOpt: Option[Long]): Option[Position] = def toPosition(lineOpt: Option[Long], columnOpt: Option[Long]): Option[Position] =
lineOpt.map(line => Position(line, columnOpt.getOrElse(0L))) lineOpt.map(line => Position(line, columnOpt.getOrElse(0L)))
@ -199,45 +223,6 @@ final class BuildServerReporterImpl(
Range(startPos, endPosOpt.getOrElse(startPos)) Range(startPos, endPosOpt.getOrElse(startPos))
} }
private def toDiagnostic(problem: Problem): Diagnostic = {
val actions0 = problem.actions().asScala.toVector
val data =
if (actions0.isEmpty) None
else
Some(
ScalaDiagnostic(
actions = actions0.map { a =>
ScalaAction(
title = a.title,
description = a.description.toOption,
edit = Some(
ScalaWorkspaceEdit(
changes = a.edit.changes().asScala.toVector.map { edit =>
ScalaTextEdit(
range = toRange(edit.position),
newText = edit.newText,
)
}
)
),
)
}
)
)
Diagnostic(
range = toRange(problem.position),
severity = Option(toDiagnosticSeverity(problem.severity)),
code = problem.diagnosticCode().toOption.map(_.code),
source = Option("sbt"),
message = problem.message,
relatedInformation = Vector.empty,
dataKind = data.map { _ =>
"scala"
},
data = data,
)
}
private def toDiagnosticSeverity(severity: Severity): Long = severity match { private def toDiagnosticSeverity(severity: Severity): Long = severity match {
case Severity.Info => DiagnosticSeverity.Information case Severity.Info => DiagnosticSeverity.Information
case Severity.Warn => DiagnosticSeverity.Warning case Severity.Warn => DiagnosticSeverity.Warning

View File

@ -42,6 +42,10 @@ lazy val javaProj = project
javacOptions += "-Xlint:all" javacOptions += "-Xlint:all"
) )
lazy val twirlProj = project
.in(file("twirlProj"))
.enablePlugins(SbtTwirl)
def somethingBad = throw new MessageOnlyException("I am a bad build target") def somethingBad = throw new MessageOnlyException("I am a bad build target")
// other build targets should not be affected by this bad build target // other build targets should not be affected by this bad build target
lazy val badBuildTarget = project.in(file("bad-build-target")) lazy val badBuildTarget = project.in(file("bad-build-target"))

View File

@ -0,0 +1 @@
addSbtPlugin("com.typesafe.play" % "sbt-twirl" % "1.5.2")

View File

@ -0,0 +1,14 @@
@(title: String, paragraphs: Seq[String])
<!DOCTYPE HTML>
<html lang="en">
<head>
<title>@title</title>
</head>
<body>
<h1>@tilte</h1>
@for(paragraph <- paragraphs) {
<p>@paragraph</p>
}
</body>
</html>

View File

@ -33,11 +33,11 @@ object BuildServerTest extends AbstractServerTest {
test("build/initialize") { _ => test("build/initialize") { _ =>
val id = initializeRequest() val id = initializeRequest()
assert(svr.waitForString(10.seconds) { s => assertMessage(
(s contains s""""id":"${id}"""") && s""""id":"${id}"""",
(s contains """"resourcesProvider":true""") && """"resourcesProvider":true""",
(s contains """"outputPathsProvider":true""") """"outputPathsProvider":true"""
}) )()
} }
test("workspace/buildTargets") { _ => test("workspace/buildTargets") { _ =>
@ -102,30 +102,17 @@ object BuildServerTest extends AbstractServerTest {
compile(buildTarget) compile(buildTarget)
// This doesn't always come back in 10s on CI. // This doesn't always come back in 10s on CI.
assert(svr.waitForString(60.seconds) { s => assertMessage("build/taskStart", """"message":"Compiling runAndTest"""")(duration = 60.seconds)
s.contains("build/taskStart") && assertMessage(
s.contains(""""message":"Compiling runAndTest"""") "build/taskProgress",
}) """"message":"Compiling runAndTest (15%)""""
)(duration = 60.seconds)
assert(svr.waitForString(60.seconds) { s => assertMessage(
s.contains("build/taskProgress") && "build/taskProgress",
s.contains(""""message":"Compiling runAndTest (15%)"""") """"message":"Compiling runAndTest (100%)""""
}) )(duration = 60.seconds)
assertMessage("build/publishDiagnostics", """"diagnostics":[]""")(duration = 60.seconds)
assert(svr.waitForString(60.seconds) { s => assertMessage("build/taskFinish", """"message":"Compiled runAndTest"""")(duration = 60.seconds)
s.contains("build/taskProgress") &&
s.contains(""""message":"Compiling runAndTest (100%)"""")
})
assert(svr.waitForString(60.seconds) { s =>
s.contains("build/publishDiagnostics") &&
s.contains(""""diagnostics":[]""")
})
assert(svr.waitForString(60.seconds) { s =>
s.contains("build/taskFinish") &&
s.contains(""""message":"Compiled runAndTest"""")
})
} }
test( test(
@ -136,10 +123,7 @@ object BuildServerTest extends AbstractServerTest {
compile(buildTarget) compile(buildTarget)
assert(svr.waitForString(30.seconds) { s => assertMessage("build/taskFinish", """"message":"Compiled diagnostics"""")(30.seconds)
s.contains("build/taskFinish") &&
s.contains(""""message":"Compiled diagnostics"""")
})
// introduce compile error // introduce compile error
IO.write( IO.write(
@ -152,13 +136,13 @@ object BuildServerTest extends AbstractServerTest {
reloadWorkspace() reloadWorkspace()
compile(buildTarget) compile(buildTarget)
assert( assertMessage(
svr.waitForString(30.seconds) { s => "build/publishDiagnostics",
s.contains("build/publishDiagnostics") && "Diagnostics.scala",
s.contains("Diagnostics.scala") && "\"message\":\"type mismatch"
s.contains("\"message\":\"type mismatch") )(
}, duration = 30.seconds,
"should send publishDiagnostics with type error for Main.scala" message = "should send publishDiagnostics with type error for Main.scala"
) )
// fix compilation error // fix compilation error
@ -172,13 +156,13 @@ object BuildServerTest extends AbstractServerTest {
reloadWorkspace() reloadWorkspace()
compile(buildTarget) compile(buildTarget)
assert( assertMessage(
svr.waitForString(30.seconds) { s => "build/publishDiagnostics",
s.contains("build/publishDiagnostics") && "Diagnostics.scala",
s.contains("Diagnostics.scala") && "\"diagnostics\":[]"
s.contains("\"diagnostics\":[]") )(
}, duration = 30.seconds,
"should send publishDiagnostics with empty diagnostics" message = "should send publishDiagnostics with empty diagnostics"
) )
// trigger no-op compilation // trigger no-op compilation
@ -199,13 +183,13 @@ object BuildServerTest extends AbstractServerTest {
compile(buildTarget) compile(buildTarget)
assert( assertMessage(
svr.waitForString(30.seconds) { s => "build/publishDiagnostics",
s.contains("build/publishDiagnostics") && "PatternMatch.scala",
s.contains("PatternMatch.scala") && """"message":"match may not be exhaustive"""
s.contains(""""message":"match may not be exhaustive""") )(
}, duration = 30.seconds,
"should send publishDiagnostics with type error for PatternMatch.scala" message = "should send publishDiagnostics with type error for PatternMatch.scala"
) )
IO.write( IO.write(
@ -223,15 +207,10 @@ object BuildServerTest extends AbstractServerTest {
reloadWorkspace() reloadWorkspace()
compile(buildTarget) compile(buildTarget)
assert( assertMessage("build/publishDiagnostics", "PatternMatch.scala", "\"diagnostics\":[]")(
svr.waitForString(30.seconds) { s => duration = 30.seconds,
s.contains("build/publishDiagnostics") && message = "should send publishDiagnostics with empty diagnostics"
s.contains("PatternMatch.scala") &&
s.contains("\"diagnostics\":[]")
},
"should send publishDiagnostics with empty diagnostics"
) )
} }
test("buildTarget/compile: Java diagnostics") { _ => test("buildTarget/compile: Java diagnostics") { _ =>
@ -239,42 +218,32 @@ object BuildServerTest extends AbstractServerTest {
compile(buildTarget) compile(buildTarget)
assert( assertMessage(
svr.waitForString(10.seconds) { s => "build/publishDiagnostics",
s.contains("build/publishDiagnostics") && "Hello.java",
s.contains("Hello.java") && """"severity":2""",
s.contains(""""severity":2""") && """missing type arguments for generic class java.util.List"""
s.contains("""missing type arguments for generic class java.util.List""") )(message = "should send publishDiagnostics with severity 2 for Hello.java")
},
"should send publishDiagnostics with severity 2 for Hello.java"
)
assert( assertMessage(
svr.waitForString(1.seconds) { s => "build/publishDiagnostics",
s.contains("build/publishDiagnostics") && "Hello.java",
s.contains("Hello.java") && """"severity":1""",
s.contains(""""severity":1""") && """incompatible types: int cannot be converted to java.lang.String"""
s.contains("""incompatible types: int cannot be converted to java.lang.String""") )(
}, message = "should send publishDiagnostics with severity 1 for Hello.java"
"should send publishDiagnostics with severity 1 for Hello.java"
) )
} }
test("buildTarget/scalacOptions, buildTarget/javacOptions") { _ => test("buildTarget/scalacOptions, buildTarget/javacOptions") { _ =>
val buildTarget = buildTargetUri("util", "Compile") val buildTarget = buildTargetUri("util", "Compile")
val badBuildTarget = buildTargetUri("badBuildTarget", "Compile") val badBuildTarget = buildTargetUri("badBuildTarget", "Compile")
val id1 = scalacOptions(Seq(buildTarget, badBuildTarget))
assert(svr.waitForString(10.seconds) { s => val id1 = scalacOptions(Seq(buildTarget, badBuildTarget))
(s contains s""""id":"$id1"""") && assertMessage(s""""id":"$id1"""", "scala-library-2.13.11.jar")()
(s contains "scala-library-2.13.11.jar")
})
val id2 = javacOptions(Seq(buildTarget, badBuildTarget)) val id2 = javacOptions(Seq(buildTarget, badBuildTarget))
assert(svr.waitForString(10.seconds) { s => assertMessage(s""""id":"$id2"""", "scala-library-2.13.11.jar")()
(s contains s""""id":"$id2"""") &&
(s contains "scala-library-2.13.11.jar")
})
} }
test("buildTarget/cleanCache") { _ => test("buildTarget/cleanCache") { _ =>
@ -328,10 +297,7 @@ object BuildServerTest extends AbstractServerTest {
s"""{ "jsonrpc": "2.0", "id": "$id", "method": "workspace/reload"}""" s"""{ "jsonrpc": "2.0", "id": "$id", "method": "workspace/reload"}"""
) )
assertProcessing("workspace/reload") assertProcessing("workspace/reload")
assert(svr.waitForString(10.seconds) { s => assertMessage(s""""id":"$id"""", """"result":null""")()
(s contains s""""id":"$id"""") &&
(s contains """"result":null""")
})
} }
test("workspace/reload: send diagnostic and respond with error") { _ => test("workspace/reload: send diagnostic and respond with error") { _ =>
@ -347,22 +313,19 @@ object BuildServerTest extends AbstractServerTest {
) )
val id = reloadWorkspace() val id = reloadWorkspace()
// reload // reload
assert( assertMessage(
svr.waitForString(10.seconds) { s => s""""buildTarget":{"uri":"$metaBuildTarget"}""",
s.contains(s""""buildTarget":{"uri":"$metaBuildTarget"}""") && s""""textDocument":{"uri":"${otherBuildFile.toPath.toUri}"}""",
s.contains(s""""textDocument":{"uri":"${otherBuildFile.toPath.toUri}"}""") && """"severity":1""",
s.contains(""""severity":1""") && """"reset":true"""
s.contains(""""reset":true""") )()
}
) assertMessage(
assert( s""""id":"$id"""",
svr.waitForString(10.seconds) { s => """"error"""",
s.contains(s""""id":"$id"""") && s""""code":${ErrorCodes.InternalError}""",
s.contains(""""error"""") && "Type error in expression"
s.contains(s""""code":${ErrorCodes.InternalError}""") && )()
s.contains("Type error in expression")
}
)
// fix the other-build.sbt file and reload again // fix the other-build.sbt file and reload again
IO.write( IO.write(
otherBuildFile, otherBuildFile,
@ -374,14 +337,12 @@ object BuildServerTest extends AbstractServerTest {
) )
reloadWorkspace() reloadWorkspace()
// assert received an empty diagnostic // assert received an empty diagnostic
assert( assertMessage(
svr.waitForString(10.seconds) { s => s""""buildTarget":{"uri":"$metaBuildTarget"}""",
s.contains(s""""buildTarget":{"uri":"$metaBuildTarget"}""") && s""""textDocument":{"uri":"${otherBuildFile.toPath.toUri}"}""",
s.contains(s""""textDocument":{"uri":"${otherBuildFile.toPath.toUri}"}""") && """"diagnostics":[]""",
s.contains(""""diagnostics":[]""") && """"reset":true"""
s.contains(""""reset":true""") )()
}
)
IO.delete(otherBuildFile) IO.delete(otherBuildFile)
} }
@ -395,10 +356,7 @@ object BuildServerTest extends AbstractServerTest {
|} }""".stripMargin |} }""".stripMargin
) )
assertProcessing("buildTarget/scalaMainClasses") assertProcessing("buildTarget/scalaMainClasses")
assert(svr.waitForString(30.seconds) { s => assertMessage(s""""id":"$id"""", """"class":"main.Main"""")(duration = 30.seconds)
(s contains s""""id":"$id"""") &&
(s contains """"class":"main.Main"""")
})
} }
test("buildTarget/run") { _ => test("buildTarget/run") { _ =>
@ -412,14 +370,8 @@ object BuildServerTest extends AbstractServerTest {
|} }""".stripMargin |} }""".stripMargin
) )
assertProcessing("buildTarget/run") assertProcessing("buildTarget/run")
assert(svr.waitForString(10.seconds) { s => assertMessage("build/logMessage", """"message":"Hello World!"""")()
(s contains "build/logMessage") && assertMessage(s""""id":"$id"""", """"statusCode":1""")()
(s contains """"message":"Hello World!"""")
})
assert(svr.waitForString(10.seconds) { s =>
(s contains s""""id":"$id"""") &&
(s contains """"statusCode":1""")
})
} }
test("buildTarget/jvmRunEnvironment") { _ => test("buildTarget/jvmRunEnvironment") { _ =>
@ -433,15 +385,13 @@ object BuildServerTest extends AbstractServerTest {
|}""".stripMargin |}""".stripMargin
) )
assertProcessing("buildTarget/jvmRunEnvironment") assertProcessing("buildTarget/jvmRunEnvironment")
assert { assertMessage(
svr.waitForString(10.seconds) { s => s""""id":"$id"""",
(s contains s""""id":"$id"""") && "jsoniter-scala-core_2.13-2.13.11.jar", // compile dependency
(s contains "jsoniter-scala-core_2.13-2.13.11.jar") && // compile dependency "\"jvmOptions\":[\"Xmx256M\"]",
(s contains "\"jvmOptions\":[\"Xmx256M\"]") && "\"environmentVariables\":{\"KEY\":\"VALUE\"}",
(s contains "\"environmentVariables\":{\"KEY\":\"VALUE\"}") && "/buildserver/run-and-test/" // working directory
(s contains "/buildserver/run-and-test/") // working directory )()
}
}
} }
test("buildTarget/jvmTestEnvironment") { _ => test("buildTarget/jvmTestEnvironment") { _ =>
@ -455,16 +405,13 @@ object BuildServerTest extends AbstractServerTest {
|}""".stripMargin |}""".stripMargin
) )
assertProcessing("buildTarget/jvmTestEnvironment") assertProcessing("buildTarget/jvmTestEnvironment")
assert { assertMessage(
svr.waitForString(10.seconds) { s => s""""id":"$id"""",
(s contains s""""id":"$id"""") && "jsoniter-scala-core_2.13-2.13.11.jar", // compile dependency
// test depends on compile so it has dependencies from both "scalatest_2.13-3.0.8.jar", // test dependency
(s contains "jsoniter-scala-core_2.13-2.13.11.jar") && // compile dependency "\"jvmOptions\":[\"Xmx512M\"]",
(s contains "scalatest_2.13-3.0.8.jar") && // test dependency "\"environmentVariables\":{\"KEY_TEST\":\"VALUE_TEST\"}"
(s contains "\"jvmOptions\":[\"Xmx512M\"]") && )()
(s contains "\"environmentVariables\":{\"KEY_TEST\":\"VALUE_TEST\"}")
}
}
} }
test("buildTarget/scalaTestClasses") { _ => test("buildTarget/scalaTestClasses") { _ =>
@ -477,12 +424,12 @@ object BuildServerTest extends AbstractServerTest {
|} }""".stripMargin |} }""".stripMargin
) )
assertProcessing("buildTarget/scalaTestClasses") assertProcessing("buildTarget/scalaTestClasses")
assert(svr.waitForString(10.seconds) { s => assertMessage(
(s contains s""""id":"$id"""") && s""""id":"$id"""",
(s contains """"tests.FailingTest"""") && """"tests.FailingTest"""",
(s contains """"tests.PassingTest"""") && """"tests.PassingTest"""",
(s contains """"framework":"ScalaTest"""") """"framework":"ScalaTest""""
}) )()
} }
test("buildTarget/test: run all tests") { _ => test("buildTarget/test: run all tests") { _ =>
@ -494,10 +441,7 @@ object BuildServerTest extends AbstractServerTest {
|} }""".stripMargin |} }""".stripMargin
) )
assertProcessing("buildTarget/test") assertProcessing("buildTarget/test")
assert(svr.waitForString(10.seconds) { s => assertMessage(s""""id":"$id"""", """"statusCode":2""")()
(s contains s""""id":"$id"""") &&
(s contains """"statusCode":2""")
})
} }
test("buildTarget/test: run one test class") { _ => test("buildTarget/test: run one test class") { _ =>
@ -518,41 +462,38 @@ object BuildServerTest extends AbstractServerTest {
|} }""".stripMargin |} }""".stripMargin
) )
assertProcessing("buildTarget/test") assertProcessing("buildTarget/test")
assert(svr.waitForString(10.seconds) { s => assertMessage(s""""id":"$id"""", """"statusCode":1""")()
(s contains s""""id":"$id"""") &&
(s contains """"statusCode":1""")
})
} }
test("buildTarget/compile: report error") { _ => test("buildTarget/compile: report error") { _ =>
val buildTarget = buildTargetUri("reportError", "Compile") val buildTarget = buildTargetUri("reportError", "Compile")
compile(buildTarget) compile(buildTarget)
assert(svr.waitForString(10.seconds) { s => assertMessage(
(s contains s""""buildTarget":{"uri":"$buildTarget"}""") && s""""buildTarget":{"uri":"$buildTarget"}""",
(s contains """"severity":1""") && """"severity":1""",
(s contains """"reset":true""") """"reset":true"""
}) )()
} }
test("buildTarget/compile: report warning") { _ => test("buildTarget/compile: report warning") { _ =>
val buildTarget = buildTargetUri("reportWarning", "Compile") val buildTarget = buildTargetUri("reportWarning", "Compile")
compile(buildTarget) compile(buildTarget)
assert(svr.waitForString(10.seconds) { s => assertMessage(
(s contains s""""buildTarget":{"uri":"$buildTarget"}""") && s""""buildTarget":{"uri":"$buildTarget"}""",
(s contains """"severity":2""") && """"severity":2""",
(s contains """"reset":true""") """"reset":true"""
}) )()
} }
test("buildTarget/compile: respond error") { _ => test("buildTarget/compile: respond error") { _ =>
val buildTarget = buildTargetUri("respondError", "Compile") val buildTarget = buildTargetUri("respondError", "Compile")
val id = compile(buildTarget) val id = compile(buildTarget)
assert(svr.waitForString(10.seconds) { s => assertMessage(
s.contains(s""""id":"$id"""") && s""""id":"$id"""",
s.contains(""""error"""") && """"error"""",
s.contains(s""""code":${ErrorCodes.InternalError}""") && s""""code":${ErrorCodes.InternalError}""",
s.contains("custom message") "custom message"
}) )()
} }
test("buildTarget/resources") { _ => test("buildTarget/resources") { _ =>
@ -565,9 +506,7 @@ object BuildServerTest extends AbstractServerTest {
|} }""".stripMargin |} }""".stripMargin
) )
assertProcessing("buildTarget/resources") assertProcessing("buildTarget/resources")
assert(svr.waitForString(10.seconds) { s => assertMessage(s""""id":"$id"""", "util/src/main/resources/")()
(s contains s""""id":"$id"""") && (s contains "util/src/main/resources/")
})
} }
test("buildTarget/outputPaths") { _ => test("buildTarget/outputPaths") { _ =>
@ -596,6 +535,47 @@ object BuildServerTest extends AbstractServerTest {
assert(actualResult == expectedResult) assert(actualResult == expectedResult)
} }
test("buildTarget/compile: twirl diagnostics (sourcePositionMappers)") { _ =>
val buildTarget = buildTargetUri("twirlProj", "Compile")
val testFile = new File(svr.baseDirectory, s"twirlProj/src/main/twirl/main.scala.html")
compile(buildTarget)
assertMessage(
"build/publishDiagnostics",
"main.scala.html",
""""severity":1""",
"not found: value tilte"
)(message = "should report diagnostic in Twirl file")
IO.write(
testFile,
"""|@(title: String, paragraphs: Seq[String])
|
|<!DOCTYPE HTML>
|<html lang="en">
| <head>
| <title>@title</title>
| </head>
| <body>
| <h1>@title</h1>
| @for(paragraph <- paragraphs) {
| <p>@paragraph</p>
| }
| </body>
|</html>
|""".stripMargin
)
compile(buildTarget)
assertMessage(
"build/publishDiagnostics",
"main.scala.html",
""""diagnostics":[]""",
""""reset":true"""
)(
duration = 30.seconds,
message = "should reset diagnostic in Twirl file"
)
}
private def initializeRequest(): Int = { private def initializeRequest(): Int = {
val params = InitializeBuildParams( val params = InitializeBuildParams(
"test client", "test client",
@ -608,11 +588,18 @@ object BuildServerTest extends AbstractServerTest {
sendRequest("build/initialize", params) sendRequest("build/initialize", params)
} }
private def assertProcessing(method: String, debug: Boolean = false): Unit = { private def assertProcessing(method: String, debug: Boolean = false): Unit =
assert(svr.waitForString(10.seconds) { msg => assertMessage("build/logMessage", s""""message":"Processing $method"""")(debug = debug)
if (debug) println(msg)
msg.contains("build/logMessage") && msg.contains(s""""message":"Processing $method"""") def assertMessage(
}) parts: String*
)(duration: FiniteDuration = 10.seconds, debug: Boolean = false, message: String = ""): Unit = {
def assertion =
svr.waitForString(duration) { msg =>
if (debug) println(msg)
parts.forall(msg.contains)
}
if (message.nonEmpty) assert.apply(assertion, message) else assert(assertion)
} }
private def reloadWorkspace(): Int = private def reloadWorkspace(): Int =