Skip to content

Commit

Permalink
add adbserver plugin and setup publication (#611)
Browse files Browse the repository at this point in the history
* remove now stable feature VERSION_CATALOGS definition

* move samples as separate project

* rename alure && compose modules with gradle module naming convention

* fix .cirrus to run tests form samples

* add empty kaspresso plugin && add it to samples

* fix cirrus gradlew paths resolution

* setup empty start/stop adb server tasks

* update desktop to run async

* add working directory support for adb server

* move test artifacts to sample directory

* remove manual start adb server from cirrus

* add specify adb server path option for desktop server

* fix detekt

* add gradle logger

* add documentation to Desktop.kt && fix pattern compile at every getAttachedDevicesByAdb function call

* add adb path resolution && logging

* setup gradle plugin portal publishing

* fix build error

* fix detekt

* move allure support files back where they should be

* fix build

* temporarily remove unknown props for api 30 tests

* try fix api 30 tests

* fix gradle sync for samples

* more test fixes

---------

Co-authored-by: Vladislav Sumin <vladislav.sumin@kaspersky.com>
  • Loading branch information
mfglushchenko and Vladislav Sumin authored Dec 21, 2023
1 parent 69ddba1 commit df165a9
Show file tree
Hide file tree
Showing 71 changed files with 715 additions and 46 deletions.
19 changes: 15 additions & 4 deletions .cirrus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,17 @@ check_android_30_task:
kvm: true
cpu: 8
memory: 24G
start_adb_server_background_script:
java -jar artifacts/adbserver-desktop.jar || true
# xfce4 somehow helps to pass "Geolocation" test
install_de_script: |
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install xfce4 -y
apt-get purge -y pm-utils xscreensaver*
apt-get install xvfb -y
start_de_background_script: |
Xvfb :99 -screen 0 1000x1000x16 &
sleep 5
startxfce4
accept_licenses_script:
echo yes | sdkmanager --licenses
install_emulator_script:
Expand All @@ -33,7 +42,7 @@ check_android_30_task:
-no-snapshot
-no-window
assemble_instrumented_tests_script:
./gradlew -PCI=true assembleDebugAndroidTest
cd samples && ./gradlew assembleDebugAndroidTest
wait_for_avd_script:
adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 3; done; input keyevent 82'
configure_avd_script: |
Expand All @@ -45,7 +54,9 @@ check_android_30_task:
start_logcat_background_script:
adb logcat > log.log
run_tests_script:
./gradlew -PCI=true connectedDebugAndroidTest
cd samples && ./gradlew connectedDebugAndroidTest
# After we do "adb root" connection is closed for a moment. So first attempt to pull screenshots usually fails
# That's why we make 5 attempts to pull folders
always:
stop_logcat_script: |
if [[ $(adb devices | awk 'NR>1 {print $1}') =~ "emulator.*" ]]; then
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.kaspersky.adbserver.common.log

import com.kaspersky.adbserver.common.log.filterlog.FullLoggerOptimiser
import com.kaspersky.adbserver.common.log.fulllogger.FullLogger
import com.kaspersky.adbserver.common.log.fulllogger.FullLoggerSystemImpl
import com.kaspersky.adbserver.common.log.logger.DesktopLogger
import com.kaspersky.adbserver.common.log.logger.LogLevel
Expand All @@ -12,8 +13,12 @@ import com.kaspersky.adbserver.common.log.logger.LoggerImpl
*/
object LoggerFactory {

fun getDesktopLogger(logLevel: LogLevel, desktopName: String): DesktopLogger {
val logger = getCommonLogger(logLevel, desktopName)
fun getDesktopLogger(
logLevel: LogLevel,
desktopName: String,
fullLogger: FullLogger = FullLoggerSystemImpl(logLevel, desktopName, null)
): DesktopLogger {
val logger = getCommonLogger(logLevel, desktopName, fullLogger = fullLogger)
return DesktopLogger(logger, logLevel, desktopName)
}

Expand All @@ -26,8 +31,12 @@ object LoggerFactory {
fun getDeviceLogger(logLevel: LogLevel): Logger =
getCommonLogger(logLevel)

private fun getCommonLogger(logLevel: LogLevel, desktopName: String? = null, deviceName: String? = null): Logger {
val fullLogger = FullLoggerSystemImpl(logLevel, desktopName, deviceName)
private fun getCommonLogger(
logLevel: LogLevel,
desktopName: String? = null,
deviceName: String? = null,
fullLogger: FullLogger = FullLoggerSystemImpl(logLevel, desktopName, deviceName)
): Logger {
val fullLoggerWrapper =
if (logLevel == LogLevel.DEBUG) FullLoggerOptimiser(originalFullLogger = fullLogger, generateLogs = true) else fullLogger
return LoggerImpl(fullLoggerWrapper)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.kaspersky.adbserver.common.log.fulllogger

import com.kaspersky.adbserver.common.log.logger.LogLevel

internal interface FullLogger {
interface FullLogger {

fun log(
logLevel: LogLevel? = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.kaspersky.adbserver.desktop

import com.kaspersky.adbserver.common.api.CommandResult
import java.nio.file.Path

/**
* @param adbPath - path to adb binary
*/
class AdbCommandPerformer(
private val adbPath: Path,
private val cmdCommandPerformer: CmdCommandPerformer,
) {

/**
* Be aware it's a synchronous method
* @param command - adb command without path to adb binaries
*/
fun perform(command: String): CommandResult {
return cmdCommandPerformer.perform("$adbPath $command")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ package com.kaspersky.adbserver.desktop

import com.kaspersky.adbserver.common.api.CommandResult
import com.kaspersky.adbserver.common.api.ExecutorResultStatus
import java.io.File
import java.nio.file.Path
import java.util.concurrent.TimeUnit

internal class CmdCommandPerformer(
private val desktopName: String
/**
* @param workingDir - working directory used to execute any cmd command if null when use default process working directory
*/
class CmdCommandPerformer(
private val desktopName: String,
private val workingDir: Path? = null
) {

companion object {
Expand All @@ -17,7 +23,8 @@ internal class CmdCommandPerformer(
*/
fun perform(command: String): CommandResult {
val serviceInfo = "The command was executed on desktop=$desktopName"
val process = Runtime.getRuntime().exec(command)
val workingDir = workingDir?.toFile() ?: File(".")
val process = Runtime.getRuntime().exec(command, emptyArray(), workingDir)
try {
if (process.waitFor(EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
val exitCode = process.exitValue()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import java.lang.UnsupportedOperationException

internal class CommandExecutorImpl(
private val cmdCommandPerformer: CmdCommandPerformer,
private val adbCommandPerformer: AdbCommandPerformer,
private val deviceName: String,
private val adbServerPort: String?,
private val logger: Logger,
Expand All @@ -20,9 +21,9 @@ internal class CommandExecutorImpl(
return when (command) {
is CmdCommand -> cmdCommandPerformer.perform(command.body)
is AdbCommand -> {
val adbCommand = "$adbPath ${ adbServerPort?.let { "-P $adbServerPort " } ?: "" }-s $deviceName ${command.body}"
logger.d("The created adbCommand=$adbCommand")
cmdCommandPerformer.perform(adbCommand)
val adbCommand = "${ adbServerPort?.let { "-P $adbServerPort " } ?: "" }-s $deviceName ${command.body}"
logger.d("The created adbCommand=adb $adbCommand")
adbCommandPerformer.perform(adbCommand)
}
else -> throw UnsupportedOperationException("The command=$command is unsupported command")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package com.kaspersky.adbserver.desktop
import com.kaspersky.adbserver.common.api.ExecutorResultStatus
import com.kaspersky.adbserver.common.log.LoggerFactory
import com.kaspersky.adbserver.common.log.logger.DesktopLogger
import java.util.concurrent.atomic.AtomicBoolean
import java.util.regex.Pattern
import kotlin.concurrent.thread

internal class Desktop(
class Desktop(
private val cmdCommandPerformer: CmdCommandPerformer,
private val adbCommandPerformer: AdbCommandPerformer,
private val presetEmulators: List<String>,
private val adbServerPort: String?,
private val logger: DesktopLogger,
Expand All @@ -15,20 +18,52 @@ internal class Desktop(

companion object {
private const val PAUSE_MS = 500L
private val DEVICE_PATTERN = Pattern.compile("^([a-zA-Z0-9\\-:.]+)(\\s+)(device)")
}

private val devices: MutableCollection<DeviceMirror> = mutableListOf()
private var isRunning = AtomicBoolean(false)

fun startDevicesObserving() {
/**
* Start Desktop server.
* Blocking current thread while server working
* @throws IllegalStateException - if server already running
*/
fun startDevicesObservingSync() {
if (!isRunning.compareAndSet(false, true)) error("Desktop already running")
startDevicesObservingInternal()
}

/**
* Start Desktop server asynchronously
* @throws IllegalStateException - if server already running
*/
fun startDevicesObservingAsync() {
if (!isRunning.compareAndSet(false, true)) error("Desktop already running")
thread {
startDevicesObservingInternal()
}
}

/**
* Stop Desktop server
* @throws IllegalStateException - if server already stopped
*/
fun stopDevicesObserving() {
if (!isRunning.compareAndSet(true, false)) error("Desktop already stopped")
}

private fun startDevicesObservingInternal() {
logger.d("start")
while (true) {
while (isRunning.get()) {
val namesOfAttachedDevicesByAdb = getAttachedDevicesByAdb()
namesOfAttachedDevicesByAdb.forEach { deviceName ->
if (devices.find { client -> client.deviceName == deviceName } == null) {
logger.i("New device has been found: $deviceName. Initialize connection to the device...")
val deviceMirror =
DeviceMirror.create(
cmdCommandPerformer,
adbCommandPerformer,
deviceName,
adbServerPort,
LoggerFactory.getDesktopLoggerReflectingDevice(logger, deviceName),
Expand All @@ -49,18 +84,22 @@ internal class Desktop(
}
Thread.sleep(PAUSE_MS)
}

devices.forEach { client ->
client.stopConnectionToDevice()
}
devices.clear()
}

private fun getAttachedDevicesByAdb(): List<String> {
val pattern = Pattern.compile("^([a-zA-Z0-9\\-:.]+)(\\s+)(device)")
val commandResult = cmdCommandPerformer.perform("$adbPath devices")
val commandResult = adbCommandPerformer.perform("devices")
if (commandResult.status != ExecutorResultStatus.SUCCESS) {
return emptyList()
}
val adbDevicesCommandResult: String = commandResult.description
return adbDevicesCommandResult.lines()
.asSequence()
.map { pattern.matcher(it) }
.map { DEVICE_PATTERN.matcher(it) }
.filter { matcher -> matcher.find() }
.map { matcher -> matcher.group(1) }
.filter { foundEmulator -> presetEmulators.isEmpty() || presetEmulators.contains(foundEmulator) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ internal class DeviceMirror private constructor(

fun create(
cmdCommandPerformer: CmdCommandPerformer,
adbCommandPerformer: AdbCommandPerformer,
deviceName: String,
adbServerPort: String?,
logger: Logger,
Expand All @@ -31,6 +32,7 @@ internal class DeviceMirror private constructor(
DesktopDeviceSocketConnectionFactory.getSockets(DesktopDeviceSocketConnectionType.FORWARD)
val commandExecutor = CommandExecutorImpl(
cmdCommandPerformer,
adbCommandPerformer,
deviceName,
adbServerPort,
logger,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import kotlinx.cli.ArgType
import kotlinx.cli.default
import kotlinx.cli.delimiter
import java.lang.management.ManagementFactory
import java.nio.file.Path

private const val DESKTOP = "Desktop-"
private const val ERROR_EXIT_CODE = -1

// It is assumed that adb is preinstall and available by "adb" keyword
private const val DEFAULT_ADB_PATH = "adb"

Expand Down Expand Up @@ -51,14 +54,16 @@ internal fun main(args: Array<String>) {
desktopLogger.i("Desktop started with arguments: emulators=$emulators, adbServerPort=$port, adbPath=$adbPath")

val cmdCommandPerformer = CmdCommandPerformer(desktopName)
val adbCommandPerformer = AdbCommandPerformer(Path.of(adbPath), cmdCommandPerformer)
val desktop = Desktop(
cmdCommandPerformer = cmdCommandPerformer,
adbCommandPerformer = adbCommandPerformer,
presetEmulators = emulators,
adbServerPort = port,
logger = desktopLogger,
adbPath = adbPath
)
desktop.startDevicesObserving()
desktop.startDevicesObservingSync()
}

private fun getDesktopName(): String {
Expand Down
2 changes: 0 additions & 2 deletions build-logic/settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
enableFeaturePreview("VERSION_CATALOGS")

rootProject.name = "build-logic"

include("android")
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
38 changes: 38 additions & 0 deletions kaspresso-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
plugins {
`kotlin-dsl`
`java-gradle-plugin`
id("com.gradle.plugin-publish") version "1.2.1"
}

dependencies {
implementation(libs.androidPlugin)
implementation(projects.adbServer.adbServerCommon)
implementation(projects.adbServer.adbserverDesktop)
}

group = "com.kaspersky.kaspresso"
version = "1.0"
gradlePlugin {
// TODO: fix for tests on API 30 and uncomment
// website.set("https://kasperskylab.github.io/Kaspresso/en/")
// vcsUrl.set("https://github.com/KasperskyLab/Kaspresso/")

plugins {
create("AdbServerPlugin") {
id = "com.kaspersky.kaspresso-adb-server-plugin"
displayName = "Kaspresso ADB-server plugin"
description = "Run Kaspresso ADB server for Android UI tests"
// tags.set(listOf("testing", "UI tests", "test automation", "android", "kasresso", "adb server"))
implementationClass = "com.kaspersky.kaspresso.plugin.KaspressoPlugin"
}
}
}

publishing {
repositories {
maven {
name = "localPluginRepository"
url = uri("../local-plugin-repository")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.kaspersky.kaspresso.plugin

import com.kaspersky.adbserver.common.log.LoggerFactory
import com.kaspersky.adbserver.common.log.logger.LogLevel
import com.kaspersky.adbserver.desktop.AdbCommandPerformer
import com.kaspersky.adbserver.desktop.CmdCommandPerformer
import com.kaspersky.adbserver.desktop.Desktop
import org.gradle.api.logging.Logger
import java.nio.file.Path

internal class DesktopServerHolder(private val logger: Logger) {
companion object {
private const val DESKTOP_NAME = "kaspresso-plugin-adb-server"
}

private var desktop: Desktop? = null

@Synchronized
fun start(workingDirectory: Path, adbPath: Path) {
check(desktop == null) { "Desktop already started" }

logger.debug("Starting Desktop server. workingDir=$workingDirectory, adbPath=$adbPath")

val cmdCommandPerformer = CmdCommandPerformer(DESKTOP_NAME, workingDirectory)
val adbCommandPerformer = AdbCommandPerformer(adbPath, cmdCommandPerformer)
val logger = LoggerFactory.getDesktopLogger(LogLevel.VERBOSE, DESKTOP_NAME, GradleFullLogger(logger))
desktop = Desktop(
cmdCommandPerformer = cmdCommandPerformer,
adbCommandPerformer = adbCommandPerformer,
presetEmulators = emptyList(),
adbServerPort = null,
logger = logger,
adbPath = adbPath.toString()
)
.apply { startDevicesObservingAsync() }
}

@Synchronized
fun stop() {
check(desktop != null) { "Desktop not started" }
desktop!!.stopDevicesObserving()
desktop = null
}
}
Loading

0 comments on commit df165a9

Please sign in to comment.