From b82a1870a7783b57a75d79c6162120c05cb38c54 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sat, 10 Mar 2018 16:50:03 -0500 Subject: [PATCH 01/26] add tests around scope delegation --- main/src/test/scala/Delegates.scala | 51 +++++++++++++++- .../test/scala/sbt/internal/TestBuild.scala | 59 ++++++++++--------- 2 files changed, 81 insertions(+), 29 deletions(-) diff --git a/main/src/test/scala/Delegates.scala b/main/src/test/scala/Delegates.scala index 492cdcdd9..11a511ad4 100644 --- a/main/src/test/scala/Delegates.scala +++ b/main/src/test/scala/Delegates.scala @@ -16,10 +16,10 @@ import Prop._ import Gen._ object Delegates extends Properties("delegates") { - property("generate non-empty configs") = forAll { (c: Seq[Configuration]) => + property("generate non-empty configs") = forAll { (c: Vector[Configuration]) => c.nonEmpty } - property("generate non-empty tasks") = forAll { (t: Seq[Taskk]) => + property("generate non-empty tasks") = forAll { (t: Vector[Taskk]) => t.nonEmpty } @@ -46,6 +46,7 @@ object Delegates extends Properties("delegates") { global forall { _ == Zero } } } + property("Initial scope present with all combinations of Global axes") = allAxes( globalCombinations) @@ -54,12 +55,58 @@ object Delegates extends Properties("delegates") { ds.head == scope } } + property("global scope last") = forAll { (keys: Keys) => allDelegates(keys) { (_, ds) => ds.last == Scope.GlobalScope } } + property("Project axis delegates to BuildRef then Zero") = forAll { (keys: Keys) => + allDelegates(keys) { (key, ds) => + key.project match { + case Zero => true // filtering out of testing + case Select(ProjectRef(uri, _)) => + val buildScoped = key.copy(project = Select(BuildRef(uri))) + val idxKey = ds.indexOf(key) + val idxB = ds.indexOf(buildScoped) + val z = key.copy(project = Zero) + val idxZ = ds.indexOf(z) + if (z == Scope.GlobalScope) true + else { + (s"idxKey = $idxKey; idxB = $idxB; idxZ = $idxZ") |: + (idxKey < idxB) && (idxB < idxZ) + } + case Select(BuildRef(_)) => + ds.indexOf(key) < ds.indexOf(key.copy(project = Zero)) + } + } + } + + property("Config axis delegates to parent configuration") = forAll { (keys: Keys) => + allDelegates(keys) { (key, ds) => + key.config match { + case Zero => true + case Select(config) if key.project.isSelect => + val p = key.project.toOption.get + val r = keys.env.resolve(p) + val proj = keys.env.projectFor(r) + val inh: Vector[ConfigKey] = keys.env.inheritConfig(r, config) + val conf = proj.confMap(config.name) + if (inh.isEmpty) true + else { + val idxKey = ds.indexOf(key) + val parent = inh.head + val a = key.copy(config = Select(parent)) + val idxA = ds.indexOf(a) + (s"idxKey = $idxKey; a = $a; idxA = $idxA") |: + idxKey < idxA + } + case _ => true + } + } + } + def allAxes(f: (Scope, Seq[Scope], Scope => ScopeAxis[_]) => Prop): Prop = forAll { (keys: Keys) => allDelegates(keys) { (s, ds) => diff --git a/main/src/test/scala/sbt/internal/TestBuild.scala b/main/src/test/scala/sbt/internal/TestBuild.scala index 7b8a70db5..7b53b1c7c 100644 --- a/main/src/test/scala/sbt/internal/TestBuild.scala +++ b/main/src/test/scala/sbt/internal/TestBuild.scala @@ -113,7 +113,7 @@ abstract class TestBuild { (taskAxes, zero.toSet, single.toSet, multi.toSet) } } - final class Env(val builds: Seq[Build], val tasks: Seq[Taskk]) { + final class Env(val builds: Vector[Build], val tasks: Vector[Taskk]) { override def toString = "Env:\n " + " Tasks:\n " + tasks.mkString("\n ") + "\n" + builds.mkString("\n ") val root = builds.head @@ -129,20 +129,21 @@ abstract class TestBuild { def inheritConfig(ref: ResolvedReference, config: ConfigKey) = projectFor(ref).confMap(config.name).extendsConfigs map toConfigKey def inheritTask(task: AttributeKey[_]) = taskMap.get(task) match { - case None => Nil; case Some(t) => t.delegates map getKey + case None => Vector() + case Some(t) => t.delegates.toVector map getKey } - def inheritProject(ref: ProjectRef) = project(ref).delegates + def inheritProject(ref: ProjectRef) = project(ref).delegates.toVector def resolve(ref: Reference) = Scope.resolveReference(root.uri, rootProject, ref) lazy val delegates: Scope => Seq[Scope] = Scope.delegates( allProjects, - (_: Proj).configurations.map(toConfigKey), + (_: Proj).configurations.toVector.map(toConfigKey), resolve, uri => buildMap(uri).root.id, inheritProject, inheritConfig, inheritTask, - (ref, mp) => Nil + (ref, mp) => Vector() ) lazy val allFullScopes: Seq[Scope] = for { @@ -262,13 +263,14 @@ abstract class TestBuild { implicit lazy val uriGen: Gen[URI] = for (sch <- idGen; ssp <- idGen; frag <- optIDGen) yield new URI(sch, ssp, frag.orNull) - implicit def envGen(implicit bGen: Gen[Build], tasks: Gen[Seq[Taskk]]): Gen[Env] = - for (i <- MaxBuildsGen; bs <- listOfN(i, bGen); ts <- tasks) yield new Env(bs, ts) + implicit def envGen(implicit bGen: Gen[Build], tasks: Gen[Vector[Taskk]]): Gen[Env] = + for (i <- MaxBuildsGen; bs <- containerOfN[Vector, Build](i, bGen); ts <- tasks) + yield new Env(bs, ts) implicit def buildGen(implicit uGen: Gen[URI], pGen: URI => Gen[Seq[Proj]]): Gen[Build] = for (u <- uGen; ps <- pGen(u)) yield new Build(u, ps) - def nGen[T](igen: Gen[Int])(implicit g: Gen[T]): Gen[List[T]] = igen flatMap { ig => - listOfN(ig, g) + def nGen[T](igen: Gen[Int])(implicit g: Gen[T]): Gen[Vector[T]] = igen flatMap { ig => + containerOfN[Vector, T](ig, g) } implicit def genProjects(build: URI)(implicit genID: Gen[String], @@ -285,34 +287,37 @@ abstract class TestBuild { def genConfigs(implicit genName: Gen[String], maxDeps: Gen[Int], - count: Gen[Int]): Gen[Seq[Configuration]] = + count: Gen[Int]): Gen[Vector[Configuration]] = genAcyclicDirect[Configuration, String](maxDeps, genName, count)( (key, deps) => Configuration .of(key.capitalize, key) .withExtendsConfigs(deps.toVector)) - def genTasks(implicit genName: Gen[String], maxDeps: Gen[Int], count: Gen[Int]): Gen[Seq[Taskk]] = + def genTasks(implicit genName: Gen[String], + maxDeps: Gen[Int], + count: Gen[Int]): Gen[Vector[Taskk]] = genAcyclicDirect[Taskk, String](maxDeps, genName, count)((key, deps) => new Taskk(AttributeKey[String](key), deps)) def genAcyclicDirect[A, T](maxDeps: Gen[Int], keyGen: Gen[T], max: Gen[Int])( - make: (T, Seq[A]) => A): Gen[Seq[A]] = + make: (T, Vector[A]) => A): Gen[Vector[A]] = genAcyclic[A, T](maxDeps, keyGen, max) { t => Gen.const { deps => - make(t, deps) + make(t, deps.toVector) } } def genAcyclic[A, T](maxDeps: Gen[Int], keyGen: Gen[T], max: Gen[Int])( - make: T => Gen[Seq[A] => A]): Gen[Seq[A]] = + make: T => Gen[Vector[A] => A]): Gen[Vector[A]] = max flatMap { count => - listOfN(count, keyGen) flatMap { keys => + containerOfN[Vector, T](count, keyGen) flatMap { keys => genAcyclic(maxDeps, keys.distinct)(make) } } - def genAcyclic[A, T](maxDeps: Gen[Int], keys: List[T])(make: T => Gen[Seq[A] => A]): Gen[Seq[A]] = - genAcyclic(maxDeps, keys, Nil) flatMap { pairs => + def genAcyclic[A, T](maxDeps: Gen[Int], keys: Vector[T])( + make: T => Gen[Vector[A] => A]): Gen[Vector[A]] = + genAcyclic(maxDeps, keys, Vector()) flatMap { pairs => sequence(pairs.map { case (key, deps) => mapMake(key, deps, make) }) flatMap { inputs => val made = new collection.mutable.HashMap[T, A] for ((key, deps, mk) <- inputs) @@ -321,25 +326,25 @@ abstract class TestBuild { } } - def mapMake[A, T](key: T, deps: Seq[T], make: T => Gen[Seq[A] => A]): Gen[Inputs[A, T]] = - make(key) map { (mk: Seq[A] => A) => + def mapMake[A, T](key: T, deps: Vector[T], make: T => Gen[Vector[A] => A]): Gen[Inputs[A, T]] = + make(key) map { (mk: Vector[A] => A) => (key, deps, mk) } def genAcyclic[T](maxDeps: Gen[Int], - names: List[T], - acc: List[Gen[(T, Seq[T])]]): Gen[Seq[(T, Seq[T])]] = + names: Vector[T], + acc: Vector[Gen[(T, Vector[T])]]): Gen[Vector[(T, Vector[T])]] = names match { - case Nil => sequence(acc) - case x :: xs => + case Vector() => sequence(acc) + case Vector(x, xs @ _*) => val next = for (depCount <- maxDeps; d <- pick(depCount min xs.size, xs)) - yield (x, d.toList) - genAcyclic(maxDeps, xs, next :: acc) + yield (x, d.toVector) + genAcyclic(maxDeps, xs.toVector, next +: acc) } - def sequence[T](gs: Seq[Gen[T]]): Gen[Seq[T]] = Gen.parameterized { prms => + def sequence[T](gs: Vector[Gen[T]]): Gen[Vector[T]] = Gen.parameterized { prms => delay(gs map { g => g(prms, seed) getOrElse sys.error("failed generator") }) } - type Inputs[A, T] = (T, Seq[T], Seq[A] => A) + type Inputs[A, T] = (T, Vector[T], Vector[A] => A) } From 67e1e4a9ffb5a2faa47f8d4696204760a83d584c Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sat, 24 Mar 2018 02:07:28 +0900 Subject: [PATCH 02/26] improve server testing previously I was using separate thread in a forked test to test the server, but that is not enough isolation to run multiple server tests. This adds `RunFromSourceMain.fork(workingDirectory: File)`, which allows us to run a fresh sbt on the given working directory. Next, I've refactored the stateful client-side buffer to a class `TestServer`. --- build.sbt | 6 +- .../src/test/scala/sbt/std/TestUtil.scala | 2 +- .../test/scala/sbt/RunFromSourceMain.scala | 21 +- sbt/src/test/scala/sbt/ServerSpec.scala | 193 ------------------ sbt/src/test/scala/testpkg/ServerSpec.scala | 184 +++++++++++++++++ 5 files changed, 207 insertions(+), 199 deletions(-) delete mode 100644 sbt/src/test/scala/sbt/ServerSpec.scala create mode 100644 sbt/src/test/scala/testpkg/ServerSpec.scala diff --git a/build.sbt b/build.sbt index 5e50c04aa..41723a157 100644 --- a/build.sbt +++ b/build.sbt @@ -497,7 +497,6 @@ lazy val sbtProj = (project in file("sbt")) normalizedName := "sbt", crossScalaVersions := Seq(baseScalaVersion), crossPaths := false, - javaOptions ++= Seq("-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005"), mimaSettings, mimaBinaryIssueFilters ++= sbtIgnoredProblems, BuildInfoPlugin.buildInfoDefaultSettings, @@ -506,10 +505,9 @@ lazy val sbtProj = (project in file("sbt")) buildInfoKeys in Test := Seq[BuildInfoKey]( // WORKAROUND https://github.com/sbt/sbt-buildinfo/issues/117 BuildInfoKey.map((fullClasspath in Compile).taskValue) { case (ident, cp) => ident -> cp.files }, + classDirectory in Compile, + classDirectory in Test, ), - connectInput in run in Test := true, - outputStrategy in run in Test := Some(StdoutOutput), - fork in Test := true, ) .configure(addSbtCompilerBridge) diff --git a/main-settings/src/test/scala/sbt/std/TestUtil.scala b/main-settings/src/test/scala/sbt/std/TestUtil.scala index df02fb29c..dc0098f19 100644 --- a/main-settings/src/test/scala/sbt/std/TestUtil.scala +++ b/main-settings/src/test/scala/sbt/std/TestUtil.scala @@ -27,6 +27,6 @@ object TestUtil { val mainClassesDir = buildinfo.TestBuildInfo.classDirectory val testClassesDir = buildinfo.TestBuildInfo.test_classDirectory val depsClasspath = buildinfo.TestBuildInfo.dependencyClasspath - mainClassesDir +: testClassesDir +: depsClasspath mkString ":" + mainClassesDir +: testClassesDir +: depsClasspath mkString java.io.File.pathSeparator } } diff --git a/sbt/src/test/scala/sbt/RunFromSourceMain.scala b/sbt/src/test/scala/sbt/RunFromSourceMain.scala index 6816e24c7..2f4e81a51 100644 --- a/sbt/src/test/scala/sbt/RunFromSourceMain.scala +++ b/sbt/src/test/scala/sbt/RunFromSourceMain.scala @@ -7,14 +7,33 @@ package sbt +import scala.concurrent.Future +import sbt.util.LogExchange import scala.annotation.tailrec - +import buildinfo.TestBuildInfo import xsbti._ object RunFromSourceMain { private val sbtVersion = "1.0.3" // "dev" private val scalaVersion = "2.12.4" + def fork(workingDirectory: File): Future[Unit] = { + val fo = ForkOptions() + .withWorkingDirectory(workingDirectory) + .withOutputStrategy(OutputStrategy.StdoutOutput) + implicit val runner = new ForkRun(fo) + val cp = { + TestBuildInfo.test_classDirectory +: TestBuildInfo.fullClasspath + } + val options = Vector(workingDirectory.toString) + val log = LogExchange.logger("RunFromSourceMain.fork", None, None) + import scala.concurrent.ExecutionContext.Implicits.global + Future { + Run.run("sbt.RunFromSourceMain", cp, options, log) + () + } + } + def main(args: Array[String]): Unit = args match { case Array() => sys.error(s"Must specify working directory as the first argument") case Array(wd, args @ _*) => run(file(wd), args) diff --git a/sbt/src/test/scala/sbt/ServerSpec.scala b/sbt/src/test/scala/sbt/ServerSpec.scala deleted file mode 100644 index a328c6dfe..000000000 --- a/sbt/src/test/scala/sbt/ServerSpec.scala +++ /dev/null @@ -1,193 +0,0 @@ -/* - * sbt - * Copyright 2011 - 2017, Lightbend, Inc. - * Copyright 2008 - 2010, Mark Harrah - * Licensed under BSD-3-Clause license (see LICENSE) - */ - -package sbt - -import org.scalatest._ -import scala.concurrent._ -import scala.annotation.tailrec -import java.io.{ InputStream, OutputStream } -import java.util.concurrent.atomic.AtomicInteger -import java.util.concurrent.{ ThreadFactory, ThreadPoolExecutor } -import sbt.protocol.ClientSocket - -class ServerSpec extends AsyncFlatSpec with Matchers { - import ServerSpec._ - - "server" should "start" in { - withBuildSocket("handshake") { (out, in, tkn) => - writeLine( - """{ "jsonrpc": "2.0", "id": 3, "method": "sbt/setting", "params": { "setting": "root/name" } }""", - out) - Thread.sleep(100) - assert(waitFor(in, 10) { s => - s contains """"id":3""" - }) - } - } - - @tailrec - private[this] def waitFor(in: InputStream, num: Int)(f: String => Boolean): Boolean = { - if (num < 0) false - else - readFrame(in) match { - case Some(x) if f(x) => true - case _ => - waitFor(in, num - 1)(f) - } - } -} - -object ServerSpec { - private val serverTestBase: File = new File(".").getAbsoluteFile / "sbt" / "src" / "server-test" - private val nextThreadId = new AtomicInteger(1) - private val threadGroup = Thread.currentThread.getThreadGroup() - val readBuffer = new Array[Byte](4096) - var buffer: Vector[Byte] = Vector.empty - var bytesRead = 0 - private val delimiter: Byte = '\n'.toByte - private val RetByte = '\r'.toByte - - private val threadFactory = new ThreadFactory() { - override def newThread(runnable: Runnable): Thread = { - val thread = - new Thread(threadGroup, - runnable, - s"sbt-test-server-threads-${nextThreadId.getAndIncrement}") - // Do NOT setDaemon because then the code in TaskExit.scala in sbt will insta-kill - // the backgrounded process, at least for the case of the run task. - thread - } - } - - private val executor = new ThreadPoolExecutor( - 0, /* corePoolSize */ - 1, /* maxPoolSize, max # of servers */ - 2, - java.util.concurrent.TimeUnit.SECONDS, - /* keep alive unused threads this long (if corePoolSize < maxPoolSize) */ - new java.util.concurrent.SynchronousQueue[Runnable](), - threadFactory - ) - - def backgroundRun(baseDir: File, args: Seq[String]): Unit = { - executor.execute(new Runnable { - def run(): Unit = { - RunFromSourceMain.run(baseDir, args) - } - }) - } - - def shutdown(): Unit = executor.shutdown() - - def withBuildSocket(testBuild: String)( - f: (OutputStream, InputStream, Option[String]) => Future[Assertion]): Future[Assertion] = { - IO.withTemporaryDirectory { temp => - IO.copyDirectory(serverTestBase / testBuild, temp / testBuild) - withBuildSocket(temp / testBuild)(f) - } - } - - def sendJsonRpc(message: String, out: OutputStream): Unit = { - writeLine(s"""Content-Length: ${message.size + 2}""", out) - writeLine("", out) - writeLine(message, out) - } - - def readFrame(in: InputStream): Option[String] = { - val l = contentLength(in) - readLine(in) - readLine(in) - readContentLength(in, l) - } - - def contentLength(in: InputStream): Int = { - readLine(in) map { line => - line.drop(16).toInt - } getOrElse (0) - } - - def readLine(in: InputStream): Option[String] = { - if (buffer.isEmpty) { - val bytesRead = in.read(readBuffer) - if (bytesRead > 0) { - buffer = buffer ++ readBuffer.toVector.take(bytesRead) - } - } - val delimPos = buffer.indexOf(delimiter) - if (delimPos > 0) { - val chunk0 = buffer.take(delimPos) - buffer = buffer.drop(delimPos + 1) - // remove \r at the end of line. - val chunk1 = if (chunk0.lastOption contains RetByte) chunk0.dropRight(1) else chunk0 - Some(new String(chunk1.toArray, "utf-8")) - } else None // no EOL yet, so skip this turn. - } - - def readContentLength(in: InputStream, length: Int): Option[String] = { - if (buffer.isEmpty) { - val bytesRead = in.read(readBuffer) - if (bytesRead > 0) { - buffer = buffer ++ readBuffer.toVector.take(bytesRead) - } - } - if (length <= buffer.size) { - val chunk = buffer.take(length) - buffer = buffer.drop(length) - Some(new String(chunk.toArray, "utf-8")) - } else None // have not read enough yet, so skip this turn. - } - - def writeLine(s: String, out: OutputStream): Unit = { - def writeEndLine(): Unit = { - val retByte: Byte = '\r'.toByte - val delimiter: Byte = '\n'.toByte - out.write(retByte.toInt) - out.write(delimiter.toInt) - out.flush - } - - if (s != "") { - out.write(s.getBytes("UTF-8")) - } - writeEndLine - } - - def withBuildSocket(baseDirectory: File)( - f: (OutputStream, InputStream, Option[String]) => Future[Assertion]): Future[Assertion] = { - backgroundRun(baseDirectory, Nil) - - val portfile = baseDirectory / "project" / "target" / "active.json" - - def waitForPortfile(n: Int): Unit = - if (portfile.exists) () - else { - if (n <= 0) sys.error(s"Timeout. $portfile is not found.") - else { - Thread.sleep(1000) - waitForPortfile(n - 1) - } - } - waitForPortfile(10) - val (sk, tkn) = ClientSocket.socket(portfile) - val out = sk.getOutputStream - val in = sk.getInputStream - - sendJsonRpc( - """{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "initializationOptions": { } } }""", - out) - - try { - f(out, in, tkn) - } finally { - sendJsonRpc( - """{ "jsonrpc": "2.0", "id": 9, "method": "sbt/exec", "params": { "commandLine": "exit" } }""", - out) - shutdown() - } - } -} diff --git a/sbt/src/test/scala/testpkg/ServerSpec.scala b/sbt/src/test/scala/testpkg/ServerSpec.scala new file mode 100644 index 000000000..f34eee6b1 --- /dev/null +++ b/sbt/src/test/scala/testpkg/ServerSpec.scala @@ -0,0 +1,184 @@ +/* + * sbt + * Copyright 2011 - 2017, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under BSD-3-Clause license (see LICENSE) + */ + +package testpkg + +import org.scalatest._ +import scala.concurrent._ +import scala.annotation.tailrec +import sbt.protocol.ClientSocket +import TestServer.withTestServer +import java.io.File +import sbt.io.syntax._ +import sbt.io.IO +import sbt.RunFromSourceMain + +class ServerSpec extends AsyncFreeSpec with Matchers { + "server" - { + "should start" in withTestServer("handshake") { p => + p.writeLine( + """{ "jsonrpc": "2.0", "id": "3", "method": "sbt/setting", "params": { "setting": "root/name" } }""") + assert(p.waitForString(10) { s => + s contains """"id":"3"""" + }) + } + + "return number id when number id is sent" in withTestServer("handshake") { p => + p.writeLine( + """{ "jsonrpc": "2.0", "id": 3, "method": "sbt/setting", "params": { "setting": "root/name" } }""") + assert(p.waitForString(10) { s => + s contains """"id":3""" + }) + } + } +} + +object TestServer { + private val serverTestBase: File = new File(".").getAbsoluteFile / "sbt" / "src" / "server-test" + + def withTestServer(testBuild: String)(f: TestServer => Future[Assertion]): Future[Assertion] = { + IO.withTemporaryDirectory { temp => + IO.copyDirectory(serverTestBase / testBuild, temp / testBuild) + withTestServer(temp / testBuild)(f) + } + } + + def withTestServer(baseDirectory: File)(f: TestServer => Future[Assertion]): Future[Assertion] = { + val testServer = TestServer(baseDirectory) + try { + f(testServer) + } finally { + testServer.bye() + } + } + + def hostLog(s: String): Unit = { + println(s"""[${scala.Console.MAGENTA}build-1${scala.Console.RESET}] $s""") + } +} + +case class TestServer(baseDirectory: File) { + import TestServer.hostLog + + val readBuffer = new Array[Byte](4096) + var buffer: Vector[Byte] = Vector.empty + var bytesRead = 0 + private val delimiter: Byte = '\n'.toByte + private val RetByte = '\r'.toByte + + hostLog("fork to a new sbt instance") + RunFromSourceMain.fork(baseDirectory) + lazy val portfile = baseDirectory / "project" / "target" / "active.json" + + hostLog("wait 30s until the server is ready to respond") + def waitForPortfile(n: Int): Unit = + if (portfile.exists) () + else { + if (n <= 0) sys.error(s"Timeout. $portfile is not found.") + else { + Thread.sleep(1000) + waitForPortfile(n - 1) + } + } + waitForPortfile(30) + + // make connection to the socket described in the portfile + val (sk, tkn) = ClientSocket.socket(portfile) + val out = sk.getOutputStream + val in = sk.getInputStream + + // initiate handshake + sendJsonRpc( + """{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "initializationOptions": { } } }""") + + def test(f: TestServer => Future[Assertion]): Future[Assertion] = { + f(this) + } + + def bye(): Unit = { + hostLog("sending exit") + sendJsonRpc( + """{ "jsonrpc": "2.0", "id": 9, "method": "sbt/exec", "params": { "commandLine": "exit" } }""") + } + + def sendJsonRpc(message: String): Unit = { + writeLine(s"""Content-Length: ${message.size + 2}""") + writeLine("") + writeLine(message) + } + + def writeLine(s: String): Unit = { + def writeEndLine(): Unit = { + val retByte: Byte = '\r'.toByte + val delimiter: Byte = '\n'.toByte + out.write(retByte.toInt) + out.write(delimiter.toInt) + out.flush + } + + if (s != "") { + out.write(s.getBytes("UTF-8")) + } + writeEndLine + } + + def readFrame: Option[String] = { + def getContentLength: Int = { + readLine map { line => + line.drop(16).toInt + } getOrElse (0) + } + + val l = getContentLength + readLine + readLine + readContentLength(l) + } + + @tailrec + final def waitForString(num: Int)(f: String => Boolean): Boolean = { + if (num < 0) false + else + readFrame match { + case Some(x) if f(x) => true + case _ => + waitForString(num - 1)(f) + } + } + + def readLine: Option[String] = { + if (buffer.isEmpty) { + val bytesRead = in.read(readBuffer) + if (bytesRead > 0) { + buffer = buffer ++ readBuffer.toVector.take(bytesRead) + } + } + val delimPos = buffer.indexOf(delimiter) + if (delimPos > 0) { + val chunk0 = buffer.take(delimPos) + buffer = buffer.drop(delimPos + 1) + // remove \r at the end of line. + val chunk1 = if (chunk0.lastOption contains RetByte) chunk0.dropRight(1) else chunk0 + Some(new String(chunk1.toArray, "utf-8")) + } else None // no EOL yet, so skip this turn. + } + + def readContentLength(length: Int): Option[String] = { + if (buffer.isEmpty) { + val bytesRead = in.read(readBuffer) + if (bytesRead > 0) { + buffer = buffer ++ readBuffer.toVector.take(bytesRead) + } + } + if (length <= buffer.size) { + val chunk = buffer.take(length) + buffer = buffer.drop(length) + Some(new String(chunk.toArray, "utf-8")) + } else None // have not read enough yet, so skip this turn. + } + +} From fff32db7ceca4f933dc9a903da76ede4598a468e Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Wed, 28 Mar 2018 17:09:12 -0700 Subject: [PATCH 03/26] Use MacOSXWatchService instead of PollingWatchService This watch service should be more responsive and significantly reduce the disk overhead of the polling based service for large repos. --- main-command/src/main/scala/sbt/Watched.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main-command/src/main/scala/sbt/Watched.scala b/main-command/src/main/scala/sbt/Watched.scala index 8b8385b61..1c2396593 100644 --- a/main-command/src/main/scala/sbt/Watched.scala +++ b/main-command/src/main/scala/sbt/Watched.scala @@ -141,7 +141,7 @@ object Watched { FileSystems.getDefault.newWatchService() case _ if Properties.isMac => // WatchService is slow on macOS - use old polling mode - new PollingWatchService(PollDelay) + new MacOSXWatchService(PollDelay) case _ => FileSystems.getDefault.newWatchService() } From 88f50ce35d006dad6b810c801d10fdb92af3943a Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Fri, 30 Mar 2018 23:09:01 -0400 Subject: [PATCH 04/26] Fix console, JLine issue Fixes #3482 --- main-actions/src/main/scala/sbt/Console.scala | 7 +++---- main/src/main/scala/sbt/Defaults.scala | 6 ++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/main-actions/src/main/scala/sbt/Console.scala b/main-actions/src/main/scala/sbt/Console.scala index c2792d595..d94484030 100644 --- a/main-actions/src/main/scala/sbt/Console.scala +++ b/main-actions/src/main/scala/sbt/Console.scala @@ -9,8 +9,8 @@ package sbt import java.io.File import sbt.internal.inc.AnalyzingCompiler +import sbt.internal.util.JLine import sbt.util.Logger - import xsbti.compile.{ Inputs, Compilers } import scala.util.Try @@ -41,11 +41,10 @@ final class Console(compiler: AnalyzingCompiler) { implicit log: Logger): Try[Unit] = { def console0() = compiler.console(classpath, options, initialCommands, cleanupCommands, log)(loader, bindings) - // TODO: Fix JLine - //JLine.withJLine(Run.executeTrapExit(console0, log)) - Run.executeTrapExit(console0, log) + JLine.withJLine(Run.executeTrapExit(console0, log)) } } + object Console { def apply(conf: Inputs): Console = conf.compilers match { diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 3ae24adde..b3fb287e0 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -1373,10 +1373,8 @@ object Defaults extends BuildCommon { val sc = (scalacOptions in task).value val ic = (initialCommands in task).value val cc = (cleanupCommands in task).value - JLine.usingTerminal { _ => - (new Console(compiler))(cpFiles, sc, loader, ic, cc)()(s.log).get - println() - } + (new Console(compiler))(cpFiles, sc, loader, ic, cc)()(s.log).get + println() } private[this] def exported(w: PrintWriter, command: String): Seq[String] => Unit = From 0b801e7598f9cd2076155f7c90c99bf87de6c38e Mon Sep 17 00:00:00 2001 From: Jason Zaugg Date: Tue, 3 Apr 2018 14:25:59 +1000 Subject: [PATCH 05/26] Bump Scala version to 2.12.5 --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 94a583ea8..d7272d442 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -4,7 +4,7 @@ import sbt.contraband.ContrabandPlugin.autoImport._ object Dependencies { // WARNING: Please Scala update versions in PluginCross.scala too - val scala212 = "2.12.4" + val scala212 = "2.12.5" val baseScalaVersion = scala212 // sbt modules From 70d3484896345698d5d142578e451bdb6dcd388a Mon Sep 17 00:00:00 2001 From: Jason Zaugg Date: Tue, 3 Apr 2018 14:27:21 +1000 Subject: [PATCH 06/26] Avoid printing RejectedExectionExeption stack trace after cancellation Before: ``` [warn] /Users/jz/code/zinc/internal/zinc-apiinfo/src/main/scala/xsbt/api/Visit.scala:187:19: parameter value s in method visitString is never used [warn] def visitString(s: String): Unit = () [warn] ^ ^C [warn] Canceling execution... [warn] 10 warnings found [info] Compilation has been cancelled [error] java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.ExecutorCompletionService$QueueingFuture@33ec70dd rejected from java.util.concurrent.ThreadPoolExecutor@4b832de6[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 1410] [error] at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063) [error] at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830) [error] at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379) [error] at java.util.concurrent.ExecutorCompletionService.submit(ExecutorCompletionService.java:181) [error] at sbt.CompletionService$.submit(CompletionService.scala:36) [error] at sbt.ConcurrentRestrictions$$anon$4.submitValid(ConcurrentRestrictions.scala:176) [error] at sbt.ConcurrentRestrictions$$anon$4.submit(ConcurrentRestrictions.scala:165) [error] at sbt.Execute.submit(Execute.scala:262) [error] at sbt.Execute.ready(Execute.scala:242) [error] at sbt.Execute.notifyDone(Execute.scala:181) [error] at sbt.Execute.$anonfun$retire$2(Execute.scala:152) [error] at sbt.Execute.$anonfun$retire$2$adapted(Execute.scala:151) [error] at scala.collection.immutable.List.foreach(List.scala:389) [error] at sbt.Execute.retire(Execute.scala:151) [error] at sbt.Execute.$anonfun$work$2(Execute.scala:279) [error] at sbt.Execute$$anon$1.process(Execute.scala:24) [error] at sbt.Execute.next$1(Execute.scala:104) [error] at sbt.Execute.processAll(Execute.scala:107) [error] at sbt.Execute.runKeep(Execute.scala:84) [error] at sbt.EvaluateTask$.liftedTree1$1(EvaluateTask.scala:387) [error] at sbt.EvaluateTask$.run$1(EvaluateTask.scala:386) [error] at sbt.EvaluateTask$.runTask(EvaluateTask.scala:405) [error] at sbt.internal.Aggregation$.$anonfun$timedRun$4(Aggregation.scala:100) [error] at sbt.EvaluateTask$.withStreams(EvaluateTask.scala:331) [error] at sbt.internal.Aggregation$.timedRun(Aggregation.scala:98) [error] at sbt.internal.Aggregation$.runTasks(Aggregation.scala:111) [error] at sbt.internal.Aggregation$.$anonfun$applyTasks$1(Aggregation.scala:68) [error] at sbt.Command$.$anonfun$applyEffect$2(Command.scala:130) [error] at sbt.internal.Aggregation$.$anonfun$evaluatingParser$11(Aggregation.scala:220) [error] at sbt.internal.Act$.$anonfun$actParser0$3(Act.scala:387) [error] at sbt.MainLoop$.processCommand(MainLoop.scala:154) [error] at sbt.MainLoop$.$anonfun$next$2(MainLoop.scala:137) [error] at sbt.State$$anon$1.runCmd$1(State.scala:242) [error] at sbt.State$$anon$1.process(State.scala:248) [error] at sbt.MainLoop$.$anonfun$next$1(MainLoop.scala:137) [error] at sbt.internal.util.ErrorHandling$.wideConvert(ErrorHandling.scala:16) [error] at sbt.MainLoop$.next(MainLoop.scala:137) [error] at sbt.MainLoop$.run(MainLoop.scala:130) [error] at sbt.MainLoop$.$anonfun$runWithNewLog$1(MainLoop.scala:108) [error] at sbt.io.Using.apply(Using.scala:22) [error] at sbt.MainLoop$.runWithNewLog(MainLoop.scala:102) [error] at sbt.MainLoop$.runAndClearLast(MainLoop.scala:58) [error] at sbt.MainLoop$.runLoggedLoop(MainLoop.scala:43) [error] at sbt.MainLoop$.runLogged(MainLoop.scala:35) [error] at sbt.StandardMain$.runManaged(Main.scala:113) [error] at sbt.xMain.run(Main.scala:76) [error] at xsbt.boot.Launch$$anonfun$run$1.apply(Launch.scala:109) [error] at xsbt.boot.Launch$.withContextLoader(Launch.scala:128) [error] at xsbt.boot.Launch$.run(Launch.scala:109) [error] at xsbt.boot.Launch$$anonfun$apply$1.apply(Launch.scala:35) [error] at xsbt.boot.Launch$.launch(Launch.scala:117) [error] at xsbt.boot.Launch$.apply(Launch.scala:18) [error] at xsbt.boot.Boot$.runImpl(Boot.scala:41) [error] at xsbt.boot.Boot$.main(Boot.scala:17) [error] at xsbt.boot.Boot.main(Boot.scala) [error] java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.ExecutorCompletionService$QueueingFuture@33ec70dd rejected from java.util.concurrent.ThreadPoolExecutor@4b832de6[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 1410] [error] Use 'last' for the full log. ``` After: ``` sbt:zinc Root> ;zincIvyIntegration/cleanClasses;zincIvyIntegration/compile [success] Total time: 0 s, completed 03/04/2018 2:24:33 PM [info] Compiling 5 Scala sources and 1 Java source to /Users/jz/code/zinc/internal/zinc-ivy-integration/target/scala-2.12/classes ... [warn] /Users/jz/code/zinc/internal/zinc-ivy-integration/src/main/scala/sbt/internal/inc/ResourceLoader.scala:14:42: parameter value classLoader in method getPropertiesFor is never used [warn] def getPropertiesFor(resource: String, classLoader: ClassLoader): Properties = { [warn] ^ [warn] /Users/jz/code/zinc/internal/zinc-ivy-integration/src/main/scala/sbt/internal/inc/ZincComponentCompiler.scala:13:17: Unused import [warn] import java.net.URLClassLoader [warn] ^ [warn] /Users/jz/code/zinc/internal/zinc-ivy-integration/src/main/scala/sbt/internal/inc/ZincComponentCompiler.scala:137:26: Unused import [warn] import sbt.io.Path.toURLs [warn] ^ [warn] /Users/jz/code/zinc/internal/zinc-ivy-integration/src/main/scala/sbt/internal/inc/ZincComponentCompiler.scala:94:30: The outer reference in this type test cannot be checked at run time. [warn] private final case class ScalaArtifacts(compiler: File, library: File, others: Vector[File]) [warn] ^ ^C [warn] Canceling execution... [warn] four warnings found [info] Compilation has been cancelled [error] Total time: 1 s, completed 03/04/2018 2:24:35 PM ``` Best served with https://github.com/scala/scala/pull/6479 Fixes #3958 --- main/src/main/scala/sbt/EvaluateTask.scala | 7 ++++++- tasks/src/main/scala/sbt/Execute.scala | 11 +++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/main/src/main/scala/sbt/EvaluateTask.scala b/main/src/main/scala/sbt/EvaluateTask.scala index 1d17f5cc3..558fe2daf 100644 --- a/main/src/main/scala/sbt/EvaluateTask.scala +++ b/main/src/main/scala/sbt/EvaluateTask.scala @@ -11,6 +11,7 @@ import sbt.internal.{ Load, BuildStructure, TaskTimings, TaskName, GCUtil } import sbt.internal.util.{ Attributed, ErrorHandling, HList, RMap, Signals, Types } import sbt.util.{ Logger, Show } import sbt.librarymanagement.{ Resolver, UpdateReport } +import java.util.concurrent.RejectedExecutionException import scala.concurrent.duration.Duration import java.io.File @@ -387,7 +388,11 @@ object EvaluateTask { val results = x.runKeep(root)(service) storeValuesForPrevious(results, state, streams) applyResults(results, state, root) - } catch { case inc: Incomplete => (state, Inc(inc)) } finally shutdown() + } catch { + case _: RejectedExecutionException => + (state, Inc(Incomplete(None, message = Some("cancelled")))) + case inc: Incomplete => (state, Inc(inc)) + } finally shutdown() val replaced = transformInc(result) logIncResult(replaced, state, streams) (newState, replaced) diff --git a/tasks/src/main/scala/sbt/Execute.scala b/tasks/src/main/scala/sbt/Execute.scala index 267a1383a..e3030cc00 100644 --- a/tasks/src/main/scala/sbt/Execute.scala +++ b/tasks/src/main/scala/sbt/Execute.scala @@ -7,8 +7,10 @@ package sbt +import java.util.concurrent.RejectedExecutionException + import sbt.internal.util.ErrorHandling.wideConvert -import sbt.internal.util.{ DelegatingPMap, PMap, RMap, IDSet, ~> } +import sbt.internal.util.{ DelegatingPMap, IDSet, PMap, RMap, ~> } import sbt.internal.util.Types._ import Execute._ @@ -76,7 +78,12 @@ private[sbt] final class Execute[A[_] <: AnyRef]( "State: " + state.toString + "\n\nResults: " + results + "\n\nCalls: " + callers + "\n\n" def run[T](root: A[T])(implicit strategy: Strategy): Result[T] = - try { runKeep(root)(strategy)(root) } catch { case i: Incomplete => Inc(i) } + try { + runKeep(root)(strategy)(root) + } catch { + case i: Incomplete => Inc(i) + case _: RejectedExecutionException => Inc(Incomplete(None, message = Some("cancelled"))) + } def runKeep[T](root: A[T])(implicit strategy: Strategy): RMap[A, Result] = { assert(state.isEmpty, "Execute already running/ran.") From d48aa310e6442ff7ed2909c7686496822cea5ab4 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Tue, 3 Apr 2018 16:02:52 +0100 Subject: [PATCH 07/26] Catch RejectedExecutionException in Execute#runKeep --- main/src/main/scala/sbt/EvaluateTask.scala | 7 +----- tasks/src/main/scala/sbt/Execute.scala | 29 +++++++++++----------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/main/src/main/scala/sbt/EvaluateTask.scala b/main/src/main/scala/sbt/EvaluateTask.scala index 558fe2daf..1d17f5cc3 100644 --- a/main/src/main/scala/sbt/EvaluateTask.scala +++ b/main/src/main/scala/sbt/EvaluateTask.scala @@ -11,7 +11,6 @@ import sbt.internal.{ Load, BuildStructure, TaskTimings, TaskName, GCUtil } import sbt.internal.util.{ Attributed, ErrorHandling, HList, RMap, Signals, Types } import sbt.util.{ Logger, Show } import sbt.librarymanagement.{ Resolver, UpdateReport } -import java.util.concurrent.RejectedExecutionException import scala.concurrent.duration.Duration import java.io.File @@ -388,11 +387,7 @@ object EvaluateTask { val results = x.runKeep(root)(service) storeValuesForPrevious(results, state, streams) applyResults(results, state, root) - } catch { - case _: RejectedExecutionException => - (state, Inc(Incomplete(None, message = Some("cancelled")))) - case inc: Incomplete => (state, Inc(inc)) - } finally shutdown() + } catch { case inc: Incomplete => (state, Inc(inc)) } finally shutdown() val replaced = transformInc(result) logIncResult(replaced, state, streams) (newState, replaced) diff --git a/tasks/src/main/scala/sbt/Execute.scala b/tasks/src/main/scala/sbt/Execute.scala index e3030cc00..f063080ca 100644 --- a/tasks/src/main/scala/sbt/Execute.scala +++ b/tasks/src/main/scala/sbt/Execute.scala @@ -78,22 +78,21 @@ private[sbt] final class Execute[A[_] <: AnyRef]( "State: " + state.toString + "\n\nResults: " + results + "\n\nCalls: " + callers + "\n\n" def run[T](root: A[T])(implicit strategy: Strategy): Result[T] = - try { - runKeep(root)(strategy)(root) - } catch { - case i: Incomplete => Inc(i) - case _: RejectedExecutionException => Inc(Incomplete(None, message = Some("cancelled"))) - } - def runKeep[T](root: A[T])(implicit strategy: Strategy): RMap[A, Result] = { - assert(state.isEmpty, "Execute already running/ran.") + try { runKeep(root)(strategy)(root) } catch { case i: Incomplete => Inc(i) } - addNew(root) - processAll() - assert(results contains root, "No result for root node.") - val finalResults = triggers.onComplete(results) - progressState = progress.allCompleted(progressState, finalResults) - finalResults - } + def runKeep[T](root: A[T])(implicit strategy: Strategy): RMap[A, Result] = + try { + assert(state.isEmpty, "Execute already running/ran.") + + addNew(root) + processAll() + assert(results contains root, "No result for root node.") + val finalResults = triggers.onComplete(results) + progressState = progress.allCompleted(progressState, finalResults) + finalResults + } catch { + case _: RejectedExecutionException => throw Incomplete(None, message = Some("cancelled")) + } def processAll()(implicit strategy: Strategy): Unit = { @tailrec def next(): Unit = { From b78a0f667be2c0d27d4f313aeacb98a53833d2cd Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Tue, 3 Apr 2018 16:05:56 +0100 Subject: [PATCH 08/26] Catch RejectedExecutionException in CompletionService.submit --- .../main/scala/sbt/CompletionService.scala | 7 ++++-- tasks/src/main/scala/sbt/Execute.scala | 23 ++++++++----------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/tasks/src/main/scala/sbt/CompletionService.scala b/tasks/src/main/scala/sbt/CompletionService.scala index f6fc3a957..88f00a5e8 100644 --- a/tasks/src/main/scala/sbt/CompletionService.scala +++ b/tasks/src/main/scala/sbt/CompletionService.scala @@ -17,7 +17,8 @@ import java.util.concurrent.{ CompletionService => JCompletionService, Executor, Executors, - ExecutorCompletionService + ExecutorCompletionService, + RejectedExecutionException, } object CompletionService { @@ -33,7 +34,9 @@ object CompletionService { def take() = completion.take().get() } def submit[T](work: () => T, completion: JCompletionService[T]): () => T = { - val future = completion.submit { new Callable[T] { def call = work() } } + val future = try completion.submit { new Callable[T] { def call = work() } } catch { + case _: RejectedExecutionException => throw Incomplete(None, message = Some("cancelled")) + } () => future.get() } diff --git a/tasks/src/main/scala/sbt/Execute.scala b/tasks/src/main/scala/sbt/Execute.scala index f063080ca..c5d16288e 100644 --- a/tasks/src/main/scala/sbt/Execute.scala +++ b/tasks/src/main/scala/sbt/Execute.scala @@ -7,8 +7,6 @@ package sbt -import java.util.concurrent.RejectedExecutionException - import sbt.internal.util.ErrorHandling.wideConvert import sbt.internal.util.{ DelegatingPMap, IDSet, PMap, RMap, ~> } import sbt.internal.util.Types._ @@ -80,19 +78,16 @@ private[sbt] final class Execute[A[_] <: AnyRef]( def run[T](root: A[T])(implicit strategy: Strategy): Result[T] = try { runKeep(root)(strategy)(root) } catch { case i: Incomplete => Inc(i) } - def runKeep[T](root: A[T])(implicit strategy: Strategy): RMap[A, Result] = - try { - assert(state.isEmpty, "Execute already running/ran.") + def runKeep[T](root: A[T])(implicit strategy: Strategy): RMap[A, Result] = { + assert(state.isEmpty, "Execute already running/ran.") - addNew(root) - processAll() - assert(results contains root, "No result for root node.") - val finalResults = triggers.onComplete(results) - progressState = progress.allCompleted(progressState, finalResults) - finalResults - } catch { - case _: RejectedExecutionException => throw Incomplete(None, message = Some("cancelled")) - } + addNew(root) + processAll() + assert(results contains root, "No result for root node.") + val finalResults = triggers.onComplete(results) + progressState = progress.allCompleted(progressState, finalResults) + finalResults + } def processAll()(implicit strategy: Strategy): Unit = { @tailrec def next(): Unit = { From 8c1337455da230b8bef98723b354eed0df9f2751 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Wed, 4 Apr 2018 14:26:03 +0100 Subject: [PATCH 09/26] Fix migrate URL Fixes #4062 --- main-settings/src/main/scala/sbt/Structure.scala | 2 +- main-settings/src/main/scala/sbt/std/TaskMacro.scala | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/main-settings/src/main/scala/sbt/Structure.scala b/main-settings/src/main/scala/sbt/Structure.scala index bcedc4171..3c643f00d 100644 --- a/main-settings/src/main/scala/sbt/Structure.scala +++ b/main-settings/src/main/scala/sbt/Structure.scala @@ -565,7 +565,7 @@ object Scoped { /** The sbt 0.10 style DSL was deprecated in 0.13.13, favouring the use of the '.value' macro. * - * See http://www.scala-sbt.org/0.13/docs/Migrating-from-sbt-012x.html for how to migrate. + * See http://www.scala-sbt.org/1.x/docs/Migrating-from-sbt-013x.html for how to migrate. */ trait TupleSyntax { import Scoped._ diff --git a/main-settings/src/main/scala/sbt/std/TaskMacro.scala b/main-settings/src/main/scala/sbt/std/TaskMacro.scala index efeff7877..e7ffcee63 100644 --- a/main-settings/src/main/scala/sbt/std/TaskMacro.scala +++ b/main-settings/src/main/scala/sbt/std/TaskMacro.scala @@ -89,12 +89,12 @@ object TaskMacro { final val InputTaskCreateDynName = "createDyn" final val InputTaskCreateFreeName = "createFree" final val append1Migration = - "`<+=` operator is removed. Try `lhs += { x.value }`\n or see http://www.scala-sbt.org/1.0/docs/Migrating-from-sbt-012x.html." + "`<+=` operator is removed. Try `lhs += { x.value }`\n or see http://www.scala-sbt.org/1.x/docs/Migrating-from-sbt-013x.html." final val appendNMigration = - "`<++=` operator is removed. Try `lhs ++= { x.value }`\n or see http://www.scala-sbt.org/1.0/docs/Migrating-from-sbt-012x.html." + "`<++=` operator is removed. Try `lhs ++= { x.value }`\n or see http://www.scala-sbt.org/1.x/docs/Migrating-from-sbt-013x.html." final val assignMigration = """`<<=` operator is removed. Use `key := { x.value }` or `key ~= (old => { newValue })`. - |See http://www.scala-sbt.org/1.0/docs/Migrating-from-sbt-012x.html""".stripMargin + |See http://www.scala-sbt.org/1.x/docs/Migrating-from-sbt-013x.html""".stripMargin import LinterDSL.{ Empty => EmptyLinter } From 04d6a8b44c6b1385ebe84236af332b8de59d644e Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Thu, 5 Apr 2018 02:14:00 -0400 Subject: [PATCH 10/26] perf: optimize hash for build This hot path was discovered by retronym using FlameGraph. This removes intermediate creation of Array. `filesModifiedBytes` in particular was bad as it was going through all `*.class` files, each generating an Array. This replaces that with `fileModifiedHash` that accepts `MessageDigest`. According to the flamegraph, evalCommon footprint reduced from 4.5% to 3.6%. Using `time sbt reload reload reload exit`, the median reduced from 41.450s to 39.467s. --- build.sbt | 4 ++ .../src/main/scala/sbt/compiler/Eval.scala | 51 ++++++++++++++----- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/build.sbt b/build.sbt index d2b19aef3..349c89e62 100644 --- a/build.sbt +++ b/build.sbt @@ -310,6 +310,10 @@ lazy val actionsProj = (project in file("main-actions")) name := "Actions", libraryDependencies += sjsonNewScalaJson.value, mimaSettings, + mimaBinaryIssueFilters ++= Seq( + exclude[DirectMissingMethodProblem]("sbt.compiler.Eval.filesModifiedBytes"), + exclude[DirectMissingMethodProblem]("sbt.compiler.Eval.fileModifiedBytes"), + ) ) .configure( addSbtIO, diff --git a/main-actions/src/main/scala/sbt/compiler/Eval.scala b/main-actions/src/main/scala/sbt/compiler/Eval.scala index a9d97246a..d2afb03b9 100644 --- a/main-actions/src/main/scala/sbt/compiler/Eval.scala +++ b/main-actions/src/main/scala/sbt/compiler/Eval.scala @@ -18,6 +18,7 @@ import Tokens.{ EOF, NEWLINE, NEWLINES, SEMI } import java.io.File import java.nio.ByteBuffer import java.net.URLClassLoader +import java.security.MessageDigest import Eval.{ getModule, getValue, WrapValName } import sbt.io.{ DirectoryFilter, FileFilter, GlobFilter, Hash, IO, Path } @@ -159,12 +160,31 @@ final class Eval(optionsNoncp: Seq[String], // TODO - We also encode the source of the setting into the hash to avoid conflicts where the exact SAME setting // is defined in multiple evaluated instances with a backing. This leads to issues with finding a previous // value on the classpath when compiling. - val hash = Hash.toHex( - Hash(bytes( - stringSeqBytes(content) :: optBytes(backing)(fileExistsBytes) :: stringSeqBytes(options) :: - seqBytes(classpath)(fileModifiedBytes) :: stringSeqBytes(imports.strings.map(_._1)) :: optBytes( - tpeName)(bytes) :: - bytes(ev.extraHash) :: Nil))) + + // This is a hot path. + val digester = MessageDigest.getInstance("SHA") + content foreach { c => + digester.update(bytes(c)) + } + backing foreach { x => + digester.update(fileExistsBytes(x)) + } + options foreach { o => + digester.update(bytes(o)) + } + classpath foreach { f => + fileModifiedHash(f, digester) + } + imports.strings.map(_._1) foreach { x => + digester.update(bytes(x)) + } + tpeName foreach { x => + digester.update(bytes(x)) + } + digester.update(bytes(ev.extraHash)) + val d = digester.digest() + + val hash = Hash.toHex(d) val moduleName = makeModuleName(hash) lazy val unit = { @@ -482,13 +502,18 @@ private[sbt] object Eval { def seqBytes[T](s: Seq[T])(f: T => Array[Byte]): Array[Byte] = bytes(s map f) def bytes(b: Seq[Array[Byte]]): Array[Byte] = bytes(b.length) ++ b.flatten.toArray[Byte] def bytes(b: Boolean): Array[Byte] = Array[Byte](if (b) 1 else 0) - def filesModifiedBytes(fs: Array[File]): Array[Byte] = - if (fs eq null) filesModifiedBytes(Array[File]()) else seqBytes(fs)(fileModifiedBytes) - def fileModifiedBytes(f: File): Array[Byte] = - (if (f.isDirectory) - filesModifiedBytes(f listFiles classDirFilter) - else - bytes(IO.getModifiedTimeOrZero(f))) ++ bytes(f.getAbsolutePath) + + // fileModifiedBytes is a hot method, taking up 0.85% of reload time + // This is a procedural version + def fileModifiedHash(f: File, digester: MessageDigest): Unit = { + if (f.isDirectory) + (f listFiles classDirFilter) foreach { x => + fileModifiedHash(x, digester) + } else digester.update(bytes(IO.getModifiedTimeOrZero(f))) + + digester.update(bytes(f.getAbsolutePath)) + } + def fileExistsBytes(f: File): Array[Byte] = bytes(f.exists) ++ bytes(f.getAbsolutePath) From ad3692b2dfc8a7e2164f4a9b7d578a4723daac8c Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Thu, 5 Apr 2018 04:22:16 -0400 Subject: [PATCH 11/26] Use NIO Files.getLastModifiedTime for hashing --- main-actions/src/main/scala/sbt/compiler/Eval.scala | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/main-actions/src/main/scala/sbt/compiler/Eval.scala b/main-actions/src/main/scala/sbt/compiler/Eval.scala index d2afb03b9..f9b0ffeee 100644 --- a/main-actions/src/main/scala/sbt/compiler/Eval.scala +++ b/main-actions/src/main/scala/sbt/compiler/Eval.scala @@ -509,11 +509,22 @@ private[sbt] object Eval { if (f.isDirectory) (f listFiles classDirFilter) foreach { x => fileModifiedHash(x, digester) - } else digester.update(bytes(IO.getModifiedTimeOrZero(f))) + } else digester.update(bytes(JavaMilli.getModifiedTimeOrZero(f))) digester.update(bytes(f.getAbsolutePath)) } + // This uses NIO instead of the JNA-based IO.getModifiedTimeOrZero for speed + object JavaMilli { + import java.nio.file.{ Files, NoSuchFileException } + def getModifiedTimeOrZero(f: File): Long = + try { + Files.getLastModifiedTime(f.toPath).toMillis + } catch { + case e: NoSuchFileException => 0L + } + } + def fileExistsBytes(f: File): Array[Byte] = bytes(f.exists) ++ bytes(f.getAbsolutePath) From 4dc76e2b38c7f6f257a638d7a66a7f8838b735c5 Mon Sep 17 00:00:00 2001 From: Jason Steenstra-Pickens Date: Fri, 6 Apr 2018 10:41:31 +1200 Subject: [PATCH 12/26] Add dependencyResolution scoped to updateSbtClassifiers task Fixes #3432 --- main/src/main/scala/sbt/Defaults.scala | 1 + .../update-sbt-classifiers/build.sbt | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index b3fb287e0..c631ff114 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -2168,6 +2168,7 @@ object Classpaths { filterImplicit = false, overrideScalaVersion = true).withScalaOrganization(scalaOrganization.value)) }, + dependencyResolution := IvyDependencyResolution(ivyConfiguration.value), updateSbtClassifiers in TaskGlobal := (Def.task { val lm = dependencyResolution.value val s = streams.value diff --git a/sbt/src/sbt-test/dependency-management/update-sbt-classifiers/build.sbt b/sbt/src/sbt-test/dependency-management/update-sbt-classifiers/build.sbt index b4f691e86..36f0b199b 100644 --- a/sbt/src/sbt-test/dependency-management/update-sbt-classifiers/build.sbt +++ b/sbt/src/sbt-test/dependency-management/update-sbt-classifiers/build.sbt @@ -1 +1,9 @@ scalaVersion := "2.11.11" + +ivyConfiguration := { + throw new RuntimeException("updateSbtClassifiers should use updateSbtClassifiers / ivyConfiguration") +} + +dependencyResolution := { + throw new RuntimeException("updateSbtClassifiers should use updateSbtClassifiers / dependencyResolution") +} From 707bf08c4e46faadce3adbbc8ab4514e3ba5bf4b Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Thu, 5 Apr 2018 20:29:26 -0400 Subject: [PATCH 13/26] Add new closewatch mode --- main-command/src/main/scala/sbt/Watched.scala | 6 +++--- project/Dependencies.scala | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/main-command/src/main/scala/sbt/Watched.scala b/main-command/src/main/scala/sbt/Watched.scala index 1c2396593..cff9c1597 100644 --- a/main-command/src/main/scala/sbt/Watched.scala +++ b/main-command/src/main/scala/sbt/Watched.scala @@ -134,14 +134,14 @@ object Watched { AttributeKey[Watched]("watched-configuration", "Configures continuous execution.") def createWatchService(): WatchService = { + def closeWatch = new MacOSXWatchService() sys.props.get("sbt.watch.mode") match { case Some("polling") => new PollingWatchService(PollDelay) case Some("nio") => FileSystems.getDefault.newWatchService() - case _ if Properties.isMac => - // WatchService is slow on macOS - use old polling mode - new MacOSXWatchService(PollDelay) + case Some("closewatch") => closeWatch + case _ if Properties.isMac => closeWatch case _ => FileSystems.getDefault.newWatchService() } diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 94a583ea8..88d72b833 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -8,7 +8,7 @@ object Dependencies { val baseScalaVersion = scala212 // sbt modules - private val ioVersion = "1.1.4" + private val ioVersion = "1.1.5" private val utilVersion = "1.1.3" private val lmVersion = "1.1.4" private val zincVersion = "1.1.3" From 8781c16cbb8695a14a11b3372eaca4971e7bf56b Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Fri, 6 Apr 2018 16:30:33 -0400 Subject: [PATCH 14/26] bump IO and Zinc --- .../src/main/scala/sbt/compiler/Eval.scala | 19 ++++++++----------- project/Dependencies.scala | 4 ++-- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/main-actions/src/main/scala/sbt/compiler/Eval.scala b/main-actions/src/main/scala/sbt/compiler/Eval.scala index f9b0ffeee..56fbce162 100644 --- a/main-actions/src/main/scala/sbt/compiler/Eval.scala +++ b/main-actions/src/main/scala/sbt/compiler/Eval.scala @@ -15,7 +15,7 @@ import ast.parser.Tokens import reporters.{ ConsoleReporter, Reporter } import scala.reflect.internal.util.{ AbstractFileClassLoader, BatchSourceFile } import Tokens.{ EOF, NEWLINE, NEWLINES, SEMI } -import java.io.File +import java.io.{ File, FileNotFoundException } import java.nio.ByteBuffer import java.net.URLClassLoader import java.security.MessageDigest @@ -509,21 +509,18 @@ private[sbt] object Eval { if (f.isDirectory) (f listFiles classDirFilter) foreach { x => fileModifiedHash(x, digester) - } else digester.update(bytes(JavaMilli.getModifiedTimeOrZero(f))) + } else digester.update(bytes(getModifiedTimeOrZero(f))) digester.update(bytes(f.getAbsolutePath)) } // This uses NIO instead of the JNA-based IO.getModifiedTimeOrZero for speed - object JavaMilli { - import java.nio.file.{ Files, NoSuchFileException } - def getModifiedTimeOrZero(f: File): Long = - try { - Files.getLastModifiedTime(f.toPath).toMillis - } catch { - case e: NoSuchFileException => 0L - } - } + def getModifiedTimeOrZero(f: File): Long = + try { + sbt.io.JavaMilli.getModifiedTime(f.getPath) + } catch { + case _: FileNotFoundException => 0L + } def fileExistsBytes(f: File): Array[Byte] = bytes(f.exists) ++ diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 88d72b833..50d38e300 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -8,10 +8,10 @@ object Dependencies { val baseScalaVersion = scala212 // sbt modules - private val ioVersion = "1.1.5" + private val ioVersion = "1.1.6" private val utilVersion = "1.1.3" private val lmVersion = "1.1.4" - private val zincVersion = "1.1.3" + private val zincVersion = "1.1.4" private val sbtIO = "org.scala-sbt" %% "io" % ioVersion From 32385c8bb7985fb3bc545c59f6c8bc1292f0e283 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Fri, 6 Apr 2018 18:36:10 -0400 Subject: [PATCH 15/26] launcher 1.0.4 --- project/Dependencies.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 50d38e300..6ee63a29c 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -26,8 +26,9 @@ object Dependencies { private val libraryManagementCore = "org.scala-sbt" %% "librarymanagement-core" % lmVersion private val libraryManagementIvy = "org.scala-sbt" %% "librarymanagement-ivy" % lmVersion - val launcherInterface = "org.scala-sbt" % "launcher-interface" % "1.0.3" - val rawLauncher = "org.scala-sbt" % "launcher" % "1.0.3" + val launcherVersion = "1.0.4" + val launcherInterface = "org.scala-sbt" % "launcher-interface" % launcherVersion + val rawLauncher = "org.scala-sbt" % "launcher" % launcherVersion val testInterface = "org.scala-sbt" % "test-interface" % "1.0" val ipcSocket = "org.scala-sbt.ipcsocket" % "ipcsocket" % "1.0.0" From 1abb0a3641e68b635f5f50229c21cfb96eff85d1 Mon Sep 17 00:00:00 2001 From: Jason Zaugg Date: Sun, 8 Apr 2018 14:31:48 +1000 Subject: [PATCH 16/26] Upgrade to latest sbt-houserules --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index c91a3bc6f..68b01d14b 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,7 +1,7 @@ scalaVersion := "2.12.4" scalacOptions ++= Seq("-feature", "-language:postfixOps") -addSbtPlugin("org.scala-sbt" % "sbt-houserules" % "0.3.5") +addSbtPlugin("org.scala-sbt" % "sbt-houserules" % "0.3.6") addSbtPlugin("org.scala-sbt" % "sbt-contraband" % "0.4.0") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "3.0.2") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.8.0") From 8ed796fb2570956a2edfa9cc36e4c80ddb05438b Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Mon, 9 Apr 2018 00:11:17 -0400 Subject: [PATCH 17/26] Re-fixing console and JLine Fixes #3482 take 2 I thought I tested #4054 using a local build, but when I ran 1.1.3, `console` did not display anything that I typed. Switching to `usingTerminal` which calls `terminal.restore` similar to what I had in 1.1.1 fixes `console`. --- main-actions/src/main/scala/sbt/Console.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/main-actions/src/main/scala/sbt/Console.scala b/main-actions/src/main/scala/sbt/Console.scala index d94484030..61bfb9e47 100644 --- a/main-actions/src/main/scala/sbt/Console.scala +++ b/main-actions/src/main/scala/sbt/Console.scala @@ -41,7 +41,9 @@ final class Console(compiler: AnalyzingCompiler) { implicit log: Logger): Try[Unit] = { def console0() = compiler.console(classpath, options, initialCommands, cleanupCommands, log)(loader, bindings) - JLine.withJLine(Run.executeTrapExit(console0, log)) + JLine.usingTerminal { _ => + Run.executeTrapExit(console0, log) + } } } From b9eb7764f3d28c7b30bbf3ceb641afde9f583e3f Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Mon, 9 Apr 2018 12:37:42 -0400 Subject: [PATCH 18/26] Zinc 1.1.5 --- build.sbt | 2 +- project/Dependencies.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 349c89e62..a45918803 100644 --- a/build.sbt +++ b/build.sbt @@ -9,7 +9,7 @@ def buildLevelSettings: Seq[Setting[_]] = inThisBuild( Seq( organization := "org.scala-sbt", - version := "1.1.3-SNAPSHOT", + version := "1.1.4-SNAPSHOT", description := "sbt is an interactive build tool", bintrayOrganization := Some("sbt"), bintrayRepository := { diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 6ee63a29c..5705334fa 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -11,7 +11,7 @@ object Dependencies { private val ioVersion = "1.1.6" private val utilVersion = "1.1.3" private val lmVersion = "1.1.4" - private val zincVersion = "1.1.4" + private val zincVersion = "1.1.5" private val sbtIO = "org.scala-sbt" %% "io" % ioVersion From e5a37cad429bcfc7d74a8820c13be3381cc5f2fe Mon Sep 17 00:00:00 2001 From: Heikki Vesalainen Date: Wed, 11 Apr 2018 00:10:56 +0300 Subject: [PATCH 19/26] Update to Jline 2.14.6 This version of Jline fixes three things for Emacs users: - ANSI colors are now enabled for Emacs. - Terminal echo is now disabled for Emacs. - History is enabled for all dump terminals. --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 5705334fa..56706d639 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -98,7 +98,7 @@ object Dependencies { "com.eed3si9n" %% "sjson-new-scalajson" % contrabandSjsonNewVersion.value } - val jline = "jline" % "jline" % "2.14.4" + val jline = "jline" % "jline" % "2.14.6" val scalatest = "org.scalatest" %% "scalatest" % "3.0.4" val scalaCheck = "org.scalacheck" %% "scalacheck" % "1.13.4" val specs2 = "org.specs2" %% "specs2-junit" % "4.0.1" From a1e3146c0865f085b5ac65966561efc02c16e922 Mon Sep 17 00:00:00 2001 From: Alexey Alekhin Date: Wed, 11 Apr 2018 18:37:16 +0200 Subject: [PATCH 20/26] Don't use initialize request id for the internal collectAnalyses call --- .../main/scala/sbt/internal/server/LanguageServerProtocol.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala b/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala index 77314890a..af5b37337 100644 --- a/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala @@ -64,7 +64,7 @@ private[sbt] trait LanguageServerProtocol extends CommandChannel { else throw LangServerError(ErrorCodes.InvalidRequest, "invalid token") } else () setInitialized(true) - append(Exec(s"collectAnalyses", Some(request.id), Some(CommandSource(name)))) + append(Exec(s"collectAnalyses", None, Some(CommandSource(name)))) langRespond(InitializeResult(serverCapabilities), Option(request.id)) case "textDocument/definition" => import scala.concurrent.ExecutionContext.Implicits.global From 2bb717dbf97f94bdc6d7259654757dea2a278b35 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Wed, 11 Apr 2018 18:51:53 -0700 Subject: [PATCH 21/26] Exclude directories instead of including files The existing filter caused SourceModificationWatch.watch to ignore deleted files because !file.exists implies !file.isFile. The intention of the filter was to exclude directories that had a name ending in ".scala". --- main/src/main/scala/sbt/Defaults.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index c631ff114..82af2a9e4 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -162,8 +162,7 @@ object Defaults extends BuildCommon { artifactClassifier in packageSrc :== Some(SourceClassifier), artifactClassifier in packageDoc :== Some(DocClassifier), includeFilter :== NothingFilter, - includeFilter in unmanagedSources :== ("*.java" | "*.scala") && new SimpleFileFilter( - _.isFile), + includeFilter in unmanagedSources :== ("*.java" | "*.scala") -- DirectoryFilter, includeFilter in unmanagedJars :== "*.jar" | "*.so" | "*.dll" | "*.jnilib" | "*.zip", includeFilter in unmanagedResources :== AllPassFilter, bgList := { bgJobService.value.jobs }, From 1ec07c1867d2b7eb01503f68e0fc43d84f445911 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Mon, 26 Mar 2018 10:37:25 -0400 Subject: [PATCH 22/26] Recover sbtOn --- build.sbt | 4 ++++ sbt/src/test/scala/sbt/RunFromSourceMain.scala | 17 +++++++++-------- sbt/src/test/scala/testpkg/ServerSpec.scala | 6 +++++- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/build.sbt b/build.sbt index 41723a157..01f18a2a8 100644 --- a/build.sbt +++ b/build.sbt @@ -497,6 +497,7 @@ lazy val sbtProj = (project in file("sbt")) normalizedName := "sbt", crossScalaVersions := Seq(baseScalaVersion), crossPaths := false, + javaOptions ++= Seq("-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005"), mimaSettings, mimaBinaryIssueFilters ++= sbtIgnoredProblems, BuildInfoPlugin.buildInfoDefaultSettings, @@ -508,6 +509,9 @@ lazy val sbtProj = (project in file("sbt")) classDirectory in Compile, classDirectory in Test, ), + Test / run / connectInput := true, + Test / run / outputStrategy := Some(StdoutOutput), + Test / run / fork := true, ) .configure(addSbtCompilerBridge) diff --git a/sbt/src/test/scala/sbt/RunFromSourceMain.scala b/sbt/src/test/scala/sbt/RunFromSourceMain.scala index 2f4e81a51..c0bd9cfc1 100644 --- a/sbt/src/test/scala/sbt/RunFromSourceMain.scala +++ b/sbt/src/test/scala/sbt/RunFromSourceMain.scala @@ -7,7 +7,7 @@ package sbt -import scala.concurrent.Future +import scala.util.Try import sbt.util.LogExchange import scala.annotation.tailrec import buildinfo.TestBuildInfo @@ -17,21 +17,22 @@ object RunFromSourceMain { private val sbtVersion = "1.0.3" // "dev" private val scalaVersion = "2.12.4" - def fork(workingDirectory: File): Future[Unit] = { + def fork(workingDirectory: File): Try[Unit] = { val fo = ForkOptions() - .withWorkingDirectory(workingDirectory) .withOutputStrategy(OutputStrategy.StdoutOutput) + fork(fo, workingDirectory) + } + + def fork(fo0: ForkOptions, workingDirectory: File): Try[Unit] = { + val fo = fo0 + .withWorkingDirectory(workingDirectory) implicit val runner = new ForkRun(fo) val cp = { TestBuildInfo.test_classDirectory +: TestBuildInfo.fullClasspath } val options = Vector(workingDirectory.toString) val log = LogExchange.logger("RunFromSourceMain.fork", None, None) - import scala.concurrent.ExecutionContext.Implicits.global - Future { - Run.run("sbt.RunFromSourceMain", cp, options, log) - () - } + Run.run("sbt.RunFromSourceMain", cp, options, log) } def main(args: Array[String]): Unit = args match { diff --git a/sbt/src/test/scala/testpkg/ServerSpec.scala b/sbt/src/test/scala/testpkg/ServerSpec.scala index f34eee6b1..259e5c248 100644 --- a/sbt/src/test/scala/testpkg/ServerSpec.scala +++ b/sbt/src/test/scala/testpkg/ServerSpec.scala @@ -71,7 +71,11 @@ case class TestServer(baseDirectory: File) { private val RetByte = '\r'.toByte hostLog("fork to a new sbt instance") - RunFromSourceMain.fork(baseDirectory) + import scala.concurrent.ExecutionContext.Implicits.global + Future { + RunFromSourceMain.fork(baseDirectory) + () + } lazy val portfile = baseDirectory / "project" / "target" / "active.json" hostLog("wait 30s until the server is ready to respond") From eeeb4c9ff2e047599b9bbc83a1dcd8951cf9ddad Mon Sep 17 00:00:00 2001 From: OlegYch Date: Mon, 23 Apr 2018 20:46:08 +0300 Subject: [PATCH 23/26] Remove usage of DynamicVariable and fix memory leak, fixes https://github.com/sbt/sbt/issues/4112 --- build.sbt | 2 ++ .../scala/sbt/JUnitXmlTestsListener.scala | 23 ++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/build.sbt b/build.sbt index 5baf26bfc..c1e10290a 100644 --- a/build.sbt +++ b/build.sbt @@ -220,6 +220,8 @@ lazy val testingProj = (project in file("testing")) exclude[DirectMissingMethodProblem]("sbt.protocol.testing.TestItemEvent.copy$default$*"), exclude[DirectMissingMethodProblem]("sbt.protocol.testing.TestStringEvent.copy"), exclude[DirectMissingMethodProblem]("sbt.protocol.testing.TestStringEvent.copy$default$1"), + //no reason to use + exclude[DirectMissingMethodProblem]("sbt.JUnitXmlTestsListener.testSuite"), ) ) .configure(addSbtIO, addSbtCompilerClasspath, addSbtUtilLogging) diff --git a/testing/src/main/scala/sbt/JUnitXmlTestsListener.scala b/testing/src/main/scala/sbt/JUnitXmlTestsListener.scala index 3e62dcea2..29a4ebc8c 100644 --- a/testing/src/main/scala/sbt/JUnitXmlTestsListener.scala +++ b/testing/src/main/scala/sbt/JUnitXmlTestsListener.scala @@ -12,7 +12,6 @@ import java.net.InetAddress import java.util.Hashtable import scala.collection.mutable.ListBuffer -import scala.util.DynamicVariable import scala.xml.{ Elem, Node => XNode, XML } import testing.{ Event => TEvent, @@ -124,21 +123,28 @@ class JUnitXmlTestsListener(val outputDir: String) extends TestsListener { } /**The currently running test suite*/ - val testSuite = new DynamicVariable(null: TestSuite) + private val testSuite = new InheritableThreadLocal[Option[TestSuite]] { + override def initialValue(): Option[TestSuite] = None + } + + private def withTestSuite[T](f: TestSuite => T) = + testSuite.get().map(f).getOrElse(sys.error("no test suite")) /**Creates the output Dir*/ - override def doInit() = { targetDir.mkdirs() } + override def doInit() = { + val _ = targetDir.mkdirs() + } /** * Starts a new, initially empty Suite with the given name. */ - override def startGroup(name: String): Unit = testSuite.value_=(new TestSuite(name)) + override def startGroup(name: String): Unit = testSuite.set(Some(new TestSuite(name))) /** * Adds all details for the given even to the current suite. */ override def testEvent(event: TestEvent): Unit = for (e <- event.detail) { - testSuite.value.addEvent(e) + withTestSuite(_.addEvent(e)) } /** @@ -174,7 +180,7 @@ class JUnitXmlTestsListener(val outputDir: String) extends TestsListener { def selector = null def throwable = new OptionalThrowable(t) } - testSuite.value.addEvent(event) + withTestSuite(_.addEvent(event)) writeSuite() } @@ -191,10 +197,11 @@ class JUnitXmlTestsListener(val outputDir: String) extends TestsListener { private[this] def normalizeName(s: String) = s.replaceAll("""\s+""", "-") private def writeSuite() = { - val file = new File(targetDir, s"${normalizeName(testSuite.value.name)}.xml").getAbsolutePath + val file = new File(targetDir, s"${normalizeName(withTestSuite(_.name))}.xml").getAbsolutePath // TODO would be nice to have a logger and log this with level debug // System.err.println("Writing JUnit XML test report: " + file) - XML.save(file, testSuite.value.stop(), "UTF-8", true, null) + XML.save(file, withTestSuite(_.stop()), "UTF-8", true, null) + testSuite.remove() } /**Does nothing, as we write each file after a suite is done.*/ From 065b65a05e25885840c1f713ef16cffeb6a1f8af Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Tue, 24 Apr 2018 07:29:49 +0100 Subject: [PATCH 24/26] Drop -Ywarn-unused:-params Previously we'd get in the build logs: [error] params cannot be negated, it enables other arguments and lots of wawrnings. Now we just get lots of warnings without the non-fatal error message. --- project/Util.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Util.scala b/project/Util.scala index 8c68f4ced..d2141341b 100644 --- a/project/Util.scala +++ b/project/Util.scala @@ -48,7 +48,7 @@ object Util { "-Yno-adapted-args", "-Ywarn-dead-code", "-Ywarn-numeric-widen", - "-Ywarn-unused:-patvars,-params,-implicits,_", + "-Ywarn-unused:-patvars,-implicits,_", "-Ywarn-unused-import" ) }), From 334789c919eb3c9fed2670fb26e3a53e7e2dc6dc Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Wed, 25 Apr 2018 16:15:19 -0400 Subject: [PATCH 25/26] 1.1.5-SNAPSHOT --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index c1e10290a..33480b08c 100644 --- a/build.sbt +++ b/build.sbt @@ -9,7 +9,7 @@ def buildLevelSettings: Seq[Setting[_]] = inThisBuild( Seq( organization := "org.scala-sbt", - version := "1.1.4-SNAPSHOT", + version := "1.1.5-SNAPSHOT", description := "sbt is an interactive build tool", bintrayOrganization := Some("sbt"), bintrayRepository := { From a798c4adffb66b631c6965bf53a999037f05323b Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Wed, 25 Apr 2018 16:55:26 -0400 Subject: [PATCH 26/26] Re-fix console and JLine bug Fixes #3482 take 3 There are two bugs related REPL and JLine. 1. JLine getting disabled (up arrow not working) on the second run of `console` task. 2. Typed characters not showing up even on the `console` task. The first issue was fixed by #4054 which added `t.init`. When I noticed the second problem, I fixed it in #4082 (adds `t.restore`) but undid the first fix. This attempts to fix both the issues. --- main-actions/src/main/scala/sbt/Console.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main-actions/src/main/scala/sbt/Console.scala b/main-actions/src/main/scala/sbt/Console.scala index 61bfb9e47..9bc20d676 100644 --- a/main-actions/src/main/scala/sbt/Console.scala +++ b/main-actions/src/main/scala/sbt/Console.scala @@ -41,7 +41,8 @@ final class Console(compiler: AnalyzingCompiler) { implicit log: Logger): Try[Unit] = { def console0() = compiler.console(classpath, options, initialCommands, cleanupCommands, log)(loader, bindings) - JLine.usingTerminal { _ => + JLine.usingTerminal { t => + t.init Run.executeTrapExit(console0, log) } }