Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
55f7513
chore: update Gradle and Kotlin configurations
MohamadJaara Feb 6, 2026
bd006b1
use kotlin 2.3.0
MohamadJaara Feb 6, 2026
fc918dd
update shadow to 9.3.1
MohamadJaara Feb 6, 2026
2b9a2dd
chore: update package names and rename destination files
MohamadJaara Feb 6, 2026
a82c209
set the correct gradle distributionSha256Sum
MohamadJaara Feb 6, 2026
b291072
chore: configure shadowJar task for testservice
MohamadJaara Feb 6, 2026
2cd9c7c
chore: update commonAndroidLibConfig to conditionally include consume…
MohamadJaara Feb 6, 2026
952eed4
chore: update compile SDK version to 36 and refactor configurations
MohamadJaara Feb 7, 2026
7828511
Merge branch 'develop' into mo/chore/update-kotliun
MohamadJaara Feb 7, 2026
30f4900
detekt
MohamadJaara Feb 7, 2026
f6e6798
update mokkery
MohamadJaara Feb 7, 2026
d2eeb43
chore: implement DagCommandTask for affected module detection and upd…
MohamadJaara Feb 8, 2026
cee05c6
chore: optimize task retrieval logic in OnlyAffectedTestTask
MohamadJaara Feb 8, 2026
a283063
chore: update test compilation commands for Android device and host t…
MohamadJaara Feb 8, 2026
a55285b
chore: rename test files and update detekt configuration exclusions
MohamadJaara Feb 8, 2026
7653f48
Merge branch 'develop' into mo/chore/update-kotliun
MohamadJaara Feb 9, 2026
c47a4cd
chore: update test logging configuration and add silent logger for da…
MohamadJaara Feb 9, 2026
8fde412
chore: update test logging configuration and add silent logger for da…
MohamadJaara Feb 9, 2026
28141ed
Merge remote-tracking branch 'origin/develop' into mo/chore/update-ko…
MohamadJaara Feb 10, 2026
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 .github/workflows/gradle-android-instrumented-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ jobs:

- name: Build the samples
run: |
./gradlew :sample:samples:compileDebugSources
./gradlew :sample:samples:compileAndroidDeviceTestSources

