diff --git a/Plugins/Gradle/src/main/kotlin/dev/testify/TestifyPlugin.kt b/Plugins/Gradle/src/main/kotlin/dev/testify/TestifyPlugin.kt index f5e48428..54d63de8 100644 --- a/Plugins/Gradle/src/main/kotlin/dev/testify/TestifyPlugin.kt +++ b/Plugins/Gradle/src/main/kotlin/dev/testify/TestifyPlugin.kt @@ -1,7 +1,7 @@ /* * The MIT License (MIT) * - * Modified work copyright (c) 2022-2024 ndtp + * Modified work copyright (c) 2022-2026 ndtp * Original work copyright (c) 2019 Shopify Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -26,7 +26,6 @@ package dev.testify import dev.testify.TestifyPlugin.Companion.EVALUATED_SETTINGS -import dev.testify.internal.Adb import dev.testify.internal.Style.Description import dev.testify.internal.android import dev.testify.internal.isVerbose @@ -81,8 +80,6 @@ class TestifyPlugin : Plugin { if (project.isVerbose) println(Description, "Adding androidTestImplementation($dependency)") project.dependencies.add("androidTestImplementation", dependency) } - - Adb.init(project) } private fun Project.addManifestPlaceholders(settings: TestifySettings) { diff --git a/Plugins/Gradle/src/main/kotlin/dev/testify/internal/Adb.kt b/Plugins/Gradle/src/main/kotlin/dev/testify/internal/Adb.kt index 7d8199a9..6d1f1776 100644 --- a/Plugins/Gradle/src/main/kotlin/dev/testify/internal/Adb.kt +++ b/Plugins/Gradle/src/main/kotlin/dev/testify/internal/Adb.kt @@ -1,7 +1,7 @@ /* * The MIT License (MIT) * - * Modified work copyright (c) 2022-2024 ndtp + * Modified work copyright (c) 2022-2026 ndtp * Original work copyright (c) 2019 Shopify Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -25,18 +25,29 @@ package dev.testify.internal -import com.android.build.api.variant.ApplicationAndroidComponentsExtension -import com.android.build.api.variant.LibraryAndroidComponentsExtension import dev.testify.internal.StreamData.BufferedStream import dev.testify.internal.Style.Description import org.gradle.api.GradleException -import org.gradle.api.Project -class Adb { +class Adb( + private val adbService: AdbService +) { private val arguments = ArrayList() private var streamData: StreamData? = null + private val adbPath: String + get() = adbService.adbPath + + private val deviceTargetIndex: Int + get() = adbService.deviceTargetIndex + + private val verbose: Boolean + get() = adbService.verbose + + private val forcedUser: Int? + get() = adbService.forcedUser + fun emulator(): Adb { arguments.add("-e") return this @@ -72,7 +83,7 @@ class Adb { if (forcedUser != null) { arguments("--user", "$forcedUser") } else { - val user = Device.user + val user = Device.user(adbService) if (user.isNotEmpty() && (user.toIntOrNull() ?: 0) > 0) { arguments("--user", user) } @@ -97,7 +108,7 @@ class Adb { fun execute(targetsDevice: Boolean = true): String { if (targetsDevice) { - val deviceTarget = Device.targets[deviceTargetIndex] + val deviceTarget = Device.targets(adbService)[deviceTargetIndex] if (deviceTarget != null) { arguments.add(0, "-s") arguments.add(1, deviceTarget) @@ -121,32 +132,6 @@ class Adb { return this } - - companion object { - private var adbPathProvider: (() -> String)? = null - private var verbose: Boolean = false - var forcedUser: Int? = null - private var deviceTargetIndex: Int = 0 - - fun init(project: Project) { - adbPathProvider = { - val androidComponents = project.extensions.findByType( - ApplicationAndroidComponentsExtension::class.java - ) ?: project.extensions.findByType( - LibraryAndroidComponentsExtension::class.java - ) - androidComponents?.sdkComponents?.adb?.get()?.asFile?.absolutePath - ?: throw GradleException("adb not found via androidComponents.sdkComponents") - } - deviceTargetIndex = (project.properties["device"] as? String)?.toInt() ?: 0 - verbose = project.isVerbose - forcedUser = project.user - } - - private val adbPath: String - get() = adbPathProvider?.invoke() - ?: throw GradleException("Adb.init() must be called before using Adb") - } } typealias AdbParam = Pair diff --git a/Plugins/Gradle/src/main/kotlin/dev/testify/internal/AdbService.kt b/Plugins/Gradle/src/main/kotlin/dev/testify/internal/AdbService.kt new file mode 100644 index 00000000..6e27e6e8 --- /dev/null +++ b/Plugins/Gradle/src/main/kotlin/dev/testify/internal/AdbService.kt @@ -0,0 +1,69 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 ndtp + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.testify.internal + +import com.android.build.api.variant.ApplicationAndroidComponentsExtension +import com.android.build.api.variant.LibraryAndroidComponentsExtension +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.services.BuildService +import org.gradle.api.services.BuildServiceParameters + +abstract class AdbService : BuildService { + interface Params : BuildServiceParameters { + val adbPath: Property + val verbose: Property + val forcedUser: Property + val deviceTargetIndex: Property + } + + val adbPath: String + get() = parameters.adbPath.get() + + val verbose: Boolean + get() = parameters.verbose.get() + + val forcedUser: Int? + get() = parameters.forcedUser.orNull + + val deviceTargetIndex: Int + get() = parameters.deviceTargetIndex.get() +} + +internal fun Project.getAdbServiceProvider(): Provider = + gradle.sharedServices.registerIfAbsent("adbService", AdbService::class.java) { + val androidComponents = + extensions.findByType(ApplicationAndroidComponentsExtension::class.java) + ?: extensions.findByType(LibraryAndroidComponentsExtension::class.java) + ?: throw GradleException("?") + it.parameters.adbPath.set( + androidComponents?.sdkComponents?.adb?.get()?.asFile?.absolutePath + ?: throw GradleException("adb not found via androidComponents.sdkComponents") + ) + it.parameters.verbose.set(project.isVerbose) + it.parameters.forcedUser.set(project.user) + it.parameters.deviceTargetIndex.set((project.properties["device"] as? String)?.toInt() ?: 0) + } diff --git a/Plugins/Gradle/src/main/kotlin/dev/testify/internal/Device.kt b/Plugins/Gradle/src/main/kotlin/dev/testify/internal/Device.kt index 9211e7b0..ba424066 100644 --- a/Plugins/Gradle/src/main/kotlin/dev/testify/internal/Device.kt +++ b/Plugins/Gradle/src/main/kotlin/dev/testify/internal/Device.kt @@ -1,7 +1,7 @@ /* * The MIT License (MIT) * - * Modified work copyright (c) 2022-2024 ndtp + * Modified work copyright (c) 2022-2026 ndtp * Original work copyright (c) 2019 Shopify Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -27,119 +27,112 @@ package dev.testify.internal object Device { - private val version: Int - get() { - return Adb().getprop("ro.build.version.sdk").toInt() - } + fun version(adbService: AdbService): Int { + return Adb(adbService).getprop(adbService, "ro.build.version.sdk").toInt() + } - val locale: String - get() { - return when { - version in 21..22 -> { - var language = Adb().getprop("persist.sys.language") - if (language.isBlank()) { - language = "en" - } - var region = Adb().getprop("persist.sys.country") - if (region.isBlank()) { - region = "US" - } - "${language}_$region" + fun locale(adbService: AdbService): String { + return when { + version(adbService) in 21..22 -> { + var language = Adb(adbService).getprop(adbService, "persist.sys.language") + if (language.isBlank()) { + language = "en" } - - version >= 23 -> { - var result = Adb().getprop("persist.sys.locale").trim().replace("-", "_") - if (result.isBlank()) { - result = "en_US" - } - return result + var region = Adb(adbService).getprop(adbService, "persist.sys.country") + if (region.isBlank()) { + region = "US" } + "${language}_$region" + } - else -> "unsupported" + version(adbService) >= 23 -> { + var result = Adb(adbService).getprop(adbService, "persist.sys.locale").trim().replace("-", "_") + if (result.isBlank()) { + result = "en_US" + } + return result } - } - val timeZone: String - get() { - return Adb().getprop("persist.sys.timezone") + else -> "unsupported" } + } - private val displayDensity: Int - get() { - val densityLine = Adb() - .shell() - .arguments( - "wm", - "density" - ) - .execute().trim() - return if (densityLine.contains("Override density", true)) { - densityLine.split(":").last().trim().toInt() - } else { - densityLine.substring("Physical density: ".length).trim().toInt() - } - } + fun timeZone(adbService: AdbService): String { + return Adb(adbService).getprop(adbService, "persist.sys.timezone") + } - private val displaySize: String - get() { - val sizeLine = Adb() - .shell() - .arguments( - "wm", - "size" - ) - .execute().trim() - return sizeLine.substring("Physical size: ".length).trim() + fun displayDensity(adbService: AdbService): Int { + val densityLine = Adb(adbService) + .shell() + .arguments( + "wm", + "density" + ) + .execute().trim() + return if (densityLine.contains("Override density", true)) { + densityLine.split(":").last().trim().toInt() + } else { + densityLine.substring("Physical density: ".length).trim().toInt() } + } - internal val user: String - get() { - val user = Adb() - .shell() - .arguments( - "am", - "get-current-user" - ) - .execute().trim() - return user.ifEmpty { "0" } - } + fun displaySize(adbService: AdbService): String { + val sizeLine = Adb(adbService) + .shell() + .arguments( + "wm", + "size" + ) + .execute().trim() + return sizeLine.substring("Physical size: ".length).trim() + } + + internal fun user(adbService: AdbService): String { + val user = Adb(adbService) + .shell() + .arguments( + "am", + "get-current-user" + ) + .execute().trim() + return user.ifEmpty { "0" } + } - internal fun deviceKey(): String { - return "$version-$displaySize@${displayDensity}dp-$locale" + internal fun deviceKey(adbService: AdbService): String { + return "${version(adbService)}-${displaySize(adbService)}@${displayDensity(adbService)}dp-${locale(adbService)}" } - private fun Adb.getprop(prop: String): String { + private fun Adb.getprop(adbService: AdbService, prop: String): String { shell() argument("getprop") argument(prop) return execute().trim() } - val isEmpty: Boolean - get() = (count == 0) + fun isEmpty(adbService: AdbService): Boolean { + return count(adbService) == 0 + } - val count: Int - get() { - val result = Adb() - .argument("devices") - .execute(targetsDevice = false) + fun count(adbService: AdbService): Int { + val result = Adb(adbService) + .argument("devices") + .execute(targetsDevice = false) - return result.lines().count { - it.isNotBlank() && !it.contains("List of devices attached") - } + return result.lines().count { + it.isNotBlank() && !it.contains("List of devices attached") } + } - val targets: Map - get() { - val map = HashMap() - enumerateDevices().mapIndexed { index, s -> - map[index] = s - } - return map + fun targets(adbService: AdbService): Map { + val map = HashMap() + enumerateDevices(adbService).mapIndexed { index, s -> + map[index] = s } + return map + } - private fun enumerateDevices(): List { - val result = Adb() + private fun enumerateDevices(adbService: AdbService): List { + val result = Adb(adbService) .argument("devices") .execute(targetsDevice = false) diff --git a/Plugins/Gradle/src/main/kotlin/dev/testify/internal/DeviceUtilities.kt b/Plugins/Gradle/src/main/kotlin/dev/testify/internal/DeviceUtilities.kt index 4ed98205..b79451a6 100644 --- a/Plugins/Gradle/src/main/kotlin/dev/testify/internal/DeviceUtilities.kt +++ b/Plugins/Gradle/src/main/kotlin/dev/testify/internal/DeviceUtilities.kt @@ -1,7 +1,7 @@ /* * The MIT License (MIT) * - * Modified work copyright (c) 2022-2024 ndtp + * Modified work copyright (c) 2022-2026 ndtp * Original work copyright (c) 2019 Shopify Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -55,13 +55,13 @@ internal fun Adb.listFiles(path: String): List { return log.lines().filter { it.isNotEmpty() }.map { "$path/$it" } } -internal fun listFailedScreenshotsWithPath(src: String, targetPackageId: String, isVerbose: Boolean): List { - val rootDir = Adb() +internal fun listFailedScreenshotsWithPath(adbService: AdbService, src: String, targetPackageId: String, isVerbose: Boolean): List { + val rootDir = Adb(adbService) .shell() .runAs(targetPackageId) .listFiles(src) val files = rootDir.flatMap { - Adb() + Adb(adbService) .shell() .runAs(targetPackageId) .listFiles(it) @@ -74,12 +74,14 @@ internal fun listFailedScreenshotsWithPath(src: String, targetPackageId: String, } internal fun listFailedScreenshots( + adbService: AdbService, src: String, dst: String, targetPackageId: String, isVerbose: Boolean ): List { val files = listFailedScreenshotsWithPath( + adbService = adbService, src = src, targetPackageId = targetPackageId, isVerbose = isVerbose @@ -90,8 +92,8 @@ internal fun listFailedScreenshots( internal val Project.reportFilePath: String get() = "${root.replace("testify_", "")}testify" -internal fun File.deleteOnDevice(targetPackageId: String) { - Adb() +internal fun File.deleteOnDevice(adbService: AdbService, targetPackageId: String) { + Adb(adbService) .shell() .runAs(targetPackageId) .argument("rm") diff --git a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/internal/TestifyDefaultTask.kt b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/internal/TestifyDefaultTask.kt index 96550e6b..a5cc5c60 100644 --- a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/internal/TestifyDefaultTask.kt +++ b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/internal/TestifyDefaultTask.kt @@ -1,7 +1,7 @@ /* * The MIT License (MIT) * - * Modified work copyright (c) 2022-2024 ndtp + * Modified work copyright (c) 2022-2026 ndtp * Original work copyright (c) 2019 Shopify Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -27,9 +27,11 @@ package dev.testify.tasks.internal import dev.testify.internal.Device import dev.testify.internal.Style.Header +import dev.testify.internal.getAdbServiceProvider import org.gradle.api.DefaultTask import org.gradle.api.GradleException import org.gradle.api.Project +import org.gradle.api.provider.Property import org.gradle.api.tasks.Internal import org.gradle.api.tasks.TaskAction @@ -43,6 +45,9 @@ internal interface TaskDependencyProvider { abstract class TestifyDefaultTask : DefaultTask() { + @get:Internal + val adbServiceProvider: Property = project.objects.property(dev.testify.internal.AdbService::class.java) + @Internal override fun getDescription() = super.getDescription() @@ -58,10 +63,12 @@ abstract class TestifyDefaultTask : DefaultTask() { @get:Internal open val isDeviceRequired = true - internal open fun provideInput(project: Project) {} + internal open fun provideInput(project: Project) { + this.adbServiceProvider.set(project.getAdbServiceProvider()) + } protected open fun beforeAction() { - if (isDeviceRequired && Device.isEmpty) { + if (isDeviceRequired && Device.isEmpty(adbServiceProvider.get())) { throw GradleException( "No Android Virtual Device found. Please start an emulator prior to running Testify tasks." ) diff --git a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/main/ScreenshotClearTask.kt b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/main/ScreenshotClearTask.kt index c87846a7..15f4c500 100644 --- a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/main/ScreenshotClearTask.kt +++ b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/main/ScreenshotClearTask.kt @@ -1,7 +1,7 @@ /* * The MIT License (MIT) * - * Modified work copyright (c) 2022-2024 ndtp + * Modified work copyright (c) 2022-2026 ndtp * Original work copyright (c) 2019 Shopify Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -61,6 +61,7 @@ open class ScreenshotClearTask : TestifyDefaultTask() { override fun taskAction() { val failedScreenshots = listFailedScreenshotsWithPath( + adbService = adbServiceProvider.get(), src = screenshotDirectory, targetPackageId = targetPackageId, isVerbose = isVerbose @@ -75,7 +76,7 @@ open class ScreenshotClearTask : TestifyDefaultTask() { failedScreenshots.forEach { val file = File(it) println(Failure, " x ${file.nameWithoutExtension}") - file.deleteOnDevice(targetPackageId) + file.deleteOnDevice(adbServiceProvider.get(), targetPackageId) } } diff --git a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/main/ScreenshotPullTask.kt b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/main/ScreenshotPullTask.kt index 95dba4e1..b001516c 100644 --- a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/main/ScreenshotPullTask.kt +++ b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/main/ScreenshotPullTask.kt @@ -1,7 +1,7 @@ /* * The MIT License (MIT) * - * Modified work copyright (c) 2022-2024 ndtp + * Modified work copyright (c) 2022-2026 ndtp * Original work copyright (c) 2019 Shopify Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -61,6 +61,9 @@ open class ScreenshotPullTask : TestifyDefaultTask() { @get:Input var pullWaitTime: Long = 0L + @get:Input + lateinit var parentDirectory: String + override fun getDescription() = "Pull screenshots from the device and wait for all files to be committed to disk" override fun provideInput(project: Project) { @@ -70,6 +73,7 @@ open class ScreenshotPullTask : TestifyDefaultTask() { targetPackageId = project.testifySettings.targetPackageId isVerbose = project.isVerbose pullWaitTime = project.testifySettings.pullWaitTime + parentDirectory = project.projectDir.absolutePath } override fun taskAction() { @@ -80,18 +84,13 @@ open class ScreenshotPullTask : TestifyDefaultTask() { println(" Destination = $destinationImageDirectory") println() - val failedScreenshots = listFailedScreenshots( + listFailedScreenshots( + adbService = adbServiceProvider.get(), src = screenshotDirectory, dst = destinationImageDirectory, targetPackageId = targetPackageId, isVerbose = isVerbose ) - if (failedScreenshots.isEmpty()) { - println(Success, " No failed screenshots found") - return - } - - println(" ${failedScreenshots.size} images to be pulled") pullScreenshots() syncScreenshots() @@ -105,7 +104,7 @@ open class ScreenshotPullTask : TestifyDefaultTask() { val dstFile = if (File(dst).isAbsolute) { File(dst) } else { - File(project.projectDir, dst) + File(parentDirectory, dst) } val key = this.removePrefix("$src/").replace('/', File.separatorChar) return File(dstFile, "$SCREENSHOT_DIR${File.separatorChar}$key").path @@ -116,11 +115,12 @@ open class ScreenshotPullTask : TestifyDefaultTask() { val dstFile = if (File(dst).isAbsolute) { File(dst) } else { - File(project.projectDir, dst) + File(parentDirectory, dst) } dstFile.assurePath() val failedScreenshots = listFailedScreenshotsWithPath( + adbService = adbServiceProvider.get(), src = screenshotDirectory, targetPackageId = targetPackageId, isVerbose = isVerbose @@ -135,7 +135,7 @@ open class ScreenshotPullTask : TestifyDefaultTask() { File(localPath).parentFile.assurePath() - Adb() + Adb(adbServiceProvider.get()) .execOut() .runAs(targetPackageId) .argument("cat") @@ -149,6 +149,7 @@ open class ScreenshotPullTask : TestifyDefaultTask() { private fun syncScreenshots() { val failedScreenshots = listFailedScreenshots( + adbService = adbServiceProvider.get(), src = screenshotDirectory, dst = destinationImageDirectory, targetPackageId = targetPackageId, diff --git a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/main/ScreenshotTestTask.kt b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/main/ScreenshotTestTask.kt index 813f38cf..240c7888 100644 --- a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/main/ScreenshotTestTask.kt +++ b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/main/ScreenshotTestTask.kt @@ -1,7 +1,7 @@ /* * The MIT License (MIT) * - * Modified work copyright (c) 2022-2024 ndtp + * Modified work copyright (c) 2022-2026 ndtp * Original work copyright (c) 2019 Shopify Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -159,7 +159,7 @@ open class ScreenshotTestTask : TestifyDefaultTask() { .addAll(getRuntimeParams()) .add(annotation) - val log = Adb() + val log = Adb(adbServiceProvider.get()) .shell() .argument("am") .argument("instrument") diff --git a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/report/ReportPullTask.kt b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/report/ReportPullTask.kt index 66df22ca..d8e2fde9 100644 --- a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/report/ReportPullTask.kt +++ b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/report/ReportPullTask.kt @@ -1,7 +1,7 @@ /* * The MIT License (MIT) * - * Modified work copyright (c) 2022-2024 ndtp + * Modified work copyright (c) 2022-2026 ndtp * Original work copyright (c) 2021 Shopify Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -74,7 +74,7 @@ open class ReportPullTask : ReportTask() { println(" Pulling report:") val reportFilePath = reportFilePath - val files = Adb() + val files = Adb(adbServiceProvider.get()) .shell() .runAs(targetPackageId) .listFiles(reportFilePath) @@ -107,7 +107,7 @@ open class ReportPullTask : ReportTask() { println(ProgressStatus, "Copying $sourceFilePath to ${destinationFile.absolutePath}") } - Adb() + Adb(adbServiceProvider.get()) .execOut() .runAs(targetPackageId) .argument("cat") diff --git a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/report/ReportShowTask.kt b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/report/ReportShowTask.kt index 044445c4..51a3b55e 100644 --- a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/report/ReportShowTask.kt +++ b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/report/ReportShowTask.kt @@ -1,7 +1,7 @@ /* * The MIT License (MIT) * - * Modified work copyright (c) 2022-2024 ndtp + * Modified work copyright (c) 2022-2026 ndtp * Original work copyright (c) 2021 Shopify Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -56,7 +56,7 @@ open class ReportShowTask : ReportTask() { override fun taskAction() { val reportFilePath = reportFilePath - val files = Adb() + val files = Adb(adbServiceProvider.get()) .shell() .runAs(targetPackageId) .listFiles(reportFilePath) @@ -71,7 +71,7 @@ open class ReportShowTask : ReportTask() { } private fun show(sourceFilePath: String) { - Adb() + Adb(adbServiceProvider.get()) .execOut() .runAs(targetPackageId) .argument("cat") diff --git a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/DeviceKeyTask.kt b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/DeviceKeyTask.kt index 5a764c56..8bf095db 100644 --- a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/DeviceKeyTask.kt +++ b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/DeviceKeyTask.kt @@ -1,7 +1,7 @@ /* * The MIT License (MIT) * - * Modified work copyright (c) 2022-2024 ndtp + * Modified work copyright (c) 2022-2026 ndtp * Original work copyright (c) 2019 Shopify Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -35,7 +35,7 @@ open class DeviceKeyTask : TestifyUtilityTask() { override fun taskAction() { println(" Format: {api_version}-{width_in_pixels}x{height_in_pixels}@{dpi}_{locale}") - println(" key = ${Device.deviceKey()}") + println(" key = ${Device.deviceKey(adbServiceProvider.get())}") } companion object : TaskNameProvider { diff --git a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/DevicesTask.kt b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/DevicesTask.kt index b63b5fb8..56715a0f 100644 --- a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/DevicesTask.kt +++ b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/DevicesTask.kt @@ -1,7 +1,7 @@ /* * The MIT License (MIT) * - * Modified work copyright (c) 2022 ndtp + * Modified work copyright (c) 2022-2026 ndtp * Original work copyright (c) 2019 Shopify Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -36,7 +36,7 @@ open class DevicesTask : TestifyUtilityTask() { override fun getDescription() = "Displays Testify devices" override fun taskAction() { - val devices = Device.targets + val devices = Device.targets(adbServiceProvider.get()) println(" Connected devices = ${devices.size}") println(divider) if (devices.isEmpty()) { diff --git a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/DisableSoftKeyboardTask.kt b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/DisableSoftKeyboardTask.kt index e92fab0e..1a74ea31 100644 --- a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/DisableSoftKeyboardTask.kt +++ b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/DisableSoftKeyboardTask.kt @@ -1,7 +1,7 @@ /* * The MIT License (MIT) * - * Modified work copyright (c) 2022 ndtp + * Modified work copyright (c) 2022-2026 ndtp * Original work copyright (c) 2019 Shopify Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -34,7 +34,7 @@ open class DisableSoftKeyboardTask : TestifyUtilityTask() { override fun getDescription() = "Disables the soft keyboard on the device" override fun taskAction() { - Adb().arguments( + Adb(adbServiceProvider.get()).arguments( "shell", "settings", "put", diff --git a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/HidePasswordsTasks.kt b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/HidePasswordsTasks.kt index b8bbb695..0df39d49 100644 --- a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/HidePasswordsTasks.kt +++ b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/HidePasswordsTasks.kt @@ -1,7 +1,7 @@ /* * The MIT License (MIT) * - * Modified work copyright (c) 2022 ndtp + * Modified work copyright (c) 2022-2026 ndtp * Original work copyright (c) 2019 Shopify Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -34,7 +34,7 @@ open class HidePasswordsTasks : TestifyUtilityTask() { override fun getDescription() = "Hides passwords fully on the device" override fun taskAction() { - Adb().arguments( + Adb(adbServiceProvider.get()).arguments( "shell", "settings", "put", diff --git a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/LocaleTask.kt b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/LocaleTask.kt index 457b3edc..224c2955 100644 --- a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/LocaleTask.kt +++ b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/LocaleTask.kt @@ -1,7 +1,7 @@ /* * The MIT License (MIT) * - * Modified work copyright (c) 2022 ndtp + * Modified work copyright (c) 2022-2026 ndtp * Original work copyright (c) 2019 Shopify Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -34,7 +34,7 @@ open class LocaleTask : TestifyUtilityTask() { override fun getDescription() = "Displays the device locale." override fun taskAction() { - println(" Current Locale = ${Device.locale}") + println(" Current Locale = ${Device.locale(adbServiceProvider.get())}") } companion object : TaskNameProvider { diff --git a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/SettingsTask.kt b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/SettingsTask.kt index 870ffeae..0d7fea81 100644 --- a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/SettingsTask.kt +++ b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/SettingsTask.kt @@ -1,7 +1,7 @@ /* * The MIT License (MIT) * - * Modified work copyright (c) 2022-2024 ndtp + * Modified work copyright (c) 2022-2026 ndtp * Original work copyright (c) 2019 Shopify Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -25,7 +25,6 @@ package dev.testify.tasks.utility -import dev.testify.internal.Adb import dev.testify.internal.Device import dev.testify.internal.reportFilePath import dev.testify.internal.screenshotDirectory @@ -114,7 +113,7 @@ open class SettingsTask : TestifyUtilityTask() { } override fun taskAction() { - val userId = Adb.forcedUser?.toString() ?: Device.user.takeUnless { Device.isEmpty } ?: "Device not found" + val userId = adbServiceProvider.get().forcedUser?.toString() ?: Device.user(adbServiceProvider.get()).takeUnless { Device.isEmpty(adbServiceProvider.get()) } ?: "Device not found" println(" baselineSourceDir = $baselineSourceDir") println(" installAndroidTestTask = $installAndroidTestTask") diff --git a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/TimeZoneTask.kt b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/TimeZoneTask.kt index fae27446..089ec711 100644 --- a/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/TimeZoneTask.kt +++ b/Plugins/Gradle/src/main/kotlin/dev/testify/tasks/utility/TimeZoneTask.kt @@ -1,7 +1,7 @@ /* * The MIT License (MIT) * - * Modified work copyright (c) 2022 ndtp + * Modified work copyright (c) 2022-2026 ndtp * Original work copyright (c) 2019 Shopify Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -34,7 +34,7 @@ open class TimeZoneTask : TestifyUtilityTask() { override fun getDescription() = "Displays the time zone currently set on the device" override fun taskAction() { - println(" Time zone = ${Device.timeZone}") + println(" Time zone = ${Device.timeZone(adbServiceProvider.get())}") } companion object : TaskNameProvider { diff --git a/Plugins/Gradle/src/test/kotlin/dev/testify/internal/AdbTest.kt b/Plugins/Gradle/src/test/kotlin/dev/testify/internal/AdbTest.kt index b57198fa..f9a5f9ae 100644 --- a/Plugins/Gradle/src/test/kotlin/dev/testify/internal/AdbTest.kt +++ b/Plugins/Gradle/src/test/kotlin/dev/testify/internal/AdbTest.kt @@ -1,7 +1,7 @@ /* * The MIT License (MIT) * - * Copyright (c) 2023-2024 ndtp + * Copyright (c) 2023-2026 ndtp * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -30,13 +30,20 @@ import com.google.common.truth.Truth.assertThat import dev.testify.test.BaseTest import io.mockk.every import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.slot import io.mockk.verify +import org.gradle.api.Action import org.gradle.api.GradleException import org.gradle.api.Project import org.gradle.api.file.RegularFile +import org.gradle.api.invocation.Gradle +import org.gradle.api.plugins.ExtensionContainer +import org.gradle.api.provider.Property import org.gradle.api.provider.Provider +import org.gradle.api.services.BuildServiceRegistry +import org.gradle.api.services.BuildServiceSpec import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows @@ -47,7 +54,7 @@ class AdbTest : BaseTest() { lateinit var project: Project @RelaxedMockK - lateinit var extensions: org.gradle.api.plugins.ExtensionContainer + lateinit var extensions: ExtensionContainer @RelaxedMockK lateinit var androidComponents: ApplicationAndroidComponentsExtension @@ -58,12 +65,45 @@ class AdbTest : BaseTest() { @RelaxedMockK lateinit var adbProvider: Provider + @RelaxedMockK + lateinit var adbServiceProvider: Provider + @RelaxedMockK lateinit var regularFile: RegularFile @RelaxedMockK lateinit var adbExecutable: File + @RelaxedMockK + lateinit var gradle: Gradle + + @RelaxedMockK + lateinit var buildServiceRegistry: BuildServiceRegistry + + private val adbServiceParameters by lazy { + object : AdbService.Params { + override val adbPath: Property = mockk(relaxed = true) { + every { get() } answers { adbExecutable.absolutePath } + } + + private var _verbose: Boolean = false + override val verbose: Property = mockk(relaxed = true) { + every { get() } answers { _verbose } + every { set(any()) } answers { _verbose = this.args.first() as Boolean } + } + override val forcedUser: Property = mockk(relaxed = true) { + every { getOrNull() } answers { project.user } + } + override val deviceTargetIndex: Property = mockk(relaxed = true) { + every { get() } returns 0 + } + } + } + + val adbService = object : AdbService() { + override fun getParameters() = adbServiceParameters + } + private var processLog = mutableListOf() private val defaultResultMap = @@ -77,7 +117,18 @@ class AdbTest : BaseTest() { @BeforeEach override fun setUp() { super.setUp() - + every { project.gradle } returns gradle + every { gradle.sharedServices } returns buildServiceRegistry + every { buildServiceRegistry.registerIfAbsent("adbService", AdbService::class.java, any()) } answers { + @Suppress("UNCHECKED_CAST") + val configureAction = this.args.last() as Action>? + val buildServiceSpec = mockk>(relaxed = true) { + every { getParameters() } returns adbService.parameters + } + configureAction?.execute(buildServiceSpec) + every { adbServiceProvider.get() } returns adbService + adbServiceProvider + } every { project.extensions } returns extensions every { extensions.findByType(ApplicationAndroidComponentsExtension::class.java) } returns androidComponents every { extensions.findByType(LibraryAndroidComponentsExtension::class.java) } returns null @@ -93,14 +144,16 @@ class AdbTest : BaseTest() { mockkStatic(::println) mockkStatic(::runProcess) - every { any().isVerbose } returns false every { any().user } returns null every { println(any(), any()) } returns Unit configureRunProcessCapture(defaultResultMap) - Adb.init(project) - subject = Adb() + adbInit() + } + + private fun adbInit() { + subject = Adb(project.getAdbServiceProvider().get()) } private fun configureRunProcessCapture(resultMap: Map) { @@ -117,24 +170,24 @@ class AdbTest : BaseTest() { fun `WHEN init AND no android closure THEN throw exception`() { every { extensions.findByType(ApplicationAndroidComponentsExtension::class.java) } returns null every { extensions.findByType(LibraryAndroidComponentsExtension::class.java) } returns null - Adb.init(project) assertThrows { - Adb().argument("test").execute() + adbInit() + Adb(adbService).argument("test").execute() } } @Test fun `WHEN init THEN initialize adb`() { - Adb.init(project) + adbInit() } @Test fun `WHEN init AND no adb path THEN throw exception`() { @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") every { adbExecutable.absolutePath } returns null - Adb.init(project) assertThrows { - Adb().argument("test").execute() + adbInit() + Adb(adbService).argument("test").execute() } } @@ -148,7 +201,7 @@ class AdbTest : BaseTest() { @Test fun `WHEN project is verbose THEN print`() { every { any().isVerbose } returns true - Adb.init(project) + adbInit() subject.argument("test") subject.execute() @@ -164,7 +217,7 @@ class AdbTest : BaseTest() { @Test fun `WHEN executing any command THEN always check which device to run on`() { - Adb.init(project) + adbInit() subject.shell().execute() assertThat(processLog[0]).contains("devices") assertThat(processLog[1]).contains("-s emulator-5554") @@ -203,7 +256,7 @@ class AdbTest : BaseTest() { configureRunProcessCapture(mapOf("get-current-user" to "10")) every { any().user } returns 99 - Adb.init(project) + adbInit() subject.shell().runAs("dev.testify").execute() assertThat(processLog.last()).contains("--user 99") } diff --git a/bitrise.yml b/bitrise.yml index 8252cfd0..7181f400 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -235,6 +235,7 @@ workflows: - pipeline_build_url: "$BITRISE_BUILD_URL" before_run: - _globalSetup + - _emulatorSetup test_accessibility_ext: steps: