diff --git a/main-actions/src/main/scala/sbt/ForkTests.scala b/main-actions/src/main/scala/sbt/ForkTests.scala index f83bd71bb..8f6b030c5 100755 --- a/main-actions/src/main/scala/sbt/ForkTests.scala +++ b/main-actions/src/main/scala/sbt/ForkTests.scala @@ -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, diff --git a/main-actions/src/main/scala/sbt/Tests.scala b/main-actions/src/main/scala/sbt/Tests.scala index aac68dc55..c6f3397c6 100644 --- a/main-actions/src/main/scala/sbt/Tests.scala +++ b/main-actions/src/main/scala/sbt/Tests.scala @@ -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], diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 71291cc06..11dbc1bd0 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -1030,7 +1030,14 @@ object Defaults extends BuildCommon { val tests = definedTests.value val fk = fork.value val opts = forkOptions.value - Seq(new Tests.Group("", tests, if (fk) Tests.SubProcess(opts) else Tests.InProcess)) + Seq( + new Tests.Group( + "", + 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 => diff --git a/sbt/src/sbt-test/tests/fork-test-group-parallel-custom-tags/build.sbt b/sbt/src/sbt-test/tests/fork-test-group-parallel-custom-tags/build.sbt new file mode 100644 index 000000000..47898b625 --- /dev/null +++ b/sbt/src/sbt-test/tests/fork-test-group-parallel-custom-tags/build.sbt @@ -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 +)) diff --git a/sbt/src/sbt-test/tests/fork-test-group-parallel-custom-tags/src/test/scala/example/Test.scala b/sbt/src/sbt-test/tests/fork-test-group-parallel-custom-tags/src/test/scala/example/Test.scala new file mode 100644 index 000000000..8222e3afe --- /dev/null +++ b/sbt/src/sbt-test/tests/fork-test-group-parallel-custom-tags/src/test/scala/example/Test.scala @@ -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" +} diff --git a/sbt/src/sbt-test/tests/fork-test-group-parallel-custom-tags/test b/sbt/src/sbt-test/tests/fork-test-group-parallel-custom-tags/test new file mode 100644 index 000000000..c3fa9262d --- /dev/null +++ b/sbt/src/sbt-test/tests/fork-test-group-parallel-custom-tags/test @@ -0,0 +1,3 @@ +> test +> set concurrentRestrictions in Global := Seq(Tags.limitAll(4)) +> testFailure