From d0f255dfe529a7553c69de3a42a84a1faa135df3 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Mon, 22 Jan 2024 11:43:24 +0100 Subject: [PATCH] Filter examples based on TestSelector (#1216) Previously, the Specs2 runner would ignore the selectors that are passed via a `TaskDef`. With this patch, when a `TaskDef`'s selectors contains only `TestSelector`s, then only the examples whose name match the input selectors will be executed. --- .../org/specs2/runner/SbtSelectorSpec.scala | 84 +++++++++++++++++++ .../scala/org/specs2/runner/SbtRunner.scala | 12 ++- 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 core/jvm/src/test/scala/org/specs2/runner/SbtSelectorSpec.scala diff --git a/core/jvm/src/test/scala/org/specs2/runner/SbtSelectorSpec.scala b/core/jvm/src/test/scala/org/specs2/runner/SbtSelectorSpec.scala new file mode 100644 index 000000000..8108dd100 --- /dev/null +++ b/core/jvm/src/test/scala/org/specs2/runner/SbtSelectorSpec.scala @@ -0,0 +1,84 @@ +package org.specs2 +package runner + +import sbt.testing.{Event, EventHandler, Logger, Selector, Status, SuiteSelector, TaskDef, TestSelector} + +import scala.collection.mutable.ArrayBuffer + +class SbtSelectorSpec extends Specification: + def is = s2""" + + An sbt runner executes the examples passed in the `TestSelector`s + when there is a single `TestSelector` $singleTestSelector + when there are 2 `TestSelector`s $twoTestSelectors + run everything if there are other selectors $otherSelectors + run nothing if there are no matches $noMatches + regexes in test selectors are escaped $regexesAreEscaped +""" + + private def singleTestSelector = + val wholeSuiteEvents = runWithSelectors(new SuiteSelector :: Nil) + wholeSuiteEvents.length === 3 + val failEvents = wholeSuiteEvents.filter(_.status == Status.Failure) + val singleExampleEvents = runWithSelectors(failEvents.map(_.selector())) + + (failEvents must haveSize(1)) and + (testName(failEvents.head) === "has a failing test") and + (singleExampleEvents must haveSize(1)) and + (singleExampleEvents.head.status() === Status.Failure) + + private def twoTestSelectors = + val events = runWithSelectors( + List(new TestSelector("has a successful test"), new TestSelector("has a failing test")) + ) + (events must haveSize(2)) and + (events.map(testName) must contain("has a successful test", "has a failing test")) + + private def otherSelectors = + val events = runWithSelectors(List(new SuiteSelector, new TestSelector("hello"))) + (events must haveSize(3)) and + (events.map(testName) must contain("has a successful test", "has a failing test", ".*")) + + private def noMatches = + val events = runWithSelectors(List(new TestSelector("won't match anything"))) + events must beEmpty + + private def regexesAreEscaped = + val events = runWithSelectors(new TestSelector(".*") :: Nil) + (events must haveSize(1)) and + (events.head.status() === Status.Success) and + (testName(events.head) === ".*") + + private def runWithSelectors(selectors: List[Selector]): List[Event] = + val loggers = Array(NoLogger: Logger) + val events = ArrayBuffer.empty[Event] + val handler: EventHandler = (e: Event) => events.append(e) + val framework = new Specs2Framework() + val runner = framework.runner(Array.empty, Array.empty, getClass.getClassLoader) + val fqcn = classOf[HelperSpec].getName + + val taskDef = new TaskDef(fqcn, Fingerprints.fp1m, true, selectors.toArray) + val tasks = runner.tasks(Array(taskDef)) + tasks.foreach(_.execute(handler, loggers)) + + events.toList + + private def testName(event: Event): String = + event.selector() match + case ts: TestSelector => ts.testName() + +private class HelperSpec extends Specification: + def is = s2""" + The helper spec + has a successful test $ok + has a failing test $ko + .* $ok +""" + +private object NoLogger extends Logger: + override def ansiCodesSupported(): Boolean = false + override def error(msg: String): Unit = () + override def warn(msg: String): Unit = () + override def info(msg: String): Unit = () + override def debug(msg: String): Unit = () + override def trace(t: Throwable): Unit = () diff --git a/core/shared/src/main/scala/org/specs2/runner/SbtRunner.scala b/core/shared/src/main/scala/org/specs2/runner/SbtRunner.scala index 30d8da41f..4922bab62 100644 --- a/core/shared/src/main/scala/org/specs2/runner/SbtRunner.scala +++ b/core/shared/src/main/scala/org/specs2/runner/SbtRunner.scala @@ -16,6 +16,9 @@ import org.specs2.fp.*, syntax.* import org.specs2.concurrent.ExecutionEnv import org.specs2.data.NamedTag import scala.util.* + +import java.util.regex.Pattern + import scala.concurrent.duration.Duration import scala.concurrent.{Await, Future, ExecutionContext} @@ -38,7 +41,14 @@ abstract class BaseSbtRunner(args: Array[String], remoteArgs: Array[String], loa /** create a new test task */ def newTask(aTaskDef: TaskDef): Task = - SbtTask(aTaskDef, env, loader, this) + val fullEnv = + if (env.arguments.select._ex.isDefined || aTaskDef.selectors().exists(!_.isInstanceOf[TestSelector])) env + else + val names = aTaskDef.selectors().toList.collect { case ts: TestSelector => Pattern.quote(ts.testName()) } + val select = env.arguments.select.copy(_ex = Some(names.mkString("|"))) + env.setArguments(env.arguments.copy(select = select)) + + SbtTask(aTaskDef, fullEnv, loader, this) def done = val result = env.shutdown()