mirror of https://github.com/sbt/sbt.git
Merge pull request #3151 from scalacenter/parallel-scripted-tests
Add parallel batch mode to scripted tests
This commit is contained in:
commit
a265600154
22
.travis.yml
22
.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
|
||||
|
|
|
|||
11
build.sbt
11
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"}""")
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}""")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
// This is necessary because tests are run in batch mode
|
||||
object CompileState {
|
||||
@volatile var previousIterations: Int = -1
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
// This is necessary because tests are run in batch mode
|
||||
object CompileState {
|
||||
@volatile var previousIterations: Int = -1
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
// This is necessary because tests are run in batch mode
|
||||
object CompileState {
|
||||
@volatile var previousIterations: Int = -1
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
lazy val a,b = project
|
||||
lazy val a = project
|
||||
lazy val b = project
|
||||
|
||||
def now = System.currentTimeMillis
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
> reboot
|
||||
> checkCount 1 0
|
||||
> checkCount 1 0
|
||||
> reload
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
// This is necessary because tests are run in batch mode
|
||||
object CompileState {
|
||||
@volatile var previousIterations: Int = -1
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
// This is necessary because tests are run in batch mode
|
||||
object CompileState {
|
||||
@volatile var previousIterations: Int = -1
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
// This is necessary because tests are run in batch mode
|
||||
object CompileState {
|
||||
@volatile var previousIterations: Int = -1
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
// This is necessary because tests are run in batch mode
|
||||
object CompileState {
|
||||
@volatile var previousIterations: Int = -1
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
# 1 iteration from initial full compile
|
||||
> recordPreviousIterations
|
||||
> run
|
||||
$ copy-file changes/A2.java A.java
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
// This is necessary because tests are run in batch mode
|
||||
object CompileState {
|
||||
@volatile var previousIterations: Int = -1
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
# 1 iteration from initial full compile
|
||||
> recordPreviousIterations
|
||||
> run
|
||||
$ copy-file changes/A2.scala A.scala
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
// This is necessary because tests are run in batch mode
|
||||
object CompileState {
|
||||
@volatile var previousIterations: Int = -1
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
// This is necessary because tests are run in batch mode
|
||||
object CompileState {
|
||||
@volatile var isNew: Boolean = false
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
// This is necessary because tests are run in batch mode
|
||||
object CompileState {
|
||||
@volatile var isNew: Boolean = false
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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."
|
||||
|
|
|
|||
Loading…
Reference in New Issue