You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by dw...@apache.org on 2026/10/06 17:23:22 UTC

(lucene) branch main updated: Clean up parent test class references to junit. (#16763)

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

dweiss pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/lucene.git


The following commit(s) were added to refs/heads/main by this push:
     new 7787f8bc1a4 Clean up parent test class references to junit. (#16763)
7787f8bc1a4 is described below

commit 7787f8bc1a43fb3baf8b2ccd8a7173c073add19e
Author: Dawid Weiss <da...@carrotsearch.com>
AuthorDate: Tue Oct 6 19:23:14 2026 +0200

    Clean up parent test class references to junit. (#16763)
    
    * Clean up parent test class references to junit. Make executor setup lazy.
    
    * Remove comment.
---
 .../apache/lucene/tests/util/LuceneTestCase.java   | 248 +++++++++++----------
 .../lucene/tests/util/LuceneTestCaseJupiter.java   |  25 ++-
 .../lucene/tests/util/LuceneTestCaseParent.java    | 118 ++--------
 .../tests/util/SetupAndRestoreCpuCoreCount.java    |  38 ++++
 .../tests/util/SetupAndRestoreQueryCache.java      |  56 +++++
 .../lucene/tests/util/SharedExecutorService.java   |  72 ++++++
 .../tests/util/TestLuceneTestCaseParentSanity.java |  83 +++++++
 7 files changed, 414 insertions(+), 226 deletions(-)

diff --git a/lucene/test-framework/src/java/org/apache/lucene/tests/util/LuceneTestCase.java b/lucene/test-framework/src/java/org/apache/lucene/tests/util/LuceneTestCase.java
index dbe21490e5f..65e2406119e 100644
--- a/lucene/test-framework/src/java/org/apache/lucene/tests/util/LuceneTestCase.java
+++ b/lucene/test-framework/src/java/org/apache/lucene/tests/util/LuceneTestCase.java
@@ -49,6 +49,7 @@ import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 import java.lang.reflect.Method;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Random;
 import java.util.concurrent.atomic.AtomicLong;
@@ -355,121 +356,121 @@ public abstract non-sealed class LuceneTestCase extends LuceneTestCaseParent {
             LuceneTestCase::random,
             () -> RandomizedContext.current().getTargetClass());
 
-    classRules =
-        RuleChain.outerRule(new TestRuleIgnoreTestSuites())
-            .around(
-                new TestRuleAdapter() {
-                  @SuppressWarnings("NonFinalStaticField")
-                  private static TestFrameworkInfra testFrameworkInfra;
-
-                  @Override
-                  protected void before() throws Throwable {
-                    int maxCalls =
-                        Integer.parseInt(System.getProperty(SYSPROP_RANDOM_MAXCALLS, "0"));
-                    Supplier<Random> supplier = () -> RandomizedContext.current().getRandom();
-                    if (maxCalls > 0) {
-                      var finalizedSupplier = supplier;
-                      supplier = () -> new MaxCallCountRandom(finalizedSupplier.get(), maxCalls);
+    List<TestRule> orderedClassRules = new ArrayList<>();
+    orderedClassRules.add(new TestRuleIgnoreTestSuites());
+    orderedClassRules.add(
+        new TestRuleAdapter() {
+          @SuppressWarnings("NonFinalStaticField")
+          private static TestFrameworkInfra testFrameworkInfra;
+
+          @Override
+          protected void before() throws Throwable {
+            int maxCalls = Integer.parseInt(System.getProperty(SYSPROP_RANDOM_MAXCALLS, "0"));
+            Supplier<Random> supplier = () -> RandomizedContext.current().getRandom();
+            if (maxCalls > 0) {
+              var finalizedSupplier = supplier;
+              supplier = () -> new MaxCallCountRandom(finalizedSupplier.get(), maxCalls);
+            }
+
+            int maxAcquires = Integer.parseInt(System.getProperty(SYSPROP_RANDOM_MAXACQUIRES, "0"));
+            if (maxAcquires > 0) {
+              var finalizedSupplier = supplier;
+              supplier =
+                  () -> {
+                    if (randomCalls.incrementAndGet() > maxAcquires) {
+                      throw new RuntimeException(
+                          "Too many random() calls. Consider using LuceneTestCase.nonAssertingRandom for"
+                              + " large loops or data generation.");
                     }
+                    return finalizedSupplier.get();
+                  };
+            }
+
+            var finalizedSupplier = supplier;
+            setTestFrameworkInfra(
+                null,
+                testFrameworkInfra =
+                    new TestFrameworkInfra() {
+                      @Override
+                      public Random threadRandom() {
+                        return finalizedSupplier.get();
+                      }
 
-                    int maxAcquires =
-                        Integer.parseInt(System.getProperty(SYSPROP_RANDOM_MAXACQUIRES, "0"));
-                    if (maxAcquires > 0) {
-                      var finalizedSupplier = supplier;
-                      supplier =
-                          () -> {
-                            if (randomCalls.incrementAndGet() > maxAcquires) {
-                              throw new RuntimeException(
-                                  "Too many random() calls. Consider using LuceneTestCase.nonAssertingRandom for"
-                                      + " large loops or data generation.");
-                            }
-                            return finalizedSupplier.get();
-                          };
-                    }
+                      @Override
+                      public SetupAndRestoreStaticEnv getClassEnv() {
+                        return setupAndRestoreClassEnv;
+                      }
+
+                      @Override
+                      public TemporaryFilesSupplier getTempFilesSupplier() {
+                        return tempFilesSupplier;
+                      }
 
-                    var finalizedSupplier = supplier;
-                    setTestFrameworkInfra(
-                        null,
-                        testFrameworkInfra =
-                            new TestFrameworkInfra() {
-                              @Override
-                              public Random threadRandom() {
-                                return finalizedSupplier.get();
-                              }
-
-                              @Override
-                              public SetupAndRestoreStaticEnv getClassEnv() {
-                                return setupAndRestoreClassEnv;
-                              }
-
-                              @Override
-                              public TemporaryFilesSupplier getTempFilesSupplier() {
-                                return tempFilesSupplier;
-                              }
-
-                              @Override
-                              public SuiteFailureState getSuiteFailureState() {
-                                return suiteFailureMarker;
-                              }
-
-                              @Override
-                              public Field newField(
-                                  Random random, String name, Object value, FieldType type) {
-                                return fieldToType.newField(random, name, value, type);
-                              }
-                            });
-                  }
-
-                  @Override
-                  protected void afterAlways(List<Throwable> errors) {
-                    setTestFrameworkInfra(testFrameworkInfra, null);
-                  }
-                })
-            .around(ignoreAfterMaxFailures)
-            .around(suiteFailureMarker)
-            .around(
-                new VerifyTestClassNamingConvention(
-                    "org.apache.lucene", Pattern.compile("(.+\\.)(Test)([^.]+)")))
-            .around(new TestRuleAssertionsRequired())
-            .around(new TestRuleLimitSysouts(suiteFailureMarker))
-            .around(new CallbacksToRuleAdapter(tempFilesSupplier))
-            .around(new NoClassHooksShadowingRule())
-            .around(
-                new NoInstanceHooksOverridesRule() {
-                  @Override
-                  protected boolean verify(Method key) {
-                    String name = key.getName();
-                    return !(name.equals("setUp") || name.equals("tearDown"));
-                  }
-                })
-            .around(classNameRule = new TestRuleStoreClassName())
-            .around(
-                new TestRuleRestoreSystemProperties(
-                    // Enlist all properties to which we have write access (security manager);
-                    // these should be restored to previous state, no matter what the outcome of the
-                    // test.
-
-                    // We reset the default locale and timezone; these properties change as a
-                    // side-effect
-                    "user.language", "user.timezone"))
-            .around(new CallbacksToRuleAdapter(setupAndRestoreClassEnv))
-            .around(new CallbacksToRuleAdapter(fieldToType = new FieldToType()))
-            .around(
-                new CallbacksToRuleAdapter(
-                    new BeforeAfterCallback() {
                       @Override
-                      public void before() {
-                        // Save environment information to reproduce-info listener.
-                        // This listener can be invoked after all the tests and other callbacks have
-                        // completed; I don't see any clean way to pass it there.
-                        RunListenerPrintReproduceInfo.envInfoJunit4 =
-                            new TestEnvInfo(
-                                setupAndRestoreClassEnv.codec,
-                                setupAndRestoreClassEnv.similarity,
-                                setupAndRestoreClassEnv.locale,
-                                setupAndRestoreClassEnv.timeZone);
+                      public SuiteFailureState getSuiteFailureState() {
+                        return suiteFailureMarker;
                       }
-                    }));
+
+                      @Override
+                      public Field newField(
+                          Random random, String name, Object value, FieldType type) {
+                        return fieldToType.newField(random, name, value, type);
+                      }
+                    });
+          }
+
+          @Override
+          protected void afterAlways(List<Throwable> errors) {
+            setTestFrameworkInfra(testFrameworkInfra, null);
+          }
+        });
+    orderedClassRules.add(ignoreAfterMaxFailures);
+    orderedClassRules.add(suiteFailureMarker);
+    orderedClassRules.add(
+        new VerifyTestClassNamingConvention(
+            "org.apache.lucene", Pattern.compile("(.+\\.)(Test)([^.]+)")));
+    orderedClassRules.add(new TestRuleAssertionsRequired());
+    orderedClassRules.add(new TestRuleLimitSysouts(suiteFailureMarker));
+    orderedClassRules.add(new CallbacksToRuleAdapter(tempFilesSupplier));
+    orderedClassRules.add(new NoClassHooksShadowingRule());
+    orderedClassRules.add(
+        new NoInstanceHooksOverridesRule() {
+          @Override
+          protected boolean verify(Method key) {
+            String name = key.getName();
+            return !(name.equals("setUp") || name.equals("tearDown"));
+          }
+        });
+    orderedClassRules.add(classNameRule = new TestRuleStoreClassName());
+    orderedClassRules.add(
+        new TestRuleRestoreSystemProperties(
+            // Enlist all properties to which we have write access (security manager);
+            // these should be restored to previous state, no matter what the outcome of the
+            // test.
+
+            // We reset the default locale and timezone; these properties change as a
+            // side-effect
+            "user.language", "user.timezone"));
+    orderedClassRules.add(new CallbacksToRuleAdapter(setupAndRestoreClassEnv));
+    orderedClassRules.add(new CallbacksToRuleAdapter(fieldToType = new FieldToType()));
+    orderedClassRules.add(
+        new CallbacksToRuleAdapter(
+            new BeforeAfterCallback() {
+              @Override
+              public void before() {
+                // Save environment information to reproduce-info listener.
+                // This listener can be invoked after all the tests and other callbacks have
+                // completed; I don't see any clean way to pass it there.
+                RunListenerPrintReproduceInfo.envInfoJunit4 =
+                    new TestEnvInfo(
+                        setupAndRestoreClassEnv.codec,
+                        setupAndRestoreClassEnv.similarity,
+                        setupAndRestoreClassEnv.locale,
+                        setupAndRestoreClassEnv.timeZone);
+              }
+            }));
+    orderedClassRules.addAll(suiteCallbacks().stream().map(CallbacksToRuleAdapter::new).toList());
+    classRules = asRuleChain(orderedClassRules);
   }
 
   // -----------------------------------------------------------------
@@ -490,13 +491,26 @@ public abstract non-sealed class LuceneTestCase extends LuceneTestCaseParent {
    * This controls how individual test rules are nested. It is important that _all_ rules declared
    * in {@link LuceneTestCase} are executed in proper order if they depend on each other.
    */
-  @Rule
-  public final TestRule ruleChain =
-      RuleChain.outerRule(testFailureMarker)
-          .around(ignoreAfterMaxFailures)
-          .around(threadAndTestNameRule)
-          .around(new CallbacksToRuleAdapter(new TestRuleSetupAndRestoreInstanceEnv()))
-          .around(parentChainCallRule);
+  @Rule public final TestRule ruleChain;
+
+  {
+    List<TestRule> orderedTestRules = new ArrayList<>();
+    orderedTestRules.add(testFailureMarker);
+    orderedTestRules.add(ignoreAfterMaxFailures);
+    orderedTestRules.add(threadAndTestNameRule);
+    orderedTestRules.add(new CallbacksToRuleAdapter(new TestRuleSetupAndRestoreInstanceEnv()));
+    orderedTestRules.add(parentChainCallRule);
+    orderedTestRules.addAll(testCallbacks().stream().map(CallbacksToRuleAdapter::new).toList());
+    ruleChain = asRuleChain(orderedTestRules);
+  }
+
+  private static RuleChain asRuleChain(List<? extends TestRule> rules) {
+    RuleChain chain = RuleChain.emptyRuleChain();
+    for (TestRule rule : rules) {
+      chain = chain.around(rule);
+    }
+    return chain;
+  }
 
   /** A counter of calls to {@link #random()} if {@link #SYSPROP_RANDOM_MAXACQUIRES} is defined. */
   @SuppressWarnings("NonFinalStaticField")
diff --git a/lucene/test-framework/src/java/org/apache/lucene/tests/util/LuceneTestCaseJupiter.java b/lucene/test-framework/src/java/org/apache/lucene/tests/util/LuceneTestCaseJupiter.java
index ebd6865444d..fa627f7fcaa 100644
--- a/lucene/test-framework/src/java/org/apache/lucene/tests/util/LuceneTestCaseJupiter.java
+++ b/lucene/test-framework/src/java/org/apache/lucene/tests/util/LuceneTestCaseJupiter.java
@@ -24,6 +24,7 @@ import com.carrotsearch.randomizedtesting.jupiter.SystemThreadFilter;
 import java.io.Closeable;
 import java.io.PrintStream;
 import java.util.ArrayDeque;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
@@ -360,15 +361,15 @@ public abstract non-sealed class LuceneTestCaseJupiter extends LuceneTestCasePar
       // This is the chain of before-after callbacks that must be called in the right order for
       // compatibility
       // with junit4 implementation.
-      this.beforeAfters =
-          new OrderedBeforeAfterCallbacks(
-              List.of(
-                  perThreadRandom,
-                  installFrameworkInfraSupport,
-                  classEnvRule,
-                  fieldToType,
-                  installEnvInfo,
-                  tempFileSupplier));
+      List<BeforeAfterCallback> callbacks = new ArrayList<>();
+      callbacks.add(perThreadRandom);
+      callbacks.add(installFrameworkInfraSupport);
+      callbacks.add(classEnvRule);
+      callbacks.add(fieldToType);
+      callbacks.add(installEnvInfo);
+      callbacks.add(tempFileSupplier);
+      callbacks.addAll(LuceneTestCaseParent.suiteCallbacks());
+      this.beforeAfters = new OrderedBeforeAfterCallbacks(callbacks);
 
       beforeAfters.before();
     }
@@ -526,8 +527,10 @@ public abstract non-sealed class LuceneTestCaseJupiter extends LuceneTestCasePar
 
     @Override
     public void beforeEach(ExtensionContext context) throws Exception {
-      this.callbacks =
-          new OrderedBeforeAfterCallbacks(List.of(new TestRuleSetupAndRestoreInstanceEnv()));
+      List<BeforeAfterCallback> testCallbacks = new ArrayList<>();
+      testCallbacks.add(new TestRuleSetupAndRestoreInstanceEnv());
+      testCallbacks.addAll(LuceneTestCaseParent.testCallbacks());
+      this.callbacks = new OrderedBeforeAfterCallbacks(testCallbacks);
       callbacks.before();
     }
 
diff --git a/lucene/test-framework/src/java/org/apache/lucene/tests/util/LuceneTestCaseParent.java b/lucene/test-framework/src/java/org/apache/lucene/tests/util/LuceneTestCaseParent.java
index b1cee33890f..f43e085990d 100644
--- a/lucene/test-framework/src/java/org/apache/lucene/tests/util/LuceneTestCaseParent.java
+++ b/lucene/test-framework/src/java/org/apache/lucene/tests/util/LuceneTestCaseParent.java
@@ -16,7 +16,6 @@
  */
 package org.apache.lucene.tests.util;
 
-import static com.carrotsearch.randomizedtesting.RandomizedTest.frequently;
 import static com.carrotsearch.randomizedtesting.RandomizedTest.systemPropertyAsBoolean;
 import static com.carrotsearch.randomizedtesting.RandomizedTest.systemPropertyAsInt;
 import static org.apache.lucene.search.DocIdSetIterator.NO_MORE_DOCS;
@@ -57,9 +56,6 @@ import java.util.TimeZone;
 import java.util.TreeSet;
 import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 import junit.framework.AssertionFailedError;
 import org.apache.lucene.analysis.Analyzer;
@@ -121,7 +117,6 @@ import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.LRUQueryCache;
 import org.apache.lucene.search.Query;
-import org.apache.lucene.search.QueryCache;
 import org.apache.lucene.search.QueryCachingPolicy;
 import org.apache.lucene.store.ByteBuffersDirectory;
 import org.apache.lucene.store.Directory;
@@ -157,7 +152,6 @@ import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.CommandLineUtil;
 import org.apache.lucene.util.IOUtils;
 import org.apache.lucene.util.InfoStream;
-import org.apache.lucene.util.NamedThreadFactory;
 import org.apache.lucene.util.SuppressForbidden;
 import org.apache.lucene.util.automaton.Automaton;
 import org.apache.lucene.util.automaton.CompiledAutomaton;
@@ -165,11 +159,7 @@ import org.apache.lucene.util.automaton.Operations;
 import org.apache.lucene.util.automaton.RegExp;
 import org.hamcrest.Matcher;
 import org.hamcrest.MatcherAssert;
-import org.junit.After;
-import org.junit.AfterClass;
 import org.junit.Assert;
-import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.internal.AssumptionViolatedException;
 
 /**
@@ -1156,7 +1146,7 @@ public abstract sealed class LuceneTestCaseParent extends Assert
         leaves,
         maxDocsPerSlice,
         maxSegmentsPerSlice,
-        concurrency == Concurrency.INTRA_SEGMENT && frequently());
+        concurrency == Concurrency.INTRA_SEGMENT && TestUtil.nextInt(random(), 0, 100) < 90);
   }
 
   /**
@@ -2534,95 +2524,27 @@ public abstract sealed class LuceneTestCaseParent extends Assert
     }
   }
 
-  private static final QueryCache DEFAULT_QUERY_CACHE = IndexSearcher.getDefaultQueryCache();
-  private static final QueryCachingPolicy DEFAULT_CACHING_POLICY =
-      IndexSearcher.getDefaultQueryCachingPolicy();
-  private static LRUQueryCache classQueryCache;
+  /** A before-after hook providing {@link ExecutorService}. */
+  private static final SharedExecutorService sharedExecutor = new SharedExecutorService();
 
-  private static LRUQueryCache createAndOverrideDefaultQueryCache() {
-    LRUQueryCache lruQueryCache =
-        new LRUQueryCache(10000, 1 << 25, _ -> true, Float.POSITIVE_INFINITY);
-    IndexSearcher.setDefaultQueryCache(lruQueryCache);
-    IndexSearcher.setDefaultQueryCachingPolicy(MAYBE_CACHE_POLICY);
-    return lruQueryCache;
-  }
-
-  @BeforeClass
-  public static void overrideClassDefaultQueryCache() {
-    // we need to reset the query cache in an @BeforeClass so that tests that
-    // instantiate an IndexSearcher in an @BeforeClass method use a fresh new cache
-    classQueryCache = createAndOverrideDefaultQueryCache();
-  }
-
-  @AfterClass
-  public static void resetClassDefaultQueryCache() throws IOException {
-    IndexSearcher.setDefaultQueryCache(DEFAULT_QUERY_CACHE);
-    IndexSearcher.setDefaultQueryCachingPolicy(DEFAULT_CACHING_POLICY);
-    if (classQueryCache != null) {
-      classQueryCache.clear();
-    }
-    IOUtils.close(classQueryCache);
-    classQueryCache = null;
-  }
-
-  private LRUQueryCache methodQueryCache;
-
-  @Before
-  public void overrideTestDefaultQueryCache() {
-    methodQueryCache = createAndOverrideDefaultQueryCache();
-  }
-
-  @After
-  public void resetTestDefaultQueryCache() throws IOException {
-    // Restore the class-level cache so TestRules and @AfterClass still see a
-    // test-scoped cache rather than the process-wide production default.
-    IndexSearcher.setDefaultQueryCache(classQueryCache);
-    IndexSearcher.setDefaultQueryCachingPolicy(MAYBE_CACHE_POLICY);
-    if (methodQueryCache != null) {
-      methodQueryCache.clear();
-    }
-    IOUtils.close(methodQueryCache);
-    methodQueryCache = null;
-  }
-
-  @BeforeClass
-  public static void setupCPUCoreCount() {
-    // Randomize core count so CMS varies its dynamic defaults, and this also "fixes" core
-    // count from the master seed so it will always be the same on reproduce:
-    int numCores = TestUtil.nextInt(random(), 1, 4);
-    System.setProperty(
-        ConcurrentMergeScheduler.DEFAULT_CPU_CORE_COUNT_PROPERTY, Integer.toString(numCores));
-  }
-
-  @AfterClass
-  public static void restoreCPUCoreCount() {
-    System.clearProperty(ConcurrentMergeScheduler.DEFAULT_CPU_CORE_COUNT_PROPERTY);
-  }
-
-  private static ExecutorService executor;
-
-  @BeforeClass
-  public static void setUpExecutorService() {
-    int threads = TestUtil.nextInt(random(), 1, 2);
-    executor =
-        new ThreadPoolExecutor(
-            threads,
-            threads,
-            0L,
-            TimeUnit.MILLISECONDS,
-            new LinkedBlockingQueue<>(),
-            new NamedThreadFactory("LuceneTestCase"));
-    // uncomment to intensify LUCENE-3840
-    // executor.prestartAllCoreThreads();
-    if (VERBOSE) {
-      System.out.println("NOTE: Created shared ExecutorService with " + threads + " threads");
-    }
+  /**
+   * Suite-level setup and cleanup of the shared state used by this class. Test framework-specific
+   * subclasses must call these, in order, from their suite-level before/after hooks, after {@link
+   * #setTestFrameworkInfra} has been called.
+   */
+  static List<BeforeAfterCallback> suiteCallbacks() {
+    // we need to reset the query cache at suite level so that tests that instantiate an
+    // IndexSearcher in suite-level hooks use a fresh new cache
+    return List.of(
+        new SetupAndRestoreQueryCache(), new SetupAndRestoreCpuCoreCount(), sharedExecutor);
   }
 
-  @AfterClass
-  public static void shutdownExecutorService() {
-    TestUtil.shutdownExecutorService(executor);
-    executor = null;
+  /**
+   * Test-level setup and cleanup of the shared state used by this class. Test framework-specific
+   * subclasses must call these, in order, from their test-level before/after hooks.
+   */
+  static List<BeforeAfterCallback> testCallbacks() {
+    return List.of(new SetupAndRestoreQueryCache());
   }
 
   /** Create a new searcher over the reader. This searcher might randomly use threads. */
@@ -2720,7 +2642,7 @@ public abstract sealed class LuceneTestCaseParent extends Assert
       if (random.nextBoolean()) {
         ex = null;
       } else {
-        ex = executor;
+        ex = sharedExecutor.get();
         if (VERBOSE) {
           System.out.println("NOTE: newSearcher using shared ExecutorService");
         }
diff --git a/lucene/test-framework/src/java/org/apache/lucene/tests/util/SetupAndRestoreCpuCoreCount.java b/lucene/test-framework/src/java/org/apache/lucene/tests/util/SetupAndRestoreCpuCoreCount.java
new file mode 100644
index 00000000000..c1a934b3683
--- /dev/null
+++ b/lucene/test-framework/src/java/org/apache/lucene/tests/util/SetupAndRestoreCpuCoreCount.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.tests.util;
+
+import org.apache.lucene.index.ConcurrentMergeScheduler;
+
+/**
+ * Randomizes the CPU core count seen by {@link ConcurrentMergeScheduler} so that it varies its
+ * dynamic defaults. This also "fixes" the core count from the master seed so it will always be the
+ * same on reproduce.
+ */
+final class SetupAndRestoreCpuCoreCount implements BeforeAfterCallback {
+  @Override
+  public void before() {
+    int numCores = TestUtil.nextInt(LuceneTestCaseParent.random(), 1, 4);
+    System.setProperty(
+        ConcurrentMergeScheduler.DEFAULT_CPU_CORE_COUNT_PROPERTY, Integer.toString(numCores));
+  }
+
+  @Override
+  public void after() {
+    System.clearProperty(ConcurrentMergeScheduler.DEFAULT_CPU_CORE_COUNT_PROPERTY);
+  }
+}
diff --git a/lucene/test-framework/src/java/org/apache/lucene/tests/util/SetupAndRestoreQueryCache.java b/lucene/test-framework/src/java/org/apache/lucene/tests/util/SetupAndRestoreQueryCache.java
new file mode 100644
index 00000000000..d72d028eefb
--- /dev/null
+++ b/lucene/test-framework/src/java/org/apache/lucene/tests/util/SetupAndRestoreQueryCache.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.tests.util;
+
+import java.io.IOException;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.LRUQueryCache;
+import org.apache.lucene.search.QueryCache;
+import org.apache.lucene.search.QueryCachingPolicy;
+import org.apache.lucene.util.IOUtils;
+
+/**
+ * Replaces {@link IndexSearcher}'s default query cache and caching policy with a fresh cache and a
+ * policy that randomly caches. The previous defaults are restored afterwards, so this callback can
+ * be nested (suite level and test level).
+ */
+final class SetupAndRestoreQueryCache implements BeforeAfterCallback {
+  private QueryCache previousCache;
+  private QueryCachingPolicy previousPolicy;
+  private LRUQueryCache cache;
+
+  @Override
+  public void before() {
+    previousCache = IndexSearcher.getDefaultQueryCache();
+    previousPolicy = IndexSearcher.getDefaultQueryCachingPolicy();
+
+    cache = new LRUQueryCache(10000, 1 << 25, _ -> true, Float.POSITIVE_INFINITY);
+    IndexSearcher.setDefaultQueryCache(cache);
+    IndexSearcher.setDefaultQueryCachingPolicy(LuceneTestCaseParent.MAYBE_CACHE_POLICY);
+  }
+
+  @Override
+  public void after() throws IOException {
+    IndexSearcher.setDefaultQueryCache(previousCache);
+    IndexSearcher.setDefaultQueryCachingPolicy(previousPolicy);
+    if (cache != null) {
+      cache.clear();
+    }
+    IOUtils.close(cache);
+    cache = null;
+  }
+}
diff --git a/lucene/test-framework/src/java/org/apache/lucene/tests/util/SharedExecutorService.java b/lucene/test-framework/src/java/org/apache/lucene/tests/util/SharedExecutorService.java
new file mode 100644
index 00000000000..037e46c0ced
--- /dev/null
+++ b/lucene/test-framework/src/java/org/apache/lucene/tests/util/SharedExecutorService.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.tests.util;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+import org.apache.lucene.util.NamedThreadFactory;
+
+/** Lazily sets up and shuts down an {@link ExecutorService} shared by all tests in a suite. */
+final class SharedExecutorService implements BeforeAfterCallback, Supplier<ExecutorService> {
+  private int threadCount;
+  // Just for sanity checks; we don't need full thread coordination here.
+  private volatile boolean active;
+  private ExecutorService executor;
+
+  @Override
+  public void before() {
+    // Pick the thread count at start, for consistency.
+    threadCount = TestUtil.nextInt(LuceneTestCaseParent.random(), 1, 2);
+    active = true;
+  }
+
+  @Override
+  public void after() {
+    if (executor != null) {
+      TestUtil.shutdownExecutorService(executor);
+    }
+    executor = null;
+    active = false;
+  }
+
+  /** Returns the shared executor. Must be called within the before-after scope. */
+  @Override
+  public ExecutorService get() {
+    synchronized (this) {
+      if (!active) {
+        throw new AssertionError("Shared executor is not available outside of the suite scope.");
+      }
+
+      if (executor == null) {
+        executor =
+            new ThreadPoolExecutor(
+                threadCount,
+                threadCount,
+                0L,
+                TimeUnit.MILLISECONDS,
+                new LinkedBlockingQueue<>(),
+                new NamedThreadFactory("LuceneTestCase"));
+        // uncomment to intensify LUCENE-3840
+        // executor.prestartAllCoreThreads();
+      }
+      return executor;
+    }
+  }
+}
diff --git a/lucene/test-framework/src/test/org/apache/lucene/tests/util/TestLuceneTestCaseParentSanity.java b/lucene/test-framework/src/test/org/apache/lucene/tests/util/TestLuceneTestCaseParentSanity.java
new file mode 100644
index 00000000000..7142a2b0db3
--- /dev/null
+++ b/lucene/test-framework/src/test/org/apache/lucene/tests/util/TestLuceneTestCaseParentSanity.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.tests.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.classfile.Attributes;
+import java.lang.classfile.ClassFile;
+import java.lang.classfile.ClassModel;
+import java.lang.classfile.constantpool.ClassEntry;
+import java.lang.classfile.constantpool.Utf8Entry;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * Ensures {@link LuceneTestCaseParent} remains (mostly) independent of the test framework used: any
+ * junit4 or junit-jupiter specific code should live in {@link LuceneTestCase} or {@link
+ * LuceneTestCaseJupiter}.
+ */
+public class TestLuceneTestCaseParentSanity extends LuceneTestCase {
+  /** The only classes {@link LuceneTestCaseParent} is allowed to reference (as bytecode refs). */
+  private static final Set<String> ALLOWED =
+      Set.of("org/junit/Assert", "org/junit/internal/AssumptionViolatedException");
+
+  private static final String JUNIT_PREFIX = "org/junit/";
+
+  public void testNoJUnitSpecificReferences() throws IOException {
+    Set<String> referenced = new TreeSet<>();
+
+    // Parent test class and its nested classes.
+    List<String> classFiles = new ArrayList<>();
+    ClassModel host = parse(LuceneTestCaseParent.class.getSimpleName() + ".class");
+    classFiles.add(host.thisClass().asInternalName());
+    host.findAttribute(Attributes.nestMembers())
+        .ifPresent(
+            attr ->
+                attr.nestMembers().stream()
+                    .map(ClassEntry::asInternalName)
+                    .forEach(classFiles::add));
+
+    for (String internalName : classFiles) {
+      ClassModel model =
+          parse(internalName.substring(internalName.lastIndexOf('/') + 1) + ".class");
+      for (var entry : model.constantPool()) {
+        if (entry instanceof Utf8Entry utf8) {
+          var internalClassRef = utf8.stringValue();
+          if (internalClassRef.startsWith(JUNIT_PREFIX) && !ALLOWED.contains(internalClassRef)) {
+            referenced.add(utf8.stringValue());
+          }
+        }
+      }
+    }
+
+    assertEquals(
+        "LuceneTestCaseParent must not reference test framework-specific classes, move such code to"
+            + " LuceneTestCase or LuceneTestCaseJupiter.",
+        Set.of(),
+        referenced);
+  }
+
+  private static ClassModel parse(String resource) throws IOException {
+    try (InputStream is = LuceneTestCaseParent.class.getResourceAsStream(resource)) {
+      assertNotNull("Class file not found: " + resource, is);
+      return ClassFile.of().parse(is.readAllBytes());
+    }
+  }
+}