diff --git a/.travis.yml b/.travis.yml index 59cb0e286..984384aff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,29 +16,18 @@ matrix: env: matrix: - - SBT_CMD=";test:compile;scalafmtCheck" - - SBT_CMD="mimaReportBinaryIssues" - - SBT_CMD="safeUnitTests" - - SBT_CMD="otherUnitTests" + - SBT_CMD=";mimaReportBinaryIssues;test:compile;scalafmtCheck;safeUnitTests;otherUnitTests" - SBT_CMD="scripted actions/*" - - SBT_CMD="scripted apiinfo/*" - - SBT_CMD="scripted compiler-project/*" + - SBT_CMD="scripted apiinfo/* compiler-project/* ivy-deps-management/*" - SBT_CMD="scripted dependency-management/*1of4" - SBT_CMD="scripted dependency-management/*2of4" - SBT_CMD="scripted dependency-management/*3of4" - SBT_CMD="scripted dependency-management/*4of4" - - SBT_CMD="scripted ivy-deps-management/*" - - SBT_CMD="scripted java/*" - - SBT_CMD="scripted package/*" + - SBT_CMD="scripted java/* package/* reporter/* run/* project-load/*" - SBT_CMD="scripted project/*1of2" - SBT_CMD="scripted project/*2of2" - - SBT_CMD="scripted reporter/*" - - SBT_CMD="scripted run/*" - - SBT_CMD="scripted source-dependencies/*1of3" - - SBT_CMD="scripted source-dependencies/*2of3" - - SBT_CMD="scripted source-dependencies/*3of3" + - SBT_CMD="scripted source-dependencies/*" - SBT_CMD="scripted tests/*" - - SBT_CMD="scripted project-load/*" - SBT_CMD="repoOverrideTest:scripted dependency-management/*" notifications: @@ -46,7 +35,8 @@ notifications: - sbt-dev-bot@googlegroups.com script: - - sbt -J-XX:ReservedCodeCacheSize=128m "$SBT_CMD" + # It doesn't need that much memory because compile and run are forked + - sbt -J-XX:ReservedCodeCacheSize=128m -J-Xmx800M -J-Xms800M -J-server "$SBT_CMD" before_cache: - find $HOME/.ivy2 -name "ivydata-*.properties" -print -delete diff --git a/build.sbt b/build.sbt index 071b73a9a..913d31f78 100644 --- a/build.sbt +++ b/build.sbt @@ -53,7 +53,9 @@ def commonSettings: Seq[Setting[_]] = mimaBinaryIssueFilters ++= { import com.typesafe.tools.mima.core._, ProblemFilters._ Seq() - } + }, + fork in compile := true, + fork in run := true ) flatMap (_.settings) def minimalSettings: Seq[Setting[_]] = @@ -352,7 +354,7 @@ def otherRootSettings = scriptedUnpublished := scriptedUnpublishedTask.evaluated, scriptedSource := (sourceDirectory in sbtProj).value / "sbt-test", // scriptedPrescripted := { addSbtAlternateResolver _ }, - scriptedLaunchOpts := List("-XX:MaxPermSize=256M", "-Xmx1G"), + scriptedLaunchOpts := List("-Xmx1500M", "-Xms512M", "-server"), publishAll := { val _ = (publishLocal).all(ScopeFilter(inAnyProject)).value }, publishLocalBinAll := { val _ = (publishLocalBin).all(ScopeFilter(inAnyProject)).value }, aggregate in bintrayRelease := false @@ -362,8 +364,9 @@ def otherRootSettings = () }, scriptedLaunchOpts := { - List("-XX:MaxPermSize=256M", - "-Xmx1G", + List("-Xmx1500M", + "-Xms512M", + "-server", "-Dsbt.override.build.repos=true", s"""-Dsbt.repository.config=${scriptedSource.value / "repo.config"}""") }, diff --git a/main/src/main/scala/sbt/internal/parser/SbtParser.scala b/main/src/main/scala/sbt/internal/parser/SbtParser.scala index ce75a8503..c1e2f1cbb 100644 --- a/main/src/main/scala/sbt/internal/parser/SbtParser.scala +++ b/main/src/main/scala/sbt/internal/parser/SbtParser.scala @@ -3,7 +3,6 @@ package internal package parser import sbt.internal.util.{ LineRange, MessageOnlyException } - import java.io.File import java.util.concurrent.ConcurrentHashMap @@ -14,10 +13,9 @@ import scala.reflect.internal.util.{ BatchSourceFile, Position } import scala.reflect.io.VirtualDirectory import scala.reflect.internal.Positions import scala.tools.nsc.{ CompilerCommand, Global } -import scala.tools.nsc.reporters.{ Reporter, StoreReporter } +import scala.tools.nsc.reporters.{ ConsoleReporter, Reporter, StoreReporter } import scala.util.Random - -import scala.util.{ Success, Failure } +import scala.util.{ Failure, Success } private[sbt] object SbtParser { val END_OF_LINE_CHAR = '\n' @@ -78,9 +76,10 @@ private[sbt] object SbtParser { private def getReporter(fileName: String) = { val reporter = reporters.get(fileName) - if (reporter == null) - sys.error(s"Sbt parser failure: no reporter for $fileName.") - reporter + if (reporter == null) { + scalacGlobalInitReporter.getOrElse( + sys.error(s"Sbt forgot to initialize `scalacGlobalInitReporter`.")) + } else reporter } def throwParserErrorsIfAny(reporter: StoreReporter, fileName: String): Unit = { @@ -99,14 +98,15 @@ private[sbt] object SbtParser { } private[sbt] final val globalReporter = new UniqueParserReporter + private[sbt] var scalacGlobalInitReporter: Option[ConsoleReporter] = None private[sbt] final lazy val defaultGlobalForParser = { - import scala.reflect.internal.util.NoPosition val options = "-cp" :: s"$defaultClasspath" :: "-Yrangepos" :: Nil - val reportError = (msg: String) => globalReporter.error(NoPosition, msg) + val reportError = (msg: String) => System.err.println(msg) val command = new CompilerCommand(options, reportError) val settings = command.settings settings.outputDirs.setSingleOutput(new VirtualDirectory("(memory)", None)) + scalacGlobalInitReporter = Some(new ConsoleReporter(settings)) // Mix Positions, otherwise global ignores -Yrangepos val global = new Global(settings, globalReporter) with Positions diff --git a/project/Scripted.scala b/project/Scripted.scala index a669e6256..96473f4a9 100644 --- a/project/Scripted.scala +++ b/project/Scripted.scala @@ -70,7 +70,7 @@ object Scripted { else dropped.take(pageSize) } def nameP(group: String) = { - token("*".id | id.examples(pairMap(group))) + token("*".id | id.examples(pairMap.getOrElse(group, Set.empty[String]))) } val PagedIds: Parser[Seq[String]] = for { @@ -89,12 +89,12 @@ object Scripted { // Interface to cross class loader type SbtScriptedRunner = { - def run(resourceBaseDirectory: File, - bufferLog: Boolean, - tests: Array[String], - bootProperties: File, - launchOpts: Array[String], - prescripted: java.util.List[File]): Unit + def runInParallel(resourceBaseDirectory: File, + bufferLog: Boolean, + tests: Array[String], + bootProperties: File, + launchOpts: Array[String], + prescripted: java.util.List[File]): Unit } def doScripted(launcher: File, @@ -120,7 +120,12 @@ object Scripted { def get(x: Int): sbt.File = ??? def size(): Int = 0 } - bridge.run(sourcePath, bufferLog, args.toArray, launcher, launchOpts.toArray, callback) + bridge.runInParallel(sourcePath, + bufferLog, + args.toArray, + launcher, + launchOpts.toArray, + callback) } catch { case ite: java.lang.reflect.InvocationTargetException => throw ite.getCause } } } diff --git a/sbt/src/sbt-test/actions/command-to-string/build.sbt b/sbt/src/sbt-test/actions/command-to-string/build.sbt index 00aeb8a65..8dc3e9a93 100644 --- a/sbt/src/sbt-test/actions/command-to-string/build.sbt +++ b/sbt/src/sbt-test/actions/command-to-string/build.sbt @@ -1,6 +1,6 @@ commands += Command.command("noop") { s => s } TaskKey[Unit]("check") := { - assert(commands.value.toString() == "List(SimpleCommand(noop))", - s"""commands should display "List(SimpleCommand(noop))" but is ${commands.value}""") + assert(commands.value.toString().contains("SimpleCommand(noop)"), + s"""commands should contain "SimpleCommand(noop)" in ${commands.value}""") } diff --git a/sbt/src/sbt-test/actions/task-cancel/build.sbt b/sbt/src/sbt-test/actions/task-cancel/build.sbt index 149e93163..334fa825d 100644 --- a/sbt/src/sbt-test/actions/task-cancel/build.sbt +++ b/sbt/src/sbt-test/actions/task-cancel/build.sbt @@ -1,9 +1,9 @@ import sbt.ExposeYourself._ -taskCancelHandler := { (state: State) => - new TaskEvaluationCancelHandler { +taskCancelStrategy := { (state: State) => + new TaskCancellationStrategy { type State = Unit - override def onTaskEngineStart(canceller: TaskCancel): Unit = canceller.cancel() - override def finish(result: Unit): Unit = () + override def onTaskEngineStart(canceller: RunningTaskEngine): Unit = canceller.cancelAndShutdown() + override def onTaskEngineFinish(state: State): Unit = () } -} \ No newline at end of file +} diff --git a/sbt/src/sbt-test/actions/task-cancel/project/Build.scala b/sbt/src/sbt-test/actions/task-cancel/project/Build.scala index cd7055cf0..33011f1fb 100644 --- a/sbt/src/sbt-test/actions/task-cancel/project/Build.scala +++ b/sbt/src/sbt-test/actions/task-cancel/project/Build.scala @@ -1,5 +1,5 @@ package sbt // this API is private[sbt], so only exposed for trusted clients and folks who like breaking. object ExposeYourself { - val taskCancelHandler = sbt.Keys.taskCancelHandler -} \ No newline at end of file + val taskCancelStrategy = sbt.Keys.taskCancelStrategy +} diff --git a/sbt/src/sbt-test/apiinfo/unstable-existential-names/build.sbt b/sbt/src/sbt-test/apiinfo/unstable-existential-names/build.sbt index 90c322356..ae689f73d 100644 --- a/sbt/src/sbt-test/apiinfo/unstable-existential-names/build.sbt +++ b/sbt/src/sbt-test/apiinfo/unstable-existential-names/build.sbt @@ -1,12 +1,26 @@ import sbt.internal.inc.Analysis +import complete.DefaultParsers._ -// checks number of compilation iterations performed since last `clean` run -InputKey[Unit]("check-number-of-compiler-iterations") := { - val args = Def.spaceDelimited().parsed - val a = (compile in Compile).value.asInstanceOf[Analysis] - assert(args.size == 1) - val expectedIterationsNumber = args(0).toInt - val allCompilationsSize = a.compilations.allCompilations.size - assert(allCompilationsSize == expectedIterationsNumber, - "allCompilationsSize == %d (expected %d)".format(allCompilationsSize, expectedIterationsNumber)) +// Reset compiler iterations, necessary because tests run in batch mode +val recordPreviousIterations = taskKey[Unit]("Record previous iterations.") +recordPreviousIterations := { + CompileState.previousIterations = { + val previousAnalysis = (previousCompile in Compile).value.analysis + if (previousAnalysis.isEmpty) { + streams.value.log.info("No previous analysis detected") + 0 + } else { + previousAnalysis.get match { + case a: Analysis => a.compilations.allCompilations.size + } + } + } +} + +val checkIterations = inputKey[Unit]("Verifies the accumulated number of iterations of incremental compilation.") + +checkIterations := { + val expected: Int = (Space ~> NatBasic).parsed + val actual: Int = ((compile in Compile).value match { case a: Analysis => a.compilations.allCompilations.size }) - CompileState.previousIterations + assert(expected == actual, s"Expected $expected compilations, got $actual") } diff --git a/sbt/src/sbt-test/apiinfo/unstable-existential-names/project/CompileState.scala b/sbt/src/sbt-test/apiinfo/unstable-existential-names/project/CompileState.scala new file mode 100644 index 000000000..078db9c7b --- /dev/null +++ b/sbt/src/sbt-test/apiinfo/unstable-existential-names/project/CompileState.scala @@ -0,0 +1,4 @@ +// This is necessary because tests are run in batch mode +object CompileState { + @volatile var previousIterations: Int = -1 +} diff --git a/sbt/src/sbt-test/apiinfo/unstable-existential-names/test b/sbt/src/sbt-test/apiinfo/unstable-existential-names/test index 4a895eb1c..1eff3117d 100644 --- a/sbt/src/sbt-test/apiinfo/unstable-existential-names/test +++ b/sbt/src/sbt-test/apiinfo/unstable-existential-names/test @@ -2,6 +2,7 @@ # do not introduce unnecessary compile iterations # introduces first compile iteration +> recordPreviousIterations > compile # this change is local to a method and does not change the api so introduces # only one additional compile iteration @@ -9,4 +10,4 @@ $ copy-file changes/Foo1.scala src/main/scala/Foo.scala # second iteration > compile # check if there are only two compile iterations being performed -> checkNumberOfCompilerIterations 2 +> checkIterations 2 diff --git a/sbt/src/sbt-test/compiler-project/inc-pickled-existential/build.sbt b/sbt/src/sbt-test/compiler-project/inc-pickled-existential/build.sbt index 59de073d7..c1e5d8839 100644 --- a/sbt/src/sbt-test/compiler-project/inc-pickled-existential/build.sbt +++ b/sbt/src/sbt-test/compiler-project/inc-pickled-existential/build.sbt @@ -1,15 +1,28 @@ import sbt.internal.inc.Analysis +import complete.DefaultParsers._ logLevel := Level.Debug -// dumps analysis into target/analysis-dump.txt file -InputKey[Unit]("check-number-of-compiler-iterations") := { - val args = Def.spaceDelimited().parsed - val a = (compile in Compile).value.asInstanceOf[Analysis] - assert(args.size == 1) - val expectedIterationsNumber = args(0).toInt - assert( - a.compilations.allCompilations.size == expectedIterationsNumber, - "a.compilations.allCompilations.size = %d (expected %d)".format( - a.compilations.allCompilations.size, expectedIterationsNumber)) +// Reset compiler iterations, necessary because tests run in batch mode +val recordPreviousIterations = taskKey[Unit]("Record previous iterations.") +recordPreviousIterations := { + CompileState.previousIterations = { + val previousAnalysis = (previousCompile in Compile).value.analysis + if (previousAnalysis.isEmpty) { + streams.value.log.info("No previous analysis detected") + 0 + } else { + previousAnalysis.get match { + case a: Analysis => a.compilations.allCompilations.size + } + } + } +} + +val checkIterations = inputKey[Unit]("Verifies the accumulated number of iterations of incremental compilation.") + +checkIterations := { + val expected: Int = (Space ~> NatBasic).parsed + val actual: Int = ((compile in Compile).value match { case a: Analysis => a.compilations.allCompilations.size }) - CompileState.previousIterations + assert(expected == actual, s"Expected $expected compilations, got $actual") } diff --git a/sbt/src/sbt-test/compiler-project/inc-pickled-existential/project/CompileState.scala b/sbt/src/sbt-test/compiler-project/inc-pickled-existential/project/CompileState.scala new file mode 100644 index 000000000..078db9c7b --- /dev/null +++ b/sbt/src/sbt-test/compiler-project/inc-pickled-existential/project/CompileState.scala @@ -0,0 +1,4 @@ +// This is necessary because tests are run in batch mode +object CompileState { + @volatile var previousIterations: Int = -1 +} diff --git a/sbt/src/sbt-test/compiler-project/inc-pickled-existential/test b/sbt/src/sbt-test/compiler-project/inc-pickled-existential/test index b671a0166..cd9556fa5 100644 --- a/sbt/src/sbt-test/compiler-project/inc-pickled-existential/test +++ b/sbt/src/sbt-test/compiler-project/inc-pickled-existential/test @@ -2,6 +2,7 @@ # do not introduce unnecessary compile iterations # introduces first compile iteration +> recordPreviousIterations > compile # this change is local to a method and does not change the api so introduces # only one additional compile iteration @@ -9,4 +10,4 @@ $ copy-file changes/B1.scala src/main/scala/B.scala # second iteration > compile # check if there are only two compile iterations being performed -> checkNumberOfCompilerIterations 2 +> checkIterations 2 diff --git a/sbt/src/sbt-test/compiler-project/inc-pickled-refinement/build.sbt b/sbt/src/sbt-test/compiler-project/inc-pickled-refinement/build.sbt index 4a78ef4e7..ae689f73d 100644 --- a/sbt/src/sbt-test/compiler-project/inc-pickled-refinement/build.sbt +++ b/sbt/src/sbt-test/compiler-project/inc-pickled-refinement/build.sbt @@ -1,11 +1,26 @@ import sbt.internal.inc.Analysis +import complete.DefaultParsers._ -InputKey[Unit]("check-number-of-compiler-iterations") := { - val args = Def.spaceDelimited().parsed - val a = (compile in Compile).value.asInstanceOf[Analysis] - assert(args.size == 1) - val expectedIterationsNumber = args(0).toInt - assert(a.compilations.allCompilations.size == expectedIterationsNumber, - "a.compilations.allCompilations.size = %d (expected %d)".format( - a.compilations.allCompilations.size, expectedIterationsNumber)) +// Reset compiler iterations, necessary because tests run in batch mode +val recordPreviousIterations = taskKey[Unit]("Record previous iterations.") +recordPreviousIterations := { + CompileState.previousIterations = { + val previousAnalysis = (previousCompile in Compile).value.analysis + if (previousAnalysis.isEmpty) { + streams.value.log.info("No previous analysis detected") + 0 + } else { + previousAnalysis.get match { + case a: Analysis => a.compilations.allCompilations.size + } + } + } +} + +val checkIterations = inputKey[Unit]("Verifies the accumulated number of iterations of incremental compilation.") + +checkIterations := { + val expected: Int = (Space ~> NatBasic).parsed + val actual: Int = ((compile in Compile).value match { case a: Analysis => a.compilations.allCompilations.size }) - CompileState.previousIterations + assert(expected == actual, s"Expected $expected compilations, got $actual") } diff --git a/sbt/src/sbt-test/compiler-project/inc-pickled-refinement/project/CompileState.scala b/sbt/src/sbt-test/compiler-project/inc-pickled-refinement/project/CompileState.scala new file mode 100644 index 000000000..078db9c7b --- /dev/null +++ b/sbt/src/sbt-test/compiler-project/inc-pickled-refinement/project/CompileState.scala @@ -0,0 +1,4 @@ +// This is necessary because tests are run in batch mode +object CompileState { + @volatile var previousIterations: Int = -1 +} diff --git a/sbt/src/sbt-test/compiler-project/inc-pickled-refinement/test b/sbt/src/sbt-test/compiler-project/inc-pickled-refinement/test index 110af1ccb..7a83d8efd 100644 --- a/sbt/src/sbt-test/compiler-project/inc-pickled-refinement/test +++ b/sbt/src/sbt-test/compiler-project/inc-pickled-refinement/test @@ -4,6 +4,7 @@ # https://github.com/sbt/sbt/issues/610 # introduces first compile iteration +> recordPreviousIterations > compile # this change is local to method and does not change api so introduces # only one additional compile iteration @@ -11,4 +12,4 @@ $ copy-file changes/Impl1.scala src/main/scala/Impl.scala # second iteration > compile # check if there are only two compile iterations performed -> checkNumberOfCompilerIterations 2 +> checkIterations 2 diff --git a/sbt/src/sbt-test/dependency-management/cached-resolution-conflicts/pending b/sbt/src/sbt-test/dependency-management/cached-resolution-conflicts/pending index 55385448d..9c88a5605 100644 --- a/sbt/src/sbt-test/dependency-management/cached-resolution-conflicts/pending +++ b/sbt/src/sbt-test/dependency-management/cached-resolution-conflicts/pending @@ -1,5 +1,4 @@ # Quoting @eed3si9n in https://github.com/dwijnand/sbt-lm/pull/1 : -# # > After several experiments, I'm actually convinced that force() is unrelated to the scripted scenario, # > and it's # currently passing by virtue of the questionable caching behavior: # > https://github.com/sbt/sbt/blob/c223dccb542beaf763a3a2909cda74bdad39beca/ivy/src/main/scala/sbt/ivyint/CachedResolutionResolveEngine.scala#L715 diff --git a/sbt/src/sbt-test/project-load/sha-conflict/pending b/sbt/src/sbt-test/project-load/sha-conflict/disabled similarity index 100% rename from sbt/src/sbt-test/project-load/sha-conflict/pending rename to sbt/src/sbt-test/project-load/sha-conflict/disabled diff --git a/sbt/src/sbt-test/project/auto-plugins/build.sbt b/sbt/src/sbt-test/project/auto-plugins/build.sbt index 8038f2a06..058343927 100644 --- a/sbt/src/sbt-test/project/auto-plugins/build.sbt +++ b/sbt/src/sbt-test/project/auto-plugins/build.sbt @@ -52,9 +52,9 @@ check := { same(ddel, None, "del in projD") // val buildValue = (demo in ThisBuild).value - same(buildValue, "build 0", "demo in ThisBuild") + same(buildValue, "build 1", "demo in ThisBuild") // this is temporary, should be 0 until # is fixed val globalValue = (demo in Global).value - same(globalValue, "global 0", "demo in Global") + same(globalValue, "global 1", "demo in Global") // this is temporary, should be 0 until # is fixed val projValue = (demo in projC).?.value same(projValue, Some("project projC Q R"), "demo in projC") val qValue = (del in projC in q).?.value diff --git a/sbt/src/sbt-test/project/binary-plugin/test b/sbt/src/sbt-test/project/binary-plugin/test index a8ccb21b4..b1f744b7a 100644 --- a/sbt/src/sbt-test/project/binary-plugin/test +++ b/sbt/src/sbt-test/project/binary-plugin/test @@ -3,7 +3,7 @@ $ copy-file changes/define/build.sbt build.sbt $ copy-file changes/define/A.scala A.scala $ copy-file changes/define/D.scala D.scala -# reload implied +> reload > publishLocal # Now we remove the source code and define a project which uses the build. diff --git a/sbt/src/sbt-test/project/defs/build.sbt b/sbt/src/sbt-test/project/defs/build.sbt index f5dca00bb..94be70893 100644 --- a/sbt/src/sbt-test/project/defs/build.sbt +++ b/sbt/src/sbt-test/project/defs/build.sbt @@ -1,4 +1,5 @@ -lazy val a,b = project +lazy val a = project +lazy val b = project def now = System.currentTimeMillis diff --git a/sbt/src/sbt-test/project/load-hooks/test b/sbt/src/sbt-test/project/load-hooks/test index 2e3f94ffd..4db9a6e26 100644 --- a/sbt/src/sbt-test/project/load-hooks/test +++ b/sbt/src/sbt-test/project/load-hooks/test @@ -1,3 +1,4 @@ +> reboot > checkCount 1 0 > checkCount 1 0 > reload diff --git a/sbt/src/sbt-test/project/old-ops/test b/sbt/src/sbt-test/project/old-ops/test index 2eae3cfe4..dca604af7 100644 --- a/sbt/src/sbt-test/project/old-ops/test +++ b/sbt/src/sbt-test/project/old-ops/test @@ -1,17 +1,18 @@ $ copy-file changes/settingAssign/build.sbt build.sbt --> compile +# Necessary to reload since the test assumes that the new build is picked up +-> reload $ copy-file changes/settingAppend1/build.sbt build.sbt --> compile +-> reload $ copy-file changes/settingAppendN/build.sbt build.sbt --> compile +-> reload $ copy-file changes/taskAssign/build.sbt build.sbt --> compile +-> reload $ copy-file changes/taskAppend1/build.sbt build.sbt --> compile +-> reload $ copy-file changes/taskAppendN/build.sbt build.sbt --> compile +-> reload diff --git a/sbt/src/sbt-test/source-dependencies/abstract-type-override/build.sbt b/sbt/src/sbt-test/source-dependencies/abstract-type-override/build.sbt index b0d683951..ae689f73d 100644 --- a/sbt/src/sbt-test/source-dependencies/abstract-type-override/build.sbt +++ b/sbt/src/sbt-test/source-dependencies/abstract-type-override/build.sbt @@ -1,12 +1,26 @@ import sbt.internal.inc.Analysis +import complete.DefaultParsers._ -InputKey[Unit]("checkNumberOfCompilerIterations") := { - val a = (compile in Compile).value.asInstanceOf[Analysis] - val args = Def.spaceDelimited().parsed - assert(args.size == 1) - val expectedIterationsNumber = args(0).toInt - assert(a.compilations.allCompilations.size == expectedIterationsNumber, - "a.compilations.allCompilations.size = %d (expected %d)".format( - a.compilations.allCompilations.size, expectedIterationsNumber) - ) +// Reset compiler iterations, necessary because tests run in batch mode +val recordPreviousIterations = taskKey[Unit]("Record previous iterations.") +recordPreviousIterations := { + CompileState.previousIterations = { + val previousAnalysis = (previousCompile in Compile).value.analysis + if (previousAnalysis.isEmpty) { + streams.value.log.info("No previous analysis detected") + 0 + } else { + previousAnalysis.get match { + case a: Analysis => a.compilations.allCompilations.size + } + } + } +} + +val checkIterations = inputKey[Unit]("Verifies the accumulated number of iterations of incremental compilation.") + +checkIterations := { + val expected: Int = (Space ~> NatBasic).parsed + val actual: Int = ((compile in Compile).value match { case a: Analysis => a.compilations.allCompilations.size }) - CompileState.previousIterations + assert(expected == actual, s"Expected $expected compilations, got $actual") } diff --git a/sbt/src/sbt-test/source-dependencies/abstract-type-override/project/CompileState.scala b/sbt/src/sbt-test/source-dependencies/abstract-type-override/project/CompileState.scala new file mode 100644 index 000000000..078db9c7b --- /dev/null +++ b/sbt/src/sbt-test/source-dependencies/abstract-type-override/project/CompileState.scala @@ -0,0 +1,4 @@ +// This is necessary because tests are run in batch mode +object CompileState { + @volatile var previousIterations: Int = -1 +} diff --git a/sbt/src/sbt-test/source-dependencies/abstract-type-override/test b/sbt/src/sbt-test/source-dependencies/abstract-type-override/test index 9ffa4fb17..30168afbf 100644 --- a/sbt/src/sbt-test/source-dependencies/abstract-type-override/test +++ b/sbt/src/sbt-test/source-dependencies/abstract-type-override/test @@ -4,6 +4,7 @@ # See https://github.com/sbt/sbt/issues/726 for details # introduces first compile iteration +> recordPreviousIterations > compile # this change adds a comment and does not change api so introduces # only one additional compile iteration @@ -11,4 +12,4 @@ $ copy-file changes/Bar1.scala src/main/scala/Bar.scala # second iteration #> compile # check if there are only two compile iterations performed -> checkNumberOfCompilerIterations 2 +> checkIterations 2 diff --git a/sbt/src/sbt-test/source-dependencies/canon/build.sbt b/sbt/src/sbt-test/source-dependencies/canon/build.sbt index 6a00e6d3e..ae689f73d 100644 --- a/sbt/src/sbt-test/source-dependencies/canon/build.sbt +++ b/sbt/src/sbt-test/source-dependencies/canon/build.sbt @@ -1,10 +1,26 @@ import sbt.internal.inc.Analysis import complete.DefaultParsers._ +// Reset compiler iterations, necessary because tests run in batch mode +val recordPreviousIterations = taskKey[Unit]("Record previous iterations.") +recordPreviousIterations := { + CompileState.previousIterations = { + val previousAnalysis = (previousCompile in Compile).value.analysis + if (previousAnalysis.isEmpty) { + streams.value.log.info("No previous analysis detected") + 0 + } else { + previousAnalysis.get match { + case a: Analysis => a.compilations.allCompilations.size + } + } + } +} + val checkIterations = inputKey[Unit]("Verifies the accumulated number of iterations of incremental compilation.") checkIterations := { val expected: Int = (Space ~> NatBasic).parsed - val actual: Int = (compile in Compile).value match { case a: Analysis => a.compilations.allCompilations.size } + val actual: Int = ((compile in Compile).value match { case a: Analysis => a.compilations.allCompilations.size }) - CompileState.previousIterations assert(expected == actual, s"Expected $expected compilations, got $actual") } diff --git a/sbt/src/sbt-test/source-dependencies/canon/project/CompileState.scala b/sbt/src/sbt-test/source-dependencies/canon/project/CompileState.scala new file mode 100644 index 000000000..078db9c7b --- /dev/null +++ b/sbt/src/sbt-test/source-dependencies/canon/project/CompileState.scala @@ -0,0 +1,4 @@ +// This is necessary because tests are run in batch mode +object CompileState { + @volatile var previousIterations: Int = -1 +} diff --git a/sbt/src/sbt-test/source-dependencies/canon/test b/sbt/src/sbt-test/source-dependencies/canon/test index 13caf4871..24c4bcd21 100644 --- a/sbt/src/sbt-test/source-dependencies/canon/test +++ b/sbt/src/sbt-test/source-dependencies/canon/test @@ -5,6 +5,7 @@ # This also verifies that compilation does not get repeatedly triggered by a mismatch in # paths. +> recordPreviousIterations > compile > compile -> checkIterations 1 \ No newline at end of file +> checkIterations 1 diff --git a/sbt/src/sbt-test/source-dependencies/constants/pending b/sbt/src/sbt-test/source-dependencies/constants/pending index eebce96e4..7a5ae5879 100644 --- a/sbt/src/sbt-test/source-dependencies/constants/pending +++ b/sbt/src/sbt-test/source-dependencies/constants/pending @@ -1,5 +1,4 @@ # Marked as pending, see https://github.com/sbt/sbt/issues/1543 -# # Tests if source dependencies are tracked properly # for compile-time constants (like final vals in top-level objects) # see https://issues.scala-lang.org/browse/SI-7173 for details diff --git a/sbt/src/sbt-test/source-dependencies/ext/build.sbt b/sbt/src/sbt-test/source-dependencies/ext/build.sbt index b66d2a521..ae689f73d 100644 --- a/sbt/src/sbt-test/source-dependencies/ext/build.sbt +++ b/sbt/src/sbt-test/source-dependencies/ext/build.sbt @@ -1,10 +1,26 @@ import sbt.internal.inc.Analysis import complete.DefaultParsers._ +// Reset compiler iterations, necessary because tests run in batch mode +val recordPreviousIterations = taskKey[Unit]("Record previous iterations.") +recordPreviousIterations := { + CompileState.previousIterations = { + val previousAnalysis = (previousCompile in Compile).value.analysis + if (previousAnalysis.isEmpty) { + streams.value.log.info("No previous analysis detected") + 0 + } else { + previousAnalysis.get match { + case a: Analysis => a.compilations.allCompilations.size + } + } + } +} + val checkIterations = inputKey[Unit]("Verifies the accumulated number of iterations of incremental compilation.") checkIterations := { - val expected: Int = (Space ~> NatBasic).parsed - val actual: Int = (compile in Compile).value match { case a: Analysis => a.compilations.allCompilations.size } - assert(expected == actual, s"Expected $expected compilations, got $actual") -} \ No newline at end of file + val expected: Int = (Space ~> NatBasic).parsed + val actual: Int = ((compile in Compile).value match { case a: Analysis => a.compilations.allCompilations.size }) - CompileState.previousIterations + assert(expected == actual, s"Expected $expected compilations, got $actual") +} diff --git a/sbt/src/sbt-test/source-dependencies/ext/project/CompileState.scala b/sbt/src/sbt-test/source-dependencies/ext/project/CompileState.scala new file mode 100644 index 000000000..078db9c7b --- /dev/null +++ b/sbt/src/sbt-test/source-dependencies/ext/project/CompileState.scala @@ -0,0 +1,4 @@ +// This is necessary because tests are run in batch mode +object CompileState { + @volatile var previousIterations: Int = -1 +} diff --git a/sbt/src/sbt-test/source-dependencies/ext/test b/sbt/src/sbt-test/source-dependencies/ext/test index 2b01a3de7..b92cb041c 100644 --- a/sbt/src/sbt-test/source-dependencies/ext/test +++ b/sbt/src/sbt-test/source-dependencies/ext/test @@ -1,7 +1,8 @@ # initial compilation +> recordPreviousIterations > checkIterations 1 # no further compilation should be necessary, since nothing changed # previously, a dependency on a jar in lib/ext/ would # always force recompilation -> checkIterations 1 \ No newline at end of file +> checkIterations 1 diff --git a/sbt/src/sbt-test/source-dependencies/less-inter-inv-java/build.sbt b/sbt/src/sbt-test/source-dependencies/less-inter-inv-java/build.sbt index 6a00e6d3e..ae689f73d 100644 --- a/sbt/src/sbt-test/source-dependencies/less-inter-inv-java/build.sbt +++ b/sbt/src/sbt-test/source-dependencies/less-inter-inv-java/build.sbt @@ -1,10 +1,26 @@ import sbt.internal.inc.Analysis import complete.DefaultParsers._ +// Reset compiler iterations, necessary because tests run in batch mode +val recordPreviousIterations = taskKey[Unit]("Record previous iterations.") +recordPreviousIterations := { + CompileState.previousIterations = { + val previousAnalysis = (previousCompile in Compile).value.analysis + if (previousAnalysis.isEmpty) { + streams.value.log.info("No previous analysis detected") + 0 + } else { + previousAnalysis.get match { + case a: Analysis => a.compilations.allCompilations.size + } + } + } +} + val checkIterations = inputKey[Unit]("Verifies the accumulated number of iterations of incremental compilation.") checkIterations := { val expected: Int = (Space ~> NatBasic).parsed - val actual: Int = (compile in Compile).value match { case a: Analysis => a.compilations.allCompilations.size } + val actual: Int = ((compile in Compile).value match { case a: Analysis => a.compilations.allCompilations.size }) - CompileState.previousIterations assert(expected == actual, s"Expected $expected compilations, got $actual") } diff --git a/sbt/src/sbt-test/source-dependencies/less-inter-inv-java/project/CompileState.scala b/sbt/src/sbt-test/source-dependencies/less-inter-inv-java/project/CompileState.scala new file mode 100644 index 000000000..078db9c7b --- /dev/null +++ b/sbt/src/sbt-test/source-dependencies/less-inter-inv-java/project/CompileState.scala @@ -0,0 +1,4 @@ +// This is necessary because tests are run in batch mode +object CompileState { + @volatile var previousIterations: Int = -1 +} diff --git a/sbt/src/sbt-test/source-dependencies/less-inter-inv-java/test b/sbt/src/sbt-test/source-dependencies/less-inter-inv-java/test index 85a7c97e9..b8a6167c7 100644 --- a/sbt/src/sbt-test/source-dependencies/less-inter-inv-java/test +++ b/sbt/src/sbt-test/source-dependencies/less-inter-inv-java/test @@ -1,4 +1,5 @@ # 1 iteration from initial full compile +> recordPreviousIterations > run $ copy-file changes/A2.java A.java diff --git a/sbt/src/sbt-test/source-dependencies/less-inter-inv/build.sbt b/sbt/src/sbt-test/source-dependencies/less-inter-inv/build.sbt index 6a00e6d3e..ae689f73d 100644 --- a/sbt/src/sbt-test/source-dependencies/less-inter-inv/build.sbt +++ b/sbt/src/sbt-test/source-dependencies/less-inter-inv/build.sbt @@ -1,10 +1,26 @@ import sbt.internal.inc.Analysis import complete.DefaultParsers._ +// Reset compiler iterations, necessary because tests run in batch mode +val recordPreviousIterations = taskKey[Unit]("Record previous iterations.") +recordPreviousIterations := { + CompileState.previousIterations = { + val previousAnalysis = (previousCompile in Compile).value.analysis + if (previousAnalysis.isEmpty) { + streams.value.log.info("No previous analysis detected") + 0 + } else { + previousAnalysis.get match { + case a: Analysis => a.compilations.allCompilations.size + } + } + } +} + val checkIterations = inputKey[Unit]("Verifies the accumulated number of iterations of incremental compilation.") checkIterations := { val expected: Int = (Space ~> NatBasic).parsed - val actual: Int = (compile in Compile).value match { case a: Analysis => a.compilations.allCompilations.size } + val actual: Int = ((compile in Compile).value match { case a: Analysis => a.compilations.allCompilations.size }) - CompileState.previousIterations assert(expected == actual, s"Expected $expected compilations, got $actual") } diff --git a/sbt/src/sbt-test/source-dependencies/less-inter-inv/project/CompileState.scala b/sbt/src/sbt-test/source-dependencies/less-inter-inv/project/CompileState.scala new file mode 100644 index 000000000..078db9c7b --- /dev/null +++ b/sbt/src/sbt-test/source-dependencies/less-inter-inv/project/CompileState.scala @@ -0,0 +1,4 @@ +// This is necessary because tests are run in batch mode +object CompileState { + @volatile var previousIterations: Int = -1 +} diff --git a/sbt/src/sbt-test/source-dependencies/less-inter-inv/test b/sbt/src/sbt-test/source-dependencies/less-inter-inv/test index c6df5698e..fe8596e4c 100644 --- a/sbt/src/sbt-test/source-dependencies/less-inter-inv/test +++ b/sbt/src/sbt-test/source-dependencies/less-inter-inv/test @@ -1,4 +1,5 @@ # 1 iteration from initial full compile +> recordPreviousIterations > run $ copy-file changes/A2.scala A.scala diff --git a/sbt/src/sbt-test/source-dependencies/restore-classes/build.sbt b/sbt/src/sbt-test/source-dependencies/restore-classes/build.sbt index 08931b91c..ca1fd1b5d 100644 --- a/sbt/src/sbt-test/source-dependencies/restore-classes/build.sbt +++ b/sbt/src/sbt-test/source-dependencies/restore-classes/build.sbt @@ -3,10 +3,26 @@ import complete.DefaultParsers._ crossTarget in Compile := target.value +// Reset compiler iterations, necessary because tests run in batch mode +val recordPreviousIterations = taskKey[Unit]("Record previous iterations.") +recordPreviousIterations := { + CompileState.previousIterations = { + val previousAnalysis = (previousCompile in Compile).value.analysis + if (previousAnalysis.isEmpty) { + streams.value.log.info("No previous analysis detected") + 0 + } else { + previousAnalysis.get match { + case a: Analysis => a.compilations.allCompilations.size + } + } + } +} + val checkIterations = inputKey[Unit]("Verifies the accumulated number of iterations of incremental compilation.") checkIterations := { val expected: Int = (Space ~> NatBasic).parsed - val actual: Int = (compile in Compile).value match { case a: Analysis => a.compilations.allCompilations.size } + val actual: Int = ((compile in Compile).value match { case a: Analysis => a.compilations.allCompilations.size }) - CompileState.previousIterations assert(expected == actual, s"Expected $expected compilations, got $actual") } diff --git a/sbt/src/sbt-test/source-dependencies/restore-classes/project/CompileState.scala b/sbt/src/sbt-test/source-dependencies/restore-classes/project/CompileState.scala new file mode 100644 index 000000000..078db9c7b --- /dev/null +++ b/sbt/src/sbt-test/source-dependencies/restore-classes/project/CompileState.scala @@ -0,0 +1,4 @@ +// This is necessary because tests are run in batch mode +object CompileState { + @volatile var previousIterations: Int = -1 +} diff --git a/sbt/src/sbt-test/source-dependencies/restore-classes/test b/sbt/src/sbt-test/source-dependencies/restore-classes/test index 028d6226c..ad191d5fe 100644 --- a/sbt/src/sbt-test/source-dependencies/restore-classes/test +++ b/sbt/src/sbt-test/source-dependencies/restore-classes/test @@ -2,6 +2,7 @@ $ copy-file changes/A1.scala A.scala $ copy-file changes/B.scala B.scala # B depends on A # 1 iteration +> recordPreviousIterations > compile $ copy-file changes/A2.scala A.scala diff --git a/sbt/src/sbt-test/source-dependencies/trait-member-modified/build.sbt b/sbt/src/sbt-test/source-dependencies/trait-member-modified/build.sbt index 9675b95c8..75a14e146 100644 --- a/sbt/src/sbt-test/source-dependencies/trait-member-modified/build.sbt +++ b/sbt/src/sbt-test/source-dependencies/trait-member-modified/build.sbt @@ -1,4 +1,14 @@ import sbt.internal.inc.Analysis +import xsbti.Maybe +import xsbti.compile.{PreviousResult, CompileAnalysis, MiniSetup} + +previousCompile in Compile := { + if (!CompileState.isNew) { + val res = new PreviousResult(Maybe.nothing[CompileAnalysis], Maybe.nothing[MiniSetup]) + CompileState.isNew = true + res + } else (previousCompile in Compile).value +} /* Performs checks related to compilations: * a) checks in which compilation given set of files was recompiled @@ -22,7 +32,7 @@ TaskKey[Unit]("checkCompilations") := { val files = fileNames.map(new java.io.File(_)) assert(recompiledFiles(iteration) == files, "%s != %s".format(recompiledFiles(iteration), files)) } - assert(allCompilations.size == 2) + assert(allCompilations.size == 2, s"All compilations is ${allCompilations.size}") // B.scala is just compiled at the beginning recompiledFilesInIteration(0, Set("B.scala")) // A.scala is changed and recompiled diff --git a/sbt/src/sbt-test/source-dependencies/trait-member-modified/project/CompileState.scala b/sbt/src/sbt-test/source-dependencies/trait-member-modified/project/CompileState.scala new file mode 100644 index 000000000..c50b231cc --- /dev/null +++ b/sbt/src/sbt-test/source-dependencies/trait-member-modified/project/CompileState.scala @@ -0,0 +1,4 @@ +// This is necessary because tests are run in batch mode +object CompileState { + @volatile var isNew: Boolean = false +} diff --git a/sbt/src/sbt-test/source-dependencies/transitive-memberRef/build.sbt b/sbt/src/sbt-test/source-dependencies/transitive-memberRef/build.sbt index ef7386f4c..7f4c0fd78 100644 --- a/sbt/src/sbt-test/source-dependencies/transitive-memberRef/build.sbt +++ b/sbt/src/sbt-test/source-dependencies/transitive-memberRef/build.sbt @@ -1,7 +1,18 @@ import sbt.internal.inc.Analysis +import xsbti.Maybe +import xsbti.compile.{PreviousResult, CompileAnalysis, MiniSetup} logLevel := Level.Debug +// Reset compile status because scripted tests are run in batch mode +previousCompile in Compile := { + if (!CompileState.isNew) { + val res = new PreviousResult(Maybe.nothing[CompileAnalysis], Maybe.nothing[MiniSetup]) + CompileState.isNew = true + res + } else (previousCompile in Compile).value +} + // disable sbt's heuristic which recompiles everything in case // some fraction (e.g. 50%) of files is scheduled to be recompiled // in this test we want precise information about recompiled files diff --git a/sbt/src/sbt-test/source-dependencies/transitive-memberRef/project/CompileState.scala b/sbt/src/sbt-test/source-dependencies/transitive-memberRef/project/CompileState.scala new file mode 100644 index 000000000..c50b231cc --- /dev/null +++ b/sbt/src/sbt-test/source-dependencies/transitive-memberRef/project/CompileState.scala @@ -0,0 +1,4 @@ +// This is necessary because tests are run in batch mode +object CompileState { + @volatile var isNew: Boolean = false +} diff --git a/scripted/plugin/src/main/scala/sbt/ScriptedPlugin.scala b/scripted/plugin/src/main/scala/sbt/ScriptedPlugin.scala index 212afc1c3..7aa4edcc0 100644 --- a/scripted/plugin/src/main/scala/sbt/ScriptedPlugin.scala +++ b/scripted/plugin/src/main/scala/sbt/ScriptedPlugin.scala @@ -59,13 +59,14 @@ object ScriptedPlugin extends AutoPlugin { ModuleUtilities.getObject("sbt.test.ScriptedTests", loader) } - def scriptedRunTask: Initialize[Task[Method]] = Def task ( + def scriptedRunTask: Initialize[Task[Method]] = Def.task( scriptedTests.value.getClass.getMethod("run", classOf[File], classOf[Boolean], classOf[Array[String]], classOf[File], - classOf[Array[String]]) + classOf[Array[String]], + classOf[java.util.List[File]]) ) import DefaultParsers._ @@ -98,7 +99,7 @@ object ScriptedPlugin extends AutoPlugin { else dropped.take(pageSize) } def nameP(group: String) = { - token("*".id | id.examples(pairMap(group))) + token("*".id | id.examples(pairMap.getOrElse(group, Set.empty[String]))) } val PagedIds: Parser[Seq[String]] = for { @@ -125,7 +126,8 @@ object ScriptedPlugin extends AutoPlugin { scriptedBufferLog.value: java.lang.Boolean, args.toArray, sbtLauncher.value, - scriptedLaunchOpts.value.toArray + scriptedLaunchOpts.value.toArray, + new java.util.ArrayList() ) } catch { case e: java.lang.reflect.InvocationTargetException => throw e.getCause } } diff --git a/scripted/sbt/src/main/scala/sbt/test/BatchScriptRunner.scala b/scripted/sbt/src/main/scala/sbt/test/BatchScriptRunner.scala new file mode 100644 index 000000000..d181a2ac1 --- /dev/null +++ b/scripted/sbt/src/main/scala/sbt/test/BatchScriptRunner.scala @@ -0,0 +1,58 @@ +package sbt +package test + +import sbt.internal.scripted._ +import sbt.test.BatchScriptRunner.States + +/** Defines an alternative script runner that allows batch execution. */ +private[sbt] class BatchScriptRunner extends ScriptRunner { + + /** Defines a method to run batched execution. + * + * @param statements The list of handlers and statements. + * @param states The states of the runner. In case it's empty, inherited apply is called. + */ + def apply(statements: List[(StatementHandler, Statement)], states: States): Unit = { + if (states.isEmpty) super.apply(statements) + else statements.foreach(st => processStatement(st._1, st._2, states)) + } + + def initStates(states: States, handlers: Seq[StatementHandler]): Unit = + handlers.foreach(handler => states(handler) = handler.initialState) + + def cleanUpHandlers(handlers: Seq[StatementHandler], states: States): Unit = { + for (handler <- handlers; state <- states.get(handler)) { + try handler.finish(state.asInstanceOf[handler.State]) + catch { case _: Exception => () } + } + } + + def processStatement(handler: StatementHandler, statement: Statement, states: States): Unit = { + val state = states(handler).asInstanceOf[handler.State] + val nextState = + try { Right(handler(statement.command, statement.arguments, state)) } catch { + case e: Exception => Left(e) + } + nextState match { + case Left(err) => + if (statement.successExpected) { + err match { + case t: TestFailed => + throw new TestException(statement, "Command failed: " + t.getMessage, null) + case _ => throw new TestException(statement, "Command failed", err) + } + } else + () + case Right(s) => + if (statement.successExpected) + states(handler) = s + else + throw new TestException(statement, "Command succeeded but failure was expected", null) + } + } +} + +private[sbt] object BatchScriptRunner { + import scala.collection.mutable + type States = mutable.HashMap[StatementHandler, Any] +} diff --git a/scripted/sbt/src/main/scala/sbt/test/ScriptedTests.scala b/scripted/sbt/src/main/scala/sbt/test/ScriptedTests.scala index c72e41c30..7502a8ae3 100644 --- a/scripted/sbt/src/main/scala/sbt/test/ScriptedTests.scala +++ b/scripted/sbt/src/main/scala/sbt/test/ScriptedTests.scala @@ -8,120 +8,285 @@ package test import java.io.File import scala.util.control.NonFatal - -import sbt.internal.scripted.{ - CommentHandler, - FileCommands, - ScriptRunner, - TestScriptParser, - TestException -} -import sbt.io.{ DirectoryFilter, HiddenFileFilter } +import sbt.internal.scripted._ +import sbt.io.{ DirectoryFilter, HiddenFileFilter, IO } import sbt.io.IO.wrapNull +import sbt.io.FileFilter._ import sbt.internal.io.Resources - import sbt.internal.util.{ BufferedLogger, ConsoleLogger, FullLogger } import sbt.util.{ AbstractLogger, Logger } +import scala.collection.mutable +import scala.collection.parallel.ForkJoinTaskSupport +import scala.collection.parallel.mutable.ParSeq + final class ScriptedTests(resourceBaseDirectory: File, bufferLog: Boolean, launcher: File, launchOpts: Seq[String]) { + import sbt.io.syntax._ import ScriptedTests._ private val testResources = new Resources(resourceBaseDirectory) val ScriptFilename = "test" val PendingScriptFilename = "pending" - def scriptedTest(group: String, name: String, log: xsbti.Logger): Seq[() => Option[String]] = + def scriptedTest(group: String, name: String, log: xsbti.Logger): Seq[TestRunner] = scriptedTest(group, name, Logger.xlog2Log(log)) - def scriptedTest(group: String, name: String, log: Logger): Seq[() => Option[String]] = - scriptedTest(group, name, emptyCallback, log) - def scriptedTest(group: String, - name: String, - prescripted: File => Unit, - log: Logger): Seq[() => Option[String]] = { - import sbt.io.syntax._ + def scriptedTest(group: String, name: String, log: Logger): Seq[TestRunner] = + singleScriptedTest(group, name, emptyCallback, log) + + /** Returns a sequence of test runners that have to be applied in the call site. */ + def singleScriptedTest(group: String, + name: String, + prescripted: File => Unit, + log: Logger): Seq[TestRunner] = { + + // Test group and names may be file filters (like '*') for (groupDir <- (resourceBaseDirectory * group).get; nme <- (groupDir * name).get) yield { val g = groupDir.getName val n = nme.getName - val str = s"$g / $n" + val label = s"$g / $n" () => { - println("Running " + str) - testResources.readWriteResourceDirectory(g, n) { testDirectory => - val disabled = new File(testDirectory, "disabled").isFile - if (disabled) { - log.info("D " + str + " [DISABLED]") - None - } else { - try { scriptedTest(str, testDirectory, prescripted, log); None } catch { - case _: TestException | _: PendingTestSuccessException => Some(str) - } + println(s"Running $label") + val result = testResources.readWriteResourceDirectory(g, n) { testDirectory => + val buffer = new BufferedLogger(new FullLogger(log)) + val singleTestRunner = () => { + val handlers = createScriptedHandlers(testDirectory, buffer) + val runner = new BatchScriptRunner + val states = new mutable.HashMap[StatementHandler, Any]() + commonRunTest(label, testDirectory, prescripted, handlers, runner, states, buffer) } + runOrHandleDisabled(label, testDirectory, singleTestRunner, buffer) } + Seq(result) } } } - private def scriptedTest(label: String, - testDirectory: File, - prescripted: File => Unit, - log: Logger): Unit = { - val buffered = new BufferedLogger(new FullLogger(log)) - if (bufferLog) - buffered.record() + private def createScriptedHandlers(testDir: File, + buffered: Logger): Map[Char, StatementHandler] = { + val fileHandler = new FileCommands(testDir) + val sbtHandler = new SbtHandler(testDir, launcher, buffered, launchOpts) + Map('$' -> fileHandler, '>' -> sbtHandler, '#' -> CommentHandler) + } - def createParser() = { - val fileHandler = new FileCommands(testDirectory) - val sbtHandler = new SbtHandler(testDirectory, launcher, buffered, launchOpts) - new TestScriptParser(Map('$' -> fileHandler, '>' -> sbtHandler, '#' -> CommentHandler)) + /** Returns a sequence of test runners that have to be applied in the call site. */ + def batchScriptedRunner( + testGroupAndNames: Seq[(String, String)], + prescripted: File => Unit, + sbtInstances: Int, + log: Logger + ): Seq[TestRunner] = { + // Test group and names may be file filters (like '*') + val groupAndNameDirs = { + for { + (group, name) <- testGroupAndNames + groupDir <- resourceBaseDirectory.*(group).get + testDir <- groupDir.*(name).get + } yield (groupDir, testDir) } + + val labelsAndDirs = groupAndNameDirs.map { + case (groupDir, nameDir) => + val groupName = groupDir.getName + val testName = nameDir.getName + val testDirectory = testResources.readOnlyResourceDirectory(groupName, testName) + (groupName, testName) -> testDirectory + } + + if (labelsAndDirs.isEmpty) List() + else { + val batchSeed = labelsAndDirs.size / sbtInstances + val batchSize = if (batchSeed == 0) labelsAndDirs.size else batchSeed + labelsAndDirs + .grouped(batchSize) + .map(batch => () => IO.withTemporaryDirectory(runBatchedTests(batch, _, prescripted, log))) + .toList + } + } + + /** Defines an auto plugin that is injected to sbt between every scripted session. + * + * It sets the name of the local root project for those tests run in batch mode. + * + * This is necessary because the current design to run tests in batch mode forces + * scripted tests to share one common sbt dir instead of each one having its own. + * + * Sbt extracts the local root project name from the directory name. So those + * scripted tests that don't set the name for the root and whose test files check + * information based on the name will fail. + * + * The reason why we set the name here and not via `set` is because some tests + * dump the session to check that their settings have been correctly applied. + * + * @param testName The test name used to extract the root project name. + * @return A string-based implementation to run between every reload. + */ + private def createAutoPlugin(testName: String) = + s""" + |import sbt._, Keys._ + |object InstrumentScripted extends AutoPlugin { + | override def trigger = AllRequirements + | override def globalSettings: Seq[Setting[_]] = + | Seq(commands += setUpScripted) ++ super.globalSettings + | + | def setUpScripted = Command.command("setUpScripted") { (state0: State) => + | val nameScriptedSetting = name.in(LocalRootProject).:=( + | if (name.value.startsWith("sbt_")) "$testName" else name.value) + | val state1 = Project.extract(state0).append(nameScriptedSetting, state0) + | "initialize" :: state1 + | } + |} + """.stripMargin + + /** Defines the batch execution of scripted tests. + * + * Scripted tests are run one after the other one recycling the handlers, under + * the assumption that handlers do not produce side effects that can change scripted + * tests' behaviours. + * + * In batch mode, the test runner performs these operations between executions: + * + * 1. Delete previous test files in the common test directory. + * 2. Copy over next test files to the common test directory. + * 3. Reload the sbt handler. + * + * @param groupedTests The labels and directories of the tests to run. + * @param tempTestDir The common test directory. + * @param preHook The hook to run before scripted execution. + * @param log The logger. + */ + private def runBatchedTests( + groupedTests: Seq[((String, String), File)], + tempTestDir: File, + preHook: File => Unit, + log: Logger + ): Seq[Option[String]] = { + + val runner = new BatchScriptRunner + val buffer = new BufferedLogger(new FullLogger(log)) + val handlers = createScriptedHandlers(tempTestDir, buffer) + val states = new BatchScriptRunner.States + val seqHandlers = handlers.values.toList + runner.initStates(states, seqHandlers) + + def runBatchTests = { + groupedTests.map { + case ((group, name), originalDir) => + val label = s"$group / $name" + println(s"Running $label") + // Copy test's contents and reload the sbt instance to pick them up + IO.copyDirectory(originalDir, tempTestDir) + + val runTest = () => { + // Reload and initialize (to reload contents of .sbtrc files) + val pluginImplementation = createAutoPlugin(name) + IO.write(tempTestDir / "project" / "InstrumentScripted.scala", pluginImplementation) + val sbtHandlerError = "Missing sbt handler. Scripted is misconfigured." + val sbtHandler = handlers.getOrElse('>', sbtHandlerError).asInstanceOf[SbtHandler] + val commandsToRun = ";reload;setUpScripted" + val statement = Statement(commandsToRun, Nil, successExpected = true, line = -1) + + // Run reload inside the hook to reuse error handling for pending tests + val wrapHook = (file: File) => { + preHook(file) + try runner.processStatement(sbtHandler, statement, states) + catch { + case t: Throwable => + val newMsg = "Reload for scripted batch execution failed." + throw new TestException(statement, newMsg, t) + } + } + + commonRunTest(label, tempTestDir, wrapHook, handlers, runner, states, buffer) + } + + // Run the test and delete files (except global that holds local scala jars) + val result = runOrHandleDisabled(label, tempTestDir, runTest, buffer) + IO.delete(tempTestDir.*("*" -- "global").get) + result + } + } + + try runBatchTests + finally runner.cleanUpHandlers(seqHandlers, states) + } + + private def runOrHandleDisabled( + label: String, + testDirectory: File, + runTest: () => Option[String], + log: Logger + ): Option[String] = { + val existsDisabled = new File(testDirectory, "disabled").isFile + if (!existsDisabled) runTest() + else { + log.info(s"D $label [DISABLED]") + None + } + } + + private val PendingLabel = "[PENDING]" + + private def commonRunTest( + label: String, + testDirectory: File, + preScriptedHook: File => Unit, + createHandlers: Map[Char, StatementHandler], + runner: BatchScriptRunner, + states: BatchScriptRunner.States, + log: BufferedLogger + ): Option[String] = { + if (bufferLog) log.record() + val (file, pending) = { val normal = new File(testDirectory, ScriptFilename) val pending = new File(testDirectory, PendingScriptFilename) if (pending.isFile) (pending, true) else (normal, false) } - val pendingString = if (pending) " [PENDING]" else "" - def runTest() = { - val run = new ScriptRunner - val parser = createParser() - run(parser.parse(file)) - } - def testFailed(): Unit = { - if (pending) buffered.clear() else buffered.stop() - buffered.error("x " + label + pendingString) - } - - try { - prescripted(testDirectory) - runTest() - buffered.info("+ " + label + pendingString) - if (pending) throw new PendingTestSuccessException(label) - } catch { - case e: TestException => - testFailed() - e.getCause match { - case null | _: java.net.SocketException => buffered.error(" " + e.getMessage) - case _ => e.printStackTrace + val pendingMark = if (pending) PendingLabel else "" + def testFailed(t: Throwable): Option[String] = { + if (pending) log.clear() else log.stop() + log.error(s"x $label $pendingMark") + if (!NonFatal(t)) throw t // We make sure fatal errors are rethrown + if (t.isInstanceOf[TestException]) { + t.getCause match { + case null | _: java.net.SocketException => + log.error(" Cause of test exception: " + t.getMessage) + case _ => t.printStackTrace() } - if (!pending) throw e - case e: PendingTestSuccessException => - testFailed() - buffered.error(" Mark as passing to remove this failure.") - throw e - case NonFatal(e) => - testFailed() - if (!pending) throw e - } finally { buffered.clear() } + } + if (pending) None else Some(label) + } + + import scala.util.control.Exception.catching + catching(classOf[TestException]).withApply(testFailed).andFinally(log.clear).apply { + preScriptedHook(testDirectory) + val handlers = createHandlers + val parser = new TestScriptParser(handlers) + val handlersAndStatements = parser.parse(file) + runner.apply(handlersAndStatements, states) + + // Handle successful tests + log.info(s"+ $label $pendingMark") + if (pending) { + log.clear() + log.error(" Pending test passed. Mark as passing to remove this failure.") + Some(label) + } else None + } } } object ScriptedTests extends ScriptedRunner { - val emptyCallback: File => Unit = { _ => - () - } + + /** Represents the function that runs the scripted tests, both in single or batch mode. */ + type TestRunner = () => Seq[Option[String]] + + val emptyCallback: File => Unit = _ => () def main(args: Array[String]): Unit = { val directory = new File(args(0)) val buffer = args(1).toBoolean @@ -136,22 +301,6 @@ object ScriptedTests extends ScriptedRunner { } class ScriptedRunner { - import ScriptedTests._ - - @deprecated("No longer used", "0.13.9") - def run(resourceBaseDirectory: File, - bufferLog: Boolean, - tests: Array[String], - bootProperties: File, - launchOpts: Array[String]): Unit = - run(resourceBaseDirectory, - bufferLog, - tests, - ConsoleLogger(), - bootProperties, - launchOpts, - emptyCallback) //new FullLogger(Logger.xlog2Log(log))) - // This is called by project/Scripted.scala // Using java.util.List[File] to encode File => Unit def run(resourceBaseDirectory: File, @@ -165,30 +314,6 @@ class ScriptedRunner { prescripted.add(f); () }) //new FullLogger(Logger.xlog2Log(log))) - @deprecated("No longer used", "0.13.9") - def run(resourceBaseDirectory: File, - bufferLog: Boolean, - tests: Array[String], - bootProperties: File, - launchOpts: Array[String], - prescripted: File => Unit): Unit = - run(resourceBaseDirectory, - bufferLog, - tests, - ConsoleLogger(), - bootProperties, - launchOpts, - prescripted) - - @deprecated("No longer used", "0.13.9") - def run(resourceBaseDirectory: File, - bufferLog: Boolean, - tests: Array[String], - logger: AbstractLogger, - bootProperties: File, - launchOpts: Array[String]): Unit = - run(resourceBaseDirectory, bufferLog, tests, logger, bootProperties, launchOpts, emptyCallback) - def run(resourceBaseDirectory: File, bufferLog: Boolean, tests: Array[String], @@ -199,22 +324,65 @@ class ScriptedRunner { val runner = new ScriptedTests(resourceBaseDirectory, bufferLog, bootProperties, launchOpts) val allTests = get(tests, resourceBaseDirectory, logger) flatMap { case ScriptedTest(group, name) => - runner.scriptedTest(group, name, prescripted, logger) + runner.singleScriptedTest(group, name, prescripted, logger) } runAll(allTests) } - def runAll(tests: Seq[() => Option[String]]): Unit = { - val errors = for (test <- tests; err <- test()) yield err - if (errors.nonEmpty) - sys.error(errors.mkString("Failed tests:\n\t", "\n\t", "\n")) + def runInParallel(resourceBaseDirectory: File, + bufferLog: Boolean, + tests: Array[String], + bootProperties: File, + launchOpts: Array[String], + prescripted: java.util.List[File]): Unit = { + val logger = ConsoleLogger() + val addTestFile = (f: File) => { prescripted.add(f); () } + runInParallel(resourceBaseDirectory, + bufferLog, + tests, + logger, + bootProperties, + launchOpts, + addTestFile, + 1) + } + + def runInParallel( + resourceBaseDirectory: File, + bufferLog: Boolean, + tests: Array[String], + logger: AbstractLogger, + bootProperties: File, + launchOpts: Array[String], + prescripted: File => Unit, + instances: Int + ): Unit = { + val runner = new ScriptedTests(resourceBaseDirectory, bufferLog, bootProperties, launchOpts) + // The scripted tests mapped to the inputs that the user wrote after `scripted`. + val scriptedTests = get(tests, resourceBaseDirectory, logger).map(st => (st.group, st.name)) + val scriptedRunners = runner.batchScriptedRunner(scriptedTests, prescripted, instances, logger) + val parallelRunners = scriptedRunners.toParArray + val pool = new java.util.concurrent.ForkJoinPool(instances) + parallelRunners.tasksupport = new ForkJoinTaskSupport(pool) + runAllInParallel(parallelRunners) + } + + private def reportErrors(errors: Seq[String]): Unit = + if (errors.nonEmpty) sys.error(errors.mkString("Failed tests:\n\t", "\n\t", "\n")) else () + + def runAll(toRun: Seq[ScriptedTests.TestRunner]): Unit = + reportErrors(toRun.flatMap(test => test.apply().flatten.toSeq)) + + // We cannot reuse `runAll` because parallel collections != collections + def runAllInParallel(tests: ParSeq[ScriptedTests.TestRunner]): Unit = { + reportErrors(tests.flatMap(test => test.apply().flatten.toSeq).toList) } def get(tests: Seq[String], baseDirectory: File, log: Logger): Seq[ScriptedTest] = if (tests.isEmpty) listTests(baseDirectory, log) else parseTests(tests) def listTests(baseDirectory: File, log: Logger): Seq[ScriptedTest] = - (new ListTests(baseDirectory, _ => true, log)).listTests + new ListTests(baseDirectory, _ => true, log).listTests def parseTests(in: Seq[String]): Seq[ScriptedTest] = for (testString <- in) yield { @@ -260,18 +428,6 @@ private[test] final class ListTests(baseDirectory: File, } } -object CompatibilityLevel extends Enumeration { - val Full, Basic, Minimal, Minimal27, Minimal28 = Value - - def defaultVersions(level: Value) = - level match { - case Full => "2.7.4 2.7.7 2.9.0.RC1 2.8.0 2.8.1" - case Basic => "2.7.7 2.7.4 2.8.1 2.8.0" - case Minimal => "2.7.7 2.8.1" - case Minimal27 => "2.7.7" - case Minimal28 => "2.8.1" - } -} class PendingTestSuccessException(label: String) extends Exception { override def getMessage: String = s"The pending test $label succeeded. Mark this test as passing to remove this failure."