Leonard Brünings created GROOVY-12145:
-----------------------------------------
Summary: JUnit 5 migration silently disabled ~195 test methods:
base class removed but @Test not added
Key: GROOVY-12145
URL: https://issues.apache.org/jira/browse/GROOVY-12145
Project: Groovy
Issue Type: Bug
Reporter: Leonard Brünings
h2. Summary
The JUnit 5 migration commit {{a1897e0811}} ("Convert to JUnit5", Daniel Sun)
converted many JUnit3-style Groovy tests ({{extends GroovyTestCase}}, methods
named {{void testXxx()}}) to JUnit 5. The mechanical part of the conversion was
to (a) drop the {{GroovyTestCase}} base class and (b) add {{@Test}} to every
{{void testXxx()}} method.
For a number of files step (b) was *partially or completely skipped*. Because
the build uses {{useJUnitPlatform()}} and the JUnit Platform discovers tests
*only* by the {{@Test}} annotation (there is no longer a {{TestCase}} base
class for the vintage engine to discover), every un-annotated {{void
testXxx()}} method *silently stopped executing*. The build stays green because
"0 tests" is not a failure.
*Headline numbers* (see the Totals table for the full breakdown, and
"Verification and confidence" for how each portion was established):
* *~195 dead test methods across 32 files* total, spanning {{a1897e0811}} and
its sibling JUnit 5 migration commits.
* *~175 of those (27 files) are attributable to {{a1897e0811}} alone*; the
remaining ~20 (5 files) come from sibling migration commits and are listed
under category (c) for completeness.
* *~169 methods across 19 classes are fully dead and were confirmed by
execution* (the class discovers *0 tests*). The remaining *~26 partially-dead
methods are source-inferred only* (method present, annotation absent) and were
*not* individually confirmed by a per-method execution diff.
This is the same defect already known for {{groovy-console
AstNodeToScriptAdapterTest}} (63 methods) — see "Already tracked" below. That
file is *not* included in the counts here because it is being fixed under a
separate change.
h2. Root cause
* {{extends GroovyTestCase}} was removed from the class.
* {{@Test}} was *not* added (or was added to only some methods).
* No {{@RunWith}} / JUnit4 style and no {{Specification}} base remain.
* Result: JUnit Platform discovers *nothing* (fully-dead file) or *only the
annotated subset* (partially-dead file). The remaining {{testXxx()}} methods
never run.
A secondary variant of the same commit: in several bug tests the injected
{{import org.junit.jupiter.api.Test}} was placed *inside a triple-quoted script
string* (e.g. inside {{shell.evaluate """ ... """}}) instead of at the top of
the file — so the import is dead code inside test data and no annotation was
ever added.
A third, *collateral* variant: {{a1897e0811}} removed {{extends
GroovyTestCase}} from the *abstract base* {{BuilderTestSupport}}. Its two
concrete subclasses were never edited, but they inherited neither {{@Test}} nor
a discovering base, so they died too.
h2. Reproduction
Any listed fully-dead class produces "No tests found", e.g. (verified
empirically):
{code}
./gradlew :groovy-astbuilder:test --tests
'org.codehaus.groovy.ast.builder.AstBuilderFromSpecificationTest'
> Task :groovy-astbuilder:test FAILED
Gradle Test Run :groovy-astbuilder:test: 0 passed, 0 skipped (0 tests)
> No tests found for given includes:
[org.codehaus.groovy.ast.builder.AstBuilderFromSpecificationTest](--tests
filter)
{code}
For a file that still {{extends groovy.test.GroovyTestCase}} the vintage engine
runs it normally — those files are *fine* and are excluded from this report.
h2. Verification and confidence
*This section is authoritative.* Where the per-file tables below (which were
derived statically from source) disagree with what is stated here, the
executed/verified facts here win.
h3. Fully-dead classes — verified by execution
Every *fully-dead* class was confirmed by running it through a {{--tests}}
filter and observing {{0 tests}} / "No tests found" — not merely inferred from
source. Use the *fully-qualified* class name; a {{*Wildcard}} pattern does not
filter reliably in this build and silently runs a broad set instead.
*Confirmed dead (0 tests discovered):*
* Core {{:test}} (13):
{{org.codehaus.groovy.runtime.trampoline.TrampolineTest}},
{{bugs.Groovy779_Bug}}, {{groovy.util.ResourceBundleTest}},
{{org.codehaus.groovy.tools.rootloadersync.SubclassingInGroovyTest}},
{{org.codehaus.groovy.runtime.memoize.CacheCleanupTest}},
{{bugs.Groovy4414Bug}}, {{groovy.SingletonBugTest}}, {{groovy.StackTraceTest}},
{{bugs.Groovy7031Bug}}, {{bugs.Groovy8212Bug}}, {{bugs.Groovy4080Bug}},
{{bugs.Groovy6755Bug}}, {{bugs.Groovy8138Bug}}
* {{:groovy-astbuilder}} (1):
{{org.codehaus.groovy.ast.builder.AstBuilderFromSpecificationTest}}
* {{:groovy-xml}} (2, collateral via {{BuilderTestSupport}}):
{{groovy.xml.MarkupBuilderTest}}, {{groovy.xml.StreamingMarkupBuilderTest}}
* {{:groovy-templates}} (1): {{groovy.bugs.groovy4585.Groovy4585Bug}}
* {{:groovy-json}} (1): {{json.JsonTest}}
* {{:groovy-contracts}} (1):
{{org.apache.groovy.contracts.tests.post.ImplicitVariableNamesPostconditionTests}}
That is *19 classes verified by execution* (~169 dead test methods). These map
onto the tables below as: category (a) minus its abstract base (15 classes),
category (a2) (2 classes), and the two *fully-dead* rows of category (c) —
{{json.JsonTest}} and {{ImplicitVariableNamesPostconditionTests}}. The abstract
base {{BuilderTestSupport}} cannot be run directly; its deadness is *inferred*
but demonstrated in practice by its two concrete subclasses above, which do run
and discover 0 tests.
h3. Partially-dead classes — source-inferred only
The per-file counts for the *partially-dead* tables ((b) and the partial rows
of (c)) were derived *statically* and are *not* each confirmed by a per-method
execution diff. Two caveats apply:
* *A gross execution count cannot prove a specific named method is dead.* The
partial-dead claims rest on source inspection (method present,
{{@Test}}/{{@ParameterizedTest}} annotation absent) — sound, but not
execution-confirmed per method.
* *The static counts do not account for {{@ParameterizedTest}} methods*, so the
number of executions a class reports can *exceed* its {{@Test}} count.
Spot-checks: {{groovy.DynamicMemberTest}} ran 1 (matches its table),
{{SortableTransformTest}} ran 11 (matches), but {{groovy.ForLoopTest}} ran *26*
— parameterized inflation well above its tabulated 18 {{@Test}}. Its named
victim {{testClassicForWithMultiAssignment2}} nonetheless carries no
{{@Test}}/{{@ParameterizedTest}} and so is genuinely undiscoverable. Read the
"# test methods" / "# @Test" columns below as *source counts*, not as execution
counts.
h2. (a) Fully-dead files caused by {{a1897e0811}}
*Verified by execution* (each concrete class discovers 0 tests), except the
abstract base {{BuilderTestSupport}}, whose deadness is inferred via its
subclasses in (a2). Base class removed, *zero* {{@Test}} annotations added —
every method below never runs.
||File||# test methods||# @Test||stray import-in-string||
|{{subprojects/groovy-astbuilder/.../AstBuilderFromSpecificationTest.groovy}}|67|0|no|
|{{subprojects/groovy-xml/.../groovy/xml/BuilderTestSupport.groovy}} (abstract
base; inferred, see collateral below)|15|0|no|
|{{src/test/groovy/org/codehaus/groovy/runtime/trampoline/TrampolineTest.groovy}}|4|0|no|
|{{src/test/groovy/bugs/Groovy779_Bug.groovy}}|3|0|no|
|{{src/test/groovy/groovy/util/ResourceBundleTest.groovy}}|3|0|no|
|{{src/test/groovy/org/codehaus/groovy/tools/rootloadersync/SubclassingInGroovyTest.groovy}}|2|0|no|
|{{src/test/groovy/org/codehaus/groovy/runtime/memoize/CacheCleanupTest.groovy}}|2|0|no|
|{{src/test/groovy/bugs/Groovy4414Bug.groovy}}|2|0|no|
|{{src/test/groovy/groovy/SingletonBugTest.groovy}}|2|0|no|
|{{src/test/groovy/groovy/StackTraceTest.groovy}}|2|0|no|
|{{subprojects/groovy-templates/.../groovy/bugs/groovy4585/Groovy4585Bug.groovy}}|1|0|no|
|{{src/test/groovy/bugs/Groovy7031Bug.groovy}}|1|0|no|
|{{src/test/groovy/bugs/Groovy8212Bug.groovy}}|1|0|no|
|{{src/test/groovy/bugs/Groovy4080Bug.groovy}}|1|0|*YES*|
|{{src/test/groovy/bugs/Groovy6755Bug.groovy}}|1|0|*YES*|
|{{src/test/groovy/bugs/Groovy8138Bug.groovy}}|1|0|*YES*|
*Subtotal: 16 files, 108 dead test methods* (3 with the stray import-in-string
bug). Of these, 15 concrete classes are execution-confirmed; the abstract base
{{BuilderTestSupport}} is inferred.
h3. (a2) Collateral fully-dead files (base {{BuilderTestSupport}} was migrated)
*Verified by execution.* These two files were *not* edited by {{a1897e0811}},
but they {{extends BuilderTestSupport}}, whose {{GroovyTestCase}} base was
removed. They have no {{@Test}} of their own, so all inherited + local tests
are dead.
||File||# test methods (own + inherited)||# @Test||
|{{subprojects/groovy-xml/.../groovy/xml/MarkupBuilderTest.groovy}}|29 (14 own
+ 15 inherited)|0|
|{{subprojects/groovy-xml/.../groovy/xml/StreamingMarkupBuilderTest.groovy}}|18
(3 own + 15 inherited)|0|
*Subtotal: 2 files, 47 dead test executions.*
h2. (b) Partially-dead files caused by {{a1897e0811}}
*Source-inferred only* (see "Partially-dead classes" above): the counts are
static source counts and are not each confirmed by a per-method execution diff.
h3. (b1) Base class removed, only *some* {{@Test}} added
||File||# test methods||# @Test||# dead||dead methods||
|{{src/test/groovy/groovy/DynamicMemberTest.groovy}}|6|1|5|{{testGStringMethodCall}},
{{testGStringPropertyAccess}}, {{testStringMethodCallAndAttributeAccess}},
{{testDynamicAttributeAccess}}, {{testDynamicMethodClosure}}|
|{{src/test/groovy/groovy/lang/ScriptCacheTest.groovy}}|2|1|1|{{testScriptCaching}}|
h3. (b2) Touched by {{a1897e0811}}, single/few {{testXxx()}} left un-annotated
The bulk of these classes are annotated, but the listed methods were
overlooked. Verified as *standalone* tests (not helpers — each is referenced
only by its own declaration).
||File||# test methods||# @Test||# dead||dead methods||
|{{subprojects/groovy-json/.../groovy/json/JsonSlurperLaxTest.groovy}}|8 own
(+60 inherited @Test)|60 inherited|8|{{testNullEmptyMalformedPayloads}},
{{testObjectWithSimpleValues}}, {{testArrayOfArrayWithSimpleValues}},
{{testBackSlashEscaping}}, {{testLaxCommentsAndKeys}}, {{testInvalidNumbers}}*,
{{testGroovy7728}}, {{testGroovy9954}}|
|{{subprojects/groovy-json/.../groovy/json/JsonSlurperCharSourceTest.groovy}}|1
own (+60 inherited @Test)|60 inherited|1|{{testParsingDecimalValue}}|
|{{src/test/groovy/gls/annotations/AnnotationTest.groovy}}|64|63|1|{{testAnnotateAnnotationDefinitionWithAnnotationWithAnnotationTypeTarget}}|
|{{src/test/groovy/org/codehaus/groovy/classgen/asm/sc/StaticCompileFlowTypingTest.groovy}}|7|6|1|{{testFlowTyping4}}|
|{{src/test/groovy/org/codehaus/groovy/control/CompilerConfigurationTest.java}}|6|5|1|{{testSetViaSystemProperties}}|
|{{src/test/groovy/bugs/Groovy11046.groovy}}|2|1|1|{{testMissingDependency2}}|
|{{subprojects/groovy-contracts/.../tests/doc/RootClassExampleTests.groovy}}|18|17|1|{{test_postcond_with_result_and_old_variables_switched}}|
{{*}} {{JsonSlurperLaxTest.testInvalidNumbers}} is an {{@Override}} of a
{{@Test}} method in the parent {{JsonSlurperTest}}; whether it executes depends
on Jupiter's inherited-annotation resolution — treat as suspect. The other 7
Lax-specific methods are unambiguously dead. ({{JsonSlurperLaxTest}} /
{{JsonSlurperCharSourceTest}} still run the 60 inherited parent tests, so only
the subclass-specific methods are lost.)
*Subtotal (b): 9 files, 20 dead test methods (source-inferred).*
h2. (c) Related — same defect, introduced by sibling JUnit 5 migration commits
(not {{a1897e0811}})
Found during the same sweep. Same root cause, different commit; listed for
completeness. The two *fully dead* rows ({{JsonTest}},
{{ImplicitVariableNamesPostconditionTests}}) are *execution-verified* (see
above); the three partial rows are *source-inferred only* and are subject to
the {{@ParameterizedTest}} caveat (notably {{ForLoopTest}} actually executes
26, above its tabulated 18 {{@Test}}).
||File||# test methods||# @Test||# dead||verification||migrating commit /
cause||
|{{subprojects/groovy-json/src/spec/test/json/JsonTest.groovy}}|13|0|13 (fully
dead)|execution-verified|{{92cbb3f39c}} "minor refactor: junit5" — base
removed, no {{@Test}}|
|{{src/test/groovy/org/codehaus/groovy/transform/SortableTransformTest.groovy}}|15|11|4|source-inferred|{{5afd9b9a91}}
"junit jupiter" removed {{GroovyTestCase}} from base {{CompilableTestSupport}}|
|{{src/test/groovy/gls/invocation/MethodSelectionTest.groovy}}|18|17|1|source-inferred|same
— base {{CompilableTestSupport}}|
|{{subprojects/groovy-contracts/.../tests/post/ImplicitVariableNamesPostconditionTests.groovy}}|1|0|1
(fully dead)|execution-verified|{{extends BaseTestClass}} (no {{@Test}}, no
discovering base); pre-existing / not {{a1897e0811}}|
|{{src/test/groovy/groovy/ForLoopTest.groovy}}|20|18|1
({{testClassicForWithMultiAssignment2}})|source-inferred (class runs 26 via
{{@ParameterizedTest}}; named victim still un-annotated)|migrated by an earlier
commit; single missed {{@Test}}|
*Subtotal (c): 5 files, 20 dead test methods* (2 files / 14 methods
execution-verified fully dead; 3 files / 6 methods source-inferred partial).
h2. Totals
The "# Dead test methods" figures are the source-derived method counts; the
"Verified by" column records how the deadness was established (execution vs.
source inspection), per the "Verification and confidence" section.
||Category||Files||Dead test methods||Verified by||
|(a) Fully-dead, {{a1897e0811}} base removed|16|108|execution (15 concrete
classes) + inference for abstract base {{BuilderTestSupport}}|
|(a2) Collateral fully-dead ({{BuilderTestSupport}} subclasses)|2|47|execution|
|(b) Partially-dead, {{a1897e0811}}|9|20|source inspection only|
|*{{a1897e0811}} total*|*27*|*~175*| |
|(c) Related (sibling migration commits)|5|20|execution (2 fully-dead classes,
14 methods) + source inspection (3 partial classes, 6 methods)|
|*Grand total*|*32*|*~195*| |
*Verified-vs-inferred split of the grand total:* ~169 methods across *19
fully-dead classes* are confirmed by execution; the remaining ~26 methods
(categories (b) and the partial rows of (c)) are *source-inferred only*.
h2. Suggested fix
* For every file above, add {{@Test}} (import {{org.junit.jupiter.api.Test}})
to each standalone {{void testXxx()}} method that should run.
* For the *stray import-in-string* files ({{Groovy4080Bug}}, {{Groovy6755Bug}},
{{Groovy8138Bug}}) — and the already-tracked {{AstNodeToScriptAdapterTest}} —
move {{import org.junit.jupiter.api.Test}} out of the triple-quoted script
string to the top-level import block, then annotate the methods.
* For the {{BuilderTestSupport}} / {{CompilableTestSupport}} families: either
annotate all inherited {{testXxx()}} in the concrete subclasses
({{MarkupBuilderTest}}, {{StreamingMarkupBuilderTest}},
{{SortableTransformTest}}, {{MethodSelectionTest}}), or restore a
JUnit-discoverable arrangement.
* Consider a CI guard: fail the build (or a lint check) when a test *class
file* named {{*Test}}/{{*Bug}} contributes *0* discovered tests, to prevent
silent regressions of this kind.
h2. Already tracked / excluded
*
{{subprojects/groovy-console/.../groovy/console/ui/AstNodeToScriptAdapterTest.groovy}}
(63 dead methods + stray import-in-string in
{{testAtImmutableClassWithProperties}}) is the original signature of this bug
and is *already being fixed under a separate change* GROOVY-12144 —
cross-reference only, not counted above.
h3. Method notes (how the list was produced)
* Started from the {{git show --name-only a1897e0811}} file set (1225 source
files).
* Parsed each file with string/comment masking so that {{void testXxx()}} and
{{@Test}} occurrences *inside triple-quoted script strings* are ignored (this
removed many false positives such as {{Groovy10133}} and
{{TraitASTTransformationTest}}, whose "missing" annotations were embedded test
scripts / a custom {{@CompileModesTest}} = {{@ParameterizedTest}} collector).
* Recognized custom test meta-annotations (e.g. {{@ParameterizedTest}}
collectors) and resolved class-inheritance chains so subclasses of a
still-{{GroovyTestCase}} base (e.g. {{StaticImportTest extends
groovy.test.GroovyTestCase}}) are correctly treated as *fine*.
* Excluded parameterized/helper methods (those with a parameter list, e.g.
{{AuthorTestHelper.testAuthorDataSet(Sql)}}) and private methods.
* Confirmed attribution with {{git show a1897e0811 -- <file>}} (base-class
removal + net {{@Test}} added).
* *Fully-dead classes were then confirmed empirically* via Gradle ({{--tests}}
filter → "No tests found"); *partially-dead classes were left at
source-inferred confidence* because a gross execution count (inflated by
{{@ParameterizedTest}}) cannot prove a specific named method is dead — see
"Verification and confidence".
{panel:title=Provenance}
Assisted-by: Claude Code (Opus 4.8).
{panel}
--
This message was sent by Atlassian Jira
(v8.20.10#820010)