All findings · Lucene finding · LFV-389 · junit-cleanup-dropped-test-annotations

Six test classes on main lost their @Test annotations in #16028 and no longer run

Since #16028 (May 2026), 18 test methods in six classes never run on main, and the default build stays green: these classes do not extend LuceneTestCase, so JUnit runs only @Test methods, and the cleanup removed all their @Test lines. Lucene's own test suite only; no release has it.

Summary

Priority
Low, rank 1218It affects only Lucene's own test suite on unreleased main (18 test methods silently do not run) and changes no shipped behaviour, but it loses regression coverage.
Impact
minor
Since
main, #16028 (bfa16d51c21)introduced 2026-05-02
Affected
main and branch_11x (11.0.0-SNAPSHOT); no release; 10.6.0 not affected
Status
documented
Found by
non-formal: Lucene's opt-in Hunspell conformance test, then git history

AI-assisted page: this finding was first signalled by code reading or testing and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

LuceneTestCase finds test methods by name; plain JUnit 4 needs @Test. #16028 removed @Test on both sides of that line.

Background: how a Lucene test class finds its test methods

  1. Lucene's build runs every class whose file is named Test*.class; on main, JUnit 4 classes run through the JUnit Platform's vintage engine.
  2. Inside a class, the JUnit 4 runner decides which methods are tests. The default runner and Parameterized take only methods annotated with @org.junit.Test.
  3. LuceneTestCase adds a rule: every public, non-static, no-argument method whose name starts with test (and is not overridden lower down) is a test too. In its subclasses @Test is redundant.
  4. A few test classes avoid LuceneTestCase on purpose, for example so a forked JVM starts with no Lucene class initialized, or because the distribution tests must not load Lucene's test framework. They need @Test.
Diagram of the bug. #16028 removed @Test from test* methods. In a LuceneTestCase subclass the methods still run, because LuceneTestCase finds test* methods by name. In the six classes that do not extend LuceneTestCase, JUnit finds only @Test methods, so none of their 18 methods runs: TestRuntimeDependenciesSane and TestCandidateMatcherVisibility report 0 tests, TestClassloadingDeadlock, TestModularLayer and TestScripts report one green entry named after the class, and TestHunspellRepositoryTestCases fails with No runnable methods. Expected: 4 tests for TestRuntimeDependenciesSane; got: no tests matched.
How LuceneTestCase finds test* methods by name while plain JUnit 4 needs @Test, and what the six classes report on main after #16028; then what you expect and what you get. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. On main, run ./gradlew :lucene:core.tests:test --tests org.apache.lucene.core.tests.TestRuntimeDependenciesSane. The class has 4 test* methods that check the tests load JUnit, Lucene core and themselves as named modules.
  2. You expect its 4 tests to run and pass, as they do on main with the annotations restored ((SUCCESS): 4 tests); on 10.5.2 and on main before May 2026 all 4 methods are found.
  3. Instead the build fails with "1 test task executed but no tests matched the provided --tests filters?". Without the filter, :lucene:core.tests:test succeeds, and the 4 methods never run.
  4. Why: #16028 removed 1,334 @Test lines from test* methods. LuceneTestCase runs test* methods by name, so for its subclasses that is harmless. These six classes do not extend it, and plain JUnit 4 runs only @Test methods.
  5. On main: 2 classes report 0 tests, 3 report one green entry named after the class (all of distribution.tests is 2 such entries), and the opt-in Hunspell conformance run fails with "No runnable methods". Restoring the 18 annotations brings all 18 methods back.

Who is affected

Only Lucene developers and CI on main and branch_11x. Every default Gradle run no longer runs 16 methods in four classes: the test-module checks, the class-loading deadlock guard, distribution.tests' module-layer checks and monitor's visibility calls. The documented Hunspell conformance run fails. No jar changes, no release has it, and every restored test that ran passes.

Mechanism, terms and numbers573 words

#16028 ("Misc junit4 test cleanups", main, 2026-05-02) removed 1,334 @Test lines from test* methods in 203 test files. For LuceneTestCase subclasses that is harmless, because LuceneTestCase also runs test* methods by name. Six of the classes do not extend LuceneTestCase, on purpose, and for them JUnit runs only @Test methods. So 18 test methods never run on main, and the build reports success: TestRuntimeDependenciesSane and TestCandidateMatcherVisibility report no tests, TestClassloadingDeadlock, TestModularLayer and TestScripts each report one passing entry named after the class with no method run, and the opt-in TestHunspellRepositoryTestCases fails with "No runnable methods". No shipped code, API or index format changes and no release has it; main and the 11.0 branch branch_11x do. Restoring the 6 imports and 18 annotations (the reverse of #16028 in these files) brings the 18 methods back: 17 run and pass, and TestScripts' GUI test is reported as skipped, as it was before #16028, because the build never passes its tests.gui option to the test JVM.

Where the name rule comes from

