fix(gradle): fix gradle test running gaps (#31313)
<!-- 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 --> - for the atomized test, currenly, its testClassName is just the 1st class name in the file ## Expected Behavior <!-- This is the behavior we should expect with the changes in this PR --> - upgrade gradle to latest version from 8.13 to 8.14 - for test task, exclude all its depends on tasks - it currently only exclude its direct depends on, its children - now it will go down the dependency tree and exclude all of its depends on, its children and grandchildren - for the atomized test target, its testClassName will be the full package name - e.g. org.springframework.boot.autoconfigure.jersey.JerseyAutoConfigurationCustomObjectMapperProviderTest - add logics to handle nested class - exclude private class name <img width="1081" alt="Screenshot 2025-06-06 at 2 53 39 PM" src="https://github.com/user-attachments/assets/285792fb-f098-4511-85dc-ee1263d75929" /> - add build-ci target <img width="1140" alt="Screenshot 2025-06-10 at 10 21 06 AM" src="https://github.com/user-attachments/assets/25db4a3e-2794-4654-9a95-1b66d229340b" /> ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #
This commit is contained in:
parent
8daad98992
commit
7a53477adc
2
.gitignore
vendored
2
.gitignore
vendored
@ -96,6 +96,8 @@ node_modules/
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.specstory/**
|
||||
.cursorindexingignore
|
||||
# OS specific
|
||||
# Task files
|
||||
tasks.json
|
||||
|
||||
@ -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": [
|
||||
|
||||
@ -1,19 +1,18 @@
|
||||
package dev.nx.gradle
|
||||
|
||||
import com.google.gson.Gson
|
||||
import dev.nx.gradle.cli.configureLogger
|
||||
import dev.nx.gradle.cli.parseArgs
|
||||
import dev.nx.gradle.runner.runTasksInParallel
|
||||
import dev.nx.gradle.util.configureSingleLineLogger
|
||||
import dev.nx.gradle.util.logger
|
||||
import java.io.File
|
||||
import kotlin.system.exitProcess
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.gradle.tooling.GradleConnector
|
||||
import org.gradle.tooling.ProjectConnection
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val options = parseArgs(args)
|
||||
configureLogger(options.quiet)
|
||||
configureSingleLineLogger(options.quiet)
|
||||
logger.info("NxBatchOptions: $options")
|
||||
|
||||
if (options.workspaceRoot.isBlank()) {
|
||||
@ -26,15 +25,21 @@ fun main(args: Array<String>) {
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
var connection: ProjectConnection? = null
|
||||
var buildConnection: ProjectConnection? = null
|
||||
|
||||
try {
|
||||
connection =
|
||||
GradleConnector.newConnector().forProjectDirectory(File(options.workspaceRoot)).connect()
|
||||
val connector = GradleConnector.newConnector().forProjectDirectory(File(options.workspaceRoot))
|
||||
|
||||
val results = runBlocking {
|
||||
runTasksInParallel(connection, options.tasks, options.args, options.excludeTasks)
|
||||
}
|
||||
buildConnection = connector.connect()
|
||||
logger.info("🏁 Gradle connection open.")
|
||||
|
||||
val results =
|
||||
runTasksInParallel(
|
||||
buildConnection,
|
||||
options.tasks,
|
||||
options.args,
|
||||
options.excludeTasks,
|
||||
options.excludeTestTasks)
|
||||
|
||||
val reportJson = Gson().toJson(results)
|
||||
println(reportJson)
|
||||
@ -47,7 +52,7 @@ fun main(args: Array<String>) {
|
||||
exitProcess(1)
|
||||
} finally {
|
||||
try {
|
||||
connection?.close()
|
||||
buildConnection?.close()
|
||||
logger.info("✅ Gradle connection closed.")
|
||||
} catch (e: Exception) {
|
||||
logger.warning("⚠️ Failed to close Gradle connection cleanly: ${e.message}")
|
||||
|
||||
@ -4,7 +4,6 @@ import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import dev.nx.gradle.data.GradleTask
|
||||
import dev.nx.gradle.data.NxBatchOptions
|
||||
import dev.nx.gradle.util.logger
|
||||
|
||||
fun parseArgs(args: Array<String>): NxBatchOptions {
|
||||
val argMap = mutableMapOf<String, String>()
|
||||
@ -33,20 +32,15 @@ fun parseArgs(args: Array<String>): NxBatchOptions {
|
||||
argMap["--excludeTasks"]?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
|
||||
?: emptyList()
|
||||
|
||||
val excludeTestTasks =
|
||||
argMap["--excludeTestTasks"]?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
|
||||
?: emptyList()
|
||||
|
||||
return NxBatchOptions(
|
||||
workspaceRoot = argMap["--workspaceRoot"] ?: "",
|
||||
tasks = tasksMap,
|
||||
args = argMap["--args"] ?: "",
|
||||
quiet = argMap["--quiet"]?.toBoolean() ?: false,
|
||||
excludeTasks = excludeTasks)
|
||||
}
|
||||
|
||||
fun configureLogger(quiet: Boolean) {
|
||||
if (quiet) {
|
||||
logger.setLevel(java.util.logging.Level.OFF)
|
||||
logger.useParentHandlers = false
|
||||
logger.handlers.forEach { it.level = java.util.logging.Level.OFF }
|
||||
} else {
|
||||
logger.setLevel(java.util.logging.Level.INFO)
|
||||
}
|
||||
excludeTasks = excludeTasks,
|
||||
excludeTestTasks = excludeTestTasks)
|
||||
}
|
||||
|
||||
@ -5,5 +5,6 @@ data class NxBatchOptions(
|
||||
val tasks: Map<String, GradleTask>,
|
||||
val args: String,
|
||||
val quiet: Boolean,
|
||||
val excludeTasks: List<String>
|
||||
val excludeTasks: List<String>,
|
||||
val excludeTestTasks: List<String>
|
||||
)
|
||||
|
||||
@ -3,8 +3,6 @@ package dev.nx.gradle.runner
|
||||
import dev.nx.gradle.data.GradleTask
|
||||
import dev.nx.gradle.data.TaskResult
|
||||
import dev.nx.gradle.util.logger
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import org.gradle.tooling.events.ProgressEvent
|
||||
import org.gradle.tooling.events.task.TaskFailureResult
|
||||
import org.gradle.tooling.events.task.TaskFinishEvent
|
||||
@ -22,35 +20,38 @@ fun buildListener(
|
||||
.find { it.value.taskName == event.descriptor.taskPath }
|
||||
?.key
|
||||
?.let { nxTaskId ->
|
||||
taskStartTimes[nxTaskId] = min(System.currentTimeMillis(), event.eventTime)
|
||||
taskStartTimes[nxTaskId] = event.eventTime
|
||||
logger.info("🏁 Task start: $nxTaskId ${event.descriptor.taskPath}")
|
||||
}
|
||||
}
|
||||
|
||||
is TaskFinishEvent -> {
|
||||
val taskPath = event.descriptor.taskPath
|
||||
val success =
|
||||
when (event.result) {
|
||||
is TaskSuccessResult -> {
|
||||
logger.info("✅ Task finished successfully: $taskPath")
|
||||
true
|
||||
}
|
||||
|
||||
is TaskFailureResult -> {
|
||||
logger.warning("❌ Task failed: $taskPath")
|
||||
false
|
||||
}
|
||||
|
||||
else -> true
|
||||
}
|
||||
|
||||
val success = getTaskFinishEventSuccess(event, taskPath)
|
||||
tasks.entries
|
||||
.find { it.value.taskName == taskPath }
|
||||
?.key
|
||||
?.let { nxTaskId ->
|
||||
val endTime = max(System.currentTimeMillis(), event.eventTime)
|
||||
val endTime = event.result.endTime
|
||||
val startTime = taskStartTimes[nxTaskId] ?: event.result.startTime
|
||||
taskResults[nxTaskId] = TaskResult(success, startTime, endTime, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getTaskFinishEventSuccess(event: TaskFinishEvent, taskPath: String): Boolean {
|
||||
return when (event.result) {
|
||||
is TaskSuccessResult -> {
|
||||
logger.info("✅ Task finished successfully: $taskPath")
|
||||
true
|
||||
}
|
||||
|
||||
is TaskFailureResult -> {
|
||||
logger.warning("❌ Task failed: $taskPath")
|
||||
false
|
||||
}
|
||||
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,17 +6,17 @@ import dev.nx.gradle.runner.OutputProcessor.buildTerminalOutput
|
||||
import dev.nx.gradle.runner.OutputProcessor.finalizeTaskResults
|
||||
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
|
||||
|
||||
suspend fun runTasksInParallel(
|
||||
fun runTasksInParallel(
|
||||
connection: ProjectConnection,
|
||||
tasks: Map<String, GradleTask>,
|
||||
additionalArgs: String,
|
||||
excludeTasks: List<String>
|
||||
): Map<String, TaskResult> = coroutineScope {
|
||||
excludeTasks: List<String>,
|
||||
excludeTestTasks: List<String>
|
||||
): Map<String, TaskResult> {
|
||||
logger.info("▶️ Running all tasks in a single Gradle run: ${tasks.keys.joinToString(", ")}")
|
||||
|
||||
val (testClassTasks, buildTasks) = tasks.entries.partition { it.value.testClassName != null }
|
||||
@ -29,54 +29,62 @@ suspend fun runTasksInParallel(
|
||||
val outputStream2 = ByteArrayOutputStream()
|
||||
val errorStream2 = ByteArrayOutputStream()
|
||||
|
||||
val args = buildList {
|
||||
// --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"))
|
||||
addAll(additionalArgs.split(" ").filter { it.isNotBlank() })
|
||||
excludeTasks.forEach {
|
||||
add("--exclude-task")
|
||||
add(it)
|
||||
}
|
||||
// --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=0 is to kill daemon after 0 ms
|
||||
val cpuCores = Runtime.getRuntime().availableProcessors()
|
||||
val workersMax = (cpuCores * 0.5).toInt().coerceAtLeast(1)
|
||||
val args =
|
||||
mutableListOf(
|
||||
"--info",
|
||||
"--continue",
|
||||
"-Dorg.gradle.daemon.idletimeout=0",
|
||||
"--parallel",
|
||||
"-Dorg.gradle.workers.max=$workersMax")
|
||||
|
||||
if (additionalArgs.isNotBlank()) {
|
||||
val splitResult = additionalArgs.split(" ")
|
||||
val filteredResult = splitResult.filter { it.isNotBlank() }
|
||||
args.addAll(filteredResult)
|
||||
}
|
||||
|
||||
logger.info("🏳️ Args: ${args.joinToString(", ")}")
|
||||
|
||||
val buildJob = async {
|
||||
if (buildTasks.isNotEmpty()) {
|
||||
runBuildLauncher(
|
||||
connection,
|
||||
buildTasks.associate { it.key to it.value },
|
||||
args,
|
||||
outputStream1,
|
||||
errorStream1)
|
||||
} else emptyMap()
|
||||
}
|
||||
|
||||
val testJob = async {
|
||||
if (testClassTasks.isNotEmpty()) {
|
||||
runTestLauncher(
|
||||
connection,
|
||||
testClassTasks.associate { it.key to it.value },
|
||||
args,
|
||||
outputStream2,
|
||||
errorStream2)
|
||||
} else emptyMap()
|
||||
}
|
||||
|
||||
val allResults = mutableMapOf<String, TaskResult>()
|
||||
allResults.putAll(buildJob.await())
|
||||
allResults.putAll(testJob.await())
|
||||
|
||||
return@coroutineScope allResults
|
||||
if (buildTasks.isNotEmpty()) {
|
||||
val buildResults =
|
||||
runBuildLauncher(
|
||||
connection,
|
||||
buildTasks.associate { it.key to it.value },
|
||||
args,
|
||||
excludeTasks,
|
||||
outputStream1,
|
||||
errorStream1)
|
||||
allResults.putAll(buildResults)
|
||||
}
|
||||
|
||||
if (testClassTasks.isNotEmpty()) {
|
||||
val testResults =
|
||||
runTestLauncher(
|
||||
connection,
|
||||
testClassTasks.associate { it.key to it.value },
|
||||
args,
|
||||
excludeTestTasks,
|
||||
outputStream2,
|
||||
errorStream2)
|
||||
allResults.putAll(testResults)
|
||||
}
|
||||
|
||||
return allResults
|
||||
}
|
||||
|
||||
fun runBuildLauncher(
|
||||
connection: ProjectConnection,
|
||||
tasks: Map<String, GradleTask>,
|
||||
args: List<String>,
|
||||
excludeTasks: List<String>,
|
||||
outputStream: ByteArrayOutputStream,
|
||||
errorStream: ByteArrayOutputStream
|
||||
): Map<String, TaskResult> {
|
||||
@ -89,14 +97,17 @@ fun runBuildLauncher(
|
||||
val globalStart = System.currentTimeMillis()
|
||||
var globalOutput: String
|
||||
|
||||
val excludeArgs = excludeTasks.map { "--exclude-task=$it" }
|
||||
|
||||
try {
|
||||
connection
|
||||
.newBuild()
|
||||
.apply {
|
||||
forTasks(*taskNames)
|
||||
withArguments(*args.toTypedArray())
|
||||
addArguments(*(args + excludeArgs).toTypedArray())
|
||||
setStandardOutput(outputStream)
|
||||
setStandardError(errorStream)
|
||||
withDetailedFailure()
|
||||
addProgressListener(buildListener(tasks, taskStartTimes, taskResults), OperationType.TASK)
|
||||
}
|
||||
.run()
|
||||
@ -111,6 +122,13 @@ fun runBuildLauncher(
|
||||
}
|
||||
|
||||
val globalEnd = System.currentTimeMillis()
|
||||
val maxEndTime = taskResults.values.map { it.endTime }.maxOrNull() ?: globalEnd
|
||||
val minStartTime = taskResults.values.map { it.startTime }.minOrNull() ?: globalStart
|
||||
logger.info(
|
||||
"⏱️ Build start timing gap: ${minStartTime - globalStart}ms (time between first task start and build launcher start) ")
|
||||
logger.info(
|
||||
"⏱️ Build completion timing gap: ${globalEnd - maxEndTime}ms (time between last task finish and build end)")
|
||||
|
||||
finalizeTaskResults(
|
||||
tasks = tasks,
|
||||
taskResults = taskResults,
|
||||
@ -127,49 +145,47 @@ fun runTestLauncher(
|
||||
connection: ProjectConnection,
|
||||
tasks: Map<String, GradleTask>,
|
||||
args: List<String>,
|
||||
excludeTestTasks: List<String>,
|
||||
outputStream: ByteArrayOutputStream,
|
||||
errorStream: ByteArrayOutputStream
|
||||
): Map<String, TaskResult> {
|
||||
val taskNames = tasks.values.map { it.taskName }.distinct().toTypedArray()
|
||||
logger.info("📋 Collected ${taskNames.size} unique task names: ${taskNames.joinToString(", ")}")
|
||||
|
||||
val taskStartTimes = mutableMapOf<String, Long>()
|
||||
val taskResults = mutableMapOf<String, TaskResult>()
|
||||
val testTaskStatus = mutableMapOf<String, Boolean>()
|
||||
val testStartTimes = mutableMapOf<String, Long>()
|
||||
val testEndTimes = mutableMapOf<String, Long>()
|
||||
|
||||
tasks.forEach { (nxTaskId, taskConfig) ->
|
||||
if (taskConfig.testClassName != null) {
|
||||
testTaskStatus[nxTaskId] = true
|
||||
}
|
||||
}
|
||||
// Group the list of GradleTask by their taskName
|
||||
val groupedTasks: Map<String, List<GradleTask>> = tasks.values.groupBy { it.taskName }
|
||||
logger.info("📋 Collected ${groupedTasks.keys.size} unique task names: $groupedTasks")
|
||||
|
||||
val globalStart = System.currentTimeMillis()
|
||||
var globalOutput: String
|
||||
val eventTypes: MutableSet<OperationType> = HashSet()
|
||||
eventTypes.add(OperationType.TASK)
|
||||
eventTypes.add(OperationType.TEST)
|
||||
|
||||
val excludeArgs = excludeTestTasks.flatMap { listOf("--exclude-task", it) }
|
||||
logger.info("excludeTestTasks $excludeArgs")
|
||||
|
||||
try {
|
||||
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())
|
||||
groupedTasks.forEach { withTaskAndTestClasses(it.key, it.value.map { it.testClassName }) }
|
||||
addArguments("-Djunit.jupiter.execution.parallel.enabled=true") // Add JUnit 5 parallelism
|
||||
// arguments here
|
||||
addArguments(
|
||||
*(args + excludeArgs).toTypedArray()) // Combine your existing args with JUnit args
|
||||
setStandardOutput(outputStream)
|
||||
setStandardError(errorStream)
|
||||
addProgressListener(
|
||||
testListener(
|
||||
tasks, taskStartTimes, taskResults, testTaskStatus, testStartTimes, testEndTimes),
|
||||
OperationType.TEST)
|
||||
testListener(tasks, testTaskStatus, testStartTimes, testEndTimes), eventTypes)
|
||||
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 =
|
||||
@ -181,16 +197,21 @@ fun runTestLauncher(
|
||||
}
|
||||
|
||||
val globalEnd = System.currentTimeMillis()
|
||||
val maxEndTime = testEndTimes.values.maxOrNull() ?: globalEnd
|
||||
val minStartTime = testStartTimes.values.minOrNull() ?: globalStart
|
||||
logger.info(
|
||||
"⏱️ Test start timing gap: ${minStartTime - globalStart}ms (time between first test start and test launcher start) ")
|
||||
logger.info(
|
||||
"⏱️ Test completion timing gap: ${globalEnd - maxEndTime}ms (time between last test finish and test launcher end)")
|
||||
|
||||
val taskResults = mutableMapOf<String, TaskResult>()
|
||||
tasks.forEach { (nxTaskId, taskConfig) ->
|
||||
if (taskConfig.testClassName != null) {
|
||||
val success = testTaskStatus[nxTaskId] ?: false
|
||||
val startTime = testStartTimes[nxTaskId] ?: globalStart
|
||||
val endTime = testEndTimes[nxTaskId] ?: globalEnd
|
||||
|
||||
if (!taskResults.containsKey(nxTaskId)) {
|
||||
taskResults[nxTaskId] = TaskResult(success, startTime, endTime, "")
|
||||
}
|
||||
taskResults[nxTaskId] = TaskResult(success, startTime, endTime, "")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,61 +1,83 @@
|
||||
package dev.nx.gradle.runner
|
||||
|
||||
import dev.nx.gradle.data.GradleTask
|
||||
import dev.nx.gradle.data.TaskResult
|
||||
import dev.nx.gradle.util.formatMillis
|
||||
import dev.nx.gradle.util.logger
|
||||
import org.gradle.tooling.events.ProgressEvent
|
||||
import org.gradle.tooling.events.task.TaskFinishEvent
|
||||
import org.gradle.tooling.events.task.TaskStartEvent
|
||||
import org.gradle.tooling.events.test.*
|
||||
|
||||
fun testListener(
|
||||
tasks: Map<String, GradleTask>,
|
||||
taskStartTimes: MutableMap<String, Long>,
|
||||
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 ->
|
||||
testEndTimes: MutableMap<String, Long>,
|
||||
): (ProgressEvent) -> Unit {
|
||||
return { event ->
|
||||
when (event) {
|
||||
is TaskFinishEvent -> {
|
||||
val taskPath = event.descriptor.taskPath
|
||||
val success = getTaskFinishEventSuccess(event, taskPath)
|
||||
|
||||
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")
|
||||
.filter {
|
||||
it.value.taskName == taskPath
|
||||
} // Filters the entries to keep only matching ones
|
||||
.map { it.key }
|
||||
.forEach { nxTaskId -> // Iterate over the filtered entries
|
||||
testTaskStatus.computeIfAbsent(nxTaskId) { success }
|
||||
testEndTimes.computeIfAbsent(nxTaskId) { event.result.endTime }
|
||||
}
|
||||
})
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
is TestSkippedResult ->
|
||||
logger.warning("\u26A0\uFE0F Test skipped: $nxTaskId $simpleClassName")
|
||||
is TestStartEvent -> {
|
||||
val descriptor = event.descriptor as? JvmTestOperationDescriptor
|
||||
|
||||
else ->
|
||||
logger.warning("\u26A0\uFE0F Unknown test result: $nxTaskId $simpleClassName")
|
||||
descriptor?.className?.let { className ->
|
||||
tasks.entries
|
||||
.find { (_, v) -> v.testClassName == className }
|
||||
?.key
|
||||
?.let { nxTaskId ->
|
||||
testStartTimes.computeIfAbsent(nxTaskId) { event.eventTime }
|
||||
logger.info("🏁 Test start: $nxTaskId $className")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is TestFinishEvent -> {
|
||||
val descriptor = event.descriptor as? JvmTestOperationDescriptor
|
||||
val nxTaskId =
|
||||
descriptor?.className?.let { className ->
|
||||
tasks.entries.find { (_, v) -> v.testClassName == className }?.key
|
||||
}
|
||||
})
|
||||
|
||||
nxTaskId?.let {
|
||||
testEndTimes[it] = event.result.endTime
|
||||
val name = descriptor.className ?: "unknown"
|
||||
|
||||
when (event.result) {
|
||||
is TestSuccessResult -> {
|
||||
testTaskStatus[it] = true
|
||||
logger.info("✅ Test passed at ${formatMillis(event.result.endTime)}: $nxTaskId $name")
|
||||
}
|
||||
|
||||
is TestFailureResult -> {
|
||||
testTaskStatus[it] = false
|
||||
logger.warning("❌ Test failed: $nxTaskId $name")
|
||||
}
|
||||
|
||||
is TestSkippedResult -> {
|
||||
testTaskStatus[it] = true
|
||||
logger.warning("⚠️ Test skipped: $nxTaskId $name")
|
||||
}
|
||||
|
||||
else -> {
|
||||
testTaskStatus[it] = true
|
||||
logger.warning("⚠️ Unknown test result: $nxTaskId $name")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,65 @@
|
||||
package dev.nx.gradle.util
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.logging.ConsoleHandler // Or your preferred handler
|
||||
import java.util.logging.Formatter
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.LogRecord
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* A custom Formatter for java.util.logging that outputs logs in a single line. Format: [LEVEL]:
|
||||
* Message
|
||||
*/
|
||||
class SingleLineFormatter : Formatter() {
|
||||
override fun format(record: LogRecord): String {
|
||||
// Get the log level and the message
|
||||
val level = record.level.name
|
||||
val message = formatMessage(record)
|
||||
|
||||
// Return the formatted string in a single line
|
||||
return "${formatMillis(System.currentTimeMillis())} [$level]: $message\n"
|
||||
}
|
||||
}
|
||||
|
||||
// Your existing logger setup, modified to use the custom formatter
|
||||
val logger: Logger = Logger.getLogger("NxBatchRunner")
|
||||
|
||||
fun configureSingleLineLogger(quiet: Boolean) {
|
||||
// Get the root logger
|
||||
val rootLogger = Logger.getLogger("") // The empty string gets the root logger
|
||||
|
||||
// Remove all handlers from the root logger first
|
||||
rootLogger.handlers.forEach { handler -> rootLogger.removeHandler(handler) }
|
||||
|
||||
// Now, configure your specific logger and its handler
|
||||
// Ensure this logger does NOT use parent handlers
|
||||
logger.useParentHandlers = false
|
||||
|
||||
// Remove any existing handlers from your specific logger (if any were added before)
|
||||
logger.handlers.forEach { handler -> logger.removeHandler(handler) }
|
||||
|
||||
val consoleHandler = ConsoleHandler()
|
||||
consoleHandler.formatter = SingleLineFormatter()
|
||||
|
||||
// Add the configured handler to your specific logger
|
||||
logger.addHandler(consoleHandler)
|
||||
|
||||
// Set levels
|
||||
if (quiet) {
|
||||
logger.level = Level.OFF // Turn off your specific logger
|
||||
consoleHandler.level = Level.OFF // Turn off its handler
|
||||
rootLogger.level = Level.OFF // Ensure root logger is also off (or a high level)
|
||||
} else {
|
||||
logger.level = Level.INFO
|
||||
consoleHandler.level = Level.INFO // Ensure handler is on
|
||||
rootLogger.level = Level.INFO // Ensure root logger passes INFO and above
|
||||
}
|
||||
}
|
||||
|
||||
fun formatMillis(millis: Long): String {
|
||||
val sdf = SimpleDateFormat("HH:mm:ss.SSS")
|
||||
sdf.timeZone = TimeZone.getTimeZone("UTC") // so it doesn't apply your local timezone offset
|
||||
return sdf.format(Date(millis))
|
||||
}
|
||||
|
||||
@ -41,6 +41,17 @@ To pass in a hash parameter:
|
||||
./gradlew nxProjectGraph -Phash=12345
|
||||
```
|
||||
|
||||
To control whether Nx generates individual targets for each Gradle task (atomized targets) or a single target for the entire Gradle project, set the `atomized` boolean in your `build.gradle` or `build.gradle.kts` file.
|
||||
|
||||
To disable atomized targets:
|
||||
|
||||
```
|
||||
nxProjectReport {
|
||||
atomized = false
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
It generates a json file to be consumed by nx:
|
||||
|
||||
```json
|
||||
|
||||
@ -10,7 +10,7 @@ plugins {
|
||||
|
||||
group = "dev.nx.gradle"
|
||||
|
||||
version = "0.1.0"
|
||||
version = "0.0.1-alpha.6"
|
||||
|
||||
repositories { mavenCentral() }
|
||||
|
||||
|
||||
@ -11,7 +11,23 @@ const val testCiTargetGroup = "verification"
|
||||
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 val classDeclarationRegex =
|
||||
Regex("""^\s*(?:@[\w]+)?\s*(class|object)\s+([A-Za-z_][A-Za-z0-9_]*)""")
|
||||
private val privateClassRegex = Regex("""\bprivate\s+(class|object)\s+([A-Za-z_][A-Za-z0-9_]*)""")
|
||||
|
||||
// Essential annotations (most common subset)
|
||||
private val essentialTestAnnotations =
|
||||
setOf(
|
||||
"@Test",
|
||||
"@TestTemplate",
|
||||
"@ParameterizedTest",
|
||||
"@RepeatedTest",
|
||||
"@TestFactory",
|
||||
"@org.junit.Test", // JUnit 4
|
||||
"@org.testng.annotations.Test" // TestNG
|
||||
)
|
||||
|
||||
fun addTestCiTargets(
|
||||
testFiles: FileCollection,
|
||||
@ -31,15 +47,18 @@ fun addTestCiTargets(
|
||||
testFiles
|
||||
.filter { isTestFile(it, workspaceRoot) }
|
||||
.forEach { testFile ->
|
||||
val className = getTestClassNameIfAnnotated(testFile) ?: return@forEach
|
||||
val classNames = getAllVisibleClassesWithNestedAnnotation(testFile)
|
||||
|
||||
val targetName = "$ciTestTargetName--$className"
|
||||
targets[targetName] =
|
||||
buildTestCiTarget(
|
||||
projectBuildPath, className, testFile, testTask, projectRoot, workspaceRoot)
|
||||
targetGroups[testCiTargetGroup]?.add(targetName)
|
||||
classNames?.entries?.forEach { (className, testClassPackagePath) ->
|
||||
val targetName = "$ciTestTargetName--$className"
|
||||
targets[targetName] =
|
||||
buildTestCiTarget(
|
||||
projectBuildPath, testClassPackagePath, testTask, projectRoot, workspaceRoot)
|
||||
targetGroups[testCiTargetGroup]?.add(targetName)
|
||||
|
||||
ciDependsOn.add(mapOf("target" to targetName, "projects" to "self", "params" to "forward"))
|
||||
ciDependsOn.add(
|
||||
mapOf("target" to targetName, "projects" to "self", "params" to "forward"))
|
||||
}
|
||||
}
|
||||
|
||||
testTask.logger.info("${testTask.path} generated CI targets: ${ciDependsOn.map { it["target"] }}")
|
||||
@ -56,21 +75,72 @@ fun addTestCiTargets(
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTestClassNameIfAnnotated(file: File): String? {
|
||||
return file
|
||||
.takeIf { it.exists() }
|
||||
?.readText()
|
||||
?.takeIf {
|
||||
it.contains("@Test") || it.contains("@TestTemplate") || it.contains("@ParameterizedTest")
|
||||
private fun containsEssentialTestAnnotations(content: String): Boolean {
|
||||
return essentialTestAnnotations.any { content.contains(it) }
|
||||
}
|
||||
|
||||
// This function return all class names and nested class names inside a file
|
||||
fun getAllVisibleClassesWithNestedAnnotation(file: File): MutableMap<String, String>? {
|
||||
val content = file.takeIf { it.exists() }?.readText() ?: return null
|
||||
|
||||
val lines = content.lines()
|
||||
val result = mutableMapOf<String, String>()
|
||||
var packageName: String?
|
||||
val classStack = mutableListOf<Pair<String, Int>>() // (className, indent)
|
||||
|
||||
var previousLine: String? = null
|
||||
|
||||
for (i in lines.indices) {
|
||||
val line = lines[i]
|
||||
val trimmed = line.trimStart()
|
||||
val indent = line.indexOfFirst { !it.isWhitespace() }.takeIf { it >= 0 } ?: 0
|
||||
|
||||
// Skip private classes
|
||||
if (privateClassRegex.containsMatchIn(trimmed)) continue
|
||||
|
||||
packageName = packageDeclarationRegex.find(content)?.groupValues?.getOrNull(1)
|
||||
val match = classDeclarationRegex.find(trimmed)
|
||||
if (match == null) {
|
||||
previousLine = trimmed
|
||||
continue
|
||||
}
|
||||
|
||||
val className = match.groupValues.getOrNull(2)
|
||||
if (className == null) {
|
||||
previousLine = trimmed
|
||||
continue
|
||||
}
|
||||
val isAnnotatedNested = previousLine?.trimStart()?.startsWith("@Nested") == true
|
||||
|
||||
// Top-level class (no indentation or same as outermost level)
|
||||
if (indent == 0) {
|
||||
// Exclude top-level @nested classes
|
||||
if (!isAnnotatedNested) {
|
||||
result.put(className, packageName?.let { "$it.$className" } ?: className)
|
||||
}
|
||||
?.let { content ->
|
||||
val className = classDeclarationRegex.find(content)?.groupValues?.getOrNull(1)
|
||||
return if (className != null && !className.startsWith("Fake")) {
|
||||
className
|
||||
} else {
|
||||
null
|
||||
}
|
||||
classStack.clear()
|
||||
classStack.add(className to indent)
|
||||
} else {
|
||||
// Maintain nesting stack
|
||||
while (classStack.isNotEmpty() && indent <= classStack.last().second) {
|
||||
classStack.removeLast()
|
||||
}
|
||||
|
||||
val parent = classStack.lastOrNull()?.first
|
||||
if (isAnnotatedNested && parent != null) {
|
||||
val packageClassName = "$parent$$className"
|
||||
result["$parent$className"] =
|
||||
packageName?.let { "$it.$packageClassName" } ?: packageClassName
|
||||
result.remove(parent) // remove the parent class since child nested class is added
|
||||
}
|
||||
|
||||
classStack.add(className to indent)
|
||||
}
|
||||
|
||||
previousLine = trimmed
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun ensureTargetGroupExists(targetGroups: TargetGroups, group: String) {
|
||||
@ -84,8 +154,7 @@ private fun isTestFile(file: File, workspaceRoot: String): Boolean {
|
||||
|
||||
private fun buildTestCiTarget(
|
||||
projectBuildPath: String,
|
||||
testClassName: String,
|
||||
testFile: File,
|
||||
testClassPackagePath: String,
|
||||
testTask: Task,
|
||||
projectRoot: String,
|
||||
workspaceRoot: String,
|
||||
@ -98,9 +167,9 @@ private fun buildTestCiTarget(
|
||||
"options" to
|
||||
mapOf(
|
||||
"taskName" to "${projectBuildPath}:${testTask.name}",
|
||||
"testClassName" to testClassName),
|
||||
"testClassName" to testClassPackagePath),
|
||||
"metadata" to
|
||||
getMetadata("Runs Gradle test $testClassName in CI", projectBuildPath, "test"),
|
||||
getMetadata("Runs Gradle test $testClassPackagePath in CI", projectBuildPath, "test"),
|
||||
"cache" to true,
|
||||
"inputs" to taskInputs)
|
||||
|
||||
|
||||
@ -79,7 +79,6 @@ fun processTargetsForProject(
|
||||
|
||||
val ciTestTargetName = targetNameOverrides["ciTestTargetName"]
|
||||
val ciIntTestTargetName = targetNameOverrides["ciIntTestTargetName"]
|
||||
val ciCheckTargetName = targetNameOverrides.getOrDefault("ciCheckTargetName", "check-ci")
|
||||
val testTargetName = targetNameOverrides.getOrDefault("testTargetName", "test")
|
||||
val intTestTargetName = targetNameOverrides.getOrDefault("intTestTargetName", "intTest")
|
||||
|
||||
@ -138,6 +137,7 @@ fun processTargetsForProject(
|
||||
ciIntTestTargetName!!)
|
||||
}
|
||||
|
||||
val ciCheckTargetName = targetNameOverrides.getOrDefault("ciCheckTargetName", "check-ci")
|
||||
if (task.name == "check") {
|
||||
val replacedDependencies =
|
||||
(target["dependsOn"] as? List<*>)?.map { dep ->
|
||||
@ -153,16 +153,44 @@ fun processTargetsForProject(
|
||||
}
|
||||
} ?: emptyList()
|
||||
|
||||
val newTarget: MutableMap<String, Any?> =
|
||||
mutableMapOf(
|
||||
"dependsOn" to replacedDependencies,
|
||||
"executor" to "nx:noop",
|
||||
"cache" to true,
|
||||
"metadata" to getMetadata("Runs Gradle Check in CI", projectBuildPath, "check"))
|
||||
if (atomized) {
|
||||
val newTarget: MutableMap<String, Any?> =
|
||||
mutableMapOf(
|
||||
"dependsOn" to replacedDependencies,
|
||||
"executor" to "nx:noop",
|
||||
"cache" to true,
|
||||
"metadata" to getMetadata("Runs Gradle Check in CI", projectBuildPath, "check"))
|
||||
|
||||
targets[ciCheckTargetName] = newTarget
|
||||
ensureTargetGroupExists(targetGroups, testCiTargetGroup)
|
||||
targetGroups[testCiTargetGroup]?.add(ciCheckTargetName)
|
||||
targets[ciCheckTargetName] = newTarget
|
||||
ensureTargetGroupExists(targetGroups, testCiTargetGroup)
|
||||
targetGroups[testCiTargetGroup]?.add(ciCheckTargetName)
|
||||
}
|
||||
}
|
||||
|
||||
if (task.name == "build") {
|
||||
val ciBuildTargetName = targetNameOverrides.getOrDefault("ciBuildTargetName", "build-ci")
|
||||
val replacedDependencies =
|
||||
(target["dependsOn"] as? List<*>)?.map { dep ->
|
||||
val dependsOn = dep.toString()
|
||||
if (dependsOn == "${project.name}:check" && atomized) {
|
||||
"${project.name}:$ciCheckTargetName"
|
||||
} else {
|
||||
dep
|
||||
}
|
||||
} ?: emptyList()
|
||||
|
||||
if (atomized) {
|
||||
val newTarget: MutableMap<String, Any?> =
|
||||
mutableMapOf(
|
||||
"dependsOn" to replacedDependencies,
|
||||
"executor" to "nx:noop",
|
||||
"cache" to true,
|
||||
"metadata" to getMetadata("Runs Gradle Build in CI", projectBuildPath, "build"))
|
||||
|
||||
targets[ciBuildTargetName] = newTarget
|
||||
ensureTargetGroupExists(targetGroups, "build")
|
||||
targetGroups["build"]?.add(ciBuildTargetName)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("$now ${project.name}: Processed task ${task.path}")
|
||||
|
||||
@ -161,7 +161,7 @@ fun getOutputsForTask(task: Task, projectRoot: String, workspaceRoot: String): L
|
||||
*/
|
||||
fun getDependsOnForTask(
|
||||
task: Task,
|
||||
dependencies: MutableSet<Dependency>?,
|
||||
dependencies: MutableSet<Dependency>?, // Assuming Dependency class is defined elsewhere
|
||||
targetNameOverrides: Map<String, String> = emptyMap()
|
||||
): List<String>? {
|
||||
|
||||
@ -187,19 +187,30 @@ fun getDependsOnForTask(
|
||||
}
|
||||
|
||||
return try {
|
||||
// get depends on using taskDependencies.getDependencies(task) because task.dependsOn has
|
||||
// missing deps
|
||||
val dependsOn =
|
||||
// 1. Get dependencies from task.taskDependencies.getDependencies(task)
|
||||
val dependsOnFromTaskDependencies: Set<Task> =
|
||||
try {
|
||||
task.taskDependencies.getDependencies(null)
|
||||
task.taskDependencies.getDependencies(task)
|
||||
} catch (e: Exception) {
|
||||
task.logger.info("Error calling getDependencies for ${task.path}: ${e.message}")
|
||||
task.logger.debug("Stack trace:", e)
|
||||
emptySet<Task>()
|
||||
emptySet<Task>() // If it fails, return an empty set to be combined later
|
||||
}
|
||||
|
||||
if (dependsOn.isNotEmpty()) {
|
||||
return mapTasksToNames(dependsOn)
|
||||
// 2. Get dependencies from task.dependsOn and filter for Task instances
|
||||
val dependsOnFromDependsOnProperty: Set<Task> = task.dependsOn.filterIsInstance<Task>().toSet()
|
||||
|
||||
// 3. Combine the two sets of dependencies
|
||||
val combinedDependsOn = dependsOnFromTaskDependencies.union(dependsOnFromDependsOnProperty)
|
||||
|
||||
task.logger.info(
|
||||
"Dependencies from taskDependencies.getDependencies for $task: $dependsOnFromTaskDependencies")
|
||||
task.logger.info(
|
||||
"Dependencies from task.dependsOn property for $task: $dependsOnFromDependsOnProperty")
|
||||
task.logger.info("Combined dependencies for $task: $combinedDependsOn")
|
||||
|
||||
if (combinedDependsOn.isNotEmpty()) {
|
||||
return mapTasksToNames(combinedDependsOn)
|
||||
}
|
||||
|
||||
null
|
||||
|
||||
@ -0,0 +1,87 @@
|
||||
package dev.nx.gradle.utils
|
||||
|
||||
import java.io.File
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class ClassDetectionTest {
|
||||
@Test
|
||||
fun `detects top-level and annotated nested classes only`() {
|
||||
val kotlinSource =
|
||||
"""
|
||||
class ClassA {
|
||||
@Nested
|
||||
class ClassB
|
||||
|
||||
class ClassC
|
||||
|
||||
private class ClassD
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ClassX // not nested — should be ignored
|
||||
|
||||
private class ClassY
|
||||
|
||||
class ClassZ
|
||||
"""
|
||||
.trimIndent()
|
||||
|
||||
// Write to temp file
|
||||
val tempFile = File.createTempFile("ClassDetectionTest", ".kt")
|
||||
tempFile.writeText(kotlinSource)
|
||||
tempFile.deleteOnExit()
|
||||
|
||||
// Run the function
|
||||
val result = getAllVisibleClassesWithNestedAnnotation(tempFile)
|
||||
|
||||
// Expected output
|
||||
val expected = mapOf("ClassAClassB" to "ClassA\$ClassB", "ClassZ" to "ClassZ")
|
||||
|
||||
assertEquals(expected, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `detects top-level and annotated nested classes only with package name`() {
|
||||
val kotlinSource =
|
||||
"""
|
||||
package dev.test
|
||||
class ClassA {
|
||||
@Nested
|
||||
class ClassB
|
||||
|
||||
class ClassC
|
||||
|
||||
private class ClassD
|
||||
|
||||
@Nested
|
||||
class ClassE
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ClassX // not nested — should be ignored
|
||||
|
||||
private class ClassY
|
||||
|
||||
class ClassZ
|
||||
"""
|
||||
.trimIndent()
|
||||
|
||||
// Write to temp file
|
||||
val tempFile = File.createTempFile("ClassDetectionTest", ".kt")
|
||||
tempFile.writeText(kotlinSource)
|
||||
tempFile.deleteOnExit()
|
||||
|
||||
// Run the function
|
||||
val result = getAllVisibleClassesWithNestedAnnotation(tempFile)
|
||||
|
||||
// Expected output
|
||||
val expected =
|
||||
mapOf(
|
||||
"ClassAClassB" to "dev.test.ClassA\$ClassB",
|
||||
"ClassAClassE" to "dev.test.ClassA\$ClassE",
|
||||
"ClassZ" to "dev.test.ClassZ")
|
||||
|
||||
assertEquals(expected, result)
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
package dev.nx.gradle.utils
|
||||
|
||||
import dev.nx.gradle.data.*
|
||||
import java.io.File
|
||||
import kotlin.test.*
|
||||
import org.gradle.testfixtures.ProjectBuilder
|
||||
|
||||
@ -50,4 +51,104 @@ class CreateNodeForProjectTest {
|
||||
assertTrue(result.dependencies.isEmpty(), "Expected no dependencies")
|
||||
assertTrue(result.externalNodes.isEmpty(), "Expected no external nodes")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should not generate atomized targets when atomized is false`() {
|
||||
// Arrange
|
||||
val workspaceRoot = createTempDir("workspace").absolutePath
|
||||
val projectDir = createTempDir("project")
|
||||
val project = ProjectBuilder.builder().withProjectDir(projectDir).build()
|
||||
|
||||
// Create tasks that would normally trigger atomized targets
|
||||
val testFile1 =
|
||||
File(projectDir, "src/test/kotlin/MyFirstTest.kt").apply {
|
||||
parentFile.mkdirs()
|
||||
writeText("@Test class MyFirstTest")
|
||||
}
|
||||
|
||||
val testTask =
|
||||
project.task("test").apply {
|
||||
group = "verification"
|
||||
description = "Runs the tests"
|
||||
inputs.files(project.files(testFile1))
|
||||
}
|
||||
project
|
||||
.task("compileTestKotlin")
|
||||
.dependsOn(testTask) // This task name triggers ci test target logic
|
||||
|
||||
val checkTask =
|
||||
project.task("check").apply {
|
||||
group = "verification"
|
||||
description = "Runs all checks"
|
||||
dependsOn(testTask)
|
||||
}
|
||||
project.task("build").apply {
|
||||
group = "build"
|
||||
description = "Assembles and tests"
|
||||
dependsOn(checkTask)
|
||||
}
|
||||
|
||||
val targetNameOverrides =
|
||||
mapOf(
|
||||
"ciTestTargetName" to "ci-test",
|
||||
"ciCheckTargetName" to "ci-check",
|
||||
"ciBuildTargetName" to "ci-build")
|
||||
|
||||
// Act
|
||||
val result =
|
||||
createNodeForProject(
|
||||
project = project,
|
||||
targetNameOverrides = targetNameOverrides,
|
||||
workspaceRoot = workspaceRoot,
|
||||
atomized = false) // Test with atomized = false
|
||||
|
||||
// Assert
|
||||
val projectRoot = project.projectDir.absolutePath
|
||||
val projectNode = result.nodes[projectRoot]
|
||||
assertNotNull(projectNode, "ProjectNode should not be null")
|
||||
|
||||
// Verify that individual atomized targets are NOT created
|
||||
assertFalse(
|
||||
projectNode.targets.containsKey("ci--MyFirstTest"),
|
||||
"Expected ci--MyFirstTest target NOT to be present")
|
||||
|
||||
// Verify 'test' and 'check' targets are present but not their 'ci' counterparts if atomized is
|
||||
// false
|
||||
assertNotNull(projectNode.targets["test"], "Expected 'test' target to be present")
|
||||
assertNotNull(projectNode.targets["check"], "Expected 'check' target to be present")
|
||||
assertNotNull(projectNode.targets["build"], "Expected 'build' target to be present")
|
||||
|
||||
// Verify that 'ci-test', 'ci-check', 'ci-build' are not created as main targets if atomized is
|
||||
// false
|
||||
assertFalse(
|
||||
projectNode.targets.containsKey("ci-test"),
|
||||
"Expected ci-test target NOT to be present as a main target")
|
||||
assertFalse(
|
||||
projectNode.targets.containsKey("ci-check"),
|
||||
"Expected ci-check target NOT to be present as a main target")
|
||||
assertFalse(
|
||||
projectNode.targets.containsKey("ci-build"),
|
||||
"Expected ci-build target NOT to be present as a main target")
|
||||
|
||||
// Verify dependencies are NOT rewritten for 'check' and 'build' tasks
|
||||
val checkTarget = projectNode.targets["check"]
|
||||
assertNotNull(checkTarget, "Check target should exist")
|
||||
val checkDependsOn = checkTarget["dependsOn"] as? List<*>
|
||||
assertNotNull(checkDependsOn, "Check dependsOn should not be null")
|
||||
assertTrue(
|
||||
checkDependsOn.contains("${project.name}:test"), "Expected 'check' to depend on 'test'")
|
||||
assertFalse(
|
||||
checkDependsOn.contains("${project.name}:ci-test"),
|
||||
"Expected 'check' NOT to depend on 'ci-test'")
|
||||
|
||||
val buildTarget = projectNode.targets["build"]
|
||||
assertNotNull(buildTarget, "Build target should exist")
|
||||
val buildDependsOn = buildTarget["dependsOn"] as? List<*>
|
||||
assertNotNull(buildDependsOn, "Build dependsOn should not be null")
|
||||
assertTrue(
|
||||
buildDependsOn.contains("${project.name}:check"), "Expected 'build' to depend on 'check'")
|
||||
assertFalse(
|
||||
buildDependsOn.contains("${project.name}:ci-check"),
|
||||
"Expected 'build' NOT to depend on 'ci-check'")
|
||||
}
|
||||
}
|
||||
|
||||
153
packages/gradle/src/executors/gradle/get-exclude-task.spec.ts
Normal file
153
packages/gradle/src/executors/gradle/get-exclude-task.spec.ts
Normal file
@ -0,0 +1,153 @@
|
||||
import { getExcludeTasks, getAllDependsOn } from './get-exclude-task';
|
||||
|
||||
describe('getExcludeTasks', () => {
|
||||
const nodes: any = {
|
||||
app1: {
|
||||
name: 'app1',
|
||||
type: 'app',
|
||||
data: {
|
||||
root: 'app1',
|
||||
targets: {
|
||||
test: {
|
||||
dependsOn: ['app1:lint', 'app2:build'],
|
||||
options: { taskName: 'testApp1' },
|
||||
},
|
||||
lint: { options: { taskName: 'lintApp1' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
app2: {
|
||||
name: 'app2',
|
||||
type: 'app',
|
||||
data: {
|
||||
root: 'app2',
|
||||
targets: {
|
||||
build: { dependsOn: [], options: { taskName: 'buildApp2' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
app3: {
|
||||
name: 'app3',
|
||||
type: 'app',
|
||||
data: {
|
||||
root: 'app3',
|
||||
targets: {
|
||||
deploy: {
|
||||
dependsOn: ['app1:test'],
|
||||
options: { taskName: 'deployApp3' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('should exclude tasks that are not in runningTaskIds and have excludeDependsOn true', () => {
|
||||
const targets = new Set<string>(['app1:test', 'app2:build']);
|
||||
const runningTaskIds = new Set<string>(['app1:test']);
|
||||
const excludes = getExcludeTasks(targets, nodes, runningTaskIds);
|
||||
expect(excludes).toEqual(new Set(['lintApp1', 'buildApp2']));
|
||||
});
|
||||
|
||||
it('should not exclude tasks if direct dependencies are running', () => {
|
||||
const targets = new Set<string>(['app1:test']);
|
||||
const runningTaskIds = new Set<string>([
|
||||
'app1:test',
|
||||
'app1:lint',
|
||||
'app2:build',
|
||||
]);
|
||||
const excludes = getExcludeTasks(targets, nodes, runningTaskIds);
|
||||
expect(excludes).toEqual(new Set());
|
||||
});
|
||||
|
||||
it('should handle targets with no dependencies', () => {
|
||||
const targets = new Set<string>(['app2:build']);
|
||||
const runningTaskIds = new Set<string>(['app2:build']);
|
||||
const excludes = getExcludeTasks(targets, nodes, runningTaskIds);
|
||||
expect(excludes).toEqual(new Set());
|
||||
});
|
||||
|
||||
it('should handle missing project or target', () => {
|
||||
const targets = new Set<string>(['nonexistent:test']);
|
||||
const runningTaskIds = new Set<string>();
|
||||
const excludes = getExcludeTasks(targets, nodes, runningTaskIds);
|
||||
expect(excludes).toEqual(new Set());
|
||||
});
|
||||
|
||||
it('should handle dependencies that are also running tasks', () => {
|
||||
const targets = new Set<string>(['app1:test']);
|
||||
const runningTaskIds = new Set<string>(['app1:test', 'app1:lint']);
|
||||
const excludes = getExcludeTasks(targets, nodes, runningTaskIds);
|
||||
expect(excludes).toEqual(new Set(['buildApp2']));
|
||||
});
|
||||
|
||||
it('should handle recursive dependencies', () => {
|
||||
// Assuming app3:deploy depends on app1:test, which in turn depends on app1:lint and app2:build
|
||||
const targets = new Set<string>(['app3:deploy']);
|
||||
const runningTaskIds = new Set<string>(['app3:deploy']);
|
||||
const excludes = getExcludeTasks(targets, nodes, runningTaskIds);
|
||||
expect(excludes).toEqual(new Set(['testApp1']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllDependsOn', () => {
|
||||
const nodes: any = {
|
||||
a: {
|
||||
name: 'a',
|
||||
type: 'lib',
|
||||
data: {
|
||||
root: 'a',
|
||||
targets: { build: { dependsOn: ['b:build', 'c:build'] } },
|
||||
},
|
||||
},
|
||||
b: {
|
||||
name: 'b',
|
||||
type: 'lib',
|
||||
data: { root: 'b', targets: { build: { dependsOn: ['d:build'] } } },
|
||||
},
|
||||
c: {
|
||||
name: 'c',
|
||||
type: 'lib',
|
||||
data: { root: 'c', targets: { build: {} } },
|
||||
},
|
||||
d: {
|
||||
name: 'd',
|
||||
type: 'lib',
|
||||
data: { root: 'd', targets: { build: {} } },
|
||||
},
|
||||
e: {
|
||||
name: 'e',
|
||||
type: 'lib',
|
||||
data: { root: 'e', targets: { build: { dependsOn: ['f:build'] } } },
|
||||
},
|
||||
f: {
|
||||
name: 'f',
|
||||
type: 'lib',
|
||||
data: { root: 'f', targets: { build: { dependsOn: ['e:build'] } } }, // Circular dependency
|
||||
},
|
||||
};
|
||||
|
||||
it('should return all transitive dependencies excluding the starting task', () => {
|
||||
const dependencies = getAllDependsOn(nodes, 'a', 'build');
|
||||
expect(dependencies).toEqual(new Set(['b:build', 'c:build', 'd:build']));
|
||||
});
|
||||
|
||||
it('should handle no dependencies', () => {
|
||||
const dependencies = getAllDependsOn(nodes, 'c', 'build');
|
||||
expect(dependencies).toEqual(new Set());
|
||||
});
|
||||
|
||||
it('should handle missing project or target', () => {
|
||||
const dependencies = getAllDependsOn(nodes, 'nonexistent', 'build');
|
||||
expect(dependencies).toEqual(new Set());
|
||||
});
|
||||
|
||||
it('should handle circular dependencies gracefully', () => {
|
||||
const dependencies = getAllDependsOn(nodes, 'e', 'build');
|
||||
expect(dependencies).toEqual(new Set(['f:build']));
|
||||
});
|
||||
|
||||
it('should not include the starting task in the result', () => {
|
||||
const dependencies = getAllDependsOn(nodes, 'a', 'build');
|
||||
expect(dependencies).not.toContain('a:build');
|
||||
});
|
||||
});
|
||||
@ -1,4 +1,7 @@
|
||||
import { ProjectGraph } from 'nx/src/config/project-graph';
|
||||
import {
|
||||
ProjectGraph,
|
||||
ProjectGraphProjectNode,
|
||||
} from 'nx/src/config/project-graph';
|
||||
|
||||
/**
|
||||
* Returns Gradle CLI arguments to exclude dependent tasks
|
||||
@ -8,28 +11,22 @@ import { ProjectGraph } from 'nx/src/config/project-graph';
|
||||
* and only `test` is running, this will return: ['lint']
|
||||
*/
|
||||
export function getExcludeTasks(
|
||||
projectGraph: ProjectGraph,
|
||||
targets: { project: string; target: string; excludeDependsOn: boolean }[],
|
||||
taskIds: Set<string>,
|
||||
nodes: Record<string, ProjectGraphProjectNode>,
|
||||
runningTaskIds: Set<string> = new Set()
|
||||
): Set<string> {
|
||||
const excludes = new Set<string>();
|
||||
|
||||
for (const { project, target, excludeDependsOn } of targets) {
|
||||
if (!excludeDependsOn) {
|
||||
continue;
|
||||
}
|
||||
const taskDeps =
|
||||
projectGraph.nodes[project]?.data?.targets?.[target]?.dependsOn ?? [];
|
||||
for (const taskId of taskIds) {
|
||||
const [project, target] = taskId.split(':');
|
||||
const taskDeps = nodes[project]?.data?.targets?.[target]?.dependsOn ?? [];
|
||||
|
||||
for (const dep of taskDeps) {
|
||||
const taskId = typeof dep === 'string' ? dep : dep?.target;
|
||||
if (taskId && !runningTaskIds.has(taskId)) {
|
||||
const [projectName, targetName] = taskId.split(':');
|
||||
const taskName =
|
||||
projectGraph.nodes[projectName]?.data?.targets?.[targetName]?.options
|
||||
?.taskName;
|
||||
if (taskName) {
|
||||
excludes.add(taskName);
|
||||
const gradleTaskName = getGradleTaskNameWithNxTaskId(taskId, nodes);
|
||||
if (gradleTaskName) {
|
||||
excludes.add(gradleTaskName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -38,30 +35,43 @@ export function getExcludeTasks(
|
||||
return excludes;
|
||||
}
|
||||
|
||||
export function getGradleTaskNameWithNxTaskId(
|
||||
nxTaskId: string,
|
||||
nodes: Record<string, ProjectGraphProjectNode>
|
||||
): string | null {
|
||||
const [projectName, targetName] = nxTaskId.split(':');
|
||||
const gradleTaskName =
|
||||
nodes[projectName]?.data?.targets?.[targetName]?.options?.taskName;
|
||||
return gradleTaskName;
|
||||
}
|
||||
|
||||
export function getAllDependsOn(
|
||||
projectGraph: ProjectGraph,
|
||||
nodes: Record<string, ProjectGraphProjectNode>,
|
||||
projectName: string,
|
||||
targetName: string,
|
||||
visited: Set<string> = new Set()
|
||||
): string[] {
|
||||
const dependsOn =
|
||||
projectGraph[projectName]?.data?.targets?.[targetName]?.dependsOn ?? [];
|
||||
targetName: string
|
||||
): Set<string> {
|
||||
const allDependsOn = new Set<string>();
|
||||
const stack: string[] = [`${projectName}:${targetName}`];
|
||||
|
||||
const allDependsOn: string[] = [];
|
||||
while (stack.length > 0) {
|
||||
const currentTaskId = stack.pop();
|
||||
if (currentTaskId && !allDependsOn.has(currentTaskId)) {
|
||||
allDependsOn.add(currentTaskId);
|
||||
|
||||
for (const dependency of dependsOn) {
|
||||
if (!visited.has(dependency)) {
|
||||
visited.add(dependency);
|
||||
const [currentProjectName, currentTargetName] = currentTaskId.split(':');
|
||||
const directDependencies =
|
||||
nodes[currentProjectName]?.data?.targets?.[currentTargetName]
|
||||
?.dependsOn ?? [];
|
||||
|
||||
const [depProjectName, depTargetName] = dependency.split(':');
|
||||
allDependsOn.push(dependency);
|
||||
|
||||
// Recursively get dependencies of the current dependency
|
||||
allDependsOn.push(
|
||||
...getAllDependsOn(projectGraph, depProjectName, depTargetName, visited)
|
||||
);
|
||||
for (const dep of directDependencies) {
|
||||
const depTaskId = typeof dep === 'string' ? dep : dep?.target;
|
||||
if (depTaskId && !allDependsOn.has(depTaskId)) {
|
||||
stack.push(depTaskId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
allDependsOn.delete(`${projectName}:${targetName}`); // Exclude the starting task itself
|
||||
|
||||
return allDependsOn;
|
||||
}
|
||||
|
||||
173
packages/gradle/src/executors/gradle/gradle-batch.impl.spec.ts
Normal file
173
packages/gradle/src/executors/gradle/gradle-batch.impl.spec.ts
Normal file
@ -0,0 +1,173 @@
|
||||
import { getGradlewTasksToRun } from './gradle-batch.impl';
|
||||
import { TaskGraph, ProjectGraphProjectNode } from '@nx/devkit';
|
||||
import { GradleExecutorSchema } from './schema';
|
||||
|
||||
describe('getGradlewTasksToRun', () => {
|
||||
let taskGraph: TaskGraph;
|
||||
let inputs: Record<string, GradleExecutorSchema>;
|
||||
let nodes: Record<string, ProjectGraphProjectNode>;
|
||||
|
||||
beforeEach(() => {
|
||||
nodes = {
|
||||
app1: {
|
||||
name: 'app1',
|
||||
type: 'app',
|
||||
data: {
|
||||
root: 'app1',
|
||||
targets: {
|
||||
test: {
|
||||
dependsOn: ['app1:lint', 'app2:build'],
|
||||
options: { taskName: 'testApp1' },
|
||||
},
|
||||
lint: {
|
||||
options: { taskName: 'lintApp1' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
app2: {
|
||||
name: 'app2',
|
||||
type: 'app',
|
||||
data: {
|
||||
root: 'app2',
|
||||
targets: {
|
||||
build: {
|
||||
dependsOn: [],
|
||||
options: { taskName: 'buildApp2' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
app3: {
|
||||
name: 'app3',
|
||||
type: 'app',
|
||||
data: {
|
||||
root: 'app3',
|
||||
targets: {
|
||||
deploy: {
|
||||
dependsOn: ['app1:test'],
|
||||
options: { taskName: 'deployApp3' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
taskGraph = {
|
||||
tasks: {
|
||||
'app1:test': {
|
||||
id: 'app1:test',
|
||||
target: { project: 'app1', target: 'test' },
|
||||
outputs: [],
|
||||
overrides: {},
|
||||
projectRoot: 'app1',
|
||||
parallelism: false,
|
||||
},
|
||||
'app2:build': {
|
||||
id: 'app2:build',
|
||||
target: { project: 'app2', target: 'build' },
|
||||
outputs: [],
|
||||
overrides: {},
|
||||
projectRoot: 'app2',
|
||||
parallelism: false,
|
||||
},
|
||||
},
|
||||
dependencies: {},
|
||||
continuousDependencies: {},
|
||||
roots: ['app1:test', 'app2:build'],
|
||||
};
|
||||
|
||||
inputs = {
|
||||
'app1:test': {
|
||||
taskName: 'test',
|
||||
excludeDependsOn: true,
|
||||
},
|
||||
'app2:build': {
|
||||
taskName: 'build',
|
||||
excludeDependsOn: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('should correctly categorize tasks and their dependencies for exclusion', () => {
|
||||
const taskIds = ['app1:test', 'app2:build'];
|
||||
const result = getGradlewTasksToRun(taskIds, taskGraph, inputs, nodes);
|
||||
|
||||
expect(result.gradlewTasksToRun).toEqual({
|
||||
'app1:test': inputs['app1:test'],
|
||||
'app2:build': inputs['app2:build'],
|
||||
});
|
||||
expect(result.excludeTasks).toEqual(new Set(['lintApp1']));
|
||||
expect(result.excludeTestTasks).toEqual(new Set());
|
||||
});
|
||||
|
||||
it('should handle tasks with no excludeDependsOn', () => {
|
||||
inputs['app1:test'].excludeDependsOn = false;
|
||||
const taskIds = ['app1:test', 'app2:build'];
|
||||
const result = getGradlewTasksToRun(taskIds, taskGraph, inputs, nodes);
|
||||
|
||||
expect(result.excludeTasks).toEqual(new Set());
|
||||
expect(result.excludeTestTasks).toEqual(new Set());
|
||||
});
|
||||
|
||||
it('should handle testClassName for excludeTestTasks', () => {
|
||||
inputs['app1:test'].testClassName = 'com.example.MyTestClass';
|
||||
const taskIds = ['app1:test'];
|
||||
const result = getGradlewTasksToRun(taskIds, taskGraph, inputs, nodes);
|
||||
|
||||
expect(result.excludeTasks).toEqual(new Set());
|
||||
// Test task's dependsOn should be added to excludeTestTasks if testClassName is present
|
||||
expect(result.excludeTestTasks).toEqual(new Set(['buildApp2', 'lintApp1']));
|
||||
});
|
||||
|
||||
it('should include all dependencies when excludeDependsOn is false for a task', () => {
|
||||
inputs = {
|
||||
'app1:test': {
|
||||
taskName: 'test',
|
||||
excludeDependsOn: false,
|
||||
},
|
||||
};
|
||||
const taskIds = ['app1:test'];
|
||||
const result = getGradlewTasksToRun(taskIds, taskGraph, inputs, nodes);
|
||||
|
||||
// Since excludeDependsOn is false, no tasks should be excluded via excludeTasks
|
||||
// This part of the logic is used for `allDependsOn` to correctly calculate runningTaskIds
|
||||
// for `getExcludeTasks` later.
|
||||
// In this specific test, we're not checking `allDependsOn` directly, but the outcome
|
||||
// on `excludeTasks` confirms its effect (or lack thereof due to `excludeDependsOn: false`).
|
||||
expect(result.excludeTasks).toEqual(new Set());
|
||||
expect(result.excludeTestTasks).toEqual(new Set());
|
||||
});
|
||||
|
||||
it('should correctly handle a mix of excludeDependsOn true and false', () => {
|
||||
taskGraph.tasks['app3:deploy'] = {
|
||||
id: 'app3:deploy',
|
||||
target: { project: 'app3', target: 'deploy' },
|
||||
outputs: [],
|
||||
overrides: {},
|
||||
projectRoot: 'app3',
|
||||
parallelism: false,
|
||||
};
|
||||
taskGraph.roots.push('app3:deploy');
|
||||
inputs['app3:deploy'] = {
|
||||
taskName: 'deploy',
|
||||
excludeDependsOn: true,
|
||||
};
|
||||
|
||||
const taskIds = ['app1:test', 'app2:build', 'app3:deploy'];
|
||||
const result = getGradlewTasksToRun(taskIds, taskGraph, inputs, nodes);
|
||||
|
||||
expect(result.gradlewTasksToRun).toEqual({
|
||||
'app1:test': inputs['app1:test'],
|
||||
'app2:build': inputs['app2:build'],
|
||||
'app3:deploy': inputs['app3:deploy'],
|
||||
});
|
||||
|
||||
// app1:test (excludeDependsOn: true) -> exclude lintApp1
|
||||
// app3:deploy (excludeDependsOn: true) -> depends on app1:test
|
||||
// Since app1:test is also running, 'testApp1' should not be excluded.
|
||||
// However, 'lintApp1' and 'buildApp2' (dependencies of 'app1:test') should be excluded.
|
||||
expect(result.excludeTasks).toEqual(new Set(['lintApp1']));
|
||||
expect(result.excludeTestTasks).toEqual(new Set());
|
||||
});
|
||||
});
|
||||
@ -1,5 +1,11 @@
|
||||
import { ExecutorContext, output, TaskGraph, workspaceRoot } from '@nx/devkit';
|
||||
import runCommandsImpl, {
|
||||
import {
|
||||
ExecutorContext,
|
||||
output,
|
||||
ProjectGraphProjectNode,
|
||||
TaskGraph,
|
||||
workspaceRoot,
|
||||
} from '@nx/devkit';
|
||||
import {
|
||||
LARGE_BUFFER,
|
||||
RunCommandsOptions,
|
||||
} from 'nx/src/executors/run-commands/run-commands.impl';
|
||||
@ -12,18 +18,17 @@ import {
|
||||
createPseudoTerminal,
|
||||
PseudoTerminal,
|
||||
} from 'nx/src/tasks-runner/pseudo-terminal';
|
||||
import { getAllDependsOn, getExcludeTasks } from './get-exclude-task';
|
||||
import {
|
||||
getAllDependsOn,
|
||||
getExcludeTasks,
|
||||
getGradleTaskNameWithNxTaskId,
|
||||
} from './get-exclude-task';
|
||||
|
||||
export const batchRunnerPath = join(
|
||||
__dirname,
|
||||
'../../../batch-runner/build/libs/batch-runner-all.jar'
|
||||
);
|
||||
|
||||
interface GradleTask {
|
||||
taskName: string;
|
||||
testClassName: string;
|
||||
}
|
||||
|
||||
export default async function gradleBatch(
|
||||
taskGraph: TaskGraph,
|
||||
inputs: Record<string, GradleExecutorSchema>,
|
||||
@ -50,59 +55,20 @@ export default async function gradleBatch(
|
||||
args.push(...overrides.__overrides_unparsed__);
|
||||
}
|
||||
|
||||
const taskIdsWithExclude = [];
|
||||
const taskIdsWithoutExclude = [];
|
||||
|
||||
const taskIds = Object.keys(taskGraph.tasks);
|
||||
for (const taskId of taskIds) {
|
||||
if (inputs[taskId].excludeDependsOn) {
|
||||
taskIdsWithExclude.push(taskId);
|
||||
} else {
|
||||
taskIdsWithoutExclude.push(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
const allDependsOn = new Set<string>(taskIds);
|
||||
taskIdsWithoutExclude.forEach((taskId) => {
|
||||
const [projectName, targetName] = taskId.split(':');
|
||||
const dependencies = getAllDependsOn(
|
||||
context.projectGraph,
|
||||
projectName,
|
||||
targetName
|
||||
const { gradlewTasksToRun, excludeTasks, excludeTestTasks } =
|
||||
getGradlewTasksToRun(
|
||||
taskIds,
|
||||
taskGraph,
|
||||
inputs,
|
||||
context.projectGraph.nodes
|
||||
);
|
||||
dependencies.forEach((dep) => allDependsOn.add(dep));
|
||||
});
|
||||
|
||||
const gradlewTasksToRun: Record<string, GradleTask> = taskIds.reduce(
|
||||
(gradlewTasksToRun, taskId) => {
|
||||
const task = taskGraph.tasks[taskId];
|
||||
const gradlewTaskName = inputs[task.id].taskName;
|
||||
const testClassName = inputs[task.id].testClassName;
|
||||
gradlewTasksToRun[taskId] = {
|
||||
taskName: gradlewTaskName,
|
||||
testClassName: testClassName,
|
||||
};
|
||||
return gradlewTasksToRun;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
const excludeTasks = getExcludeTasks(
|
||||
context.projectGraph,
|
||||
taskIdsWithExclude.map((taskId) => {
|
||||
const task = taskGraph.tasks[taskId];
|
||||
return {
|
||||
project: task?.target?.project,
|
||||
target: task?.target?.target,
|
||||
excludeDependsOn: inputs[taskId]?.excludeDependsOn,
|
||||
};
|
||||
}),
|
||||
allDependsOn
|
||||
);
|
||||
|
||||
const batchResults = await runTasksInBatch(
|
||||
gradlewTasksToRun,
|
||||
excludeTasks,
|
||||
excludeTestTasks,
|
||||
args,
|
||||
root
|
||||
);
|
||||
@ -129,9 +95,71 @@ export default async function gradleBatch(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the gradlew task ids to run
|
||||
*/
|
||||
export function getGradlewTasksToRun(
|
||||
taskIds: string[],
|
||||
taskGraph: TaskGraph,
|
||||
inputs: Record<string, GradleExecutorSchema>,
|
||||
nodes: Record<string, ProjectGraphProjectNode>
|
||||
) {
|
||||
const taskIdsWithExclude: Set<string> = new Set([]);
|
||||
const testTaskIdsWithExclude: Set<string> = new Set([]);
|
||||
const taskIdsWithoutExclude: Set<string> = new Set([]);
|
||||
const gradlewTasksToRun: Record<string, GradleExecutorSchema> = {};
|
||||
|
||||
for (const taskId of taskIds) {
|
||||
const task = taskGraph.tasks[taskId];
|
||||
const input = inputs[task.id];
|
||||
|
||||
gradlewTasksToRun[taskId] = input;
|
||||
|
||||
if (input.excludeDependsOn) {
|
||||
if (input.testClassName) {
|
||||
testTaskIdsWithExclude.add(taskId);
|
||||
} else {
|
||||
taskIdsWithExclude.add(taskId);
|
||||
}
|
||||
} else {
|
||||
taskIdsWithoutExclude.add(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
const allDependsOn = new Set<string>(taskIds);
|
||||
for (const taskId of taskIdsWithoutExclude) {
|
||||
const [projectName, targetName] = taskId.split(':');
|
||||
const dependencies = getAllDependsOn(nodes, projectName, targetName);
|
||||
dependencies.forEach((dep) => allDependsOn.add(dep));
|
||||
}
|
||||
|
||||
const excludeTasks = getExcludeTasks(taskIdsWithExclude, nodes, allDependsOn);
|
||||
|
||||
const allTestsDependsOn = new Set<string>();
|
||||
for (const taskId of testTaskIdsWithExclude) {
|
||||
const [projectName, targetName] = taskId.split(':');
|
||||
const taskDependsOn = getAllDependsOn(nodes, projectName, targetName);
|
||||
taskDependsOn.forEach((dep) => allTestsDependsOn.add(dep));
|
||||
}
|
||||
const excludeTestTasks = new Set<string>();
|
||||
for (let taskId of allTestsDependsOn) {
|
||||
const gradleTaskName = getGradleTaskNameWithNxTaskId(taskId, nodes);
|
||||
if (gradleTaskName) {
|
||||
excludeTestTasks.add(gradleTaskName);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
gradlewTasksToRun,
|
||||
excludeTasks,
|
||||
excludeTestTasks,
|
||||
};
|
||||
}
|
||||
|
||||
async function runTasksInBatch(
|
||||
gradlewTasksToRun: Record<string, GradleTask>,
|
||||
gradlewTasksToRun: Record<string, GradleExecutorSchema>,
|
||||
excludeTasks: Set<string>,
|
||||
excludeTestTasks: Set<string>,
|
||||
args: string[],
|
||||
root: string
|
||||
): Promise<BatchResults> {
|
||||
@ -146,7 +174,9 @@ async function runTasksInBatch(
|
||||
.join(' ')
|
||||
.replaceAll("'", '"')}' --excludeTasks='${Array.from(excludeTasks).join(
|
||||
','
|
||||
)}' ${process.env.NX_VERBOSE_LOGGING === 'true' ? '' : '--quiet'}`;
|
||||
)}' --excludeTestTasks='${Array.from(excludeTestTasks).join(',')}' ${
|
||||
process.env.NX_VERBOSE_LOGGING === 'true' ? '' : '--quiet'
|
||||
}`;
|
||||
let batchResults;
|
||||
if (usePseudoTerminal && process.env.NX_VERBOSE_LOGGING !== 'true') {
|
||||
const terminal = createPseudoTerminal();
|
||||
|
||||
@ -24,13 +24,10 @@ export default async function gradleExecutor(
|
||||
args.push(`--tests`, options.testClassName);
|
||||
}
|
||||
|
||||
getExcludeTasks(context.projectGraph, [
|
||||
{
|
||||
project: context.projectName,
|
||||
target: context.targetName,
|
||||
excludeDependsOn: options.excludeDependsOn,
|
||||
},
|
||||
]).forEach((task) => {
|
||||
getExcludeTasks(
|
||||
new Set([`${context.projectName}:${context.targetName}`]),
|
||||
context.projectGraph.nodes
|
||||
).forEach((task) => {
|
||||
if (task) {
|
||||
args.push('--exclude-task', task);
|
||||
}
|
||||
|
||||
@ -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": [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user