From 07e0f2ebdef48ef457183426c5bd1b59e98e7a81 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Mon, 22 Jan 2024 14:55:35 +0100 Subject: [PATCH] Filter examples based on TestSelector (#1217) 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 | 96 +++++++++++++++++++ .../scala/org/specs2/runner/SbtRunner.scala | 15 ++- 2 files changed, 109 insertions(+), 2 deletions(-) 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 0000000000..4c14e64b53 --- /dev/null +++ b/core/jvm/src/test/scala/org/specs2/runner/SbtSelectorSpec.scala @@ -0,0 +1,96 @@ +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 067e8f97c0..86cdc84aac 100644 --- a/core/shared/src/main/scala/org/specs2/runner/SbtRunner.scala +++ b/core/shared/src/main/scala/org/specs2/runner/SbtRunner.scala @@ -19,6 +19,8 @@ import org.specs2.concurrent.awaitResult import org.specs2.control.ExecuteActions._ import org.specs2.data.NamedTag +import java.util.regex.Pattern + import scala.concurrent.duration.Duration import scala.concurrent.{Future} @@ -35,8 +37,17 @@ abstract class BaseSbtRunner(args: Array[String], remoteArgs: Array[String], loa taskDefs.toList.map(newTask).toArray /** create a new test task */ - def newTask(aTaskDef: TaskDef): Task = - SbtTask(aTaskDef, env, loader) + def newTask(aTaskDef: TaskDef): Task = { + 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) + } def done() = { env.shutdown