Skip to content

Commit a7b74d2

Browse files
committed
fix(gradle): fix gradle test gap
1 parent 5e3f66c commit a7b74d2

File tree

10 files changed

+123
-61
lines changed

10 files changed

+123
-61
lines changed

docs/generated/packages/gradle/executors/gradle.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
},
1616
"testClassName": {
1717
"type": "string",
18-
"description": "The test class name to run for test task."
18+
"description": "The full test name to run for test task (package name and class name)."
1919
},
2020
"args": {
2121
"oneOf": [

gradle/wrapper/gradle-wrapper.jar

59 Bytes
Binary file not shown.

gradle/wrapper/gradle-wrapper.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
44
networkTimeout=10000
55
validateDistributionUrl=true
66
zipStoreBase=GRADLE_USER_HOME

gradlew

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ case "$( uname )" in #(
114114
NONSTOP* ) nonstop=true ;;
115115
esac
116116

117-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
117+
CLASSPATH="\\\"\\\""
118118

119119

120120
# Determine the Java command to use to start the JVM.
@@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
213213
set -- \
214214
"-Dorg.gradle.appname=$APP_BASE_NAME" \
215215
-classpath "$CLASSPATH" \
216-
org.gradle.wrapper.GradleWrapperMain \
216+
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
217217
"$@"
218218

219219
# Stop when "xargs" is not available.

gradlew.bat

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,11 @@ goto fail
7070
:execute
7171
@rem Setup the command line
7272

73-
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73+
set CLASSPATH=
7474

7575

7676
@rem Execute Gradle
77-
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
77+
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
7878

7979
:end
8080
@rem End local scope for the variables with windows NT shell

packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/runner/GradleRunner.kt

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import dev.nx.gradle.util.logger
88
import java.io.ByteArrayOutputStream
99
import kotlinx.coroutines.async
1010
import kotlinx.coroutines.coroutineScope
11+
import org.gradle.tooling.BuildCancelledException
12+
import org.gradle.tooling.GradleConnector
1113
import org.gradle.tooling.ProjectConnection
1214
import org.gradle.tooling.events.OperationType
1315

@@ -130,7 +132,7 @@ fun runTestLauncher(
130132
outputStream: ByteArrayOutputStream,
131133
errorStream: ByteArrayOutputStream
132134
): Map<String, TaskResult> {
133-
val taskNames = tasks.values.map { it.taskName }.distinct().toTypedArray()
135+
val taskNames = tasks.values.map { it.taskName }.distinct()
134136
logger.info("📋 Collected ${taskNames.size} unique task names: ${taskNames.joinToString(", ")}")
135137

136138
val taskStartTimes = mutableMapOf<String, Long>()
@@ -147,29 +149,46 @@ fun runTestLauncher(
147149

148150
val globalStart = System.currentTimeMillis()
149151
var globalOutput: String
152+
val cancellationTokenSource = GradleConnector.newCancellationTokenSource()
150153

151154
try {
152155
connection
153156
.newTestLauncher()
154157
.apply {
155-
forTasks(*taskNames)
158+
forTasks(*taskNames.toTypedArray())
156159
tasks.values
157160
.mapNotNull { it.testClassName }
158161
.forEach {
159162
logger.info("Registering test class: $it")
160-
withArguments("--tests", it)
161163
withJvmTestClasses(it)
164+
withArguments("--tests", it)
162165
}
166+
167+
withArguments("--no-rebuild")
168+
withArguments("--isolate-projects") // Isolate project execution
169+
withArguments("--no-configure-on-demand") // Prevent auto-configuration
170+
163171
withArguments(*args.toTypedArray())
164172
setStandardOutput(outputStream)
165173
setStandardError(errorStream)
166174
addProgressListener(
167175
testListener(
168-
tasks, taskStartTimes, taskResults, testTaskStatus, testStartTimes, testEndTimes),
176+
tasks,
177+
taskStartTimes,
178+
taskResults,
179+
testTaskStatus,
180+
testStartTimes,
181+
testEndTimes,
182+
taskNames,
183+
cancellationTokenSource),
169184
OperationType.TEST)
185+
withCancellationToken(cancellationTokenSource.token()) // Add cancellation token
170186
}
171187
.run()
172188
globalOutput = buildTerminalOutput(outputStream, errorStream)
189+
} catch (e: BuildCancelledException) {
190+
globalOutput = buildTerminalOutput(outputStream, errorStream)
191+
logger.info("✅ Build cancelled gracefully by token.")
173192
} catch (e: Exception) {
174193
logger.warning(errorStream.toString())
175194
globalOutput =

packages/gradle/batch-runner/src/main/kotlin/dev/nx/gradle/runner/TestListener.kt

Lines changed: 65 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package dev.nx.gradle.runner
33
import dev.nx.gradle.data.GradleTask
44
import dev.nx.gradle.data.TaskResult
55
import dev.nx.gradle.util.logger
6+
import org.gradle.tooling.CancellationTokenSource
67
import org.gradle.tooling.events.ProgressEvent
78
import org.gradle.tooling.events.task.TaskFinishEvent
89
import org.gradle.tooling.events.task.TaskStartEvent
@@ -14,48 +15,74 @@ fun testListener(
1415
taskResults: MutableMap<String, TaskResult>,
1516
testTaskStatus: MutableMap<String, Boolean>,
1617
testStartTimes: MutableMap<String, Long>,
17-
testEndTimes: MutableMap<String, Long>
18-
): (ProgressEvent) -> Unit = { event ->
19-
when (event) {
20-
is TaskStartEvent,
21-
is TaskFinishEvent -> buildListener(tasks, taskStartTimes, taskResults)(event)
22-
is TestStartEvent -> {
23-
((event.descriptor as? JvmTestOperationDescriptor)?.className?.substringAfterLast('.')?.let {
24-
simpleClassName ->
25-
tasks.entries
26-
.find { entry -> entry.value.testClassName?.let { simpleClassName == it } ?: false }
27-
?.key
28-
?.let { nxTaskId ->
29-
testStartTimes.computeIfAbsent(nxTaskId) { event.eventTime }
30-
logger.info("🏁 Test start at ${event.eventTime}: $nxTaskId $simpleClassName")
18+
testEndTimes: MutableMap<String, Long>,
19+
expectedTestTasks: List<String>,
20+
cancellationTokenSource: CancellationTokenSource
21+
): (ProgressEvent) -> Unit {
22+
val completedTasks = mutableSetOf<String>()
23+
return { event ->
24+
when (event) {
25+
is TaskStartEvent,
26+
is TaskFinishEvent -> buildListener(tasks, taskStartTimes, taskResults)(event)
27+
28+
is TestStartEvent -> {
29+
logger.info("TestStartEvent $event")
30+
((event.descriptor as? JvmTestOperationDescriptor)?.className?.let { className ->
31+
tasks.entries
32+
.find { entry ->
33+
entry.value.testClassName?.let { className.endsWith(".${it}") || it == className }
34+
?: false
35+
}
36+
?.key
37+
?.let { nxTaskId ->
38+
testStartTimes.computeIfAbsent(nxTaskId) { event.eventTime }
39+
logger.info("🏁 Test start at ${event.eventTime}: $nxTaskId $className")
40+
}
41+
})
42+
}
43+
44+
is TestFinishEvent -> {
45+
if ((event.descriptor as? JvmTestOperationDescriptor)?.className == null &&
46+
event.descriptor.name.startsWith("Gradle Test Run ")) {
47+
val taskPath = event.descriptor.name.removePrefix("Gradle Test Run ").trim()
48+
49+
if (taskPath in expectedTestTasks && completedTasks.add(taskPath)) {
50+
logger.info("✅ Task succeeded: $taskPath")
51+
if (completedTasks.containsAll(expectedTestTasks) &&
52+
!cancellationTokenSource.token().isCancellationRequested) {
53+
logger.info("✅ All expected test tasks succeeded, cancelling execution.")
54+
cancellationTokenSource.cancel()
3155
}
32-
})
33-
}
34-
is TestFinishEvent -> {
35-
((event.descriptor as? JvmTestOperationDescriptor)?.className?.substringAfterLast('.')?.let {
36-
simpleClassName ->
37-
tasks.entries
38-
.find { entry -> entry.value.testClassName?.let { simpleClassName == it } ?: false }
39-
?.key
40-
?.let { nxTaskId ->
41-
testEndTimes.compute(nxTaskId) { _, _ -> event.result.endTime }
42-
when (event.result) {
43-
is TestSuccessResult ->
44-
logger.info(
45-
"\u2705 Test passed at ${event.result.endTime}: $nxTaskId $simpleClassName")
46-
is TestFailureResult -> {
47-
testTaskStatus[nxTaskId] = false
48-
logger.warning("\u274C Test failed: $nxTaskId $simpleClassName")
49-
}
56+
}
57+
}
58+
59+
((event.descriptor as? JvmTestOperationDescriptor)?.className?.let { className ->
60+
tasks.entries
61+
.find { entry ->
62+
entry.value.testClassName?.let { className.endsWith(".${it}") || it == className }
63+
?: false
64+
}
65+
?.key
66+
?.let { nxTaskId ->
67+
testEndTimes.compute(nxTaskId) { _, _ -> event.result.endTime }
68+
when (event.result) {
69+
is TestSuccessResult ->
70+
logger.info(
71+
"\u2705 Test passed at ${event.result.endTime}: $nxTaskId $className")
5072

51-
is TestSkippedResult ->
52-
logger.warning("\u26A0\uFE0F Test skipped: $nxTaskId $simpleClassName")
73+
is TestFailureResult -> {
74+
testTaskStatus[nxTaskId] = false
75+
logger.warning("\u274C Test failed: $nxTaskId $className")
76+
}
5377

54-
else ->
55-
logger.warning("\u26A0\uFE0F Unknown test result: $nxTaskId $simpleClassName")
78+
is TestSkippedResult ->
79+
logger.warning("\u26A0\uFE0F Test skipped: $nxTaskId $className")
80+
81+
else -> logger.warning("\u26A0\uFE0F Unknown test result: $nxTaskId $className")
82+
}
5683
}
57-
}
58-
})
84+
})
85+
}
5986
}
6087
}
6188
}

packages/gradle/project-graph/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ plugins {
1010

1111
group = "dev.nx.gradle"
1212

13-
version = "0.1.0"
13+
version = "0.0.1-alpha.5"
1414

1515
repositories { mavenCentral() }
1616

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

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ private val testFileNameRegex =
1212
Regex("^(?!(abstract|fake)).*?(Test)(s)?\\d*", RegexOption.IGNORE_CASE)
1313

1414
private val classDeclarationRegex = Regex("""class\s+([A-Za-z_][A-Za-z0-9_]*)""")
15+
private val packageDeclarationRegex =
16+
Regex("""package\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)""")
17+
18+
private data class TestClassInfo(
19+
val className: String,
20+
val packageName: String,
21+
val fullQualifiedName: String
22+
)
1523

1624
fun addTestCiTargets(
1725
testFiles: FileCollection,
@@ -31,12 +39,16 @@ fun addTestCiTargets(
3139
testFiles
3240
.filter { isTestFile(it, workspaceRoot) }
3341
.forEach { testFile ->
34-
val className = getTestClassNameIfAnnotated(testFile) ?: return@forEach
42+
val testClassInfo = getTestClassInfoIfAnnotated(testFile) ?: return@forEach
3543

36-
val targetName = "$ciTestTargetName--$className"
44+
val targetName = "$ciTestTargetName--${testClassInfo.className}"
3745
targets[targetName] =
3846
buildTestCiTarget(
39-
projectBuildPath, className, testFile, testTask, projectRoot, workspaceRoot)
47+
projectBuildPath,
48+
testClassInfo.fullQualifiedName,
49+
testTask,
50+
projectRoot,
51+
workspaceRoot)
4052
targetGroups[testCiTargetGroup]?.add(targetName)
4153

4254
ciDependsOn.add(mapOf("target" to targetName, "projects" to "self", "params" to "forward"))
@@ -56,7 +68,7 @@ fun addTestCiTargets(
5668
}
5769
}
5870

59-
private fun getTestClassNameIfAnnotated(file: File): String? {
71+
private fun getTestClassInfoIfAnnotated(file: File): TestClassInfo? {
6072
return file
6173
.takeIf { it.exists() }
6274
?.readText()
@@ -65,8 +77,16 @@ private fun getTestClassNameIfAnnotated(file: File): String? {
6577
}
6678
?.let { content ->
6779
val className = classDeclarationRegex.find(content)?.groupValues?.getOrNull(1)
80+
val packageName = packageDeclarationRegex.find(content)?.groupValues?.getOrNull(1)
81+
6882
return if (className != null && !className.startsWith("Fake")) {
69-
className
83+
val fullQualifiedName =
84+
if (packageName != null) {
85+
"$packageName.$className"
86+
} else {
87+
className
88+
}
89+
TestClassInfo(className, packageName ?: "", fullQualifiedName)
7090
} else {
7191
null
7292
}
@@ -85,7 +105,6 @@ private fun isTestFile(file: File, workspaceRoot: String): Boolean {
85105
private fun buildTestCiTarget(
86106
projectBuildPath: String,
87107
testClassName: String,
88-
testFile: File,
89108
testTask: Task,
90109
projectRoot: String,
91110
workspaceRoot: String,
@@ -96,11 +115,8 @@ private fun buildTestCiTarget(
96115
mutableMapOf<String, Any?>(
97116
"executor" to "@nx/gradle:gradle",
98117
"options" to
99-
mapOf(
100-
"taskName" to "${projectBuildPath}:${testTask.name}",
101-
"testClassName" to testClassName),
102-
"metadata" to
103-
getMetadata("Runs Gradle test $testClassName in CI", projectBuildPath, "test"),
118+
mapOf("taskName" to "${projectBuildPath}:${testTask.name}", "testClassName" to testClassName),
119+
"metadata" to getMetadata("Runs Gradle test $testClassName in CI", projectBuildPath, "test"),
104120
"cache" to true,
105121
"inputs" to taskInputs)
106122

packages/gradle/src/executors/gradle/schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
},
1212
"testClassName": {
1313
"type": "string",
14-
"description": "The test class name to run for test task."
14+
"description": "The full test name to run for test task (package name and class name)."
1515
},
1616
"args": {
1717
"oneOf": [

0 commit comments

Comments
 (0)