This is an automated email from the ASF dual-hosted git repository.

sarutak pushed a commit to branch branch-4.3
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.3 by this push:
     new c476acc411d8 [SPARK-59825][SQL][TEST] Fix `HiveClientSuites` to work 
with sbt `testOnly` and `-z` filtering
c476acc411d8 is described below

commit c476acc411d84c792e6ab792d262e1865cdb2482
Author: Kousuke Saruta <[email protected]>
AuthorDate: Wed Sep 30 14:21:43 2026 +0900

    [SPARK-59825][SQL][TEST] Fix `HiveClientSuites` to work with sbt `testOnly` 
and `-z` filtering
    
    ### What changes were proposed in this pull request?
    `HiveClientSuites` runs `HiveClientSuite` once per supported Hive metastore 
(HMS) version by exposing them through ScalaTest's `nestedSuites`. This pattern 
does not interoperate well with SBT's ScalaTest framework integration, and this 
PR fixes two resulting problems:
    
    1. `testOnly ... -- -z "<keyword>"` reports "No tests were executed" 
because the ScalaTest SBT framework cannot propagate the test-name filter down 
to the nested suites (their `suiteId`s differ from the parent's).
    2. During a full run, test names and version info for HMS versions other 
than the first are not displayed properly.
    
    The fix works around a known ScalaTest limitation 
(https://github.com/scalatest/scalatest/issues/2375):
    
    - `HiveClientSuites`
      - Override `testNames` to include the nested suites' test names so that 
the SBT framework's `-z` substring matching can see them.
      - Override `run` to handle both filtered (`-z`/`-t`) and full runs. The 
SBT ScalaTest framework always calls `run` with `testName == None` and encodes 
any selection as a `Selected` tag plus per-test `dynaTags` keyed by this 
suite's `suiteId`; because our `testNames` exposes the nested names, those 
selections land in this suite's`dynaTags`. The override runs our own tests 
separately (filtered against`super.testNames`), then re-keys the matching 
`dynaTags` to each nested suite's`suite [...]
      - Factor the per-suite `SuiteStarting`/`SuiteCompleted`/`SuiteAborted` 
reporting into a private `runNestedSuiteWithEvents` helper (needed because each 
nested suite must receive its own re-keyed `Filter`, so `runNestedSuites` 
cannot be used directly).
      - Make `nestedSuites` a `lazy val` so the nested suites are not 
re-created on every call.
    - `HiveVersionSuite`
      - Override `suiteId` to `s"${getClass.getName}($version)"` so each 
version's nested suite has a unique `suiteId` (required for the `dynaTags` 
redistribution above).
    - `HiveClientSuite`
      - Move client construction/teardown from the `"create client"` test body 
into `beforeAll`/`afterAll`, so that running a single test via `-z` still 
initializes `client`/`versionSpark`. The `"create client"` test now asserts 
they were initialized in `beforeAll`.
    
    Note on scope: the nested `HiveClientSuite` tests are order-dependent -- 
they share a single `client`/`versionSpark` and many tests rely on state 
(databases, tables, partitions, functions) created by earlier tests. Selecting 
a single order-dependent test with `-z` runs it in isolation and may fail 
because its prerequisites were filtered out; independent tests (e.g. `"create 
client"`) can be selected safely. This is a pre-existing property of these 
tests and is called out in a code com [...]
    
    ### Why are the changes needed?
    Developers cannot currently run a single Hive client test (or a subset) via 
SBT's `-z` filter -- the run silently executes nothing, which is easy to 
misread as "passed". This makes iterating on Hive client changes slow and 
error-prone. The full-run display issue also hides which HMS versions actually 
ran.
    
    ### Does this PR introduce _any_ user-facing change?
    No. This only affects test-suite execution/reporting for `HiveClientSuites`.
    
    ### How was this patch tested?
    Existing tests, plus manual verification against `master` and this branch 
(JDK 17, `build/sbt`):
    
    - Reproduced the bug on `master`:
    
      ```
      build/sbt 'hive/testOnly 
org.apache.spark.sql.hive.client.HiveClientSuites \
        -- -z "should pick right user name as owner"'
      ```
    
      -> `Total number of tests run: 0` / `No tests were executed.`
    
    - On this branch, the same command runs the selected test across all HMS 
versions:
    
      -> `Total number of tests run: 8` / `succeeded 8, failed 0` / `All tests 
passed.`
    
    - Full run on this branch is unaffected:
    
      ```
      build/sbt 'hive/testOnly 
org.apache.spark.sql.hive.client.HiveClientSuites'
      ```
    
      -> `595 succeeded, 0 failed` and test names for all HMS versions (2.0 / 
2.1 / 2.2 / 2.3 / 3.0 / 3.1 / 4.0 / 4.1) are displayed (problem 2 fixed), with 
no double execution.
    
    - After the review-driven cleanup (removing the unreachable `runTest` 
override, extracting `runNestedSuiteWithEvents`, switching to `NonFatal`), 
re-verified that behavior is unchanged: `hive/Test/compile` succeeds, `-z` 
selection still runs the selected test across all 8 HMS versions (8 passed, one 
`beforeAll` per version), the full run is still `595 succeeded, 0 failed` with 
all versions listed, and `hive/scalastyle` reports 0 errors.
    
    Note: order-dependent tests in `HiveClientSuite` (e.g. `getTablesByName 
when multiple tables`, which relies on the earlier `createTable` test) will 
fail if selected alone with `-z`. That is a pre-existing property of these 
tests, out of scope here, and is tracked separately.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    Kiro CLI/ Claude
    
    Closes #59098 from sarutak/fix-hive-client-suites-sbt-test-filtering.
    
    Authored-by: Kousuke Saruta <[email protected]>
    Signed-off-by: Kousuke Saruta <[email protected]>
    (cherry picked from commit 578105289d82158f7d04d7976d547038d1a270c2)
    Signed-off-by: Kousuke Saruta <[email protected]>
---
 .../spark/sql/hive/client/HiveClientSuite.scala    |  30 ++++-
 .../spark/sql/hive/client/HiveClientSuites.scala   | 141 ++++++++++++++++++++-
 .../spark/sql/hive/client/HiveVersionSuite.scala   |   2 +
 3 files changed, 164 insertions(+), 9 deletions(-)

diff --git 
a/sql/hive/src/test/scala/org/apache/spark/sql/hive/client/HiveClientSuite.scala
 
b/sql/hive/src/test/scala/org/apache/spark/sql/hive/client/HiveClientSuite.scala
index 0eebdc9006db..bb701d80bea0 100644
--- 
a/sql/hive/src/test/scala/org/apache/spark/sql/hive/client/HiveClientSuite.scala
+++ 
b/sql/hive/src/test/scala/org/apache/spark/sql/hive/client/HiveClientSuite.scala
@@ -49,6 +49,27 @@ class HiveClientSuite(version: String) extends 
HiveVersionSuite(version) {
 
   private val ver = IsolatedClientLoader.hiveVersion(version)
 
+  override def beforeAll(): Unit = {
+    super.beforeAll()
+    System.gc() // Hack to avoid SEGV on some JVM versions.
+    val hadoopConf = new Configuration()
+    hadoopConf.set("test", "success")
+    client = buildClient(hadoopConf)
+    versionSpark = TestHiveVersion(client)
+  }
+
+  override def afterAll(): Unit = {
+    try {
+      if (versionSpark != null) {
+        versionSpark.reset()
+        versionSpark = null
+      }
+      client = null
+    } finally {
+      super.afterAll()
+    }
+  }
+
   /**
    * Drops table `tableName` after calling `f`.
    */
@@ -61,13 +82,8 @@ class HiveClientSuite(version: String) extends 
HiveVersionSuite(version) {
   }
 
   test("create client") {
-    client = null
-    System.gc() // Hack to avoid SEGV on some JVM versions.
-    val hadoopConf = new Configuration()
-    hadoopConf.set("test", "success")
-    client = buildClient(hadoopConf)
-    if (versionSpark != null) versionSpark.reset()
-    versionSpark = TestHiveVersion(client)
+    assert(client != null, "client should be initialized in beforeAll")
+    assert(versionSpark != null, "versionSpark should be initialized in 
beforeAll")
     
assert(versionSpark.sharedState.externalCatalog.unwrapped.asInstanceOf[HiveExternalCatalog]
       .client.version.fullVersion.startsWith(version))
   }
diff --git 
a/sql/hive/src/test/scala/org/apache/spark/sql/hive/client/HiveClientSuites.scala
 
b/sql/hive/src/test/scala/org/apache/spark/sql/hive/client/HiveClientSuites.scala
index e43308f62a49..8b4270573ef7 100644
--- 
a/sql/hive/src/test/scala/org/apache/spark/sql/hive/client/HiveClientSuites.scala
+++ 
b/sql/hive/src/test/scala/org/apache/spark/sql/hive/client/HiveClientSuites.scala
@@ -19,8 +19,10 @@ package org.apache.spark.sql.hive.client
 
 import java.net.URI
 
+import scala.util.control.NonFatal
+
 import org.apache.hadoop.conf.Configuration
-import org.scalatest.Suite
+import org.scalatest.{Args, CompositeStatus, DynaTags, FailedStatus, Filter, 
Status, Suite}
 
 import org.apache.spark.SparkFunSuite
 import org.apache.spark.sql.catalyst.catalog.CatalogDatabase
@@ -90,7 +92,142 @@ class HiveClientSuites extends SparkFunSuite with 
HiveClientVersions {
     assert(getNestedMessages(e) contains "Unknown column 'A0.OWNER_NAME' in 
'field list'")
   }
 
-  override def nestedSuites: IndexedSeq[Suite] = {
+  // Lazily initialize nested suites to avoid re-creating them on every call
+  private lazy val versionedSuites: IndexedSeq[HiveClientSuite] = {
     versions.map(new HiveClientSuite(_))
   }
+
+  override def nestedSuites: IndexedSeq[Suite] = versionedSuites
+
+  // Include nested suite test names in this suite's testNames so that SBT's
+  // `-z "keyword"` filtering can find them. Without this, the ScalaTest SBT 
framework
+  // would skip the entire suite when no parent-level test matches the keyword.
+  override def testNames: Set[String] = {
+    super.testNames ++ versionedSuites.flatMap(_.testNames)
+  }
+
+  // Override run to handle both filtered and full test runs correctly.
+  //
+  // The ScalaTest SBT framework always invokes a suite's run with testName == 
None and
+  // encodes any `-z` / `-t` selection as a "Selected" tag plus per-test 
dynaTags keyed by
+  // the suite's suiteId (see the workaround for
+  // https://github.com/scalatest/scalatest/issues/2375). Because our 
testNames override
+  // exposes the nested suites' test names, those selections land in *this* 
suite's dynaTags
+  // rather than the nested suites'. This override redistributes them:
+  //   - run our own tests via super.runTest (filtered against super.testNames 
only), then
+  //   - for each nested suite, re-key the matching dynaTags to that nested 
suite's suiteId
+  //     and run it once.
+  // Own tests and nested-suite tests are dispatched separately so a full run 
never
+  // double-executes (our testNames intentionally includes the nested names).
+  //
+  // NOTE: the nested HiveClientSuite tests are order-dependent (they share a 
single
+  // `client`/`versionSpark` built in beforeAll, and e.g. 
table/partition/function tests
+  // rely on state created by earlier tests). Selecting a single 
order-dependent test with
+  // `-z` will run it in isolation and may fail because its prerequisites were 
filtered out.
+  // Independent tests (e.g. "create client") can be selected safely. Making 
every test
+  // self-contained is out of scope for this change.
+  override def run(testName: Option[String], args: Args): Status = {
+    testName match {
+      case Some(_) =>
+        // Not exercised by the SBT framework (it uses testName == None + 
Selected tag, see
+        // above); delegate to the standard implementation as a fallback for 
other runners.
+        super.run(testName, args)
+      case None =>
+        // Run our own tests only (filter against super.testNames, not the 
nested names).
+        val ownTests = super.testNames
+        val statusBuffer = new scala.collection.mutable.ListBuffer[Status]()
+        for ((tn, ignoreTest) <- args.filter(ownTests, tags, suiteId)) {
+          if (!args.stopper.stopRequested) {
+            if (!ignoreTest) {
+              statusBuffer += super.runTest(tn, args)
+            }
+          }
+        }
+        val testsStatus = new CompositeStatus(Set.empty ++ statusBuffer)
+
+        // Then run the nested suites. When a `-z`/`-t` selection is active 
the SBT framework
+        // sets tagsToInclude = Some(Set("Selected")) with dynaTags keyed by 
THIS suite's
+        // suiteId; we redistribute those tags to each nested suite by its own 
suiteId.
+        val selectedTag = "org.scalatest.Selected"
+        val isSelectedFiltering = 
args.filter.tagsToInclude.exists(_.contains(selectedTag))
+
+        val nestedSuitesStatus = if (isSelectedFiltering) {
+          val myTestTags = args.filter.dynaTags.testTags.getOrElse(suiteId, 
Map.empty)
+          val nestedStatusBuffer = new 
scala.collection.mutable.ListBuffer[Status]()
+
+          for (nestedSuite <- nestedSuites) {
+            if (!args.stopper.stopRequested) {
+              // Find which tests in this nested suite were "Selected" in our 
dynaTags.
+              val nestedTestNames = nestedSuite.testNames
+              val selectedForNested = myTestTags.filter {
+                case (testName, tagSet) =>
+                  tagSet.contains(selectedTag) && 
nestedTestNames.contains(testName)
+              }
+              if (selectedForNested.nonEmpty) {
+                val nestedFilter = Filter(
+                  tagsToInclude = Some(Set(selectedTag)),
+                  args.filter.tagsToExclude,
+                  excludeNestedSuites = false,
+                  dynaTags = DynaTags(Map.empty, Map(nestedSuite.suiteId -> 
selectedForNested))
+                )
+                nestedStatusBuffer +=
+                  runNestedSuiteWithEvents(nestedSuite, args.copy(filter = 
nestedFilter), args)
+              }
+            }
+          }
+          new CompositeStatus(Set.empty ++ nestedStatusBuffer)
+        } else {
+          // No `-z`/`-t` selection: run all nested suites normally via 
runNestedSuites,
+          // which handles SuiteStarting/SuiteCompleted event reporting itself.
+          val nestedFilter = Filter(
+            tagsToInclude = None,
+            args.filter.tagsToExclude,
+            excludeNestedSuites = false
+          )
+          runNestedSuites(args.copy(filter = nestedFilter))
+        }
+        new CompositeStatus(Set(testsStatus, nestedSuitesStatus))
+    }
+  }
+
+  // Run a single nested suite while emitting the 
SuiteStarting/SuiteCompleted/SuiteAborted
+  // events that runNestedSuites would normally emit. We cannot use 
runNestedSuites directly
+  // here because we need to pass a per-suite Filter (with re-keyed dynaTags) 
to each nested
+  // suite. `runArgs` carries the reporter/tracker; `nestedArgs` carries the 
per-suite filter.
+  private def runNestedSuiteWithEvents(
+      nestedSuite: Suite, nestedArgs: Args, runArgs: Args): Status = {
+    import org.scalatest.events._
+    val report = runArgs.reporter
+    val tracker = runArgs.tracker
+    val suiteClassName = nestedSuite.getClass.getName
+    val suiteStartTime = System.currentTimeMillis
+    report(SuiteStarting(tracker.nextOrdinal(), nestedSuite.suiteName, 
nestedSuite.suiteId,
+      Some(suiteClassName), None, Some(TopOfClass(suiteClassName)), 
nestedSuite.rerunner))
+    try {
+      val status = nestedSuite.run(None, nestedArgs)
+      val duration = System.currentTimeMillis - suiteStartTime
+      status.unreportedException match {
+        case Some(ue) =>
+          report(SuiteAborted(tracker.nextOrdinal(), ue.getMessage, 
nestedSuite.suiteName,
+            nestedSuite.suiteId, Some(suiteClassName), Some(ue), 
Some(duration),
+            None, Some(SeeStackDepthException), nestedSuite.rerunner))
+          FailedStatus
+        case None =>
+          report(SuiteCompleted(tracker.nextOrdinal(), nestedSuite.suiteName,
+            nestedSuite.suiteId, Some(suiteClassName), Some(duration), None,
+            Some(TopOfClass(suiteClassName)), nestedSuite.rerunner))
+          status
+      }
+    } catch {
+      case NonFatal(e) =>
+        val eMessage = e.getMessage
+        val rawString = if (eMessage != null && eMessage.nonEmpty) eMessage
+          else e.getClass.getName
+        val duration = System.currentTimeMillis - suiteStartTime
+        report(SuiteAborted(tracker.nextOrdinal(), rawString, 
nestedSuite.suiteName,
+          nestedSuite.suiteId, Some(suiteClassName), Some(e), Some(duration),
+          None, Some(SeeStackDepthException), nestedSuite.rerunner))
+        FailedStatus
+    }
+  }
 }
diff --git 
a/sql/hive/src/test/scala/org/apache/spark/sql/hive/client/HiveVersionSuite.scala
 
b/sql/hive/src/test/scala/org/apache/spark/sql/hive/client/HiveVersionSuite.scala
index 9866ce58775e..ba8d22f69389 100644
--- 
a/sql/hive/src/test/scala/org/apache/spark/sql/hive/client/HiveVersionSuite.scala
+++ 
b/sql/hive/src/test/scala/org/apache/spark/sql/hive/client/HiveVersionSuite.scala
@@ -45,6 +45,8 @@ private[client] abstract class HiveVersionSuite(version: 
String) extends SparkFu
 
   override def suiteName: String = s"${super.suiteName}($version)"
 
+  override val suiteId: String = s"${getClass.getName}($version)"
+
   override protected def test(testName: String, testTags: Tag*)(testFun: => 
Any)
       (implicit pos: Position): Unit = {
     super.test(s"$version: $testName", testTags: _*)(testFun)


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to