Merge pull request #5527 from frosforever/tag-fork-groups

Add user defined tags to Tests.Group to allow for better concurrency control
This commit is contained in:
eugene yokota 2020-05-04 11:50:47 -04:00 committed by GitHub
commit 5d97d643b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 231 additions and 30 deletions

View File

@ -28,7 +28,7 @@ private[sbt] object ForkTests {
classpath: Seq[File],
fork: ForkOptions,
log: Logger,
tag: Tag
tags: (Tag, Int)*
): Task[TestOutput] = {
val opts = processOptions(config, tests, log)
@ -41,11 +41,23 @@ private[sbt] object ForkTests {
constant(TestOutput(TestResult.Passed, Map.empty[String, SuiteResult], Iterable.empty))
else
mainTestTask(runners, opts, classpath, fork, log, config.parallel).tagw(config.tags: _*)
main.tag(tag).dependsOn(all(opts.setup): _*) flatMap { results =>
main.tagw(tags: _*).dependsOn(all(opts.setup): _*) flatMap { results =>
all(opts.cleanup).join.map(_ => results)
}
}
def apply(
runners: Map[TestFramework, Runner],
tests: Vector[TestDefinition],
config: Execution,
classpath: Seq[File],
fork: ForkOptions,
log: Logger,
tag: Tag
): Task[TestOutput] = {
apply(runners, tests, config, classpath, fork, log, tag -> 1)
}
private[this] def mainTestTask(
runners: Map[TestFramework, Runner],
opts: ProcessedOptions,

View File

@ -16,24 +16,25 @@ import xsbti.api.Definition
import xsbti.api.ClassLike
import xsbti.compile.CompileAnalysis
import ConcurrentRestrictions.Tag
import testing.{
AnnotatedFingerprint,
Fingerprint,
Framework,
SubclassFingerprint,
Runner,
TaskDef,
Selector,
SubclassFingerprint,
SuiteSelector,
TaskDef,
Task => TestTask
}
import scala.annotation.tailrec
import scala.annotation.tailrec
import sbt.internal.util.ManagedLogger
import sbt.util.Logger
import sbt.protocol.testing.TestResult
import scala.runtime.AbstractFunction3
sealed trait TestOption
object Tests {
@ -133,7 +134,113 @@ object Tests {
final case class SubProcess(config: ForkOptions) extends TestRunPolicy
/** A named group of tests configured to run in the same JVM or be forked. */
final case class Group(name: String, tests: Seq[TestDefinition], runPolicy: TestRunPolicy)
final class Group(
val name: String,
val tests: Seq[TestDefinition],
val runPolicy: TestRunPolicy,
val tags: Seq[(Tag, Int)]
) extends Product
with Serializable {
def this(name: String, tests: Seq[TestDefinition], runPolicy: TestRunPolicy) = {
this(name, tests, runPolicy, Seq.empty)
}
def withName(name: String): Group = {
new Group(name, tests, runPolicy, tags)
}
def withTests(tests: Seq[TestDefinition]): Group = {
new Group(name, tests, runPolicy, tags)
}
def withRunPolicy(runPolicy: TestRunPolicy): Group = {
new Group(name, tests, runPolicy, tags)
}
def withTags(tags: Seq[(Tag, Int)]): Group = {
new Group(name, tests, runPolicy, tags)
}
//- EXPANDED CASE CLASS METHOD BEGIN -//
@deprecated("Methods generated for case class will be removed in the future.", "1.4.0")
def copy(
name: String = this.name,
tests: Seq[TestDefinition] = this.tests,
runPolicy: TestRunPolicy = this.runPolicy
): Group = {
new Group(name, tests, runPolicy, this.tags)
}
@deprecated("Methods generated for case class will be removed in the future.", "1.4.0")
override def productElement(x$1: Int): Any = x$1 match {
case 0 => Group.this.name
case 1 => Group.this.tests
case 2 => Group.this.runPolicy
case 3 => Group.this.tags
}
@deprecated("Methods generated for case class will be removed in the future.", "1.4.0")
override def productArity: Int = 4
@deprecated("Methods generated for case class will be removed in the future.", "1.4.0")
def canEqual(x$1: Any): Boolean = x$1.isInstanceOf[Group]
override def hashCode(): Int = {
scala.runtime.ScalaRunTime._hashCode(Group.this)
}
override def toString(): String = scala.runtime.ScalaRunTime._toString(Group.this)
override def equals(x$1: Any): Boolean = {
this.eq(x$1.asInstanceOf[Object]) || (x$1.isInstanceOf[Group] && ({
val Group$1: Group = x$1.asInstanceOf[Group]
name == Group$1.name && tests == Group$1.tests &&
runPolicy == Group$1.runPolicy && tags == Group$1.tags
}))
}
//- EXPANDED CASE CLASS METHOD END -//
}
object Group
extends AbstractFunction3[String, Seq[TestDefinition], TestRunPolicy, Group]
with Serializable {
//- EXPANDED CASE CLASS METHOD BEGIN -//
final override def toString(): String = "Group"
def apply(
name: String,
tests: Seq[TestDefinition],
runPolicy: TestRunPolicy
): Group = {
new Group(name, tests, runPolicy, Seq.empty)
}
def apply(
name: String,
tests: Seq[TestDefinition],
runPolicy: TestRunPolicy,
tags: Seq[(Tag, Int)]
): Group = {
new Group(name, tests, runPolicy, tags)
}
@deprecated("Methods generated for case class will be removed in the future.", "1.4.0")
def unapply(
x$0: Group
): Option[(String, Seq[TestDefinition], TestRunPolicy)] = {
if (x$0 == null) None
else
Some.apply[(String, Seq[TestDefinition], TestRunPolicy)](
Tuple3.apply[String, Seq[TestDefinition], TestRunPolicy](
x$0.name,
x$0.tests,
x$0.runPolicy
)
)
}
private def readResolve(): Object = Group
//- EXPANDED CASE CLASS METHOD END -//
}
private[sbt] final class ProcessedOptions(
val tests: Vector[TestDefinition],

View File

@ -1030,7 +1030,14 @@ object Defaults extends BuildCommon {
val tests = definedTests.value
val fk = fork.value
val opts = forkOptions.value
Seq(new Tests.Group("<default>", tests, if (fk) Tests.SubProcess(opts) else Tests.InProcess))
Seq(
new Tests.Group(
"<default>",
tests,
if (fk) Tests.SubProcess(opts) else Tests.InProcess,
Seq.empty
)
)
}
def forkOptionsTask: Initialize[Task[ForkOptions]] =
Def.task {
@ -1207,28 +1214,34 @@ object Defaults extends BuildCommon {
projectId: String
): Initialize[Task[Tests.Output]] = {
val runners = createTestRunners(frameworks, loader, config)
val groupTasks = groups map {
case Tests.Group(_, tests, runPolicy) =>
runPolicy match {
case Tests.SubProcess(opts) =>
s.log.debug(s"javaOptions: ${opts.runJVMOptions}")
val forkedConfig = config.copy(parallel = config.parallel && forkedParallelExecution)
s.log.debug(s"Forking tests - parallelism = ${forkedConfig.parallel}")
ForkTests(
runners,
tests.toVector,
forkedConfig,
cp.files,
opts,
s.log,
Tags.ForkedTestGroup
)
case Tests.InProcess =>
if (javaOptions.nonEmpty) {
s.log.warn("javaOptions will be ignored, fork is set to false")
}
Tests(frameworks, loader, runners, tests.toVector, config, s.log)
}
val groupTasks = groups map { group =>
group.runPolicy match {
case Tests.SubProcess(opts) =>
s.log.debug(s"javaOptions: ${opts.runJVMOptions}")
val forkedConfig = config.copy(parallel = config.parallel && forkedParallelExecution)
s.log.debug(s"Forking tests - parallelism = ${forkedConfig.parallel}")
ForkTests(
runners,
group.tests.toVector,
forkedConfig,
cp.files,
opts,
s.log,
(Tags.ForkedTestGroup, 1) +: group.tags: _*
)
case Tests.InProcess =>
if (javaOptions.nonEmpty) {
s.log.warn("javaOptions will be ignored, fork is set to false")
}
Tests(
frameworks,
loader,
runners,
group.tests.toVector,
config.copy(tags = config.tags ++ group.tags),
s.log
)
}
}
val output = Tests.foldTasks(groupTasks, config.parallel)
val result = output map { out =>

View File

@ -0,0 +1,33 @@
val specs = "org.specs2" %% "specs2-core" % "4.3.4"
ThisBuild / scalaVersion := "2.12.11"
val TestATypeTag = Tags.Tag("TestA")
val TestBTypeTag = Tags.Tag("TestB")
Global / concurrentRestrictions := Seq(Tags.limit(TestATypeTag, 1), Tags.limit(TestBTypeTag, 1))
libraryDependencies += specs % Test
inConfig(Test)(Seq(
testGrouping := {
val home = javaHome.value
val strategy = outputStrategy.value
val baseDir = baseDirectory.value
val options = javaOptions.value
val connect = connectInput.value
val vars = envVars.value
definedTests.value.map { test => new Tests.Group(test.name, Seq(test), Tests.SubProcess(
ForkOptions(
javaHome = home,
outputStrategy = strategy,
bootJars = Vector(),
workingDirectory = Some(baseDir),
runJVMOptions = options.toVector,
connectInput = connect,
envVars = vars
)
), Seq((if (test.name.contains("TestA")) TestATypeTag else TestBTypeTag) -> 1))
}
},
TaskKey[Unit]("test-failure") := test.failure.value
))

View File

@ -0,0 +1,33 @@
package example
import java.io.File
import org.specs2.mutable.Specification
trait Test extends Specification {
def testType: String
"spec" should {
"be run one at time" in {
val lock = new File(s"lock${testType}")
println(lock)
lock.mkdir() should beTrue
Thread.sleep(2000)
lock.delete() should beTrue
}
}
}
class TestA0 extends Test {
override def testType: String = "A"
}
class TestA1 extends Test {
override def testType: String = "A"
}
class TestB0 extends Test {
override def testType: String = "B"
}
class TestB1 extends Test {
override def testType: String = "B"
}

View File

@ -0,0 +1,3 @@
> test
> set concurrentRestrictions in Global := Seq(Tags.limitAll(4))
> testFailure