[
https://issues.apache.org/jira/browse/GROOVY-12145?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Paul King reassigned GROOVY-12145:
----------------------------------
Assignee: Paul King
> 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
> Assignee: Paul King
> Priority: Major
>
> 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)