Skip to content

Commit b54b839

Browse files
lourwnx-cloud[bot]
authored andcommitted
fix(gradle): remove annotations from atomizer (#34871)
<!-- Please make sure you have read the submission guidelines before posting an PR --> <!-- https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr --> <!-- Please make sure that your commit message follows our format --> <!-- Example: `fix(nx): must begin with lowercase` --> <!-- If this is a particularly complex change or feature addition, you can request a dedicated Nx release for this pull request branch. Mention someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they will confirm if the PR warrants its own release for testing purposes, and generate it for you if appropriate. --> ## Current Behavior <!-- This is the behavior we have today --> The Gradle plugin's test class atomizer was incorrectly treating Kotlin annotation class declarations as test targets, generating invalid test tasks for them. This caused issues when projects defined custom annotations alongside their test classes—the atomizer would attempt to run annotation classes as tests. ## Expected Behavior Both the AST-based parser and the regex fallback parser now skip annotation classes, matching the existing behavior for data classes, enum classes, sealed classes, and abstract classes. A new test suite covers all annotation class exclusion scenarios for both parsers, including files with multiple annotation classes and mixed annotation/test class files. ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes # --------- Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com> Co-authored-by: lourw <lourw@users.noreply.github.com>
1 parent d0f2482 commit b54b839

File tree

3 files changed

+155
-3
lines changed

3 files changed

+155
-3
lines changed

packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/utils/parsing/KotlinAstTestParser.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,13 @@ private fun processClass(
9999
return
100100
}
101101

102-
// Only include regular classes - skip data classes, object declarations, enum classes, etc.
102+
// Only include regular classes - skip data classes, object declarations, enum classes,
103+
// annotation classes, etc.
103104
if (ktClass.hasModifier(KtTokens.DATA_KEYWORD) ||
104105
ktClass.hasModifier(KtTokens.ENUM_KEYWORD) ||
105106
ktClass.hasModifier(KtTokens.SEALED_KEYWORD) ||
106-
ktClass.hasModifier(KtTokens.ABSTRACT_KEYWORD)) {
107+
ktClass.hasModifier(KtTokens.ABSTRACT_KEYWORD) ||
108+
ktClass.isAnnotation()) {
107109
return
108110
}
109111

packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/utils/parsing/RegexTestParser.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ private val fakeClassRegex =
1414
private val abstractClassRegex =
1515
Regex(
1616
"""^\s*(?:@\w+\s*)*(?:public\s+|protected\s+)?abstract\s+class\s+([A-Za-z_][A-Za-z0-9_]*)""")
17+
private val annotationClassRegex =
18+
Regex(
19+
"""^\s*(?:@\w+\s*)*(?:public\s+|protected\s+)?annotation\s+class\s+([A-Za-z_][A-Za-z0-9_]*)""")
1720

1821
/** Fallback to original regex-based parsing when AST parsing fails */
1922
fun parseTestClassesWithRegex(file: File): MutableMap<String, String>? {
@@ -30,9 +33,10 @@ fun parseTestClassesWithRegex(file: File): MutableMap<String, String>? {
3033
val trimmed = line.trimStart()
3134
val indent = line.indexOfFirst { !it.isWhitespace() }.takeIf { it >= 0 } ?: 0
3235

33-
// Skip private, internal, abstract, and fake classes
36+
// Skip private, internal, abstract, annotation, and fake classes
3437
if (excludedClassRegex.containsMatchIn(trimmed)) continue
3538
if (abstractClassRegex.containsMatchIn(trimmed)) continue
39+
if (annotationClassRegex.containsMatchIn(trimmed)) continue
3640
if (fakeClassRegex.containsMatchIn(trimmed)) continue
3741

3842
val match = classDeclarationRegex.find(trimmed)
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package dev.nx.gradle.utils.parsing
2+
3+
import java.io.File
4+
import kotlin.test.assertFalse
5+
import kotlin.test.assertNotNull
6+
import kotlin.test.assertTrue
7+
import org.junit.jupiter.api.Test
8+
import org.junit.jupiter.api.io.TempDir
9+
10+
class TestClassParserTest {
11+
12+
@TempDir lateinit var tempDir: File
13+
14+
@Test
15+
fun `should exclude annotation classes from Kotlin files`() {
16+
val file =
17+
File(tempDir, "AnnotationTest.kt").apply {
18+
writeText(
19+
"""
20+
package com.example
21+
import org.junit.jupiter.api.Test
22+
23+
@Target(AnnotationTarget.CLASS)
24+
@Retention(AnnotationRetention.RUNTIME)
25+
annotation class CustomTestAnnotation
26+
27+
@CustomTestAnnotation
28+
class ActualTestClass {
29+
@Test
30+
fun testMethod() {}
31+
}
32+
"""
33+
.trimIndent())
34+
}
35+
36+
val result = getAllVisibleClassesWithNestedAnnotation(file)
37+
38+
assertNotNull(result)
39+
assertTrue(result.containsKey("ActualTestClass"))
40+
assertFalse(
41+
result.containsKey("CustomTestAnnotation"),
42+
"Annotation classes should not be included as test targets")
43+
}
44+
45+
@Test
46+
fun `should exclude annotation classes from regex fallback`() {
47+
val file =
48+
File(tempDir, "AnnotationTest.kt").apply {
49+
writeText(
50+
"""
51+
package com.example
52+
import org.junit.jupiter.api.Test
53+
54+
annotation class SomeAnnotation
55+
56+
class RealTest {
57+
@Test
58+
fun testMethod() {}
59+
}
60+
"""
61+
.trimIndent())
62+
}
63+
64+
val result = parseTestClassesWithRegex(file)
65+
66+
assertNotNull(result)
67+
assertTrue(result.containsKey("RealTest"))
68+
assertFalse(
69+
result.containsKey("SomeAnnotation"),
70+
"Annotation classes should not be included as test targets")
71+
}
72+
73+
@Test
74+
fun `should exclude multiple annotation classes in same file`() {
75+
val file =
76+
File(tempDir, "MultiAnnotation.kt").apply {
77+
writeText(
78+
"""
79+
package com.example
80+
import org.junit.jupiter.api.Test
81+
82+
@Target(AnnotationTarget.CLASS)
83+
annotation class FirstAnnotation
84+
85+
@Target(AnnotationTarget.CLASS)
86+
annotation class SecondAnnotation
87+
88+
@FirstAnnotation
89+
@SecondAnnotation
90+
class AnnotatedTest {
91+
@Test
92+
fun testMethod() {}
93+
}
94+
"""
95+
.trimIndent())
96+
}
97+
98+
val result = getAllVisibleClassesWithNestedAnnotation(file)
99+
100+
assertNotNull(result)
101+
assertTrue(result.containsKey("AnnotatedTest"))
102+
assertFalse(result.containsKey("FirstAnnotation"))
103+
assertFalse(result.containsKey("SecondAnnotation"))
104+
}
105+
106+
@Test
107+
fun `should still include regular test classes alongside annotation classes`() {
108+
val file =
109+
File(tempDir, "MixedFile.kt").apply {
110+
writeText(
111+
"""
112+
package com.example
113+
import org.junit.jupiter.api.Test
114+
import org.junit.jupiter.api.extension.ExtendWith
115+
116+
annotation class AssertFileChannelDataBlocksClosed
117+
118+
@ExtendWith(value = [])
119+
class ExtendWithTestClass {
120+
@Test
121+
fun testMethod() {}
122+
}
123+
124+
@AssertFileChannelDataBlocksClosed
125+
class CustomAnnotatedTestClass {
126+
@Test
127+
fun testMethod() {}
128+
}
129+
130+
annotation class TestAnnotation
131+
"""
132+
.trimIndent())
133+
}
134+
135+
val result = getAllVisibleClassesWithNestedAnnotation(file)
136+
137+
assertNotNull(result)
138+
assertTrue(result.containsKey("ExtendWithTestClass"))
139+
assertTrue(result.containsKey("CustomAnnotatedTestClass"))
140+
assertFalse(
141+
result.containsKey("AssertFileChannelDataBlocksClosed"),
142+
"Annotation class should not be a test target")
143+
assertFalse(
144+
result.containsKey("TestAnnotation"), "Annotation class should not be a test target")
145+
}
146+
}

0 commit comments

Comments
 (0)