Skip to content

Fix gradle test gap #31313

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/generated/packages/gradle/executors/gradle.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
},
"testClassName": {
"type": "string",
"description": "The test class name to run for test task."
"description": "The full test name to run for test task (package name and class name)."
},
"args": {
"oneOf": [
Expand Down
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
4 changes: 2 additions & 2 deletions gradlew
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac

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


# Determine the Java command to use to start the JVM.
Expand Down Expand Up @@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"

# Stop when "xargs" is not available.
Expand Down
4 changes: 2 additions & 2 deletions gradlew.bat
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ goto fail
:execute
@rem Setup the command line

set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
set CLASSPATH=


@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%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" %*

:end
@rem End local scope for the variables with windows NT shell
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ fun main(args: Array<String>) {
var connection: ProjectConnection? = null

try {
connection =
GradleConnector.newConnector().forProjectDirectory(File(options.workspaceRoot)).connect()
val connector = GradleConnector.newConnector().forProjectDirectory(File(options.workspaceRoot))
connection = connector.connect()

val results = runBlocking {
runTasksInParallel(connection, options.tasks, options.args, options.excludeTasks)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import dev.nx.gradle.util.logger
import java.io.ByteArrayOutputStream
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import org.gradle.tooling.BuildCancelledException
import org.gradle.tooling.ProjectConnection
import org.gradle.tooling.events.OperationType

Expand All @@ -33,8 +34,8 @@ suspend fun runTasksInParallel(
// --info is for terminal per task
// --continue is for continue running tasks if one failed in a batch
// --parallel is for performance
// -Dorg.gradle.daemon.idletimeout=10000 is to kill daemon after 10 seconds
addAll(listOf("--info", "--continue", "-Dorg.gradle.daemon.idletimeout=10000"))
// -Dorg.gradle.daemon.idletimeout=10000 is to kill daemon after 0 ms
addAll(listOf("--info", "--continue", "-Dorg.gradle.daemon.idletimeout=0"))
addAll(additionalArgs.split(" ").filter { it.isNotBlank() })
excludeTasks.forEach {
add("--exclude-task")
Expand Down Expand Up @@ -130,7 +131,7 @@ fun runTestLauncher(
outputStream: ByteArrayOutputStream,
errorStream: ByteArrayOutputStream
): Map<String, TaskResult> {
val taskNames = tasks.values.map { it.taskName }.distinct().toTypedArray()
val taskNames = tasks.values.map { it.taskName }.distinct()
logger.info("📋 Collected ${taskNames.size} unique task names: ${taskNames.joinToString(", ")}")

val taskStartTimes = mutableMapOf<String, Long>()
Expand All @@ -144,6 +145,7 @@ fun runTestLauncher(
testTaskStatus[nxTaskId] = true
}
}
val expectedTestClasses = tasks.values.mapNotNull { it.testClassName }.toSet()

val globalStart = System.currentTimeMillis()
var globalOutput: String
Expand All @@ -152,24 +154,25 @@ fun runTestLauncher(
connection
.newTestLauncher()
.apply {
forTasks(*taskNames)
tasks.values
.mapNotNull { it.testClassName }
.forEach {
logger.info("Registering test class: $it")
withArguments("--tests", it)
withJvmTestClasses(it)
}
withArguments(*args.toTypedArray())
forTasks(*taskNames.toTypedArray())
expectedTestClasses.forEach {
logger.info("Registering test class: $it")
withJvmTestClasses(it)
}
addArguments(*args.toTypedArray())
setStandardOutput(outputStream)
setStandardError(errorStream)
addProgressListener(
testListener(
tasks, taskStartTimes, taskResults, testTaskStatus, testStartTimes, testEndTimes),
OperationType.TEST)
withDetailedFailure()
}
.run()
globalOutput = buildTerminalOutput(outputStream, errorStream)
} catch (e: BuildCancelledException) {
globalOutput = buildTerminalOutput(outputStream, errorStream)
logger.info("✅ Build cancelled gracefully by token.")
} catch (e: Exception) {
logger.warning(errorStream.toString())
globalOutput =
Expand All @@ -181,12 +184,14 @@ fun runTestLauncher(
}

val globalEnd = System.currentTimeMillis()
val maxEndTime = testEndTimes.values.maxOrNull() ?: globalEnd
val delta = globalEnd - maxEndTime

tasks.forEach { (nxTaskId, taskConfig) ->
if (taskConfig.testClassName != null) {
val success = testTaskStatus[nxTaskId] ?: false
val startTime = testStartTimes[nxTaskId] ?: globalStart
val endTime = testEndTimes[nxTaskId] ?: globalEnd
val endTime = testEndTimes[nxTaskId]?.plus(delta) ?: globalEnd

if (!taskResults.containsKey(nxTaskId)) {
taskResults[nxTaskId] = TaskResult(success, startTime, endTime, "")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,48 +14,56 @@ fun testListener(
taskResults: MutableMap<String, TaskResult>,
testTaskStatus: MutableMap<String, Boolean>,
testStartTimes: MutableMap<String, Long>,
testEndTimes: MutableMap<String, Long>
): (ProgressEvent) -> Unit = { event ->
when (event) {
is TaskStartEvent,
is TaskFinishEvent -> buildListener(tasks, taskStartTimes, taskResults)(event)
is TestStartEvent -> {
((event.descriptor as? JvmTestOperationDescriptor)?.className?.substringAfterLast('.')?.let {
simpleClassName ->
tasks.entries
.find { entry -> entry.value.testClassName?.let { simpleClassName == it } ?: false }
?.key
?.let { nxTaskId ->
testStartTimes.computeIfAbsent(nxTaskId) { event.eventTime }
logger.info("🏁 Test start at ${event.eventTime}: $nxTaskId $simpleClassName")
}
})
}
is TestFinishEvent -> {
((event.descriptor as? JvmTestOperationDescriptor)?.className?.substringAfterLast('.')?.let {
simpleClassName ->
tasks.entries
.find { entry -> entry.value.testClassName?.let { simpleClassName == it } ?: false }
?.key
?.let { nxTaskId ->
testEndTimes.compute(nxTaskId) { _, _ -> event.result.endTime }
when (event.result) {
is TestSuccessResult ->
logger.info(
"\u2705 Test passed at ${event.result.endTime}: $nxTaskId $simpleClassName")
is TestFailureResult -> {
testTaskStatus[nxTaskId] = false
logger.warning("\u274C Test failed: $nxTaskId $simpleClassName")
}
testEndTimes: MutableMap<String, Long>,
): (ProgressEvent) -> Unit {
return { event ->
logger.info("event $event")
when (event) {
is TaskStartEvent,
is TaskFinishEvent -> buildListener(tasks, taskStartTimes, taskResults)(event)

is TestStartEvent -> {
logger.info("TestStartEvent $event")
((event.descriptor as? JvmTestOperationDescriptor)?.className?.let { className ->
tasks.entries
.find { entry ->
entry.value.testClassName?.let { className.endsWith(".${it}") || it == className }
?: false
}
?.key
?.let { nxTaskId ->
testStartTimes.computeIfAbsent(nxTaskId) { event.eventTime }
logger.info("🏁 Test start at ${event.eventTime}: $nxTaskId $className")
}
})
}

is TestFinishEvent -> {
val className = (event.descriptor as? JvmTestOperationDescriptor)?.className
className?.let {
tasks.entries
.find { entry ->
entry.value.testClassName?.let { className.endsWith(".${it}") || it == className }
?: false
}
?.key
?.let { nxTaskId ->
testEndTimes.compute(nxTaskId) { _, _ -> event.result.endTime }
when (event.result) {
is TestSuccessResult ->
logger.info("✅ Test passed at ${event.result.endTime}: $nxTaskId $className")

is TestSkippedResult ->
logger.warning("\u26A0\uFE0F Test skipped: $nxTaskId $simpleClassName")
is TestFailureResult -> {
testTaskStatus[nxTaskId] = false
logger.warning("❌ Test failed: $nxTaskId $className")
}

else ->
logger.warning("\u26A0\uFE0F Unknown test result: $nxTaskId $simpleClassName")
is TestSkippedResult -> logger.warning("⚠️ Test skipped: $nxTaskId $className")
else -> logger.warning("⚠️ Unknown test result: $nxTaskId $className")
}
}
}
})
}
}
}
}
}
2 changes: 1 addition & 1 deletion packages/gradle/project-graph/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ plugins {

group = "dev.nx.gradle"

version = "0.1.0"
version = "0.0.1-alpha.5"

repositories { mavenCentral() }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ private val testFileNameRegex =
Regex("^(?!(abstract|fake)).*?(Test)(s)?\\d*", RegexOption.IGNORE_CASE)

private val classDeclarationRegex = Regex("""class\s+([A-Za-z_][A-Za-z0-9_]*)""")
private val packageDeclarationRegex =
Regex("""package\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)""")

private data class TestClassInfo(
val className: String,
val packageName: String,
val fullQualifiedName: String
)

fun addTestCiTargets(
testFiles: FileCollection,
Expand All @@ -31,12 +39,16 @@ fun addTestCiTargets(
testFiles
.filter { isTestFile(it, workspaceRoot) }
.forEach { testFile ->
val className = getTestClassNameIfAnnotated(testFile) ?: return@forEach
val testClassInfo = getTestClassInfoIfAnnotated(testFile) ?: return@forEach

val targetName = "$ciTestTargetName--$className"
val targetName = "$ciTestTargetName--${testClassInfo.className}"
targets[targetName] =
buildTestCiTarget(
projectBuildPath, className, testFile, testTask, projectRoot, workspaceRoot)
projectBuildPath,
testClassInfo.fullQualifiedName,
testTask,
projectRoot,
workspaceRoot)
targetGroups[testCiTargetGroup]?.add(targetName)

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

private fun getTestClassNameIfAnnotated(file: File): String? {
private fun getTestClassInfoIfAnnotated(file: File): TestClassInfo? {
return file
.takeIf { it.exists() }
?.readText()
Expand All @@ -65,8 +77,16 @@ private fun getTestClassNameIfAnnotated(file: File): String? {
}
?.let { content ->
val className = classDeclarationRegex.find(content)?.groupValues?.getOrNull(1)
val packageName = packageDeclarationRegex.find(content)?.groupValues?.getOrNull(1)

return if (className != null && !className.startsWith("Fake")) {
className
val fullQualifiedName =
if (packageName != null) {
"$packageName.$className"
} else {
className
}
TestClassInfo(className, packageName ?: "", fullQualifiedName)
} else {
null
}
Expand All @@ -85,7 +105,6 @@ private fun isTestFile(file: File, workspaceRoot: String): Boolean {
private fun buildTestCiTarget(
projectBuildPath: String,
testClassName: String,
testFile: File,
testTask: Task,
projectRoot: String,
workspaceRoot: String,
Expand All @@ -96,11 +115,8 @@ private fun buildTestCiTarget(
mutableMapOf<String, Any?>(
"executor" to "@nx/gradle:gradle",
"options" to
mapOf(
"taskName" to "${projectBuildPath}:${testTask.name}",
"testClassName" to testClassName),
"metadata" to
getMetadata("Runs Gradle test $testClassName in CI", projectBuildPath, "test"),
mapOf("taskName" to "${projectBuildPath}:${testTask.name}", "testClassName" to testClassName),
"metadata" to getMetadata("Runs Gradle test $testClassName in CI", projectBuildPath, "test"),
"cache" to true,
"inputs" to taskInputs)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ExecutorContext, output, TaskGraph, workspaceRoot } from '@nx/devkit';
import runCommandsImpl, {
import {
LARGE_BUFFER,
RunCommandsOptions,
} from 'nx/src/executors/run-commands/run-commands.impl';
Expand Down
2 changes: 1 addition & 1 deletion packages/gradle/src/executors/gradle/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
},
"testClassName": {
"type": "string",
"description": "The test class name to run for test task."
"description": "The full test name to run for test task (package name and class name)."
},
"args": {
"oneOf": [
Expand Down
Loading