# API 30+ emulators only have x86_64 system images.
- name: Get AVD info
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/gradle-android-unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
GITHUB_USER: ${{ github.actor }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
./gradlew :sample:samples:compileDebugSources
./gradlew :sample:samples:compileAndroidHostTestSources

- name: Android Unit Tests
run: ./gradlew androidUnitOnlyAffectedTest
Expand Down
24 changes: 14 additions & 10 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ plugins {
id("scripts.testing")
id("scripts.detekt")
alias(libs.plugins.moduleGraph)
alias(libs.plugins.dagCommand)
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.compose.jetbrains) apply false
}
Expand All @@ -70,14 +69,25 @@ tasks.withType<Test> {
// For some reason xml and html generation is failing, looks like tests running in parallel
subprojects {
tasks.withType<org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeTest>().configureEach {
val fullSqliterTraces = providers.gradleProperty("kalium.sqliter.fullTraces")
.orNull
?.lowercase()
?.let { it == "true" || it == "1" || it == "yes" }
?: false
environment("KALIUM_SQLITER_FULL_TRACES", fullSqliterTraces.toString())

providers.gradleProperty("kalium.sqliter.traceFile").orNull?.takeIf { it.isNotBlank() }?.let {
environment("KALIUM_SQLITER_TRACE_FILE", it)
}

reports.junitXml.required.set(false)
reports.html.required.set(false)

// workaround for knowing which tests passed failed since HTML reporting is disabled for native tests
// can be removed once HTML reporting is working
// Keep failed test visibility without forwarding all native stdout/stderr to Gradle's
// test output store, which can fail after Gradle/Kotlin upgrades.
testLogging {
events("failed")
showStandardStreams = true
showStandardStreams = false
}
}

Expand All @@ -104,12 +114,6 @@ allprojects {
}
}

dagCommand {
defaultBranch = "origin/develop"
outputType = "json"
printModulesInfo = true
}

kover {
useJacoco()
}
Expand Down
6 changes: 6 additions & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ dependencies {
implementation("org.jetbrains.dokka:dokka-gradle-plugin:${libs.versions.dokka.get()}")
implementation("com.android.tools.build:gradle:${libs.versions.agp.get()}")
implementation("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:${libs.versions.detekt.get()}")
testImplementation(gradleTestKit())
testImplementation(kotlin("test-junit5"))
}

tasks.test {
useJUnitPlatform()
}

gradlePlugin {
Expand Down
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/Android.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ object Android {
const val testRunner = "androidx.test.runner.AndroidJUnitRunner"
object Sdk {
const val min = 26
const val compile = 35
const val compile = 36
const val target = compile
}
object Ndk {
Expand Down
149 changes: 149 additions & 0 deletions buildSrc/src/main/kotlin/DagCommandTask.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/

import org.gradle.api.DefaultTask
import org.gradle.api.artifacts.ProjectDependency
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction

/**
* Gradle 9 compatible replacement for the external dag-command plugin.
* It writes the same affected-modules output used by [OnlyAffectedTestTask].
*/
open class DagCommandTask : DefaultTask() {

@Input
var defaultBranch: String = "origin/develop"

init {
group = "verification"
description = "Computes affected modules and writes build/dag-command/affected-modules.json"
}

@TaskAction
fun computeAffectedModules() {
val outputFile = project.layout.buildDirectory.file(OUTPUT_FILE).get().asFile
val changedModules = changedModules() ?: run {
outputFile.delete()
return
}
val affectedModules = expandToDependents(changedModules)

outputFile.parentFile.mkdirs()
outputFile.writeText(
affectedModules
.sorted()
.joinToString(prefix = "[", postfix = "]") { "\"$it\"" }
)

println("Default branch: $defaultBranch")
println("Output type: json")
println("Output path: ${project.layout.buildDirectory.get().asFile.absolutePath}")
println("Changed modules: ${changedModules.size}, affected modules: ${affectedModules.size}")
}

private fun changedModules(): Set<String>? {
val process = ProcessBuilder(
"git",
"-C",
project.rootDir.absolutePath,
"diff",
defaultBranch,
"--dirstat=files,0"
)
.redirectErrorStream(true)
.start()

val output = process.inputStream.bufferedReader().readText()
if (process.waitFor() != 0) {
println("Unable to resolve changed files from git. Falling back to running all tests.")
return null
}

val allModules = project.subprojects.map { it.path }.toSet()
val parsedModules = output
.lineSequence()
.map { it.trim() }
.filter { it.isNotEmpty() }
.flatMap { parseModuleCandidates(it).asSequence() }
.toSet()

return if (parsedModules.contains(BUILD_SRC) || parsedModules.contains(GRADLE)) {
allModules
} else {
parsedModules.filter(allModules::contains).toSet()
}
}

private fun parseModuleCandidates(dirstatLine: String): Set<String> {
val fullPath = dirstatLine
.trimStart()
.split(" ", limit = 2)
.getOrNull(1)
?.trim()
.orEmpty()

val words = fullPath.split("/")
.takeWhile { it != "src" }
.filter { it.isNotEmpty() }

return words.fold(emptySet()) { acc, word ->
if (acc.isEmpty()) {
setOf(":$word")
} else {
val lastWord = acc.last()
acc + "$lastWord:$word"
}
}
}

private fun expandToDependents(changedModules: Set<String>): Set<String> {
if (changedModules.isEmpty()) return emptySet()

val reverseGraph = mutableMapOf<String, MutableSet<String>>()
project.subprojects.forEach { module ->
val dependencies = module.configurations.flatMap { configuration ->
configuration.dependencies
.withType(ProjectDependency::class.java)
.map { dependency -> dependency.path }
}.toSet()

dependencies.forEach { dependencyPath ->
reverseGraph.getOrPut(dependencyPath) { mutableSetOf() }.add(module.path)
}
}

val visited = changedModules.toMutableSet()
val queue = ArrayDeque(changedModules)
while (queue.isNotEmpty()) {
val current = queue.removeFirst()
reverseGraph[current].orEmpty().forEach { dependent ->
if (visited.add(dependent)) {
queue.add(dependent)
}
}
}
return visited
}

private companion object {
const val OUTPUT_FILE = "dag-command/affected-modules.json"
const val BUILD_SRC = ":buildSrc"
const val GRADLE = ":gradle"
}
}
70 changes: 40 additions & 30 deletions buildSrc/src/main/kotlin/OnlyAffectedTestTask.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,8 @@ import org.gradle.kotlin.dsl.support.get
import org.gradle.process.ExecOperations

/**
* This task will run only the tests for affected modules and dependants.
*
* This task dependsOn: [https://github.com/leandroBorgesFerreira/dag-command]
* That will generate for us a list of affected modules
* This task will run only the tests for affected modules and dependants when affected-module
* information is available in [AFFECTED_MODULES_FILE].
*
* You can define your own task by manually.
* Or you can add it to the [TestTaskConfiguration] enum, and it will be added automatically
Expand All @@ -42,51 +40,62 @@ open class OnlyAffectedTestTask : DefaultTask() {

init {
group = "verification"
description = "Installs and runs the tests for debug on connected devices (Only for affected modules)."
// TODO(refactor): The task should not exec other gradlew tasks,
// but rather be configured based on dagCommand, and add other tasks as dependency,
// simplifying the logic and making it cacheable
description = "Runs tests for affected modules when available, otherwise runs all tests."
setDependsOn(mutableListOf("dag-command"))
}

@TaskAction
fun runOnlyAffectedConnectedTest() {
var affectedModules: Set<String> = setOf()
project.layout.buildDirectory.file("dag-command/affected-modules.json").get().asFile.useLines {
affectedModules = it.joinToString()
.removeSurrounding("[", "]")
.replace("\"", "")
.split(",")
.toSet()
}
val affectedModules = readAffectedModules()
val missingAffectedModulesData = affectedModules == null
val runAllTests = hasToRunAllTests() || missingAffectedModulesData

if (!hasToRunAllTests() && (affectedModules.isEmpty() || affectedModules.first().isEmpty())) {
if (!runAllTests && affectedModules.orEmpty().isEmpty()) {
println("\uD83E\uDD8B It is not necessary to run any test, ending here to free up some resources.")
return
}

executeTask(affectedModules)
executeTask(
affectedModules = affectedModules.orEmpty(),
runAllTests = runAllTests,
Comment on lines +49 to +60
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the dag-command is not supported by Gradle 9 so here is a task that does more or less the same

missingAffectedModulesData = missingAffectedModulesData
)
}

private fun executeTask(affectedModules: Set<String>) {
private fun executeTask(affectedModules: Set<String>, runAllTests: Boolean, missingAffectedModulesData: Boolean) {
val tasksName = mutableListOf<String>()
val hasToRunAllTests = hasToRunAllTests()
project.subprojects
.filter { (hasToRunAllTests || affectedModules.contains(it.path)) && !ignoredModules.contains(it.name) }
.filter { (runAllTests || affectedModules.contains(it.path)) && !ignoredModules.contains(it.path) }
.forEach { childProject ->
tasksName.addAll(
childProject.tasks
.filter { it.name.equals(configuration.testTarget, true) }
.map { task ->
println("Adding task: ${childProject.path}:${task.name}")
"${childProject.path}:${task.name}"
}.toList()
)
val targetTaskName = childProject.tasks.names.firstOrNull { it.equals(configuration.testTarget, true) }
targetTaskName?.let { taskName ->
println("Adding task: ${childProject.path}:$taskName")
tasksName.add("${childProject.path}:$taskName")
}
}

if (missingAffectedModulesData) {
println("\uD83D\uDD27 Running all tests because affected-modules data is unavailable.")
}
tasksName.forEach(::runTargetTask)
}

private fun readAffectedModules(): Set<String>? {
val affectedModulesFile = project.layout.buildDirectory.file(AFFECTED_MODULES_FILE).get().asFile
if (!affectedModulesFile.exists()) {
println("\uD83D\uDD27 Missing '$AFFECTED_MODULES_FILE', falling back to all modules.")
return null
}

return affectedModulesFile.readText()
.trim()
.removeSurrounding("[", "]")
.split(",")
.map { it.trim().removeSurrounding("\"") }
.filter { it.isNotBlank() }
.toSet()
}

private fun runTargetTask(targetTask: String) {
println("\uD83D\uDD27 Running tests on '$targetTask'.")
val execOperations = services.get<ExecOperations>()
Expand All @@ -97,7 +106,7 @@ open class OnlyAffectedTestTask : DefaultTask() {
}

/**
* Check if we have to run all tests, by looking at untracked by dag-command files [globalBuildSettingsFiles].
* Check if we have to run all tests by looking at root-level build files [globalBuildSettingsFiles].
*/
private fun hasToRunAllTests(): Boolean {
val globalBuildSettingsFiles = listOf(
Expand Down Expand Up @@ -131,5 +140,6 @@ open class OnlyAffectedTestTask : DefaultTask() {

private companion object {
val IGNORED_MODULES = listOf(":data:protobuf", ":tools:protobuf-codegen")
const val AFFECTED_MODULES_FILE = "dag-command/affected-modules.json"
}
}
Loading
Loading