LuceneTestCase is @RunWith(RandomizedRunner.class) with @TestMethodProviders({LuceneJUnit3MethodProvider.class, JUnit4MethodProvider.class}) (LuceneTestCase.java:113-114). LuceneJUnit3MethodProvider selects public, non-static, no-argument methods named test* (LuceneJUnit3MethodProvider.java:39-42), skipping methods overridden or shadowed lower in the class hierarchy (:35-36); JUnit4MethodProvider selects @Test methods. Every class #16028 touched apart from the six below reaches LuceneTestCase.

The six classes

classhow it runstest methodson main
TestRuntimeDependenciesSane (core.tests)plain JUnit 4; "Intentionally not a subclass of {@code LuceneTestCase}." (:22)4no tests
TestCandidateMatcherVisibility (monitor)plain JUnit 4 (:29)3no tests
TestClassloadingDeadlock (core)@RunWith(RandomizedRunner.class) extends Assert, no method providers (:54-55)1one green entry named after the class
TestModularLayer (distribution.tests)extends AbstractLuceneDistributionTest extends RandomizedTest8one green entry
TestScripts (distribution.tests)same; its test is @RequiresGUI, which the Gradle build skips on every run (see below)1one green entry
TestHunspellRepositoryTestCases (analysis-common)@RunWith(Parameterized.class); runs only with -Phunspell.repo.path1InvalidTestClassError: No runnable methods

RandomizedRunner without a @TestMethodProviders annotation falls back to JUnit4MethodProvider, which takes only @Test methods (an independent verifier read this from the randomizedtesting-runner 2.9.2 bytecode; the Gradle runs show the effect). The distribution tests' base class says so itself: "the default {@code LuceneTestCase} configuration setup is not used (you have to annotate test for JUnit, for example)" (AbstractLuceneDistributionTest.java:39-42).

Why the build stays green

  • The build selects classes by file name, not by scanning for tests: setScanForTestClasses(false), include("**/Test*.class") (TestsAndRandomizationPlugin.java:545-546), and on main runs JUnit 4 through the vintage engine (:568-569, since #15949).
  • On that engine a plain JUnit 4 class with no @Test yields no test; a RandomizedRunner class with no test method yields one passing entry named after the class (JUnit XML: <testcase name="TestClassloadingDeadlock" classname="Gradle Test Run :lucene:core:test" time="0.005"/>), and none of its methods run (nor its @BeforeClass: an independent check ran TestModularLayer without a distribution path, which its @BeforeClass rejects, TestModularLayer.java:69-74, and it still "passed"); Parameterized fails with "No runnable methods".
  • Lucene turns off Gradle's per-task no-match failure (FailOnNoMatchingFilteredTestsPlugin.java:98) and fails only when a --tests filter matched nothing anywhere, a case it describes as "usually caused by a typo in the filter pattern" (:36-38). An unfiltered run reports nothing.

The change

In these six files bfa16d51c21 removed exactly 6 import org.junit.Test; lines and 18 @Test lines, and nothing else. The six files are unchanged since, apart from one comment in TestClassloadingDeadlock (#16027).

Exposure and likelihood386 words
  • Who: Lucene developers and CI on main and branch_11x. Users of Lucene are not affected: #16028 changed only test sources.
  • What is no longer checked on main:
    • TestRuntimeDependenciesSane (4): the tests of a modular project really load JUnit, Lucene core and the test and main source sets as named modules (added with LUCENE-10328, which fixed that module path).
    • TestClassloadingDeadlock (1): a forked JVM whose 20 threads initialise Codec, PostingsFormat, DocValuesFormat, TestSecrets, IndexWriter and SegmentReader at once must exit within 60 s. #15864 (2026-03-25, "Fix circular dependency deadlock in TestSecrets initialization") extended it; 38 days later it stopped running on main.
    • TestModularLayer (8): the binary distribution's module names, versions, exported and opened packages, service providers and multi-release jar. With TestScripts (1, the Luke launch check) these are the only test classes of lucene/distribution.tests, so the GitHub workflow dedicated to that module (run on every pull request and push to main) runs no test method.
    • TestCandidateMatcherVisibility (3): monitor's CandidateMatcher methods are callable from another package. Its compile-time part still holds; its three runtime calls no longer run.
    • TestHunspellRepositoryTestCases: the conformance run against Hunspell's own test data, documented as -Phunspell.repo.path (lucene/analysis/common/build.gradle:244), now fails whenever it is run with that property (without it, the class is skipped by its assumption, as in every version). Loud, not silent; no GitHub workflow sets the property.
  • Default or opt-in: every default Gradle run, local or CI, no longer runs 16 methods in four classes (4 + 3 + 1 + 8). TestScripts' one method is also no longer found, but the build skipped it on every run already before #16028: the build registers tests.gui (on by default for CI builds) but never passes it to the test JVM, so even -Ptests.gui=true or CI=true leaves it skipped (an older, separate matter, from #14764; not claimed here). The Hunspell runner only matters when someone opts in.
  • Scale: 18 methods; main since 2026-05-02, and branch_11x, the 11.0 branch cut from main on 2026-10-28, which has the six files unchanged; no release. A fix needs a backport to branch_11x to reach 11.0.0.
  • No hidden regression found: with the annotations restored, every method that ran passes (17 of 18; the build skips TestScripts' GUI test), and the deadlock guard passed 20 of 20 forked runs earlier.

Minimal repro

One JUnit 4 test in lucene/monitor. It runs main's unmodified TestCandidateMatcherVisibility (3 test* methods, no @Test, not a LuceneTestCase) through JUnitCore and asserts that its 3 methods run. Deterministic, single thread.

TestCandidateMatcherVisibilityRuns.java (JUnit 4, one test)35 lines
/*
 * 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.monitor.outsidepackage;

import static org.junit.Assert.assertEquals;

import org.junit.Test;
import org.junit.runner.JUnitCore;
import org.junit.runner.Result;

public class TestCandidateMatcherVisibilityRuns {

  @Test
  public void testItsThreeTestMethodsRun() {
    Result result = JUnitCore.runClasses(TestCandidateMatcherVisibility.class);
    // expected: 3 tests run, no failure; actual: initializationError "No runnable methods"
    assertEquals(
        "run=3 failures=[]",
        "run=" + result.getRunCount() + " failures=" + result.getFailures());
  }
}
Command
cp TestCandidateMatcherVisibilityRuns.java $LUCENE/lucene/monitor/src/test/org/apache/lucene/monitor/outsidepackage/
cd $LUCENE && ./gradlew :lucene:monitor:test --tests org.apache.lucene.monitor.outsidepackage.TestCandidateMatcherVisibilityRuns -Ptests.seed=DEADBEEF
# -Ptests.asserts=true and =false give the same result
Expected
JUnitCore runs the 3 methods of TestCandidateMatcherVisibility with no failure: run=3 failures=[].
Observed
run=1 failures=[initializationError(...TestCandidateMatcherVisibility): Invalid test class ... 1. No runnable methods], with assertions on and off.

All files of this repro: repro/LFV-389/

More files, environment, full output278 words

TestCandidateMatcherVisibilityRuns sits next to the class it runs, so main's own test source is compiled unmodified. It shows the plain JUnit 4 case, one of the six; the other five show the same cause in the build (see "The finding in 30 seconds"). Nothing random; the seed is only for the build.

No new file is needed to see it: on main, ./gradlew :lucene:core.tests:test --tests org.apache.lucene.core.tests.TestRuntimeDependenciesSane fails with "no tests matched the provided --tests filters?", while with the annotations restored it runs 4 tests ((SUCCESS): 4 tests); 10.6.0 keeps all 4 @Test annotations.

As a regression test: this same test (google-java-format wants the two assertEquals arguments on one line).

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT); upstream main f815e679bf6c has the same six files
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64
Command (environment)
cp TestCandidateMatcherVisibilityRuns.java $LUCENE/lucene/monitor/src/test/org/apache/lucene/monitor/outsidepackage/
cd $LUCENE && ./gradlew :lucene:monitor:test --tests org.apache.lucene.monitor.outsidepackage.TestCandidateMatcherVisibilityRuns -Ptests.seed=DEADBEEF
# -Ptests.asserts=true and =false give the same result

Observed output

# gradle, main 645d4fe14a20, seed DEADBEEF, -Ptests.asserts=true (the same with =false)
TestCandidateMatcherVisibilityRuns > testItsThreeTestMethodsRun FAILED
    org.junit.ComparisonFailure: expected:<run=[3 failures=[]]> but was:<run=[1 failures=[initializationError(
    org.apache.lucene.monitor.outsidepackage.TestCandidateMatcherVisibility): Invalid test class
    'org.apache.lucene.monitor.outsidepackage.TestCandidateMatcherVisibility':
      1. No runnable methods]]>
:lucene:monitor:test (FAILURE): 1 test, 1  failure

# with the six classes' @Test restored: :lucene:monitor:test (SUCCESS): 1 test

# how the six classes look in the same build (not the repro):
# --tests ...core.tests.TestRuntimeDependenciesSane, or ...outsidepackage.TestCandidateMatcherVisibility:
> 1 test task executed but no tests matched the provided --tests filters?
# --tests org.apache.lucene.index.TestClassloadingDeadlock:
:lucene:core:test (SUCCESS): 1 test      (the entry is named TestClassloadingDeadlock; testDeadlock never runs)
# :lucene:distribution.tests:test:
:lucene:distribution.tests:test (SUCCESS): 2 tests      (entries TestModularLayer and TestScripts, no method runs)
# -Phunspell.repo.path=<a test directory> --tests ...hunspell.TestHunspellRepositoryTestCases:
TestHunspellRepositoryTestCases > initializationError FAILED
    org.junit.runners.model.InvalidTestClassError: ... 1. No runnable methods

Why it looks unintended

  • The classes avoid LuceneTestCase on purpose: "Intentionally not a subclass of {@code LuceneTestCase}."; the distribution tests' base class says "you have to annotate test for JUnit".
  • MIGRATE.md: "LuceneTestCase (JUnit4) is unchanged." The build still documents -Phunspell.repo.path for the Hunspell runner.
  • #16028 called the annotations "spurious", true only under LuceneTestCase. It had no review, and 10.6.0 keeps every annotation.
The full argument147 words
  • The classes: TestRuntimeDependenciesSane.java:22 "Intentionally not a subclass of {@code LuceneTestCase}."; TestClassloadingDeadlock.java:51-52 "This test does not extend LuceneTestCase to prevent static class initialization when spawned as a subprocess"; AbstractLuceneDistributionTest.java:39-42 "... (you have to annotate test for JUnit, for example)"; TestHunspellRepositoryTestCases.java:32-34 "checks all Hunspell's test data".
  • The build: lucene/analysis/common/build.gradle:244 registers hunspell.repo.path as "Path to hunspell repo path for TestHunspellRepositoryTestCases".
  • The migration notes: lucene/MIGRATE.md:45-46 "These apply if you extend LuceneTestCaseJupiter. LuceneTestCase (JUnit4) is unchanged."; for Jupiter, :48-50 "Methods must be annotated with @Test. Method prefix test* is not sufficient."
  • The PR: body "This removes spurious Test annotations and some junit4-specific syntax."; commit message "Drop @Test annotation on methods that are test* prefixed." Merged 13 minutes after it was opened, with no review or comment. No CHANGES.txt or MIGRATE.md entry retires these tests; 10.6.0, branch_10_6 and branch_10_5 keep every annotation, as did branch_10x until it was decommissioned (#16911, 2026-10-29).
The devil's-advocate check149 words

The strongest case (built on purpose by an intended-behaviour checker): the removal was deliberate, as the commit message says; nothing user-facing changes, so strictly it is not a Lucene bug; TestCandidateMatcherVisibility is a compile-time check that still compiles; the Hunspell runner is opt-in, run by no workflow, and fails loudly; and Lucene's no-match check is meant to catch filter typos, not empty classes.

Why it fails: the deliberate step is "drop @Test where test* already runs", which is false for exactly these six classes, and nobody chose to retire their tests. Their own comments, MIGRATE.md and the build option say they should run, and with the 18 annotations back all 18 are found again and 17 run and pass. For five of the six the loss is silent, including the whole distribution test module. It is a test-coverage regression and a small follow-up to #16028, not a product bug.

Suggested fix

Recommended A. Restore the 6 imports and 18 @Test annotations (all six classes)

Restore import org.junit.Test; and @Test on the 18 methods of all six classes (the exact reverse of #16028 in these files, +24 lines), on main and branch_11x. With it the repro passes, 17 of the 18 methods run and pass in Gradle (the GUI test is skipped, as before), and the monitor, core.tests and Hunspell suites pass.

--- a/lucene/core.tests/src/test/org/apache/lucene/core/tests/TestRuntimeDependenciesSane.java
+++ b/lucene/core.tests/src/test/org/apache/lucene/core/tests/TestRuntimeDependenciesSane.java
 import org.apache.lucene.index.IndexWriter;
+import org.junit.Test;
 
 /** Intentionally not a subclass of {@code LuceneTestCase}. */
 public class TestRuntimeDependenciesSane {
+  @Test
   public void testExternalDependenciesAreModules() {
# ... and the same for the other 17 test* methods of the six classes
All options and trade-offs4 options · 455 words

Option A was validated with Lucene's Gradle build in a scratch worktree of 645d4fe14a20. B and D are alternatives that change more; C is an optional guard against the same mistake recurring. The hunt's first fix sketch covered only three classes; the fix must cover all six.

A. Restore the 6 imports and 18 @Test annotations (all six classes) (validated by the checks) (recommended)

git diff bfa16d51c21 bfa16d51c21^ -- the six files: TestRuntimeDependenciesSane +5, TestModularLayer +9, TestCandidateMatcherVisibility +4, TestClassloadingDeadlock, TestScripts and TestHunspellRepositoryTestCases +2 each. It applies cleanly to 645d4fe14a20.

--- a/lucene/core.tests/src/test/org/apache/lucene/core/tests/TestRuntimeDependenciesSane.java
+++ b/lucene/core.tests/src/test/org/apache/lucene/core/tests/TestRuntimeDependenciesSane.java
 import org.apache.lucene.index.IndexWriter;
+import org.junit.Test;
 
 /** Intentionally not a subclass of {@code LuceneTestCase}. */
 public class TestRuntimeDependenciesSane {
+  @Test
   public void testExternalDependenciesAreModules() {
# ... and the same for the other 17 test* methods of the six classes

Trade-offs

None for production code: test sources only.

What the checks validated

  • Gradle, seed DEADBEEF: the repro passes with assertions on and off.
  • Selected by name: TestCandidateMatcherVisibility 3 tests, TestRuntimeDependenciesSane 4, TestClassloadingDeadlock 1 (testDeadlock), TestHunspellRepositoryTestCases 1 (a one-case test directory); all pass.
  • :lucene:distribution.tests:test on the assembled distribution: 9 tests, 0 failures, 1 skipped (8 of 8 TestModularLayer pass; testLukeCanBeLaunched is @RequiresGUI and is skipped on every run, also with -Ptests.gui=true and with CI=true, because the build does not pass tests.gui to the test JVM).
  • Unfiltered :lucene:monitor:test (185 tests, 1 skipped) and :lucene:core.tests:test (7), and the org.apache.lucene.analysis.hunspell.* tests (158, 13 skipped): 0 failures.
  • checkGoogleJavaFormat passes for core, core.tests, distribution.tests and analysis-common; monitor fails only on the repro file (one line to join).

B. Give the classes Lucene's method providers instead (not run)

Add @TestMethodProviders({LuceneJUnit3MethodProvider.class, JUnit4MethodProvider.class}) to the RandomizedRunner classes, or make the plain classes extend LuceneTestCase.

Trade-offs

Works against why the classes exist: they avoid LuceneTestCase on purpose, and the distribution tests must not depend on Lucene classes. It also does nothing for the plain and Parameterized classes unless they change runner.

What the checks validated

Not run.

C. Optional follow-up: a guard against the same mistake (not run)

A build or test-framework check that fails when a Test* class yields no test method, or that flags public void test*() methods with neither @Test nor LuceneTestCase.

Trade-offs

More build machinery; it must allow abstract bases, the Hunspell runner's assumption skip and the RandomizedRunner placeholder entry. Would have caught #16028 at once; not needed to fix this bug.

What the checks validated

Not run.

D. Move the six classes to JUnit 5 Jupiter (not run)

Use @org.junit.jupiter.api.Test, the direction main is taking with #16027.

Trade-offs

A larger change: Parameterized becomes @ParameterizedTest, and the RandomizedRunner features these classes use (RandomizedContext, the @RequiresGUI test group, @ThreadLeakScope) need Jupiter equivalents.

What the checks validated

Not run.

Downstream note

No downstream impact: only Lucene's own test sources on main changed, and no release jar contains them.

How it was found

Not formal. An AI tool ran Lucene's opt-in Hunspell conformance test against Hunspell's own test data; it failed with "No runnable methods". Git history led to #16028, and a scan of every class it touched found the classes outside LuceneTestCase. An independent verifier widened the set from three to six.

The method in full83 words
  • Trigger: running TestHunspellRepositoryTestCases with hunspell.repo.path pointing at Hunspell 1.7.2's test data (hunt unit on Hunspell conformance) gave InvalidTestClassError: No runnable methods.
  • Cause: git log on the class led to bfa16d51c21 (#16028). The finder scanned every class the commit touched for classes with neither LuceneTestCase ancestry nor @Test and found two plain JUnit 4 classes.
  • Widening: an independent reproducer ran each touched class through the JUnit Platform launcher, as the build does, and found the three RandomizedRunner classes that also lost their tests.

Prior reports, history and checks

Prior reports

No prior report: scripted searches (GitHub, commits, downstream trackers, four mailing lists, JIRA archive), 67 GitHub queries and all 410 open PRs by file (2026-10-26), and a GitHub refresh on 2026-11-01. Upstream main still has the change.

Searches5 searches

Closest items, none this bug: #16028 itself (no review or comment); #16027 (JUnit 5 infrastructure; "Method prefix test* is not sufficient" is said only for the Jupiter base class); #15949 (vintage engine switch); #16121 / #16140 / #16149 ("no tests matched" under -Dtests.iters, a different, fixed defect). The only open PR touching one of the files, #13949, does not restore @Test. Limits: keyword search; private channels not searched; one 2026-11-01 listing was incomplete.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues and PRs (incl. migrated JIRA), commits, downstream trackers, dev@, issues@, java-user@, commits@, JIRA archive2026-10-26two scripted search runs (89 API queries): the six class names, the method names, 16028, "No runnable methods", "no tests matched", the method-provider names, "missing @Test", "hunspell.repo.path"175 and 198 distinct hits, 0 high; none this bug
GitHub (GraphQL)2026-10-2667 queries: class names, symptoms, @Test wording, the mechanism, JUnit migration items since 2026-04-01 read in full, people, open items, downstream forksno report
Open PRs2026-10-26the changed-file lists of all 410 open apache/lucene PRsonly #13949 touches one file (TestModularLayer's module list); it does not restore @Test
GitHub search refresh2026-11-01TestRuntimeDependenciesSane, TestCandidateMatcherVisibility, TestClassloadingDeadlock, TestModularLayer (since 2026-10-20), "No runnable methods", hunspell.repo.path, 16028 (since 2026-05-03), junit and "@Test" (since 2026-10-26)only old or unrelated items (#571, #15864, #4875, #16953); #16937, a general tracking issue, does not mention it
Upstream main2026-11-01last commit and @Test count of the six files at f815e679bf6cnot fixed: 0 @Test in all six

History

Introduced on main by #16028 (bfa16d51c21, 2026-05-02); at its parent all 18 methods are found. branch_11x, cut from main on 2026-10-28, inherited it. The plain classes are silent rather than loud because main had moved to the JUnit vintage engine (#15949). No release has it: 10.6.0, branch_10_6, branch_10_5 and every release tag keep the annotations in the files they have.

Versions tested8 rows

Introduced by #16028 "Misc junit4 test cleanups" (bfa16d51c21, committed 2026-05-02, main only, milestone 11.0.0; parent 41ebd23821a); branch_11x inherited it when it was cut from main on 2026-10-28. git blame at the parent attributes every removed line to the commit that created the class or method, from LUCENE-6482 (2015) to #13632 (2024). The zero-test symptom of the plain classes comes from main's earlier switch to the JUnit Platform vintage engine (5d7f8bd46fb, #15949, 2026-04-10), which is not itself a defect: the parent already used it and found every method. Commit-level runs compiled each commit's test sources against main's jars; the change between them is annotation-only.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedGradle: the repro fails (assertions on and off); two classes "no tests matched", three green placeholders, Hunspell runner "No runnable methods"; with option A all 18 methods are found and 17 run and pass
main ea57360912ed, a9779ef3c193 affectedsix-class discovery fails 6 of 6 (-ea, -da); the earlier repro fails 2 of 2; the intended-behaviour check's Gradle runs show the same symptoms
bfa16d51c21 vs parent 41ebd23821a introducing commitparent: all 18 methods found (6 of 6 discovery checks pass); bfa16d51c21: none (6 of 6 fail)
upstream main f815e679bf6c (2026-11-01) read0 @Test lines in all six files; not fixed
branch_11x 603af870375 (2026-10-29) readthe 11.0 branch, cut from main on 2026-10-28 (c5ab3e57b9b): the six files byte-identical to 645d4fe14a20, 0 @Test; needs the backport
10.6.0 read3/4/1/8/1/1 @Test lines; the tag does not contain bfa16d51c21
9.12.3, 10.4.0, 10.5.0, 10.5.2 not affectedeach release's own test sources and jars: a discovery run finds all methods (classes absent in a release skipped)
9.0.0 to 10.5.2 tags, branch_10_6, branch_10_5 readeach of the six files that exists there has all its annotations (9.0.0 has only the Hunspell runner); branch_10x too until its decommissioning (#16911, 2026-10-29)

Independent checks

Confirmed by an independent reproducer (who widened it to six classes), an intended-behaviour devil's advocate with a real Gradle build, an adjudication, reviews by OpenAI GPT-5.6 (one a procedural "not real": it could not run code), a credibility audit, this doc's Gradle validation of the fix, and fact checks.

Checks and verdicts399 words

No check disputed the root cause; the one "not real" verdict came from a reviewer that could not run code; a separate re-run of the repro saw it fail.

Each independent check and its verdict
CheckVerdictWhat it found
finder reproducedIts two-class JUnitCore repro fails 2 of 2 with assertions on and off and passes with @Test restored.
cross-model review, refute lens (OpenAI GPT-5.6) not real, low (procedural)It could not run code; root cause agreed and the intended-behaviour argument rated weak; a separate run re-ran the repro: 2 of 2 fail.
Independent reproducer real, widened to six classesIts own JUnit Platform launcher run fails 6 of 6 classes (on and off); 10.5.2 and 9.12.3 pass; the annotation-only control passes 6 of 6.
Cross-model adversary, intended lens (OpenAI GPT-5.6) real, mediumIntended-behaviour argument weak; a separate run re-ran the repro (2 of 2 fail) and the fix (2 of 2 pass).
Intended-behaviour devil's advocate real, highReal Gradle build: "no tests matched" for the plain classes, "No runnable methods" for the Hunspell runner; with the fix 3, 4 and 1 tests pass.
adjudication confirmed, highConsole launcher on verbatim main sources: 0 / 0 tests, one class-named entry per RandomizedRunner class, initializationError for the Hunspell runner; annotation-only sources 3/4/1/8/1/1.
security screen noneTest sources only; the restored deadlock guard passes 20 of 20 forked runs.
credibility audit reproducesRe-ran the six-class check and the control; noted that TestScripts is GUI-gated, so "always-on" is loose for it.
minimal repro verifiedOne test; fails on main 645d4fe14a20 with assertions on and off; passes with the class's part of the fix.
doc validation (Gradle) fix A validatedOn 645d4fe14a20 the repro fails and four classes show the symptom; with fix A all six classes find their methods, 17 run and pass (the build skips the GUI test), and the closest suites pass. Closes two earlier gaps: RandomizedRunner classes in a real Gradle run, and restored TestModularLayer on the assembled distribution.
fact checks (2026-11-01) real; corrections appliedIndependent fact checks (one by OpenAI GPT-5.6) and a diagram check: bug and repro confirmed, a checker re-ran the Gradle repro and fix in its own scratch worktree. Corrected: the CI/GUI claim (the build skips the GUI test everywhere), branch_11x is affected, branch_10x is decommissioned, older-release runs show discovery, and wording details.

Diagram file

The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.

Sources and provenance

Finding record, Lucene and upstream sources, repro files and diagram notes1,080 words

Finding record

LFV-389, snapshot of 2026-11-06

Registry title
Test suite on main: #16028 removed @Test from six test classes that do not extend LuceneTestCase, so their 18 test methods never run (TestRuntimeDependenciesSane and TestCandidateMatcherVisibility report no tests; TestClassloadingDeadlock, TestModularLayer and TestScripts each report one green placeholder; the opt-in TestHunspellRepositoryTestCases fails with 'No runnable methods')
Area
test infrastructure / JUnit 4 test discovery (core, core.tests, monitor, distribution.tests, analysis-common hunspell)
Priority
Low, rank 1218: It affects only Lucene's own test suite on unreleased main (18 test methods silently do not run) and changes no shipped behaviour, but it loses regression coverage.
Impact class
minor
Impact note
Only Lucene's own test suite on unreleased main and branch_11x, since bfa16d51c21 (2026-05-02): every default main test run skips, without any error, the 16 test methods of four always-on classes (module-path sanity checks, the class-initialization deadlock guard, the distribution module-layer checks, monitor API visibility calls), no longer finds the Luke launch check (which the build skips on every run anyway), and the documented -Phunspell.repo.path conformance run fails with 'No runnable methods'; no shipped jar, API or index format changes and no release or 10.x branch is affected, so the cost is lost regression coverage, not wrong behaviour.
Summary
You run Lucene's test suite on main and expect TestRuntimeDependenciesSane, TestCandidateMatcherVisibility, TestClassloadingDeadlock, TestModularLayer and TestScripts to run their 17 test methods, and TestHunspellRepositoryTestCases to check Hunspell's test data when -Phunspell.repo.path is set. Instead the build passes without running any of those methods: #16028 removed their @Test annotations, which is harmless in LuceneTestCase subclasses (LuceneTestCase finds test* methods by name), but these six classes do not extend LuceneTestCase, so JUnit 4 finds only @Test methods and there are none. Two classes report zero tests, three report one green placeholder named after the class, and the Hunspell runner fails with 'No runnable methods'.
Introduced
bfa16d51c21b323e7ba5d9ca06f9150774ce4c7d, apache/lucene#16028 'Misc junit4 test cleanups' (by dweiss, merged to main 2026-05-02, milestone 11.0.0, 0 reviews, 0 comments; parent 41ebd23821a) [exec: the six classes' test sources at the parent discover all 18 methods (6 of 6 pass) and at bfa16d51c21 none (6 of 6 fail), both compiled against the main jars; the finder's JUnitCore repro passes at the parent and fails at bfa16d51c21] [read: the commit removes exactly 6 'import org.junit.Test;' and 18 '@Test' lines from these six classes, git blame at the parent attributes each removed line to the class's creation, and no later commit adds or removes an @Test line in them; 227255fdb08 (#16027, 2026-05-14) only edited a comment in TestClassloadingDeadlock]. The zero-test (rather than 'No runnable methods') symptom of the two plain JUnit 4 classes comes from main's earlier switch to the JUnit Platform vintage engine (5d7f8bd46fb, #15949, 2026-04-10, main only), which is not itself a defect. (2026-05-02)
First affected version
unreleased (main / 11.0.0-SNAPSHOT from bfa16d51c21 on; no release tag contains the commit, and releases 10.5.0-10.5.2, tagged later, come from branch_10_5, which never received it) [read]
Affected versions
main from bfa16d51c21 (2026-05-02) through 645d4fe14a20 [exec: six-class discovery fails 6 of 6 with -ea and -da at bfa16d51c21 and ea57360912ed; the finder's repro fails 2 of 2; the minimal repro fails through Gradle on 645d4fe14a20 with -ea and -da]; branch_11x (603af870375), cut from main on 2026-10-28, has the six files unchanged [read]; upstream main as of 2026-10-26 (head cf398bfb142c) still has no @Test in any of the six files [read]. Not affected: 9.12.3, 10.4.0, 10.5.0 and 10.5.2, each release's own test sources on its own jars [exec: all methods discovered; classes absent in a release skipped]; every release tag 9.0.0-10.5.2 and branch_10x, branch_10_6 and branch_10_5 have the annotations [read, by file content]. The defect travels with the test sources, not the jars (main's test sources on 10.5.2 jars fail 2 of 2) [exec].
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code (its low-confidence “not real” is procedural), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
non-formal
Upstream and prior reports
none found (2026-10-26: two scripted search runs (89 API queries over GitHub issues and PRs, commits, downstream repositories, the dev@, issues@, java-user@ and commits@ lists and the JIRA archive; 175 and 198 distinct hits, 0 high, 7 medium, all unrelated), 67 manual GitHub GraphQL queries, the changed-file lists of all 410 open apache/lucene PRs, list-archive and JIRA queries and 13 web queries; the independent reproduction's and the intended-behaviour check's own searches agree). Related items, none the same bug: #16028 (the change itself; no review or comment), #16027 (JUnit 5 infrastructure; says 'Method prefix test* is not sufficient' only for the new Jupiter base class), #15949 (vintage engine switch), #16121 / #16140 / #16149 ('no tests matched' under -Dtests.iters, a different defect). The only open PR touching one of the files, #13949, edits TestModularLayer's expected module list and does not restore @Test. Not fixed upstream (main head cf398bfb142c).
Lucene commit tested
main 645d4fe14a20 with Lucene's Gradle build in a scratch worktree, stock and with the fix, assertions on and off, and standalone; earlier mains ea57360912ed and a9779ef3c193; the parent 41ebd23821a and bfa16d51c21 (their test sources on main's jars); 9.12.3, 10.4.0, 10.5.0 and 10.5.2 (own test sources and jars). 10.6.0 and upstream main f815e679bf6c (2026-11-01) were read, not run.

Lucene and upstream sources

Finding record
  • Lucene main ea57360912ed: lucene/test-framework/src/java/org/apache/lucene/tests/util/LuceneTestCase.java:114 (@TestMethodProviders with LuceneJUnit3MethodProvider); lucene/core.tests/src/test/org/apache/lucene/core/tests/TestRuntimeDependenciesSane.java:22-23; lucene/monitor/src/test/org/apache/lucene/monitor/outsidepackage/TestCandidateMatcherVisibility.java:29; lucene/analysis/common/src/test/org/apache/lucene/analysis/hunspell/TestHunspellRepositoryTestCases.java:32-37, 76; lucene/core/src/test/org/apache/lucene/index/TestClassloadingDeadlock.java:51-59; lucene/distribution.tests/src/test/org/apache/lucene/distribution/AbstractLuceneDistributionTest.java:36-45; TestModularLayer.java:54; TestScripts.java:38-40; build-tools/build-infra/src/main/java/org/apache/lucene/gradle/plugins/java/TestsAndRandomizationPlugin.java:544-546, 568-569; lucene/analysis/common/build.gradle:244; lucene/MIGRATE.md:45-50
The finding in 30 seconds
  • lucene/test-framework/src/java/org/apache/lucene/tests/util/LuceneTestCase.java:113-114
  • lucene/test-framework/src/java/org/apache/lucene/tests/util/LuceneJUnit3MethodProvider.java:35-42
  • lucene/core.tests/src/test/org/apache/lucene/core/tests/TestRuntimeDependenciesSane.java:22-41
  • lucene/monitor/src/test/org/apache/lucene/monitor/outsidepackage/TestCandidateMatcherVisibility.java:29-60
  • lucene/core/src/test/org/apache/lucene/index/TestClassloadingDeadlock.java:51-59
  • lucene/distribution.tests/src/test/org/apache/lucene/distribution/AbstractLuceneDistributionTest.java:39-45
  • lucene/distribution.tests/src/test/org/apache/lucene/distribution/TestModularLayer.java:54, 114-372
  • lucene/distribution.tests/src/test/org/apache/lucene/distribution/TestScripts.java:38-40
  • lucene/analysis/common/src/test/org/apache/lucene/analysis/hunspell/TestHunspellRepositoryTestCases.java:32-37, 55-76
  • build-tools/build-infra/src/main/java/org/apache/lucene/gradle/plugins/java/TestsAndRandomizationPlugin.java:292-295, 545-546, 568-569
  • build-tools/build-infra/src/main/java/org/apache/lucene/gradle/plugins/java/FailOnNoMatchingFilteredTestsPlugin.java:36-38, 98
  • lucene/distribution.tests/src/test/org/apache/lucene/distribution/TestModularLayer.java:69-74
Who is affected
  • lucene/core/src/test/org/apache/lucene/index/TestClassloadingDeadlock.java:79, 85, 90, 108-120
  • .github/workflows/run-checks-mod-distribution.tests.yml:6-14, 48-49
  • .github/workflows/run-checks-mod-analysis-common-hunspell.yml:49; run-checks-all.yml:148
  • lucene/analysis/common/build.gradle:244
  • a Lucene clone: origin/branch_11x 603af870375 (six files byte-identical to 645d4fe14a20, 0 @Test); merge-base with main c5ab3e57b9b (2026-10-28)
Why it looks unintended
  • lucene/core.tests/src/test/org/apache/lucene/core/tests/TestRuntimeDependenciesSane.java:22
  • lucene/core/src/test/org/apache/lucene/index/TestClassloadingDeadlock.java:51-52
  • lucene/distribution.tests/src/test/org/apache/lucene/distribution/AbstractLuceneDistributionTest.java:39-42
  • lucene/analysis/common/src/test/org/apache/lucene/analysis/hunspell/TestHunspellRepositoryTestCases.java:32-34
  • lucene/analysis/common/build.gradle:244
  • lucene/MIGRATE.md:45-50
Suggested fix
  • lucene/distribution.tests/src/test/org/apache/lucene/distribution/AbstractLuceneDistributionTest.java:39-42, 44
  • lucene/core/src/test/org/apache/lucene/index/TestClassloadingDeadlock.java:20, 61
History
  • a Lucene clone: origin/branch_11x 603af870375, merge-base c5ab3e57b9b; origin/branch_10x 48cf3f515d6 (#16911, README.md and .gitignore only); 657014b3c57 (last development commit, annotations present)

Other statements trace to the hunt's working files (models, logs, reviews and briefs), which are not published.

Repro files

About the diagrams

Made in parallel with this doc from the mechanism in the brief; the PNG is the summary frame.

Built 2026-11-06 from the hunt's record of this finding and its doc file. Lucene main under test in this hunt: ea57360912ed. Every listed finding was re-run on Lucene main e1faeba08bde on 2026-11-02.