Merge pull request #3151 from scalacenter/parallel-scripted-tests

Add parallel batch mode to scripted tests
This commit is contained in:
eugene yokota 2017-05-10 16:46:01 -04:00 committed by GitHub
commit a265600154
49 changed files with 670 additions and 245 deletions

View File

@ -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

View File

@ -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"}""")
},

View File

@ -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

View File

@ -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 }
}
}

View File

@ -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}""")
}

View File

@ -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 = ()
}
}
}

View File

@ -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
}
val taskCancelStrategy = sbt.Keys.taskCancelStrategy
}

View File

@ -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")
}

View File

@ -0,0 +1,4 @@
// This is necessary because tests are run in batch mode
object CompileState {
@volatile var previousIterations: Int = -1
}

View File

@ -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

View File

@ -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")
}

View File

@ -0,0 +1,4 @@
// This is necessary because tests are run in batch mode
object CompileState {
@volatile var previousIterations: Int = -1
}

View File

@ -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

View File

@ -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")
}

View File

@ -0,0 +1,4 @@
// This is necessary because tests are run in batch mode
object CompileState {
@volatile var previousIterations: Int = -1
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -1,4 +1,5 @@
lazy val a,b = project
lazy val a = project
lazy val b = project
def now = System.currentTimeMillis

View File

@ -1,3 +1,4 @@
> reboot
> checkCount 1 0
> checkCount 1 0
> reload

View File

@ -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

View File

@ -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")
}

View File

@ -0,0 +1,4 @@
// This is necessary because tests are run in batch mode
object CompileState {
@volatile var previousIterations: Int = -1
}

View File

@ -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

View File

@ -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")
}

View File

@ -0,0 +1,4 @@
// This is necessary because tests are run in batch mode
object CompileState {
@volatile var previousIterations: Int = -1
}

View File

@ -5,6 +5,7 @@
# This also verifies that compilation does not get repeatedly triggered by a mismatch in
# paths.
> recordPreviousIterations
> compile
> compile
> checkIterations 1
> checkIterations 1

View File

@ -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

View File

@ -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")
}
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")
}

View File

@ -0,0 +1,4 @@
// This is necessary because tests are run in batch mode
object CompileState {
@volatile var previousIterations: Int = -1
}

View File

@ -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 <java.home>lib/ext/ would
# always force recompilation
> checkIterations 1
> checkIterations 1

View File

@ -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")
}

View File

@ -0,0 +1,4 @@
// This is necessary because tests are run in batch mode
object CompileState {
@volatile var previousIterations: Int = -1
}

View File

@ -1,4 +1,5 @@
# 1 iteration from initial full compile
> recordPreviousIterations
> run
$ copy-file changes/A2.java A.java

View File

@ -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")
}

View File

@ -0,0 +1,4 @@
// This is necessary because tests are run in batch mode
object CompileState {
@volatile var previousIterations: Int = -1
}

View File

@ -1,4 +1,5 @@
# 1 iteration from initial full compile
> recordPreviousIterations
> run
$ copy-file changes/A2.scala A.scala

View File

@ -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")
}

View File

@ -0,0 +1,4 @@
// This is necessary because tests are run in batch mode
object CompileState {
@volatile var previousIterations: Int = -1
}

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,4 @@
// This is necessary because tests are run in batch mode
object CompileState {
@volatile var isNew: Boolean = false
}

View File

@ -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

View File

@ -0,0 +1,4 @@
// This is necessary because tests are run in batch mode
object CompileState {
@volatile var isNew: Boolean = false
}

View File

@ -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 }
}

View File

@ -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]
}

View File

@ -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."