diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9a83196af..2031cb473d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,11 +84,9 @@ jobs: with: jvm: "zulu:17" - run: | - ./mill -i __.publishLocal &&\ ./mill -i integration.test.jvm if: runner.os != 'Windows' - run: | - @call ./mill.bat -i __.publishLocal @call ./mill.bat -i integration.test.jvm shell: cmd if: runner.os == 'Windows' @@ -127,11 +125,9 @@ jobs: if-no-files-found: error retention-days: 1 - run: | - ./mill -i __.publishLocal &&\ ./mill -i "integration.test.native" if: github.event_name == 'push' && runner.os != 'Windows' - run: | - ./mill.bat -i __.publishLocal ./mill.bat -i integration.test.native if: github.event_name == 'push' && runner.os == 'Windows' shell: bash diff --git a/build.sc b/build.sc index 21b745e904..10c67ea979 100644 --- a/build.sc +++ b/build.sc @@ -19,6 +19,8 @@ object Dependencies { def scalaVersions = Seq(scala212, scala213) + def serverScalaVersion = scala212 + def asmVersion = "9.6" def coursierVersion = "2.1.0-M6-53-gb4f448130" def graalvmVersion = "22.2.0" @@ -46,6 +48,7 @@ object Dependencies { ivy"com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core:$jsoniterVersion" def jsoniterMacros = ivy"com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:$jsoniterVersion" + def jsonrpc4s = ivy"io.github.alexarchambault.bleep::jsonrpc4s:0.1.1" def junit = ivy"com.github.sbt:junit-interface:0.13.3" def libdaemonjvm = ivy"io.github.alexarchambault.libdaemon::libdaemon:0.0.11" def libraryManagement = ivy"org.scala-sbt::librarymanagement-ivy:1.9.3" @@ -156,7 +159,11 @@ class Shared(val crossScalaVersion: String) extends BloopCrossSbtModule with Pub emptyZip() } def ivyDeps = super.ivyDeps() ++ Agg( - Dependencies.bsp4s.exclude(("com.github.plokhotnyuk.jsoniter-scala", "*")), + Dependencies.bsp4s + .exclude(("com.github.plokhotnyuk.jsoniter-scala", "*")) + .exclude(("me.vican.jorge", "jsonrpc4s_2.12")) + .exclude(("me.vican.jorge", "jsonrpc4s_2.13")), + Dependencies.jsonrpc4s, Dependencies.coursierInterface, Dependencies.jsoniterCore, Dependencies.log4j, @@ -532,8 +539,8 @@ object `bloop-rifle` extends BloopCliModule { | |/** Build-time constants. Generated by mill. */ |object Constants { - | def bloopVersion = "${frontend(Dependencies.scala212).publishVersion()}" - | def bloopScalaVersion = "${Dependencies.scala212}" + | def bloopVersion = "${frontend(Dependencies.serverScalaVersion).publishVersion()}" + | def bloopScalaVersion = "${Dependencies.serverScalaVersion}" | def bspVersion = "${Dependencies.bsp4j.dep.version}" |} |""".stripMargin @@ -607,6 +614,19 @@ object integration extends BloopCliModule { Dependencies.pprint ) + private def repoRoot = os.pwd / "out" / "repo" + def localRepo = T { + val modules = Seq( + frontend(Dependencies.serverScalaVersion), + backend(Dependencies.serverScalaVersion), + shared(Dependencies.serverScalaVersion) + ) + os.remove.all(repoRoot) + os.makeDir.all(repoRoot) + val tasks = modules.map(_.publishLocalNoFluff(repoRoot.toString)) + define.Target.sequence(tasks) + } + def forkEnv = super.forkEnv() ++ Seq( "BLOOP_CLI_TESTS_TMP_DIR" -> tmpDir() ) @@ -615,8 +635,10 @@ object integration extends BloopCliModule { def test(args: String*) = { val argsTask = T.task { val launcher = launcherTask().path + localRepo() val extraArgs = Seq( - s"-Dtest.bloop-cli.path=$launcher" + s"-Dtest.bloop-cli.path=$launcher", + s"-Dtest.bloop-cli.repo=$repoRoot" ) args ++ extraArgs } diff --git a/frontend/src/main/scala/bloop/bsp/BspServer.scala b/frontend/src/main/scala/bloop/bsp/BspServer.scala index 23fc09e29e..23bc6682c1 100644 --- a/frontend/src/main/scala/bloop/bsp/BspServer.scala +++ b/frontend/src/main/scala/bloop/bsp/BspServer.scala @@ -163,6 +163,8 @@ object BspServer { case scala.util.Success(socket: ServerSocket) => listenToConnection(handle, socket).onErrorRecover { case t => + System.err.println("Exiting BSP server with:") + t.printStackTrace(System.err) state.withError(s"Exiting BSP server with ${t.getMessage}", t) } case scala.util.Failure(t: Throwable) => diff --git a/integration/test/src/bloop/cli/integration/BloopCliTests.scala b/integration/test/src/bloop/cli/integration/BloopCliTests.scala index ef599a0bf5..8c3f8f08af 100644 --- a/integration/test/src/bloop/cli/integration/BloopCliTests.scala +++ b/integration/test/src/bloop/cli/integration/BloopCliTests.scala @@ -9,22 +9,21 @@ class BloopCliTests extends munit.FunSuite { val dirArgs = Seq[os.Shellable]("--daemon-dir", root / "daemon") os.proc(Launcher.launcher, "exit", dirArgs) - .call(cwd = root) + .call(cwd = root, stdin = os.Inherit, stdout = os.Inherit) val statusCheck0 = os.proc(Launcher.launcher, "status", dirArgs) .call(cwd = root) .out.trim() expect(statusCheck0 == "stopped") - val res = os.proc(Launcher.launcher, dirArgs, "about") - .call(cwd = root) - expect(res.out.text().startsWith("bloop v")) + os.proc(Launcher.launcher, dirArgs, "about") + .call(cwd = root, stdin = os.Inherit, stdout = os.Inherit, env = Launcher.extraEnv) val statusCheck1 = os.proc(Launcher.launcher, "status", dirArgs) .call(cwd = root) .out.trim() expect(statusCheck1 == "running") os.proc(Launcher.launcher, "exit", dirArgs) - .call(cwd = root) + .call(cwd = root, stdin = os.Inherit, stdout = os.Inherit) val statusCheck2 = os.proc(Launcher.launcher, "status", dirArgs) .call(cwd = root) .out.trim() diff --git a/integration/test/src/bloop/cli/integration/BspTests.scala b/integration/test/src/bloop/cli/integration/BspTests.scala new file mode 100644 index 0000000000..5f75c70a5c --- /dev/null +++ b/integration/test/src/bloop/cli/integration/BspTests.scala @@ -0,0 +1,134 @@ +package bloop.cli.integration + +import java.net.{StandardProtocolFamily, UnixDomainSocketAddress} +import java.nio.channels.SocketChannel +import java.nio.charset.StandardCharsets +import java.nio.ByteBuffer + +class BspTests extends munit.FunSuite { + + test("no JSON junk in errors") { + TmpDir.fromTmpDir { root => + val dirArgs = Seq[os.Shellable]("--daemon-dir", root / "daemon") + val bspFile = root / "bsp-socket" + + val dummyMsg = + """{ + | "jsonrpc": "2.0", + | "method": "workspace/buildTargetz", + | "params": null, + | "id": 2 + |}""".stripMargin + + var bspProc: os.SubProcess = null + var socket: SocketChannel = null + + try { + os.proc(Launcher.launcher, dirArgs, "about") + .call(cwd = root, stdin = os.Inherit, stdout = os.Inherit, env = Launcher.extraEnv) + + bspProc = + os.proc(Launcher.launcher, dirArgs, "bsp", "--protocol", "local", "--socket", bspFile) + .spawn(cwd = root, stdin = os.Inherit, stdout = os.Inherit) + + val addr = UnixDomainSocketAddress.of(bspFile.toNIO) + var connected = false + var attemptCount = 0 + + while (!connected && bspProc.isAlive() && attemptCount < 10) { + if (attemptCount > 0) + Thread.sleep(1000L) + attemptCount += 1 + + if (os.exists(bspFile)) { + socket = SocketChannel.open(StandardProtocolFamily.UNIX) + socket.connect(addr) + socket.finishConnect() + connected = true + } + } + + if (!connected) + sys.error("Not connected to Bloop server via BSP :|") + + def sendMsg(msg: String): Unit = { + val bytes = msg.getBytes(StandardCharsets.UTF_8) + + def doWrite(buf: ByteBuffer): Unit = + if (buf.position() < buf.limit()) { + val written = socket.write(buf) + if (written == 0) + Thread.sleep(100L) + doWrite(buf) + } + + doWrite(ByteBuffer.wrap( + (s"Content-Length: ${bytes.length}" + "\r\n\r\n").getBytes(StandardCharsets.UTF_8) + )) + doWrite(ByteBuffer.wrap(bytes)) + } + + sendMsg(dummyMsg) + + val arr = Array.ofDim[Byte](10 * 1024) + val buf = ByteBuffer.wrap(arr) + + // seems to do the job… + def doRead(buf: ByteBuffer, count: Int): Int = { + val read = socket.read(buf) + if (read <= 0 && count < 100) { + Thread.sleep(100L) + doRead(buf, count + 1) + } + else + read + } + + val read = doRead(buf, 0) + assert(read > 0) + + val resp = new String(arr, 0, read, StandardCharsets.UTF_8) + + def validateJson(content: String): Boolean = { + assert(content.startsWith("{")) + assert(content.endsWith("}")) + val chars = content.toCharArray + val objCount = Array.ofDim[Int](chars.length) + for (i <- 0 until chars.length) { + val previous = if (i == 0) 0 else objCount(i - 1) + val count = previous + (if (chars(i) == '{') 1 else if (chars(i) == '}') -1 else 0) + objCount(i) = count + } + objCount.dropRight(1).forall(_ > 0) + } + + resp.linesWithSeparators.toVector match { + case Seq(cl, empty, other @ _*) + if cl.startsWith("Content-Length:") && empty.trim.isEmpty => + val json = other.mkString + val validated = validateJson(json) + if (!validated) + pprint.err.log(json) + assert(validated, "Unexpected JSON response shape") + case _ => + pprint.err.log(resp) + sys.error("Unexpected response shape") + } + } + finally { + if (socket != null) + socket.close() + + if (bspProc != null) { + bspProc.waitFor(1000L) + if (bspProc.isAlive()) + bspProc.close() + + os.proc(Launcher.launcher, dirArgs, "exit") + .call(cwd = root, stdin = os.Inherit, stdout = os.Inherit, check = false) + } + } + } + } + +} diff --git a/integration/test/src/bloop/cli/integration/Launcher.scala b/integration/test/src/bloop/cli/integration/Launcher.scala index c2b824c4bd..436c15dd41 100644 --- a/integration/test/src/bloop/cli/integration/Launcher.scala +++ b/integration/test/src/bloop/cli/integration/Launcher.scala @@ -7,4 +7,14 @@ object Launcher { sys.error("test.bloop-cli.path not set") ) + lazy val repoRoot = os.Path(sys.props.getOrElse( + "test.bloop-cli.repo", + sys.error("test.bloop-cli.repo not set") + )) + + lazy val extraEnv = + Map( + "COURSIER_REPOSITORIES" -> s"ivy:${repoRoot.toNIO.toUri.toASCIIString}/[defaultPattern]|ivy2Local|central" + ) + }