diff --git a/android-snaptesting/build.gradle.kts b/android-snaptesting/build.gradle.kts index 04855c9..4c3e870 100644 --- a/android-snaptesting/build.gradle.kts +++ b/android-snaptesting/build.gradle.kts @@ -1,6 +1,5 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin) } android { @@ -31,13 +30,11 @@ android { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = "17" - } } kotlin { explicitApi() + jvmToolchain(17) } dependencies { diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 91036c9..b02bb75 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,5 @@ plugins { alias(libs.plugins.android.application) - alias(libs.plugins.kotlin) id("com.telefonica.androidsnaptesting-plugin") } @@ -31,9 +30,10 @@ android { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = "17" - } +} + +kotlin { + jvmToolchain(17) } dependencies { diff --git a/gradle.properties b/gradle.properties index 3c5031e..ad187a7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,4 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 android.useAndroidX=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official -# Enables namespacing of each library's R class so that its R class includes only the -# resources declared in the library itself and none from the library's dependencies, -# thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +# Non-transitive R classes are the default in AGP 9.x and above diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 56e66af..853f5da 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,11 +1,11 @@ [versions] -agp = "8.13.1" +agp = "9.1.0" constraintlayout = "2.2.1" min-sdk = "23" target-sdk = "36" compile-sdk = "36" material = "1.12.0" -kotlin = "2.1.21" +kotlin = "2.3.0" appcompat = "1.7.0" androidx-junit = "1.2.1" androidx-monitor = "1.7.2" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 35751eb..3c29d3e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Mar 22 10:54:28 CET 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/include-build/gradle-plugin/src/main/java/com/telefonica/androidsnaptesting/AndroidSnaptestingPlugin.kt b/include-build/gradle-plugin/src/main/java/com/telefonica/androidsnaptesting/AndroidSnaptestingPlugin.kt index 4d95713..8c5c063 100644 --- a/include-build/gradle-plugin/src/main/java/com/telefonica/androidsnaptesting/AndroidSnaptestingPlugin.kt +++ b/include-build/gradle-plugin/src/main/java/com/telefonica/androidsnaptesting/AndroidSnaptestingPlugin.kt @@ -1,17 +1,29 @@ package com.telefonica.androidsnaptesting -import com.android.build.gradle.TestedExtension +import com.android.build.api.variant.AndroidComponentsExtension +import com.android.build.api.variant.ApplicationAndroidComponentsExtension import com.android.build.gradle.internal.tasks.DeviceProviderInstrumentTestTask import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.file.Directory +import org.gradle.api.provider.Provider import org.gradle.api.provider.ProviderFactory import java.io.File class AndroidSnaptestingPlugin : Plugin { override fun apply(project: Project) { + // Collect applicationId per test-variant name at configuration time using the new variant API. + // onVariants runs during project configuration, before afterEvaluate. + val applicationIds = mutableMapOf>() + + project.extensions.findByType(ApplicationAndroidComponentsExtension::class.java) + ?.onVariants { variant -> + // variant.name == "debug" → test task variant name == "debugAndroidTest" + applicationIds["${variant.name}AndroidTest"] = variant.applicationId + } + project.afterEvaluate { val deviceProviderInstrumentTestTasks = project.tasks @@ -21,8 +33,8 @@ class AndroidSnaptestingPlugin : Plugin { throw AndroidSnaptestingNoDeviceProviderInstrumentTestTasksException() } - val extension = project.extensions.findByType(TestedExtension::class.java) - ?: throw RuntimeException("TestedExtension not found") + val androidComponents = project.extensions.findByType(AndroidComponentsExtension::class.java) + ?: throw RuntimeException("AndroidComponentsExtension not found") val isRecordMode = project.properties["android.testInstrumentationRunnerArguments.record"] == "true" val providerFactory: ProviderFactory = project.providers @@ -32,7 +44,18 @@ class AndroidSnaptestingPlugin : Plugin { taskName, DeviceProviderInstrumentTestTask::class.java, ).get() - registerTasksForVariant(project, taskName, deviceProviderTask, extension, isRecordMode, providerFactory) + val variantName = deviceProviderTask.variantName + val applicationIdProvider = applicationIds[variantName] + ?: throw RuntimeException( + "applicationId not found for test variant '$variantName'. " + + "Available variants: ${applicationIds.keys}. " + + "Make sure the plugin is applied to a com.android.application module." + ) + registerTasksForVariant( + project, taskName, deviceProviderTask, + androidComponents, applicationIdProvider, + isRecordMode, providerFactory, + ) } } } @@ -42,17 +65,13 @@ class AndroidSnaptestingPlugin : Plugin { project: Project, taskName: String, deviceProviderTask: DeviceProviderInstrumentTestTask, - extension: TestedExtension, + androidComponents: AndroidComponentsExtension<*, *, *>, + applicationIdProvider: Provider, isRecordMode: Boolean, providerFactory: ProviderFactory, ) { val capitalizedVariant = deviceProviderTask.variantName.capitalizeFirstLetter() - - val testedVariant = extension.testVariants - .firstOrNull { it.name == deviceProviderTask.variantName } - ?: throw RuntimeException("TestVariant not found for ${deviceProviderTask.variantName}") - val applicationIdProvider = providerFactory.provider { testedVariant.applicationId } - val adbExecutablePath = extension.adbExecutable.absolutePath + val adbExecutablePath = androidComponents.sdkComponents.adb.get().asFile.absolutePath val goldenSnapshotsSourcePath = run { val variantSourceFolder = deviceProviderTask diff --git a/include-build/gradle-plugin/src/main/java/com/telefonica/androidsnaptesting/DeviceFileManager.kt b/include-build/gradle-plugin/src/main/java/com/telefonica/androidsnaptesting/DeviceFileManager.kt index aac1a7d..83f4b04 100644 --- a/include-build/gradle-plugin/src/main/java/com/telefonica/androidsnaptesting/DeviceFileManager.kt +++ b/include-build/gradle-plugin/src/main/java/com/telefonica/androidsnaptesting/DeviceFileManager.kt @@ -2,13 +2,10 @@ package com.telefonica.androidsnaptesting import com.android.build.gradle.internal.tasks.DeviceProviderInstrumentTestTask import com.android.build.gradle.internal.testing.ConnectedDevice -import com.android.ddmlib.CollectingOutputReceiver -import com.android.ddmlib.FileListingService -import com.android.ddmlib.FileListingService.FileEntry -import com.android.ddmlib.IDevice import org.gradle.api.file.RegularFile import org.gradle.api.provider.ProviderFactory import java.io.File +import java.util.concurrent.TimeUnit fun DeviceProviderInstrumentTestTask.deviceFileManager( applicationId: String, @@ -23,39 +20,25 @@ class DeviceFileManager( private val providerFactory: ProviderFactory, ) { - fun pullRecordedSnapshots( - destinationPath: String, - ) { + fun pullRecordedSnapshots(destinationPath: String) { pullSnapshots("recorded", destinationPath) } - fun pullFailuresSnapshots( - destinationPath: String, - ) { + fun pullFailuresSnapshots(destinationPath: String) { pullSnapshots("failures", destinationPath) } fun clearAllSnapshots() { withConnectedDevices { devices -> - devices.forEach { - val receiver = CollectingOutputReceiver() - it.iDevice.executeShellCommand("rm -rf ${getDeviceAndroidSnaptestingRootAbsolutePath()}", receiver) - println(receiver.output) + devices.forEach { device -> + runAdb(device.serialNumber, "shell", "rm", "-rf", getDeviceAndroidSnaptestingRootAbsolutePath()) } } } - private fun String.toFileEntry(): FileEntry { - val parts = this.split("/") - var fileEntry = FileEntry(null, null, FileListingService.TYPE_DIRECTORY, true) - parts.forEach { - fileEntry = FileEntry(fileEntry, it, FileListingService.TYPE_DIRECTORY, false) - } - return fileEntry - } - private fun getDeviceAndroidSnaptestingRootAbsolutePath(): String = - "${FileListingService.DIRECTORY_SDCARD}/Download/android-snaptesting/$applicationId" + "/sdcard/Download/android-snaptesting/$applicationId" + private fun getDeviceAndroidSnaptestingSubfolderAbsolutePath(subFolder: String): String = "${getDeviceAndroidSnaptestingRootAbsolutePath()}/$subFolder" @@ -77,25 +60,40 @@ class DeviceFileManager( androidSnaptestingSubFolderInDevice: String, destinationPath: String, ) { - val fileEntry = getDeviceAndroidSnaptestingSubfolderAbsolutePath(androidSnaptestingSubFolderInDevice).toFileEntry() + val remotePath = getDeviceAndroidSnaptestingSubfolderAbsolutePath(androidSnaptestingSubFolderInDevice) withConnectedDevices { devices -> - devices.forEach { - pullFolderFiles( - fileEntry, - it.iDevice, - destinationPath, - ) + devices.forEach { device -> + val serial = device.serialNumber + // List files in the remote folder; ignore errors if the folder doesn't exist yet + val lsOutput = runAdbCapture(serial, "shell", "ls", remotePath) + val fileNames = lsOutput.lines() + .map { it.trim() } + .filter { it.isNotBlank() && !it.startsWith("ls:") && !it.contains("No such file") } + // Pull each file to the local destination + fileNames.forEach { fileName -> + runAdb(serial, "pull", "$remotePath/$fileName", "$destinationPath/$fileName") + } } } } - private fun pullFolderFiles( - androidSnaptestingDeviceFolder: FileEntry, - device: IDevice, - destinationPath: String, - ) { - device.fileListingService.getChildrenSync(androidSnaptestingDeviceFolder).forEach { - device.pullFile(it.fullPath, "$destinationPath/${it.name}") + private fun runAdb(serial: String, vararg args: String) { + val output = runAdbCapture(serial, *args) + println(output) + } + + private fun runAdbCapture(serial: String, vararg args: String): String { + val command = buildList { + add(adbExecutablePath) + add("-s") + add(serial) + addAll(args.toList()) } + val process = ProcessBuilder(command) + .redirectErrorStream(true) + .start() + val output = process.inputStream.bufferedReader().readText() + process.waitFor(60, TimeUnit.SECONDS) + return output } } diff --git a/include-build/gradle/libs.versions.toml b/include-build/gradle/libs.versions.toml index 19319fc..c038ad5 100644 --- a/include-build/gradle/libs.versions.toml +++ b/include-build/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] -agp = "8.4.1" -common = "31.4.1" -ddmlib = "31.4.1" -kotlin = "1.9.23" +agp = "9.1.0" +common = "32.1.0" +ddmlib = "32.1.0" +kotlin = "2.3.0" detekt = "1.23.6" publish-plugin = "1.2.0" diff --git a/mavencentral.gradle b/mavencentral.gradle index 35239cb..604a8c7 100644 --- a/mavencentral.gradle +++ b/mavencentral.gradle @@ -17,7 +17,7 @@ publishing { artifactId 'androidsnaptesting' version version - artifact("$buildDir/outputs/aar/android-snaptesting-release.aar") + artifact("${layout.buildDirectory.get().asFile}/outputs/aar/android-snaptesting-release.aar") artifact androidSourcesJar pom {