From 7e7bcd48b704a85448307aeb56fdca9e26e470e4 Mon Sep 17 00:00:00 2001 From: jakobcodes Date: Mon, 2 Dec 2024 14:28:19 +0100 Subject: [PATCH 1/3] Implement method level test filtering --- .../org/scalatestplus/junit/JUnitRunner.scala | 44 +++++++++++++++---- .../junit/JUnitRunnerSuite.scala | 44 +++++++++++++++++++ 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/src/main/scala/org/scalatestplus/junit/JUnitRunner.scala b/src/main/scala/org/scalatestplus/junit/JUnitRunner.scala index 2d39b63..e21e516 100644 --- a/src/main/scala/org/scalatestplus/junit/JUnitRunner.scala +++ b/src/main/scala/org/scalatestplus/junit/JUnitRunner.scala @@ -16,12 +16,15 @@ */ package org.scalatestplus.junit -import org.scalatest._ +import org.scalatest.{Args, ConfigMap, DynaTags, Stopper, Suite, Tracker, Filter} import org.junit.runner.notification.RunNotifier import org.junit.runner.notification.Failure import org.junit.runner.Description import org.junit.runner.manipulation.{Filter => TestFilter, Filterable, NoTestsRemainException} +import scala.collection.JavaConverters._ +import scala.collection.mutable + /* I think that Stopper really should be a no-op, like it is, because the user has no way to stop it. This is wierd, because it will call nested suites. So the tests @@ -71,6 +74,8 @@ final class JUnitRunner(suiteClass: java.lang.Class[_ <: Suite]) extends org.jun */ val getDescription = createDescription(suiteToRun) + private val excludedTests: mutable.Set[String] = mutable.Set() + private def createDescription(suite: Suite): Description = { val description = Description.createSuiteDescription(suite.getClass) // If we don't add the testNames and nested suites in, we get @@ -96,12 +101,29 @@ final class JUnitRunner(suiteClass: java.lang.Class[_ <: Suite]) extends org.jun */ def run(notifier: RunNotifier): Unit = { try { + val includedTests: Set[String] = suiteToRun.testNames.diff(excludedTests) + val testTags: Map[String, Map[String, Set[String]]] = Map( + suiteToRun.suiteId -> + includedTests.map(test => test -> Set("INCLUDE")).toMap + ) + val filter = Filter( + tagsToInclude = Some(Set("INCLUDE")), + dynaTags = DynaTags(suiteTags = Map.empty, testTags = testTags) + ) // TODO: What should this Tracker be? - suiteToRun.run(None, Args(new RunNotifierReporter(notifier), - Stopper.default, Filter(), ConfigMap.empty, None, - new Tracker)) - } - catch { + suiteToRun.run( + None, + Args( + new RunNotifierReporter(notifier), + Stopper.default, + filter, + ConfigMap.empty, + None, + new Tracker, + Set.empty + ) + ) + } catch { case e: Exception => notifier.fireTestFailure(new Failure(getDescription, e)) } @@ -113,10 +135,16 @@ final class JUnitRunner(suiteClass: java.lang.Class[_ <: Suite]) extends org.jun * * @return the expected number of tests that will run when this suite is run */ - override def testCount() = suiteToRun.expectedTestCount(Filter()) + override def testCount(): Int = suiteToRun.expectedTestCount(Filter()) + @throws(classOf[NoTestsRemainException]) override def filter(filter: TestFilter): Unit = { - if (!filter.shouldRun(getDescription)) throw new NoTestsRemainException + getDescription.getChildren.asScala + .filter(child => !filter.shouldRun(child)) + .foreach(child => excludedTests.add(child.getMethodName)) + if (getDescription.getChildren.isEmpty) { + throw new NoTestsRemainException() + } } } diff --git a/src/test/scala/org/scalatestplus/junit/JUnitRunnerSuite.scala b/src/test/scala/org/scalatestplus/junit/JUnitRunnerSuite.scala index 648949f..111c37f 100644 --- a/src/test/scala/org/scalatestplus/junit/JUnitRunnerSuite.scala +++ b/src/test/scala/org/scalatestplus/junit/JUnitRunnerSuite.scala @@ -103,6 +103,30 @@ package org.scalatestplus.junit { assert(1 === 1) } } + + class MethodExclusionFilter(methodNamePattern: String) extends TestFilter { + override def shouldRun(description: Description): Boolean = { + !description.getMethodName().contains(methodNamePattern) + } + + override def describe(): String = s"Excludes tests with method names containing: '$methodNamePattern'" + } + + @RunWith(classOf[JUnitRunner]) + class MethodFilterTargetSuite extends funsuite.AnyFunSuite { + + test("JUnit ran this OK!") { + assert(1 === 1) + } + + test("should pass filter and execute") { + assert(2 === 2) + } + + test("should be filtered out") { + assert(3 === 5) + } + } } import org.junit.runner.Description @@ -112,6 +136,7 @@ package org.scalatestplus.junit { import org.scalatestplus.junit.helpers.EasySuite import org.scalatestplus.junit.helpers.KerblooeySuite import org.scalatestplus.junit.helpers.{FilteredInSuite, FilteredOutSuite, NameFilter} + import org.scalatestplus.junit.helpers.{MethodExclusionFilter, MethodFilterTargetSuite} import scala.util.Try class JUnitRunnerSuite extends funsuite.AnyFunSuite { @@ -190,5 +215,24 @@ package org.scalatestplus.junit { assert(runNotifier.ran.head.getDisplayName === "JUnit ran this OK!(org.scalatestplus.junit.helpers.FilteredInSuite)") } + + test("Should execute only methods that don't match the filter pattern") { + class ExecutedTestsNotifier extends RunNotifier { + var executedTests: List[Description] = Nil + + override def fireTestFinished(description: Description): Unit = { + executedTests = description :: executedTests + } + } + val runNotifier = new ExecutedTestsNotifier() + val runner = new JUnitRunner(classOf[MethodFilterTargetSuite]) + + val excludeMethodFilter = new MethodExclusionFilter("should be filtered out") + + runner.filter(excludeMethodFilter) + runner.run(runNotifier) + + assert(runNotifier.executedTests.size === 2) // Verifies that only 2 tests ran (one was filtered out) + } } } From 8cc845817f2e47db398c834df324e32a5f2bd03b Mon Sep 17 00:00:00 2001 From: jakobcodes Date: Mon, 2 Dec 2024 14:30:57 +0100 Subject: [PATCH 2/3] Make filter method more concise --- .../scala/org/scalatestplus/junit/JUnitRunner.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/scala/org/scalatestplus/junit/JUnitRunner.scala b/src/main/scala/org/scalatestplus/junit/JUnitRunner.scala index e21e516..0912863 100644 --- a/src/main/scala/org/scalatestplus/junit/JUnitRunner.scala +++ b/src/main/scala/org/scalatestplus/junit/JUnitRunner.scala @@ -139,12 +139,12 @@ final class JUnitRunner(suiteClass: java.lang.Class[_ <: Suite]) extends org.jun @throws(classOf[NoTestsRemainException]) override def filter(filter: TestFilter): Unit = { - getDescription.getChildren.asScala - .filter(child => !filter.shouldRun(child)) - .foreach(child => excludedTests.add(child.getMethodName)) - if (getDescription.getChildren.isEmpty) { - throw new NoTestsRemainException() - } + val children = getDescription.getChildren.asScala + excludedTests ++= children + .filterNot(filter.shouldRun) + .map(_.getMethodName) + + if (children.isEmpty) throw new NoTestsRemainException } } From f9962292ee3ea950cf1b3f62b6296cca971a5a51 Mon Sep 17 00:00:00 2001 From: MajaSt1 Date: Tue, 25 Feb 2025 16:03:31 +0100 Subject: [PATCH 3/3] Refactor test filtering: use ConcurrentHashMap.newKeySet for exclusions and tag tests with "org.scalatest.Selected" --- src/main/scala/org/scalatestplus/junit/JUnitRunner.scala | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/scala/org/scalatestplus/junit/JUnitRunner.scala b/src/main/scala/org/scalatestplus/junit/JUnitRunner.scala index 0912863..da3738e 100644 --- a/src/main/scala/org/scalatestplus/junit/JUnitRunner.scala +++ b/src/main/scala/org/scalatestplus/junit/JUnitRunner.scala @@ -22,6 +22,7 @@ import org.junit.runner.notification.Failure import org.junit.runner.Description import org.junit.runner.manipulation.{Filter => TestFilter, Filterable, NoTestsRemainException} +import java.util.concurrent.ConcurrentHashMap import scala.collection.JavaConverters._ import scala.collection.mutable @@ -74,7 +75,7 @@ final class JUnitRunner(suiteClass: java.lang.Class[_ <: Suite]) extends org.jun */ val getDescription = createDescription(suiteToRun) - private val excludedTests: mutable.Set[String] = mutable.Set() + private val excludedTests: mutable.Set[String] = ConcurrentHashMap.newKeySet[String]().asScala private def createDescription(suite: Suite): Description = { val description = Description.createSuiteDescription(suite.getClass) @@ -104,10 +105,10 @@ final class JUnitRunner(suiteClass: java.lang.Class[_ <: Suite]) extends org.jun val includedTests: Set[String] = suiteToRun.testNames.diff(excludedTests) val testTags: Map[String, Map[String, Set[String]]] = Map( suiteToRun.suiteId -> - includedTests.map(test => test -> Set("INCLUDE")).toMap + includedTests.map(test => test -> Set("org.scalatest.Selected")).toMap ) val filter = Filter( - tagsToInclude = Some(Set("INCLUDE")), + tagsToInclude = Some(Set("org.scalatest.Selected")), dynaTags = DynaTags(suiteTags = Map.empty, testTags = testTags) ) // TODO: What should this Tracker be?