diff --git a/client/rb/Gemfile.lock b/client/rb/Gemfile.lock index ea41bcbc..cbcd9988 100644 --- a/client/rb/Gemfile.lock +++ b/client/rb/Gemfile.lock @@ -4,7 +4,7 @@ GEM childprocess (0.9.0) ffi (~> 1.0, >= 1.0.11) ffi (1.9.25) - rubyzip (1.2.2) + rubyzip (1.3.0) selenium-webdriver (3.12.0) childprocess (~> 0.5) rubyzip (~> 1.2) diff --git a/device-server/build.gradle b/device-server/build.gradle index 76c5d003..f710d901 100644 --- a/device-server/build.gradle +++ b/device-server/build.gradle @@ -4,7 +4,7 @@ buildscript { ext.ktor_version = '0.9.1' repositories { - mavenCentral() + jcenter() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" @@ -33,7 +33,7 @@ dependencies { compile group: 'org.jetbrains.kotlin', name: 'kotlin-reflect', version: kotlin_version compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-io', version: kotlinx_version compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-jdk8', version: kotlinx_version - compile group: 'org.apache.commons', name: 'commons-configuration2', version: '2.7' + compile group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.2.0' //region ktor dependencies @@ -56,7 +56,6 @@ dependencies { exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-io" } compile group: 'io.ktor', name: 'ktor-auth', version: ktor_version - compile group: 'org.apache.commons', name: 'commons-pool2', version: '2.8.0' //endregion //region log dependencies @@ -71,7 +70,7 @@ dependencies { //endregion //region process management dependencies - compile group: 'net.java.dev.jna', name: 'jna', version: "5.13.0" + compile group: 'net.java.dev.jna', name: 'jna', version: "5.5.0" compile group: 'com.zaxxer', name: 'nuprocess', version: "1.1.3" //region diff --git a/device-server/jar_launcher.sh b/device-server/jar_launcher.sh index 6ab07414..b7b6a59c 100755 --- a/device-server/jar_launcher.sh +++ b/device-server/jar_launcher.sh @@ -5,14 +5,13 @@ set -xe declare -r DEVICE_SERVER_CONFIG_PATH="${DEVICE_SERVER_CONFIG_PATH}" declare -r DEVICE_SERVER_JAR="${DEVICE_SERVER_JAR:?Jar file is required}" -declare -r WDA_RUNNER=${DEVICE_SERVER_WDA_SIMULATOR_RUNNER:-'../ios/facebook/simulators'} -declare -r WDA_DEVICE_RUNNER=${DEVICE_SERVER_WDA_DEVICE_RUNNER:-'../ios/facebook/devices'} -declare -r FBSIMCTL_VERSION=${DEVICE_SERVER_FBSIMCTL_VERSION:-'HEAD-a7c52ce'} +declare -r WDA_RUNNER=${DEVICE_SERVER_WDA_SIMULATOR_RUNNER:-'../ios/facebook/simulators/WebDriverAgentRunner-Runner.app'} +declare -r WDA_DEVICE_RUNNER=${DEVICE_SERVER_WDA_DEVICE_RUNNER:-'../ios/facebook/devices/WebDriverAgentRunner-Runner.app'} +declare -r FBSIMCTL_VERSION=${DEVICE_SERVER_FBSIMCTL_VERSION:-'HEAD-d30c2a73'} declare -r LOG_CONFIG=${DEVICE_SERVER_LOG_CONFIG:-'logback-test.xml'} declare -r NETTY_WORKER_GROUP_SIZE=${NETTY_WORKER_GROUP_SIZE:-''} declare -r NETTY_CALL_GROUP_SIZE=${NETTY_CALL_GROUP_SIZE:-''} -declare -r VIDEO_RECORDER=${VIDEO_RECORDER:-'com.badoo.automation.deviceserver.ios.simulator.video.FFMPEGVideoRecorder'} -declare -r SIMULATOR_WDA_CLASS=${SIMULATOR_WDA_CLASS:-'com.badoo.automation.deviceserver.ios.proc.XcodeTestRunnerDeviceAgent'} + export JAVA_HOME=$(/usr/libexec/java_home -v 10 -F || /usr/libexec/java_home -v 9 -F) pushd "$( dirname "${BASH_SOURCE[0]}" )" @@ -20,21 +19,11 @@ pushd "$( dirname "${BASH_SOURCE[0]}" )" exec java \ -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:InitiatingHeapOccupancyPercent=45 \ -XX:+HeapDumpOnOutOfMemoryError \ - -XX:+UnlockCommercialFeatures -XX:+FlightRecorder \ - -Dcom.sun.management.jmxremote=true \ - -Dcom.sun.management.jmxremote.port=7091 \ - -Dcom.sun.management.jmxremote.rmi.port=7091 \ - -Dcom.sun.management.jmxremote.authenticate=false \ - -Dcom.sun.management.jmxremote.ssl=false \ - -Djava.rmi.server.hostname=localhost \ -Dlogback.configurationFile=${LOG_CONFIG} \ -Ddevice.server.config.path=${DEVICE_SERVER_CONFIG_PATH} \ - -Dwda.simulator.bundles=${DA_RUNNER} \ - -Dwda.device.bundles=${DA_DEVICE_RUNNER} \ + -Dwda.bundle.path=${WDA_RUNNER} \ + -Dwda.device.bundle.path=${WDA_DEVICE_RUNNER} \ -Dfbsimctl.version=${FBSIMCTL_VERSION} \ -Dembedded.netty.workerGroupSize=${NETTY_WORKER_GROUP_SIZE} \ -Dembedded.netty.callGroupSize=${NETTY_CALL_GROUP_SIZE} \ - -Dvideo.recorder=${VIDEO_RECORDER} \ - -Dsimulator.wda=${SIMULATOR_WDA_CLASS} \ - -DuseFbsimctlProc=true \ -jar ${DEVICE_SERVER_JAR} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt index 7d7c99f7..9248da3a 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt @@ -1,34 +1,35 @@ package com.badoo.automation.deviceserver -import com.badoo.automation.deviceserver.ios.simulator.video.FFMPEGVideoRecorder -import java.io.File -import java.lang.IllegalStateException +import com.badoo.automation.deviceserver.ios.proc.SimulatorWebDriverAgent +import com.badoo.automation.deviceserver.ios.simulator.video.SimulatorVideoRecorder +import java.lang.Boolean.getBoolean class ApplicationConfiguration { - val wdaDeviceBundles: String = System.getProperty("wda.device.bundles") - ?: throw RuntimeException("Must set system property: -Dwda.device.bundles=/ABSOLUTE/PATH/ios/facebook/devices/,") + private val wdaSimulatorBundlePathProperty = "wda.bundle.path" + val wdaSimulatorBundlePath: String = System.getProperty(wdaSimulatorBundlePathProperty) + ?: throw RuntimeException("Must set system property: -D$wdaSimulatorBundlePathProperty=" + + "/ABSOLUTE/PATH/ios/facebook/simulators/WebDriverAgentRunner-Runner.app") + val wdaSimulatorBundleId: String = System.getProperty("wda.bundle.id", "com.facebook.WebDriverAgentRunner.xctrunner") - val wdaSimulatorBundles: String = System.getProperty("wda.simulator.bundles") - ?: throw RuntimeException("Must set system property: -Dwda.simulator.bundles=/ABSOLUTE/PATH/ios/facebook/simulator/") + private val wdaDeviceBundlePathProperty = "wda.device.bundle.path" + val wdaDeviceBundlePath: String = System.getProperty(wdaDeviceBundlePathProperty) + ?: throw RuntimeException("Must set system property: -D$wdaDeviceBundlePathProperty=" + + "/ABSOLUTE/PATH/ios/facebook/devices/WebDriverAgentRunner-Runner.app") private val deviceServerConfigPathProperty = "device.server.config.path" val deviceServerConfigPath: String = System.getProperty(deviceServerConfigPathProperty) - ?: throw RuntimeException("Must set system property: -D$deviceServerConfigPathProperty=./config/.device_config") + ?: throw RuntimeException("Must set system property: -D$deviceServerConfigPathProperty=./config/.device_config") val fbsimctlVersion: String = System.getProperty("fbsimctl.version", "HEAD-d30c2a73") val remoteWdaSimulatorBundleRoot = System.getProperty("remote.wda.simulator.bundle.path", "/usr/local/opt/web_driver_agent_simulator") val remoteWdaDeviceBundleRoot = System.getProperty("remote.wda.device.bundle.path", "/usr/local/opt/web_driver_agent_device") - val remoteTestHelperAppBundleRoot = System.getProperty("remote.test.helper.app.bundle.path", "/usr/local/opt/ios-device-server/test_helper_app") - val useTestHelperApp = java.lang.Boolean.getBoolean("useTestHelperApp") - val remoteVideoRecorder = File(System.getProperty("remote.video.recorder.path", "/usr/local/opt/ios-device-server-utils/record_video_x264.sh")) - val useFbsimctlProc = java.lang.Boolean.getBoolean("useFbsimctlProc") - val tempFolder = File(System.getProperty("java.io.tmpdir") ?: throw IllegalStateException("Property java.io.tmpdir is not defined")) + val trustStorePath: String = System.getProperty("trust.store.path", "") val assetsPath: String = System.getProperty("media.assets.path", "") - val appBundleCachePath: File = File(System.getProperty("app.bundle.cache.path", System.getenv("HOME")), "app_bundle_cache") - val appBundleCacheRemotePath: File = File(System.getProperty("app.bundle.cache.remote.path", "/Users/qa/app_bundle_cache")) - val videoRecorderClassName = System.getProperty("video.recorder", FFMPEGVideoRecorder::class.qualifiedName) - val simulatorBackupPath: String? = System.getProperty("simulator.backup.path") + val videoRecorderClassName = System.getProperty("video.recorder", SimulatorVideoRecorder::class.qualifiedName) + val videoRecorderFrameRate = Integer.getInteger("video.recorder.frame.rate", 4) + val simulatorWdaClassName = System.getProperty("simulator.wda.class", SimulatorWebDriverAgent::class.qualifiedName) + val shouldPreinstallWDA: Boolean = getBoolean("preinstall.simulator.wda") } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt index b1856f3a..ccb90da8 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt @@ -1,5 +1,6 @@ package com.badoo.automation.deviceserver +import com.badoo.automation.deviceserver.command.ZombieReaper import com.badoo.automation.deviceserver.controllers.DevicesController import com.badoo.automation.deviceserver.controllers.StatusController import com.badoo.automation.deviceserver.data.* @@ -13,25 +14,25 @@ import io.ktor.application.Application import io.ktor.application.ApplicationCall import io.ktor.application.call import io.ktor.application.install +import io.ktor.auth.UnauthorizedResponse import io.ktor.auth.UserIdPrincipal import io.ktor.auth.authentication import io.ktor.auth.principal -import io.ktor.features.* +import io.ktor.features.CallLogging +import io.ktor.features.ContentNegotiation +import io.ktor.features.DefaultHeaders +import io.ktor.features.StatusPages import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode import io.ktor.jackson.jackson -import io.ktor.request.path import io.ktor.request.uri import io.ktor.response.respond -import io.ktor.response.respondFile import io.ktor.response.respondText import io.ktor.routing.* import io.ktor.server.engine.ApplicationEngineEnvironmentReloading import io.ktor.server.engine.ShutDownUrl -import net.logstash.logback.marker.MapEntriesAppendingMarker import org.slf4j.LoggerFactory import java.io.File -import java.io.FileNotFoundException import java.lang.IllegalStateException import java.net.NetworkInterface import java.util.* @@ -60,8 +61,8 @@ private fun paramInt(call: ApplicationCall, s: String): Int { } fun getAddresses(): List { - return NetworkInterface.getNetworkInterfaces().toList().flatMap { networkInterface -> - networkInterface.inetAddresses.toList() + return NetworkInterface.getNetworkInterfaces().toList().flatMap { + it.inetAddresses.toList() .filter { it.address.size == 4 } .filter { !it.isLoopbackAddress } .map { it.hostAddress + "/" + it.hostName } @@ -69,6 +70,7 @@ fun getAddresses(): List { } private val appConfiguration = ApplicationConfiguration() +private val zombieReaper = ZombieReaper() private fun serverConfig(): DeviceServerConfig { if (appConfiguration.deviceServerConfigPath.isEmpty()) { @@ -96,23 +98,17 @@ private val logger = LoggerFactory.getLogger(DevicesController::class.java.simpl @Suppress("unused") fun Application.module() { val config = serverConfig() - val startTime = System.nanoTime() - val hostFactory = HostFactory( - fbsimctlVersion = appConfiguration.fbsimctlVersion, - remoteTestHelperAppRoot = File(appConfiguration.remoteTestHelperAppBundleRoot).canonicalFile, - remoteVideoRecorder = appConfiguration.remoteVideoRecorder, - appConfiguration = ApplicationConfiguration() + wdaSimulatorBundle = File(appConfiguration.wdaSimulatorBundlePath).canonicalFile, + remoteWdaSimulatorBundleRoot = File(appConfiguration.remoteWdaSimulatorBundleRoot).canonicalFile, + wdaDeviceBundle = File(appConfiguration.wdaDeviceBundlePath).canonicalFile, + remoteWdaDeviceBundleRoot = File(appConfiguration.remoteWdaDeviceBundleRoot).canonicalFile, + fbsimctlVersion = appConfiguration.fbsimctlVersion ) val deviceManager = DeviceManager(config, hostFactory) - deviceManager.cleanupTemporaryFiles() - if (appConfiguration.useTestHelperApp) { - deviceManager.extractTestApp() - } - deviceManager.extractVideoRecorder() - deviceManager.startPeriodicFileCleanup() deviceManager.startAutoRegisteringDevices() - deviceManager.launchZombieReaper() + + zombieReaper.launchReapingZombies() val devicesController = DevicesController(deviceManager) val statusController = StatusController(deviceManager) @@ -150,7 +146,7 @@ fun Application.module() { route("status") { get { val code = if (deviceManager.isReady()) HttpStatusCode.OK else HttpStatusCode.ServiceUnavailable - call.respond(code, statusController.getServerStatus(startTime)) + call.respond(code, statusController.getServerStatus()) } get("config") { call.respond(config) @@ -161,9 +157,7 @@ fun Application.module() { post("restart_gracefully") { val params = jsonContent(call) val isParallelRestart = params["parallel"]?.asBoolean() ?: false - val shouldReboot = params["reboot"]?.asBoolean() ?: false - val forceReboot = params["force_reboot"]?.asBoolean() ?: false - val restartScheduled = deviceManager.restartNodesGracefully(isParallelRestart, shouldReboot, forceReboot) + val restartScheduled = deviceManager.restartNodesGracefully(isParallelRestart) if (restartScheduled) { call.respond(HttpStatusCode.Accepted, mapOf("status" to "Scheduled graceful restart of nodes")) @@ -179,21 +173,16 @@ fun Application.module() { } post { val user = call.principal() - val deviceDto: DeviceDTO = devicesController.createDevice(jsonContent(call), user) - call.respond(deviceDto) + call.respond(devicesController.createDevice(jsonContent(call), user)) } delete { val user = call.principal() if (user == null) { - call.respond(devicesController.releaseAllDevices()) + call.respond(UnauthorizedResponse()) } else { call.respond(devicesController.releaseDevices(user)) } } - post("deploy_app") { - val appBundle = jsonContent(call) - call.respond(devicesController.deployApplication(appBundle)) - } post("-/capacity") { call.respond(devicesController.getTotalCapacity(jsonContent(call))) } @@ -207,17 +196,6 @@ fun Application.module() { delete { call.respond(devicesController.deleteReleaseDevice(param(call, "ref"))) } - delete("delete_forcefully") { - call.respond(devicesController.deleteDevice(param(call, "ref"))) - } - post("push_notification") { - val notification = jsonContent(call) - call.respond(devicesController.sendPushNotification(param(call, "ref"), notification.bundleId, notification.notificationContent)) - } - post("pasteboard") { - val pasteboard = jsonContent(call) - call.respond(devicesController.sendPasteboard(param(call, "ref"), pasteboard.pasteboardСontent)) - } post("permissions") { call.respond(devicesController.setPermissions(param(call, "ref"), jsonContent(call))) } @@ -241,24 +219,6 @@ fun Application.module() { call.respond(devicesController.getLastCrashLog(param(call, "ref"))) } } - route("shared_resources") { - delete { - val deviceRef = param(call, "ref") - val path = param(call, "path") - call.respond(devicesController.deleteFile(deviceRef, File(path).toPath())) - } - post { - val deviceRef = param(call, "ref") - val sharedResource = jsonContent(call) - call.respond(devicesController.pushFile(deviceRef, sharedResource.data, File(sharedResource.path).toPath())) - } - get { - // API(/devices/{ref}/shared_resources?path={file_path}") to get the file from shared resource directory. - val deviceRef = param(call, "ref") - val path = param(call, "path") - call.respond(devicesController.pullFile(deviceRef, File(path).toPath())) - } - } route("data") { post("pull_file") { val ref = param(call, "ref") @@ -270,7 +230,7 @@ fun Application.module() { val dataPath = jsonContent(call) if (dataPath.bundleId == null) { - throw IllegalArgumentException("Bundle id is not set. Have to set 'bundle_id' to aprropriate value.") + throw IllegalArgumentException("Bundle id is not set. Set 'bundle_id' to appropriate value.") } call.respond(devicesController.pushFile(ref, dataPath.file_name, dataPath.data, dataPath.bundleId)) @@ -280,12 +240,6 @@ fun Application.module() { val dataPath = jsonContent(call) call.respond(devicesController.listFiles(ref, dataPath)) } - - delete("{bundleId}") { - val ref = param(call, "ref") - val bundleId = param(call, "bundleId") - call.respond(devicesController.deleteAppData(ref, bundleId)) - } } route("app") { delete("{bundleId}") { @@ -293,24 +247,11 @@ fun Application.module() { val bundleId = param(call, "bundleId") call.respond(devicesController.uninstallApplication(ref, bundleId)) } - post("install") { - val ref = param(call, "ref") - val appBundle = jsonContent(call) - call.respond(devicesController.installApplication(ref, appBundle)) - } - get("install_status") { - val ref = param(call, "ref") - call.respond(devicesController.appInstallationStatus(ref)) - } - post("update_plist") { val ref = param(call, "ref") val plistEntries = jsonContent(call) call.respond(devicesController.updateApplicationPlist(ref, plistEntries)) } - get("list") { - call.respond(devicesController.listApps(param(call, "ref"))) - } } route("media") { get { @@ -327,54 +268,6 @@ fun Application.module() { call.respond(devicesController.addMedia(ref, dataPath.file_name, dataPath.data)) } } - - route("media_data") { - get { - val ref = param(call, "ref") - call.respond(JsonMapper().toJson(MediaDto(devicesController.listPhotoData(ref)))) - } - } - route("syslog") { - get { - val ref = param(call, "ref") - val logFile = devicesController.syslog(ref) - call.respondFile(logFile) - } - delete { - val ref = param(call, "ref") - call.respond(devicesController.syslogDelete(ref)) - } - post("start") { - val ref = param(call, "ref") - call.respond(devicesController.syslogStart(ref, jsonContent(call))) - } - post("stop") { - val ref = param(call, "ref") - call.respond(devicesController.syslogStop(ref)) - } - } - route("device_agent_log") { - get { - val ref = param(call, "ref") - val logFile = devicesController.instrumentationAgentLog(ref) - call.respondFile(logFile) - } - delete { - val ref = param(call, "ref") - call.respond(devicesController.deleteInstrumentationAgentLog(ref)) - } - } - route("appium_server_log") { - get { - val ref = param(call, "ref") - val logFile = devicesController.appiumServerLog(ref) - call.respondFile(logFile) - } - delete { - val ref = param(call, "ref") - call.respond(devicesController.deleteAppiumServerLog(ref)) - } - } route("diagnose/{type}") { get { val ref = param(call, "ref") @@ -406,9 +299,6 @@ fun Application.module() { //FIXME: see [call.respondFile] basically - read from ssh proc listener's ByteBuffer call.respond(devicesController.getVideo(param(call, "ref"))) } - get("log") { - call.respond(devicesController.getVideoLog(param(call, "ref"))) - } post { call.respond(devicesController.startStopVideo(param(call, "ref"), jsonContent(call))) } @@ -416,26 +306,6 @@ fun Application.module() { call.respond(devicesController.deleteVideo(param(call, "ref"))) } } - route("location") { - get("scenarios") { - call.respond(devicesController.locationListScenarios(param(call, "ref"))) - } - delete { - call.respond(devicesController.locationClear(param(call, "ref"))) - } - post("set") { - val location = jsonContent(call) - call.respond(devicesController.locationSet(param(call, "ref"), location.latitude, location.longitude)) - } - post("run") { - val scenario = jsonContent(call) - call.respond(devicesController.locationRunScenario(param(call, "ref"), scenario.scenarioName)) - } - post("start") { - val waypoints = jsonContent(call) - call.respond(devicesController.locationStartLocationSequence(param(call, "ref"), waypoints.speed, waypoints.distance, waypoints.interval, waypoints.waypoints)) - } - } get("state") { call.respond(devicesController.getDeviceState(param(call, "ref"))) } @@ -444,15 +314,6 @@ fun Application.module() { val environmentVariables = jsonContent>(call) call.respond(devicesController.setEnvironmentVariables(ref, environmentVariables)) } - get("environment/{variableName}") { - val ref = param(call, "ref") - val variableName = param(call, "variableName") - if (variableName.isNullOrEmpty()) { - throw IllegalArgumentException("Environment variable name shouldn't be empty") - } else { - call.respond(devicesController.getEnvironmentVariable(ref, variableName)) - } - } } } } @@ -471,21 +332,13 @@ fun Application.module() { is IllegalArgumentException -> HttpStatusCode(422, "Unprocessable Entity") is IllegalStateException -> HttpStatusCode.Conflict is DeviceNotFoundException -> HttpStatusCode.NotFound - is FileNotFoundException -> HttpStatusCode.NotFound is NoAliveNodesException -> HttpStatusCode.TooManyRequests is OverCapacityException -> HttpStatusCode.TooManyRequests is DeviceCreationException -> HttpStatusCode.ServiceUnavailable else -> HttpStatusCode.InternalServerError } - val path = call.request.path() - val marker = MapEntriesAppendingMarker(mapOf( - "http_api" to path, - "exception_class" to exception.javaClass.canonicalName - )) - - logger.error(marker, "HTTP_API: $path | Error: ${exception.message}", exception) - + logger.error(call.request.toString(), exception) call.respond(statusCode, hashMapOf( "error" to exception.toDto() diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/NodeConfig.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/NodeConfig.kt index 39b6dac5..4aef4282 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/NodeConfig.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/NodeConfig.kt @@ -1,6 +1,6 @@ package com.badoo.automation.deviceserver -import com.badoo.automation.deviceserver.ios.device.ConfiguredDevice +import com.badoo.automation.deviceserver.ios.device.KnownDevice import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonProperty @@ -31,7 +31,7 @@ data class NodeConfig( val uninstallApps: Boolean = false, @JsonProperty("devices") - val configuredDevices: Set = emptySet(), + val knownDevices: List = emptyList(), @JsonProperty("shutdown_simulators") val shutdownSimulators: Boolean = false diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/Program.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/Program.kt index b1ae8ac7..98bd28fb 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/Program.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/Program.kt @@ -6,8 +6,7 @@ import io.ktor.server.netty.Netty fun main(args: Array) { embeddedServer(Netty, commandLineEnvironment(args)) { - connectionGroupSize = Integer.getInteger("embedded.netty.connectionGroupSize", connectionGroupSize) workerGroupSize = Integer.getInteger("embedded.netty.workerGroupSize", workerGroupSize) callGroupSize = Integer.getInteger("embedded.netty.callGroupSize", callGroupSize) }.start(wait = true) -} +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/BackgroundProcess.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/BackgroundProcess.kt deleted file mode 100644 index 89796dd4..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/BackgroundProcess.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.badoo.automation.deviceserver.command - -class BackgroundProcess( - private val executor: IShellCommand -) { -} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ChildProcess.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ChildProcess.kt index 6adad517..e65daab5 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ChildProcess.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ChildProcess.kt @@ -4,13 +4,12 @@ import com.badoo.automation.deviceserver.LogMarkers import com.badoo.automation.deviceserver.host.Remote import net.logstash.logback.marker.MapEntriesAppendingMarker import org.slf4j.LoggerFactory -import java.io.BufferedReader import java.io.InputStream import java.io.InputStreamReader -import java.lang.RuntimeException import java.nio.charset.StandardCharsets -import java.time.Duration -import java.util.concurrent.* +import java.util.concurrent.Executors +import java.util.concurrent.Future +import java.util.concurrent.TimeUnit class ChildProcess private constructor( command: List, @@ -22,57 +21,68 @@ class ChildProcess private constructor( ) { private val logger = LoggerFactory.getLogger(javaClass.simpleName) private val logMarker = MapEntriesAppendingMarker(mapOf(LogMarkers.HOSTNAME to remoteHostname)) + private val poolExecutor = Executors.newFixedThreadPool(2) private val process: Process - private val poolExecutor: ExecutorService - val stdOutTask: Future<*> - val stdErrTask: Future<*> + private val stdOutTask: Future<*> + private val stdErrTask: Future<*> init { logger.debug(logMarker, "Starting long living process from command [$command]") process = executor.startProcess(command, commandEnvironment) - - poolExecutor = Executors.newFixedThreadPool(2) - stdOutTask = poolExecutor.submit(lineReader(process.inputStream, outWriter)) - stdErrTask = poolExecutor.submit(lineReader(process.errorStream, errWriter)) - + stdOutTask = poolExecutor.submit(streamReader(process.inputStream, outWriter)) + stdErrTask = poolExecutor.submit(streamReader(process.errorStream, errWriter)) logger.debug(logMarker, "Started long living process $this from command [$command]") } - val onExit: CompletableFuture = process.onExit() - override fun toString(): String = "< PID: ${process.pid()}>" fun isAlive(): Boolean = process.isAlive - private val processDestroyTimeOut = Duration.ofSeconds(15) fun kill() { logger.debug(logMarker, "Sending SIGTERM to process $this") - try { - process.destroy() - val exited = process.waitFor(processDestroyTimeOut.seconds, TimeUnit.SECONDS) - if (!exited) { - logger.warn(logMarker, "Process $this did not terminate gracefully within [${processDestroyTimeOut.seconds}] seconds. Sending SIGKILL") - process.destroyForcibly() - } - } catch (e: RuntimeException) { - logger.error(logMarker, "Error while terminating process $this. ${e.message}", e) + process.destroy() + + val exited = process.waitFor(PROCESS_TIMEOUT, TimeUnit.SECONDS) + + if (!exited) { + logger.warn(logMarker, "Process $this did not terminate gracefully within $PROCESS_TIMEOUT seconds. Sending SIGKILL") + process.destroyForcibly() } } - private fun lineReader(inputStream: InputStream, writer: ((line: String) -> Unit)?): Runnable { - return Runnable { - inputStream.use { stream -> - val inputStreamReader = InputStreamReader(stream, StandardCharsets.UTF_8) - val reader = BufferedReader(inputStreamReader, 65356) + private fun streamReader(inputStream: InputStream, writer: ((line: String) -> Unit)?): Runnable { + return Runnable { inputStream.use { readStream(it, writer) } } + } + + private fun readStream(stream: InputStream, writer: ((line: String) -> Unit)?) { + val reader = InputStreamReader(stream, StandardCharsets.UTF_8) + val stringBuilder = StringBuilder(BUFFER_SIZE) - var line: String? = reader.readLine() + while (true) { + val bytes = reader.read() - while (line != null) { - writer?.invoke(line) - line = reader.readLine() - } + if (bytes == EOF) { + writeString(writer, stringBuilder.toString()) + break } + + val char = bytes.toChar() + + if (char == NEWLINE) { + writeString(writer, stringBuilder.toString()) + stringBuilder.clear() + } else { + stringBuilder.append(char) + } + } + } + + private fun writeString(writer: ((line: String) -> Unit)?, string: String) { + if (writer == null || string.isEmpty()) { + return } + + writer(string) } companion object { @@ -94,23 +104,12 @@ class ChildProcess private constructor( errWriter = err_reader ) } - fun fromLocalCommand( - remoteHost: String, - userName: String, - cmd: List, - commandEnvironment: Map, - out_reader: ((line: String) -> Unit)?, - err_reader: ((line: String) -> Unit)? - ): ChildProcess { - return ChildProcess( - command = cmd, - commandEnvironment = commandEnvironment, - executor = Remote.getLocalCommandExecutor(), - remoteHostname = remoteHost, - outWriter = out_reader, - errWriter = err_reader - ) - } + + private const val EOF = -1 + private const val NEWLINE = '\n' + private const val PROCESS_TIMEOUT = 15L + private const val BUFFER_SIZE = 65536 // seems arbitrary, but using buffer with this size fastest (jmh) on multiple small commands } } +private fun StringBuilder.clear() = setLength(0) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt index e197e243..7c226a50 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt @@ -10,8 +10,8 @@ class RemoteShellCommand( private val remoteHost: String, userName: String, commonEnvironment: Map = mapOf(), - private val isVerboseMode: Boolean = false, - private val connectionTimeout: Int = 60 + isVerboseMode: Boolean = false, + connectionTimeout: Int = 10 ) : ShellCommand(commonEnvironment) { private val userAtHost: String = if (userName.isBlank()) { remoteHost } else { "$userName@$remoteHost" } override val logMarker: Marker get() = MapEntriesAppendingMarker(mapOf(LogMarkers.HOSTNAME to remoteHost)) @@ -24,7 +24,6 @@ class RemoteShellCommand( SSH_COMMAND, "-o", "ConnectTimeout=$connectionTimeout", "-o", "PreferredAuthentications=publickey", - "-o", "VerifyHostKeyDNS=no", QUIET_MODE )) @@ -34,6 +33,7 @@ class RemoteShellCommand( sshPrefix.add("-vvv") } + sshPrefix.add(userAtHost) sshCommandPrefix = ArrayList(sshPrefix) //ssh environment @@ -48,67 +48,49 @@ class RemoteShellCommand( } override fun exec(command: List, environment: Map, timeOut: Duration, - returnFailure: Boolean, logMarker: Marker?, processBuilder: ProcessBuilder): CommandResult { - val cmd = getCommandWithSSHPrefix(command, environment.keys) + returnFailure: Boolean, logMarker: Marker?, + processBuilder: ProcessBuilder): CommandResult { + val cmd = getCommandWithSSHPrefix(command) val startTime = System.nanoTime() - val remoteShellLogMarker = MapEntriesAppendingMarker(mapOf(LogMarkers.HOSTNAME to remoteHost)) - logMarker?.let { remoteShellLogMarker.add(it) } - val result = super.exec(cmd, getEnvironmentForSSH(environment), timeOut, returnFailure, remoteShellLogMarker, processBuilder) + val result = super.exec(cmd, getEnvironmentForSSH(), timeOut, returnFailure, logMarker, processBuilder) val elapsedTime = System.nanoTime() - startTime val elapsedMillis = TimeUnit.NANOSECONDS.toMillis(elapsedTime) - remoteShellLogMarker.add(MapEntriesAppendingMarker(mapOf(LogMarkers.SSH_PROFILING_MS to elapsedMillis))) - logger.debug(remoteShellLogMarker, - "Execution of SSH command took $elapsedMillis ms. Command: ${cmd.joinToString(" ")}, PID: ${result.pid}") + val marker = MapEntriesAppendingMarker( + mapOf( + LogMarkers.HOSTNAME to remoteHost, + LogMarkers.SSH_PROFILING_MS to elapsedMillis + ) + ) + + logger.debug(marker, "Execution of SSH command took $elapsedMillis ms. Command: ${cmd.joinToString(" ")}, PID: ${result.pid}") if (result.exitCode == SSH_ERROR) { // FIXME: Check stdout and stderr, if they are empty – ssh timeout, otherwise, it is likely to be command error val message = "Probably SSH could not connect to node $remoteHost. Result: $result" - logger.error(remoteShellLogMarker, message) + logger.error(logMarker, message) throw SshConnectionException(message) } return result } - override fun startProcess(command: List, environment: Map, logMarker: Marker?, processBuilder: ProcessBuilder): Process { - val remoteShellLogMarker = MapEntriesAppendingMarker(mapOf(LogMarkers.HOSTNAME to remoteHost)) - logMarker?.let { remoteShellLogMarker.add(it) } - return super.startProcess(getCommandWithSSHPrefix(command, environment.keys), getEnvironmentForSSH(environment), remoteShellLogMarker, processBuilder) + override fun startProcess(command: List, environment: Map, logMarker: Marker?, + processBuilder: ProcessBuilder): Process { + return super.startProcess(getCommandWithSSHPrefix(command), getEnvironmentForSSH(), logMarker, processBuilder) } override fun escape(value: String): String { return ShellUtils.escape(value) } - private fun getEnvironmentForSSH(environment: Map): HashMap { + private fun getEnvironmentForSSH(): HashMap { val envWithSsh = HashMap(sshEnv) - envWithSsh.putAll(environment) + envWithSsh.putAll(envWithSsh) return envWithSsh } - private fun getCommandWithSSHPrefix(command: List, environmentVariables: Set): ArrayList { - val commandWithSshPrefix = ArrayList() - commandWithSshPrefix.addAll(listOf( - SSH_COMMAND, - "-o", "ConnectTimeout=$connectionTimeout", - "-o", "PreferredAuthentications=publickey" - )) - - environmentVariables.forEach { - commandWithSshPrefix.add("-o") - commandWithSshPrefix.add("SendEnv=$it") - } - - commandWithSshPrefix.addAll(FORCE_PSEUDO_TERMINAL_ALLOCATION) - - if (isVerboseMode) { - commandWithSshPrefix.add("-vvv") - } else { - commandWithSshPrefix.add(QUIET_MODE) - } - - commandWithSshPrefix.add(userAtHost) - + private fun getCommandWithSSHPrefix(command: List): ArrayList { + val commandWithSshPrefix = ArrayList(sshCommandPrefix) commandWithSshPrefix.addAll(command) return commandWithSshPrefix } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommand.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommand.kt index bce96849..88e6a6e2 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommand.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommand.kt @@ -2,149 +2,131 @@ package com.badoo.automation.deviceserver.command import com.badoo.automation.deviceserver.LogMarkers import com.badoo.automation.deviceserver.util.ensure -import com.zaxxer.nuprocess.internal.LibC import net.logstash.logback.marker.MapEntriesAppendingMarker import org.slf4j.Logger import org.slf4j.LoggerFactory import org.slf4j.Marker -import java.io.BufferedReader import java.io.IOException import java.io.InputStream import java.io.InputStreamReader import java.nio.charset.StandardCharsets import java.time.Duration -import java.util.concurrent.* +import java.util.concurrent.Callable +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit open class ShellCommand( - private val commonEnvironment: Map = mapOf("HOME" to System.getProperty("user.home")) + private val commonEnvironment: Map ) : IShellCommand { protected val logger: Logger = LoggerFactory.getLogger(javaClass.simpleName) protected open val logMarker: Marker get() = MapEntriesAppendingMarker(mapOf(LogMarkers.HOSTNAME to "localhost")) private val executor = Executors.newCachedThreadPool() - override fun exec( - command: List, environment: Map, timeOut: Duration, - returnFailure: Boolean, logMarker: Marker?, processBuilder: ProcessBuilder - ): CommandResult { - val commandString = command.joinToString(" ") + override fun exec(command: List, environment: Map, timeOut: Duration, + returnFailure: Boolean, logMarker: Marker?, processBuilder: ProcessBuilder): CommandResult { processBuilder.command(command) - processBuilder.environment().clear() - processBuilder.environment().putAll(commonEnvironment) - processBuilder.environment().putAll(environment) + setEnvironment(processBuilder, environment) - try { - val process: Process = processBuilder.start() - val pid = process.pid() - val pidLogMarker = MapEntriesAppendingMarker(mapOf("PID" to pid)) - logMarker?.let { pidLogMarker.add(it) } - logger.debug(pidLogMarker, "Executing command: $commandString, PID: $pid") - val stdOut = executor.submit(lineReader(process.inputStream)) - val stdErr = executor.submit(lineReader(process.errorStream)) - - val hasExited = process.waitFor(timeOut.toMillis(), TimeUnit.MILLISECONDS) - - val exitCode = if (hasExited) { - process.exitValue() - } else { - Int.MIN_VALUE - } + val process: Process = processBuilder.start() + val stdOut = executor.submit(streamReader(process.inputStream)) + val stdErr = executor.submit(streamReader(process.errorStream)) + val pid = process.pid() - if (!hasExited) { - logger.error(pidLogMarker, "Command has failed to complete in time. Timeout: ${timeOut.toSeconds()} seconds. Command: $commandString, PID: $pid") - executor.submit { - waitForProcessToComplete(process, pidLogMarker, commandString, pid.toInt(), timeOut) - } - } + val commandString = command.joinToString(" ") + logger.debug(MapEntriesAppendingMarker(mapOf("PID" to pid)), "Executing command: $commandString, PID: $pid") - val result = CommandResult( - stdOut = stdOut.get(), - stdErr = stdErr.get(), - exitCode = exitCode, - cmd = command, // Store actual command - including ssh stuff. - pid = pid - ) - ensure(exitCode == 0 || returnFailure) { - val errorMessage = "Error while running command: $commandString Result=$result" - logger.error(pidLogMarker, errorMessage) - ShellCommandException(errorMessage) - } - return result - } catch (e: IOException) { - logger.error(logMarker, "Failed to execute command $command. Error: ${e.javaClass} ${e.message}", e) - val message = e.message ?: "Failed to execute command. ${e.javaClass}" - return CommandResult( - stdOut = message, - stdErr = message, - exitCode = -1, - cmd = command, - pid = -1 - ) + val hasExited = process.waitFor(timeOut.toMillis(), TimeUnit.MILLISECONDS) + val exitCode = if (hasExited) process.exitValue() else Int.MIN_VALUE + + if (!hasExited) { + logger.error(logMarker, "Command has failed to complete in time. Command: $commandString, PID: $pid") + executor.submit { waitForProcessToComplete(process, logMarker, commandString, pid.toInt(), timeOut) } + } + + val result = CommandResult( + stdOut = stdOut.get(), + stdErr = stdErr.get(), + exitCode = exitCode, + cmd = command, + pid = pid + ) + + ensure(exitCode == 0 || returnFailure) { + val errorMessage = "Error while running command: $commandString Result=$result" + logger.error(logMarker, errorMessage) + ShellCommandException(errorMessage) } + + return result } - private fun waitForProcessToComplete( - process: Process, - logMarker: Marker?, - commandString: String, - pid: Int, - timeOut: Duration - ) { - logger.debug(logMarker, "Trying to kill failed command. Command: $commandString, PID: $pid") - LibC.kill(pid, LibC.SIGTERM) - process.waitFor(timeOut.toMillis(), TimeUnit.MILLISECONDS) + override fun startProcess(command: List, environment: Map, logMarker: Marker?, + processBuilder: ProcessBuilder): Process { + logger.debug(this.logMarker, "Executing command: ${command.joinToString(" ")}") + processBuilder.command(command) + setEnvironment(processBuilder, environment) + return processBuilder.start() + } - if (process.isAlive) { - logger.debug(logMarker, "Trying to destroy failed command. Command: $commandString, PID: $pid") - process.destroy() - process.waitFor(timeOut.toMillis(), TimeUnit.MILLISECONDS) - logger.debug(logMarker, "Destroyed failed command. Command: $commandString, PID: $pid") - } + override fun escape(value: String): String { + return value + } - if (process.isAlive) { - logger.debug(logMarker, "Trying to destroy forcibly failed command. Command: $commandString, PID: $pid") - process.destroyForcibly() - process.waitFor(timeOut.toMillis(), TimeUnit.MILLISECONDS) - logger.debug(logMarker, "Destroyed forcibly failed command. Command: $commandString, PID: $pid") - } + private fun setEnvironment(processBuilder: ProcessBuilder, environment: Map) { + processBuilder.environment().clear() // do not inherit current process environment + processBuilder.environment().putAll(commonEnvironment) + processBuilder.environment().putAll(environment) + } - process.waitFor(timeOut.toMillis(), TimeUnit.MILLISECONDS) + private fun streamReader(inputStream: InputStream): Callable { + return Callable { readStream(inputStream) } } - private fun lineReader(inputStream: InputStream): Callable { - return Callable { - inputStream.use { - val inputStreamReader = InputStreamReader(it, StandardCharsets.UTF_8) - val builder = StringBuilder() - val reader = BufferedReader(inputStreamReader, 1045696) + private fun readStream(inputStream: InputStream): String { + val reader = InputStreamReader(inputStream, StandardCharsets.UTF_8) + val writer = StringBuilder(BUFFER_SIZE) + val buffer = CharArray(BUFFER_SIZE) - var line: String? = reader.readLine() + try { + while (true) { + val charCount = reader.read(buffer) - while (line != null) { - builder.append(line) - builder.append("\n") - line = reader.readLine() - } + if (charCount == EOF) break - builder.toString() + writer.append(buffer, 0, charCount) } + } catch (e: IOException) { + logger.error("Failed to read from input stream", e) + } finally { + inputStream.close() } + + return writer.toString() } - override fun startProcess( - command: List, - environment: Map, - logMarker: Marker?, - processBuilder: ProcessBuilder - ): Process { - logger.debug(this.logMarker, "Executing command: ${command.joinToString(" ")}") - processBuilder.command(command) - processBuilder.environment().clear() - processBuilder.environment().putAll(commonEnvironment) - processBuilder.environment().putAll(environment) - return processBuilder.start() + private fun waitForProcessToComplete(process: Process, logMarker: Marker?, commandString: String, pid: Int, timeOut: Duration) { + terminateProcessGracefully(process, logMarker, commandString, pid, timeOut) + terminateProcessForcibly(logMarker, commandString, pid, process, timeOut) } - override fun escape(value: String): String { - return value + private fun terminateProcessGracefully(process: Process, logMarker: Marker?, commandString: String, pid: Int, timeOut: Duration) { + if (!process.isAlive) return + + logger.debug(logMarker, "Trying to destroy failed command. Command: $commandString, PID: $pid") + process.destroy() + process.waitFor(timeOut.toMillis(), TimeUnit.MILLISECONDS) + } + + private fun terminateProcessForcibly(logMarker: Marker?, commandString: String, pid: Int, process: Process, timeOut: Duration) { + if (!process.isAlive) return + + logger.debug(logMarker, "Trying to destroy failed command forcibly. Command: $commandString, PID: $pid") + process.destroyForcibly() + process.waitFor(timeOut.toMillis(), TimeUnit.MILLISECONDS) + } + + companion object { + private const val EOF = -1 + private const val BUFFER_SIZE = 65536 // seems arbitrary, but using buffer with this size fastest (jmh) on multiple small commands } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ZombieReaper.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ZombieReaper.kt new file mode 100644 index 00000000..6c00874a --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ZombieReaper.kt @@ -0,0 +1,119 @@ +package com.badoo.automation.deviceserver.command + +import com.sun.jna.Library +import com.sun.jna.Native +import com.sun.jna.ptr.IntByReference +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import kotlin.streams.toList + +class ZombieReaper { + private val logger: Logger = LoggerFactory.getLogger(javaClass.simpleName) + private val executor = Executors.newScheduledThreadPool(1) + + fun launchReapingZombies() { + val task = { + // Try-catch is here in order to: + // - not to loose errors silently + // - not to interrupt subsequent executions + try { + reapZombies() + } catch (t: Throwable) { + logger.error("Failed to reap zombie processes. Reason: ${t.message}", t) + } + } + + executor.scheduleWithFixedDelay(task, 60L, 60L, TimeUnit.SECONDS) + } + + fun reapZombies() { + val zombies = findZombies() + + zombies.forEach { zombie -> + reap(zombie) + } + } + + private fun findZombies(): List { + val childProcesses = ProcessHandle.current().children() + val zombies = childProcesses.filter { it.isZombie } + return zombies.map { it.pid().toInt() }.toList() + } + + private fun reap(pid: Int) { + val statusReference = IntByReference() + val waitResult = cLibrary.waitpid(pid, statusReference, WNOHANG) + val exitStatus = ProcessExitStatus(statusReference.value) + + when (waitResult) { + pid -> logger.trace("Successfully reaped zombie process with PID $pid") + 0 -> logger.trace("The zombie process with PID $pid has not yet changed it's state") + -1 -> logger.error("Error happened while reaping zombie process with PID $pid") + } + + when { + exitStatus.isExited -> logger.trace("Zombie process with PID $pid had normal termination") + exitStatus.isSignaled -> logger.error("Zombie process with PID $pid was terminated by signal ${exitStatus.termSignal}") + } + } + + private val ProcessHandle.isZombie: Boolean get() = !info().command().isPresent + + companion object { + private val cLibrary: CLibrary = Native.load("c", CLibrary::class.java) + private const val WNOHANG = 1 /* Don't block waiting. */ + } +} + +/** + * Have to use C library to reap zombies + */ +private interface CLibrary : Library { + /** + * Wait for process to change state + * Refer to man page for waitpid + */ + fun waitpid(pid: Int, statusReference: IntByReference, options: Int): Int +} + +/** + * Exit status of a child process + * Refer to man page for waitpid + */ +private class ProcessExitStatus(private val status: Int) { + /** + * LibC macros WIFSIGNALED + * + * Nonzero if STATUS indicates termination by a signal. + * + * (((status) & 0x7f) + 1) >> 1) > 0) + */ + val isSignaled: Boolean + get() { + return (((status and 0x7f) + 1) shl 1) > 0 + } + + /** + * LibC macros WIFEXITED + * + * True if STATUS indicates normal termination. + */ + val isExited: Boolean + get() { + return termSignal == 0 + } + + /** + * LibC macros WTERMSIG + * + * If WIFSIGNALED(STATUS), the terminating signal. + * + * ((status) & 0x7f) + */ + val termSignal: Int + get() { + return status and 0x7f + } +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt index 4f54ae0b..64100f55 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt @@ -4,11 +4,8 @@ import com.badoo.automation.deviceserver.EmptyMap import com.badoo.automation.deviceserver.JsonMapper import com.badoo.automation.deviceserver.data.* import com.badoo.automation.deviceserver.host.management.DeviceManager -import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlAppInfo import com.fasterxml.jackson.databind.JsonNode import io.ktor.auth.UserIdPrincipal -import java.io.File -import java.nio.file.Path class DevicesController(private val deviceManager: DeviceManager) { private val happy = emptyMap() @@ -41,65 +38,20 @@ class DevicesController(private val deviceManager: DeviceManager) { return happy } - fun deleteDevice(ref: DeviceRef): EmptyMap { - deviceManager.deleteDevice(ref, "httpRequest") - return happy - } - fun releaseDevices(user: UserIdPrincipal) { deviceManager.releaseUserDevices(user.name, "httpRequest") } - fun releaseAllDevices() { - deviceManager.releaseAllDevices("httpRequest") - } - - /** - * List available simulation scenarios - */ - fun locationListScenarios(ref: DeviceRef): List { - return deviceManager.locationListScenarios(ref) - } - - /** - * Stop any running scenario and clear any simulated location - */ - fun locationClear(ref: DeviceRef) { - deviceManager.locationClear(ref) - } - - /** - * Set the location to a specific latitude and longitude - */ - fun locationSet(ref: DeviceRef, latitude: Double, longitude: Double) { - deviceManager.locationSet(ref, latitude, longitude) - } - - /** - * Run a simulated location scenario (use the list action to get a list of scenarios) - */ - fun locationRunScenario(ref: DeviceRef, scenarioName: String) { - deviceManager.locationRunScenario(ref, scenarioName) - } - - /** - * Run a custom location scenario - */ - fun locationStartLocationSequence(ref: DeviceRef, speed: Int, distance: Int, interval: Int, waypoints: List) { - deviceManager.locationStartLocationSequence(ref, speed, distance, interval, waypoints) - } - - fun sendPushNotification(ref: DeviceRef, bundleId: String, notificationContent: ByteArray): EmptyMap { - deviceManager.sendPushNotification(ref, bundleId, notificationContent) - return happy - } - - fun sendPasteboard(ref: DeviceRef, payload: ByteArray): EmptyMap { - deviceManager.sendPasteboard(ref, payload) + fun setAccessToCameraAndThings(ref: DeviceRef, jsonContent: JsonNode): EmptyMap { + jsonContent.elements().forEach { deviceManager.approveAccess(ref, it["bundle_id"].textValue()) } return happy } fun setPermissions(ref: DeviceRef, json: JsonNode): EmptyMap { + if (json.isArray) { + return setAccessToCameraAndThings(ref, json) + } + val permissions = JsonMapper().fromJson(json) deviceManager.setPermissions(ref, permissions) @@ -118,8 +70,6 @@ class DevicesController(private val deviceManager: DeviceManager) { } } - fun listApps(ref: DeviceRef): List = deviceManager.listApps(ref) - fun crashLogs(ref: DeviceRef, appName: String?): List> { val logs = deviceManager.crashLogs(ref, appName) @@ -154,10 +104,6 @@ class DevicesController(private val deviceManager: DeviceManager) { return deviceManager.getVideo(ref) } - fun getVideoLog(ref: DeviceRef): String { - return deviceManager.getVideoLog(ref) - } - fun deleteVideo(ref: DeviceRef): EmptyMap { deviceManager.deleteVideo(ref) return happy @@ -167,10 +113,6 @@ class DevicesController(private val deviceManager: DeviceManager) { return deviceManager.listMedia(ref) } - fun listPhotoData(ref: DeviceRef): List { - return deviceManager.listPhotoData(ref) - } - fun resetMedia(ref: DeviceRef): EmptyMap { deviceManager.resetMedia(ref) @@ -182,41 +124,6 @@ class DevicesController(private val deviceManager: DeviceManager) { return happy } - fun syslog(ref: DeviceRef): File { - return deviceManager.syslog(ref) - } - - fun instrumentationAgentLog(ref: DeviceRef): File { - return deviceManager.instrumentationAgentLog(ref) - } - - fun deleteInstrumentationAgentLog(ref: DeviceRef) { - deviceManager.deleteInstrumentationAgentLog(ref) - } - - fun appiumServerLog(ref: DeviceRef): File { - return deviceManager.appiumServerLog(ref) - } - - fun deleteAppiumServerLog(ref: DeviceRef) { - deviceManager.deleteAppiumServerLog(ref) - } - - fun syslogDelete(ref: DeviceRef): EmptyMap { - deviceManager.syslogDelete(ref) - return happy - } - - fun syslogStart(ref: DeviceRef, sysLogCaptureOptions: SysLogCaptureOptions): EmptyMap { - deviceManager.syslogStart(ref, sysLogCaptureOptions) - return happy - } - - fun syslogStop(ref: DeviceRef): EmptyMap { - deviceManager.syslogStop(ref) - return happy - } - fun getDiagnostic(ref: DeviceRef, type: String, query: DiagnosticQuery): Diagnostic { val diagnosticType = DiagnosticType.fromString(type) return deviceManager.getDiagnostic(ref, diagnosticType, query) @@ -243,25 +150,11 @@ class DevicesController(private val deviceManager: DeviceManager) { return deviceManager.pullFile(ref, dataPath) } - fun pullFile(ref: DeviceRef, path: Path): ByteArray { - return deviceManager.pullFile(ref, path) - } - fun pushFile(ref: DeviceRef, fileName: String, data: ByteArray, bundleId: String): EmptyMap { deviceManager.pushFile(ref, fileName, data, bundleId) return happy } - fun pushFile(ref: DeviceRef, data: ByteArray, path: Path): EmptyMap { - deviceManager.pushFile(ref, data, path) - return happy - } - - fun deleteFile(ref: DeviceRef, path: Path): EmptyMap { - deviceManager.deleteFile(ref, path) - return happy - } - fun openUrl(ref: DeviceRef, url: String) { return deviceManager.openUrl(ref, url) } @@ -271,34 +164,11 @@ class DevicesController(private val deviceManager: DeviceManager) { return happy } - fun deleteAppData(ref: DeviceRef, bundleId: String): EmptyMap { - deviceManager.deleteAppData(ref, bundleId) - return happy - } - fun setEnvironmentVariables(ref: DeviceRef, environmentVariables: Map): EmptyMap { deviceManager.setEnvironmentVariables(ref, environmentVariables) return happy } - fun getEnvironmentVariable(ref: DeviceRef, variableName: String): String { - return deviceManager.getEnvironmentVariable(ref, variableName) - } - - fun deployApplication(appBundleDto: AppBundleDto): EmptyMap { - deviceManager.deployApplication(appBundleDto) - return happy - } - - fun installApplication(ref: String, appBundleDto: AppBundleDto): EmptyMap { - deviceManager.installApplication(ref, appBundleDto) - return happy - } - - fun appInstallationStatus(ref: String): Map { - return deviceManager.appInstallationStatus(ref) - } - fun updateApplicationPlist(deviceRef: String, plistEntry: PlistEntryDTO): Any { deviceManager.updateApplicationPlist(deviceRef, plistEntry) return happy diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/StatusController.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/StatusController.kt index 308a4e6d..077edf64 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/StatusController.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/StatusController.kt @@ -2,7 +2,6 @@ package com.badoo.automation.deviceserver.controllers import com.badoo.automation.deviceserver.host.management.DeviceManager import io.ktor.routing.Route -import java.util.concurrent.TimeUnit class StatusController(private val deviceManager: DeviceManager) { fun welcomeMessage(route: Route?): String { @@ -11,16 +10,12 @@ class StatusController(private val deviceManager: DeviceManager) { "Minimal /status, but /quitquitquit works\n" } - fun getServerStatus(serverStartTime: Long): Map { - val requestStartTime = System.nanoTime() + fun getServerStatus(): Map { val status = deviceManager.getStatus() - val uptime = TimeUnit.MINUTES.convert(System.nanoTime() - serverStartTime, TimeUnit.NANOSECONDS) return mapOf( - "status" to "ok", - "uptime" to "$uptime minutes", - "deviceManager" to status, - "elapsedTimeSeconds" to TimeUnit.SECONDS.convert(System.nanoTime() - requestStartTime, TimeUnit.NANOSECONDS) + "status" to "ok", + "deviceManager" to status ) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/AppBundleDto.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/AppBundleDto.kt deleted file mode 100644 index a2f408c6..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/AppBundleDto.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.badoo.automation.deviceserver.data - -import com.fasterxml.jackson.annotation.JsonProperty - -data class AppBundleDto( - @JsonProperty("app_url") - val appUrl: String, - - @JsonProperty("dsym_url") - val dsymUrl: String? -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as AppBundleDto - - if (appUrl != other.appUrl) return false - - return true - } - - override fun hashCode(): Int { - return appUrl.hashCode() - } -} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DesiredCapabilities.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DesiredCapabilities.kt index 37df9951..83d9d5af 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DesiredCapabilities.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DesiredCapabilities.kt @@ -6,39 +6,10 @@ data class DesiredCapabilities( val udid: String?, val model: String?, val os: String?, - val headless: Boolean = false, + val headless: Boolean = true, val existing: Boolean = true, val arch: String? = null, @JsonProperty("use_wda") - val useWda: Boolean = true, - - @JsonProperty("use_appium") - val useAppium: Boolean = false -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as DesiredCapabilities - - if (udid != other.udid) return false - if (model != other.model) return false - if (os != other.os) return false - if (arch != other.arch) return false - if (useWda != other.useWda) return false - if (useAppium != other.useAppium) return false - - return true - } - - override fun hashCode(): Int { - var result = udid?.hashCode() ?: 0 - result = 31 * result + (model?.hashCode() ?: 0) - result = 31 * result + (os?.hashCode() ?: 0) - result = 31 * result + (arch?.hashCode() ?: 0) - result = 31 * result + useWda.hashCode() - result = 31 * result + useAppium.hashCode() - return result - } -} + val useWda: Boolean = true +) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceAllocatedPorts.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceAllocatedPorts.kt index 785d53c2..b24985cf 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceAllocatedPorts.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceAllocatedPorts.kt @@ -5,9 +5,9 @@ data class DeviceAllocatedPorts( val wdaPort: Int, val calabashPort: Int, val mjpegServerPort: Int, - val appiumPort: Int + private val defaultCalabashPort: Int = 37265 ) { fun toSet(): Set { - return setOf(fbsimctlPort, wdaPort, calabashPort, mjpegServerPort, appiumPort) + return setOf(fbsimctlPort, wdaPort, calabashPort, mjpegServerPort, defaultCalabashPort) } -} \ No newline at end of file +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceDTO.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceDTO.kt index b1ffe5fc..d702270c 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceDTO.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceDTO.kt @@ -9,10 +9,8 @@ data class DeviceDTO( val fbsimctl_endpoint: URI, val wda_endpoint: URI, val calabash_port: Int, - val calabash_endpoint: URI, val mjpeg_server_port: Int, - val appium_port: Int, - val appium_endpoint: URI, + val user_ports: Set, // From PortAllocator val info: DeviceInfo, val last_error: ErrorDto?, val capabilities: ActualCapabilities? @@ -25,12 +23,6 @@ data class ActualCapabilities( @JsonProperty("terminate_app") val terminateApp: Boolean, - @JsonProperty("remote_notifications") - val remoteNotifications: Boolean, - - @JsonProperty("appium_enabled") - val isAppiumEnabled: Boolean, - @JsonProperty("video_capture") val videoCapture: Boolean -) \ No newline at end of file +) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceInfo.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceInfo.kt index 0db77e6a..5abbced0 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceInfo.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceInfo.kt @@ -14,19 +14,4 @@ data class DeviceInfo ( ) { constructor(device: FBSimctlDevice): this(device.udid, device.model, device.os, device.arch, device.name) - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as DeviceInfo - - if (udid != other.udid) return false - - return true - } - - fun osMajorVersion(): Int { - return os.substringAfter("iOS").trim().split(".").first().toInt() - } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DiagnosticType.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DiagnosticType.kt index 05f95ebf..d754a2b6 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DiagnosticType.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DiagnosticType.kt @@ -4,7 +4,8 @@ import com.fasterxml.jackson.annotation.JsonValue import java.lang.IllegalArgumentException enum class DiagnosticType(@JsonValue val value: String) { - OsLog("os_log"); + OsLog("os_log"), + SystemLog("system_log"); companion object { fun fromString(type: String): DiagnosticType { diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/ErrorDto.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/ErrorDto.kt index 17e34e31..b9443664 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/ErrorDto.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/ErrorDto.kt @@ -1,8 +1,5 @@ package com.badoo.automation.deviceserver.data -import com.badoo.automation.deviceserver.ios.simulator.data.DataContainerException -import java.lang.RuntimeException - data class ErrorDto( val type: String, val message: String?, diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/LocationDto.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/LocationDto.kt deleted file mode 100644 index 9d74d669..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/LocationDto.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.badoo.automation.deviceserver.data - -import com.fasterxml.jackson.annotation.JsonProperty - -data class LocationDto( - @JsonProperty("latitude") - val latitude: Double, - - @JsonProperty("longitude") - val longitude: Double -) - -data class LocationScenarioDto( - @JsonProperty("scenario_name") - val scenarioName: String -) - -data class LocationWaypointsDto( - @JsonProperty("speed") - val speed: Int = 0, - - @JsonProperty("distance") - val distance: Int = 0, - - @JsonProperty("interval") - val interval: Int = 0, - - @JsonProperty("waypoints") - val waypoints: List = listOf() -) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/MediaDto.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/MediaDto.kt index 0859b9b1..25612e80 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/MediaDto.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/MediaDto.kt @@ -4,5 +4,4 @@ import com.fasterxml.jackson.annotation.JsonProperty class MediaDto( @JsonProperty("media") - val media: List -) + val media: List) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/PasteboardDto.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/PasteboardDto.kt deleted file mode 100644 index 8e512af1..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/PasteboardDto.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.badoo.automation.deviceserver.data - -import com.fasterxml.jackson.annotation.JsonProperty - -data class PasteboardDto( - @JsonProperty("pasteboard_content") - val pasteboardСontent: ByteArray -) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/PermissionType.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/PermissionType.kt index c5ab7cfe..dec34863 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/PermissionType.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/PermissionType.kt @@ -2,29 +2,53 @@ package com.badoo.automation.deviceserver.data import com.fasterxml.jackson.annotation.JsonValue -@Suppress("unused") enum class PermissionType(@JsonValue val value: String) { - All("all"), + // region yes/no/unset permissions Calendar("calendar"), + Camera("camera"), - ContactsLimited("contacts-limited"), + Contacts("contacts"), - Location("location"), - LocationAlways("location-always"), - MediaLibrary("media-library"), + + Health("health"), + + HomeKit("homekit"), + + MediaLibrary("medialibrary"), + Microphone("microphone"), + Motion("motion"), + + Notifications("notifications"), + Photos("photos"), - PhotosAdd("photos-add"), + Reminders("reminders"), + Siri("siri"), + + Speech("speech"), + // endregion + + // region always/inuse/never/unset permissions + Location("location"), + // endregion } enum class PermissionAllowed(@JsonValue val value: String) { - Grant("grant"), - Revoke("revoke"), - Reset("reset"), + Yes("yes"), + + No("no"), + + Always("always"), + + Inuse("inuse"), + + Never("never"), + + Unset("unset"); } -class PermissionSet : HashMap() +class PermissionSet: HashMap() diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/PushNotificationDto.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/PushNotificationDto.kt deleted file mode 100644 index 319c05c0..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/PushNotificationDto.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.badoo.automation.deviceserver.data - -import com.fasterxml.jackson.annotation.JsonProperty - -data class PushNotificationDto( - @JsonProperty("bundle_id") - val bundleId: String, - - @JsonProperty("notification_content") - val notificationContent: ByteArray -) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SharedResourceDto.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SharedResourceDto.kt deleted file mode 100644 index b0ad5159..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SharedResourceDto.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.badoo.automation.deviceserver.data - -import com.fasterxml.jackson.annotation.JsonProperty - -data class SharedResourceDto( - @JsonProperty("data") - val data: ByteArray, - - @JsonProperty("path") - val path: String -) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SimulatorStatus.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SimulatorStatus.kt index aed2ad76..47b3be23 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SimulatorStatus.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SimulatorStatus.kt @@ -3,7 +3,6 @@ package com.badoo.automation.deviceserver.data data class SimulatorStatus( var wdaStatus: Boolean = false, var fbsimctlStatus: Boolean = false, - var appiumStatus: Boolean = false, @Volatile var wdaStatusRetries: Int = 0, @Volatile var fbsimctlStatusRetries: Int = 0 ) { diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SimulatorStatusDTO.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SimulatorStatusDTO.kt index e6193b6d..a030d58c 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SimulatorStatusDTO.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SimulatorStatusDTO.kt @@ -1,24 +1,11 @@ package com.badoo.automation.deviceserver.data -import com.badoo.automation.deviceserver.ios.simulator.Simulator - data class SimulatorStatusDTO ( val ready: Boolean, val wda_status: Boolean, - val appium_status: Boolean, val fbsimctl_status: Boolean, val state: String, - val last_error: ExceptionDTO?, - val simulator_services: Set = mutableSetOf() + val last_error: ExceptionDTO? ) -data class ExceptionDTO(val type: String, val message: String, val stackTrace: List) - -fun Exception.toDTO(): ExceptionDTO { - - return ExceptionDTO( - type = this.javaClass.name, - message = this.message ?: "", - stackTrace = stackTrace.map { it.toString() } - ) -} +data class ExceptionDTO(val type: String, val message: String, val stackTrace: List) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SysLogCaptureOptions.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SysLogCaptureOptions.kt deleted file mode 100644 index 31554365..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SysLogCaptureOptions.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.badoo.automation.deviceserver.data - -import com.fasterxml.jackson.annotation.JsonProperty - -class SysLogCaptureOptions { - @JsonProperty("predicate_string") - val predicateString: String = "" - - @JsonProperty("matching_processes") - val matchingProcesses: String = "" - - @JsonProperty("mute_kernel") - val shouldMuteKernel: Boolean = false - - @JsonProperty("mute_system_processes") - val shouldMuteSystemProcesses: Boolean = false -} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt index ea46f030..56f1b133 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt @@ -1,48 +1,35 @@ package com.badoo.automation.deviceserver.host -import com.badoo.automation.deviceserver.ApplicationConfiguration import com.badoo.automation.deviceserver.LogMarkers import com.badoo.automation.deviceserver.data.* -import com.badoo.automation.deviceserver.host.management.ApplicationBundle import com.badoo.automation.deviceserver.host.management.PortAllocator import com.badoo.automation.deviceserver.host.management.XcodeVersion -import com.badoo.automation.deviceserver.host.management.XcodeVersion.Companion.REQUIRED_XCODE_VERSION import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException import com.badoo.automation.deviceserver.ios.device.* -import com.badoo.automation.deviceserver.ios.device.diagnostic.RealDeviceSysLog -import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlAppInfo +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl import com.badoo.automation.deviceserver.ios.simulator.periodicTasksPool -import com.badoo.automation.deviceserver.util.AppInstaller -import com.badoo.automation.deviceserver.util.WdaDeviceBundle import com.badoo.automation.deviceserver.util.deviceRefFromUDID import net.logstash.logback.marker.MapEntriesAppendingMarker import org.slf4j.LoggerFactory -import org.slf4j.Marker import java.io.File import java.net.URL -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.StandardOpenOption import java.time.Duration -import java.util.* -import java.util.concurrent.* -import kotlin.system.measureNanoTime +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Future +import java.util.concurrent.TimeUnit class DevicesNode( private val remote: IRemote, override val publicHostName: String, portAllocator: PortAllocator = PortAllocator(), - configuredDevices: Set, + wdaRunnerXctest: File, + knownDevices: List, private val whitelistedApps: Set, private val uninstallApps: Boolean, - private val wdaDeviceBundles: List, - private val fbsimctlVersion: String, - private val appInstallerExecutorService: ExecutorService = Executors.newFixedThreadPool(4) -) : IDeviceNode { - override fun updateApplicationPlist(ref: DeviceRef, plistEntry: PlistEntryDTO) { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } - + private val wdaBundlePath: File, + private val remoteWdaBundleRoot: File, + private val fbsimctlVersion: String +) : ISimulatorsNode { private val logger = LoggerFactory.getLogger(javaClass.simpleName) private val logMarker = MapEntriesAppendingMarker( mapOf( @@ -50,63 +37,10 @@ class DevicesNode( ) ) - private val appBinariesCache: MutableMap = ConcurrentHashMap(200) - - override fun deployApplication(appBundle: ApplicationBundle) { - val appDirectory = if (remote.isLocalhost()) { - appBundle.appDirectory!! - } else { - copyAppToRemoteHost(appBundle) - } - val key = appBundle.appUrl.toExternalForm() - appBinariesCache[key] = appDirectory - } - - private fun copyAppToRemoteHost(appBundle: ApplicationBundle): File { - val marker = MapEntriesAppendingMarker(mapOf(LogMarkers.HOSTNAME to remote.publicHostName, "action_name" to "scp_application")) - logger.debug(marker, "Copying application ${appBundle.appUrl} to $this") - - remote.exec(listOf("/bin/rm", "-rf", ApplicationConfiguration().appBundleCacheRemotePath.absolutePath), mapOf(), false, 90).stdOut.trim() - - val remoteDirectory = File(ApplicationConfiguration().appBundleCacheRemotePath, UUID.randomUUID().toString()).absolutePath - remote.exec(listOf("/bin/mkdir", "-p", remoteDirectory), mapOf(), false, 90).stdOut.trim() - - val nanos = measureNanoTime { - remote.scpToRemoteHost(appBundle.appDirectory!!.absolutePath, remoteDirectory) - } - val seconds = TimeUnit.NANOSECONDS.toSeconds(nanos) - val measurement = mapOf(LogMarkers.HOSTNAME to remote.publicHostName, "action_name" to "scp_application", "duration" to seconds) - - logger.debug(MapEntriesAppendingMarker(measurement), "Successfully copied application ${appBundle.appUrl} to $this. Took $seconds seconds") - return File(remoteDirectory, appBundle.appDirectory!!.name) - } - - override fun installApplication(deviceRef: DeviceRef, appBundleDto: AppBundleDto) { - logger.info(logMarker, "Ready to install app ${appBundleDto.appUrl} on device $deviceRef") - var appBinaryPath: File = appBinariesCache[appBundleDto.appUrl] - ?: throw RuntimeException("Unable to find requested binary. Deploy binary first from url ${appBundleDto.appUrl}") - - if (appBinaryPath.absolutePath.endsWith("/Payload")) { - appBinaryPath = File("${appBinaryPath.parentFile.absolutePath}.ipa") - } - - val device: Device = slotByExternalRef(deviceRef).device - - device.installApplication(appInstaller, appBundleDto.appUrl, appBinaryPath) - } - - private val commonLogMarkerDetails = mapOf( - LogMarkers.HOSTNAME to remote.hostName - ) + private val isWebDriverAgentDeployed = remote.execIgnoringErrors(listOf("test", "-d", remoteWdaBundleRoot.absolutePath)).isSuccess - private fun logMarkerDetails(udid: UDID): Map { - return commonLogMarkerDetails + mapOf( - LogMarkers.DEVICE_REF to deviceRefFromUDID( - udid, - remote.publicHostName - ), LogMarkers.UDID to udid - ) - } + override val isNodePrepared: Boolean + get() = remote.isLocalhost() || isWebDriverAgentDeployed private val deviceRegistrationInterval = Duration.ofMinutes(1) @@ -118,70 +52,27 @@ class DevicesNode( throw(NotImplementedError("Listing media is not supported by physical devices")) } - override fun listPhotoData(deviceRef: DeviceRef) : List { - throw(NotImplementedError("Listing PhotoData is not supported by physical devices")) - } - override fun addMedia(deviceRef: DeviceRef, fileName: String, data: ByteArray) { throw(NotImplementedError("Adding media is not supported by physical devices")) } - override fun instrumentationAgentLog(deviceRef: DeviceRef): File { - return slotByExternalRef(deviceRef).device.instrumentationAgentLog - } - - override fun deleteInstrumentationAgentLog(deviceRef: DeviceRef) { - val logFile = slotByExternalRef(deviceRef).device.instrumentationAgentLog - Files.write(logFile.toPath(), ByteArray(0), StandardOpenOption.TRUNCATE_EXISTING); + override fun getDiagnostic(deviceRef: DeviceRef, type: DiagnosticType, query: DiagnosticQuery): Diagnostic { + throw(NotImplementedError("Diagnostic is not supported by physical devices")) } - override fun appiumServerLog(deviceRef: DeviceRef): File { - return slotByExternalRef(deviceRef).device.appiumServerLog - } - - override fun deleteAppiumServerLog(deviceRef: DeviceRef) { - slotByExternalRef(deviceRef).device.deleteAppiumServerLog() - } - - override fun syslog(deviceRef: DeviceRef): File { - val device = slotByExternalRef(deviceRef).device - val osLog: RealDeviceSysLog = device.osLog - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + override fun resetDiagnostic(deviceRef: DeviceRef, type: DiagnosticType) { + throw(NotImplementedError("Diagnostic is not supported by physical devices")) } - override fun syslogStart(deviceRef: DeviceRef, sysLogCaptureOptions: SysLogCaptureOptions) { - val device: Device = slotByExternalRef(deviceRef).device - device.osLog.startWritingLog(sysLogCaptureOptions) - } - - override fun syslogStop(deviceRef: DeviceRef) { - val device = slotByExternalRef(deviceRef).device - device.osLog.stopWritingLog() - } - - override fun syslogDelete(deviceRef: DeviceRef) { - val device = slotByExternalRef(deviceRef).device - device.osLog.deleteLogFiles() - } - - override fun getDeviceFor(deviceRef: DeviceRef): Device = slotByExternalRef(deviceRef).device - override fun pushFile(ref: DeviceRef, fileName: String, data: ByteArray, bundleId: String) { - throw(NotImplementedError("Push files is not supported by physical devices")) - } - - override fun pushFile(ref: DeviceRef, data: ByteArray, path: Path) { - throw(NotImplementedError("Push files is not supported by physical devices")) - } - - override fun deleteFile(ref: DeviceRef, path: Path) { - throw(NotImplementedError("Delete file is not supported by physical devices")) + throw(NotImplementedError("Push file is not supported by physical devices")) } override val remoteAddress: String get() = remote.hostName private val deviceInfoProvider = DeviceInfoProvider(remote) - private val slots: DeviceSlots = DeviceSlots(remote, wdaDeviceBundles, portAllocator, deviceInfoProvider, configuredDevices) + private val slots: DeviceSlots = + DeviceSlots(remote, wdaRunnerXctest, portAllocator, deviceInfoProvider, knownDevices) private var deviceRegistrar: Future? = null @@ -201,12 +92,8 @@ class DevicesNode( throw(NotImplementedError("Reset is not supported by physical devices")) } - override fun sendPushNotification(deviceRef: DeviceRef, bundleId: String, notificationContent: ByteArray) { - throw(NotImplementedError("Simulating push notifications is not supported by physical devices")) - } - - override fun sendPasteboard(deviceRef: DeviceRef, payload: ByteArray) { - throw(NotImplementedError("Set pasteboard is not supported by physical devices")) + override fun approveAccess(deviceRef: DeviceRef, bundleId: String) { + throw(NotImplementedError("Approve Access is not supported by physical devices")) } override fun setPermissions(deviceRef: DeviceRef, appPermissions: AppPermissionsDto) { @@ -242,21 +129,24 @@ class DevicesNode( val device = slotByExternalRef(deviceRef).device val status = device.status() - return status -// return SimulatorStatusDTO( -// ready = status.ready, -// wda_status = status.wda_status, -// appium_status = status.appium_status, -// fbsimctl_status = status.fbsimctl_status, -// state = status.state, -// last_error = status.last_error -// ) + return SimulatorStatusDTO( + ready = status.ready, + wda_status = status.wda_status, + fbsimctl_status = status.fbsimctl_status, + state = status.state, + last_error = if (status.last_error == null) null else exceptionToDto(status.last_error) + ) } override fun isReachable(): Boolean { return remote.isReachable() } + override fun count(): Int { + // FIXME: Remove from common interface, it might be needed for simulators node only + throw NotImplementedError("An operation is not implemented") + } + override fun deleteRelease(deviceRef: DeviceRef, reason: String): Boolean { synchronized(this) { slotByExternalRef(deviceRef).release() @@ -266,13 +156,9 @@ class DevicesNode( } } - override fun deleteDevice(deviceRef: DeviceRef, reason: String): Boolean { - return deleteRelease(deviceRef, reason) - } - override fun getDeviceDTO(deviceRef: DeviceRef): DeviceDTO { val device = slotByExternalRef(deviceRef).device - return deviceToDto(device) + return deviceToDto(deviceRef, device) } override fun totalCapacity(desiredCaps: DesiredCapabilities): Int { @@ -296,22 +182,26 @@ class DevicesNode( } override fun createDeviceAsync(desiredCaps: DesiredCapabilities): DeviceDTO { - lateinit var slot: DeviceSlot + var slot: DeviceSlot? = null + var ref: DeviceRef? = null synchronized(this) { slot = slots.reserve(desiredCaps) - activeRefs[slot.device.ref] = slot.device.udid + ref = deviceRefFromUDID(slot!!.udid, remote.publicHostName) + + activeRefs[ref!!] = slot!!.udid } - slot.device.renewAsync(whitelistedApps = whitelistedApps, uninstallApps = uninstallApps, desiredCaps = desiredCaps) - return deviceToDto(slot.device) + slot!!.device.renewAsync(whitelistedApps = whitelistedApps, uninstallApps = uninstallApps) + + return deviceToDto(ref!!, device = slot!!.device) } override fun prepareNode() { logger.info(logMarker, "Preparing node ${remote.hostName}") checkPrerequisites() if (!remote.isLocalhost()) { - copyWdaBundlesToHost() + copyWdaBundleToHost() } cleanup() @@ -324,7 +214,7 @@ class DevicesNode( try { slots.registerDevices() } catch (e: Exception) { - logger.error(logMarker, "Failed to register devices: $e") + logger.warn(logMarker, "Failed to register devices: $e") } }, deviceRegistrationInterval.toMillis(), @@ -347,10 +237,6 @@ class DevicesNode( logger.info(logMarker, "Finalized node $this") } - override fun reboot() { - return // Not intended to reboot Real Device nodes - } - override fun list(): List { synchronized(this) { val disconnected = mutableListOf() @@ -361,7 +247,7 @@ class DevicesNode( disconnected.add(ref) return@map null } else { - return@map deviceToDto(slot.device) + return@map deviceToDto(ref, slot.device) } } @@ -376,39 +262,7 @@ class DevicesNode( override fun lastCrashLog(deviceRef: DeviceRef): CrashLog { val device = slotByExternalRef(deviceRef).device - return device.lastCrashLog() - } - - override fun listApps(deviceRef: DeviceRef): List = slotByExternalRef(deviceRef).device.listApps() - - override fun locationListScenarios(deviceRef: DeviceRef): List { - throw(NotImplementedError("Location commands are not supported by physical devices")) - } - - override fun locationClear(deviceRef: DeviceRef) { - throw(NotImplementedError("Location commands are not supported by physical devices")) - } - - override fun locationSet(deviceRef: DeviceRef, latitude: Double, longitude: Double) { - throw(NotImplementedError("Location commands are not supported by physical devices")) - } - - override fun locationRunScenario(deviceRef: DeviceRef, scenarioName: String) { - throw(NotImplementedError("Location commands are not supported by physical devices")) - } - - override fun getNodeInfo(): NodeInfo { - return NodeInfo.getNodeInfo(remote) - } - - override fun locationStartLocationSequence( - deviceRef: DeviceRef, - speed: Int, - distance: Int, - interval: Int, - waypoints: List - ) { - throw(NotImplementedError("Location commands are not supported by physical devices")) + return device.lastCrashLog() ?: CrashLog("", "") } override fun crashLogs(deviceRef: DeviceRef, pastMinutes: Long?): List { @@ -432,10 +286,6 @@ class DevicesNode( return slotByExternalRef(deviceRef).device.videoRecorder.getRecording() } - override fun videoRecordingLogGet(deviceRef: DeviceRef): String { - return slotByExternalRef(deviceRef).device.videoRecorder.getRecordingLog() - } - override fun videoRecordingStart(deviceRef: DeviceRef) { slotByExternalRef(deviceRef).device.videoRecorder.start() } @@ -447,39 +297,28 @@ class DevicesNode( override fun listFiles(deviceRef: DeviceRef, dataPath: DataPath): List = throw(NotImplementedError()) override fun pullFile(deviceRef: DeviceRef, dataPath: DataPath): ByteArray = throw(NotImplementedError()) - - override fun pullFile(deviceRef: DeviceRef, path: Path): ByteArray = throw(NotImplementedError()) - // endregion - private val appInstaller: AppInstaller = AppInstaller(remote) - override fun uninstallApplication(deviceRef: DeviceRef, bundleId: String) { val device = slotByExternalRef(deviceRef).device - device.uninstallApplication(bundleId, appInstaller) + device.uninstallApplication(bundleId) } - override fun deleteAppData(deviceRef: DeviceRef, bundleId: String) = throw(NotImplementedError()) - - private fun deviceToDto(device: Device): DeviceDTO { + private fun deviceToDto(deviceRef: DeviceRef, device: Device): DeviceDTO { return DeviceDTO( - ref = device.ref, + ref = deviceRef, state = device.deviceState, fbsimctl_endpoint = device.fbsimctlEndpoint, wda_endpoint = device.wdaEndpoint, calabash_port = device.calabashPort, - calabash_endpoint = device.calabashEndpoint, mjpeg_server_port = device.mjpegServerPort, - appium_port = device.appiumPort, - appium_endpoint = device.appiumEndpoint, + user_ports = emptySet(), info = device.deviceInfo, last_error = device.lastException?.toDto(), capabilities = ActualCapabilities( setLocation = false, terminateApp = false, - remoteNotifications = true, - isAppiumEnabled = device.isAppiumEnabled, - videoCapture = true + videoCapture = false ) ) } @@ -492,14 +331,23 @@ class DevicesNode( private fun checkPrerequisites() { val xcodeOutput = remote.execIgnoringErrors(listOf("xcodebuild", "-version")) - logger.info(logMarker, "Using default Xcode version: ${xcodeOutput.stdOut.trim().replace("\n", " ")}") val xcodeVersion = XcodeVersion.fromXcodeBuildOutput(xcodeOutput.stdOut) - if (xcodeVersion < REQUIRED_XCODE_VERSION) { - logger.error(logMarker, "Expecting Xcode $REQUIRED_XCODE_VERSION or higher, but it is $xcodeVersion") + if (xcodeVersion < XcodeVersion(9, 2)) { + throw RuntimeException("Expecting Xcode 9.2 or higher, but it is $xcodeVersion") + } + + // temp solution, prerequisites should be satisfied without having to switch anything + val switchRes = remote.execIgnoringErrors( + listOf("/usr/local/bin/brew", "switch", "fbsimctl", fbsimctlVersion), + env = mapOf("RUBYOPT" to "") + ) + + if (!switchRes.isSuccess) { + logger.warn(logMarker, "fbsimctl switch failed, see: $switchRes") } - val fbsimctlPath = remote.execIgnoringErrors(listOf("readlink", remote.fbsimctl.fbsimctlBinary)).stdOut + val fbsimctlPath = remote.execIgnoringErrors(listOf("readlink", FBSimctl.FBSIMCTL_BIN)).stdOut val match = Regex("/fbsimctl/([-.\\w]+)/bin/fbsimctl").find(fbsimctlPath) ?: throw RuntimeException("Could not read fbsimctl version from $fbsimctlPath") @@ -508,43 +356,37 @@ class DevicesNode( throw RuntimeException("Expecting fbsimctl $fbsimctlVersion, but it was $actualFbsimctlVersion ${match.groupValues}") } - val iproxyResult = remote.execIgnoringErrors((listOf(File(remote.homeBrewPath, "iproxy").absolutePath, "--help"))) - if (!iproxyResult.isSuccess) { - throw RuntimeException("Expecting iproxy to be installed. Exit code: ${iproxyResult.exitCode}\nStdErr: ${iproxyResult.stdErr}. StdOut: ${iproxyResult.stdOut}") + val iproxy = remote.execIgnoringErrors((listOf(UsbProxy.IPROXY_BIN))) + if (iproxy.exitCode != 0) { + throw RuntimeException("Expecting iproxy to be installed") } - val socatResult = remote.execIgnoringErrors((listOf(File(remote.homeBrewPath, "socat").absolutePath, "-V"))) - if (!socatResult.isSuccess) { - throw RuntimeException("Expecting socat to be installed. Exit code: ${socatResult.exitCode}\nStdErr: ${socatResult.stdErr}. StdOut: ${socatResult.stdOut}") + val socat = remote.execIgnoringErrors((listOf(UsbProxy.SOCAT_BIN, "-V"))) + if (socat.exitCode != 0) { + throw RuntimeException("Expecting socat to be installed") } } - private fun copyWdaBundlesToHost() { + private fun copyWdaBundleToHost() { logger.debug(logMarker, "Setting up remote node: copying WebDriverAgent to node ${remote.hostName}") - val remoteWdaBundleRoot = wdaDeviceBundles.first().bundlePath(remote.isLocalhost()).absolutePath - remote.rm(remoteWdaBundleRoot) - remote.execIgnoringErrors(listOf("/bin/mkdir", "-p", remoteWdaBundleRoot)) - wdaDeviceBundles.forEach { - remote.scpToRemoteHost(it.bundlePath(true).absolutePath, remoteWdaBundleRoot) - } + remote.rsync( + wdaBundlePath.absolutePath, + remoteWdaBundleRoot.absolutePath, + setOf("-r", "--delete") + ) } private fun cleanup() { // single instance of server on node is implied, so we can kill all simulators and fbsimctl processes - remote.pkill(remote.fbsimctl.fbsimctlBinary, true) - remote.pkill("/usl/local/bin/iproxy", true) - remote.pkill("/opt/homebrew/bin/iproxy", true) - remote.pkill("/usr/local/bin/socat", true) - remote.pkill("/opt/homebrew/bin/socat", true) - remote.pkill("appium_tmpdir_", true) + remote.execIgnoringErrors(listOf("pkill", "-9", "/usr/local/bin/fbsimctl")) } override fun setEnvironmentVariables(deviceRef: DeviceRef, envs: Map) { throw(NotImplementedError("Setting environment variables is not supported by physical devices")) } - override fun getEnvironmentVariable(deviceRef: DeviceRef, variableName: String): String { - throw(NotImplementedError("Getting environment variables is not supported by physical devices")) + override fun updateApplicationPlist(ref: DeviceRef, plistEntry: PlistEntryDTO) { + throw(NotImplementedError("Updating application plist is not supported by physical devices")) } override fun equals(other: Any?): Boolean { @@ -558,9 +400,6 @@ class DevicesNode( return true } - override fun appInstallationStatus(deviceRef: DeviceRef): Map { - return slotByExternalRef(deviceRef).device.appInstallationStatus() - } override fun hashCode(): Int { return publicHostName.hashCode() } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt index 1f401b5f..3c7d1f2d 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt @@ -1,51 +1,26 @@ package com.badoo.automation.deviceserver.host -import com.badoo.automation.deviceserver.ApplicationConfiguration import com.badoo.automation.deviceserver.NodeConfig import com.badoo.automation.deviceserver.host.management.IHostFactory import com.badoo.automation.deviceserver.host.management.SimulatorHostChecker -import com.badoo.automation.deviceserver.util.WdaDeviceBundle -import com.badoo.automation.deviceserver.util.WdaDeviceBundlesProvider -import com.badoo.automation.deviceserver.util.WdaSimulatorBundles -import com.badoo.automation.deviceserver.util.WdaSimulatorBundlesProvider import org.slf4j.LoggerFactory import java.io.File -import java.nio.file.Paths class HostFactory( - private val remoteProvider: (hostName: String, userName: String, publicHost: String) -> IRemote = { hostName, userName, publicHostName -> - Remote( - hostName, - userName, - publicHostName - ) - }, - private val fbsimctlVersion: String, - private val remoteTestHelperAppRoot: File, - private val remoteVideoRecorder: File, - private val appConfiguration: ApplicationConfiguration + private val remoteProvider: (hostName: String, userName: String, publicHost: String) -> IRemote = { hostName, userName, publicHostName -> Remote(hostName, userName, publicHostName) }, + private val wdaSimulatorBundle: File, + private val remoteWdaSimulatorBundleRoot: File, + private val wdaDeviceBundle: File, + private val remoteWdaDeviceBundleRoot: File, + private val fbsimctlVersion: String ) : IHostFactory { - private val logger = LoggerFactory.getLogger(javaClass.simpleName) - - fun getWdaDeviceBundles(appConfiguration: ApplicationConfiguration): List { - val wdaDeviceBundles: List = WdaDeviceBundlesProvider( - Paths.get(appConfiguration.wdaDeviceBundles), - Paths.get(appConfiguration.remoteWdaDeviceBundleRoot) - ).getWdaDeviceBundles() - - return wdaDeviceBundles + companion object { + val WDA_XCTEST = File("PlugIns/WebDriverAgentRunner.xctest") } - fun getWdaSimulatorBundles(appConfiguration: ApplicationConfiguration): WdaSimulatorBundles { - val wdaSimulatorBundles: WdaSimulatorBundles = WdaSimulatorBundlesProvider( - Paths.get(appConfiguration.wdaSimulatorBundles), - Paths.get(appConfiguration.remoteWdaSimulatorBundleRoot) - ).getWdaSimulatorBundles() - - return wdaSimulatorBundles - } + private val logger = LoggerFactory.getLogger(javaClass.simpleName) - override fun getHostFromConfig(config: NodeConfig): IDeviceNode { + override fun getHostFromConfig(config: NodeConfig): ISimulatorsNode { logger.info("Trying to start node $config.") val hostName = config.host @@ -57,41 +32,45 @@ class HostFactory( throw RuntimeException("Config for non-localhost nodes must have non-empty 'user'. Current config: $config") } - val nodeTypeResult = remote.exec(listOf("/usr/bin/arch"), mapOf(),true, 60) - if (nodeTypeResult.isSuccess) { - logger.info("ARCH: The executor arch is ${nodeTypeResult.stdOut} for node $publicHostName") - } else { - logger.error("ARCH: Failed to determine executor type for node $publicHostName. (maybe it's Linux). ${nodeTypeResult.stdErr}") - } - return if (config.type == NodeConfig.NodeType.Simulators) { - val wdaSimulatorBundles = getWdaSimulatorBundles(appConfiguration) - val hostChecker = SimulatorHostChecker( - remote, - wdaSimulatorBundles = wdaSimulatorBundles, - remoteTestHelperAppRoot = remoteTestHelperAppRoot, - remoteVideoRecorder = remoteVideoRecorder, - fbsimctlVersion = fbsimctlVersion, - shutdownSimulators = config.shutdownSimulators - ) SimulatorsNode( remote = remote, publicHostName = publicHostName, - hostChecker = hostChecker, + hostChecker = SimulatorHostChecker( + remote, + wdaBundle = wdaSimulatorBundle, + remoteWdaBundleRoot = remoteWdaSimulatorBundleRoot, + fbsimctlVersion = fbsimctlVersion, + shutdownSimulators = config.shutdownSimulators + ), simulatorLimit = config.simulatorLimit, concurrentBoots = config.concurrentBoots, - wdaSimulatorBundles = wdaSimulatorBundles + wdaRunnerXctest = getWdaRunnerXctest(remote.isLocalhost(), wdaSimulatorBundle, remoteWdaSimulatorBundleRoot) ) } else { DevicesNode( remote = remote, publicHostName = publicHostName, whitelistedApps = config.whitelistApps, - configuredDevices = config.configuredDevices, + knownDevices = config.knownDevices, uninstallApps = config.uninstallApps, - wdaDeviceBundles = getWdaDeviceBundles(appConfiguration), + wdaBundlePath = wdaDeviceBundle, + remoteWdaBundleRoot = remoteWdaDeviceBundleRoot, + wdaRunnerXctest = getWdaRunnerXctest(remote.isLocalhost(), wdaDeviceBundle, remoteWdaDeviceBundleRoot), fbsimctlVersion = fbsimctlVersion ) } } + + private fun getWdaRunnerXctest(isLocalHost: Boolean, wdaBundle: File, remoteWdaBundleRoot: File): File { + val wdaRunnerXctest = File(wdaBundle.name, WDA_XCTEST.path).path + + val wdaBundleRoot = if (isLocalHost) { + wdaBundle.parentFile + } else { + remoteWdaBundleRoot + } + + return File(wdaBundleRoot, wdaRunnerXctest) + } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IDeviceNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IDeviceNode.kt deleted file mode 100644 index 442904eb..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IDeviceNode.kt +++ /dev/null @@ -1,130 +0,0 @@ -package com.badoo.automation.deviceserver.host - -import com.badoo.automation.deviceserver.data.* -import com.badoo.automation.deviceserver.host.management.ApplicationBundle -import com.badoo.automation.deviceserver.ios.IDevice -import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlAppInfo -import java.io.File -import java.net.URL -import java.nio.file.Path -import java.util.regex.Pattern - -interface IDeviceNode { - fun supports(desiredCaps: DesiredCapabilities): Boolean - - fun resetAsync(deviceRef: DeviceRef) - - fun sendPushNotification(deviceRef: DeviceRef, bundleId: String, notificationContent: ByteArray) - fun sendPasteboard(deviceRef: DeviceRef, payload: ByteArray) - fun setPermissions(deviceRef: DeviceRef, appPermissions: AppPermissionsDto) - fun clearSafariCookies(deviceRef: DeviceRef) - fun shake(deviceRef: DeviceRef) - fun openUrl(deviceRef: DeviceRef, url: String) - fun endpointFor(deviceRef: DeviceRef, port: Int): URL - fun lastCrashLog(deviceRef: DeviceRef): CrashLog - fun crashLogs(deviceRef: DeviceRef, pastMinutes: Long?): List - fun crashLogs(deviceRef: DeviceRef, appName: String?): List - fun deleteCrashLogs(deviceRef: DeviceRef): Boolean - fun state(deviceRef: DeviceRef): SimulatorStatusDTO - - fun videoRecordingDelete(deviceRef: DeviceRef) - fun videoRecordingGet(deviceRef: DeviceRef): ByteArray - fun videoRecordingStart(deviceRef: DeviceRef) - fun videoRecordingStop(deviceRef: DeviceRef) - fun videoRecordingLogGet(deviceRef: DeviceRef): String - - fun listFiles(deviceRef: DeviceRef, dataPath: DataPath): List - fun pullFile(deviceRef: DeviceRef, dataPath: DataPath): ByteArray - fun pullFile(deviceRef: DeviceRef, path: Path): ByteArray - - fun addMedia(deviceRef: DeviceRef, fileName: String, data: ByteArray) - fun syslog(deviceRef: DeviceRef): File - fun instrumentationAgentLog(deviceRef: DeviceRef): File - fun deleteInstrumentationAgentLog(deviceRef: DeviceRef) - fun appiumServerLog(deviceRef: DeviceRef): File - fun deleteAppiumServerLog(deviceRef: DeviceRef) - fun syslogStart(deviceRef: DeviceRef, sysLogCaptureOptions: SysLogCaptureOptions) - fun syslogStop(deviceRef: DeviceRef) - fun syslogDelete(deviceRef: DeviceRef) - fun resetMedia(deviceRef: DeviceRef) - fun listMedia(deviceRef: DeviceRef): List - fun listPhotoData(deviceRef: DeviceRef): List - - fun getDeviceFor(deviceRef: DeviceRef): IDevice - - fun getDiagnostic(deviceRef: DeviceRef, type: DiagnosticType, query: DiagnosticQuery): Diagnostic { - return when (type) { - DiagnosticType.OsLog -> Diagnostic( - type = type, - content = getDeviceFor(deviceRef).osLog.content(query.process) - ) - } - } - - fun resetDiagnostic(deviceRef: DeviceRef, type: DiagnosticType) { - when (type) { - DiagnosticType.OsLog -> getDeviceFor(deviceRef).osLog.truncate() - } - } - - val remoteAddress: String - fun isReachable(): Boolean - fun prepareNode() - fun list(): List - fun deleteRelease(deviceRef: DeviceRef, reason: String): Boolean - fun deleteDevice(deviceRef: DeviceRef, reason: String): Boolean - fun getDeviceDTO(deviceRef: DeviceRef): DeviceDTO - fun totalCapacity(desiredCaps: DesiredCapabilities): Int - fun capacityRemaining(desiredCaps: DesiredCapabilities): Float - fun createDeviceAsync(desiredCaps: DesiredCapabilities): DeviceDTO - fun dispose() - fun reboot() - fun uninstallApplication(deviceRef: DeviceRef, bundleId: String) - fun deleteAppData(deviceRef: DeviceRef, bundleId: String) - fun setEnvironmentVariables(deviceRef: DeviceRef, envs: Map) - fun getEnvironmentVariable(deviceRef: DeviceRef, variableName: String): String - fun pushFile(ref: DeviceRef, fileName: String, data: ByteArray, bundleId: String) - fun pushFile(ref: DeviceRef, data: ByteArray, path: Path) - fun deleteFile(ref: DeviceRef, path: Path) - fun installApplication(deviceRef: DeviceRef, appBundleDto: AppBundleDto) - fun appInstallationStatus(deviceRef: DeviceRef): Map - fun updateApplicationPlist(ref: DeviceRef, plistEntry: PlistEntryDTO) - val publicHostName: String - fun deployApplication(appBundle: ApplicationBundle) - fun listApps(deviceRef: DeviceRef): List - fun locationListScenarios(deviceRef: DeviceRef): List - fun locationClear(deviceRef: DeviceRef) - fun locationSet(deviceRef: DeviceRef, latitude: Double, longitude: Double) - fun locationRunScenario(deviceRef: DeviceRef, scenarioName: String) - fun locationStartLocationSequence(deviceRef: DeviceRef, speed: Int, distance: Int, interval: Int, waypoints: List) - fun getNodeInfo(): NodeInfo -} - -data class NodeInfo( - val currentDate: String, - val uptime: String, - val bootTime: Long, - val info: List -) { - companion object { - private val bootTimeSplitPattern = Pattern.compile("[ ,]") - val numberRegex = Regex("\\d+") - - fun getNodeInfo(remote: IRemote): NodeInfo { - val uptimeInfo = remote.shell("/bin/date ; /usr/bin/uptime ; /usr/sbin/sysctl -n kern.boottime") - .stdOut.trim().lines() - val currentDate = uptimeInfo[0] - val uptime = uptimeInfo[1] - val bootTime = uptimeInfo[2].split(bootTimeSplitPattern).first { it.matches(numberRegex) }.toLong() - - val nodeInfo = remote.shell("/usr/sbin/system_profiler SPHardwareDataType").stdOut.trim().lines() - - return NodeInfo( - currentDate = currentDate, - uptime = uptime, - bootTime = bootTime, - info = nodeInfo - ) - } - } -} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt index e61f780e..2d026038 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt @@ -1,6 +1,5 @@ package com.badoo.automation.deviceserver.host -import XCRunSimctl import com.badoo.automation.deviceserver.command.CommandResult import com.badoo.automation.deviceserver.command.IShellCommand import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl @@ -18,14 +17,8 @@ interface IRemote { return hostName == LOCALHOST || hostName.startsWith(LOCALHOST_NET_PREFIX) } - - const val SSH_AUTH_SOCK = "SSH_AUTH_SOCK" - private val asdfUserPath = File(System.getProperty("user.home"), ".asdf/shims").absolutePath - val DEFAULT_PATH = "$asdfUserPath:/Users/qa/.asdf/shims:/usr/local/opt/appium/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" } - val homeBrewPath: File - val tmpDir: File val hostName: String val userName: String val publicHostName: String @@ -34,17 +27,17 @@ interface IRemote { fun isReachable(): Boolean fun isLocalhost(): Boolean = isLocalhost(hostName, userName) - fun execIgnoringErrors(command: List, env: Map = emptyMap(), timeOutSeconds: Long = 60): CommandResult + fun execIgnoringErrors(command: List, env: Map = emptyMap(), timeOutSeconds: Long = 30): CommandResult = exec(command, env, returnFailure = true, timeOutSeconds = timeOutSeconds) fun exec(command: List, env: Map, returnFailure: Boolean, timeOutSeconds: Long): CommandResult - fun shell(command: String, returnOnFailure: Boolean = true, environment: Map = emptyMap()) : CommandResult + fun shell(command: String, returnOnFailure: Boolean = true) : CommandResult fun escape(value: String) : String /** - * Returns [CommandResult] file contents + * Returns [ByteArray] file contents * //FIXME: should be a better way of streaming a file over HTTP. without caching bytes in server's memory. Investigate ByteReadChannel */ fun captureFile(file: File): ByteArray @@ -57,9 +50,9 @@ interface IRemote { * @return Set> parsed JSON */ val fbsimctl: FBSimctl - val xcrunSimctl: XCRunSimctl fun isDirectory(path: String): Boolean - fun scpToRemoteHost(from: String, to: String, timeOut: Duration = Duration.ofMinutes(3)) - fun rm(path: String, timeOut: Duration = Duration.ofMinutes(3)) + fun rsync(from: String, to: String, flags: Set) + fun scpToRemoteHost(from: String, to: String, timeOut: Duration) + fun rm(path: String, timeOut: Duration) fun scpFromRemoteHost(from: String, to: String, timeOut: Duration) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorFactory.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorFactory.kt index 21e07d49..e0f811ee 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorFactory.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorFactory.kt @@ -6,22 +6,23 @@ import com.badoo.automation.deviceserver.data.DeviceRef import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDevice import com.badoo.automation.deviceserver.ios.simulator.ISimulator import com.badoo.automation.deviceserver.ios.simulator.Simulator -import com.badoo.automation.deviceserver.util.WdaSimulatorBundles -import java.util.concurrent.ExecutorService +import kotlinx.coroutines.experimental.ThreadPoolDispatcher +import java.io.File interface ISimulatorFactory { fun newSimulator( - ref: DeviceRef, - remote: IRemote, - fbdev: FBSimctlDevice, - ports: DeviceAllocatedPorts, - deviceSetPath: String, - wdaSimulatorBundles: WdaSimulatorBundles, - concurrentBoot: ExecutorService, - headless: Boolean, - useWda: Boolean, - useAppium: Boolean + ref: DeviceRef, + remote: IRemote, + fbdev: FBSimctlDevice, + ports: DeviceAllocatedPorts, + deviceSetPath: String, + wdaRunnerXctest: File, + concurrentBoot: ThreadPoolDispatcher, + headless: Boolean, + useWda: Boolean, + fbsimctlSubject: String ): ISimulator { - return Simulator(ref, remote, DeviceInfo(fbdev), ports, deviceSetPath, wdaSimulatorBundles, concurrentBoot, headless, useWda, useAppium) + return Simulator(ref, remote, DeviceInfo(fbdev), ports, deviceSetPath, wdaRunnerXctest, concurrentBoot, + headless, useWda, fbsimctlSubject) } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorProvider.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorProvider.kt new file mode 100644 index 00000000..f532723c --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorProvider.kt @@ -0,0 +1,11 @@ +package com.badoo.automation.deviceserver.host + +import com.badoo.automation.deviceserver.data.DesiredCapabilities +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDevice + +interface ISimulatorProvider { + fun findBy(udid: String): FBSimctlDevice? + fun list(): List + fun create(model: String?, os: String?, transitional: Boolean): FBSimctlDevice + fun match(desiredCaps: DesiredCapabilities, usedUdids: Set): FBSimctlDevice? +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt new file mode 100644 index 00000000..e95effc9 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt @@ -0,0 +1,55 @@ +package com.badoo.automation.deviceserver.host + +import com.badoo.automation.deviceserver.data.* +import java.net.URL + +interface ISimulatorsNode { + fun supports(desiredCaps: DesiredCapabilities): Boolean + + fun resetAsync(deviceRef: DeviceRef) + + fun approveAccess(deviceRef: DeviceRef, bundleId: String) + fun setPermissions(deviceRef: DeviceRef, appPermissions: AppPermissionsDto) + fun clearSafariCookies(deviceRef: DeviceRef) + fun shake(deviceRef: DeviceRef) + fun openUrl(deviceRef: DeviceRef, url: String) + fun endpointFor(deviceRef: DeviceRef, port: Int): URL + fun lastCrashLog(deviceRef: DeviceRef): CrashLog + fun crashLogs(deviceRef: DeviceRef, pastMinutes: Long?): List + fun crashLogs(deviceRef: DeviceRef, appName: String?): List + fun deleteCrashLogs(deviceRef: DeviceRef): Boolean + fun state(deviceRef: DeviceRef): SimulatorStatusDTO + + fun videoRecordingDelete(deviceRef: DeviceRef) + fun videoRecordingGet(deviceRef: DeviceRef): ByteArray + fun videoRecordingStart(deviceRef: DeviceRef) + fun videoRecordingStop(deviceRef: DeviceRef) + + fun listFiles(deviceRef: DeviceRef, dataPath: DataPath): List + fun pullFile(deviceRef: DeviceRef, dataPath: DataPath): ByteArray + + fun addMedia(deviceRef: DeviceRef, fileName: String, data: ByteArray) + fun resetMedia(deviceRef: DeviceRef) + fun listMedia(deviceRef: DeviceRef): List + + fun getDiagnostic(deviceRef: DeviceRef, type: DiagnosticType, query: DiagnosticQuery): Diagnostic + fun resetDiagnostic(deviceRef: DeviceRef, type: DiagnosticType) + + val remoteAddress: String + fun isReachable(): Boolean + fun prepareNode() + val isNodePrepared: Boolean + fun count(): Int + fun list(): List + fun deleteRelease(deviceRef: DeviceRef, reason: String): Boolean + fun getDeviceDTO(deviceRef: DeviceRef): DeviceDTO + fun totalCapacity(desiredCaps: DesiredCapabilities): Int + fun capacityRemaining(desiredCaps: DesiredCapabilities): Float + fun createDeviceAsync(desiredCaps: DesiredCapabilities): DeviceDTO + fun dispose() + fun uninstallApplication(deviceRef: DeviceRef, bundleId: String) + fun setEnvironmentVariables(deviceRef: DeviceRef, envs: Map) + fun updateApplicationPlist(ref: DeviceRef, plistEntry: PlistEntryDTO) + fun pushFile(ref: DeviceRef, fileName: String, data: ByteArray, bundleId: String) + val publicHostName: String +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt index 2fa93210..9ed14b39 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt @@ -1,7 +1,5 @@ package com.badoo.automation.deviceserver.host -import XCRunSimctl -import com.badoo.automation.deviceserver.ApplicationConfiguration import com.badoo.automation.deviceserver.LogMarkers import com.badoo.automation.deviceserver.command.* import com.badoo.automation.deviceserver.host.IRemote.Companion.isLocalhost @@ -12,39 +10,26 @@ import net.logstash.logback.marker.MapEntriesAppendingMarker import org.slf4j.LoggerFactory import java.io.File import java.io.FileNotFoundException -import java.lang.IllegalStateException import java.time.Duration class Remote( override val hostName: String, override val userName: String, override val publicHostName: String, - override val localExecutor: IShellCommand = ShellCommand(), + override val localExecutor: IShellCommand = ShellCommand(commonEnvironment = mapOf("HOME" to System.getProperty("user.home"))), override val remoteExecutor: IShellCommand = getRemoteCommandExecutor(hostName, userName), - override val fbsimctl: FBSimctl = FBSimctl(remoteExecutor, getHomeBrewPath(remoteExecutor), FBSimctlResponseParser()), - override val xcrunSimctl: XCRunSimctl = XCRunSimctl(remoteExecutor, isLocalhost(hostName, userName), hostName), - private val appConfig: ApplicationConfiguration = ApplicationConfiguration() + override val fbsimctl: FBSimctl = FBSimctl(remoteExecutor, FBSimctlResponseParser()) ) : IRemote { companion object { + const val SSH_AUTH_SOCK = "SSH_AUTH_SOCK" + fun getRemoteCommandExecutor(hostName: String, userName: String): IShellCommand { return if (isLocalhost(hostName, userName)) { - ShellCommand() + ShellCommand(commonEnvironment = mapOf("HOME" to System.getProperty("user.home"))) } else { RemoteShellCommand(hostName, userName) } } - - fun getLocalCommandExecutor(): IShellCommand { - return ShellCommand() - } - - fun getHomeBrewPath(executor: IShellCommand): File { - return when { - executor.exec(listOf("test", "-d", "/opt/homebrew/Cellar")).isSuccess -> File("/opt/homebrew/bin") - executor.exec(listOf("test", "-d", "/usr/local/Cellar")).isSuccess -> File("/usr/local/bin") - else -> throw RuntimeException("Failed to find Homebrew directory") - } - } } private val logger = LoggerFactory.getLogger(javaClass.simpleName) @@ -55,33 +40,6 @@ class Remote( override fun toString(): String = "" - override val homeBrewPath: File by lazy { - getHomeBrewPath(remoteExecutor) - } - - override val tmpDir: File by lazy { - if (isLocalhost()) { - appConfig.tempFolder - } else { - val tmpdirEnvironmentVariable = getEnvironment()["TMPDIR"] - ?: throw IllegalStateException("Environment variable TMPDIR is unknown for host $publicHostName") - File(tmpdirEnvironmentVariable) - } - } - - private fun getEnvironment(): Map { - if (isLocalhost()) { - return System.getenv() - } - - val envDelimiter = "=" - val result = remoteExecutor.exec(command = listOf("/usr/bin/printenv"), environment = mapOf(), returnFailure = false) - - return result.stdOut.lines().associate { - it.substringBefore(envDelimiter) to it.substringAfter(envDelimiter) - } - } - override fun isReachable(): Boolean { //FIXME: We need a reliable way to determine if node is available. SSH request might just time-out if node is under heavy load. return isReachableBySSH() @@ -89,7 +47,7 @@ class Remote( private fun isReachableBySSH(): Boolean { return try { - remoteExecutor.exec(listOf("echo", "1"), returnFailure = true, timeOut = Duration.ofSeconds(20)).isSuccess + remoteExecutor.exec(listOf("echo", "1"), returnFailure = true, timeOut = Duration.ofSeconds(15)).isSuccess } catch (e: SshConnectionException) { false } @@ -101,18 +59,13 @@ class Remote( override fun escape(value: String) = remoteExecutor.escape(value) - override fun shell(command: String, returnOnFailure: Boolean, environment: Map): CommandResult { + override fun shell(command: String, returnOnFailure: Boolean): CommandResult { val cmd = when { isLocalhost() -> listOf("bash", "-c", command) else -> listOf("bash", "-c", ShellUtils.escape(command)) // workaround for how ssh executor is designed } - return try { - remoteExecutor.exec(cmd, environment, returnFailure = returnOnFailure) - } catch (e: SshConnectionException) { - logger.error("Remote retrying shell command on SSH error. Command: $cmd") - remoteExecutor.exec(cmd, environment, returnFailure = returnOnFailure) - } + return remoteExecutor.exec(cmd, emptyMap(), returnFailure = returnOnFailure) } //FIXME: should be a better way of streaming a file over HTTP. without caching bytes in server's memory. Investigating ByteReadChannel @@ -126,7 +79,7 @@ class Remote( val tempFile = File.createTempFile("remoteFile", ".bin") try { - scpFromRemoteHost(file.absolutePath, tempFile.absolutePath, Duration.ofMinutes(2)); + scpFromRemoteHost(file.absolutePath, tempFile.absolutePath, Duration.ofMinutes(2)) return tempFile.readBytes() } finally { tempFile.delete() @@ -151,8 +104,33 @@ class Remote( return remoteExecutor.exec(listOf("test", "-d", path), mapOf(), returnFailure = true).isSuccess } + override fun rsync(from: String, to: String, flags: Set) { + val cmd = mutableListOf("/usr/bin/rsync") + val rsyncFlags = mutableSetOf("--archive", "--partial") + rsyncFlags.addAll(flags) + + cmd.addAll(rsyncFlags) + cmd.add(from) + cmd.add("$userAtHost:$to") + + val env = environmentForRsync() + + logger.debug(logMarker, "Executing rsync command: ${cmd.joinToString(" ")}") + var result = localExecutor.exec(cmd, env) + + if (!result.isSuccess) { + logger.warn(logMarker, "Executing second time rsync command: ${cmd.joinToString(" ")}") + result = localExecutor.exec(cmd) + } + + ensure(result.isSuccess) { + logger.error(logMarker, "Executing rsync command failed. Result: $result") + RuntimeException("Remote $cmd failed with $result") + } + } + override fun scpToRemoteHost(from: String, to: String, timeOut: Duration) { - val result = localExecutor.exec(listOf("/usr/bin/scp", "-v", "-r", from, "$userAtHost:$to"), timeOut = timeOut, returnFailure = true) + val result = localExecutor.exec(listOf("/usr/bin/scp", "-r", from, "$userAtHost:$to"), timeOut = timeOut, returnFailure = true) ensure(result.isSuccess) { val message = "Copying files to remote host failed with ${result.stdErr}" @@ -185,4 +163,14 @@ class Remote( RuntimeException(message) } } + + private fun environmentForRsync(): MutableMap { + val env = mutableMapOf() + val sshAuthSocket = System.getenv(SSH_AUTH_SOCK) + + if (sshAuthSocket != null) { + env[SSH_AUTH_SOCK] = sshAuthSocket + } + return env + } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorProvider.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorProvider.kt index ab271c6e..4ed0aac7 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorProvider.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorProvider.kt @@ -2,70 +2,43 @@ package com.badoo.automation.deviceserver.host import com.badoo.automation.deviceserver.data.DesiredCapabilities import com.badoo.automation.deviceserver.data.DeviceInfo -import com.badoo.automation.deviceserver.data.UDID import com.badoo.automation.deviceserver.host.management.DesiredCapabilitiesMatcher import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDevice -import java.io.File -import java.lang.RuntimeException class SimulatorProvider( val remote: IRemote, - simulatorBackupsConfiguration: String?, private val desiredCapsMatcher: DesiredCapabilitiesMatcher = DesiredCapabilitiesMatcher() -) { - val deviceSetPath: String by lazy { remote.fbsimctl.defaultDeviceSet() } - private val simulatorBackupsPath = File(simulatorBackupsConfiguration ?: deviceSetPath) +) : ISimulatorProvider { + private var cache: List = emptyList() - private var cachedSimulatorList: List = emptyList() - private var cachedBackupsList: List = emptyList() + override fun match(desiredCaps: DesiredCapabilities, usedUdids: Set): FBSimctlDevice? { + val matchList = + when { + desiredCaps.udid != null -> listOfNotNull(findBy(desiredCaps.udid)) + desiredCaps.existing -> list().filter { desiredCapsMatcher.isMatch(DeviceInfo(it), desiredCaps) } + else -> return create(desiredCaps.model, desiredCaps.os, true) + } - fun provideSimulator(desiredCaps: DesiredCapabilities, usedUdids: Set): FBSimctlDevice? { - val simulators = listSimulators() + val firstMatch = matchList.find { !usedUdids.contains(it.udid) } - if (desiredCaps.udid != null && desiredCaps.udid.isNotBlank()) { - val matched = simulators.find { fbSimctlDevice -> desiredCaps.udid == fbSimctlDevice.udid } + if (firstMatch != null) return firstMatch - if (matched == null) { - throw RuntimeException("Unable to find requested device with UDID ${desiredCaps.udid}. List of known devices is $simulators") - } - - if (usedUdids.contains(matched.udid)) { - throw RuntimeException("Simulator with UDID ${matched.udid} is already in use. List of used devices is $usedUdids") - } - - return matched - } - - val matched: FBSimctlDevice? = simulators.find { fbSimctlDevice -> - val deviceInfo = DeviceInfo(fbSimctlDevice) - desiredCapsMatcher.isMatch(deviceInfo, desiredCaps) && !usedUdids.contains(deviceInfo.udid) && backupExists(deviceInfo.udid) - } - - return matched ?: create(desiredCaps.model, desiredCaps.os) + return create(desiredCaps.model, desiredCaps.os, false) } - private fun listSimulators(): List { - if (cachedSimulatorList.isEmpty()) { - cachedSimulatorList = remote.fbsimctl.listSimulators().filter { it.model.isNotBlank() && it.os.isNotBlank() } - } - return cachedSimulatorList + override fun findBy(udid: String): FBSimctlDevice? { + return remote.fbsimctl.listDevice(udid) } - private fun backupExists(udid: UDID): Boolean { - if (cachedBackupsList.isEmpty()) { - val command = listOf("/bin/ls", "-1", simulatorBackupsPath.absolutePath) - val commandResult = remote.exec(command, mapOf(), false, 60L) - val stdOut: String = commandResult.stdOut - val lines: List = stdOut.lines() - cachedBackupsList = lines + override fun list(): List { + if (cache.isEmpty()) { + cache = remote.fbsimctl.listSimulators().filter { !it.model.isBlank() && !it.os.isBlank() } } - - return cachedBackupsList.find { it.contains(udid) } != null + return cache } - private fun create(model: String?, os: String?): FBSimctlDevice { - cachedSimulatorList = emptyList() - cachedBackupsList = emptyList() - return remote.xcrunSimctl.create(model, os) + override fun create(model: String?, os: String?, transitional: Boolean): FBSimctlDevice { + cache = emptyList() + return remote.fbsimctl.create(model, os, transitional) } -} +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt index 8a1d08c8..12fb31d9 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt @@ -1,38 +1,23 @@ package com.badoo.automation.deviceserver.host -import com.badoo.automation.deviceserver.ApplicationConfiguration import com.badoo.automation.deviceserver.LogMarkers.Companion.DEVICE_REF import com.badoo.automation.deviceserver.LogMarkers.Companion.HOSTNAME import com.badoo.automation.deviceserver.LogMarkers.Companion.UDID -import com.badoo.automation.deviceserver.command.CommandResult -import com.badoo.automation.deviceserver.command.SshConnectionException import com.badoo.automation.deviceserver.data.* -import com.badoo.automation.deviceserver.host.management.ApplicationBundle import com.badoo.automation.deviceserver.host.management.ISimulatorHostChecker import com.badoo.automation.deviceserver.host.management.PortAllocator import com.badoo.automation.deviceserver.host.management.errors.OverCapacityException -import com.badoo.automation.deviceserver.ios.IDevice -import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlAppInfo import com.badoo.automation.deviceserver.ios.simulator.ISimulator import com.badoo.automation.deviceserver.ios.simulator.simulatorsThreadPool -import com.badoo.automation.deviceserver.util.AppInstaller -import com.badoo.automation.deviceserver.util.WdaSimulatorBundles import com.badoo.automation.deviceserver.util.deviceRefFromUDID -import com.badoo.automation.deviceserver.util.pollFor import kotlinx.coroutines.experimental.launch +import kotlinx.coroutines.experimental.newFixedThreadPoolContext import kotlinx.coroutines.experimental.runBlocking import net.logstash.logback.marker.MapEntriesAppendingMarker import org.slf4j.LoggerFactory import java.io.File import java.net.URL -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.StandardOpenOption -import java.time.Duration -import java.util.* -import java.util.concurrent.* -import kotlin.collections.HashMap -import kotlin.system.measureNanoTime +import java.util.concurrent.ConcurrentHashMap class SimulatorsNode( val remote: IRemote, @@ -40,127 +25,71 @@ class SimulatorsNode( private val hostChecker: ISimulatorHostChecker, private val simulatorLimit: Int, concurrentBoots: Int, - private val wdaSimulatorBundles: WdaSimulatorBundles, - private val applicationConfiguration: ApplicationConfiguration = ApplicationConfiguration(), - private val simulatorProvider: SimulatorProvider = SimulatorProvider(remote, applicationConfiguration.simulatorBackupPath), + private val wdaRunnerXctest: File, + private val simulatorProvider: ISimulatorProvider = SimulatorProvider(remote), private val portAllocator: PortAllocator = PortAllocator(), private val simulatorFactory: ISimulatorFactory = object : ISimulatorFactory {} -) : IDeviceNode { - private val appBinariesCache: MutableMap = ConcurrentHashMap(200) - private val simulatorsBootExecutorService: ExecutorService = Executors.newFixedThreadPool(simulatorLimit) - private val concurrentBoot: ExecutorService = Executors.newFixedThreadPool(concurrentBoots) - private val prepareTasks = ConcurrentHashMap>() +) : ISimulatorsNode { override fun updateApplicationPlist(ref: DeviceRef, plistEntry: PlistEntryDTO) { val applicationContainer = getDeviceFor(ref).applicationContainer(plistEntry.bundleId) val path = File(plistEntry.file_name).toPath() - val key = plistEntry.key - val value = plistEntry.value - - if (plistEntry.command == "set") { - applicationContainer.setPlistValue(path, key, value) - } else { - val type = plistEntry.type ?: throw RuntimeException("Unable to add new property $key as it requires value type.") - applicationContainer.addPlistValue(path, key, value, type) - } - } - - private val appInstaller: AppInstaller = AppInstaller(remote) - - override fun installApplication(deviceRef: DeviceRef, appBundleDto: AppBundleDto) { - logger.info(logMarker, "Ready to install app ${appBundleDto.appUrl} on device $deviceRef") - val appBinaryPath = appBinariesCache[appBundleDto.appUrl] - ?: throw RuntimeException("Unable to find requested binary. Deploy binary first from url ${appBundleDto.appUrl}") - - val device: ISimulator = getDeviceFor(deviceRef) - device.installApplication(appInstaller, appBundleDto.appUrl, appBinaryPath) - } - - override fun appInstallationStatus(deviceRef: DeviceRef): Map { - return getDeviceFor(deviceRef).appInstallationStatus() - } - - override fun deployApplication(appBundle: ApplicationBundle) { - val appDirectory = if (remote.isLocalhost()) { - appBundle.appDirectory!! - } else { - copyAppToRemoteHost(appBundle) - } - val key = appBundle.appUrl.toExternalForm() - appBinariesCache[key] = appDirectory - } - - override fun deleteAppData(deviceRef: DeviceRef, bundleId: String) { - return getDeviceFor(deviceRef).dataContainer(bundleId).delete() - } - private fun copyAppToRemoteHost(appBundle: ApplicationBundle): File { - val marker = MapEntriesAppendingMarker(mapOf(HOSTNAME to remote.publicHostName, "action_name" to "scp_application")) - logger.debug(marker, "Copying application ${appBundle.appUrl} to $this") - - val remoteDirectory = File(applicationConfiguration.appBundleCacheRemotePath, UUID.randomUUID().toString()).absolutePath - remote.exec(listOf("/bin/rm", "-rf", remoteDirectory), mapOf(), false, 90).stdOut.trim() - remote.exec(listOf("/bin/mkdir", "-p", remoteDirectory), mapOf(), false, 90).stdOut.trim() - - val nanos = measureNanoTime { - remote.scpToRemoteHost(appBundle.appDirectory!!.absolutePath, remoteDirectory) + when (plistEntry.command) { + "set" -> applicationContainer.setPlistValue(path, plistEntry.key, plistEntry.value) + "add" -> { + val entryType = plistEntry.type + ?: throw IllegalArgumentException("Unable to add new property ${plistEntry.key} as it requires value type (property_type).") + applicationContainer.addPlistValue(path, plistEntry.key, plistEntry.value, entryType) + } + else -> throw IllegalArgumentException("Unsupported operation: ${plistEntry.command}") } - val seconds = TimeUnit.NANOSECONDS.toSeconds(nanos) - val measurement = mapOf(HOSTNAME to remote.publicHostName, "action_name" to "scp_application", "duration" to seconds) - - logger.debug(MapEntriesAppendingMarker(measurement), "Successfully copied application ${appBundle.appUrl} to $this. Took $seconds seconds") - return File(remoteDirectory, appBundle.appDirectory!!.name) } override val remoteAddress: String get() = publicHostName private val logger = LoggerFactory.getLogger(javaClass.simpleName) private val logMarker = MapEntriesAppendingMarker(mapOf( - HOSTNAME to remote.publicHostName + HOSTNAME to remote.hostName )) - private var macOSVersion: String = "0" + override val isNodePrepared: Boolean + get() = remote.isLocalhost() || hostChecker.isWdaBundleDeployed override fun prepareNode() { logger.info(logMarker, "Preparing node ${remote.hostName}") hostChecker.checkPrerequisites() - hostChecker.createDirectories() if (!remote.isLocalhost()) { hostChecker.copyWdaBundleToHost() - if (applicationConfiguration.useTestHelperApp) { - hostChecker.copyTestHelperBundleToHost() - } - hostChecker.copyVideoRecorderHelperToHost() } hostChecker.cleanup() hostChecker.setupHost() - - macOSVersion = getMacOSVersion() - logger.info(logMarker, "Prepared node ${remote.hostName}. macOS version $macOSVersion") + logger.info(logMarker, "Prepared node ${remote.hostName}") } private val supportedArchitectures = listOf("x86_64") + private val deviceSetPath: String by lazy { remote.fbsimctl.defaultDeviceSet() } + private val concurrentBoot = newFixedThreadPoolContext(concurrentBoots, "sim_boot_${remote.hostName}") - override fun getDeviceFor(ref: DeviceRef): ISimulator { - return createdSimulators[ref]!! //FIXME: replace with explicit unwrapping + private fun getDeviceFor(ref: DeviceRef): ISimulator { + return createdDevices[ref]!! //FIXME: replace with explicit unwrapping } - private val createdSimulators = ConcurrentHashMap() + private val createdDevices = ConcurrentHashMap() private val allocatedPorts = HashMap() override fun createDeviceAsync(desiredCaps: DesiredCapabilities): DeviceDTO { synchronized(this) { // FIXME: synchronize in some other place? - if (createdSimulators.size >= simulatorLimit) { + if (createdDevices.size >= simulatorLimit) { val message = "$this was asked for a newSimulator, but is already at capacity $simulatorLimit" logger.error(logMarker, message) throw OverCapacityException(message) } - val usedUdids = createdSimulators.map { it.value.udid }.toSet() - val fbSimctlDevice = simulatorProvider.provideSimulator(desiredCaps, usedUdids) - + val usedUdids = createdDevices.map { it.value.udid }.toSet() + val fbSimctlDevice = simulatorProvider.match(desiredCaps, usedUdids) if (fbSimctlDevice == null) { val message = "$this could not construct or match a simulator for $desiredCaps" logger.error(logMarker, message) @@ -179,16 +108,10 @@ class SimulatorsNode( logger.debug(simLogMarker, "Will create simulator $ref") - val simulator = simulatorFactory.newSimulator(ref, remote, fbSimctlDevice, ports, simulatorProvider.deviceSetPath, - wdaSimulatorBundles, concurrentBoot, desiredCaps.headless, desiredCaps.useWda, desiredCaps.useAppium) - - cancelRunningSimulatorTask(ref, "createDeviceAsync") - - prepareTasks[ref] = simulatorsBootExecutorService.submit { - simulator.prepareAsync() - } - - createdSimulators[ref] = simulator + val simulator = simulatorFactory.newSimulator(ref, remote, fbSimctlDevice, ports, deviceSetPath, + wdaRunnerXctest, concurrentBoot, desiredCaps.headless, desiredCaps.useWda, fbSimctlDevice.toString()) + simulator.prepareAsync() + createdDevices[ref] = simulator logger.debug(simLogMarker, "Created simulator $ref") @@ -204,87 +127,53 @@ class SimulatorsNode( return getDeviceFor(deviceRef).media.list() } - override fun listPhotoData(deviceRef: DeviceRef) : List { - return getDeviceFor(deviceRef).media.listPhotoData() - } - override fun addMedia(deviceRef: DeviceRef, fileName: String, data: ByteArray) { getDeviceFor(deviceRef).media.addMedia(File(fileName), data) } - override fun syslog(deviceRef: DeviceRef) : File { - return getDeviceFor(deviceRef).osLog.osLogFile - } - - override fun instrumentationAgentLog(deviceRef: DeviceRef): File { - return getDeviceFor(deviceRef).instrumentationAgentLog - } - - override fun deleteInstrumentationAgentLog(deviceRef: DeviceRef) { - val logFile = getDeviceFor(deviceRef).instrumentationAgentLog - Files.write(logFile.toPath(), ByteArray(0), StandardOpenOption.TRUNCATE_EXISTING) - } - - override fun appiumServerLog(deviceRef: DeviceRef): File { - return getDeviceFor(deviceRef).appiumServerLog - } - - override fun deleteAppiumServerLog(deviceRef: DeviceRef) { - getDeviceFor(deviceRef).deleteAppiumServerLog() - } - - override fun syslogStart(deviceRef: DeviceRef, sysLogCaptureOptions: SysLogCaptureOptions) { - getDeviceFor(deviceRef).osLog.startWritingLog(sysLogCaptureOptions) - } - - override fun syslogStop(deviceRef: DeviceRef) { - getDeviceFor(deviceRef).osLog.stopWritingLog() - } - - override fun syslogDelete(deviceRef: DeviceRef) { - getDeviceFor(deviceRef).osLog.deleteLogFiles() - } - - private fun remoteNotificationsSupported(simulatorOSVersion: Int): Boolean { - return simulatorOSVersion >= 16 && macOSVersion.split(".").first().toInt() >= 13 + override fun getDiagnostic(deviceRef: DeviceRef, type: DiagnosticType, query: DiagnosticQuery): Diagnostic { + return when (type) { + DiagnosticType.SystemLog -> Diagnostic( + type = type, + content = getDeviceFor(deviceRef).systemLog.content() + ) + DiagnosticType.OsLog -> Diagnostic( + type = type, + content = getDeviceFor(deviceRef).osLog.content(query.process) + ) + } } - private fun getMacOSVersion(): String { - val commandResult: CommandResult = remote.shell("/usr/bin/sw_vers -productVersion", returnOnFailure = false) - return commandResult.stdOut.trim() + override fun resetDiagnostic(deviceRef: DeviceRef, type: DiagnosticType) { + when (type) { + DiagnosticType.SystemLog -> getDeviceFor(deviceRef).systemLog.truncate() + DiagnosticType.OsLog -> getDeviceFor(deviceRef).osLog.truncate() + } } private fun simulatorToDTO(device: ISimulator): DeviceDTO { with(device) { return DeviceDTO( ref, - deviceState, + state, fbsimctlEndpoint, wdaEndpoint, calabashPort, - calabashEndpoint, mjpegServerPort, - appiumPort, - appiumEndpoint, - device.deviceInfo, - device.lastException?.toDto(), + device.userPorts.toSet(), + device.info, + device.lastError?.toDto(), capabilities = ActualCapabilities( setLocation = true, terminateApp = true, - remoteNotifications = remoteNotificationsSupported(device.deviceInfo.osMajorVersion()), - isAppiumEnabled = device.isAppiumEnabled, videoCapture = true ) ) } } - override fun sendPushNotification(deviceRef: DeviceRef, bundleId: String, notificationContent: ByteArray) { - getDeviceFor(deviceRef).sendPushNotification(bundleId, notificationContent) - } - - override fun sendPasteboard(deviceRef: DeviceRef, payload: ByteArray) { - getDeviceFor(deviceRef).sendPasteboard(payload) + override fun approveAccess(deviceRef: DeviceRef, bundleId: String) { + getDeviceFor(deviceRef).approveAccess(bundleId) } override fun setPermissions(deviceRef: DeviceRef, appPermissions: AppPermissionsDto) { @@ -292,7 +181,7 @@ class SimulatorsNode( } override fun capacityRemaining(desiredCaps: DesiredCapabilities): Float { - return (simulatorLimit - createdSimulators.size) * 1F / simulatorLimit + return (simulatorLimit - count()) * 1F / simulatorLimit } override fun clearSafariCookies(deviceRef: DeviceRef) { @@ -303,15 +192,15 @@ class SimulatorsNode( getDeviceFor(deviceRef).shake() } + override fun count(): Int = createdDevices.size + override fun dispose() { logger.info(logMarker, "Finalising simulator pool for ${remote.hostName}") - val disposeJobs = createdSimulators.map { + val disposeJobs = createdDevices.map { launch(context = simulatorsThreadPool) { try { - val simulator = it.value - cancelRunningSimulatorTask(simulator.ref, "dispose") - simulator.release("Finalising pool for ${remote.hostName}") + it.value.release("Finalising pool for ${remote.hostName}") } catch (e: Throwable) { logger.error(logMarker, "While releasing '${it.key}' for ${remote.hostName}: $e") } @@ -327,51 +216,6 @@ class SimulatorsNode( logger.info(logMarker, "Finalised simulator pool for ${remote.hostName}") } - override fun getNodeInfo(): NodeInfo { - return NodeInfo.getNodeInfo(remote) - } - - override fun reboot() { - val uptimeInfoBeforeReboot = getNodeInfo() - logger.info(logMarker, "Scheduling node for reboot $publicHostName. Current uptime: [${uptimeInfoBeforeReboot.uptime}]. Boot time: ${uptimeInfoBeforeReboot.bootTime}") - - try { - remote.shell("sudo /sbin/reboot", returnOnFailure = true) - } catch (e: SshConnectionException) { - // ignore - } - - Thread.sleep(Duration.ofSeconds(60).toMillis()) - - var isReachable: Boolean = false - - pollFor( - Duration.ofSeconds(300), - "Waiting to be reachable after reboot", - true, - Duration.ofSeconds(10), - logger, - logMarker - ) { - isReachable = isReachable() - isReachable - } - - if (!isReachable) { - logger.error(logMarker, "Node $publicHostName node is not reachable after reboot") - return - } - - val uptimeInfoAfterReboot = getNodeInfo() - val wasRebooted = uptimeInfoAfterReboot.bootTime > uptimeInfoBeforeReboot.bootTime - - if (wasRebooted) { - logger.info(logMarker, "Node $publicHostName was rebooted successfully. Current uptime: [${uptimeInfoAfterReboot.uptime}]. Boot time: ${uptimeInfoBeforeReboot.bootTime}") - } else { - logger.error(logMarker, "Node $publicHostName was not rebooted. Current uptime: [${uptimeInfoAfterReboot.uptime}]. Boot time: ${uptimeInfoBeforeReboot.bootTime}") - } - } - override fun endpointFor(deviceRef: DeviceRef, port: Int): URL { return getDeviceFor(deviceRef).endpointFor(port) } @@ -384,34 +228,6 @@ class SimulatorsNode( return getDeviceFor(deviceRef).lastCrashLog() } - override fun listApps(deviceRef: DeviceRef): List = getDeviceFor(deviceRef).listApps() - - override fun locationListScenarios(deviceRef: DeviceRef): List { - return getDeviceFor(deviceRef).locationManager.listScenarios() - } - - override fun locationClear(deviceRef: DeviceRef) { - getDeviceFor(deviceRef).locationManager.clear() - } - - override fun locationSet(deviceRef: DeviceRef, latitude: Double, longitude: Double) { - getDeviceFor(deviceRef).locationManager.setLocation(latitude, longitude) - } - - override fun locationRunScenario(deviceRef: DeviceRef, scenarioName: String) { - getDeviceFor(deviceRef).locationManager.runScenario(scenarioName) - } - - override fun locationStartLocationSequence( - deviceRef: DeviceRef, - speed: Int, - distance: Int, - interval: Int, - waypoints: List - ) { - getDeviceFor(deviceRef).locationManager.startLocationSequence(speed, distance, interval, waypoints) - } - override fun crashLogs(deviceRef: DeviceRef, pastMinutes: Long?): List { return getDeviceFor(deviceRef).crashLogs(pastMinutes) } @@ -425,52 +241,22 @@ class SimulatorsNode( } override fun list(): List { - return createdSimulators.map { simulatorToDTO(it.value) } + return createdDevices.map { simulatorToDTO(it.value) } } override fun isReachable(): Boolean = remote.isReachable() override fun deleteRelease(deviceRef: DeviceRef, reason: String): Boolean { - val iSimulator = createdSimulators[deviceRef] ?: return false - - cancelRunningSimulatorTask(deviceRef, "deleteRelease") - + val iSimulator = createdDevices[deviceRef] ?: return false iSimulator.release("deleteRelease $reason $deviceRef") - - createdSimulators.remove(deviceRef) - val entries = allocatedPorts[deviceRef] ?: return true - portAllocator.deallocateDAP(entries) - - return true - } - - override fun deleteDevice(deviceRef: DeviceRef, reason: String): Boolean { - val iSimulator = createdSimulators[deviceRef] ?: return false - cancelRunningSimulatorTask(deviceRef, "deleteRelease") - iSimulator.delete("deleteForcefully $reason $deviceRef") - logger.info(logMarker, "Deleted Simulator $deviceRef successfully") - createdSimulators.remove(deviceRef) + createdDevices.remove(deviceRef) val entries = allocatedPorts[deviceRef] ?: return true portAllocator.deallocateDAP(entries) return true } - private fun cancelRunningSimulatorTask(deviceRef: DeviceRef, reason: String) { - val task = prepareTasks[deviceRef] - if (task != null) { - if (!task.isDone) { - logger.error(logMarker, "Cancelling async task for Simulator $deviceRef while performing $reason") - task.cancel(true) - } - prepareTasks.remove(deviceRef) - } - } - override fun resetAsync(deviceRef: DeviceRef) { - getDeviceFor(deviceRef).resetAsync().let { resetProc -> - cancelRunningSimulatorTask(deviceRef, "resetAsync") - prepareTasks[deviceRef] = simulatorsBootExecutorService.submit(resetProc) - } + getDeviceFor(deviceRef).resetAsync() } override fun state(deviceRef: DeviceRef): SimulatorStatusDTO { @@ -493,10 +279,6 @@ class SimulatorsNode( return getDeviceFor(deviceRef).videoRecorder.getRecording() } - override fun videoRecordingLogGet(deviceRef: DeviceRef): String { - return getDeviceFor(deviceRef).videoRecorder.getRecordingLog() - } - override fun videoRecordingStart(deviceRef: DeviceRef) { getDeviceFor(deviceRef).videoRecorder.start() } @@ -513,38 +295,22 @@ class SimulatorsNode( return getDeviceFor(deviceRef).dataContainer(dataPath.bundleId).readFile(dataPath.path) } - override fun pullFile(deviceRef: DeviceRef, path: Path): ByteArray { - return getDeviceFor(deviceRef).sharedContainer().readFile(path) - } - override fun pushFile(ref: DeviceRef, fileName: String, data: ByteArray, bundleId: String) { getDeviceFor(ref).dataContainer(bundleId).writeFile(File(fileName), data) } - override fun pushFile(ref: DeviceRef, data: ByteArray, path: Path) { - getDeviceFor(ref).sharedContainer().writeFile(data, path) - } - - override fun deleteFile(ref: DeviceRef, path: Path) { - getDeviceFor(ref).sharedContainer().delete(path) - } - override fun openUrl(deviceRef: DeviceRef, url: String) { getDeviceFor(deviceRef).openUrl(url) } override fun uninstallApplication(deviceRef: DeviceRef, bundleId: String) { - getDeviceFor(deviceRef).uninstallApplication(bundleId, appInstaller) + getDeviceFor(deviceRef).uninstallApplication(bundleId) } override fun setEnvironmentVariables(deviceRef: DeviceRef, envs: Map) { getDeviceFor(deviceRef).setEnvironmentVariables(envs) } - override fun getEnvironmentVariable(deviceRef: DeviceRef, variableName: String): String { - return getDeviceFor(deviceRef).getEnvironmentVariable(variableName) - } - override fun toString(): String { return "${javaClass.simpleName} at $remoteAddress" } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ApplicationBundle.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ApplicationBundle.kt deleted file mode 100644 index 1ba038cd..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ApplicationBundle.kt +++ /dev/null @@ -1,128 +0,0 @@ -package com.badoo.automation.deviceserver.host.management - -import com.badoo.automation.deviceserver.ApplicationConfiguration -import com.badoo.automation.deviceserver.util.CustomHttpClient -import net.logstash.logback.marker.MapEntriesAppendingMarker -import okhttp3.Request -import org.slf4j.Logger -import java.io.File -import java.io.IOException -import java.lang.RuntimeException -import java.net.URL -import java.nio.file.Files -import java.nio.file.StandardCopyOption -import java.time.Duration -import java.util.concurrent.TimeUnit -import java.util.zip.ZipFile -import kotlin.system.measureNanoTime - -class ApplicationBundle( - val appUrl: URL -) { - val bundleZip: File by lazy { - val file = File(appUrl.file) - File.createTempFile("${file.nameWithoutExtension}.", ".${file.extension}", ApplicationConfiguration().appBundleCachePath) - } - private val unzipDirectory by lazy { File(bundleZip.parent, bundleZip.nameWithoutExtension) } - var appDirectory: File? = null - private val httpClient = CustomHttpClient.client - .newBuilder() - .followRedirects(true) - .callTimeout(Duration.ofMinutes(10)) // TODO: case when timed out - .build() - - private var bundleZipSize: Long = -1 - val isDownloaded: Boolean get() = bundleZip.exists() && bundleZipSize > 0 && bundleZipSize == bundleZip.length() - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as ApplicationBundle - - if (appUrl != other.appUrl) return false - - return true - } - - override fun hashCode(): Int { - return appUrl.hashCode() - } - - fun downloadApp(logger: Logger, marker: MapEntriesAppendingMarker) { - try { - download(appUrl) - } catch (e: IOException) { - logger.error(marker, "Failed to download app from url [$appUrl]. Retrying...") - download(appUrl) - } - } - - fun unpack(logger: Logger, marker: MapEntriesAppendingMarker) { - unzipDirectory.deleteRecursively() - - val nanos = measureNanoTime { - unzipApp(bundleZip, unzipDirectory) - } - - logger.debug(marker, "Unzipped app successfully. Took ${TimeUnit.NANOSECONDS.toSeconds(nanos)} seconds") - - val unzipped = unzipDirectory.list() - if (unzipped.size != 1) { - throw RuntimeException("Unzipped archive contains too many entries") - } - - appDirectory = File(unzipDirectory, unzipped.first()) - } - - private fun unzipApp(zipFile: File, unzipDirectory: File) { - unzipDirectory.mkdirs() - - ZipFile(zipFile.absolutePath).use { zip -> - zip.entries().asSequence().forEach { zipEntry -> - val unzippedFile = File(unzipDirectory, zipEntry.name) - - if (zipEntry.isDirectory) { - unzippedFile.mkdirs() - } else { - zip.getInputStream(zipEntry).use { input -> - unzippedFile.outputStream().use { out -> input.copyTo(out) } - } - } - } - } - } - - private fun download(url: URL) { - val request: Request = Request.Builder() - .get() - .url(url) - .build() - - try { - val httpCall = httpClient.newCall(request) - val outPath = bundleZip.toPath() - - httpCall.execute().use { response -> - if (response.code != 200) { - throw RuntimeException("Unable to download binary from $url. Response code: ${response.code}. Headers: ${response.headers}. Body: ${response.peekBody(1024).string()}") - } - - val contentLength = response.headers.get("Content-Length")?.toInt() ?: -1 - response.body!!.byteStream().use { inputStream -> - Files.copy(inputStream, outPath, StandardCopyOption.REPLACE_EXISTING) - } - - val downloadLength = bundleZip.length() - if (contentLength > 0 && downloadLength != contentLength.toLong()) { - throw IOException("Downloaded file size ($downloadLength) different from Content-Length ($contentLength)") - } - - bundleZipSize = downloadLength - } - } catch (e: IOException) { - Files.deleteIfExists(bundleZip.toPath()) - throw e - } - } -} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DesiredCapabilitiesMatcher.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DesiredCapabilitiesMatcher.kt index 7f865966..ccbf1b40 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DesiredCapabilitiesMatcher.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DesiredCapabilitiesMatcher.kt @@ -2,14 +2,13 @@ package com.badoo.automation.deviceserver.host.management import com.badoo.automation.deviceserver.data.DesiredCapabilities import com.badoo.automation.deviceserver.data.DeviceInfo -import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDevice class DesiredCapabilitiesMatcher { fun isMatch(actual: DeviceInfo, desiredCaps: DesiredCapabilities): Boolean { with(desiredCaps) { - return if (udid.isNullOrBlank()) { - (model.isNullOrBlank() || model == actual.model) && (os.isNullOrBlank() || isRuntimeMatch(os!!, actual.os)) + return if (udid == null || udid.isBlank()) { + (model == null || model == actual.model) && (os == null || isRuntimeMatch(os, actual.os)) } else { udid == actual.udid } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt index d3b6bfd5..dc9368ba 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt @@ -1,27 +1,12 @@ package com.badoo.automation.deviceserver.host.management -import com.badoo.automation.deviceserver.ApplicationConfiguration import com.badoo.automation.deviceserver.DeviceServerConfig -import com.badoo.automation.deviceserver.command.ShellCommand import com.badoo.automation.deviceserver.data.* -import com.badoo.automation.deviceserver.host.NodeInfo +import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException import com.badoo.automation.deviceserver.host.management.errors.NoNodesRegisteredException import com.badoo.automation.deviceserver.ios.ActiveDevices -import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlAppInfo -import com.badoo.automation.deviceserver.ios.simulator.periodicTasksPool -import net.logstash.logback.marker.MapEntriesAppendingMarker import org.slf4j.LoggerFactory -import java.io.File import java.net.URL -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.attribute.BasicFileAttributes -import java.time.Duration -import java.util.concurrent.* -import kotlin.system.measureNanoTime - -private val INFINITE_DEVICE_TIMEOUT: Duration = Duration.ofSeconds(Integer.MAX_VALUE.toLong()) -private const val MAX_TEMP_FILE_AGE: Long = 3600L * 3 // MILLI SEC class DeviceManager( config: DeviceServerConfig, @@ -29,196 +14,28 @@ class DeviceManager( activeDevices: ActiveDevices = ActiveDevices() ) { private val logger = LoggerFactory.getLogger(javaClass.simpleName) - private val deviceTimeoutInSecs: Duration private val nodeRegistry = NodeRegistry(activeDevices) private val autoRegistrar = NodeRegistrar( nodesConfig = config.nodes, nodeFactory = nodeFactory, nodeRegistry = nodeRegistry ) - private val appConfig = ApplicationConfiguration() - - init { - val timeoutFromConfig: Long? = config.timeouts["device"]?.toLong() - - deviceTimeoutInSecs = - if (timeoutFromConfig != null && timeoutFromConfig > 0) { - Duration.ofSeconds(timeoutFromConfig) - } else { - INFINITE_DEVICE_TIMEOUT - } - } - - private val File.isTestArtifact get(): Boolean { - return name.contains(".app.zip.") - || name.contains("fbsimctl-") - || name.contains("videoRecording_") - || name.contains("iOS_SysLog_") - || name.contains("device_agent_log_") - || name.contains("appium_server_log") - || name.endsWith(".xctestrun") - } - - private val shellExecutor = ShellCommand() - - fun extractTestApp() { - val testHelperArchiveFileName = "TestHelper.app.tar.bz2" - val testHelperRoot = File(appConfig.remoteTestHelperAppBundleRoot) - val testHelperArchive = File(testHelperRoot, testHelperArchiveFileName) - - logger.info("Start to extract TestHelper application $testHelperArchiveFileName to ${testHelperRoot.absolutePath}") - - testHelperRoot.deleteRecursively() - testHelperRoot.mkdirs() - - val testHelperStream = DeviceManager::class.java.classLoader.getResourceAsStream(testHelperArchiveFileName) - - if (testHelperStream == null) { - logger.error("Failed to find test helper file $testHelperArchiveFileName in resources") - return - } - - testHelperStream.use { inputStream -> - testHelperArchive.outputStream().use { outputStream -> - inputStream.copyTo(outputStream) - } - } - - val result = shellExecutor.exec(listOf("tar", "--directory=$testHelperRoot", "-jxvf", testHelperArchive.absolutePath)) - check(result.isSuccess) { - "Failed to unpack test helper app. STDOUT: ${result.stdOut}, STDERR ${result.stdErr}" - } - - logger.info("Successfully extracted TestHelper application $testHelperArchiveFileName to ${testHelperRoot.absolutePath}") - } - - fun extractVideoRecorder() { - val videoRecorderFile = appConfig.remoteVideoRecorder - videoRecorderFile.delete() - videoRecorderFile.parentFile.mkdirs() - - logger.info("Start to copy Video recorder script ${videoRecorderFile.name} from resources to ${videoRecorderFile.absolutePath}") - - val videoRecorderFileStream = DeviceManager::class.java.classLoader.getResourceAsStream(videoRecorderFile.name) - - if (videoRecorderFileStream == null) { - logger.error("Failed to find Video recorder script ${videoRecorderFile.name} in resources") - return - } - - videoRecorderFileStream.use { inputStream -> - videoRecorderFile.outputStream().use { outputStream -> - inputStream.copyTo(outputStream) - } - } - - videoRecorderFile.setWritable(false) - videoRecorderFile.setExecutable(true) - - logger.info("Successfully copied Video recorder script ${videoRecorderFile.name} from resources to ${videoRecorderFile.absolutePath}") - } - - private fun File.isOlderThan(maxCreationTime: Long): Boolean { - val attributes = Files.readAttributes(toPath(), BasicFileAttributes::class.java) - return attributes.lastModifiedTime().toMillis() < maxCreationTime - } - - fun cleanupTemporaryFiles() { - val maxCreationTime = System.currentTimeMillis() - MAX_TEMP_FILE_AGE - - appConfig.tempFolder.listFiles()!!.forEach { - if (it.isTestArtifact && it.isFile && it.isOlderThan(maxCreationTime)) { - try { - it.delete() - } catch (e: RuntimeException) { - logger.error("Failed to cleanup file ${it.absolutePath}. Error: ${e.message}", e) - } - } - } - - logger.debug("Cleanup complete.") - } - - private lateinit var cleanUpTask: ScheduledFuture<*> - - fun startPeriodicFileCleanup() { - val runnable = Runnable { - try { - cleanupTemporaryFiles() - } catch (t: Throwable) { - logger.error( - "Cleanup failed. ${t.javaClass.name} ${t.message}\n${ - t.stackTrace.map { it.toString() }.joinToString { "\n" } - }" - ) - } - } - cleanUpTask = periodicTasksPool.scheduleWithFixedDelay( - runnable, - 0, - 60, - TimeUnit.MINUTES - ) - - } fun startAutoRegisteringDevices() { autoRegistrar.startAutoRegistering() } - fun restartNodesGracefully(isParallelRestart: Boolean, shouldReboot: Boolean, forceReboot: Boolean): Boolean { - return autoRegistrar.restartNodesGracefully(isParallelRestart, shouldReboot, forceReboot) - } - - private val zombieReaper = ZombieReaper() - - fun launchZombieReaper() { - zombieReaper.launchReapingZombies() + fun restartNodesGracefully(isParallelRestart: Boolean): Boolean { + return autoRegistrar.restartNodesGracefully(isParallelRestart) } fun getStatus(): Map { - val nodeWrappers = nodeRegistry.getAlive() - - val aliveNodesInfo: List> = getNodesInfo(nodeWrappers) - - val allNodes = nodeRegistry.getAll().map { it.node.publicHostName }.sorted() - return mapOf( "initialized" to nodeRegistry.getInitialRegistrationComplete(), - "alive_nodes" to aliveNodesInfo, - "all_nodes" to allNodes, "sessions" to listOf(nodeRegistry.activeDevices.getStatus()).toString() ) } - private fun getNodesInfo(nodeWrappers: Set): List> { - if (nodeWrappers.isEmpty()) { - logger.debug("Unable to get NodeInfo for empty list of nodes") - return listOf() - } - - val executor = Executors.newFixedThreadPool(nodeWrappers.size) - val tasks = mutableListOf>>() - - nodeWrappers.forEach { nodeWrapper -> - val task: Future> = executor.submit(Callable> { - return@Callable Pair(nodeWrapper.node.publicHostName, nodeWrapper.node.getNodeInfo()) - }) - tasks.add(task) - } - - executor.shutdown() - - val aliveNodesInfo: List> = tasks.map { it.get() } - - try { - executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); - } catch (e: InterruptedException) { - println("Failed to awaitTermination while retrieving NodeInfo due to issue. ${e.javaClass.name}, ${e.message}") - } - return aliveNodesInfo - } - fun getTotalCapacity(desiredCaps: DesiredCapabilities): Map { return nodeRegistry.capacitiesTotal(desiredCaps) } @@ -235,12 +52,8 @@ class DeviceManager( nodeRegistry.activeDevices.getNodeFor(ref).resetAsync(ref) } - fun sendPushNotification(ref: DeviceRef, bundleId: String, notificationContent: ByteArray) { - nodeRegistry.activeDevices.getNodeFor(ref).sendPushNotification(ref, bundleId, notificationContent) - } - - fun sendPasteboard(ref: DeviceRef, payload: ByteArray) { - nodeRegistry.activeDevices.getNodeFor(ref).sendPasteboard(ref, payload) + fun approveAccess(ref: DeviceRef, bundleId: String) { + nodeRegistry.activeDevices.getNodeFor(ref).approveAccess(ref, bundleId) } fun setPermissions(ref: DeviceRef, permissions: AppPermissionsDto) { @@ -268,8 +81,6 @@ class DeviceManager( return nodeRegistry.activeDevices.getNodeFor(ref).lastCrashLog(ref) } - fun listApps(ref: DeviceRef): List = nodeRegistry.activeDevices.getNodeFor(ref).listApps(ref) - fun shake(ref: DeviceRef) { nodeRegistry.activeDevices.getNodeFor(ref).shake(ref) } @@ -290,10 +101,6 @@ class DeviceManager( return nodeRegistry.activeDevices.getNodeFor(ref).videoRecordingGet(ref) } - fun getVideoLog(ref: DeviceRef): String { - return nodeRegistry.activeDevices.getNodeFor(ref).videoRecordingLogGet(ref) - } - fun deleteVideo(ref: DeviceRef) { nodeRegistry.activeDevices.getNodeFor(ref).videoRecordingDelete(ref) } @@ -302,33 +109,30 @@ class DeviceManager( nodeRegistry.activeDevices.getNodeFor(ref).uninstallApplication(ref, bundleId) } - fun deleteAppData(ref: DeviceRef, bundleId: String) { - nodeRegistry.activeDevices.getNodeFor(ref).deleteAppData(ref, bundleId) - } - fun getDeviceState(ref: DeviceRef): SimulatorStatusDTO { return nodeRegistry.activeDevices.getNodeFor(ref).state(ref) } fun createDeviceAsync(desiredCaps: DesiredCapabilities, userId: String?): DeviceDTO { try { - return nodeRegistry.createDeviceAsync(desiredCaps, deviceTimeoutInSecs, userId) - } catch (e: NoNodesRegisteredException) { + return nodeRegistry.createDeviceAsync(desiredCaps, userId) + } catch(e: NoNodesRegisteredException) { val erredNodes = autoRegistrar.nodeWrappers.filter { n -> n.lastError != null } val errors = erredNodes.joinToString { n -> "${n.node.remoteAddress} -> ${n.lastError?.localizedMessage}" } - throw(NoNodesRegisteredException(e.message + "\n$errors")) + throw(NoNodesRegisteredException(e.message+"\n$errors")) } } fun deleteReleaseDevice(ref: DeviceRef, reason: String) { - nodeRegistry.deleteReleaseDevice(ref, reason) - } - - fun deleteDevice(ref: DeviceRef, reason: String) { - nodeRegistry.deleteDevice(ref, reason) + try { // using try-catch here not to expose tryGetNodeFor + nodeRegistry.activeDevices.releaseDevice(ref, reason) + } catch (e: DeviceNotFoundException) { + logger.warn("Skipping $ref release because no node knows about it") + return + } } - fun getDeviceRefs(): List { + fun getDeviceRefs() : List { return nodeRegistry.activeDevices.deviceList() } @@ -336,30 +140,6 @@ class DeviceManager( val devices = nodeRegistry.activeDevices.getUserDeviceRefs(userId) nodeRegistry.activeDevices.releaseDevices(devices, reason) } - fun releaseAllDevices(reason: String) { - val devices = nodeRegistry.activeDevices.deviceRefs().toList() - nodeRegistry.activeDevices.releaseDevices(devices, reason) - } - - fun locationListScenarios(ref: DeviceRef): List { - return nodeRegistry.activeDevices.getNodeFor(ref).locationListScenarios(ref) - } - - fun locationClear(ref: DeviceRef) { - nodeRegistry.activeDevices.getNodeFor(ref).locationClear(ref) - } - - fun locationSet(ref: DeviceRef, latitude: Double, longitude: Double) { - nodeRegistry.activeDevices.getNodeFor(ref).locationSet(ref, latitude, longitude) - } - - fun locationRunScenario(ref: DeviceRef, scenarioName: String) { - nodeRegistry.activeDevices.getNodeFor(ref).locationRunScenario(ref, scenarioName) - } - - fun locationStartLocationSequence(ref: DeviceRef, speed: Int, distance: Int, interval: Int, waypoints: List) { - nodeRegistry.activeDevices.getNodeFor(ref).locationStartLocationSequence(ref, speed, distance, interval, waypoints) - } fun isReady(): Boolean { return nodeRegistry.getInitialRegistrationComplete() @@ -373,30 +153,14 @@ class DeviceManager( return nodeRegistry.activeDevices.getNodeFor(ref).pullFile(ref, dataPath) } - fun pullFile(ref: DeviceRef, path: Path): ByteArray { - return nodeRegistry.activeDevices.getNodeFor(ref).pullFile(ref, path) - } - fun pushFile(ref: DeviceRef, fileName: String, data: ByteArray, bundleId: String) { nodeRegistry.activeDevices.getNodeFor(ref).pushFile(ref, fileName, data, bundleId) } - fun pushFile(ref: DeviceRef, data: ByteArray, path: Path) { - nodeRegistry.activeDevices.getNodeFor(ref).pushFile(ref, data, path) - } - - fun deleteFile(ref: DeviceRef, path: Path) { - nodeRegistry.activeDevices.getNodeFor(ref).deleteFile(ref, path) - } - fun setEnvironmentVariables(ref: DeviceRef, envs: Map) { nodeRegistry.activeDevices.getNodeFor(ref).setEnvironmentVariables(ref, envs) } - fun getEnvironmentVariable(ref: DeviceRef, variableName: String): String { - return nodeRegistry.activeDevices.getNodeFor(ref).getEnvironmentVariable(ref, variableName) - } - fun resetMedia(ref: DeviceRef) { nodeRegistry.activeDevices.getNodeFor(ref).resetMedia(ref) } @@ -405,46 +169,10 @@ class DeviceManager( return nodeRegistry.activeDevices.getNodeFor(ref).listMedia(ref) } - fun listPhotoData(ref: DeviceRef): List { - return nodeRegistry.activeDevices.getNodeFor(ref).listPhotoData(ref) - } - fun addMedia(ref: DeviceRef, fileName: String, data: ByteArray) { nodeRegistry.activeDevices.getNodeFor(ref).addMedia(ref, fileName, data) } - fun syslog(ref: DeviceRef): File { - return nodeRegistry.activeDevices.getNodeFor(ref).syslog(ref) - } - - fun instrumentationAgentLog(ref: DeviceRef): File { - return nodeRegistry.activeDevices.getNodeFor(ref).instrumentationAgentLog(ref) - } - - fun deleteInstrumentationAgentLog(ref: DeviceRef) { - nodeRegistry.activeDevices.getNodeFor(ref).deleteInstrumentationAgentLog(ref) - } - - fun appiumServerLog(ref: DeviceRef): File { - return nodeRegistry.activeDevices.getNodeFor(ref).appiumServerLog(ref) - } - - fun deleteAppiumServerLog(ref: DeviceRef) { - nodeRegistry.activeDevices.getNodeFor(ref).deleteAppiumServerLog(ref) - } - - fun syslogDelete(ref: DeviceRef) { - nodeRegistry.activeDevices.getNodeFor(ref).syslogDelete(ref) - } - - fun syslogStart(ref: DeviceRef, sysLogCaptureOptions: SysLogCaptureOptions) { - nodeRegistry.activeDevices.getNodeFor(ref).syslogStart(ref, sysLogCaptureOptions) - } - - fun syslogStop(ref: DeviceRef) { - nodeRegistry.activeDevices.getNodeFor(ref).syslogStop(ref) - } - fun getDiagnostic(ref: DeviceRef, type: DiagnosticType, query: DiagnosticQuery): Diagnostic { return nodeRegistry.activeDevices.getNodeFor(ref).getDiagnostic(ref, type, query) } @@ -453,75 +181,6 @@ class DeviceManager( nodeRegistry.activeDevices.getNodeFor(ref).resetDiagnostic(ref, type) } - fun installApplication(ref: String, dto: AppBundleDto) { - nodeRegistry.activeDevices.getNodeFor(ref).installApplication(ref, dto) - } - - fun appInstallationStatus(ref: String): Map { - return nodeRegistry.activeDevices.getNodeFor(ref).appInstallationStatus(ref) - } - - fun deployApplication(dto: AppBundleDto) { - val marker = MapEntriesAppendingMarker(mapOf("operation" to "app_deploy")) - val appBundle = acquireBundle(dto, marker) - - logger.debug(marker, "Starting to deploy application ${dto.appUrl}") - - val nodeWrappers = nodeRegistry.getAll() - val executor = Executors.newFixedThreadPool(nodeWrappers.size) - val tasks = mutableListOf>() - nodeWrappers.forEach { nodeWrapper -> - val task: Future<*> = executor.submit { - nodeWrapper.node.deployApplication(appBundle) - } - tasks.add(task) - } - executor.shutdown() - - tasks.forEach { it.get() } - - try { - executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); - } catch (e: InterruptedException) { - println("Failed to awaitTermination while deploying application binary simulator hosts due to issue. ${e.javaClass.name}, ${e.message}") - } - - logger.debug(marker, "Successfully deployed application ${dto.appUrl}") - } - - private fun acquireBundle(dto: AppBundleDto, marker: MapEntriesAppendingMarker): ApplicationBundle { - val appBundle = ApplicationBundle(URL(dto.appUrl)) - downloadApplicationBinary(marker, appBundle) - appBundle.unpack(logger, marker) - return appBundle - } - - private fun downloadApplicationBinary(marker: MapEntriesAppendingMarker, appBundle: ApplicationBundle) { - var size: Long = 0 - val nanos = measureNanoTime { - logger.debug(marker, "Downloading app bundle to cache ${appBundle.appUrl}. Url: ${appBundle.appUrl}") - try { - logger.info(marker, "Cleaning out local application cache at ${appConfig.appBundleCachePath.absolutePath}") - - appConfig.appBundleCachePath.mkdirs() - appConfig.appBundleCachePath.listFiles().forEach { it.deleteRecursively() } - - logger.info(marker, "Cleaning out local application cache at ${appConfig.appBundleCachePath.absolutePath} is done") - } catch (t: Throwable) { - logger.error(marker, "Cleaning out local application cache at ${appConfig.appBundleCachePath.absolutePath} failed! Error: ${t.message}", t) - } - appBundle.downloadApp(logger, marker) - size = appBundle.bundleZip.length() - } - val seconds = TimeUnit.NANOSECONDS.toSeconds(nanos) - val measurement = mutableMapOf( - "action_name" to "download_application", - "duration" to seconds, - "app_size" to size.shr(20) // Bytes to Megabytes - ) - logger.debug(MapEntriesAppendingMarker(measurement), "Successfully downloaded application ${appBundle.appUrl} size: $size bytes. Took $seconds seconds") - } - fun updateApplicationPlist(deviceRef: String, plistEntry: PlistEntryDTO) { return nodeRegistry.activeDevices.getNodeFor(deviceRef).updateApplicationPlist(deviceRef, plistEntry) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IHostFactory.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IHostFactory.kt index 17d50e99..ae2f94b5 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IHostFactory.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IHostFactory.kt @@ -1,8 +1,8 @@ package com.badoo.automation.deviceserver.host.management import com.badoo.automation.deviceserver.NodeConfig -import com.badoo.automation.deviceserver.host.IDeviceNode +import com.badoo.automation.deviceserver.host.ISimulatorsNode interface IHostFactory { - fun getHostFromConfig(config: NodeConfig): IDeviceNode -} + fun getHostFromConfig(config: NodeConfig): ISimulatorsNode +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt index aee40ef4..c11775ac 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt @@ -1,11 +1,9 @@ package com.badoo.automation.deviceserver.host.management -import com.badoo.automation.deviceserver.ApplicationConfiguration import com.badoo.automation.deviceserver.LogMarkers import com.badoo.automation.deviceserver.host.IRemote -import com.badoo.automation.deviceserver.host.management.XcodeVersion.Companion.REQUIRED_XCODE_VERSION +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl import com.badoo.automation.deviceserver.ios.simulator.periodicTasksPool -import com.badoo.automation.deviceserver.util.WdaSimulatorBundles import net.logstash.logback.marker.MapEntriesAppendingMarker import org.slf4j.LoggerFactory import java.io.File @@ -15,72 +13,38 @@ import java.util.concurrent.TimeUnit interface ISimulatorHostChecker { fun checkPrerequisites() - fun createDirectories() fun cleanup() fun setupHost() fun killDiskCleanupThread() fun copyWdaBundleToHost() - fun copyTestHelperBundleToHost() - fun copyVideoRecorderHelperToHost() + val isWdaBundleDeployed: Boolean } class SimulatorHostChecker( val remote: IRemote, private val diskCleanupInterval: Duration = Duration.ofMinutes(15), - private val wdaSimulatorBundles: WdaSimulatorBundles, - private val remoteTestHelperAppRoot: File, + private val wdaBundle: File, + private val remoteWdaBundleRoot: File, private val fbsimctlVersion: String, - private val shutdownSimulators: Boolean, - private val remoteVideoRecorder: File + private val shutdownSimulators: Boolean ) : ISimulatorHostChecker { + override val isWdaBundleDeployed: Boolean + get() = remote.execIgnoringErrors(listOf("test", "-d", remoteWdaBundleRoot.absolutePath)).isSuccess + private val logger = LoggerFactory.getLogger(javaClass.simpleName) private val logMarker = MapEntriesAppendingMarker(mapOf( LogMarkers.HOSTNAME to remote.hostName )) private lateinit var cleanUpTask: ScheduledFuture<*> - private val applicationConfiguration = ApplicationConfiguration() - override fun createDirectories() { - remote.shell("mkdir -p ${applicationConfiguration.simulatorBackupPath}") - } override fun copyWdaBundleToHost() { logger.debug(logMarker, "Setting up remote node: copying WebDriverAgent to node ${remote.hostName}") - - val remoteBundleRoot = wdaSimulatorBundles.webDriverAgentBundle.bundlePath(remote.isLocalhost()).parent - remote.rm(remoteBundleRoot) - remote.execIgnoringErrors(listOf("/bin/mkdir", "-p", remoteBundleRoot)) - - remote.scpToRemoteHost(wdaSimulatorBundles.deviceAgentBundle.bundlePath(true).absolutePath, remoteBundleRoot) - remote.scpToRemoteHost(wdaSimulatorBundles.webDriverAgentBundle.bundlePath(true).absolutePath, remoteBundleRoot) - } - - override fun copyTestHelperBundleToHost() { - logger.debug(logMarker, "Setting up remote node: copying TestHelper app to node ${remote.hostName}") - val testHelperAppBundle = File(remoteTestHelperAppRoot, "TestHelper.app") - - if (!testHelperAppBundle.exists()) { - logger.error(logMarker, "Failed to copy TestHelper app to node ${remote.hostName}. TestHelper app does not exist") - return - } - - remote.rm(testHelperAppBundle.absolutePath) - remote.execIgnoringErrors(listOf("/bin/mkdir", "-p", remoteTestHelperAppRoot.absolutePath)) - remote.scpToRemoteHost(testHelperAppBundle.absolutePath, remoteTestHelperAppRoot.absolutePath) - } - - override fun copyVideoRecorderHelperToHost() { - logger.debug(logMarker, "Setting up remote node: Copying Video recorder helper to node ${remote.hostName}") - - if (!remoteVideoRecorder.exists()) { - logger.error(logMarker, "Failed to copy Video recorder to node ${remote.hostName}. Video recorder does not exist") - return - } - - remote.rm(remoteVideoRecorder.absolutePath) - remote.execIgnoringErrors(listOf("/bin/mkdir", "-p", remoteVideoRecorder.parent)) - remote.scpToRemoteHost(remoteVideoRecorder.absolutePath, remoteVideoRecorder.absolutePath) - remote.execIgnoringErrors(listOf("/bin/chmod", "555", remoteVideoRecorder.absolutePath)) + remote.rsync( + wdaBundle.absolutePath, + remoteWdaBundleRoot.absolutePath, + setOf("-r", "--delete") + ) } override fun killDiskCleanupThread() { @@ -91,15 +55,19 @@ class SimulatorHostChecker( override fun checkPrerequisites() { val xcodeOutput = remote.execIgnoringErrors(listOf("xcodebuild", "-version")) - logger.info(logMarker, "Using default Xcode version: ${xcodeOutput.stdOut.trim().replace("\n", " ")}") val xcodeVersion = XcodeVersion.fromXcodeBuildOutput(xcodeOutput.stdOut) - if (xcodeVersion < REQUIRED_XCODE_VERSION) { - logger.error(logMarker, "Expecting Xcode $REQUIRED_XCODE_VERSION or higher, but it is $xcodeVersion") + if (xcodeVersion < XcodeVersion(9, 0)) { + throw RuntimeException("Expecting Xcode 9 or higher, but received $xcodeVersion. $xcodeOutput") } + // temp solution, prereq should be satisfied without having to switch anything + val rv = remote.execIgnoringErrors(listOf("/usr/local/bin/brew", "switch", "fbsimctl", fbsimctlVersion), env = mapOf("RUBYOPT" to "")) + if (!rv.isSuccess) { + logger.warn(logMarker, "fbsimctl switch failed, see: $rv") + } - val fbsimctlPath = remote.execIgnoringErrors(listOf("readlink", remote.fbsimctl.fbsimctlBinary )).stdOut + val fbsimctlPath = remote.execIgnoringErrors(listOf("readlink", FBSimctl.FBSIMCTL_BIN )).stdOut val match = Regex("/fbsimctl/([-.\\w]+)/bin/fbsimctl").find(fbsimctlPath) ?: throw RuntimeException("Could not read fbsimctl version from $fbsimctlPath") val actualFbsimctlVersion = match.groupValues[1] @@ -109,65 +77,44 @@ class SimulatorHostChecker( } override fun cleanup() { - try { - logger.info(logMarker, "Will shutdown booted simulators") - remote.fbsimctl.shutdownAllBooted() - logger.info(logMarker, "Done shutting down booted simulators") - logger.info(logMarker, "Will kill abandoned long living fbsimctl processes") - remote.pkill(remote.fbsimctl.fbsimctlBinary, true) - } catch (e: Exception) { - logger.warn(logMarker, "Failed to shutdown simulator because: ${e.javaClass}: message: [${e.message}]") + if (shutdownSimulators) { + cleanupSimulators() + cleanupSimulatorServices() } try { - logger.info(logMarker, "Will shutdown iproxy and socat") - remote.pkill("/usl/local/bin/iproxy", true) - remote.pkill("/opt/homebrew/bin/iproxy", true) - remote.pkill("/usr/local/bin/socat", true) - remote.pkill("/opt/homebrew/bin/socat", true) + logger.info(logMarker, "Will kill abandoned long living fbsimctl processes") + remote.pkill("/usr/local/bin/fbsimctl", true) + logger.info(logMarker, "Will shutdown booted simulators") + remote.fbsimctl.shutdownAll() + logger.info(logMarker, "Done shutting down booted simulators") } catch (e: Exception) { logger.warn(logMarker, "Failed to shutdown simulator because: ${e.javaClass}: message: [${e.message}]") } - remote.pkill("appium_tmpdir_", true) - - if (shutdownSimulators) { - cleanupSimulators() - cleanupSimulatorServices() - } - val deviceSetsPath = remote.fbsimctl.defaultDeviceSet() check(!deviceSetsPath.isBlank()) { "Device sets must not be blank" } // fbsimctl.defaultDeviceSet will throw if empty. but paranoid mode on. - if (!remote.isLocalhost()) { - removeOldFiles(ApplicationConfiguration().appBundleCacheRemotePath.absolutePath, 0) // remove local caches - remote.shell("mkdir -p ${ApplicationConfiguration().appBundleCacheRemotePath.absolutePath}") - } - - // TODO: Use $TMPDIR instead of /private/var/folders/*/*/* val caches = listOf( - "/var/folders/*/*/*/*-*-*/*.app", - "/var/folders/*/*/*/fbsimctl-*", - "/var/folders/*/*/*/videoRecording_*", - "/var/folders/*/*/*/derivedDataDir_*", - "/var/folders/*/*/*/xctestRunDir_*", - "/var/folders/*/*/*/device_agent_log_*", - "/var/folders/*/*/*/appium_tmpdir_*", - "/private/var/tmp/test-session-systemlogs-*.logarchive", - File(ApplicationConfiguration().appBundleCacheRemotePath.absolutePath, "*").absolutePath, + "/private/var/folders/*/*/*/*-*-*/*.app", + "/private/var/folders/*/*/*/fbsimctl-*", "$deviceSetsPath/*/data/Library/Caches/com.apple.mobile.installd.staging/*/*.app" ) - caches.forEach { path -> - removeOldFiles(path, 1) - } - - val cleanUpRunnable = Runnable { - caches.forEach { path -> - removeOldFiles(path, 120) + val cleanUpRunnable: Runnable = object : Runnable { + override fun run() { + caches.forEach { + try { + val r = remote.shell("find $it -maxdepth 0 -mmin +60 -exec rm -rf {} \\;", returnOnFailure = true) // find returns non zero if nothing found + if (!r.isSuccess || r.stdErr.isNotEmpty() || r.stdOut.isNotEmpty()) { + logger.debug(logMarker, "[disc cleaner] $this returned non-empty. Result stdErr: ${r.stdErr}") + } + } catch (e: RuntimeException) { + logger.debug(logMarker, "[disc cleaner] $this got exception while cleaning caches: ${e.message}", e) + } + } } } - cleanUpTask = periodicTasksPool.scheduleWithFixedDelay( cleanUpRunnable, 0, @@ -175,20 +122,6 @@ class SimulatorHostChecker( TimeUnit.MINUTES) } - private fun removeOldFiles(path: String, minutes: Int) { - try { - val r = remote.shell( - "find $path -maxdepth 0 -mmin +$minutes -exec rm -rf {} \\;", - returnOnFailure = true - ) // find returns non zero if nothing found - if (!r.isSuccess && r.exitCode != 1 && (r.stdErr.trim().isNotEmpty() || r.stdOut.trim().isNotEmpty())) { - logger.debug(logMarker, "[disc cleaner] @ ${remote.publicHostName} returned non-empty. Result: ${r}") - } - } catch (e: RuntimeException) { - logger.debug(logMarker, "[disc cleaner] $this got exception while cleaning caches: ${e.message}", e) - } - } - private fun cleanupSimulators() { remote.pkill("Simulator.app", false) // Simulator UI application remote.pkill("launchd_sim", false) // main process for running simulator @@ -204,15 +137,11 @@ class SimulatorHostChecker( override fun setupHost() { // disable node hardware keyboard, i.e. use on-screen one remote.execIgnoringErrors("/usr/bin/defaults write com.apple.iphonesimulator ConnectHardwareKeyboard -bool false".split(" ")) - remote.execIgnoringErrors("/usr/bin/defaults write com.apple.iphonesimulator EnableKeyboardSync -bool false".split(" ")) remote.execIgnoringErrors("/usr/bin/defaults write com.apple.iphonesimulator PasteboardAutomaticSync -bool false".split(" ")) - remote.execIgnoringErrors("/usr/bin/defaults write com.apple.iphonesimulator StartLastDeviceOnLaunch -bool false".split(" ")) - remote.execIgnoringErrors("/usr/bin/defaults write com.apple.iphonesimulator DetachOnWindowClose -bool true".split(" ")) // disable simulator location remote.execIgnoringErrors("/usr/bin/defaults write com.apple.iphonesimulator LocationMode \"3101\"".split(" ")) remote.execIgnoringErrors("/usr/bin/defaults write com.apple.iphonesimulator ShowChrome -bool false".split(" ")) - remote.execIgnoringErrors("/usr/bin/defaults write com.apple.iphonesimulator ShowSingleTouches -bool true".split(" ")) // other options that might be useful are: // EnableKeyboardSync = 0; // GraphicsQualityOverride = 10; diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt index 4bd7bed1..cd31b494 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt @@ -74,22 +74,16 @@ class NodeRegistrar( } @Synchronized - fun restartNodesGracefully(isParallel: Boolean, shouldReboot: Boolean, forceReboot: Boolean): Boolean { + fun restartNodesGracefully(isParallel: Boolean): Boolean { val job = restartingJob if (job == null || job.isDone) { val executor = Executors.newSingleThreadExecutor() restartingJob = executor.submit { - try { - nodeRestarter.restartNodeWrappers( - nodeRegistry.getAll(), - isParallel, - shouldReboot, - forceReboot - ) - } catch (t: Throwable) { - logger.error("Failed to reboot all simulator hosts due to issue. ${t.javaClass.name}, ${t.message}", t) - } + nodeRestarter.restartNodeWrappers( + nodeRegistry.getAll(), + isParallel + ) } executor.shutdown() diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt index 4e39b95b..31f13caa 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt @@ -3,9 +3,7 @@ package com.badoo.automation.deviceserver.host.management import com.badoo.automation.deviceserver.LogMarkers import com.badoo.automation.deviceserver.data.DesiredCapabilities import com.badoo.automation.deviceserver.data.DeviceDTO -import com.badoo.automation.deviceserver.data.DeviceRef -import com.badoo.automation.deviceserver.host.IDeviceNode -import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException +import com.badoo.automation.deviceserver.host.ISimulatorsNode import com.badoo.automation.deviceserver.host.management.errors.NoAliveNodesException import com.badoo.automation.deviceserver.host.management.errors.NoNodesRegisteredException import com.badoo.automation.deviceserver.ios.ActiveDevices @@ -51,10 +49,10 @@ class NodeRegistry(val activeDevices: ActiveDevices = ActiveDevices()) { return nodeWrappers } - fun getAlive(): Set { + private fun getAlive(): Set { val filteredStream: Stream = nodeWrappers - .parallelStream() .filter { it.isEnabled } + .parallelStream() .filter { it.isAlive() } return filteredStream.collect(toSet()) } @@ -69,36 +67,24 @@ class NodeRegistry(val activeDevices: ActiveDevices = ActiveDevices()) { return mapOf("total" to count) } - fun hasCapacity(desiredCapabilities: DesiredCapabilities): Boolean { - val remainingCapacity = nodeWrappers - .parallelStream() - .filter { it.isEnabled } - .filter { it.isAlive() } - .map { it.node.capacityRemaining(desiredCapabilities) } - .reduce(0F, java.lang.Float::sum) - - return remainingCapacity > 0F - } - - fun createDeviceAsync(desiredCapabilities: DesiredCapabilities, deviceTimeout: Duration, userId: String?): DeviceDTO { + fun createDeviceAsync(desiredCapabilities: DesiredCapabilities, userId: String?): DeviceDTO { if (getAll().isEmpty()) { throw NoNodesRegisteredException("No nodes are registered to create a device") } - val node: IDeviceNode = getAlive() + val node: ISimulatorsNode = getAlive() .map { wrapper -> wrapper.node } .shuffled() .maxBy { node -> node.capacityRemaining(desiredCapabilities) } ?: throw NoAliveNodesException("No alive nodes are available to create device at the moment") val dto = node.createDeviceAsync(desiredCapabilities) - logger.info("Create device dto ${dto} ") val logMarker: Marker = MapEntriesAppendingMarker(mutableMapOf( LogMarkers.DEVICE_REF to dto.ref, LogMarkers.UDID to dto.info.udid )) - logger.info(logMarker, "Create device started, register with timeout ${deviceTimeout.seconds} secs") + logger.info(logMarker, "Device is created ${dto.info.model} (${dto.info.os}) ${dto.info.arch}. Ref: ${dto.ref}") activeDevices.registerDevice(dto.ref, node, userId) @@ -111,22 +97,4 @@ class NodeRegistry(val activeDevices: ActiveDevices = ActiveDevices()) { runBlocking { list.forEach { it.join() } } nodeWrappers.clear() } - - fun deleteReleaseDevice(ref: DeviceRef, reason: String) { - try { // using try-catch here not to expose tryGetNodeFor - activeDevices.releaseDevice(ref, reason) - } catch (e: DeviceNotFoundException) { - logger.warn("Skipping $ref release because no node knows about it") - return - } - } - - fun deleteDevice(ref: DeviceRef, reason: String) { - try { - activeDevices.deleteDevice(ref, reason) - } catch (e: DeviceNotFoundException) { - logger.warn("Skipping $ref delete because no node knows about it") - return - } - } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRestarter.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRestarter.kt index 9431fc86..586cb16b 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRestarter.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRestarter.kt @@ -1,104 +1,46 @@ package com.badoo.automation.deviceserver.host.management -import com.badoo.automation.deviceserver.host.IDeviceNode +import com.badoo.automation.deviceserver.host.ISimulatorsNode import com.badoo.automation.deviceserver.ios.SessionEntry import org.slf4j.LoggerFactory -import java.time.Duration -import java.util.concurrent.Executors -import java.util.concurrent.Future -import java.util.concurrent.TimeUnit class NodeRestarter( private val nodeRegistry: NodeRegistry ) { private val logger = LoggerFactory.getLogger(javaClass.simpleName) - private val activeSessionsCheckInterval = Duration.ofSeconds(30) fun restartNodeWrappers( nodes: Set, - isParallel: Boolean, - shouldReboot: Boolean, - forceReboot: Boolean + isParallel: Boolean ) { - if (isParallel) { + val nodesToRestart = if (isParallel) { logger.info("Going to restart nodes in parallel.") - - if (nodes.isEmpty()) { - logger.debug("Unable to restart empty list of nodes") - return - } - - val executor = Executors.newFixedThreadPool(nodes.size) - val tasks = mutableListOf>() - nodes.forEach { nodeWrapper -> - val task: Future<*> = executor.submit { - try { - rebootSimulatorHost(nodeWrapper, forceReboot, shouldReboot) - } catch (t: Throwable) { - logger.error("Failed to reboot simulator host ${nodeWrapper.node.publicHostName} due to issue. ${t.javaClass.name}, ${t.message}", t) - } - } - tasks.add(task) - } - executor.shutdown() - - tasks.forEach { it.get() } - - try { - executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); - } catch (e: InterruptedException) { - println("Failed to awaitTermination while reboot all simulator hosts due to issue. ${e.javaClass.name}, ${e.message}") - } + nodes.parallelStream() } else { logger.info("Going to restart nodes sequentially.") - nodes.forEach { nodeWrapper -> - try { - rebootSimulatorHost(nodeWrapper, forceReboot, shouldReboot) - } catch (t: Throwable) { - logger.error("Failed to reboot simulator host ${nodeWrapper.node.publicHostName} due to issue. ${t.javaClass.name}, ${t.message}", t) - } - } - } - - } - - private fun rebootSimulatorHost(nodeWrapper: NodeWrapper, forceReboot: Boolean, shouldReboot: Boolean) { - val startTime = System.nanoTime() - logger.info("Going to restart simulator host ${nodeWrapper.node.publicHostName}") - nodeWrapper.disable() - - if (forceReboot) { - clearActiveSessions(nodeWrapper.node) - } else if (activeSessions(nodeWrapper.node).isNotEmpty()) { - logger.error("Failed to re-start node $nodeWrapper as it has active sessions with infinite timeout") - nodeWrapper.enable() - return + nodes.stream() } - nodeWrapper.stop() + nodesToRestart.forEach { nodeWrapper -> + if (activeSessions(nodeWrapper.node).isNotEmpty()) { + logger.error("Failed to re-start node $nodeWrapper as it has active sessions with infinite timeout") + return@forEach + } - if (shouldReboot) { - nodeWrapper.reboot() - } + nodeWrapper.disable() + nodeWrapper.stop() - if (nodeWrapper.start()) { - nodeWrapper.startPeriodicHealthCheck() - nodeWrapper.enable() - } else { - logger.error("Failed to re-start node $nodeWrapper") - nodeRegistry.removeIfPresent(nodeWrapper) + if (nodeWrapper.start()) { + nodeWrapper.startPeriodicHealthCheck() + nodeWrapper.enable() + } else { + logger.error("Failed to re-start node $nodeWrapper") + nodeRegistry.removeIfPresent(nodeWrapper) + } } - - val elapsedSeconds = TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - startTime) - logger.info("Successfully restarted simulator host ${nodeWrapper.node.publicHostName}. Took time ${elapsedSeconds} seconds") } - private fun activeSessions(node: IDeviceNode): Collection { + private fun activeSessions(node: ISimulatorsNode): Collection { return nodeRegistry.activeDevices.activeDevicesByNode(node.publicHostName).values } - - private fun clearActiveSessions(node: IDeviceNode) { - val sessions = activeSessions(node).map { it.ref } - return nodeRegistry.activeDevices.releaseDevices(sessions.toList(), "Reboot of all simulator hosts") - } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeWrapper.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeWrapper.kt index 3a2c5c3f..7236d396 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeWrapper.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeWrapper.kt @@ -2,7 +2,7 @@ package com.badoo.automation.deviceserver.host.management import com.badoo.automation.deviceserver.LogMarkers.Companion.HOSTNAME import com.badoo.automation.deviceserver.NodeConfig -import com.badoo.automation.deviceserver.host.IDeviceNode +import com.badoo.automation.deviceserver.host.ISimulatorsNode import net.logstash.logback.marker.MapEntriesAppendingMarker import org.slf4j.LoggerFactory import java.time.Duration @@ -30,14 +30,13 @@ class NodeWrapper( private val lock = ReentrantLock(true) @Volatile private var isStarted = false private var healthCheckPeriodicTask: Future<*>? = null - val node: IDeviceNode by lazy { hostFactory.getHostFromConfig(config) } // workaround for SSH connection issues + val node: ISimulatorsNode = hostFactory.getHostFromConfig(config) var lastError: Exception? = null @Volatile var isEnabled: Boolean = true private set - @Volatile private var isReachable: Boolean = false - fun isAlive(): Boolean = isStarted && isReachable + fun isAlive(): Boolean = isStarted && node.isReachable() && node.isNodePrepared override fun toString(): String = "NodeWrapper for ${config.publicHost}" @@ -49,11 +48,9 @@ class NodeWrapper( } if (!node.isReachable()) { - isReachable = false logger.error(logMarker, "Failed to start the node from config: $config. Reason: unreachable node: $node.") return false } - isReachable = true logger.info(logMarker, "Starting the node from config: $config") try { @@ -84,10 +81,6 @@ class NodeWrapper( } } - fun reboot() { - node.reboot() - } - fun disable() { isEnabled = false logger.info(logMarker, "Disabled $this") @@ -105,19 +98,17 @@ class NodeWrapper( val executor = Executors.newSingleThreadExecutor() var healthCheckAttempts = 0 - healthCheckPeriodicTask = executor.submit { + healthCheckPeriodicTask = executor.submit({ while (!Thread.currentThread().isInterrupted) { Thread.sleep(nodeCheckInterval.toMillis()) - if (isStarted && node.isReachable()) { + if (isAlive()) { healthCheckAttempts = 0 - isReachable = true } else { healthCheckAttempts++ logger.debug(logMarker, "Node $this is down for last $healthCheckAttempts tries") if (healthCheckAttempts >= maxHealthCheckAttempts) { - isReachable = false registry.removeIfPresent(this) val message = "Removing node [${node.remoteAddress}]: cannot reach the node for $maxHealthCheckAttempts tries" @@ -127,7 +118,7 @@ class NodeWrapper( } } - } + }) executor.shutdown() } @@ -135,4 +126,4 @@ class NodeWrapper( healthCheckPeriodicTask?.cancel(true) healthCheckPeriodicTask = null } -} +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/PortAllocator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/PortAllocator.kt index 4bd76177..f9d32ff1 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/PortAllocator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/PortAllocator.kt @@ -9,17 +9,15 @@ class PortAllocator(min: Int = PORT_RANGE_START, max: Int = PORT_RANGE_END) { const val PORT_RANGE_END = 42507 } - private val ports: MutableSet = IntRange(min, max).toMutableSet() + private var ports: Set = IntRange(min, max).toSet() fun allocateDAP(): DeviceAllocatedPorts { - val take = allocate(5) - return DeviceAllocatedPorts(take[0], take[1], take[2], take[3], take[4]) + val ports = allocate(4) + return DeviceAllocatedPorts(ports[0], ports[1], ports[2], ports[3]) } - fun deallocateDAP(allocatedPorts: DeviceAllocatedPorts) { - synchronized(this) { - ports.addAll(allocatedPorts.toSet()) - } + fun deallocateDAP(dap: DeviceAllocatedPorts) { + deallocate(listOf(dap.calabashPort, dap.fbsimctlPort, dap.wdaPort, dap.mjpegServerPort)) } fun available(): Int { @@ -27,14 +25,19 @@ class PortAllocator(min: Int = PORT_RANGE_START, max: Int = PORT_RANGE_END) { } private fun allocate(entries: Int): List { - //TODO: Check with "netstat -nat|grep LISTEN|grep -v tcp6" if ports are already occupied synchronized(this) { if (ports.size < entries) { throw RuntimeException("No more ports to allocate") } - val takenPorts = ports.take(entries) - ports.removeAll(takenPorts) - return takenPorts + val take = ports.take(entries) + ports = ports.subtract(take) + return take + } + } + + private fun deallocate(entries: List) { + synchronized(this) { + ports = ports.plus(entries) } } -} \ No newline at end of file +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersion.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersion.kt index 4bdaabee..2b867bf3 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersion.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersion.kt @@ -2,10 +2,9 @@ package com.badoo.automation.deviceserver.host.management data class XcodeVersion(val major: Int, val minor: Int):Comparable { companion object { - val REQUIRED_XCODE_VERSION = XcodeVersion(14, 2) fun fromXcodeBuildOutput(output: String): XcodeVersion { val regex = Regex("Xcode (\\d+)\\.(\\d+)(\\.(\\d+))?") - val versionLine = output.lines().first { it.startsWith("Xcode ") } + val versionLine = output.lines().first() val match = regex.matchEntire(versionLine) match?.destructured?.let { diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ZombieReaper.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ZombieReaper.kt deleted file mode 100644 index b9cb7937..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ZombieReaper.kt +++ /dev/null @@ -1,85 +0,0 @@ -package com.badoo.automation.deviceserver.host.management - -import com.badoo.automation.deviceserver.command.ShellCommand -import com.sun.jna.ptr.IntByReference -import com.zaxxer.nuprocess.internal.LibC -import net.logstash.logback.marker.MapEntriesAppendingMarker -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import java.util.concurrent.Executors -import java.util.concurrent.Future -import java.util.concurrent.TimeUnit -import kotlin.streams.toList - -class ZombieReaper { - private val logger: Logger = LoggerFactory.getLogger(javaClass.simpleName) - private val executor = Executors.newScheduledThreadPool(1) - private val commandExecutor = ShellCommand() - - fun launchReapingZombies() { - executor.scheduleWithFixedDelay( - { reapZombies(findZombies()) }, - 60L, - 60L, - TimeUnit.SECONDS - ) - } - - private fun reapZombies(pids: List) { - if (pids.isEmpty()) { - logger.debug("No zombies to reap") - return - } - - val executor = Executors.newFixedThreadPool(pids.size) - val tasks = mutableListOf>() - pids.forEach { pid -> - val task: Future<*> = executor.submit { - reapZombie(pid) - } - tasks.add(task) - } - executor.shutdown() - - tasks.forEach { it.get() } - - try { - executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); - } catch (e: InterruptedException) { - println("Failed to awaitTermination while reaping zombiez due to issue. ${e.javaClass.name}, ${e.message}") - } - } - - private fun reapZombie(pid: Int) { - logger.debug("Reaping zombie process $pid.") - try { - val exitCode = IntByReference() - val waitpidRC = LibC.waitpid(pid, exitCode, 0) - val status = exitCode.value - val wExitStatus = LibC.WEXITSTATUS(status) - val cleanExit = waitpidRC == pid && LibC.WIFEXITED(status) && wExitStatus == 0 - logger.debug(MapEntriesAppendingMarker(mapOf("zombiePID" to pid)), "Reaped zombie process $pid. Exit status: $wExitStatus. Exit status is clean: $cleanExit.") - } catch (t: Throwable) { - logger.error("Failed to reap zombie process $pid. Error: ${t.javaClass}, ${t.message}", t) - } - } - - private fun findZombies(): List { - return try { - val result = commandExecutor.exec(listOf("/bin/ps", "axo", "pid,stat,command"), returnFailure = true) - val zombies = result.stdOut.lines().filter { it.contains("Z") } - val zombiesPids = zombies.map { - it.trim().split(" ").first().trim().toInt() - } - - val childrenPids = ProcessHandle.current().children().map { it.pid().toInt() } - val childrenZombiesPids = childrenPids.filter { zombiesPids.contains(it) }.toList() - - logger.debug(MapEntriesAppendingMarker(mapOf("zombies" to childrenZombiesPids.size)), "Found ${childrenZombiesPids.size} zombie processes: ${childrenZombiesPids.joinToString(",")}") - childrenZombiesPids - } catch (t: Throwable) { - logger.error("Failed to find zombie processes. Error: ${t.javaClass}, ${t.message}", t) - listOf() - } - } -} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt index ad28d29d..370cc85f 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt @@ -3,19 +3,17 @@ package com.badoo.automation.deviceserver.ios import com.badoo.automation.deviceserver.data.DeviceDTO import com.badoo.automation.deviceserver.data.DeviceRef import com.badoo.automation.deviceserver.data.NodeRef -import com.badoo.automation.deviceserver.host.IDeviceNode +import com.badoo.automation.deviceserver.host.ISimulatorsNode import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException import org.slf4j.LoggerFactory import java.time.Duration import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.Executors -import java.util.concurrent.Future import java.util.concurrent.TimeUnit data class SessionEntry( - val ref: DeviceRef, - val node: IDeviceNode, - val userId: String? + val ref: DeviceRef, + val node: ISimulatorsNode, + val userId: String? ) class ActiveDevices( @@ -51,26 +49,21 @@ class ActiveDevices( return list } - fun registerDevice(ref: DeviceRef, node: IDeviceNode, userId: String?) { + fun registerDevice(ref: DeviceRef, node: ISimulatorsNode, userId: String?) { devices[ref] = SessionEntry(ref, node, userId) } - fun unregisterNodeDevices(node: IDeviceNode) { + fun unregisterNodeDevices(node: ISimulatorsNode) { devices.entries .filter { it.value.node == node } .forEach { unregisterDeleteDevice(it.key) } } - private fun tryGetNodeFor(ref: DeviceRef): IDeviceNode? { - val sessionEntry = devices[ref] - if (sessionEntry != null) { - return sessionEntry.node - } else { - return null - } + private fun tryGetNodeFor(ref: DeviceRef): ISimulatorsNode? { + return devices[ref]?.node } - fun getNodeFor(ref: DeviceRef): IDeviceNode { + fun getNodeFor(ref: DeviceRef): ISimulatorsNode { val node = tryGetNodeFor(ref) if (node == null) { throw DeviceNotFoundException("Device [$ref] not found in [$sessionId] activeDevices") @@ -90,48 +83,18 @@ class ActiveDevices( } fun releaseDevice(ref: DeviceRef, reason: String) { - logger.debug("Releasing a device due to reason: ${reason}") val session = sessionByRef(ref) session.node.deleteRelease(session.ref, reason) unregisterDeleteDevice(session.ref) } - fun deleteDevice(ref: DeviceRef, reason: String) { - logger.debug("Deleting a device due to reason: ${reason}") - val session = sessionByRef(ref) - session.node.deleteDevice(session.ref, reason) - unregisterDeleteDevice(session.ref) - } - fun releaseDevices(entries: List, reason: String) { - logger.debug("Releasing active devices: ${entries.joinToString(", ")}") - if (entries == null || entries.isEmpty()) { - logger.debug("Nothing to release as active devices list is empty") - return - } - - val size: Int = entries.size - val executor = Executors.newFixedThreadPool(size) - val tasks = mutableListOf>() - entries.forEach { - val task: Future<*> = executor.submit { - try { - releaseDevice(it, reason) - } catch (e: RuntimeException) { - logger.warn("Failed to release device $it", e) - } + entries.parallelStream().forEach { + try { + releaseDevice(it, reason) + } catch (e: RuntimeException) { + logger.warn("Failed to release device $it", e) } - tasks.add(task) - } - - executor.shutdown() - - tasks.forEach { it.get() } - - try { - executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); - } catch (e: InterruptedException) { - println("Failed to awaitTermination while releasing devices due to issue. ${e.javaClass.name}, ${e.message}") } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/DeviceStatus.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/DeviceStatus.kt new file mode 100644 index 00000000..172edb75 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/DeviceStatus.kt @@ -0,0 +1,9 @@ +package com.badoo.automation.deviceserver.ios + +data class DeviceStatus ( + val ready: Boolean, + val wda_status: Boolean, + val fbsimctl_status: Boolean, + val state: String, + val last_error: Exception? +) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/IDevice.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/IDevice.kt deleted file mode 100644 index 41d489f7..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/IDevice.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.badoo.automation.deviceserver.ios - -import com.badoo.automation.deviceserver.data.* -import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlAppInfo -import com.badoo.automation.deviceserver.ios.simulator.video.VideoRecorder -import com.badoo.automation.deviceserver.util.AppInstaller -import com.badoo.automation.deviceserver.util.InstallResult -import java.io.File -import java.net.URI -import java.net.URL -import java.util.concurrent.Future - -interface IDevice { - fun prepareAsync() - val calabashPort: Int - val deviceInfo: DeviceInfo - val mjpegServerPort: Int - val appiumPort: Int - val wdaEndpoint: URI - val fbsimctlEndpoint: URI - val calabashEndpoint: URI - val appiumEndpoint: URI - val udid: UDID - val ref: DeviceRef - val deviceState: DeviceState - val videoRecorder: VideoRecorder - fun uninstallApplication(bundleId: String, appInstaller: AppInstaller) - fun status(): SimulatorStatusDTO - val lastException: Exception? - fun lastCrashLog(): CrashLog - fun endpointFor(port: Int): URL - fun release(reason: String) - fun delete(reason: String) - fun installApplication(appInstaller: AppInstaller, appBundleId: String, appBinaryPath: File) - fun getInstallTask(): Future? - - fun appInstallationStatus(): Map { - val task = getInstallTask() - ?: return mapOf( - "task_exists" to false, - "task_complete" to false, - "success" to false - ) - - val status = mutableMapOf( - "task_exists" to true, - "task_complete" to task.isDone, - "success" to (task.isDone && task.get().isSuccess) - ) - - if (task.isDone && !task.get().isSuccess) { - status["error_message"] = "${task.get().errorMessage}" - } - - return status - } - - val instrumentationAgentLog: File - val appiumServerLog: File - fun deleteAppiumServerLog() - val osLog: ISysLog - fun listApps(): List - val isAppiumEnabled: Boolean -} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ISysLog.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ISysLog.kt deleted file mode 100644 index 15d162ed..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ISysLog.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.badoo.automation.deviceserver.ios - -import com.badoo.automation.deviceserver.data.SysLogCaptureOptions -import java.io.File - -interface ISysLog { - val osLogFile: File - val osLogStderr: File - fun truncate(): Boolean - fun content(process: String?): String - fun deleteLogFiles() - fun stopWritingLog() - fun startWritingLog(sysLogCaptureOptions: SysLogCaptureOptions) -} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/WdaClient.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/WdaClient.kt index f8300e34..b13224b5 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/WdaClient.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/WdaClient.kt @@ -19,7 +19,7 @@ class WdaClient( ) { class WdaException(message: String): RuntimeException(message) - private val client: OkHttpClient = CustomHttpClient.client.newBuilder() + private val client: OkHttpClient = CustomHttpClient().client.newBuilder() .connectTimeout(openTimeout.toMillis(), TimeUnit.MILLISECONDS) .readTimeout(readTimeout.toMillis(), TimeUnit.MILLISECONDS) .build() diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt index 9b9478be..a93c1d6b 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt @@ -5,17 +5,15 @@ import com.badoo.automation.deviceserver.LogMarkers import com.badoo.automation.deviceserver.WaitTimeoutError import com.badoo.automation.deviceserver.data.* import com.badoo.automation.deviceserver.host.IRemote -import com.badoo.automation.deviceserver.ios.IDevice -import com.badoo.automation.deviceserver.ios.device.diagnostic.RealDeviceSysLog -import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlAppInfo +import com.badoo.automation.deviceserver.ios.DeviceStatus +import com.badoo.automation.deviceserver.ios.WdaClient import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDeviceState -import com.badoo.automation.deviceserver.ios.proc.* -import com.badoo.automation.deviceserver.ios.simulator.video.FFMPEGVideoRecorder +import com.badoo.automation.deviceserver.ios.proc.WebDriverAgentError +import com.badoo.automation.deviceserver.ios.simulator.video.MJPEGVideoRecorder import com.badoo.automation.deviceserver.ios.simulator.video.VideoRecorder -import com.badoo.automation.deviceserver.util.* -import kotlinx.coroutines.experimental.Job -import kotlinx.coroutines.experimental.delay -import kotlinx.coroutines.experimental.launch +import com.badoo.automation.deviceserver.util.executeWithTimeout +import com.badoo.automation.deviceserver.util.deviceRefFromUDID +import com.badoo.automation.deviceserver.util.pollFor import net.logstash.logback.marker.MapEntriesAppendingMarker import org.slf4j.LoggerFactory import org.slf4j.Marker @@ -26,67 +24,54 @@ import java.nio.file.Files import java.time.Duration import java.util.concurrent.Executors import java.util.concurrent.Future -import java.util.concurrent.locks.ReentrantLock -import kotlin.concurrent.withLock class Device( private val remote: IRemote, - override val deviceInfo: DeviceInfo, - val userPorts: DeviceAllocatedPorts, - private val wdaDeviceBundles: List, - usbProxyFactory: UsbProxyFactory = UsbProxyFactory(remote) -) : IDevice { - override val appiumPort: Int get() = userPorts.appiumPort - override val udid: String = deviceInfo.udid - override val ref: DeviceRef by lazy { - val unsafe = Regex("[^\\-_a-zA-Z\\d]") - "${udid}-${remote.publicHostName}".replace(unsafe, "-") - } - - override val osLog = RealDeviceSysLog(remote, udid) - - private val useFbsimctlProc = ApplicationConfiguration().useFbsimctlProc - - @Volatile - private var useAppium: Boolean = false - - override val isAppiumEnabled get() = useAppium - - private val calabashProxy = usbProxyFactory.create( + val deviceInfo: DeviceInfo, + val allocatedPorts: DeviceAllocatedPorts, + wdaRunnerXctest: File, + usbProxy: UsbProxyFactory = UsbProxyFactory(remote), + configuration: ApplicationConfiguration = ApplicationConfiguration() +) { + val udid: String = deviceInfo.udid + + private val calabashProxy = usbProxy.create( udid = deviceInfo.udid, - localPort = userPorts.calabashPort + localPort = allocatedPorts.calabashPort, + devicePort = CALABASH_PORT ) - private val wdaProxy = usbProxyFactory.create( + private val wdaProxy = usbProxy.create( udid = deviceInfo.udid, - localPort = userPorts.wdaPort + localPort = allocatedPorts.wdaPort, + devicePort = WDA_PORT ) - override val mjpegServerPort = userPorts.mjpegServerPort - private val mjpegProxy = usbProxyFactory.create( + val fbsimctlEndpoint = URI("http://${remote.publicHostName}:${allocatedPorts.fbsimctlPort}/$udid/") + val wdaEndpoint = URI("http://${remote.publicHostName}:${wdaProxy.localPort}") + val calabashPort = calabashProxy.localPort + val mjpegServerPort = allocatedPorts.mjpegServerPort + private val mjpegProxy = usbProxy.create( udid = deviceInfo.udid, - localPort = mjpegServerPort + localPort = mjpegServerPort, + devicePort = mjpegServerPort ) - - override val fbsimctlEndpoint = URI("http://${remote.publicHostName}:${userPorts.fbsimctlPort}/$udid/") - override val calabashEndpoint = URI("http://${remote.publicHostName}:${userPorts.calabashPort}") - override val appiumEndpoint = URI("http://${remote.publicHostName}:${userPorts.appiumPort}") - override val wdaEndpoint = URI("http://${remote.publicHostName}:${wdaProxy.localPort}") - override val calabashPort = calabashProxy.localPort - - override val videoRecorder: VideoRecorder = FFMPEGVideoRecorder( + val videoRecorder: VideoRecorder = MJPEGVideoRecorder( + deviceInfo, remote, + wdaEndpoint, mjpegServerPort, + configuration.videoRecorderFrameRate, deviceRefFromUDID(deviceInfo.udid, remote.publicHostName), deviceInfo.udid ) @Volatile - override var lastException: Exception? = null + var lastException: Exception? = null private set @Volatile - override var deviceState = DeviceState.NONE + var deviceState = DeviceState.NONE private set(value) { val oldState = field field = value @@ -94,26 +79,7 @@ class Device( } private val fbsimctlProc: DeviceFbsimctlProc = DeviceFbsimctlProc(remote, deviceInfo.udid, fbsimctlEndpoint, false) - private val instrumentationAgent = XCTestInstrumentationAgent( - remote = remote, - wdaBundles = wdaDeviceBundles, - deviceInfo = deviceInfo, - wdaEndpoint = wdaEndpoint, - mjpegServerPort = mjpegServerPort, - deviceRef = ref, - isRealDevice = true - ) - - private val appiumServer: AppiumServer = AppiumServer( - remote, - udid, - appiumPort, - userPorts.wdaPort - ) - - override val instrumentationAgentLog get() = instrumentationAgent.deviceAgentLog - override val appiumServerLog get() = appiumServer.appiumServerLog - override fun deleteAppiumServerLog() = appiumServer.deleteAppiumServerLog() + private val webDriverAgent = DeviceWebDriverAgent(remote, wdaRunnerXctest, deviceInfo.udid, wdaEndpoint, wdaProxy.devicePort, mjpegServerPort) private val status = SimulatorStatus() @@ -136,16 +102,15 @@ class Device( override fun toString(): String = "" - override fun status(): SimulatorStatusDTO { + fun status(): DeviceStatus { refreshStatus() - return SimulatorStatusDTO( + return DeviceStatus( ready = status.isReady, state = deviceState.value, // FIXME: why get rid of type here wda_status = status.wdaStatus, - appium_status = status.appiumStatus, fbsimctl_status = status.fbsimctlStatus, - last_error = lastException?.toDTO() + last_error = lastException ) } @@ -154,7 +119,6 @@ class Device( status.fbsimctlStatus = false status.wdaStatus = false - status.appiumStatus = false if (deviceState != DeviceState.CREATED) { return @@ -167,20 +131,22 @@ class Device( return } - val isWdaHealty = instrumentationAgent.isHealthy() - val fbsimctlStatus = if (useFbsimctlProc) fbsimctlProc.isHealthy() else true + val wdaStatus = webDriverAgent.isHealthy() + val fbsimctlStatus = fbsimctlProc.isHealthy() // check if WDA or fbsimctl crashed after being ok for some time - if (isWdaHealty) { + if (wdaStatus) { status.wdaStatusRetries = 0 } else { status.wdaStatusRetries += 1 } - if (status.wdaStatusRetries >= MAX_WDA_STATUS_CHECKS) { + val maxAttempts = 3 + + if (status.wdaStatusRetries >= maxAttempts) { deviceState = DeviceState.FAILED - val message = "${this} WebDriverAgent crashed. Last $MAX_WDA_STATUS_CHECKS health checks failed" + val message = "${this} WebDriverAgent crashed. Last $maxAttempts health checks failed" logger.error(logMarker, message) lastException = RuntimeException(message) } @@ -192,64 +158,28 @@ class Device( lastException = RuntimeException(message) } - status.appiumStatus = (if (useAppium) { appiumServer.isHealthy() } else true) status.fbsimctlStatus = fbsimctlStatus - status.wdaStatus = isWdaHealty + status.wdaStatus = wdaStatus } - override fun endpointFor(port: Int): URL { - val ports = userPorts.toSet() + fun endpointFor(port: Int): URL { + val ports = allocatedPorts.toSet() require(ports.contains(port)) { "Port $port is not in user ports range $ports" } return URL("http://${remote.publicHostName}:$port/") } - override fun release(reason: String) { - installTask?.cancel(true) + fun dispose() { renewPromise?.cancel(true) preparePromise?.cancel(true) - logger.debug("Disposing device $this for reason $reason") + logger.debug("Disposing device $this") disposeResources() } - override fun delete(reason: String) { - release(reason) - } - - private val deviceLock = ReentrantLock() - - @Volatile - private var installTask: Future? = null - - override fun getInstallTask(): Future? = installTask - - override fun installApplication( - appInstaller: AppInstaller, - appBundleId: String, - appBinaryPath: File - ) { - deviceLock.withLock { - installTask?.let { oldInstallTask -> - if (!oldInstallTask.isDone) { - val message = - "Failed to install app $appBundleId to simulator $udid due to previous task is not finished" - logger.error(logMarker, message) - throw RuntimeException(message) - } - } - - installTask = appInstaller.installApplication(udid, appBundleId, appBinaryPath, true) - } - } - private fun disposeResources() { - stopPeriodicHealthCheck() - if (useFbsimctlProc) { - ignoringDisposeErrors { fbsimctlProc.kill() } - } - ignoringDisposeErrors { appiumServer.kill() } - ignoringDisposeErrors { instrumentationAgent.kill() } + ignoringDisposeErrors { fbsimctlProc.kill() } + ignoringDisposeErrors { webDriverAgent.stop() } ignoringDisposeErrors { calabashProxy.stop() } ignoringDisposeErrors { wdaProxy.stop() } ignoringDisposeErrors { mjpegProxy.stop() } @@ -263,19 +193,15 @@ class Device( } } - override fun lastCrashLog(): CrashLog { - return crashLogs(null).lastOrNull() ?: CrashLog("", "") + fun lastCrashLog(): CrashLog? { + // TODO unlike for simulators, crash logs for physical devices are not at $HOME/Library/Logs/DiagnosticReports + return null } fun crashLogs(appName: String?): List { val crashReportsPath = Files.createTempDirectory("crashReports") val filter = appName?.let { "--filter \"$appName\"" } ?: "" - val command = "${ - File( - remote.homeBrewPath, - "idevicecrashreport" - ).absolutePath - } --udid $udid $filter ${crashReportsPath.toAbsolutePath()}" + val command = "/usr/local/bin/idevicecrashreport --udid $udid $filter ${crashReportsPath.toAbsolutePath()}" try { val result = remote.shell(command, returnOnFailure = true) @@ -298,25 +224,21 @@ class Device( } } - val asyncExecutor = Executors.newFixedThreadPool(3) - private fun executeAsync(action: () -> Unit?): Future<*>? { -// val executor = Executors.newSingleThreadExecutor() -// val future = executor.submit(action) -// executor.shutdown() - - val future = asyncExecutor.submit(action) + val executor = Executors.newSingleThreadExecutor() + val future = executor.submit(action) + executor.shutdown() return future } - override fun prepareAsync() { + fun prepareAsync() { if (preparePromise != null) { return } deviceState = DeviceState.NONE - stopPeriodicHealthCheck() + preparePromise = executeAsync { try { prepare() @@ -328,16 +250,11 @@ class Device( } } - fun renewAsync(whitelistedApps: Set, uninstallApps: Boolean, desiredCaps: DesiredCapabilities) { + fun renewAsync(whitelistedApps: Set, uninstallApps: Boolean) { + val currentStatus = status() var prepareRequired = false - if (useAppium != desiredCaps.useAppium) { - useAppium = desiredCaps.useAppium - prepareRequired = true - deviceState = DeviceState.CREATING - } - - if (status().state == DeviceState.FAILED.value) { + if (currentStatus.state == DeviceState.FAILED.value) { prepareRequired = true deviceState = DeviceState.REVIVING logger.warn(logMarker, "$this failed, will try to revive") @@ -345,6 +262,14 @@ class Device( renewPromise = executeAsync { try { + if (currentStatus.wda_status) { + try { + ensureNoAlerts(maxAttempts = 3) + } catch (e: Exception) { + logger.warn(logMarker, "Ensuring alerts on $this ignored error $e") + } + } + if (uninstallApps) { uninstallUserApps(whitelistedApps = whitelistedApps) } @@ -364,7 +289,18 @@ class Device( } } - override fun listApps(): List = remote.fbsimctl.listApps(udid) + private fun ensureNoAlerts(maxAttempts: Int) { + val client = WdaClient(commandExecutor = wdaEndpoint.toURL()) + client.attachToSession() + + for (attempt in 1..maxAttempts) { + val alertText = client.alertText() ?: return + + logger.debug(logMarker, "Will dismiss alert $alertText") + client.dismissAlert() + Thread.sleep(1000) + } + } private fun uninstallUserApps(whitelistedApps: Set) { logger.debug(logMarker, "About to uninstall user apps on $this") @@ -386,177 +322,74 @@ class Device( lastException = null status.wdaStatus = false status.fbsimctlStatus = false - status.appiumStatus = false logger.info(logMarker, "Starting to prepare $this") - if (useFbsimctlProc) { - fbsimctlProc.kill() - } - appiumServer.kill() - instrumentationAgent.kill() + fbsimctlProc.kill() + webDriverAgent.stop() wdaProxy.stop() mjpegProxy.stop() calabashProxy.stop() executeWithTimeout(timeout, name = "Preparing devices") { - wdaProxy.start(if (useAppium) WDA_PORT else DA_PORT) + wdaProxy.start() if (!wdaProxy.isHealthy()) { throw DeviceException("Failed to start $wdaProxy") } - mjpegProxy.start(mjpegServerPort) + mjpegProxy.start() if (!mjpegProxy.isHealthy()) { throw DeviceException("Failed to start $mjpegProxy") } - calabashProxy.start(CALABASH_PORT) + calabashProxy.start() if (!calabashProxy.isHealthy()) { throw DeviceException("Failed to start $calabashProxy") } - if (useFbsimctlProc) { - startFbsimctl() - } - + startFbsimctl() startWdaWithRetry() - if (useAppium) { - appiumServer.start() - } - - startPeriodicHealthCheck() logger.info(logMarker, "Finished preparing $this") deviceState = DeviceState.CREATED } - } - - @Volatile - private var healthChecker: Job? = null - - private fun startPeriodicHealthCheck() { - stopPeriodicHealthCheck() - - val healthCheckInterval = Duration.ofSeconds(30).toMillis() - - healthChecker = launch { - while (isActive) { - performInstrumentationAgentHealthCheck(10) - - if (useAppium) { - performAppiumServerHealthCheck() - } - - delay(healthCheckInterval) - } - } - } - - private suspend fun performInstrumentationAgentHealthCheck(maxWDAFailCount: Int) { - var wdaFailCount = 0 - - while (!instrumentationAgent.isHealthy() && wdaFailCount < maxWDAFailCount) { - val message = "WebDriverAgent health check failed $wdaFailCount times." - logger.error(logMarker, message) - wdaFailCount += 1 - delay(Duration.ofSeconds(3).toMillis()) - } - - if (wdaFailCount >= maxWDAFailCount) { - logger.error(logMarker, "WebDriverAgent health check failed $wdaFailCount times. Restarting WebDriverAgent") - - try { - instrumentationAgent.kill() - } catch (e: RuntimeException) { - logger.error(logMarker, "Failed to kill WebDriverAgent. ${e.message}", e) - } - - try { - startWdaWithRetry() - } catch (e: RuntimeException) { - logger.error(logMarker, "Failed to restart WebDriverAgent. ${e.message}", e) - deviceState = DeviceState.FAILED - throw RuntimeException("$this Failed to restart WebDriverAgent. Stopping health check") - } - } - } - - private suspend fun performAppiumServerHealthCheck() { - val maxFailCount = 5 - var appiumFailCount = 0 - - while (!appiumServer.isHealthy() && appiumFailCount < maxFailCount) { - logger.error(logMarker, "Appium health check failed $appiumFailCount times.") - appiumFailCount += 1 - delay(Duration.ofSeconds(2).toMillis()) - } - - if (appiumFailCount >= maxFailCount) { - logger.error(logMarker, "Appium health check failed $appiumFailCount times. Restarting Appium") - - try { - appiumServer.kill() - } catch (e: RuntimeException) { - logger.error(logMarker, "Failed to kill Appium. ${e.message}", e) - } - try { - appiumServer.start() - } catch (e: RuntimeException) { - logger.error(logMarker, "Failed to restart Appium. ${e.message}", e) - deviceState = DeviceState.FAILED - throw RuntimeException("${this@Device} Failed to restart Appium. Stopping health check") - } - } - } - - private fun stopPeriodicHealthCheck() { - healthChecker?.let { checker -> - checker.cancel() - while (checker.isActive) { - Thread.sleep(100) - } - } } private fun startFbsimctl() { logger.info(logMarker, "Starting fbsimctl on $this") - if (useFbsimctlProc) { - fbsimctlProc.kill() - fbsimctlProc.start() - - Thread.sleep(5000) - pollFor( - Duration.ofSeconds(60), - reasonName = "$this Fbsimctl health check", - retryInterval = Duration.ofSeconds(2), - logger = logger, - marker = logMarker - ) { - fbsimctlProc.isHealthy() - } + fbsimctlProc.kill() + fbsimctlProc.start() + + pollFor( + Duration.ofSeconds(10), + reasonName = "$this Fbsimctl health check", + retryInterval = Duration.ofSeconds(1), + logger = logger, + marker = logMarker + ) { + fbsimctlProc.isHealthy() } } private fun startWda() { - instrumentationAgent.start(useAppium) - - Thread.sleep(DEVICE_AGENT_START_TIME) + webDriverAgent.stop() + webDriverAgent.start() pollFor( Duration.ofMinutes(1), reasonName = "$this WebDriverAgent health check", - retryInterval = Duration.ofSeconds(5), + retryInterval = Duration.ofSeconds(2), logger = logger, marker = logMarker ) { - if (instrumentationAgent.isProcessAlive) { - instrumentationAgent.isHealthy() + if (webDriverAgent.isProcessAlive) { + webDriverAgent.isHealthy() } else { throw WaitTimeoutError("WebDriverAgent process is not alive") } @@ -585,16 +418,13 @@ class Device( } } - override fun uninstallApplication(bundleId: String, appInstaller: AppInstaller) { + fun uninstallApplication(bundleId: String) { remote.fbsimctl.uninstallApp(udid, bundleId) } private companion object { private const val CALABASH_PORT = 37265 private const val WDA_PORT = 8100 - private const val DA_PORT = 27753 - private val PREPARE_TIMEOUT = Duration.ofMinutes(5) - private const val DEVICE_AGENT_START_TIME = 15_000L - private const val MAX_WDA_STATUS_CHECKS = 10 + private val PREPARE_TIMEOUT = Duration.ofMinutes(4) } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceFbsimctlProc.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceFbsimctlProc.kt index e7f50996..efec87d4 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceFbsimctlProc.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceFbsimctlProc.kt @@ -7,7 +7,7 @@ import com.badoo.automation.deviceserver.ios.proc.FbsimctlProc import java.net.URI class DeviceFbsimctlProc( - private val remote: IRemote, + remote: IRemote, udid: String, fbsimctlEndpoint: URI, headless: Boolean, @@ -21,10 +21,10 @@ class DeviceFbsimctlProc( ) -> ChildProcess = ChildProcess.Companion::fromCommand ) : FbsimctlProc(remote, udid, fbsimctlEndpoint, headless, childFactory) { - override fun getFbsimctlCommand(): List { + override fun getFbsimctlCommand(headless: Boolean): List { return listOf( - remote.fbsimctl.fbsimctlBinary, + FBSimctl.FBSIMCTL_BIN, udid, "listen", "--http", @@ -32,4 +32,4 @@ class DeviceFbsimctlProc( ) } -} +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlot.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlot.kt index 357ed5cd..2a1d4032 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlot.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlot.kt @@ -3,6 +3,8 @@ package com.badoo.automation.deviceserver.ios.device class DeviceSlot(val device: Device) { private var reserved = false + val udid get() = device.udid + fun reserve() { reserved = true } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlots.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlots.kt index 06647e20..86433305 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlots.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlots.kt @@ -3,29 +3,33 @@ package com.badoo.automation.deviceserver.ios.device import com.badoo.automation.deviceserver.LogMarkers import com.badoo.automation.deviceserver.data.DesiredCapabilities import com.badoo.automation.deviceserver.data.DeviceInfo -import com.badoo.automation.deviceserver.data.DeviceState import com.badoo.automation.deviceserver.data.UDID import com.badoo.automation.deviceserver.host.IRemote import com.badoo.automation.deviceserver.host.management.DesiredCapabilitiesMatcher import com.badoo.automation.deviceserver.host.management.PortAllocator import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException import com.badoo.automation.deviceserver.host.management.errors.OverCapacityException -import com.badoo.automation.deviceserver.util.WdaDeviceBundle import net.logstash.logback.marker.MapEntriesAppendingMarker import org.slf4j.LoggerFactory import org.slf4j.Marker import java.util.concurrent.ConcurrentLinkedQueue +import java.io.File class DeviceSlots( val remote: IRemote, - private val wdaDeviceBundles: List, + private val wdaRunnerXctest: File, private val portAllocator: PortAllocator, private val deviceInfoProvider: DeviceInfoProvider, - private val configuredDevices: Set + knownDevicesList: List ) { private val activeSlots = mutableListOf() + private val dcMatcher = DesiredCapabilitiesMatcher() + private val removedSlots = ConcurrentLinkedQueue() + + private val knownDevices = knownDevicesList.map { it.udid to it }.toMap() + private val logger = LoggerFactory.getLogger(javaClass.simpleName) private val logMarker: Marker = MapEntriesAppendingMarker( mapOf( @@ -33,19 +37,18 @@ class DeviceSlots( ) ) - private fun getDevicesWithRetry(): Set { + private fun getDevicesWithRetry(): List { val maxAttempts = 3 - val connectedDevices = mutableSetOf() + var connectedDevices = emptyList() for (attempt in 1..maxAttempts) { - val devices = deviceInfoProvider.list() + connectedDevices = deviceInfoProvider.list() - if (devices.isEmpty()) { + if (connectedDevices.isEmpty()) { logger.warn("fbsimctl returned an empty list of devices on attempt $attempt/$maxAttempts") Thread.sleep(500) continue } else { - connectedDevices.addAll(devices) break } } @@ -55,14 +58,9 @@ class DeviceSlots( fun registerDevices() { val connectedDevices = getDevicesWithRetry() - val allowedConnectedDevices = if (configuredDevices.isEmpty()) { - connectedDevices - } else { - val connectedDeviceUdids = configuredDevices.map { it.udid } - connectedDevices.filter { connectedDeviceUdids.contains(it.udid) }.toSet() - } + val knownConnectedDevices = connectedDevices.filter { isWhitelisted(it.udid) } - val diff = diff(allowedConnectedDevices) + val diff = diff(knownConnectedDevices) if (diff.removed.isNotEmpty()) { logger.info(logMarker, "Will remove ${diff.removed} devices") @@ -73,7 +71,7 @@ class DeviceSlots( if (diff.added.isNotEmpty()) { logger.info(logMarker, "Will add ${diff.added} devices") - allowedConnectedDevices.filter { diff.added.contains(it.udid) }.forEach { + knownConnectedDevices.filter { diff.added.contains(it.udid) }.forEach { addSlot(it) } } @@ -94,7 +92,7 @@ class DeviceSlots( } fun tryGetSlot(udid: UDID): DeviceSlot? { - return activeSlots.firstOrNull { it.device.udid == udid } + return activeSlots.firstOrNull { it.udid == udid } } fun reserve(desiredCapabilities: DesiredCapabilities): DeviceSlot { @@ -124,11 +122,15 @@ class DeviceSlots( fun dispose() { activeSlots.forEach { - it.device.release("Disposing") + it.device.dispose() } activeSlots.clear() } + private fun isWhitelisted(udid: UDID): Boolean { + return knownDevices.isEmpty() || knownDevices.containsKey((udid)) + } + private fun availableSlots(desiredCapabilities: DesiredCapabilities): List { return activeSlots.filter { !it.isReserved() && dcMatcher.isMatch(it.device.deviceInfo, desiredCapabilities) @@ -137,13 +139,12 @@ class DeviceSlots( private data class Diff(val added: Set, val removed: Set) - private fun diff(allowedConnectedDevices: Set): Diff { - val current = activeSlots.filter { it.device.deviceState != DeviceState.FAILED }.map { it.device.udid }.toSet() - val failed = activeSlots.filter { it.device.deviceState == DeviceState.FAILED }.map { it.device.udid }.toSet() - val new = allowedConnectedDevices.map { it.udid }.toSet() + private fun diff(deviceInfos: List): Diff { + val current = activeSlots.map { it.udid }.toSet() + val new = deviceInfos.map { it.udid }.toSet() val added = new - current - val removed = (current - new) + failed + val removed = current - new return Diff(added, removed) } @@ -151,17 +152,17 @@ class DeviceSlots( private fun addSlot(deviceInfo: DeviceInfo) { val udid = deviceInfo.udid - if (activeSlots.any { it.device.udid == udid }) { + if (activeSlots.any { it.udid == udid }) { throw RuntimeException("Device $udid is already registered") } val allocatedPorts = portAllocator.allocateDAP() val device = Device( - remote = remote, + remote =remote, deviceInfo = deviceInfo, - userPorts = allocatedPorts, - wdaDeviceBundles = wdaDeviceBundles + allocatedPorts = allocatedPorts, + wdaRunnerXctest = wdaRunnerXctest ) device.prepareAsync() @@ -171,7 +172,7 @@ class DeviceSlots( } private fun removeSlotBy(udid: UDID) { - val slot = activeSlots.find { it.device.udid == udid } + val slot = activeSlots.find { it.udid == udid } if (slot == null) { throw DeviceNotFoundException("Device $udid is already unregistered") @@ -181,17 +182,17 @@ class DeviceSlots( } private fun removeSlot(slot: DeviceSlot) { - val allocatedPorts = slot.device.userPorts + val allocatedPorts = slot.device.allocatedPorts - slot.device.release("Removing slot") + slot.device.dispose() portAllocator.deallocateDAP(allocatedPorts) activeSlots.remove(slot) - removedSlots.add(RemovedSlot(slot.device.udid)) + removedSlots.add(RemovedSlot(slot.udid)) if (removedSlots.size > 1000) { removedSlots.remove() } } private data class RemovedSlot(val udid: UDID) -} +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceWebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceWebDriverAgent.kt new file mode 100644 index 00000000..4ac1ac78 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceWebDriverAgent.kt @@ -0,0 +1,30 @@ +package com.badoo.automation.deviceserver.ios.device + +import com.badoo.automation.deviceserver.data.UDID +import com.badoo.automation.deviceserver.host.IRemote +import com.badoo.automation.deviceserver.ios.proc.WebDriverAgent +import java.net.URI +import java.io.File + +class DeviceWebDriverAgent( + remote: IRemote, + wdaRunnerXctest: File, + udid: UDID, + wdaEndpoint: URI, + port: Int, + mjpegServerPort: Int, + hostApp: String = wdaRunnerXctest.parentFile.parentFile.absolutePath +) : WebDriverAgent( + remote = remote, + wdaRunnerXctest = wdaRunnerXctest, + hostApp = hostApp, + udid = udid, + wdaEndpoint = wdaEndpoint, + port = port, + mjpegServerPort = mjpegServerPort +) { + override fun terminateHostApp() { + remote.fbsimctl.uninstallApp(udid, hostApp) + Thread.sleep(1000) + } +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/ConfiguredDevice.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/KnownDevice.kt similarity index 70% rename from device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/ConfiguredDevice.kt rename to device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/KnownDevice.kt index 4f9e9d87..3b9c7da7 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/ConfiguredDevice.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/KnownDevice.kt @@ -1,9 +1,10 @@ package com.badoo.automation.deviceserver.ios.device import com.badoo.automation.deviceserver.data.UDID +import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonProperty -data class ConfiguredDevice( +data class KnownDevice( @JsonProperty("udid") val udid: UDID ) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxy.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxy.kt index 5536eb27..c73599de 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxy.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxy.kt @@ -7,12 +7,12 @@ import com.badoo.automation.deviceserver.host.IRemote import net.logstash.logback.marker.MapEntriesAppendingMarker import org.slf4j.Logger import org.slf4j.LoggerFactory -import java.io.File class UsbProxy( private val udid: UDID, private val remote: IRemote, val localPort: Int, + val devicePort: Int, private val childFactory: ( remoteHost: String, userName: String, @@ -30,28 +30,25 @@ class UsbProxy( ) ) - override fun toString(): String = "" + override fun toString(): String = "" - val iproxyBinary = File(remote.homeBrewPath, "iproxy").absolutePath - val socatBinary = File(remote.homeBrewPath, "socat").absolutePath private var iproxy: ChildProcess? = null private var socat: ChildProcess? = null - fun start(devicePort: Int) { - val iProxyCommand = listOf(iproxyBinary, "$localPort:$devicePort", "--udid", udid) + fun start() { iproxy = childFactory( remote.hostName, remote.userName, - iProxyCommand, + listOf(IPROXY_BIN, localPort.toString(), devicePort.toString(), udid), mapOf(), - { message -> }, // { message -> logger.trace(logMarker, "${this}: iproxy : ${message.trim()}") }, + { message -> logger.trace(logMarker, "${this}: iproxy : ${message.trim()}") }, { message -> logger.debug(logMarker, "${this}: iproxy : ${message.trim()}") } ) socat = childFactory( remote.hostName, remote.userName, - listOf(socatBinary, "tcp-listen:$localPort,reuseaddr,fork", "tcp:0.0.0.0:$localPort"), + listOf(SOCAT_BIN, "tcp-listen:$localPort,reuseaddr,fork", "tcp:0.0.0.0:$localPort"), mapOf(), { message -> logger.trace(logMarker, "${this}: socat : ${message.trim()}") }, { message -> logger.debug(logMarker, "${this}: socat : ${message.trim()}") } @@ -75,4 +72,9 @@ class UsbProxy( socat = null } } + + companion object { + const val IPROXY_BIN = "/usr/local/bin/iproxy" + const val SOCAT_BIN = "/usr/local/bin/socat" + } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxyFactory.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxyFactory.kt index 6577a808..45cc012f 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxyFactory.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxyFactory.kt @@ -6,7 +6,7 @@ import com.badoo.automation.deviceserver.host.IRemote class UsbProxyFactory( private val remote: IRemote ) { - fun create(udid: UDID, localPort: Int): UsbProxy { - return UsbProxy(udid, remote, localPort) + fun create(udid: UDID, localPort: Int, devicePort: Int): UsbProxy { + return UsbProxy(udid, remote, localPort, devicePort) } } \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/diagnostic/RealDeviceSysLog.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/diagnostic/RealDeviceSysLog.kt deleted file mode 100644 index d66c3f8b..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/diagnostic/RealDeviceSysLog.kt +++ /dev/null @@ -1,111 +0,0 @@ -package com.badoo.automation.deviceserver.ios.device.diagnostic - -import com.badoo.automation.deviceserver.LogMarkers -import com.badoo.automation.deviceserver.data.SysLogCaptureOptions -import com.badoo.automation.deviceserver.data.UDID -import com.badoo.automation.deviceserver.host.IRemote -import com.badoo.automation.deviceserver.ios.ISysLog -import net.logstash.logback.marker.MapEntriesAppendingMarker -import org.slf4j.LoggerFactory -import org.slf4j.Marker -import java.io.File -import java.io.InputStream -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.StandardCopyOption -import java.util.concurrent.Executors -import java.util.concurrent.Future - -class RealDeviceSysLog( - private val remote: IRemote, - private val udid: UDID, - override val osLogFile: File = File.createTempFile("iOS_RealDevice_SysLog_${udid}_", ".log"), - override val osLogStderr: File = File.createTempFile("iOS_RealDevice_SysLog_${udid}_", ".err.log") -): ISysLog { - private var outWritingTask: Future<*>? = null - private var errWritingTask: Future<*>? = null - private var osLogWriterProcess: Process? = null - private val logger = LoggerFactory.getLogger(javaClass.simpleName) - private val logMarker: Marker = MapEntriesAppendingMarker( - mapOf( - LogMarkers.UDID to udid, - LogMarkers.HOSTNAME to remote.hostName - ) - ) - - private var timestamp: String? = null - - override fun truncate(): Boolean { - val date = remote.execIgnoringErrors(listOf("date", "+%s")) - - if (date.isSuccess) { - timestamp = date.stdOut.lines().first() - } - - return date.isSuccess - } - - override fun content(process: String?): String { - return """ - Only live syslog recording is supported by real devices. Please use: - - POST {deviceRef}/syslog/start - to start live recording of logs - - POST {deviceRef}/syslog/stop - to stop live recording of logs - - GET {deviceRef}/syslog - to get recorded logs - - DELETE {deviceRef}/syslog - to reset and delete recorded logs - """.trimIndent() - } - - override fun deleteLogFiles() { - osLogFile.delete() - osLogStderr.delete() - } - - override fun stopWritingLog() { - osLogWriterProcess?.destroy() - outWritingTask?.cancel(true) - errWritingTask?.cancel(true) - } - - override fun startWritingLog(sysLogCaptureOptions: SysLogCaptureOptions) { - stopWritingLog() - deleteLogFiles() - - val cmd = mutableListOf( - File(remote.homeBrewPath, "idevicesyslog").absolutePath, - "--udid", udid, // target specific device by UDID - "--no-colors", // disable colored output - "--exit" // exit when device disconnects - ) - - if (sysLogCaptureOptions.shouldMuteKernel) { - cmd.add("--no-kernel") // suppress kernel messages - } - - if (sysLogCaptureOptions.matchingProcesses.isNotBlank()) { - cmd.add("--process") // only print messages from matching process(es) - cmd.add(sysLogCaptureOptions.matchingProcesses) // only print messages from matching process(es) - } - - if (sysLogCaptureOptions.shouldMuteSystemProcesses) { - cmd.add("--quiet") // set a filter to exclude common noisy processes (see --quiet-list) - } - - val process: Process = remote.localExecutor.startProcess(cmd, mapOf(), logMarker) - - val executor = Executors.newFixedThreadPool(2) - outWritingTask = executor.submit(write(process.inputStream, osLogFile.toPath())) - errWritingTask = executor.submit(write(process.errorStream, osLogStderr.toPath())) - executor.shutdown() - - osLogWriterProcess = process - } - - private fun write(inputStream: InputStream, path: Path): Runnable { - logger.debug("Writing log file to ${path.toFile().absolutePath}") - return Runnable { - inputStream.use { stream -> - Files.copy(stream, path, StandardCopyOption.REPLACE_EXISTING) - } - } - } -} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt index 194f3356..5feef19c 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt @@ -2,7 +2,7 @@ package com.badoo.automation.deviceserver.ios.fbsimctl import com.badoo.automation.deviceserver.command.CommandResult import com.badoo.automation.deviceserver.command.IShellCommand -import com.badoo.automation.deviceserver.command.SshConnectionException +import com.badoo.automation.deviceserver.command.RemoteShellCommand import com.badoo.automation.deviceserver.data.UDID import com.badoo.automation.deviceserver.util.ensure import org.slf4j.LoggerFactory @@ -11,20 +11,18 @@ import java.time.Duration class FBSimctl( private val shellCommand: IShellCommand, - homeBrewPath: File, private val parser: IFBSimctlResponseParser = FBSimctlResponseParser() -) : ISimulatorControl { +) : IFBSimctl { companion object { - private val SIMULATOR_SHUTDOWN_TIMEOUT: Duration = Duration.ofSeconds(90) + private val SIMULATOR_SHUTDOWN_TIMEOUT: Duration = Duration.ofSeconds(60) + const val FBSIMCTL_BIN = "/usr/local/bin/fbsimctl" const val RESPONSE_FORMAT = "--json" } - override val fbsimctlBinary: String = File(homeBrewPath, "fbsimctl").absolutePath - private val logger = LoggerFactory.getLogger(javaClass.simpleName) override fun installApp(udid: UDID, bundlePath: File) { - val response = fbsimctl(listOf("install", bundlePath.absolutePath), udid, raiseOnError = true, timeOut = Duration.ofSeconds(120)) + val response = fbsimctl(listOf("install", bundlePath.absolutePath), udid, raiseOnError = true) val result = parser.parseInstallApp(response) if (!result.isSuccess) { throw FBSimctlError("Failed to install application [${bundlePath.absolutePath}]. Error:\n${result.errorMessage}") @@ -71,7 +69,7 @@ class FBSimctl( override fun eraseSimulator(udid: UDID) = fbsimctl(cmd = "erase", udid = udid) - override fun create(model: String?, os: String?): FBSimctlDevice { + override fun create(model: String?, os: String?, transitional: Boolean): FBSimctlDevice { val args = mutableListOf("create") // FIXME: escaping should be part of exec implementation and hidden from caller. Fix in separate ticket. @@ -89,7 +87,7 @@ class FBSimctl( val suggestions = suggestCreationArgs() throw(RuntimeException("Could not create simulator \"$model\" \"$os\"\n$suggestions")) } - return parser.parseDeviceCreation(result) + return parser.parseDeviceCreation(result, transitional) } private fun suggestCreationArgs(): String { @@ -105,20 +103,20 @@ class FBSimctl( } override fun shutdown(udid: UDID): CommandResult { - return shellCommand.exec("/usr/bin/xcrun simctl shutdown $udid".split(" "), timeOut = SIMULATOR_SHUTDOWN_TIMEOUT, returnFailure = true) + return shellCommand.exec(listOf("/usr/bin/xcrun", "simctl", "shutdown", udid), timeOut = SIMULATOR_SHUTDOWN_TIMEOUT, returnFailure = true) } - override fun shutdownAllBooted() = shellCommand.exec("/usr/bin/xcrun simctl shutdown all".split(" "), timeOut = Duration.ofMinutes(3), returnFailure = true).stdOut - - override fun delete(udid: UDID): CommandResult { - return shellCommand.exec("/usr/bin/xcrun simctl delete $udid".split(" "), timeOut = SIMULATOR_SHUTDOWN_TIMEOUT, returnFailure = true) + override fun shutdownAll(): CommandResult { + return shellCommand.exec(listOf("/usr/bin/xcrun", "simctl", "shutdown", "all"), timeOut = Duration.ofMinutes(3), returnFailure = true) } + override fun delete(udid: UDID) = fbsimctl("delete", udid) + override fun terminateApp(udid: UDID, bundleId: String, raiseOnError: Boolean) = fbsimctl(listOf("terminate", bundleId), udid, raiseOnError = raiseOnError) - override fun uninstallApp(udid: UDID, bundleId: String, raiseOnError: Boolean) { - fbsimctl(listOf("uninstall", bundleId), udid, timeOut = Duration.ofSeconds(60), raiseOnError = raiseOnError) + override fun uninstallApp(udid: UDID, bundleId: String) { + fbsimctl(listOf("uninstall", bundleId), udid, raiseOnError = true) } private fun fbsimctl( @@ -144,11 +142,10 @@ class FBSimctl( ): String { val fbsimctlCommand = buildFbsimctlCommand(jsonFormat, udid, cmd) - val result = try { - shellCommand.exec(fbsimctlCommand, timeOut = timeOut, returnFailure = true) - } catch (e: SshConnectionException) { - logger.error("FBSimctl retrying command on SSH error. Command: $fbsimctlCommand") - shellCommand.exec(fbsimctlCommand, timeOut = timeOut, returnFailure = true) + var result = executeCommand(fbsimctlCommand, timeOut) + + if (result.exitCode == 255 && shellCommand is RemoteShellCommand) { // SSH_CONNECT_ERROR, but not necessary + result = executeCommand(fbsimctlCommand, timeOut) //FIXME: NOT a good place and not a good thing to do } val errors = filterFailures(result.stdOut) @@ -164,9 +161,13 @@ class FBSimctl( return result.stdOut.trim() // remove last new_line } + private fun executeCommand(fbsimctlCommand: ArrayList, timeOut: Duration): CommandResult { + return shellCommand.exec(fbsimctlCommand, timeOut = timeOut) + } + private fun buildFbsimctlCommand(jsonFormat: Boolean, udid: UDID?, command: List): ArrayList { val cmd = arrayListOf() - cmd.add(fbsimctlBinary) + cmd.add(FBSIMCTL_BIN) if (jsonFormat) { cmd.add(RESPONSE_FORMAT) @@ -180,7 +181,5 @@ class FBSimctl( return cmd } - private fun filterFailures(fbsimctlResponse: String): List> { - return parser.parseFailures(fbsimctlResponse) - } + private fun filterFailures(out: String) = parser.parse(out).filter { it["event_name"] == "failure" } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlDTO.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlDTO.kt index b587046f..10ed87d9 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlDTO.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlDTO.kt @@ -39,4 +39,4 @@ data class FBSimctlDeviceDiagnosticInfo( data class FBSimctlInstallResult( val isSuccess: Boolean, val errorMessage: String = "" -) +) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlErrors.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlErrors.kt index 517e00fd..9b7a917d 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlErrors.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlErrors.kt @@ -3,8 +3,3 @@ package com.badoo.automation.deviceserver.ios.fbsimctl open class FBSimctlError(message: String?, cause: Throwable? = null) : IllegalStateException(message, cause) class FBSimctlResponseParseError(message: String?, cause: Throwable? = null) : FBSimctlError(message, cause) - -class EmptyApplicationsListError(message: String?, cause: Throwable? = null) : FBSimctlError(message, cause) -class ApplicationNotFoundError(message: String?, cause: Throwable? = null) : FBSimctlError(message, cause) -class DataContainerNotFoundError(message: String?, cause: Throwable? = null) : FBSimctlError(message, cause) -class ApplicationContainerNotFoundError(message: String?, cause: Throwable? = null) : FBSimctlError(message, cause) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlResponseParser.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlResponseParser.kt index e51d1ab1..19fcc565 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlResponseParser.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlResponseParser.kt @@ -13,14 +13,6 @@ class FBSimctlResponseParser : IFBSimctlResponseParser { .map { mapper.fromJson>(it) } } - override fun parseFailures(response: String): List> { - val failureEvents = filteredResponseLines(response) - .filter { isOfEventName(it, "failure") } - - val jsonMapper = JsonMapper() - return failureEvents.map { jsonMapper.fromJson>(it) } - } - /** * Parses device list */ @@ -31,10 +23,8 @@ class FBSimctlResponseParser : IFBSimctlResponseParser { .map { it.subject } override fun parseDeviceSets(response: String): List { - val deviceSetsEvent = filteredResponseLines(response) - .first { isOfEventName(it, "list_device_sets") } - - return parse(deviceSetsEvent).map { it["subject"] as String } + return parse(response) + .map { it["subject"] as String } } /** @@ -47,14 +37,11 @@ class FBSimctlResponseParser : IFBSimctlResponseParser { .map { it.subject }.flatten() override fun parseDiagnosticInfo(response: String): FBSimctlDeviceDiagnosticInfo { - val jsonMapper = JsonMapper() - val diagnosticEvents = filteredResponseLines(response) - .filter { isOfEventName(it, "diagnostic") } - .map { jsonMapper.fromJson>(it) } + val result = parse(response) - val sysLog = getFileLocation(diagnosticEvents, "system_log") - val coreSimulatorLog = getFileLocation(diagnosticEvents, "coresimulator") - val videoRecording = getFileLocation(diagnosticEvents, "video") + val sysLog = getFileLocation(result, "system_log") + val coreSimulatorLog = getFileLocation(result, "coresimulator") + val videoRecording = getFileLocation(result, "video") return FBSimctlDeviceDiagnosticInfo( sysLogLocation = sysLog, @@ -63,7 +50,7 @@ class FBSimctlResponseParser : IFBSimctlResponseParser { ) } - override fun parseDeviceCreation(response: String): FBSimctlDevice { + override fun parseDeviceCreation(response: String, isTransitional: Boolean): FBSimctlDevice { val parsedResponse: FBSimctlCreateDeviceResponse? try { diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/ISimulatorControl.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctl.kt similarity index 77% rename from device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/ISimulatorControl.kt rename to device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctl.kt index 18053f4c..715371f2 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/ISimulatorControl.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctl.kt @@ -4,7 +4,7 @@ import com.badoo.automation.deviceserver.command.CommandResult import com.badoo.automation.deviceserver.data.UDID import java.io.File -interface ISimulatorControl { +interface IFBSimctl { fun installApp(udid: UDID, bundlePath: File) /** @@ -29,12 +29,11 @@ interface ISimulatorControl { fun defaultDeviceSet(): String fun eraseSimulator(udid: UDID): String - fun create(model: String?, os: String?): FBSimctlDevice + fun create(model: String?, os: String?, transitional: Boolean): FBSimctlDevice fun diagnose(udid: UDID): FBSimctlDeviceDiagnosticInfo fun shutdown(udid: UDID): CommandResult - fun shutdownAllBooted(): String - fun delete(udid: UDID): CommandResult + fun shutdownAll(): CommandResult + fun delete(udid: UDID): String fun terminateApp(udid: UDID, bundleId: String, raiseOnError: Boolean = false): String - fun uninstallApp(udid: UDID, bundleId: String, raiseOnError: Boolean = true) - val fbsimctlBinary: String + fun uninstallApp(udid: UDID, bundleId: String) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctlResponseParser.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctlResponseParser.kt index df0dd455..5be80ead 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctlResponseParser.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctlResponseParser.kt @@ -4,9 +4,8 @@ interface IFBSimctlResponseParser { fun parseDeviceList(response: String): List fun parseApplicationsList(response: String): List fun parseDiagnosticInfo(response: String): FBSimctlDeviceDiagnosticInfo - fun parseDeviceCreation(response: String): FBSimctlDevice + fun parseDeviceCreation(response: String, isTransitional: Boolean): FBSimctlDevice fun parse(response: String): List> fun parseDeviceSets(response: String): List fun parseInstallApp(response: String): FBSimctlInstallResult - fun parseFailures(response: String): List> -} +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/XCRunSimctl.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/XCRunSimctl.kt deleted file mode 100644 index ca6100ac..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/XCRunSimctl.kt +++ /dev/null @@ -1,187 +0,0 @@ -import com.badoo.automation.deviceserver.command.CommandResult -import com.badoo.automation.deviceserver.command.IShellCommand -import com.badoo.automation.deviceserver.command.SshConnectionException -import com.badoo.automation.deviceserver.data.UDID -import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlAppInfo -import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDevice -import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDeviceDiagnosticInfo -import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlError -import com.badoo.automation.deviceserver.ios.fbsimctl.ISimulatorControl -import org.slf4j.LoggerFactory -import java.io.File -import java.time.Duration - -class XCRunSimctl( - private val shellCommand: IShellCommand, - private val isRemoteHost: Boolean, - private val hostName: String, - override val fbsimctlBinary: String = "Unsupported here" -) : ISimulatorControl { - private val logger = LoggerFactory.getLogger(javaClass.simpleName) - - override fun installApp(udid: UDID, bundlePath: File) { - TODO("Not yet implemented") - } - - override fun listSimulators(): List { - val fbsimctlCommand = listOf("xcrun", "simctl", "list", "--json") - - val timeOut = Duration.ofSeconds(30) - - val result = try { - shellCommand.exec(fbsimctlCommand, timeOut = timeOut, returnFailure = true) - } catch (e: SshConnectionException) { - logger.error("XCRunSimctl retrying command on SSH error. Command: ${fbsimctlCommand.joinToString(" ")}") - shellCommand.exec(fbsimctlCommand, timeOut = timeOut, returnFailure = true) - } - - - val raiseOnError = false - if (raiseOnError) { - if (result.exitCode != 0) { - throw FBSimctlError("Error while running command. Result: $result", null) - } - } - - result.stdOut.trim() // remove last new_line - - return listOf() - } - - override fun listDevices(): List { - TODO("Not yet implemented") - } - - override fun listDevice(udid: UDID): FBSimctlDevice? { - TODO("Not yet implemented") - } - - override fun listApps(udid: UDID): List { - TODO("Not yet implemented") - } - - override fun defaultDeviceSet(): String { - TODO("Not yet implemented") - } - - override fun eraseSimulator(udid: UDID): String { - val command: List = listOf("xcrun", "simctl", "erase", udid) - - val timeOut = Duration.ofSeconds(30) - - val result = try { - shellCommand.exec(command, timeOut = timeOut, returnFailure = true) - } catch (e: SshConnectionException) { - logger.error("XCRunSimctl retrying command on SSH error. Command: ${command.joinToString(" ")}") - shellCommand.exec(command, timeOut = timeOut, returnFailure = true) - } - - if (result.exitCode != 0) { - throw FBSimctlError( - "Error while running command. Exit code: ${result.exitCode}\n" + - "StdErr: ${result.stdErr}. StdOut: ${result.stdOut}", null - ) - } - - return result.stdOut.trim() - } - - override fun create(model: String?, os: String?): FBSimctlDevice { - val modelString = model ?: "iPhone 7" - val osString = os ?: "iOS 15.0" - - val deviceModel = getDeviceModel(modelString) - val deviceName = (model ?: "iPhone 7").replace(simDeviceTypeRegex, "_") - - val deviceRuntime = "com.apple.CoreSimulator.SimRuntime.${osString.replace(runtimeRegex, "-")}" - val command: List = listOf("xcrun", "simctl", "create", deviceName, deviceModel, deviceRuntime) - - val timeOut = Duration.ofSeconds(30) - - val result = try { - shellCommand.exec(command, timeOut = timeOut, returnFailure = true) - } catch (e: SshConnectionException) { - logger.error("XCRunSimctl retrying command on SSH error. Command: ${command.joinToString(" ")}") - shellCommand.exec(command, timeOut = timeOut, returnFailure = true) - } - - if (result.exitCode != 0) { - throw FBSimctlError( - "Error while running command. Exit code: ${result.exitCode}\n" + - "StdErr: ${result.stdErr}. StdOut: ${result.stdOut}", null - ) - } - - val udid = result.stdOut.trim() // remove last new_line - val host = if (isRemoteHost) hostName else "localhost" - logger.info("Created iOS Simulator ${udid} on host ${host} ") - - return FBSimctlDevice( - arch = "x86_64", - state = "Shutdown", - model = modelString, - name = modelString, - udid = udid, - os = osString - ) - } - - override fun diagnose(udid: UDID): FBSimctlDeviceDiagnosticInfo { - TODO("Not yet implemented") - } - - override fun shutdown(udid: UDID): CommandResult { - TODO("Not yet implemented") - } - - override fun shutdownAllBooted(): String { - TODO("Not yet implemented") - } - - override fun delete(udid: UDID): CommandResult { - TODO("Not yet implemented") - } - - override fun terminateApp(udid: UDID, bundleId: String, raiseOnError: Boolean): String { - TODO("Not yet implemented") - } - - override fun uninstallApp(udid: UDID, bundleId: String, raiseOnError: Boolean) { - TODO("Not yet implemented") - } - - companion object { - val quirkyDeviceTypes: Map = mapOf( - "iPhone SE (1st generation)" to "com.apple.CoreSimulator.SimDeviceType.iPhone-SE", - "iPhone SE (2nd generation)" to "com.apple.CoreSimulator.SimDeviceType.iPhone-SE--2nd-generation-", - "iPhone SE (3rd generation)" to "com.apple.CoreSimulator.SimDeviceType.iPhone-SE-3rd-generation", - "iPhone Xs" to "com.apple.CoreSimulator.SimDeviceType.iPhone-XS", - "iPhone Xs Max" to "com.apple.CoreSimulator.SimDeviceType.iPhone-XS-Max", - "iPhone Xʀ" to "com.apple.CoreSimulator.SimDeviceType.iPhone-XR", - "iPhone XR" to "com.apple.CoreSimulator.SimDeviceType.iPhone-XR", - "iPad Pro (12.9-inch) (1st generation)" to "com.apple.CoreSimulator.SimDeviceType.iPad-Pro", - "iPad Pro (11-inch) (1st generation)" to "com.apple.CoreSimulator.SimDeviceType.iPad-Pro--11-inch-", - "iPad (9th generation)" to "com.apple.CoreSimulator.SimDeviceType.iPad-9th-generation", - "iPad Pro (11-inch) (3rd generation)" to "com.apple.CoreSimulator.SimDeviceType.iPad-Pro-11-inch-3rd-generation", - "iPad Pro (12.9-inch) (5th generation)" to "com.apple.CoreSimulator.SimDeviceType.iPad-Pro-12-9-inch-5th-generation", - "iPad mini (6th generation)" to "com.apple.CoreSimulator.SimDeviceType.iPad-mini-6th-generation", - "iPhone 14" to "com.apple.CoreSimulator.SimDeviceType.iPhone-14", - "iPhone 14 Plus" to "com.apple.CoreSimulator.SimDeviceType.iPhone-14-Plus", - "iPhone 14 Pro" to "com.apple.CoreSimulator.SimDeviceType.iPhone-14-Pro", - "iPhone 14 Pro Max" to "com.apple.CoreSimulator.SimDeviceType.iPhone-14-Pro-Max" - ) - - private val simDeviceTypeRegex = Regex("[ ().]") - private val runtimeRegex = Regex("[ .]") - - fun getDeviceModel(model: String): String { - return quirkyDeviceTypes[model] ?: "com.apple.CoreSimulator.SimDeviceType.${ - if (model.contains("Watch")) { - model.replace(" - ", " ").replace(simDeviceTypeRegex, "-") - } else { - model.replace(simDeviceTypeRegex, "-") - } - }" - } - } -} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/XCRunSimctlResponseParser.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/XCRunSimctlResponseParser.kt deleted file mode 100644 index ac00d8a5..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/XCRunSimctlResponseParser.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.badoo.automation.deviceserver.ios.fbsimctl - -import com.badoo.automation.deviceserver.JsonMapper -import com.fasterxml.jackson.core.JsonProcessingException - - -class XCRunSimctlResponseParser : IFBSimctlResponseParser { -// override fun parseRuntimes(response: String): List { -// return fromJson(response, XCRuntimesResponse::class.java).runtimes -// } - - private fun fromJson(string: String, clazz: Class): T { - try { - return JsonMapper().fromJson(string, clazz) - } catch (e: JsonProcessingException) { - throw FBSimctlResponseParseError("Failed to parse fbsimctl response. " + - "Please check maybe response format has changed. DTO class: [${clazz.name}]. FBSimctl response: [$string]", e) - } - } - -// private data class XCRuntimesResponse( -// val runtimes: List -// ) - - override fun parseDeviceList(response: String): List { - TODO("Not yet implemented") - } - - override fun parseApplicationsList(response: String): List { - TODO("Not yet implemented") - } - - override fun parseDiagnosticInfo(response: String): FBSimctlDeviceDiagnosticInfo { - TODO("Not yet implemented") - } - - override fun parseDeviceCreation(response: String): FBSimctlDevice { - TODO("Not yet implemented") - } - - override fun parse(response: String): List> { - TODO("Not yet implemented") - } - - override fun parseDeviceSets(response: String): List { - TODO("Not yet implemented") - } - - override fun parseInstallApp(response: String): FBSimctlInstallResult { - TODO("Not yet implemented") - } - - override fun parseFailures(response: String): List> { - TODO("Not yet implemented") - } -} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/AppiumServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/AppiumServer.kt deleted file mode 100644 index 10c8e704..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/AppiumServer.kt +++ /dev/null @@ -1,171 +0,0 @@ -package com.badoo.automation.deviceserver.ios.proc - -import com.badoo.automation.deviceserver.ApplicationConfiguration -import com.badoo.automation.deviceserver.command.ChildProcess -import com.badoo.automation.deviceserver.host.IRemote -import com.badoo.automation.deviceserver.host.IRemote.Companion.DEFAULT_PATH -import com.badoo.automation.deviceserver.util.ensure -import com.badoo.automation.deviceserver.util.pollFor -import com.badoo.automation.deviceserver.util.uriWithPath -import java.io.File -import java.net.URI -import java.net.URL -import java.time.Duration - -class AppiumServer( - private val remote: IRemote, - private val udid: String, - private val appiumServerPort: Int, - private val wdaPort: Int, - private val childFactory: ( - remoteHost: String, - username: String, - cmd: List, - commandEnvironment: Map, - out_reader: ((line: String) -> Unit)?, - err_reader: ((line: String) -> Unit)? - ) -> ChildProcess = ChildProcess.Companion::fromCommand -) : LongRunningProc(udid, remote.hostName) { - private val remoteAppiumTmpDir: File = File(remote.tmpDir, "appium_tmpdir_${udid}") - private val remoteAppiumServerLog: File = File(remoteAppiumTmpDir, "remote_appium_server_log_${udid}.txt") - private val appConfig = ApplicationConfiguration() - private val localAppiumServerLogCopy = File(appConfig.tempFolder, "appium_server_log_${udid}.txt") - - val appiumServerLog - get(): File { - return if (remote.isLocalhost()) { - remoteAppiumServerLog - } else { - localAppiumServerLogCopy.delete() - remote.scpFromRemoteHost( - remoteAppiumServerLog.absolutePath, - localAppiumServerLogCopy.absolutePath, - Duration.ofSeconds(120) - ) - localAppiumServerLogCopy - } - } - - fun deleteAppiumServerLog() { - remote.shell(": > ${remoteAppiumServerLog.absolutePath}") - localAppiumServerLogCopy.delete() - } - - private val statusUrl: URL = uriWithPath(URI("http://${remote.publicHostName}:$appiumServerPort"), "status").toURL() - - override fun checkHealth(): Boolean { - if (childProcess == null) { - logger.debug(logMarker, "$this Appium Server has not yet started.") - return false - } - - return try { - logger.trace(logMarker, "Checking health for Appium Server on $udid on url: $statusUrl") - val result = client.get(statusUrl) - logger.trace(logMarker, "Appium Server on $udid on url: $statusUrl returned result - ${result.httpCode} , ${result.responseBody}, Success: ${result.isSuccess}") - return result.isSuccess - } catch (e: RuntimeException) { - logger.warn(logMarker, "Failed to determine Appium Server state. Exception: $e") - false - } - } - - override fun start() { - ensure(childProcess == null) { AppiumServerProcError("Previous Appium Server process $childProcess has not been killed") } - logger.debug(logMarker, "$this — Starting child process") - kill() // cleanup old processes in case there are - deleteRemoteAppiumTmpDir() - createRemoteAppiumTmpDir() - - val outWriter: ((String) -> Unit)? = null // { message -> logger.info("[Appium Server INFO] $message") } - val errWriter: (String) -> Unit = { message -> logger.error("[Appium Server ERROR] $message") } - - val path = if (remote.isLocalhost()) { - "${System.getenv("PATH")}:${DEFAULT_PATH}" - } else { - DEFAULT_PATH - } - - val command = getAppiumServerStartCommand() - - val process = childFactory( - remote.hostName, - remote.userName, - command, - mapOf("PATH" to path, "TMPDIR" to remote.tmpDir.absolutePath), - outWriter, - errWriter - ) - - childProcess = process - - try { - Thread.sleep(3000) // initial Appium timeout to get process started - - pollFor( - Duration.ofSeconds(45), - reasonName = "Waiting for Appium Server to start serving requests", - retryInterval = Duration.ofSeconds(2), - logger = logger, - marker = logMarker - ) { - checkHealth() - } - } catch (e: Throwable) { - logger.error( - logMarker, - "$this — Appium Server on port: $appiumServerPort failed to start. Detailed log follows:\n${appiumServerLog.readText()}" - ) - - throw e - } - - logger.debug(logMarker, "$this Appium Server: $childProcess") - } - - override fun kill() { - remote.pkill(remoteAppiumTmpDir.absolutePath, false) - super.kill() - } - - private fun createRemoteAppiumTmpDir() { - remote.shell("mkdir -p ${remoteAppiumTmpDir.absolutePath}") - remote.shell("touch ${remoteAppiumServerLog.absolutePath}") - } - - private fun deleteRemoteAppiumTmpDir() { - remote.shell("rm -rf ${remoteAppiumTmpDir.absolutePath}") - } - - private fun getAppiumServerStartCommand(): List { - val logLevel: String = if (remote.isLocalhost()) { "debug" } else { "info" } - - val command = listOf( - "appium", - "--allow-cors", - "--port", - appiumServerPort.toString(), - "--driver-xcuitest-webdriveragent-port", - wdaPort.toString(), - "--log-level", - logLevel, - "--log-timestamp", - "--log-no-colors", - "--local-timezone", - "--log", - remoteAppiumServerLog.absolutePath, - "--tmp", - remoteAppiumTmpDir.absolutePath - ) - - return if (remote.isLocalhost()) { - command - } else { - listOf( - "/bin/bash", - "-c", - "'/usr/bin/env PATH=${DEFAULT_PATH} ${command.joinToString(" ")}'" // important to send PATH in order to launch Appium correctly - ) - } - } -} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/AppiumServerProcError.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/AppiumServerProcError.kt deleted file mode 100644 index 615ad96b..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/AppiumServerProcError.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.badoo.automation.deviceserver.ios.proc - -import java.lang.IllegalStateException - -class AppiumServerProcError(message: String) : IllegalStateException(message) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt index bcd869b9..9ba66558 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt @@ -22,8 +22,6 @@ open class FbsimctlProc( ) -> ChildProcess = ChildProcess.Companion::fromCommand ) : LongRunningProc(udid, remote.hostName) { private val uri: URI = uriWithPath(fbsimctlEndpoint, "list") - //private val stdOutFile: File = File.createTempFile("", "") - //private val stdErrFile: File = File.createTempFile("", "") override fun toString(): String = "<$udid at ${remote.hostName}:${fbsimctlEndpoint.port}>" @@ -31,17 +29,13 @@ open class FbsimctlProc( ensure(childProcess == null) { FbsimctlProcError("Previous fbsimctl process $childProcess has not been killed") } logger.debug(logMarker, "$this — Starting child process") - val outWriter: (String) -> Unit = { logger.debug(logMarker, it.trim()) } - val errWriter: (String) -> Unit = { logger.warn(logMarker, it.trim()) } - childProcess = childFactory( remote.hostName, remote.userName, - getFbsimctlCommand(), + getFbsimctlCommand(headless), mapOf(), - outWriter, -// null, // outReader, // TODO: write to file o.txt - errWriter // TODO: write to file e.txt + { logger.trace(logMarker, "${this@FbsimctlProc}: FbSimCtl : ${it.trim()}") }, + { logger.debug(logMarker, "${this@FbsimctlProc}: FbSimCtl : ${it.trim()}") } ) logger.debug(logMarker, "$this FBSimCtl: $childProcess") @@ -49,30 +43,37 @@ open class FbsimctlProc( override fun checkHealth(): Boolean { return try { - logger.debug(logMarker, "Checking health for ${javaClass.simpleName} on $udid on url: $uri") val result = client.get(uri.toURL()) - logger.debug(logMarker, "${javaClass.simpleName} on $udid on url: $uri returned result - ${result.httpCode} , ${result.responseBody}, Success: ${result.isSuccess}") return if (result.isSuccess) { true } else { - logger.debug(logMarker, "Failed ${javaClass.simpleName} health check. Result: $result") + logger.debug(logMarker, "Failed fbsimctl health check. Result: $result") false } } catch (e: RuntimeException) { - logger.warn(logMarker, "Failed to determine ${javaClass.simpleName} device state. Exception: $e") + logger.warn(logMarker, "Failed to determine fbsimctl device state. Exception: $e") false } } - protected open fun getFbsimctlCommand(): List { - val cmd = listOf( - remote.fbsimctl.fbsimctlBinary, + protected open fun getFbsimctlCommand(headless: Boolean): List { + val cmd = mutableListOf( + FBSimctl.FBSIMCTL_BIN, udid, + "boot" + ) + + if (headless) { + cmd.add("--direct-launch") + } + + cmd.addAll(listOf( + "--", "listen", "--http", fbsimctlEndpoint.port.toString() - ) + )) return cmd } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/IWebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/IWebDriverAgent.kt new file mode 100644 index 00000000..1b182a72 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/IWebDriverAgent.kt @@ -0,0 +1,8 @@ +package com.badoo.automation.deviceserver.ios.proc + +interface IWebDriverAgent { + fun start() + fun stop() + fun isHealthy(): Boolean + fun installHostApp() +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningFileLoggingProcessListener.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningFileLoggingProcessListener.kt deleted file mode 100644 index c533a58c..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningFileLoggingProcessListener.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.badoo.automation.deviceserver.ios.proc - -import com.zaxxer.nuprocess.NuProcess -import com.zaxxer.nuprocess.NuProcessHandler -import java.io.File -import java.io.FileOutputStream -import java.nio.ByteBuffer -import java.nio.channels.WritableByteChannel - -class LongRunningFileLoggingProcessListener( - private val stdOutFile: File, - private val stdErrFile: File -): NuProcessHandler { - private lateinit var nuProcess: NuProcess - private lateinit var stdErrChannel: WritableByteChannel - private lateinit var stdOutChannel: WritableByteChannel - private var exitCode: Int = Int.MIN_VALUE - - override fun onStdinReady(buffer: ByteBuffer): Boolean { - return false - } - - override fun onPreStart(nuProcess: NuProcess) { - this.nuProcess = nuProcess - - stdOutChannel = FileOutputStream(stdOutFile).channel - stdErrChannel = FileOutputStream(stdErrFile).channel - } - - override fun onStderr(buffer: ByteBuffer, closed: Boolean) { - writeBytesToChannel(buffer, stdErrChannel, closed) - } - - override fun onStdout(buffer: ByteBuffer, closed: Boolean) { - writeBytesToChannel(buffer, stdOutChannel, closed) - } - - override fun onExit(exitCode: Int) { - this.exitCode = exitCode - } - - override fun onStart(nuProcess: NuProcess) { - } - - private fun writeBytesToChannel(buffer: ByteBuffer, channel: WritableByteChannel, closed: Boolean) { - if (buffer.hasRemaining()) { - channel.write(buffer) - - buffer.compact(); - } else { - buffer.clear(); - } - - if (closed) { - channel.close() - } - } -} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProc.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProc.kt index fd389491..be984357 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProc.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProc.kt @@ -4,6 +4,7 @@ import com.badoo.automation.deviceserver.LogMarkers import com.badoo.automation.deviceserver.command.ChildProcess import com.badoo.automation.deviceserver.data.UDID import com.badoo.automation.deviceserver.util.CustomHttpClient +import com.badoo.automation.deviceserver.util.deviceRefFromUDID import net.logstash.logback.marker.MapEntriesAppendingMarker import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -16,7 +17,7 @@ abstract class LongRunningProc(udid: UDID, remoteHostName: String) : ILongRunnin protected val logMarker = MapEntriesAppendingMarker(mapOf( LogMarkers.HOSTNAME to remoteHostName, LogMarkers.UDID to udid, - LogMarkers.DEVICE_REF to "$udid-$remoteHostName".replace(Regex("[^-\\w]"), "-") + LogMarkers.DEVICE_REF to deviceRefFromUDID(udid, remoteHostName) )) @Volatile protected var childProcess: ChildProcess? = null override val isProcessAlive: Boolean get() = true == childProcess?.isAlive() diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt new file mode 100644 index 00000000..e5b2b8ec --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt @@ -0,0 +1,80 @@ +package com.badoo.automation.deviceserver.ios.proc + +import com.badoo.automation.deviceserver.LogMarkers +import com.badoo.automation.deviceserver.data.DeviceRef +import com.badoo.automation.deviceserver.data.UDID +import com.badoo.automation.deviceserver.host.IRemote +import com.badoo.automation.deviceserver.util.pollFor +import net.logstash.logback.marker.MapEntriesAppendingMarker +import java.io.File +import java.net.URI +import java.time.Duration +import java.util.concurrent.TimeUnit +import kotlin.system.measureNanoTime + +class SimulatorWebDriverAgent( + remote: IRemote, + wdaRunnerXctest: File, + udid: UDID, + wdaEndpoint: URI, + mjpegServerPort: Int, + deviceRef: DeviceRef +) : WebDriverAgent( + remote = remote, + wdaRunnerXctest = wdaRunnerXctest, + hostApp = wdaRunnerXctest.parentFile.parentFile.absolutePath, + udid = udid, + wdaEndpoint = wdaEndpoint, + mjpegServerPort = mjpegServerPort +) { + private val commonLogMarkerDetails = mapOf( + LogMarkers.DEVICE_REF to deviceRef, + LogMarkers.UDID to udid, + LogMarkers.HOSTNAME to remote.hostName + ) + + override fun start() { + installHostApp() + super.start() + } + + override fun installHostApp() { + logger.debug(logMarker, "Installing WDA on Simulator with xcrun simctl") + + val nanos = measureNanoTime { + val result = remote.execIgnoringErrors(listOf("xcrun", "simctl", "install", udid, hostApp)) + + if (!result.isSuccess) { + val errorMessage = "Failed to install WebDriverAgent $hostApp to simulator $udid. Result: $result" + logger.error(logMarker, errorMessage) + throw RuntimeException(errorMessage) + } + + pollFor( + Duration.ofSeconds(30), + "Installing WDA host application $hostApp", + true, + Duration.ofSeconds(2), + logger, + logMarker + ) { + isHostAppInstalled() + } + + } + + val seconds = TimeUnit.NANOSECONDS.toSeconds(nanos) + val measurement = mutableMapOf( + "action_name" to "install_WDA", + "duration" to seconds + ) + measurement.putAll(commonLogMarkerDetails) + + logger.debug(MapEntriesAppendingMarker(measurement), "Successfully installed WDA on Simulator with xcrun simctl. Took $seconds seconds") + } + + private fun isHostAppInstalled(): Boolean { + return remote.fbsimctl.listApps(udid) + .any { it.bundle.bundle_id.contains("WebDriverAgentRunner-Runner") } + } +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorXcrunWebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorXcrunWebDriverAgent.kt new file mode 100644 index 00000000..cdf4c87a --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorXcrunWebDriverAgent.kt @@ -0,0 +1,136 @@ +package com.badoo.automation.deviceserver.ios.proc + +import com.badoo.automation.deviceserver.ApplicationConfiguration +import com.badoo.automation.deviceserver.LogMarkers +import com.badoo.automation.deviceserver.data.DeviceRef +import com.badoo.automation.deviceserver.data.UDID +import com.badoo.automation.deviceserver.host.IRemote +import com.badoo.automation.deviceserver.util.CustomHttpClient +import com.badoo.automation.deviceserver.util.pollFor +import com.badoo.automation.deviceserver.util.uriWithPath +import net.logstash.logback.marker.MapEntriesAppendingMarker +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.File +import java.lang.RuntimeException +import java.net.URI +import java.time.Duration +import java.util.concurrent.TimeUnit +import kotlin.system.measureNanoTime + +class SimulatorXcrunWebDriverAgent( + private val remote: IRemote, + wdaRunnerXctest: File, + private val udid: UDID, + private val wdaEndpoint: URI, + private val mjpegServerPort: Int, + deviceRef: DeviceRef, + applicationConfiguration: ApplicationConfiguration = ApplicationConfiguration() +) : IWebDriverAgent { + private val logger: Logger = LoggerFactory.getLogger(javaClass.simpleName) + private val logMarker = MapEntriesAppendingMarker(mapOf( + LogMarkers.HOSTNAME to remote.publicHostName, + LogMarkers.UDID to udid + )) + private val commonLogMarkerDetails = mapOf( + LogMarkers.DEVICE_REF to deviceRef, + LogMarkers.UDID to udid, + LogMarkers.HOSTNAME to remote.hostName + ) + private val hostApp = wdaRunnerXctest.parentFile.parentFile.absolutePath + private val wdaBundleId = applicationConfiguration.wdaSimulatorBundleId + private val uri: URI = uriWithPath(wdaEndpoint, "status") + private val client: CustomHttpClient = CustomHttpClient() + + override fun isHealthy(): Boolean { + return try { + val result = client.get(uri.toURL()) + result.isSuccess + } catch (e: RuntimeException) { + logger.warn(logMarker, "Failed to determine WDA driver state. Exception: $e") + false + } + } + + override fun stop() { + remote.execIgnoringErrors( + listOf( + "/usr/bin/xcrun", + "simctl", + "terminate", + udid, + wdaBundleId + ) + ) + } + + override fun start() { + if (!isHostAppInstalled()) { + installHostApp() + } + + val cmd = listOf( + "/usr/bin/xcrun", + "simctl", + "launch", + udid, + wdaBundleId, + "--port", wdaEndpoint.port.toString(), + "--mjpeg-server-port", mjpegServerPort.toString() + ) + + val result = remote.execIgnoringErrors(cmd) + + if (!result.isSuccess) { + val message = "Failed to start WDA on Simulator with xcrun simctl. Stderr: ${result.stdErr}" + logger.error(logMarker, message) + throw RuntimeException(message) + } + } + + override fun installHostApp() { + logger.debug(logMarker, "Installing WDA on Simulator with xcrun simctl") + + val nanos = measureNanoTime { + val result = remote.execIgnoringErrors(listOf("xcrun", "simctl", "install", udid, hostApp)) + + if (!result.isSuccess) { + val errorMessage = "Failed to install WebDriverAgent $hostApp to simulator $udid. Result: $result" + logger.error(logMarker, errorMessage) + throw RuntimeException(errorMessage) + } + + pollFor( + Duration.ofSeconds(30), + "Installing WDA host application $hostApp", + true, + Duration.ofSeconds(5), + logger, + logMarker + ) { + isHostAppInstalled() + } + } + + val seconds = TimeUnit.NANOSECONDS.toSeconds(nanos) + val measurement = mutableMapOf( + "action_name" to "install_WDA", + "duration" to seconds + ) + measurement.putAll(commonLogMarkerDetails) + + logger.debug(MapEntriesAppendingMarker(measurement), "Successfully installed WDA on Simulator with xcrun simctl. Took $seconds seconds") + } + + private fun isHostAppInstalled(): Boolean { + val result = remote.execIgnoringErrors(listOf( + "/usr/bin/xcrun", + "simctl", + "get_app_container", + udid, + wdaBundleId + )) + + return result.isSuccess + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt new file mode 100644 index 00000000..a4586b57 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt @@ -0,0 +1,88 @@ +package com.badoo.automation.deviceserver.ios.proc + +import com.badoo.automation.deviceserver.command.ChildProcess +import com.badoo.automation.deviceserver.data.UDID +import com.badoo.automation.deviceserver.host.IRemote +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl +import com.badoo.automation.deviceserver.util.ensure +import com.badoo.automation.deviceserver.util.uriWithPath +import java.io.File +import java.net.URI + +open class WebDriverAgent( + protected val remote: IRemote, + protected val wdaRunnerXctest: File, + protected val hostApp: String, + protected val udid: UDID, + private val wdaEndpoint: URI, + port: Int = wdaEndpoint.port, + mjpegServerPort: Int, + private val childFactory: ( + remoteHost: String, + userName: String, + cmd: List, + commandEnvironment: Map, + out_reader: ((line: String) -> Unit)?, + err_reader: ((line: String) -> Unit)? + ) -> ChildProcess = ChildProcess.Companion::fromCommand +) : LongRunningProc(udid, remote.hostName), IWebDriverAgent { + private val launchXctestCommand: List = listOf( + FBSimctl.FBSIMCTL_BIN, + udid, + "launch_xctest", + wdaRunnerXctest.absolutePath, + hostApp, + "--port", + port.toString(), + "--mjpeg-server-port", + mjpegServerPort.toString(), + "--", + "listen" + ) + private val uri: URI = uriWithPath(wdaEndpoint, "status") + + override fun toString(): String = "<$udid at ${remote.hostName}:${wdaEndpoint.port}>" + + override fun installHostApp() { + remote.fbsimctl.installApp(udid, wdaRunnerXctest) + } + + override fun start() { + ensure(childProcess == null) { WebDriverAgentError("Previous WebDriverAgent childProcess $childProcess has not been killed") } + ensure(remote.isDirectory(wdaRunnerXctest.absolutePath)) { WebDriverAgentError("WebDriverAgent ${wdaRunnerXctest.absolutePath} does not exist or is not a directory") } + logger.debug(logMarker, "$this — Starting child process") + + terminateHostApp() + + childProcess = childFactory( + remote.hostName, + remote.userName, + launchXctestCommand, + mapOf(), + null, + { message -> logger.warn(logMarker, "${this@WebDriverAgent}: WDA : ${message.trim()}") } + ) + + Thread.sleep(5000) // 5 should be ok + logger.debug(logMarker, "$this WDA: $childProcess") + } + + override fun stop() { + kill() + } + + protected open fun terminateHostApp() { + remote.fbsimctl.terminateApp(udid, bundleId = hostApp, raiseOnError = false) + Thread.sleep(1000) + } + + override fun checkHealth(): Boolean { + return try { + val result = client.get(uri.toURL()) + return result.isSuccess + } catch (e: RuntimeException) { + logger.warn(logMarker, "Failed to determine WDA driver state. Exception: $e") + false + } + } +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/XCTestInstrumentationAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/XCTestInstrumentationAgent.kt deleted file mode 100644 index 4a4ca1a4..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/XCTestInstrumentationAgent.kt +++ /dev/null @@ -1,249 +0,0 @@ -package com.badoo.automation.deviceserver.ios.proc - -import com.badoo.automation.deviceserver.LogMarkers -import com.badoo.automation.deviceserver.command.ChildProcess -import com.badoo.automation.deviceserver.data.DeviceInfo -import com.badoo.automation.deviceserver.data.DeviceRef -import com.badoo.automation.deviceserver.host.IRemote -import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException -import com.badoo.automation.deviceserver.util.* -import java.io.File -import java.net.URI -import java.nio.file.Files -import java.nio.file.StandardOpenOption -import java.time.Duration - -class XCTestInstrumentationAgent( - private val remote: IRemote, - private val wdaBundles: List, - private val deviceInfo: DeviceInfo, - private val wdaEndpoint: URI, - private val mjpegServerPort: Int, - private val deviceRef: DeviceRef, - private val isRealDevice: Boolean, - private val childFactory: ( - remoteHost: String, - userName: String, - cmd: List, - commandEnvironment: Map, - out_reader: ((line: String) -> Unit)?, - err_reader: ((line: String) -> Unit)? - ) -> ChildProcess = ChildProcess.Companion::fromCommand -) : LongRunningProc(deviceInfo.udid, remote.hostName) { - private val udid = deviceInfo.udid - private val derivedDataDir = - remote.shell("/usr/bin/mktemp -d -t derivedDataDir_$udid", returnOnFailure = false).stdOut.trim() - private val xctestrunDir = - remote.shell("/usr/bin/mktemp -d -t xctestRunDir_$udid", returnOnFailure = false).stdOut.trim() - val xctestrunSuffix = "WebDriverAgent_$udid.xctestrun" - private val xctestrunFile = File(xctestrunDir, xctestrunSuffix) - private val commonLogMarkerDetails = mapOf( - LogMarkers.DEVICE_REF to deviceRef, - LogMarkers.UDID to udid, - LogMarkers.HOSTNAME to remote.hostName - ) - - private val instrumentationDaBundle = getWdaBundle("sh.calaba.DeviceAgent") - private val instrumentationWdaBundle = getWdaBundle("com.facebook.WebDriverAgentRunner") - - private fun getInstrumentationBundle(useAppium: Boolean): WdaBundle { - return if (useAppium) instrumentationWdaBundle else instrumentationDaBundle - } - - private fun getWdaBundle(instrumentationBundleId: String): WdaBundle { - return wdaBundles.find { - it.bundleId.startsWith(instrumentationBundleId) && - (!isRealDevice || it.provisionedDevices.any { it.equals(udid, ignoreCase = true) }) - } - ?: throw DeviceNotFoundException("Device with $udid does not have any $instrumentationBundleId bundle that has it's udid provisioned") - } - - private val launchXctestCommand: List = listOf( - "/usr/bin/xcodebuild", - "test-without-building", - "-xctestrun", - xctestrunFile.absolutePath, - "-destination", - "id=$udid", - "-derivedDataPath", - derivedDataDir - ) - - private fun prepareXctestrunFile(instrumentationBundle: WdaBundle) { - val xctestRunnerPath: File = instrumentationBundle.xctestRunnerPath(remote.isLocalhost()) - val xctestRunnerRelativePath = File(xctestRunnerPath.parentFile.name, xctestRunnerPath.name).toString() - val instrumentationPort = if (isRealDevice) instrumentationBundle.deviceInstrumentationPort else wdaEndpoint.port - val xctestRunContents = xctestRunTemplate - .replace("__DEVICE_AGENT_PORT__", "$instrumentationPort") - .replace("__DEVICE_AGENT_MJPEG_PORT__", "$mjpegServerPort") - .replace("__DEVICE_AGENT_BINARY_PATH__", instrumentationBundle.bundlePath(remote.isLocalhost()).absolutePath) - .replace("__DEVICE_AGENT_BUNDLE_ID__", instrumentationBundle.bundleId) - .replace("__BLUEPRINT_NAME__", instrumentationBundle.bundleName) - .replace("__PRODUCT_MODULE_NAME__", instrumentationBundle.bundleName) - .replace("__TEST_IDENTIFIER__", instrumentationBundle.testIdentifier) - .replace("__TESTBUNDLE_DESTINATION_RELATIVE_PATH__", xctestRunnerRelativePath) - - // real device Xcode 15 and iOS 17 - .replace("__DEVICE_AGENT_FULL_PATH_ON_MAC__", instrumentationBundle.bundlePath(remote.isLocalhost()).absolutePath) - - - if (remote.isLocalhost()) { - xctestrunFile.writeText(xctestRunContents) - } else { - val tmpFile = File.createTempFile("xctestRunDir_$udid.", ".xctestrun") - tmpFile.writeText(xctestRunContents) - remote.scpToRemoteHost(tmpFile.absolutePath, xctestrunFile.absolutePath) - tmpFile.delete() - } - } - - private val xctestRunTemplate: String by lazy { - if (isRealDevice) { - if (deviceInfo.osMajorVersion() >= 17) { - xctestrunRealDeviceTemplateXcode15 - } else { - xctestrunRealDeviceTemplateXcode13 - } - } else { - xctestrunSimulatorTemplate - } - } - - private val uri: URI get() { - val statusPath = if (useWebDriverAgent) "status" else "1.0/status" - return uriWithPath(wdaEndpoint, statusPath) - } - - override fun toString(): String = "<$udid at ${remote.hostName}:${wdaEndpoint.port}>" - - private fun installHostApp(instrumentationBundle: WdaBundle) { - remote.fbsimctl.installApp(udid, instrumentationBundle.bundlePath(remote.isLocalhost())) - val timeout = 3000L - logger.debug("Waiting $timeout ms after install") - Thread.sleep(timeout) - } - - val deviceAgentLog: File = File.createTempFile("web_driver_agent_log_", ".txt") - - @Volatile - private var wdaRunnerStarted = false - - override fun start() { - TODO("Not implemented. Use start(useAppium: Boolean)") - } - - @Volatile - var useWebDriverAgent: Boolean = true // use WebDriverAgent for Appium or DeviceAgent for Calabash - - fun start(useAppium: Boolean) { - val wdaProcess = childProcess - ensure(wdaProcess == null || wdaProcess.isAlive() == false) { WebDriverAgentError("Previous WebDriverAgent childProcess $childProcess has not been killed") } - - useWebDriverAgent = useAppium - val instrumentationBundle = getInstrumentationBundle(useAppium) - ensure(remote.isDirectory(instrumentationBundle.bundlePath(remote.isLocalhost()).absolutePath)) { WebDriverAgentError("WebDriverAgent ${instrumentationBundle.bundlePath(remote.isLocalhost()).absolutePath} does not exist or is not a directory") } - logger.debug(logMarker, "$this — Starting child process WebDriverAgent on: $wdaEndpoint with bundle id: ${instrumentationBundle.bundleId}") - - cleanupLogs() - prepareXctestrunFile(instrumentationBundle) - - listOf(instrumentationDaBundle.bundleId, instrumentationWdaBundle.bundleId).forEach { - remote.fbsimctl.uninstallApp(udid, it, false) - } - - installHostApp(instrumentationBundle) - - val process = childFactory( - remote.hostName, - remote.userName, - launchXctestCommand, - mapOf(), - { message -> - deviceAgentLog.appendText(message + "\n") - if (!wdaRunnerStarted && (message.contains("ServerURLHere") || message.contains("CalabashXCUITestServer started"))) { - wdaRunnerStarted = true - logger.debug(logMarker, "$this — WebDriverAgent has reported that it has Started HTTP server on port: ${wdaEndpoint.port} with bundle id: ${instrumentationBundle.bundleId} . Message: $message") - } - }, - { message -> deviceAgentLog.appendText(message + "\n") } - ) - - childProcess = process - - try { - pollFor( - Duration.ofSeconds(45), - reasonName = "$this Waiting for WebDriverAgent to start serving requests", - retryInterval = Duration.ofSeconds(1), - logger = logger, - marker = logMarker - ) { - wdaRunnerStarted - } - } catch (e: Throwable) { - wdaRunnerStarted = false - logger.error(logMarker, "$this — WebDriverAgent on: $wdaEndpoint with bundle id: ${instrumentationBundle.bundleId} failed to start. Detailed log follows:") - deviceAgentLog.readLines().forEach { logger.error("WDA OUT: $it") } - throw e - } - - Thread.sleep(2000) // 2 extra should be ok - logger.debug(logMarker, "$this WDA: $childProcess") - } - - private fun truncateAgentLog() { - Files.write(deviceAgentLog.toPath(), ByteArray(0), StandardOpenOption.TRUNCATE_EXISTING) - } - - private fun cleanupLogs() { - remote.shell("rm -rf $derivedDataDir", false) - remote.shell("mkdir -p $derivedDataDir", true) - remote.shell("rm -f $xctestrunFile", false) - truncateAgentLog() - } - - private fun terminateHostApp() { - wdaRunnerStarted = false - listOf(instrumentationDaBundle.bundleId, instrumentationWdaBundle.bundleId).forEach { - remote.fbsimctl.terminateApp(udid, bundleId = it, raiseOnError = false) - } - - Thread.sleep(1000) - remote.pkill(xctestrunSuffix, false) - Thread.sleep(3000) - } - - override fun kill() { - terminateHostApp() - super.kill() - } - - override fun checkHealth(): Boolean { - if (!wdaRunnerStarted) { - logger.debug(logMarker, "$this WebDriverAgent has not yet started.") - return false - } - - return try { - val url = uri.toURL() - val success = client.get(url).isSuccess - logger.debug(logMarker, "Checking health for WebDriverAgent on $udid on url: $url - Result: ${if (success) "Success" else "Failure"}") - return success - } catch (e: RuntimeException) { - logger.warn(logMarker, "Failed to determine WDA driver state. Exception: $e") - false - } - } - - companion object { - val xctestrunSimulatorTemplate: String = XCTestInstrumentationAgent::class.java.classLoader - .getResource("WebDriverAgent-Simulator.template.xctestrun")?.readText() - ?: throw RuntimeException("Failed to read file WebDriverAgent-Simulator.template.xctestrun from resources") - val xctestrunRealDeviceTemplateXcode13: String = XCTestInstrumentationAgent::class.java.classLoader - .getResource("WebDriverAgent-RealDevice-Xcode13.template.xctestrun")?.readText() - ?: throw RuntimeException("Failed to read file WebDriverAgent-RealDevice-Xcode13.template.xctestrun from resources") - val xctestrunRealDeviceTemplateXcode15: String = XCTestInstrumentationAgent::class.java.classLoader - .getResource("WebDriverAgent-RealDevice-Xcode15.template.xctestrun")?.readText() - ?: throw RuntimeException("Failed to read file WebDriverAgent-RealDevice-Xcode15.template.xctestrun from resources") - } -} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simctl/dto/XCRunSimctlDTO.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simctl/dto/XCRunSimctlDTO.kt deleted file mode 100644 index 39c3408d..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simctl/dto/XCRunSimctlDTO.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.badoo.automation.deviceserver.ios.simctl.dto - -data class ListResponseDTO( - val devicetypes: List, - val runtimes: List, - val devices: Map>, - val pairs: Map -) - -data class DeviceType( - val bundlePath: String, - val name: String, - val identifier: String, - val productFamily: String, - val minRuntimeVersion: Int = 0, - val maxRuntimeVersion: Int = 0 -) { - /** - * Not including minRuntimeVersion and maxRuntimeVersion - */ - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as DeviceType - - if (bundlePath != other.bundlePath) return false - if (name != other.name) return false - if (identifier != other.identifier) return false - if (productFamily != other.productFamily) return false - - return true - } - - /** - * Not including minRuntimeVersion and maxRuntimeVersion - */ - override fun hashCode(): Int { - var result = bundlePath.hashCode() - result = 31 * result + name.hashCode() - result = 31 * result + identifier.hashCode() - result = 31 * result + productFamily.hashCode() - return result - } -} - -data class Device( - val dataPath: String, - val logPath: String, - val udid: String, - val isAvailable: Boolean, - val deviceTypeIdentifier: String, - val state: String, - val name: String -) - -data class RunTime( - val bundlePath: String, - val buildversion: String, - val runtimeRoot: String, - val identifier: String, - val version: String, - val isAvailable: Boolean, - val name: String, - val supportedDeviceTypes: List -) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt index a0115088..84e7dec1 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt @@ -1,28 +1,47 @@ package com.badoo.automation.deviceserver.ios.simulator -import com.badoo.automation.deviceserver.data.CrashLog -import com.badoo.automation.deviceserver.data.PermissionSet -import com.badoo.automation.deviceserver.ios.IDevice +import com.badoo.automation.deviceserver.data.* import com.badoo.automation.deviceserver.ios.simulator.data.DataContainer import com.badoo.automation.deviceserver.ios.simulator.data.Media -import com.badoo.automation.deviceserver.ios.simulator.data.SharedContainer +import com.badoo.automation.deviceserver.ios.simulator.diagnostic.OsLog +import com.badoo.automation.deviceserver.ios.simulator.diagnostic.SystemLog +import com.badoo.automation.deviceserver.ios.simulator.video.VideoRecorder +import java.net.URI +import java.net.URL -interface ISimulator: IDevice { +interface ISimulator { + // FIXME: cleanup unnecessary properties from interface (copied attr_reader from ruby as is) + val ref: DeviceRef + val udid: UDID + val state: DeviceState + val fbsimctlEndpoint: URI + val wdaEndpoint: URI + val userPorts: DeviceAllocatedPorts + val info: DeviceInfo + val lastError: Exception? + val calabashPort: Int + val mjpegServerPort: Int + val videoRecorder: VideoRecorder + val fbsimctlSubject: String + val systemLog: SystemLog + val osLog: OsLog val media: Media - val locationManager: LocationManager - fun resetAsync(): Runnable - fun sendPushNotification(bundleId: String, notificationContent: ByteArray) - fun sendPasteboard(payload: ByteArray) + fun prepareAsync() + fun resetAsync() + fun status(): SimulatorStatusDTO + fun endpointFor(port: Int): URL + fun approveAccess(bundleId: String) fun setPermissions(bundleId: String, permissions: PermissionSet) + fun release(reason: String) fun clearSafariCookies(): Map fun shake(): Boolean fun openUrl(url: String): Boolean + fun lastCrashLog(): CrashLog fun crashLogs(pastMinutes: Long?): List fun dataContainer(bundleId: String): DataContainer - fun sharedContainer(): SharedContainer + fun uninstallApplication(bundleId: String) fun deleteCrashLogs(): Boolean fun setEnvironmentVariables(envs: Map) - fun getEnvironmentVariable(variableName: String): String fun applicationContainer(bundleId: String): DataContainer } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/LocationManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/LocationManager.kt deleted file mode 100644 index 1ffd98e7..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/LocationManager.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.badoo.automation.deviceserver.ios.simulator - -import com.badoo.automation.deviceserver.data.LocationDto -import com.badoo.automation.deviceserver.data.UDID -import com.badoo.automation.deviceserver.host.IRemote - -class LocationManager( - private val remote: IRemote, - private val udid: UDID -) { - fun clear() { - val result = remote.shell("/usr/bin/xcrun simctl location $udid clear") - - if (!result.isSuccess) { - throw RuntimeException("Could not clear location on device $udid: $result") - } - } - - fun listScenarios(): List { - val result = remote.shell("/usr/bin/xcrun simctl location $udid list") - - if (!result.isSuccess) { - throw RuntimeException("Could not list location scenarios on device $udid: $result") - } - - return result.stdOut.lines() - } - - fun setLocation(latitude: Double, longitude: Double) { - val result = remote.shell("/usr/bin/xcrun simctl location $udid set ${latitude},${longitude}") - - if (!result.isSuccess) { - throw RuntimeException("Could not set location to ${latitude},${longitude} on device $udid: $result") - } - } - - fun runScenario(scenarioName: String) { - val result = remote.shell("/usr/bin/xcrun simctl location $udid run \"$scenarioName\"") - - if (!result.isSuccess) { - throw RuntimeException("Could not run scenario \"$scenarioName\" on device $udid: $result") - } - } - - fun startLocationSequence(speed: Int, distance: Int, interval: Int, coords: List) { - check(!(distance > 0 && interval > 0)) { - "Distance and Interval are mutually exclusive parameters. Please set only one. Current values are: distance:$distance, interval:$interval" - } - - check(coords.isNotEmpty()) { - "Coordinates list must not be empty." - } - - val command = StringBuilder("/usr/bin/xcrun simctl location $udid start") - - if (speed > 0) { - command.append(" --speed=$speed") - } - - if (distance > 0) { - command.append(" --distance=$distance") - } - - if (interval > 0) { - command.append(" --interval=$interval") - } - - command.append(" ") - command.append(coords.joinToString(" ") { "${it.latitude},${it.longitude}" }) - - val result = remote.shell(command.toString()) - - if (!result.isSuccess) { - throw RuntimeException("Could not start location sequence on device $udid: $result") - } - } -} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt index 240931c9..be5a2866 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt @@ -2,136 +2,85 @@ package com.badoo.automation.deviceserver.ios.simulator import com.badoo.automation.deviceserver.ApplicationConfiguration import com.badoo.automation.deviceserver.LogMarkers -import com.badoo.automation.deviceserver.command.CommandResult +import com.badoo.automation.deviceserver.WaitTimeoutError import com.badoo.automation.deviceserver.command.ShellUtils import com.badoo.automation.deviceserver.data.* import com.badoo.automation.deviceserver.host.IRemote -import com.badoo.automation.deviceserver.host.management.errors.DeviceCreationException -import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlAppInfo -import com.badoo.automation.deviceserver.ios.proc.* +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDeviceState +import com.badoo.automation.deviceserver.ios.proc.FbsimctlProc +import com.badoo.automation.deviceserver.ios.proc.IWebDriverAgent +import com.badoo.automation.deviceserver.ios.proc.SimulatorWebDriverAgent +import com.badoo.automation.deviceserver.ios.proc.SimulatorXcrunWebDriverAgent import com.badoo.automation.deviceserver.ios.simulator.backup.ISimulatorBackup import com.badoo.automation.deviceserver.ios.simulator.backup.SimulatorBackup -import com.badoo.automation.deviceserver.ios.simulator.backup.SimulatorBackupError -import com.badoo.automation.deviceserver.ios.simulator.data.* +import com.badoo.automation.deviceserver.ios.simulator.data.DataContainer +import com.badoo.automation.deviceserver.ios.simulator.data.FileSystem +import com.badoo.automation.deviceserver.ios.simulator.data.Media import com.badoo.automation.deviceserver.ios.simulator.diagnostic.OsLog -import com.badoo.automation.deviceserver.ios.simulator.video.FFMPEGVideoRecorder +import com.badoo.automation.deviceserver.ios.simulator.diagnostic.SystemLog +import com.badoo.automation.deviceserver.ios.simulator.video.MJPEGVideoRecorder +import com.badoo.automation.deviceserver.ios.simulator.video.SimulatorVideoRecorder import com.badoo.automation.deviceserver.ios.simulator.video.VideoRecorder -import com.badoo.automation.deviceserver.util.* -import kotlinx.coroutines.experimental.Job -import kotlinx.coroutines.experimental.Runnable -import kotlinx.coroutines.experimental.delay -import kotlinx.coroutines.experimental.launch +import com.badoo.automation.deviceserver.util.executeWithTimeout +import com.badoo.automation.deviceserver.util.pollFor +import kotlinx.coroutines.experimental.* import net.logstash.logback.marker.MapEntriesAppendingMarker import org.slf4j.LoggerFactory import org.slf4j.Marker -import java.io.* +import java.io.File +import java.io.FileOutputStream +import java.io.IOException import java.net.URI import java.net.URL -import java.nio.charset.StandardCharsets import java.nio.file.Paths import java.time.Duration -import java.util.concurrent.ExecutorService -import java.util.concurrent.Future -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeUnit.NANOSECONDS import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock -import kotlin.system.measureNanoTime +import kotlin.system.measureTimeMillis -class Simulator( +class Simulator ( private val deviceRef: DeviceRef, private val remote: IRemote, - override val deviceInfo: DeviceInfo, + private val deviceInfo: DeviceInfo, private val allocatedPorts: DeviceAllocatedPorts, private val deviceSetPath: String, - private val wdaSimulatorBundles: WdaSimulatorBundles, - private val concurrentBootsPool: ExecutorService, + private val wdaRunnerXctest: File, + private val concurrentBootsPool: ThreadPoolDispatcher, headless: Boolean, private val useWda: Boolean, - private val useAppium: Boolean, - private val appConfig: ApplicationConfiguration = ApplicationConfiguration(), - private val trustStoreFile: String = appConfig.trustStorePath, - private val assetsPath: String = appConfig.assetsPath + override val fbsimctlSubject: String, + private val configuration: ApplicationConfiguration = ApplicationConfiguration(), + private val trustStoreFile: String = configuration.trustStorePath, + private val assetsPath: String = configuration.assetsPath ) : ISimulator { private companion object { - private val PREPARE_TIMEOUT: Duration = Duration.ofMinutes(10) - private val RESET_TIMEOUT: Duration = Duration.ofMinutes(5) + private val PREPARE_TIMEOUT: Duration = Duration.ofMinutes(4) + private val RESET_TIMEOUT: Duration = Duration.ofMinutes(3) private const val SAFARI_BUNDLE_ID = "com.apple.mobilesafari" - private val ENV_VAR_VALIDATE_REGEX = "[a-zA-Z0-9_]+$".toRegex() } override val ref = deviceRef - override val udid: UDID = deviceInfo.udid + override val udid = deviceInfo.udid override val fbsimctlEndpoint = URI("http://${remote.publicHostName}:${allocatedPorts.fbsimctlPort}/$udid/") - override val wdaEndpoint = URI("http://${remote.publicHostName}:${allocatedPorts.wdaPort}/") - override val calabashEndpoint = URI("http://${remote.publicHostName}:${allocatedPorts.calabashPort}/") - override val appiumEndpoint = URI("http://${remote.publicHostName}:${allocatedPorts.appiumPort}") + override val wdaEndpoint= URI("http://${remote.publicHostName}:${allocatedPorts.wdaPort}/") + override val userPorts = allocatedPorts + override val info = deviceInfo override val calabashPort: Int = allocatedPorts.calabashPort override val mjpegServerPort: Int = allocatedPorts.mjpegServerPort - override val appiumPort: Int = allocatedPorts.appiumPort - override val locationManager: LocationManager = LocationManager(remote, udid) - - override val isAppiumEnabled get() = useAppium - - private fun createVideoRecorder(): VideoRecorder { - val recorderClassName = appConfig.videoRecorderClassName - - return when (recorderClassName) { - FFMPEGVideoRecorder::class.qualifiedName -> FFMPEGVideoRecorder( - remote, - mjpegServerPort, - ref, - udid - ) - else -> throw IllegalArgumentException( - "Wrong class specified as video recorder: $recorderClassName. " + - "Available are: [${FFMPEGVideoRecorder::class.qualifiedName}]" - ) - } - } override val videoRecorder: VideoRecorder = createVideoRecorder() - + override val systemLog = SystemLog(remote, udid) override val osLog = OsLog(remote, udid) //region instance state variables private val deviceLock = ReentrantLock() - @Volatile override var deviceState: DeviceState = DeviceState.NONE // writing from separate thread - private set - - @Volatile override var lastException: Exception? = null // writing from separate thread - private set + @Volatile private var deviceState: DeviceState = DeviceState.NONE // writing from separate thread + @Volatile private var lastException: Exception? = null // writing from separate thread + private lateinit var criticalAsyncPromise: Job // 1-1 from ruby private val fbsimctlProc: FbsimctlProc = FbsimctlProc(remote, deviceInfo.udid, fbsimctlEndpoint, headless) - private val simulatorProcess = SimulatorProcess(remote, udid, deviceRef) - - private val instrumentationAgent = XCTestInstrumentationAgent( - remote, - listOf(wdaSimulatorBundles.deviceAgentBundle, wdaSimulatorBundles.webDriverAgentBundle), - deviceInfo, - wdaEndpoint, - mjpegServerPort, - deviceRef, - isRealDevice = false - ) - - private val appiumServer: AppiumServer = AppiumServer( - remote, - udid, - appiumPort, - allocatedPorts.wdaPort - ) - - override val instrumentationAgentLog get() = instrumentationAgent.deviceAgentLog - - override val appiumServerLog get() = appiumServer.appiumServerLog - - override fun deleteAppiumServerLog() = appiumServer.deleteAppiumServerLog() - - private val simulatorDirectory = File(deviceSetPath, udid) - private val simulatorDataDirectory = File(simulatorDirectory, "data") - - private val backup: ISimulatorBackup = SimulatorBackup(remote, udid, deviceSetPath, simulatorDirectory, simulatorDataDirectory) + private val webDriverAgent: IWebDriverAgent = createWebDriverAgent() + private val backup: ISimulatorBackup = SimulatorBackup(remote, udid, deviceSetPath) private val logger = LoggerFactory.getLogger(javaClass.simpleName) private val commonLogMarkerDetails = mapOf( LogMarkers.DEVICE_REF to deviceRef, @@ -140,90 +89,56 @@ class Simulator( ) private val logMarker: Marker = MapEntriesAppendingMarker(commonLogMarkerDetails) private val fileSystem = FileSystem(remote, udid) + private val simulatorProcess = SimulatorProcess(remote, udid, deviceRef) @Volatile private var healthChecker: Job? = null //endregion override val media: Media = Media(remote, udid, deviceSetPath) - override fun toString() = "" - - @Volatile - private var bootTask: Future<*>? = null - @Volatile - private var installTask: Future? = null - - override fun getInstallTask(): Future? = installTask - - override fun installApplication( - appInstaller: AppInstaller, - appBundleId: String, - appBinaryPath: File - ) { - deviceLock.withLock { - installTask?.let { oldInstallTask -> - if (!oldInstallTask.isDone) { - val message = "Failed to install app $appBundleId to simulator $udid due to previous task is not finished" - logger.error(logMarker, message) - throw RuntimeException(message) - } - } + //region properties from ruby with backing mutable field + override val state get() = deviceState + override val lastError get() = lastException + //endregion - installTask = appInstaller.installApplication(udid, appBundleId, appBinaryPath, false) - } - } + override fun toString() = "" //region prepareAsync override fun prepareAsync() { + stopPeriodicHealthCheck() executeCritical { - if (deviceState == DeviceState.CREATING || deviceState == DeviceState.RESETTING) { - throw java.lang.IllegalStateException("Simulator $udid is already in state $deviceState") - } deviceState = DeviceState.CREATING + } - val nanos = measureNanoTime { + executeCriticalAsync { + val elapsed = measureTimeMillis { try { - shutdown() prepare(clean = true) } catch (e: Exception) { // catching most wide exception - deviceState = DeviceState.FAILED + executeCritical { + deviceState = DeviceState.FAILED + } logger.error(logMarker, "Failed to prepare device ${this@Simulator}", e) - shutdown() - disposeResources() throw e } } - - val seconds = NANOSECONDS.toSeconds(nanos) - val measurement = mutableMapOf( - "action_name" to "prepareAsync", - "duration" to seconds - ) - measurement.putAll(commonLogMarkerDetails) - - logger.info(MapEntriesAppendingMarker(measurement), "Device ${this@Simulator} ready in $seconds seconds") + logger.info(logMarker, "Device ${this@Simulator} ready in ${elapsed / 1000} seconds") } } private fun prepare(timeout: Duration = PREPARE_TIMEOUT, clean: Boolean) { logger.info(logMarker, "Starting to prepare ${this@Simulator}. Will wait for ${timeout.seconds} seconds") lastException = null + webDriverAgent.stop() + shutdown() //FIXME: add checks for cancellation of criticalAsyncPromise executeWithTimeout(timeout, "Preparing simulator") { // erase simulator if there is no existing backup, this is to ensure backup is created from a clean state logger.info(logMarker, "Launch prepare sequence for ${this@Simulator} asynchronously") - appiumServer.kill() if (backup.isExist()) { if (clean) { - try { - backup.restore() - } catch (e: SimulatorBackupError) { - logger.warn(logMarker, "Will erase simulator and re-create backup for ${this@Simulator}") - shutdown() - backup.delete() - eraseSimulatorAndCreateBackup() - } + backup.restore() } } else { eraseSimulatorAndCreateBackup() @@ -231,234 +146,72 @@ class Simulator( logTiming("simulator boot") { boot() } - dismissTutorials() - - if (appConfig.useTestHelperApp) { - installTestHelperApp() - } - - fbsimctlProc.start() - if (useWda) { logTiming("starting WebDriverAgent") { startWdaWithRetry() } } - if (useAppium) { - logTiming("starting Appium Server") { - appiumServer.start() - } - } - logger.info(logMarker, "Finished preparing $this") - startPeriodicHealthCheck() deviceState = DeviceState.CREATED + startPeriodicHealthCheck() } } - private fun installTestHelperApp() { - val testHelperAppBundle = File(appConfig.remoteTestHelperAppBundleRoot, "TestHelper.app") - if (!remote.shell("test -d ${testHelperAppBundle.absolutePath}").isSuccess) { - logger.error(logMarker, "Failed to install Test Helper app. App directory does not exist: ${testHelperAppBundle.absolutePath}") - } - - logger.debug(logMarker, "Installing Test Helper app on Simulator $udid with xcrun simctl") - - val nanos = measureNanoTime { - val result = remote.execIgnoringErrors(listOf("xcrun", "simctl", "install", udid, testHelperAppBundle.absolutePath), timeOutSeconds = 120) - - if (!result.isSuccess) { - val errorMessage = "Failed to install TestHelper app $testHelperAppBundle.absolutePath to simulator $udid. Result: $result" - logger.error(logMarker, errorMessage) - throw RuntimeException(errorMessage) - } - - pollFor( - Duration.ofSeconds(60), - "Installing TestHelper host application ${testHelperAppBundle.absolutePath}", - true, - Duration.ofSeconds(5), - logger, - logMarker - ) { - remote.execIgnoringErrors(listOf( - "/usr/bin/xcrun", - "simctl", - "get_app_container", - udid, - "com.bumble.automation.TestHelper" - )).isSuccess - - } - } - - val seconds = TimeUnit.NANOSECONDS.toSeconds(nanos) - val measurement = mutableMapOf( - "action_name" to "install_TestHelperApp", - "duration" to seconds - ) - measurement.putAll(commonLogMarkerDetails) - - logger.debug(MapEntriesAppendingMarker(measurement), "Successfully installed TestHelper app on Simulator with xcrun simctl. Took $seconds seconds") - } - - private fun dismissTutorials() { - logger.info(logMarker, "Saving Preference that Continuous Path Introduction was shown") - writeSimulatorDefaults("com.apple.Preferences DidShowContinuousPathIntroduction -bool true") // iOS 13 - writeSimulatorDefaults("com.apple.keyboard.preferences DidShowContinuousPathIntroduction -bool true") // iOS 14.5 and up - writeSimulatorDefaults("com.apple.mobileslideshow LastWhatsNewShown -int 7") // iOS 15.0 What's New - writeSimulatorDefaults("com.apple.suggestions SuggestionsAppLibraryEnabled -bool false") // iOS 15.0 What's New - writeSimulatorDefaults("com.apple.mt KeepAppsUpToDateAppList -dict com.apple.news 0") // iOS 15.0 News App - writeSimulatorDefaults("com.apple.suggestions SiriCanLearnFromAppBlacklist -array com.apple.mobileslideshow com.apple.mobilesafari") // iOS 15.0 News App - } - private fun startPeriodicHealthCheck() { stopPeriodicHealthCheck() - val fbsimctlFailCount = 0 - val wdaFailCount = 0 + var fbsimctlFailCount = 0 + var wdaFailCount = 0 val maxFailCount = 3 - val healthCheckInterval = Duration.ofSeconds(15).toMillis() + val healthCheckInterval = Duration.ofSeconds(10).toMillis() healthChecker = launch { while (isActive) { - performFBSimctlHealthCheck(fbsimctlFailCount, maxFailCount) - - if (useWda) { - performInstrumentationAgentHealthCheck(wdaFailCount, maxFailCount) - } - - if (useAppium) { - performAppiumServerHealthCheck() - } - - delay(healthCheckInterval) - } - } - } - - private suspend fun performInstrumentationAgentHealthCheck(wdaFailCount: Int, maxFailCount: Int) { - var wdaFailCount1 = wdaFailCount - if (instrumentationAgent.isHealthy()) { - wdaFailCount1 = 0 - } else { - (1..5).forEach { - if (instrumentationAgent.isHealthy()) { - wdaFailCount1 = 0 - return@forEach + if (fbsimctlProc.isHealthy()) { + fbsimctlFailCount = 0 } else { - val message = "WebDriverAgent health check failed $wdaFailCount1 times." - logger.error(logMarker, message) - wdaFailCount1 += 1 - delay(Duration.ofSeconds(2).toMillis()) - } - } - - if (wdaFailCount1 >= maxFailCount) { - logger.error(logMarker, "WebDriverAgent health check failed $wdaFailCount1 times. Restarting WebDriverAgent") - - try { - instrumentationAgent.kill() - } catch (e: RuntimeException) { - logger.error(logMarker, "Failed to kill WebDriverAgent. ${e.message}", e) - } + fbsimctlFailCount += 1 - try { - startWdaWithRetry() - } catch (e: RuntimeException) { - logger.error(logMarker, "Failed to restart WebDriverAgent. ${e.message}", e) - deviceState = DeviceState.FAILED - throw RuntimeException("${this@Simulator} Failed to restart WebDriverAgent. Stopping health check") + if (fbsimctlFailCount >= maxFailCount) { + deviceState = DeviceState.FAILED + val message = "Fbsimctl health check failed $fbsimctlFailCount times. Setting device state to $deviceState" + logger.error(logMarker, message) + throw RuntimeException("${this@Simulator} $message. Stopping health check") + } } - } - } - } - - private suspend fun performAppiumServerHealthCheck() { - val maxFailCount = 5 - var appiumFailCount = 0 - - while (!appiumServer.isHealthy() && appiumFailCount < maxFailCount) { - logger.error(logMarker, "Appium health check failed $appiumFailCount times.") - appiumFailCount += 1 - delay(Duration.ofSeconds(2).toMillis()) - } - - if (appiumFailCount >= maxFailCount) { - logger.error(logMarker, "Appium health check failed $appiumFailCount times. Restarting Appium") - - try { - appiumServer.kill() - } catch (e: RuntimeException) { - logger.error(logMarker, "Failed to kill Appium. ${e.message}", e) - } - try { - appiumServer.start() - } catch (e: RuntimeException) { - logger.error(logMarker, "Failed to restart Appium. ${e.message}", e) - deviceState = DeviceState.FAILED - throw RuntimeException("${this@Simulator} Failed to restart Appium. Stopping health check") - } - } - } - - private suspend fun performFBSimctlHealthCheck(fbsimctlFailCount: Int, maxFailCount: Int) { - var fbsimctlFailCount1 = fbsimctlFailCount - if (fbsimctlProc.isHealthy()) { - fbsimctlFailCount1 = 0 - } else { - (1..5).forEach { - if (fbsimctlProc.isHealthy()) { - fbsimctlFailCount1 = 0 - return@forEach + if (webDriverAgent.isHealthy()) { + wdaFailCount = 0 } else { - val message = "Fbsimctl health check failed $fbsimctlFailCount1 times." - logger.error(logMarker, message) - fbsimctlFailCount1 += 1 - delay(Duration.ofSeconds(2).toMillis()) - } - } - - if (fbsimctlFailCount1 >= maxFailCount) { - logger.error(logMarker, "Fbsimctl health check failed $fbsimctlFailCount1 times. Restarting fbsimctl") + wdaFailCount += 1 - try { - fbsimctlProc.kill() - } catch (e: RuntimeException) { - logger.error(logMarker, "Failed to kill Fbsimctl. ${e.message}", e) + if (wdaFailCount >= maxFailCount) { + deviceState = DeviceState.FAILED + val message = "WebDriverAgent health check failed $wdaFailCount times. Setting device state to $deviceState" + logger.error(logMarker, message) + throw RuntimeException("${this@Simulator} $message. Stopping health check") + } } - try { - fbsimctlProc.start() - } catch (e: RuntimeException) { - logger.error(logMarker, "Failed to restart Fbsimctl. ${e.message}", e) - deviceState = DeviceState.FAILED - throw RuntimeException("${this@Simulator} Failed to restart WebDriverAgent. Stopping health check") - } + delay(healthCheckInterval) } } } private fun stopPeriodicHealthCheck() { - healthChecker?.let { checker -> - checker.cancel() - while (checker.isActive) { - Thread.sleep(100) - } - } + healthChecker?.cancel() } - private fun startWdaWithRetry(pollTimeout: Duration = Duration.ofSeconds(60), retryInterval: Duration = Duration.ofSeconds(2)) { - val maxRetries = 7 + private fun startWdaWithRetry(pollTimeout: Duration = Duration.ofSeconds(30), retryInterval: Duration = Duration.ofSeconds(3)) { + val maxRetries = 3 for (attempt in 1..maxRetries) { try { logger.info(logMarker, "Starting WebDriverAgent on ${this@Simulator}") - instrumentationAgent.kill() - instrumentationAgent.start(useAppium) + webDriverAgent.stop() + webDriverAgent.start() - Thread.sleep(8000) + Thread.sleep(1000) pollFor( pollTimeout, @@ -467,7 +220,7 @@ class Simulator( logger = logger, marker = logMarker ) { - instrumentationAgent.isHealthy() + webDriverAgent.isHealthy() } logger.info(logMarker, "Started WebDriverAgent on ${this@Simulator}") @@ -476,12 +229,6 @@ class Simulator( } catch (e: RuntimeException) { logger.warn(logMarker, "Attempt $attempt to start WebDriverAgent for ${this@Simulator} failed: $e") - - val wdaLogLines = instrumentationAgent.deviceAgentLog.readLines().takeLast(200) - wdaLogLines.forEach { logLine -> - logger.warn(logMarker, "[WDA]: $logLine") - } - if (attempt == maxRetries) { throw e } @@ -491,7 +238,7 @@ class Simulator( private fun eraseSimulatorAndCreateBackup() { logger.info(logMarker, "Erasing simulator ${this@Simulator} before creating a backup") - remote.xcrunSimctl.eraseSimulator(udid) + remote.fbsimctl.eraseSimulator(udid) if (trustStoreFile.isNotEmpty()) { copyTrustStore() @@ -500,79 +247,20 @@ class Simulator( logger.info(logMarker, "Booting ${this@Simulator} before creating a backup") logTiming("initial boot") { boot() } - dismissTutorials() - if (assetsPath.isNotEmpty()) { - copyMediaAssetsWithRetry() + copyMediaAssets() } - if (appConfig.useTestHelperApp) { - installTestHelperApp() + if (useWda && configuration.shouldPreinstallWDA) { + webDriverAgent.installHostApp() } - launchMobileSafari("https://localhost") - Thread.sleep(5000) - logger.info(logMarker, "Shutting down ${this@Simulator} before creating a backup") shutdown() backup.create() } - private fun openSimulatorApp() { - try { - val result = remote.execIgnoringErrors(listOf("/bin/ps", "axo", "pid,stat,command")) - val simulatorApp = "/Simulator.app/" - - if (result.isSuccess && result.stdOut.lines().none { it.contains(simulatorApp) }) { - remote.shell("open -a Simulator.app") - } - } catch (t: Throwable) { - logger.error(logMarker, "Failed to launch Simulator.app application. Error ${t.javaClass.name} ${t.message}") - } - } - - private fun useSoftwareKeyboard() { - try { - val devicePreferencesResult = remote.execIgnoringErrors(listOf("/usr/bin/defaults", "read", "com.apple.iphonesimulator", "DevicePreferences")) - if (devicePreferencesResult.isSuccess) { - if (devicePreferencesResult.stdOut.contains(udid)) { - return - } - } - - val dict = "ConnectHardwareKeyboard0" - val cmd = listOf("/usr/bin/defaults", "write", "com.apple.iphonesimulator", "DevicePreferences", "-dict-add", udid, if (remote.isLocalhost()) dict else "'$dict'") - val result = remote.execIgnoringErrors(cmd) - - val simulatorApp = "/Simulator.app/" - - if (result.isSuccess && result.stdOut.lines().none { it.contains(simulatorApp) }) { - remote.shell("open -a Simulator.app") - } - } catch (t: Throwable) { - logger.error(logMarker, "Failed to launch Simulator.app application. Error ${t.javaClass.name} ${t.message}") - } - } - - private val MEDIA_COPY_ATTEMPTS = 3 - - private fun copyMediaAssetsWithRetry() { - (1..MEDIA_COPY_ATTEMPTS).forEach { - try { - logger.info(logMarker, "Copying media assets to simulator. Attempt: $it") - copyMediaAssets() - logger.info(logMarker, "Copied media assets to simulator successfully") - return - } catch (e: MediaInconsistentcyException) { - logger.error(e.message) - if (it == MEDIA_COPY_ATTEMPTS) { - throw e - } - } - } - } - private fun copyTrustStore() { logger.debug(logMarker, "Copying trust store to ${this@Simulator}") val keyChainLocation = Paths.get(deviceSetPath, udid, "data", "Library", "Keychains").toFile().absolutePath @@ -581,7 +269,7 @@ class Simulator( if (remote.isLocalhost()) { remote.shell("cp $trustStoreFile $keyChainLocation", returnOnFailure = false) } else { - remote.scpToRemoteHost(trustStoreFile, keyChainLocation) + remote.scpToRemoteHost(trustStoreFile, keyChainLocation, Duration.ofMinutes(1)) } logger.info(logMarker, "Copied trust store to ${this@Simulator}") @@ -589,303 +277,132 @@ class Simulator( private fun copyMediaAssets() { logger.debug(logMarker, "Copying assets to ${this@Simulator}") - - val mediaFiles = File(assetsPath).walk().filter { it.isFile }.toList() - media.addMedia(mediaFiles) - - val assets = media.list() - val recordedAssets = media.listPhotoData() - - if (recordedAssets.size != recordedAssets.toSet().size) { - throw MediaInconsistentcyException("Recorded media contains wrong data. Assets: ${assets.joinToString(",")}. Recorded assets: ${recordedAssets.joinToString(",")}") - } - - if (assets.size != recordedAssets.size) { - throw MediaInconsistentcyException("Actual media is in wrong state. Assets: ${assets.joinToString(",")}. Recorded assets: ${recordedAssets.joinToString(",")}") + media.reset() + File(assetsPath).walk().filter { it.isFile }.forEach { + media.addMedia(it, it.readBytes()) } logger.info(logMarker, "Copied assets to ${this@Simulator}") } - private fun listDevices(): String { - return remote.shell("/usr/bin/xcrun simctl list devices", returnOnFailure = true).stdOut - } - - private fun isSimulatorShutdown(): Boolean { - return listDevices().lines().find { it.contains(udid) && it.contains("(Shutdown)") } != null - } - - private fun deleteSimulator() { - logger.debug(logMarker, "Will delete simulator $udid") - val result: CommandResult = remote.fbsimctl.delete(udid) - if (result.isSuccess) { - logger.debug(logMarker, "Did delete simulator $udid") - } else { - logger.error(logMarker, "Error occurred while deleting simulator $udid. Command exit code: ${result.exitCode}. Result stdErr: ${result.stdErr}") - } - } - private fun shutdown() { logger.info(logMarker, "Shutting down ${this@Simulator}") - stopPeriodicHealthCheck() - bootTask?.cancel(true) - installTask?.cancel(true) - ignoringErrors({ videoRecorder.dispose() }) - ignoringErrors({ appiumServer.kill() }) - ignoringErrors({ instrumentationAgent.kill() }) - ignoringErrors({ fbsimctlProc.kill() }) - val result = remote.fbsimctl.shutdown(udid) if (!result.isSuccess && !result.stdErr.contains("current state: Shutdown") && !result.stdOut.contains("current state: Shutdown")) { logger.debug(logMarker, "Error occurred while shutting down simulator $udid. Command exit code: ${result.exitCode}. Result stdErr: ${result.stdErr}") } + ignoringErrors { fbsimctlProc.kill() } + pollFor( - timeOut = Duration.ofSeconds(90), - retryInterval = Duration.ofSeconds(5), + timeOut = Duration.ofSeconds(60), reasonName = "${this@Simulator} to shutdown", - shouldReturnOnTimeout = true, + retryInterval = Duration.ofSeconds(10), logger = logger, marker = logMarker ) { - isSimulatorShutdown() + val fbSimctlDevice = remote.fbsimctl.listDevice(udid) + FBSimctlDeviceState.SHUTDOWN.value == fbSimctlDevice?.state } logger.info(logMarker, "Successfully shut down ${this@Simulator}") } - private fun disabledServices(): List { - val cmdLine = listOf( - "com.apple.Maps.mapspushd", - "com.apple.Maps", - "com.apple.MapsUI", - "com.apple.NPKCompanionAgent", - "com.apple.SafariBookmarksSyncAgent", - "com.apple.ScreenTimeAgent", - "com.apple.ScreenTimeWidgetApplication", - "com.apple.UsageTrackingAgent", -// "com.apple.nanoregistryd", -// "com.apple.nanoregistrylaunchd", -// "com.apple.nanoprefsyncd.2", -// "com.apple.nanomapscd", -// "com.apple.nanosystemsettingsd", -// "com.apple.nanobackupd", -// "com.apple.nanoappregistryd", -// "com.apple.nanotimekitcompaniond", - "com.apple.MapKit.SnapshotService", - "com.apple.WallpaperKit", - "com.apple.WallpaperKit.WallpaperMigrator", - "com.apple.WebBookmarks.webbookmarksd", - "com.apple.accessibility.AccessibilityUIServer", - "com.apple.addressbooksyncd", - "com.apple.ap.adprivacyd", - "com.apple.ap.promotedcontentd", - "com.apple.assistant_service", - "com.apple.assistantd", - "com.apple.avatarsd", - "com.apple.bird", - "com.apple.calaccessd", - "com.apple.carkitd", - "com.apple.cloudd", - "com.apple.companionappd", - "com.apple.coreservices.useractivityd", - "com.apple.corespeechd", - "com.apple.corespotlightservice", - "com.apple.dataaccess.dataaccessd", - "com.apple.donotdisturbd", - "com.apple.email.maild", - "com.apple.familycircled", - "com.apple.familynotification", - "com.apple.fitnesscoachingd", - "com.apple.GameController.gamecontrollerd", - "com.apple.healthappd", - "com.apple.healthd", - "com.apple.homed", - "com.apple.mobilecal", - "com.apple.mobiletimerd", - "com.apple.navd", - "com.apple.news", - "com.apple.newsd", - "com.apple.nanonewscd", - "com.apple.newscore", - "com.apple.newscore2", - "NewsToday2", - "com.apple.news.articlenotificationextension", - "com.apple.news.NewsArticleQuickLook", - "com.apple.news.openinnews", - "com.apple.news.tag", - - "com.apple.chronod", // it launches all the following services for the widgets - "com.apple.Batteries.BatteriesWidget", - "com.apple.reminders.WidgetExtension", - "com.apple.mobilecal.CalendarWidgetExtension", - "com.apple.mobileslideshow.PhotosReliveWidget", - "com.apple.PassbookStub.PassbookWidgets", - "com.apple.Maps.GeneralMapsWidget", - "com.apple.Health.Sleep.SleepWidgetExtension", - "com.apple.Passbook.PassbookWidgets", - "com.apple.PeopleViewService.PeopleWidget-iOS", - "com.apple.ScreenTimeWidgetApplication.ScreenTimeWidgetExtension", - - "com.apple.news.engagementExtension", - "com.apple.news.articlenotificationserviceextension", - "com.apple.news.marketingnotificationextension", - "com.apple.news.widget", - "com.apple.news.NewsAudioExtension", - "com.apple.news.widgetintents", - "com.apple.pairedsyncd", - "com.apple.parsecd", // https://jira.badoojira.com/browse/IOS-33218 - "com.apple.photoanalysisd", - "com.apple.PosterBoard", // iOS 16 - "com.apple.posterboardservices", // iOS 16 - "com.apple.purplebuddy.budd", - "com.apple.remindd", - "com.apple.searchd", - "com.apple.siriinferenced", - "com.apple.siriknowledged", - "com.apple.siri.ClientFlow.ClientScripter", - "com.apple.siri.context.service", - "com.apple.siriactionsd", - "com.apple.suggestd", - "com.apple.telephonyutilities.callservicesd", - "com.apple.voiced", - // "com.apple.diagnosticextensionsd", - "com.apple.intelligenceplatformd", - "com.apple.mediaremoted", - "com.apple.tvremoted", - "com.apple.videosubscriptionsd", - "com.apple.Maps.mapssyncd", - "com.apple.FamilyControlsAgent", - "com.apple.remotemanagementd", - "com.apple.fitcore", - "com.apple.weatherd" - ).map { - "--disabledJob=$it" - } - - return cmdLine - } - - private fun bootSimulator() { - val cmd = listOf("/usr/bin/xcrun", "simctl", "boot", udid) + disabledServices() - remote.exec(cmd, mapOf(), false, 60L) - } - private fun boot() { logger.info(logMarker, "Booting ${this@Simulator} asynchronously") - val nanos = measureNanoTime { - val task = concurrentBootsPool.submit { // using limited amount of workers to boot simulator - if (remote.isLocalhost()) { - useSoftwareKeyboard() - } + val bootJob = async(context = concurrentBootsPool) { + truncateSystemLogIfExists() - val bootTime = remote.exec(listOf("date", "+%s"), mapOf(), false, 30L).stdOut.trim().toLong() + logger.info(logMarker, "Starting fbsimctl on ${this@Simulator}") + fbsimctlProc.start() // boots simulator - bootSimulator() + var lastState: String? = null - if (remote.isLocalhost()) { - openSimulatorApp() + try { + pollFor( + Duration.ofSeconds(60), + reasonName = "${this@Simulator} initial boot", + shouldReturnOnTimeout = false, + logger = logger, + marker = logMarker + ) { + val simulatorInfo = remote.fbsimctl.listDevice(udid) + lastState = simulatorInfo?.state + lastState == FBSimctlDeviceState.BOOTED.value } - - waitUntilSimulatorBooted(bootTime) + } catch (e: WaitTimeoutError) { + throw WaitTimeoutError("${e.message}. Simulator is in wrong state of $lastState", e) } - bootTask = task - task.get() - } - val timingMarker = MapEntriesAppendingMarker(commonLogMarkerDetails + mapOf("simulatoBootTime" to NANOSECONDS.toSeconds(nanos))) - logger.info(timingMarker, "Device ${this@Simulator} is sufficiently booted") - } - - sealed class RequiredService(val identifier: String, @Volatile var booted: Boolean = false) { - class SpringBoard() : RequiredService("com.apple.SpringBoard") - class TextInput() : RequiredService("com.apple.TextInput.kbd") - class AccessibilityUIServer() : RequiredService("com.apple.accessibility.AccessibilityUIServer") - class Spotlight() : RequiredService("com.apple.Spotlight") - class Locationd() : RequiredService("com.apple.locationd") - - override fun toString(): String { - return identifier - } - } - - val simulatorServices = mutableSetOf() - - private fun waitUntilSimulatorBooted(bootTime: Long) { - Thread.sleep(5000L) // make sure enough time for initial boot before any other actions - - val escapedCommand = if (remote.isLocalhost()) { - "/usr/bin/xcrun simctl spawn $udid log show --color none --start @${bootTime} --predicate \"process == 'SpringBoard' AND composedMessage CONTAINS 'Bootstrap success'\"" - } else { - "\"/usr/bin/xcrun simctl spawn $udid log show --color none --start @${bootTime} --predicate \\\"process == 'SpringBoard' AND composedMessage CONTAINS 'Bootstrap success'\\\"\"" - } - val logsCommand = listOf("/bin/bash", "-c", escapedCommand) + var systemLogPath = "" - val requiredService = RequiredService.Spotlight() - val serviceBundleId = requiredService.identifier - var stdOut = "" - var stdErr = "" + pollFor(Duration.ofSeconds(20), "${this@Simulator} system log appeared", + shouldReturnOnTimeout = true, logger = logger, marker = logMarker) { + val diagnosticInfo = remote.fbsimctl.diagnose(udid) + val location = diagnosticInfo.sysLogLocation + if (location != null) { + systemLogPath = location + logger.info(logMarker, "Device ${this@Simulator} system log appeared") + true + } else { + logger.warn(logMarker, "Device ${this@Simulator} system log NOT appeared") + false + } + } - pollFor( - timeOut = Duration.ofMinutes(3), - reasonName = "Simulator boot process", - shouldReturnOnTimeout = true, - retryInterval = Duration.ofSeconds(10), - logger = logger, - marker = logMarker - ) { - val logsResult = remote.execIgnoringErrors(logsCommand, timeOutSeconds = 120L) - stdOut = logsResult.stdOut - stdErr = logsResult.stdErr + pollFor(Duration.ofSeconds(30), "${this@Simulator} to be sufficiently booted", + shouldReturnOnTimeout = true, logger = logger, marker = logMarker) { + if (!systemLogPath.isBlank()) { + remote.execIgnoringErrors(listOf("grep", "-m1", "SpringBoard", systemLogPath)).isSuccess + } else { + false + } + } - if (stdOut.contains(serviceBundleId)) { - requiredService.booted = true + pollFor( + Duration.ofSeconds(60), + reasonName = "${this@Simulator} FbSimCtl health check", + retryInterval = Duration.ofSeconds(3), + logger = logger, + marker = logMarker + ) { + fbsimctlProc.isHealthy() } - requiredService.booted + logger.info(logMarker, "Device ${this@Simulator} is sufficiently booted") } - if (!requiredService.booted) { - val failedServicesMessage = "Failed services [${requiredService}]" - val errorMessage = "Simulator $udid failed to successfully boot to sufficient state. $failedServicesMessage. StdErr: \n$stdErr\n. StdOut: \n$stdOut" - logger.error(logMarker, "Simulator $udid log has not exited in time. Possible errors. StdErr: $stdErr. StdOut: $stdOut") - logger.error(logMarker, errorMessage) - throw DeviceCreationException(errorMessage) + runBlocking { + bootJob.await() } } - private fun lineReader(inputStream: InputStream, readerProc: ((line: String) -> Unit)): java.lang.Runnable { - return Runnable { - inputStream.use { stream -> - val inputStreamReader = InputStreamReader(stream, StandardCharsets.UTF_8) - val reader = BufferedReader(inputStreamReader, 65356) - var line: String? = reader.readLine() + private fun truncateSystemLogIfExists() { + val sysLog = remote.fbsimctl.diagnose(udid).sysLogLocation ?: return - while (line != null) { - readerProc.invoke(line) - line = reader.readLine() + if (remote.isLocalhost()) { + try { + FileOutputStream(sysLog).channel.use { + it.truncate(0) } + } catch(e: IOException) { + logger.error(logMarker, "Error truncating sysLog $this", e) + } + } else { + try { + remote.shell("echo -n > $sysLog", returnOnFailure = true) + logger.debug(logMarker, "Truncated syslog of simulator ${this@Simulator}") + } catch (e: RuntimeException) { + logger.error(logMarker, "Error while truncating syslog of simulator ${this@Simulator}", e) } } } - private fun writeSimulatorDefaults(setting: String) { - remote.shell("/usr/bin/xcrun simctl spawn $udid defaults write $setting", true) - } - - private fun launchMobileSafari(url: String) { - remote.shell("/usr/bin/xcrun simctl openurl $udid $url", true) - } - - private fun readSimulatorDefaults(): String { - return remote.execIgnoringErrors("/usr/bin/xcrun simctl spawn $udid defaults read".split(" ")).stdOut - } - private fun logTiming(actionName: String, action: () -> Unit) { logger.info(logMarker, "Device ${this@Simulator} starting action <$actionName>") - val nanos = measureNanoTime(action) - val seconds = NANOSECONDS.toSeconds(nanos) + val millis = measureTimeMillis(action) + val seconds = millis / 1000 val measurement = mutableMapOf( "action_name" to actionName, "duration" to seconds @@ -896,42 +413,27 @@ class Simulator( //endregion //region reset async - override fun resetAsync(): Runnable { - val state = deviceState - if (state != DeviceState.CREATED && state != DeviceState.FAILED) { - val message = "Unable to perform reset. Simulator $udid is in state $state" - logger.error(logMarker, message) - throw IllegalStateException(message) + override fun resetAsync() { + stopPeriodicHealthCheck() + executeCritical { + deviceState = DeviceState.RESETTING } - return Runnable { - executeCritical { - deviceState = DeviceState.RESETTING - - val nanos = measureNanoTime { - shutdown() - resetFromBackup() - try { - prepare(clean = false) // simulator is already clean as it was restored from backup in resetFromBackup - } catch (e: Exception) { // catching most wide exception + executeCriticalAsync { + // FIXME: check for it.isActive to help to cancel long running tasks + val elapsed = measureTimeMillis { + resetFromBackup() + try { + prepare(clean = false) // simulator is already clean as it was restored from backup in resetFromBackup + } catch (e: Exception) { // catching most wide exception + executeCritical { deviceState = DeviceState.FAILED - logger.error(logMarker, "Failed to reset and prepare device ${this@Simulator}", e) - shutdown() - disposeResources() - throw e } + logger.error(logMarker, "Failed to reset and prepare device ${this@Simulator}", e) + throw e } - - val seconds = NANOSECONDS.toSeconds(nanos) - - val measurement = mutableMapOf( - "action_name" to "resetAsync", - "duration" to seconds - ) - measurement.putAll(commonLogMarkerDetails) - - logger.info(MapEntriesAppendingMarker(measurement), "Device ${this@Simulator} reset and ready in $seconds seconds") } + logger.info(logMarker, "Device ${this@Simulator} reset and ready in ${elapsed / 1000} seconds") } } @@ -939,6 +441,9 @@ class Simulator( logger.info(logMarker, "Starting to reset $this") executeWithTimeout(timeout, "Resetting simulator") { + disposeResources() + shutdown() + if (!backup.isExist()) { logger.error(logMarker, "Could not find backup for $this") throw SimulatorError("Could not find backup for $this") @@ -954,7 +459,19 @@ class Simulator( //endregion //region helper functions — execute critical and async + private fun executeCriticalAsync(function: (context: CoroutineScope) -> Unit) { + criticalAsyncPromise = launch(context = simulatorsThreadPool) { + executeCritical { + function(this) + } + } + } + private fun executeCritical(action: () -> Unit) { + if (deviceLock.isLocked) { + logger.info(logMarker, "Awaiting for previous action. Likely a criticalAsyncPromise $criticalAsyncPromise on ${this@Simulator}") + } + deviceLock.withLock { try { action() @@ -963,33 +480,53 @@ class Simulator( lastException = e // FIXME: force shutdown failed sim logger.error(logMarker, "Execute critical block finished with exception. Message: [${e.message}]", e) + logger.warn(logMarker, "Host stats on ${this@Simulator} are:\n${getSystemStats()}") } } } + + private fun getSystemStats(): String { + val uptime = remote.execIgnoringErrors(listOf("/usr/bin/uptime")) + val message = mutableListOf("uptime", uptime.stdOut) + + val istats = remote.execIgnoringErrors(listOf("istats", "--no-graphs"), env = mapOf("RUBYOPT" to "")) + + if (istats.isSuccess) { + message.add("istats") + message.add(istats.stdOut) + } + + return message.joinToString("\n") + } //endregion //region simulator status override fun status(): SimulatorStatusDTO { var isFbsimctlReady = false var isWdaReady = false - var isAppiumReady = false if (deviceState == DeviceState.CREATED) { isFbsimctlReady = fbsimctlProc.isHealthy() - isWdaReady = (if (useWda) { instrumentationAgent.isHealthy() } else true) - isAppiumReady = (if (useAppium) { appiumServer.isHealthy() } else true) + isWdaReady = (if (useWda) { webDriverAgent.isHealthy() } else true) } val isSimulatorReady = deviceState == DeviceState.CREATED && isFbsimctlReady && isWdaReady return SimulatorStatusDTO( - ready = isSimulatorReady, - wda_status = isWdaReady, - appium_status = isAppiumReady, - fbsimctl_status = isFbsimctlReady, - state = deviceState.value, - last_error = lastException?.toDTO(), - simulator_services = simulatorServices.toSet() + ready = isSimulatorReady, + wda_status = isWdaReady, + fbsimctl_status = isFbsimctlReady, + state = deviceState.value, + last_error = lastException?.toDTO() + ) + } + + private fun Exception.toDTO(): ExceptionDTO { + + return ExceptionDTO( + type = this.javaClass.name, + message = this.message ?: "", + stackTrace = stackTrace.map { it.toString() } ) } //endregion @@ -1003,98 +540,54 @@ class Simulator( //region approveAccess - override fun setPermissions(bundleId: String, permissions: PermissionSet) { - SimulatorPermissions(remote, udid).setPermissions(bundleId, permissions) - } - - override fun sendPushNotification(bundleId: String, notificationContent: ByteArray) { - withDefers(logger) { - val pushNotificationFile: File = File.createTempFile("push_notification_${deviceInfo.udid}_", ".json") - defer { pushNotificationFile.delete() } - pushNotificationFile.writeBytes(notificationContent) + override fun approveAccess(bundleId: String) { + val permissions = SimulatorPermissions(remote, deviceSetPath, this) - val pushNotificationPath: String = if (remote.isLocalhost()) { - pushNotificationFile.absolutePath - } else { - val remotePushNotificationDir = remote.execIgnoringErrors(listOf("/usr/bin/mktemp", "-d")).stdOut.trim() - defer { remote.execIgnoringErrors(listOf("/bin/rm", "-rf", remotePushNotificationDir)) } - remote.scpToRemoteHost(pushNotificationFile.absolutePath, remotePushNotificationDir, Duration.ofMinutes(1)) - File(remotePushNotificationDir, pushNotificationFile.name).absolutePath - } + val set = PermissionSet() - val result = remote.execIgnoringErrors(listOf("/usr/bin/xcrun", "simctl", "push", udid, bundleId, pushNotificationPath)) + set.putAll(mapOf( + PermissionType.Camera to PermissionAllowed.Yes, + PermissionType.Microphone to PermissionAllowed.Yes, + PermissionType.Photos to PermissionAllowed.Yes, + PermissionType.Contacts to PermissionAllowed.Yes + )) - if (!result.isSuccess) { - throw RuntimeException("Could not simulate push notification to device $udid: $result") - } - } + permissions.setPermissions(bundleId, set) } - override fun sendPasteboard(payload: ByteArray) { - withDefers(logger) { - val pasteboardPayloadFile: File = File.createTempFile("pasteboard_${deviceInfo.udid}_", ".data") - defer { pasteboardPayloadFile.delete() } - pasteboardPayloadFile.writeBytes(payload) - - val pasteboardPayloadPath: String = if (remote.isLocalhost()) { - pasteboardPayloadFile.absolutePath - } else { - val remotePasteboardDir = remote.execIgnoringErrors(listOf("/usr/bin/mktemp", "-d")).stdOut.trim() - defer { remote.execIgnoringErrors(listOf("/bin/rm", "-rf", remotePasteboardDir)) } - remote.scpToRemoteHost(pasteboardPayloadFile.absolutePath, remotePasteboardDir, Duration.ofMinutes(1)) - File(remotePasteboardDir, pasteboardPayloadFile.name).absolutePath - } - - val result = remote.shell("cat $pasteboardPayloadPath | /usr/bin/xcrun simctl pbcopy -v $udid") + override fun setPermissions(bundleId: String, permissions: PermissionSet) { + val manager = SimulatorPermissions(remote, deviceSetPath, this) - if (!result.isSuccess) { - throw RuntimeException("Could not send pasteboard to device $udid: $result") - } - } + manager.setPermissions(bundleId, permissions) } //endregion //region release override fun release(reason: String) { + stopPeriodicHealthCheck() logger.info(logMarker, "Releasing device $this because $reason") - ignoringErrors({ shutdown() }) - ignoringErrors({ disposeResources() }) - logger.info(logMarker, "Released device $this") - } - - override fun delete(reason: String) { - logger.info(logMarker, "Deleting device $this because $reason") - ignoringErrors({ backup.delete() }) - ignoringErrors({ shutdown() }) - ignoringErrors({ deleteSimulator() }) - ignoringErrors({ disposeResources(keepMetadata = false) }) - logger.info(logMarker, "Deleted device $this because $reason") - } - - private fun deleteSimulatorFolder(keepMetadata: Boolean) { - val directoryPath = if (keepMetadata) simulatorDataDirectory.absolutePath else simulatorDirectory.absolutePath - (1..3).any { - val chmodResult = remote.execIgnoringErrors(listOf("/bin/chmod", "-RP", "755", directoryPath), timeOutSeconds = 120L) - - if (!chmodResult.isSuccess) { - logger.error(logMarker, "Attempt number $it: Failed to chmod at path: [$directoryPath]. Result: $chmodResult") - } - - val deleteResult = remote.execIgnoringErrors(listOf("/bin/rm", "-rf", directoryPath), timeOutSeconds = 120L) + // FIXME: add background thread to clear up junk we failed to delete + if (deviceLock.isLocked) { + logger.warn(logMarker, "Going to kill previous promise $criticalAsyncPromise running on $this") + } - if (!deleteResult.isSuccess) { - logger.error(logMarker, "Attempt number $it: Failed to delete at path: [$directoryPath]. Result: $deleteResult") - } + if (criticalAsyncPromise.isActive) { + // FIXME: unlike in Ruby canceling is not immediate, consider using thread instead of async + criticalAsyncPromise.cancel(CancellationException("Killing previous $criticalAsyncPromise running on $this due to release of the device")) + } - deleteResult.isSuccess + executeCritical { + disposeResources() + shutdown() } + logger.info(logMarker, "Released device $this") } - private fun disposeResources(keepMetadata: Boolean = true) { + private fun disposeResources() { ignoringErrors({ videoRecorder.dispose() }) - deleteSimulatorFolder(keepMetadata) + ignoringErrors({ webDriverAgent.stop() }) } private fun ignoringErrors(action: () -> Unit?) { @@ -1139,8 +632,6 @@ class Simulator( return mapOf("status" to "true") } - override fun listApps(): List = remote.fbsimctl.listApps(udid) - override fun shake() : Boolean { val command = listOf("xcrun", "simctl", "notify_post", udid, "com.apple.UIKit.SimulatorShake") val result = remote.execIgnoringErrors(command) @@ -1148,15 +639,10 @@ class Simulator( } override fun openUrl(url: String) : Boolean { - val urlString = if (remote.isLocalhost()) url else "\"${url}\"" - val command = listOf("/usr/bin/xcrun", "simctl", "openurl", udid, urlString) - val result = remote.execIgnoringErrors(command) - - if (!result.isSuccess) { - logger.error("Failed to open url $url \nResult:\n$result") - throw RuntimeException("Failed to open url $url \nResult:\n$result") - } + val urlString = if (remote.isLocalhost()) { url } else { "\"$url\"" } + val command = listOf("xcrun", "simctl", "openurl", udid, urlString) + val result = remote.execIgnoringErrors(command) return result.isSuccess } @@ -1172,9 +658,9 @@ class Simulator( } override fun crashLogs(pastMinutes: Long?): List { - val crashLogFiles = listCrashLogs(pastMinutes) + var crashLogFiles = listCrashLogs(pastMinutes) - val crashLogs = crashLogFiles.map { + var crashLogs = crashLogFiles.map { val rv = remote.execIgnoringErrors(listOf("cat", it)) if (rv.isSuccess) { @@ -1209,7 +695,7 @@ class Simulator( val result = remote.shell(cmd, returnOnFailure = true) if (!result.isSuccess) { - throw SimulatorError("Failed to list crash logs for $this: $result") + SimulatorError("Failed to list crash logs for $this: $result") } return result.stdOut .lineSequence() @@ -1232,19 +718,29 @@ class Simulator( return fileSystem.dataContainer(bundleId) } - override fun sharedContainer(): SharedContainer { - val sharedResourceDirectory = getEnvironmentVariable("SIMULATOR_SHARED_RESOURCES_DIRECTORY") - - return fileSystem.sharedContainer(sharedResourceDirectory) - } - override fun applicationContainer(bundleId: String): DataContainer { return fileSystem.applicationContainer(bundleId) } //endregion - override fun uninstallApplication(bundleId: String, appInstaller: AppInstaller) { - appInstaller.uninstallApplication(udid, bundleId) + override fun uninstallApplication(bundleId: String) { + logger.debug(logMarker, "Uninstalling application $bundleId from Simulator $this") + + terminateApplication(bundleId) + + val uninstallResult = remote.execIgnoringErrors(listOf("xcrun", "simctl", "uninstall", udid, bundleId)) + + if (!uninstallResult.isSuccess) { + logger.error(logMarker, "Uninstall application $bundleId was unsuccessful. Result $uninstallResult") + } + } + + private fun terminateApplication(bundleId: String) { + val terminateResult = remote.execIgnoringErrors(listOf("xcrun", "simctl", "terminate", udid, bundleId)) + + if (!terminateResult.isSuccess) { + logger.error(logMarker, "Terminating application $bundleId was unsuccessful. Result $terminateResult") + } } override fun setEnvironmentVariables(envs: Map) { @@ -1261,12 +757,51 @@ class Simulator( remote.shell("xcrun simctl spawn $udid launchctl setenv ${envsArguments.joinToString(" ")}") } - override fun getEnvironmentVariable(variableName: String): String { - logger.debug(logMarker, "Getting environment variable $variableName for Simulator $this") - if(!ENV_VAR_VALIDATE_REGEX.matches(variableName)) { - throw IllegalArgumentException("Variable name should contain only letters, numbers and underscores. Current value: $variableName") + private fun createVideoRecorder(): VideoRecorder { + return when (configuration.videoRecorderClassName) { + SimulatorVideoRecorder::class.qualifiedName -> SimulatorVideoRecorder( + deviceInfo, + remote, + location = Paths.get(deviceSetPath, udid, "video.mp4").toFile() + ) + MJPEGVideoRecorder::class.qualifiedName -> MJPEGVideoRecorder( + deviceInfo, + remote, + wdaEndpoint, + mjpegServerPort, + configuration.videoRecorderFrameRate, + ref, + udid + ) + else -> throw IllegalArgumentException( + "Wrong class specified as video recorder: ${configuration.videoRecorderClassName}. " + + "Available are: [${SimulatorVideoRecorder::class.qualifiedName}, ${MJPEGVideoRecorder::class.qualifiedName}]" + ) } + } - return remote.shell("xcrun simctl getenv $udid $variableName").stdOut.trim() // remove last new_line + private fun createWebDriverAgent(): IWebDriverAgent { + return when (configuration.simulatorWdaClassName) { + SimulatorWebDriverAgent::class.qualifiedName -> SimulatorWebDriverAgent( + remote, + wdaRunnerXctest, + udid, + wdaEndpoint, + mjpegServerPort, + deviceRef + ) + SimulatorXcrunWebDriverAgent::class.qualifiedName -> SimulatorXcrunWebDriverAgent( + remote, + wdaRunnerXctest, + udid, + wdaEndpoint, + mjpegServerPort, + deviceRef + ) + else -> throw IllegalArgumentException( + "Wrong class specified as Simulator WebDriverAgent: $configuration.simulatorWdaClassName. " + + "Available are: [${SimulatorWebDriverAgent::class.qualifiedName}, ${SimulatorXcrunWebDriverAgent::class.qualifiedName}]" + ) + } } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt index 87c5e572..681d1a52 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt @@ -3,29 +3,127 @@ package com.badoo.automation.deviceserver.ios.simulator import com.badoo.automation.deviceserver.data.PermissionAllowed import com.badoo.automation.deviceserver.data.PermissionSet import com.badoo.automation.deviceserver.data.PermissionType -import com.badoo.automation.deviceserver.data.UDID import com.badoo.automation.deviceserver.host.IRemote +import java.io.File +import java.nio.file.Paths class SimulatorPermissions( private val remote: IRemote, - private val udid: UDID + private val deviceSetPath: String, + private val simulator: ISimulator ) { - fun setPermissions( - bundleId: String, - permissions: PermissionSet - ) { - permissions.forEach { service, action -> - setPrivacy(bundleId, service, action) + + private val serviceKeys = mapOf( + PermissionType.Calendar to "kTCCServiceCalendar", + PermissionType.Camera to "kTCCServiceCamera", + PermissionType.Contacts to "kTCCServiceAddressBook", + PermissionType.HomeKit to "kTCCServiceWillow", + PermissionType.Microphone to "kTCCServiceMicrophone", + PermissionType.Photos to "kTCCServicePhotos", + PermissionType.Reminders to "kTCCServiceReminders", + PermissionType.MediaLibrary to "kTCCServiceMediaLibrary", + PermissionType.Motion to "kTCCServiceMotion", + PermissionType.Health to "kTCCServiceMSO", + PermissionType.Siri to "kTCCServiceSiri", + PermissionType.Speech to "kTCCServiceSpeechRecognition" + ) + + fun setPermissions(bundleId: String, permissions: PermissionSet) { + val servicePermissions = PermissionSet() + + permissions.forEach { type, allowed -> + when (type) { + PermissionType.Location -> setLocationPermission(bundleId, allowed) + PermissionType.Notifications -> setNotificationsPermission(bundleId, allowed) + else -> servicePermissions[type] = allowed + } + } + + setServicePermissions(bundleId, servicePermissions) + } + + private fun setServicePermissions(bundleId: String, servicePermissions: PermissionSet) { + val sql = StringBuilder() + + servicePermissions.forEach { type, allowed -> + sql.append(sqlForPermission(bundleId, type, allowed)) + } + + val path = File(deviceSetPath, simulator.udid) + val sqlCmd = "sqlite3 ${path.absolutePath}/data/Library/TCC/TCC.db \"pragma busy_timeout=1000; $sql\"" + + val result = remote.shell(sqlCmd) + + if (!result.isSuccess) { + throw(SimulatorError("Could not set permissions: $result")) + } + } + + private fun sqlForPermission(bundleId: String, type: PermissionType, allowed: PermissionAllowed): String? { + val sql = StringBuilder() + + val key = serviceKeys[type] + ?: throw(IllegalArgumentException("Permission $type is not a service type")) + + sql.append("DELETE FROM access WHERE service = '$key' AND client = '$bundleId' AND client_type = 0;") + + if (allowed == PermissionAllowed.Unset) { + return sql.toString() + } + + val value = when (allowed) { + PermissionAllowed.Yes -> 1 + PermissionAllowed.No -> 0 + else -> throw IllegalArgumentException("Unsupported value $allowed for type $type") } + + sql.append("REPLACE INTO access (service, client, client_type, allowed, prompt_count) VALUES ('$key','$bundleId',0,$value,1);") + + return sql.toString() + } + + private val appleSimUtils = "/usr/local/bin/applesimutils" + private val plUtil = "/usr/bin/plutil" + + @Suppress("UNUSED_PARAMETER") + private fun setNotificationsPermission(bundleId: String, allowed: PermissionAllowed) { + // Setting notifications permission is disallowed as it results in SpringBoard restart + // which breaks WebDriverAgent. Restarting SpringBoard and WebDriverAgent will take too much time. + throw RuntimeException("Setting notifications permission is not supported") } - private fun setPrivacy(bundleId: String, service: PermissionType, action: PermissionAllowed) { - val cmd = listOf("/usr/bin/xcrun", "simctl", "privacy", udid, action.value, service.value, bundleId) + private fun setLocationPermission(bundleId: String, allowed: PermissionAllowed) { + // map to applesimutils values + val value = when (allowed) { + PermissionAllowed.Always -> "always" + PermissionAllowed.Inuse -> "inuse" + PermissionAllowed.Never -> "never" + PermissionAllowed.Unset -> "unset" + else -> throw IllegalArgumentException("Unsupported value $allowed for type ${PermissionType.Location}") + } + + val cmd = listOf(appleSimUtils, "--byId", simulator.udid, "--bundle", bundleId, "--setPermissions", "location=$value") + + // Without PATH applesimutils will crash with 'NSInvalidArgumentException', reason: 'must provide a launch path' val env = mapOf("PATH" to "/usr/bin") - val result = remote.execIgnoringErrors(cmd, env, timeOutSeconds = 60) + val rv = remote.execIgnoringErrors(cmd, env) - if (!result.isSuccess){ - throw RuntimeException("Could not [${action.value}] permissions for service [${service.value}] for bundle $bundleId: $result") + if (!rv.isSuccess){ + throw RuntimeException("Could not set location permission: $rv") } + + val plistPath = Paths.get(deviceSetPath, simulator.udid, "data", "Library", "Caches", "locationd", "clients.plist").toFile().absolutePath + val printCmd = listOf(plUtil, "-p", plistPath) + val result = remote.execIgnoringErrors(printCmd) + + when (allowed) { + PermissionAllowed.Unset -> if (result.stdOut.contains(bundleId)) { + throw RuntimeException("Resetting location permissions did not work. The $bundleId is present in $plistPath") + } + else -> if (!result.stdOut.contains(bundleId)) { + throw RuntimeException("Setting location permissions did not work. The $bundleId is not present in $plistPath") + } + } + } -} +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcess.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcess.kt index 8023e7cb..edef768c 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcess.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcess.kt @@ -7,7 +7,7 @@ import com.badoo.automation.deviceserver.host.IRemote import net.logstash.logback.marker.MapEntriesAppendingMarker import org.slf4j.LoggerFactory import org.slf4j.Marker -import java.lang.RuntimeException +import java.util.* class SimulatorProcess( private val remote: IRemote, @@ -22,57 +22,42 @@ class SimulatorProcess( ) private val logMarker: Marker = MapEntriesAppendingMarker(commonLogMarkerDetails) - fun getSimulatorMainProcessPid(): Int? { - val command = listOf("/usr/bin/pgrep", "-fl", "launchd_sim") - val result = remote.execIgnoringErrors(command) + fun terminateChildProcess(processName: String) { + val mainProcessPid = getSimulatorMainProcessPid() - if (result.isSuccess) { - val simulatorProcesses = result.stdOut - .lines() - .filter { it.contains(udid) } + // Sends SIGKILL to all processes that: + // 1. have parent pid $mainProcessPid + // 2. and full command line contains substring $processName - val simulatorProcess = simulatorProcesses.firstOrNull() + remote.execIgnoringErrors(listOf("/usr/bin/pkill", "-9", "-P", "$mainProcessPid", "-f", processName)) + } - if (simulatorProcess == null) { - logger.error(logMarker, "No launchd_sim process is found for Simulator $deviceRef") - return null - } + fun getSimulatorMainProcessPid(): Int { + val result = remote.execIgnoringErrors(listOf("/usr/bin/pgrep", "-fl", "launchd_sim")) - return simulatorProcess.split(" ").first().toInt() + if (result.isSuccess) { + return parseSimulatorPid(result.stdOut) } - if (result.exitCode == 1) { - logger.error(logMarker, "No launchd_sim processes are found on ${remote.publicHostName}. Result: $result") - return null + val errorMessage = if (result.exitCode == 1) { + "No launchd_sim processes found on ${remote.publicHostName}. Result: $result" + } else { + "Failed to get process list for simulators at $deviceRef. StdErr: ${result.stdErr}" } - val errorMessage = "Failed to get process list for simulators at $deviceRef. Result: $result" logger.error(logMarker, errorMessage) - throw RuntimeException(errorMessage) - } - - fun terminateMainSimulatorProcess() { - val simulatorPid = getSimulatorMainProcessPid() - if (simulatorPid == null) { - logger.error(logMarker, "No launchd_sim process is found for Simulator $deviceRef. Unable to terminate process.") - } else { - val result = remote.execIgnoringErrors(listOf("/bin/kill", "-15", "$simulatorPid")) - - if (!result.isSuccess) { - logger.error(logMarker, "Failed to send TERM signal to launchd_sim process for Simulator $deviceRef. Result: $result") - } - } + throw IllegalStateException(errorMessage) } - fun terminateChildProcess(processName: String) { - val mainProcessPid = getSimulatorMainProcessPid() + private fun parseSimulatorPid(result: String): Int { + val simulatorProcess = result.lineSequence().firstOrNull { it.contains(udid) && !it.contains("pgrep") } - if (mainProcessPid == null) { - throw IllegalStateException("No launchd_sim process is found for simulator with udid: $udid and ref: $deviceRef") + if (simulatorProcess != null) { + return Scanner(simulatorProcess).nextInt() } - // Sends SIGKILL to process with parent pid $mainProcessPid and name $processName - val command = listOf("/usr/bin/pkill", "-9", "-P", "$mainProcessPid", "-f", processName) - remote.execIgnoringErrors(command) + val errorMessage = "No launchd_sim process for $udid found on ${remote.publicHostName}. Result: $result" + logger.error(logMarker, errorMessage) + throw IllegalStateException(errorMessage) } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/backup/SimulatorBackup.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/backup/SimulatorBackup.kt index 2624dc9f..41270ca6 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/backup/SimulatorBackup.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/backup/SimulatorBackup.kt @@ -1,6 +1,5 @@ package com.badoo.automation.deviceserver.ios.simulator.backup -import com.badoo.automation.deviceserver.ApplicationConfiguration import com.badoo.automation.deviceserver.JsonMapper import com.badoo.automation.deviceserver.LogMarkers import com.badoo.automation.deviceserver.command.CommandResult @@ -18,14 +17,10 @@ import java.io.IOException class SimulatorBackup( private val remote: IRemote, private val udid: UDID, - deviceSetPath: String, - private val simulatorDirectory: File, - private val simulatorDataDirectory: File, - config: ApplicationConfiguration = ApplicationConfiguration() + deviceSetPath: String ) : ISimulatorBackup { - private val backupPath: String = File(config.simulatorBackupPath ?: deviceSetPath , udid).absolutePath + "_BACKUP" - private val backedDataFolder = File(backupPath, "data").absolutePath - + private val srcPath: String = File(deviceSetPath, udid).absolutePath + private val backupPath: String = File(deviceSetPath, udid).absolutePath + "_BACKUP" private val metaFilePath: String = File(backupPath, "data/device_server/meta.json").absolutePath private val metaFileDirectory = File(metaFilePath).parent private val logger = LoggerFactory.getLogger(javaClass.simpleName) @@ -35,7 +30,7 @@ class SimulatorBackup( )) companion object { - const val CURRENT_VERSION = 8 + const val CURRENT_VERSION = 1 } data class BackupMeta(val version: Int, val created: String) { @@ -74,25 +69,9 @@ class SimulatorBackup( //region create backup override fun create() { remote.execIgnoringErrors(listOf("rm", "-rf", backupPath)) - val result = remote.execIgnoringErrors(listOf("cp", "-Rp", simulatorDirectory.absolutePath, backupPath), timeOutSeconds = 120) - - if (!result.isSuccess) { - val stdOutLines = result.stdOut.lines().map { it.trim() }.filter { it.isNotBlank() } - - val ignorableFailures = stdOutLines.filter { it.contains("Deleting") && it.contains("No such file or directory") } - - if (ignorableFailures.isNotEmpty()) { - logger.warn(logMarker, "Failed to copy ignorable files while creating backup for simulator $udid at path: [$backupPath]: ${ignorableFailures.joinToString(", ")}") - } + val result = remote.execIgnoringErrors(listOf("cp", "-R", srcPath, backupPath)) - val failures = stdOutLines.filter { !it.contains("Deleting") && !it.contains("No such file or directory") } - - if (failures.isNotEmpty()) { - val message = "Failed to copy files while creating backup for simulator $udid at path: [$backupPath]: ${failures.joinToString(", ")}" - logger.error(logMarker, message) - throw SimulatorBackupError("$this failed to create backup $backupPath: $result", SimulatorBackupError(message)) - } - } + ensureSuccess(result, "$this failed to create backup $backupPath: $result") writeMeta() logger.debug(logMarker, "Created backup for simulator $udid at path: [$backupPath]") @@ -115,7 +94,7 @@ class SimulatorBackup( } } else -> { - remote.execIgnoringErrors(listOf("mkdir", "-p", metaFileDirectory)) + remote.execIgnoringErrors("mkdir -p $metaFileDirectory".split(" ")) val result = remote.shell( "echo ${ShellUtils.escape(content)} > $metaFilePath", returnOnFailure = true @@ -127,39 +106,15 @@ class SimulatorBackup( //endregion override fun restore() { - val simulatorDataDirectoryPath = simulatorDataDirectory.absolutePath - val deleteResult = remote.execIgnoringErrors(listOf("/bin/rm", "-rf", simulatorDataDirectoryPath), timeOutSeconds = 90L) - - if (!deleteResult.isSuccess) { - logger.error(logMarker, "Failed to delete at path: [$simulatorDataDirectoryPath]. Result: $deleteResult") - - val r = remote.execIgnoringErrors(listOf("/usr/bin/sudo", "/bin/rm", "-rf", simulatorDataDirectoryPath), timeOutSeconds = 90L); - - if (!r.isSuccess) { - val undeletedFiles = remote.execIgnoringErrors(listOf("/usr/bin/find", simulatorDataDirectoryPath), timeOutSeconds = 90L); - logger.error(logMarker, "Failed to delete at path: [$simulatorDataDirectoryPath]. Not deleted files: ${undeletedFiles.stdOut}") - } - } - - val result = remote.execIgnoringErrors(listOf("/bin/cp", "-Rfp", backedDataFolder, simulatorDirectory.absolutePath), timeOutSeconds = 120L) - - if (!result.isSuccess) { - logger.error(logMarker, "Failed to restore from backup at path: [$backedDataFolder] to path: [$result]") - - val secondTry = remote.execIgnoringErrors(listOf("/bin/cp", "-Rfp", backedDataFolder, simulatorDirectory.absolutePath), timeOutSeconds = 120L) - - if (!secondTry.isSuccess) { - val errorMessage = "Failed second attempt to restore from backup at path: [$backedDataFolder] to path: [$secondTry]" - logger.error(logMarker, errorMessage) - throw SimulatorBackupError(errorMessage) - } - } + remote.execIgnoringErrors(listOf("rm", "-rf", srcPath)) + val result = remote.execIgnoringErrors(listOf("cp", "-R", backupPath, srcPath)) - logger.debug(logMarker, "Restored simulator $udid from backup at path: [$backedDataFolder]") + ensureSuccess(result, "$this failed to restore from backup $backupPath: $result") + logger.debug(logMarker, "Restored simulator $udid from backup at path: [$backupPath]") } override fun delete() { - val result = remote.execIgnoringErrors(listOf("/bin/rm", "-rf", backupPath)) + val result = remote.execIgnoringErrors("rm -rf $backupPath".split(" ")) ensureSuccess(result, "$this failed to delete backup $backupPath: $result") logger.debug(logMarker, "Deleted backup for simulator $udid at path: [$backupPath]") @@ -170,4 +125,4 @@ class SimulatorBackup( throw SimulatorBackupError(errorMessage) } } -} +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/backup/SimulatorBackupError.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/backup/SimulatorBackupError.kt index 68918255..091e14c7 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/backup/SimulatorBackupError.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/backup/SimulatorBackupError.kt @@ -1,3 +1,3 @@ package com.badoo.automation.deviceserver.ios.simulator.backup -class SimulatorBackupError(message: String, cause: Throwable? = null) : RuntimeException(message, cause) \ No newline at end of file +class SimulatorBackupError(message: String) : RuntimeException(message) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainer.kt index 0d0f30ba..ef752333 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainer.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainer.kt @@ -3,21 +3,25 @@ package com.badoo.automation.deviceserver.ios.simulator.data import com.badoo.automation.deviceserver.LogMarkers import com.badoo.automation.deviceserver.command.ShellUtils import com.badoo.automation.deviceserver.host.IRemote -import com.badoo.automation.deviceserver.util.withDefers import net.logstash.logback.marker.MapEntriesAppendingMarker import org.slf4j.LoggerFactory import java.io.File import java.lang.RuntimeException import java.nio.file.Path +import java.time.Duration class DataContainer( private val remote: IRemote, internal val basePath: File, private val bundleId: String -): SimulatorFilesystemContainer(remote) { +) { + private val logger = LoggerFactory.getLogger(DataContainer::class.java.simpleName) + private val logMarker = MapEntriesAppendingMarker(mapOf( + LogMarkers.HOSTNAME to remote.hostName + )) fun listFiles(path: Path): List { - val expandedPath = sshNoEscapingWorkaround(expandPath(path, basePath).toString()) + val expandedPath = sshNoEscapingWorkaround(expandPath(path).toString()) val result = remote.execIgnoringErrors(listOf("ls", "-1", "-p", expandedPath)) if (!result.isSuccess) { @@ -28,28 +32,61 @@ class DataContainer( } fun readFile(path: Path): ByteArray { - val expandedPath = sshNoEscapingWorkaround(expandPath(path, basePath).toString()) + val expandedPath = sshNoEscapingWorkaround(expandPath(path).toString()) - return super.readFile(expandedPath) + try { + return remote.captureFile(File(expandedPath)) + } catch (e: RuntimeException) { + throw DataContainerException("Could not read file $path for $bundleId", e) + } } - override fun writeFile(file: File, data: ByteArray) { - val dataContainerFile = File(basePath.absolutePath, file.name) - super.writeFile(dataContainerFile, data) + fun setPlistValue(path: Path, key: String, value: String) { + val expandedPath = sshNoEscapingWorkaround(expandPath(path).toString()) + remote.shell("/usr/libexec/PlistBuddy -c 'Set $key $value' $expandedPath", false) } - fun delete() { - remote.shell("rm -rf ${basePath.absolutePath}") - remote.shell("mkdir -p ${basePath.absolutePath}") + fun addPlistValue(path: Path, key: String, value: String, type: String) { + val expandedPath = sshNoEscapingWorkaround(expandPath(path).toString()) + remote.shell("/usr/libexec/PlistBuddy -c 'Add $key $type $value' $expandedPath", false) } - fun setPlistValue(path: Path, key: String, value: String) { - val expandedPath = sshNoEscapingWorkaround(expandPath(path, basePath).toString()) - remote.shell("/usr/libexec/PlistBuddy -c 'Set $key $value' $expandedPath", false) // TODO: Simple values only for now + fun writeFile(file: File, data: ByteArray) { + val dataContainerFile = File(basePath.absolutePath, file.name) + + if (remote.isLocalhost()) { + dataContainerFile.writeBytes(data) + logger.debug(logMarker, "Successfully wrote data to file ${dataContainerFile.absolutePath}") + } else { + writeRemoteFile(file, data, dataContainerFile) + } } - fun addPlistValue(path: Path, key: String, value: String, type: String) { - val expandedPath = sshNoEscapingWorkaround(expandPath(path, basePath).toString()) - remote.shell("/usr/libexec/PlistBuddy -c 'Add $key $type $value' $expandedPath", false) // TODO: Simple values only for now + private fun writeRemoteFile(file: File, data: ByteArray, dataContainerFile: File) { + val tmpFile = File.createTempFile("${file.nameWithoutExtension}.", ".${file.extension}") + try { + tmpFile.writeBytes(data) + remote.scpToRemoteHost(tmpFile.absolutePath, dataContainerFile.absolutePath, Duration.ofMinutes(1)) + logger.debug(logMarker, "Successfully wrote data to remote file ${dataContainerFile.absolutePath}") + } finally { + tmpFile.delete() + } + } + + private fun sshNoEscapingWorkaround(path: String): String { + // FIXME: fix escaping on ssh side and remove workarounds + return when { + remote.isLocalhost() -> path + else -> ShellUtils.escape(path) + } + } + + private fun expandPath(path: Path): Path { + val expanded = basePath.toPath().resolve(path).normalize() + if (!expanded.startsWith(basePath.absolutePath)) { + throw DataContainerException("$path points outside the container of $bundleId") + } + + return expanded } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/FileSystem.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/FileSystem.kt index 3c1f4cdf..d050fcda 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/FileSystem.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/FileSystem.kt @@ -37,14 +37,6 @@ class FileSystem( ) } - fun sharedContainer(sharedResourceDirectory: String): SharedContainer { - check(!sharedResourceDirectory.isBlank()) { - "Simulator shared resources directory must not be blank for simulator: $udid" - } - - return SharedContainer(remote, File(sharedResourceDirectory)) - } - private fun getContainerPath(bundleId: String, containerType: String): File { val result = remote.exec( command = listOf( diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/Media.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/Media.kt index 80dd7ae2..134e6e52 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/Media.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/Media.kt @@ -1,11 +1,13 @@ package com.badoo.automation.deviceserver.ios.simulator.data +import com.badoo.automation.deviceserver.command.CommandResult import com.badoo.automation.deviceserver.data.UDID import com.badoo.automation.deviceserver.host.IRemote import com.badoo.automation.deviceserver.util.withDefers import org.slf4j.LoggerFactory import java.io.File import java.nio.file.Paths +import java.time.Duration class Media( private val remote: IRemote, @@ -16,10 +18,7 @@ class Media( private val logger = LoggerFactory.getLogger(javaClass.simpleName) fun reset() { - terminateMobileSlideshowApp() - val imagesPath = mediaPath.resolve("DCIM").toString() - val photoDataPath = mediaPath.resolve("PhotoData").toString() - val removeCmd = "rm -rf $imagesPath; rm -rf $photoDataPath; mkdir -p $imagesPath; mkdir -p $photoDataPath" + val removeCmd = "rm -rf $mediaPath" val result = remote.shell(removeCmd) @@ -29,49 +28,11 @@ class Media( // restart assetsd to prevent fbsimctl upload failing with Error Domain=NSCocoaErrorDomain Code=-1 \"(null)\" restartAssetsd() - - // starting MobileSlideshow app is essential for initializing PhotoData databases - startMobileSlideshowApp() - Thread.sleep(5000) // to make sure app started - } - - fun listPhotoData() : List { - val photoDataPath = mediaPath.resolve("PhotoData/Photos.sqlite").toString() - val tables: List = remote.shell("sqlite3 $photoDataPath \".tables\"").stdOut.lines().map(String::trim) - - val tableName = when { - tables.contains("ZGENERICASSET") -> "ZGENERICASSET" - tables.contains("ZASSET") -> "ZASSET" - else -> throw RuntimeException("Unable to find table with photos") - } - - val sql = "\"select ZFILENAME from $tableName;\"" - val sqlCmd = "sqlite3 $photoDataPath $sql" - return remote.shell(sqlCmd).stdOut.lines().filter(String::isNotBlank) } fun list() : List { val listCmd = listOf("ls", "-1", "$mediaPath/DCIM/100APPLE") - return remote.execIgnoringErrors(listCmd).stdOut.lines().filter(String::isNotBlank) - } - - fun addMedia(media: List) { - withDefers(logger) { - val mediaPaths = if (remote.isLocalhost()) { - media.joinToString(" ") - } else { - val remoteMediaDir = remote.execIgnoringErrors(listOf("/usr/bin/mktemp", "-d")).stdOut.trim() - defer { remote.execIgnoringErrors(listOf("/bin/rm", "-rf", remoteMediaDir)) } - media.forEach { remote.scpToRemoteHost(it.absolutePath, remoteMediaDir) } - media.joinToString(" ") { File(remoteMediaDir, it.name).absolutePath } - } - - val result = remote.shell("/usr/bin/xcrun simctl addmedia $udid $mediaPaths") - - if (!result.isSuccess) { - throw RuntimeException("Could not add Media to device: $result") - } - } + return remote.execIgnoringErrors(listCmd).stdOut.trim().lines() } fun addMedia(file: File, data: ByteArray) { @@ -85,7 +46,7 @@ class Media( } else { val remoteMediaDir = remote.execIgnoringErrors(listOf("/usr/bin/mktemp", "-d")).stdOut.trim() defer { remote.execIgnoringErrors(listOf("/bin/rm", "-rf", remoteMediaDir)) } - remote.scpToRemoteHost(tmpFile.absolutePath, remoteMediaDir) + remote.scpToRemoteHost(tmpFile.absolutePath, remoteMediaDir, Duration.ofMinutes(1)) File(remoteMediaDir, tmpFile.name).absolutePath } @@ -108,39 +69,4 @@ class Media( throw RuntimeException("Could not restart assetsd service: $result") } } - - private fun startMobileSlideshowApp() { - val appStartResult = remote.execIgnoringErrors( - listOf( - "xcrun", "simctl", "launch", udid, "com.apple.mobileslideshow" - ) - ) - - if (!appStartResult.isSuccess) { - throw RuntimeException("Could not start Mobile Slideshow app: $appStartResult") - } - } - - private val notRunningPattern = Regex(".*app is not currently running.*|.*found nothing to terminate.*", - setOf( - RegexOption.MULTILINE, - RegexOption.IGNORE_CASE, - RegexOption.DOT_MATCHES_ALL - ) - ) - - private fun terminateMobileSlideshowApp() { - val appTerminateResult = remote.execIgnoringErrors( - listOf( - "xcrun", "simctl", "terminate", udid, "com.apple.mobileslideshow" - ) - ) - - // The 'notRunningPattern' can be in stdOut when over SSH and in stdErr when local run - if (appTerminateResult.isSuccess || notRunningPattern.matches(appTerminateResult.stdOut) || notRunningPattern.matches(appTerminateResult.stdErr)) { - logger.debug("Successfully terminated the Mobile Slideshow app") - } else { - throw RuntimeException("Could not terminate Mobile Slideshow app: $appTerminateResult") - } - } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/MediaInconsistentcyException.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/MediaInconsistentcyException.kt deleted file mode 100644 index a83ac4b8..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/MediaInconsistentcyException.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.badoo.automation.deviceserver.ios.simulator.data - -class MediaInconsistentcyException: RuntimeException { - constructor(message: String) : super(message) - constructor(message: String, cause: Throwable) : super(message, cause) -} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/SharedContainer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/SharedContainer.kt deleted file mode 100644 index e4de6af9..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/SharedContainer.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.badoo.automation.deviceserver.ios.simulator.data - -import com.badoo.automation.deviceserver.host.IRemote -import java.io.File -import java.nio.file.Path - -class SharedContainer( - private val remote: IRemote, - private val basePath: File -): SimulatorFilesystemContainer(remote) { - - fun delete(path: Path) { - val expandedPath = expandPath(path, basePath) - remote.shell("rm -rf $expandedPath", false) - } - - fun writeFile(data: ByteArray, path: Path) { - val dataContainerFile = expandPath(path, basePath).toFile() - super.writeFile(dataContainerFile, data) - } - - fun readFile(path: Path): ByteArray { - val expandedPath = sshNoEscapingWorkaround(expandPath(path, basePath).toString()) - - return super.readFile(expandedPath) - } -} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/SimulatorFilesystemContainer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/SimulatorFilesystemContainer.kt deleted file mode 100644 index 1db63476..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/SimulatorFilesystemContainer.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.badoo.automation.deviceserver.ios.simulator.data - -import com.badoo.automation.deviceserver.LogMarkers -import com.badoo.automation.deviceserver.command.ShellUtils -import com.badoo.automation.deviceserver.host.IRemote -import com.badoo.automation.deviceserver.util.withDefers -import net.logstash.logback.marker.MapEntriesAppendingMarker -import org.slf4j.LoggerFactory -import java.io.File -import java.lang.RuntimeException -import java.nio.file.Path - -abstract class SimulatorFilesystemContainer(private val remote: IRemote) { - private val logger = LoggerFactory.getLogger(this.javaClass.simpleName) - private val logMarker = MapEntriesAppendingMarker(mapOf( - LogMarkers.HOSTNAME to remote.hostName - )) - - open fun writeFile(file: File, data: ByteArray) { - if (remote.isLocalhost()) { - file.writeBytes(data) - logger.debug(logMarker, "Successfully wrote data to file ${file.absolutePath}") - } else { - withDefers(logger) { - val tmpFile = File.createTempFile("${file.nameWithoutExtension}.", ".${file.extension}") - defer { tmpFile.delete() } - tmpFile.writeBytes(data) - remote.scpToRemoteHost(tmpFile.absolutePath, file.absolutePath) - logger.debug(logMarker, "Successfully wrote data to remote file ${file.absolutePath}") - } - } - } - - fun readFile(path: String): ByteArray { - try { - return remote.captureFile(File(path)) - } catch (e: RuntimeException) { - throw DataContainerException("Could not read file $path", e) - } - } - - fun expandPath(path: Path, basePath: File): Path { - val expanded = basePath.toPath().resolve(path).normalize() - - if (!expanded.startsWith(basePath.absolutePath)) { - throw DataContainerException("$path points outside the container of ${basePath.absolutePath}") - } - - return expanded - } - - internal fun sshNoEscapingWorkaround(path: String): String { - // FIXME: fix escaping on ssh side and remove workarounds - return when { - remote.isLocalhost() -> path - else -> ShellUtils.escape(path) - } - } -} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/OsLog.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/OsLog.kt index 210b8a3e..9a16eeed 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/OsLog.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/OsLog.kt @@ -1,30 +1,19 @@ package com.badoo.automation.deviceserver.ios.simulator.diagnostic import com.badoo.automation.deviceserver.LogMarkers -import com.badoo.automation.deviceserver.data.SysLogCaptureOptions +import com.badoo.automation.deviceserver.command.ShellUtils import com.badoo.automation.deviceserver.data.UDID import com.badoo.automation.deviceserver.host.IRemote -import com.badoo.automation.deviceserver.ios.ISysLog +import io.ktor.util.chomp import net.logstash.logback.marker.MapEntriesAppendingMarker import org.slf4j.LoggerFactory import org.slf4j.Marker import java.io.File -import java.io.InputStream -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.StandardCopyOption -import java.util.concurrent.Executors -import java.util.concurrent.Future class OsLog( private val remote: IRemote, - private val udid: UDID, - override val osLogFile: File = File.createTempFile("iOS_SysLog_${udid}_", ".log"), - override val osLogStderr: File = File.createTempFile("iOS_SysLog_${udid}_", ".err.log") -) : ISysLog { - private var outWritingTask: Future<*>? = null - private var errWritingTask: Future<*>? = null - private var osLogWriterProcess: Process? = null + private val udid: UDID +) { private val logger = LoggerFactory.getLogger(javaClass.simpleName) private val logMarker: Marker = MapEntriesAppendingMarker( mapOf( @@ -35,7 +24,7 @@ class OsLog( private var timestamp: String? = null - override fun truncate(): Boolean { + fun truncate(): Boolean { val date = remote.execIgnoringErrors(listOf("date", "+%s")) if (date.isSuccess) { @@ -45,7 +34,7 @@ class OsLog( return date.isSuccess } - override fun content(process: String?): String { + fun content(process: String?): String { val cmd = mutableListOf("xcrun", "simctl", "spawn", udid, "log", "show", "--style", "syslog") if (timestamp != null) { @@ -66,56 +55,4 @@ class OsLog( return result.stdOut } - - override fun deleteLogFiles() { - osLogFile.delete() - osLogStderr.delete() - } - - override fun stopWritingLog() { - osLogWriterProcess?.destroy() - outWritingTask?.cancel(true) - errWritingTask?.cancel(true) - } - - override fun startWritingLog(sysLogCaptureOptions: SysLogCaptureOptions) { - stopWritingLog() - deleteLogFiles() - - val simulatorBootTimeOutMinutes = 20 - val cmd = mutableListOf( - "/usr/bin/xcrun", "simctl", "spawn", udid, "log", "stream", - "--timeout", "${simulatorBootTimeOutMinutes}m", - "--color", "none", - "--level", "debug") - - if (sysLogCaptureOptions.predicateString.isNotBlank()) { - val predicate = if (remote.isLocalhost()) { - sysLogCaptureOptions.predicateString - } else { - "\"${sysLogCaptureOptions.predicateString}\"" - } - - cmd.add("--predicate") - cmd.add(predicate) - } - - val process: Process = remote.remoteExecutor.startProcess(cmd, mapOf(), logMarker) - - val executor = Executors.newFixedThreadPool(2) - outWritingTask = executor.submit(write(process.inputStream, osLogFile.toPath())) - errWritingTask = executor.submit(write(process.errorStream, osLogStderr.toPath())) - executor.shutdown() - - osLogWriterProcess = process - } - - private fun write(inputStream: InputStream, path: Path): Runnable { - logger.debug("Writing log file to ${path.toFile().absolutePath}") - return Runnable { - inputStream.use { stream -> - Files.copy(stream, path, StandardCopyOption.REPLACE_EXISTING) - } - } - } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/SystemLog.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/SystemLog.kt new file mode 100644 index 00000000..71b6c868 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/SystemLog.kt @@ -0,0 +1,50 @@ +package com.badoo.automation.deviceserver.ios.simulator.diagnostic + +import com.badoo.automation.deviceserver.LogMarkers +import com.badoo.automation.deviceserver.command.ShellUtils +import com.badoo.automation.deviceserver.data.UDID +import com.badoo.automation.deviceserver.host.IRemote +import net.logstash.logback.marker.MapEntriesAppendingMarker +import org.slf4j.LoggerFactory +import org.slf4j.Marker +import java.io.File +import java.nio.charset.StandardCharsets + +class SystemLog( + private val remote: IRemote, + private val udid: UDID +) { + private val logger = LoggerFactory.getLogger(javaClass.simpleName) + private val logMarker: Marker = MapEntriesAppendingMarker( + mapOf( + LogMarkers.UDID to udid, + LogMarkers.HOSTNAME to remote.hostName + ) + ) + + fun truncate(): Boolean { + val path = remote.fbsimctl.diagnose(udid).sysLogLocation ?: return false + + // Following command can be used to correctly rotate syslog. + // newsyslog -R "Log file rotated" $path + // But it requires privilege and we don't actually care about correct rotation + // (as we consider simulators and their logs to be ephemeral). + // All we want is to truncate log. + val rv = remote.shell("echo > ${ShellUtils.escape(path)}", returnOnFailure = true) + + return rv.isSuccess + } + + fun content(): String { + val path = remote.fbsimctl.diagnose(udid).sysLogLocation + ?: throw RuntimeException("Could not determine System Log path") + + try { + return String(remote.captureFile(File(path)), StandardCharsets.UTF_8) + } catch (e: RuntimeException) { + val message = "Could not read System Log. Cause: ${e.message}" + logger.error(logMarker, message) + throw RuntimeException(message, e) + } + } +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/FFMPEGVideoRecorder.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/FFMPEGVideoRecorder.kt deleted file mode 100644 index 1123e456..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/FFMPEGVideoRecorder.kt +++ /dev/null @@ -1,177 +0,0 @@ -package com.badoo.automation.deviceserver.ios.simulator.video - -import com.badoo.automation.deviceserver.ApplicationConfiguration -import com.badoo.automation.deviceserver.LogMarkers -import com.badoo.automation.deviceserver.data.DeviceRef -import com.badoo.automation.deviceserver.data.UDID -import com.badoo.automation.deviceserver.host.IRemote -import com.badoo.automation.deviceserver.util.pollFor -import net.logstash.logback.marker.MapEntriesAppendingMarker -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import java.io.File -import java.io.FileNotFoundException -import java.net.URL -import java.time.Duration - -class FFMPEGVideoRecorder( - private val remote: IRemote, - mjpegServerPort: Int, - private val ref: DeviceRef, - private val udid: UDID, - private val config: ApplicationConfiguration = ApplicationConfiguration() -) : VideoRecorder { - private val logger: Logger = LoggerFactory.getLogger(javaClass.simpleName) - private val logMarker = MapEntriesAppendingMarker( - mapOf( - LogMarkers.HOSTNAME to remote.publicHostName, - LogMarkers.UDID to udid, - LogMarkers.DEVICE_REF to ref - ) - ) - private val videoFileName = "videoRecording_${udid}.mp4" - - private val videoFile = File(config.tempFolder, videoFileName) - private val videoLogFile = File(config.tempFolder, "${videoFileName}.log") - private val videoPidFile = File(config.tempFolder, "${videoFileName}.pid") - - private val remoteVideoPath = File(remote.tmpDir, videoFileName).absolutePath - private val remoteVideoLogPath = File(remote.tmpDir, "${videoFileName}.log").absolutePath - private val remoteVideoPidPath = File(remote.tmpDir, "${videoFileName}.pid").absolutePath - - private val mjpegStreamUrl = URL("http://${remote.publicHostName}:${mjpegServerPort}") - - override fun toString(): String = "${javaClass.simpleName} for $ref" - - override fun delete() { - if (!remote.isLocalhost()) { - val remoteVideoPaths = listOf( - remoteVideoPath, - remoteVideoLogPath, - remoteVideoPidPath - ).joinToString(" ") - remote.shell("rm -vf $remoteVideoPaths") - } - - listOf( - videoFile, - videoLogFile, - videoPidFile - ).forEach { - if (it.exists()) { - it.delete() - } - } - } - - override fun start() { - logger.debug(logMarker, "Starting video recording - ${videoFile.name}") - val command = listOf( - config.remoteVideoRecorder.absolutePath, - udid, - mjpegStreamUrl.toExternalForm(), - remoteVideoPath, - remoteVideoLogPath, - remoteVideoPidPath - ).joinToString(" ") - val result = remote.shell(command) - - if (result.isSuccess) { - logger.info(logMarker, "Started video recording ${videoFile.name}") - } else { - val errorMessage = - "Failed to start video recording ${videoFile.name}. Exit code: ${result.exitCode} StdOut: ${result.stdOut} StdErr: ${result.stdErr}. Log contents: ${getRecordingLog()}" - logger.error(errorMessage) - throw VideoRecordingException(errorMessage) - } - } - - override fun stop() { - logger.debug(logMarker, "Stopping video recording ${videoFile.name}") - val lsofResult = remote.shell("lsof -p $(cat $remoteVideoPidPath) | grep ${videoFile.name}") - - if (lsofResult.isSuccess) { - val pid = lsofResult.stdOut.lines().first().split(whiteSpacesRegex)[1] - logger.debug(logMarker, "Stopping video recording ${videoFile.name}. Got PID $pid") - val killResult = remote.shell("kill -SIGINT $pid") - if (killResult.isSuccess) { - logger.debug(logMarker, "Stopping video recording ${videoFile.name}. Successfully sent SIGINT to PID $pid") - } else { - logger.error(logMarker, "Stopping video recording ${videoFile.name}. Failure while sending SIGINT to PID ${pid}. ${killResult.stdErr}") - } - } else { - logger.warn(logMarker, "Stopping video recording ${videoFile.name}. Failed to get PID from lsof. Maybe process exited. Will use pkill") - val pkillResult = remote.shell("pkill -SIGINT -f ${videoFile.name}") - if (pkillResult.isSuccess) { - logger.debug(logMarker, "Stopping video recording ${videoFile.name}. Successfully sent SIGINT using pkill") - } else { - logger.error(logMarker, "Stopping video recording ${videoFile.name}. Failure while sending SIGINT using pkill. Maybe process exited") - } - } - - var videoRecorderExited = false - val duration = Duration.ofSeconds(10) - pollFor( - duration, - reasonName = "Waiting ${duration.seconds} seconds for video recording to stop", - shouldReturnOnTimeout = true, - retryInterval = Duration.ofMillis(1000), - logger = logger, - marker = logMarker - ) { - videoRecorderExited = remote.shell("pgrep -f ${videoFile.name}").exitCode == 1 // pgrep has exit code 1 when process not found - videoRecorderExited - } - - if (videoRecorderExited) { - logger.info(logMarker, "Stopped video recording ${videoFile.name}. Successfully waited for video recording to exit") - } else { - logger.info(logMarker, "Failed to stop video recording ${videoFile.name}. Recorder process is still running after waiting for ${duration.seconds} seconds") - } - } - - private fun downloadRemoteFile(remotePath: String, localFile: File) { - try { - remote.scpFromRemoteHost(remotePath, localFile.absolutePath, Duration.ofSeconds(60)) - } catch (e: FileNotFoundException) { - logger.error("Failed to find $remotePath at ${remote.hostName}") - } - } - - override fun getRecordingLog(): String { - if (!remote.isLocalhost()) { - downloadRemoteFile(remoteVideoLogPath, videoLogFile) - } - - return if (videoLogFile.exists()) { - videoLogFile.readText() - } else { - "File $videoLogFile not found" - } - } - - override fun getRecording(): ByteArray { - logger.info(logMarker, "Getting video recording ${videoFile.name}") - - if (!remote.isLocalhost()) { - downloadRemoteFile(remoteVideoPath, videoFile) - } - - return if (videoFile.exists()) { - videoFile.readBytes() - } else { - val errorMessage = "Failed to find video recording ${videoFile.absolutePath}. Log contents: ${getRecordingLog()}" - logger.error(errorMessage) - throw VideoRecordingException(errorMessage) - } - } - - override fun dispose() { - stop() - delete() - } - - companion object { - private val whiteSpacesRegex = Regex("\\s+") - } -} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/IOSurfaceAttributes.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/IOSurfaceAttributes.kt new file mode 100644 index 00000000..be643cf8 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/IOSurfaceAttributes.kt @@ -0,0 +1,51 @@ +package com.badoo.automation.deviceserver.ios.simulator.video + +data class IOSurfaceAttributes( + val bytesPerRow: Int, + val size: Int, + val bytesPerElement: Int, + val pixelFormat: String +) { + val width = bytesPerRow / bytesPerElement + val height = size / bytesPerRow + + companion object { + fun fromFbSimctlLog(input: String): IOSurfaceAttributes { + val rowSize = intValueFor("\"row_size\"", input) + val frameSize = intValueFor("\"frame_size\"", input) + val pixelFormat = stringValueFor("format", input) + + if (rowSize == null || frameSize == null || pixelFormat == null) { + throw RuntimeException( + "Cannot parse IOSurface attributes from [$input]." + + "row size: $rowSize, frame size: $frameSize, pixel format $pixelFormat" + ) + } + + if (!pixelFormat.contains("BGRA")) { + throw RuntimeException("Unsupported format $pixelFormat. Only BGRA is supported.") + } + + val bytesPerElement = 4 // default for BGRA + + return IOSurfaceAttributes( + bytesPerRow = rowSize, + size = frameSize, + bytesPerElement = bytesPerElement, + pixelFormat = pixelFormat + ) + } + + private fun intValueFor(key: String, input: String): Int? { + val pattern = """^\s*${Regex.escape(key)}\s*=\s(\d+);$""".toRegex(RegexOption.MULTILINE) + + return pattern.find(input)?.groupValues?.get(1)?.toInt() + } + + private fun stringValueFor(key: String, input: String): String? { + val pattern = """^\s*${Regex.escape(key)}\s*=\s(.+);$""".toRegex(RegexOption.MULTILINE) + + return pattern.find(input)?.groupValues?.get(1) + } + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/MJPEGVideoRecorder.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/MJPEGVideoRecorder.kt new file mode 100644 index 00000000..812fa01c --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/MJPEGVideoRecorder.kt @@ -0,0 +1,178 @@ +package com.badoo.automation.deviceserver.ios.simulator.video + +import com.badoo.automation.deviceserver.LogMarkers +import com.badoo.automation.deviceserver.data.DeviceInfo +import com.badoo.automation.deviceserver.data.DeviceRef +import com.badoo.automation.deviceserver.data.UDID +import com.badoo.automation.deviceserver.host.IRemote +import com.badoo.automation.deviceserver.ios.WdaClient +import com.badoo.automation.deviceserver.util.CustomHttpClient +import net.logstash.logback.marker.MapEntriesAppendingMarker +import okhttp3.Call +import okhttp3.Request +import okhttp3.Response +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.File +import java.net.URI +import java.net.URL +import java.nio.file.Files +import java.nio.file.StandardCopyOption.REPLACE_EXISTING +import java.time.Duration +import java.util.concurrent.Executors +import java.util.concurrent.Future + +class MJPEGVideoRecorder( + private val deviceInfo: DeviceInfo, + private val remote: IRemote, + wdaEndpoint: URI, + mjpegServerPort: Int, + frameRate: Int, + private val ref: DeviceRef, + udid: UDID, + maxVideoDuration: Duration = Duration.ofMinutes(15), + private val videoFile: File = File.createTempFile("videoRecording_${deviceInfo.udid}_", ".mjpeg"), + private val encodedVideoFile: File = File.createTempFile("videoRecording_${deviceInfo.udid}_", ".mp4") +) : VideoRecorder { + private val logger: Logger = LoggerFactory.getLogger(javaClass.simpleName) + private val logMarker = MapEntriesAppendingMarker( + mapOf( + LogMarkers.HOSTNAME to remote.publicHostName, + LogMarkers.UDID to udid, + LogMarkers.DEVICE_REF to ref + ) + ) + private val uniqueTag = "video-recording-$ref" + private val wdaClient = WdaClient(wdaEndpoint.toURL()) + private val httpClient = CustomHttpClient().client.newBuilder().readTimeout(maxVideoDuration).build() + private val mjpegStreamUrl = URL("http://${remote.publicHostName}:${mjpegServerPort}") + private val mjpegSettings: Map = mapOf( + "settings" to mapOf( + "mjpegServerFramerate" to frameRate, + "mjpegScalingFactor" to 50, + "mjpegServerScreenshotQuality" to 100 + ) + ) + private val ffmpegCommand: List = listOf( + "$FFMPEG_PATH", + "-hide_banner", + "-loglevel", "warning", + "-f", "mjpeg", + "-framerate", "$frameRate", + "-i", videoFile.absolutePath, + "-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2", // might use ffprobe to get resolution as well + "-an", + "-vcodec", "h264", + "-preset", "ultrafast", + "-tune", "fastdecode", + "-pix_fmt", "yuv420p", + "-metadata", "comment=$uniqueTag", + "-y", + encodedVideoFile.absolutePath + ) + private var videoRecordingTask: Future<*>? = null + private var videoRecordingHttpCall: Call? = null + + override fun toString(): String = "${javaClass.simpleName} for $ref" + + override fun start() { + cleanupOldRecordings() + adjustVideoStreamSettings() + logger.debug(logMarker, "Starting video recording") + + val request: Request = Request.Builder() + .get() + .url(mjpegStreamUrl) + .build() + + val httpCall = httpClient.newCall(request) + val executor = Executors.newSingleThreadExecutor() + val recordingTask = executor.submit(recordStream(httpCall.execute())) + executor.shutdown() + + videoRecordingTask = recordingTask + videoRecordingHttpCall = httpCall + + logger.debug(logMarker, "Started video recording") + } + + override fun stop() { + logger.debug(logMarker, "Stopping video recording") + stopVideoRecording() + logger.debug(logMarker, "Stopped video recording stopped") + } + + override fun getRecording(): ByteArray { + logger.debug(logMarker, "Getting video recording") + + return try { + if (FFMPEG_PATH == null) { + logger.error("Failed to find ffmpeg utility. Returning uncompressed video.") + videoFile.readBytes() + } else { + compressedVideo() + } + } finally { + delete() + } + } + + override fun delete() { + logger.debug(logMarker, "Deleting video recording") + Files.deleteIfExists(videoFile.toPath()) + Files.deleteIfExists(encodedVideoFile.toPath()) + } + + override fun dispose() { + logger.debug(logMarker, "Disposing video recording") + cleanupOldRecordings() + logger.debug(logMarker, "Disposed video recording") + } + + private fun adjustVideoStreamSettings() { + wdaClient.attachToSession() + val response = wdaClient.updateAppiumSettings(mjpegSettings) + logger.trace(logMarker, "Updated MJPEG streaming server settings: $response") + } + + private fun cleanupOldRecordings() { + stopVideoRecording() + delete() + } + + private fun recordStream(response: Response): Runnable = Runnable { + response.use { + it.body?.let { responseBody -> + responseBody.byteStream().use { inputStream -> + Files.copy(inputStream, videoFile.toPath(), REPLACE_EXISTING) + } + } + } + } + + private fun compressedVideo(): ByteArray { + val result = remote.localExecutor.exec(ffmpegCommand, timeOut = Duration.ofSeconds(60L)) + + if (!result.isSuccess && (!encodedVideoFile.exists() || Files.size(encodedVideoFile.toPath()) == 0L)) { + val message = "Could not compress video file. Result stdErr: ${result.stdErr}" + logger.error(message) + throw SimulatorVideoRecordingException(message) + } + + logger.debug(logMarker, "Successfully compressed video recording.") + return encodedVideoFile.readBytes() + } + + private fun stopVideoRecording() { + videoRecordingHttpCall?.cancel() + videoRecordingHttpCall = null + + videoRecordingTask?.cancel(true) + videoRecordingTask = null + } + + private companion object { + private val ffmpegBinaries = listOf("/usr/local/bin/ffmpeg", "/usr/bin/ffmpeg") + val FFMPEG_PATH = ffmpegBinaries.find { File(it).canExecute() } + } +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt new file mode 100644 index 00000000..4f8878ab --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt @@ -0,0 +1,191 @@ +package com.badoo.automation.deviceserver.ios.simulator.video + +import com.badoo.automation.deviceserver.command.ChildProcess +import com.badoo.automation.deviceserver.data.DeviceInfo +import com.badoo.automation.deviceserver.host.IRemote +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl.Companion.FBSIMCTL_BIN +import com.badoo.automation.deviceserver.ios.proc.LongRunningProc +import com.badoo.automation.deviceserver.util.ensure +import com.badoo.automation.deviceserver.util.pollFor +import java.io.File +import java.time.Duration +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +class SimulatorVideoRecorder( + private val deviceInfo: DeviceInfo, + private val remote: IRemote, + private val childFactory: (remoteHost: String, username: String, cmd: List, commandEnvironment: Map, + out_reader: (line: String) -> Unit, err_reader: (line: String) -> Unit + ) -> ChildProcess? = ChildProcess.Companion::fromCommand, + private val recorderStopTimeout: Duration = RECORDER_STOP_TIMEOUT, + location: File +) : LongRunningProc(deviceInfo.udid, remote.hostName), VideoRecorder { + + private val udid = deviceInfo.udid + + @Volatile + private var isStarted: Boolean = false + + private val recordingLocation = location.absoluteFile + + private val lock = ReentrantLock(true) + + override fun toString(): String = udid + + override fun checkHealth(): Boolean = childProcess?.isAlive() ?: false + + private val uniqueTag = "video-recording-$udid" + + override fun delete() { + logger.debug(logMarker, "Deleting video recording") + + val result = remote.execIgnoringErrors(listOf("rm", "-f", recordingLocation.toString())) + ensure(result.isSuccess) { + SimulatorVideoRecordingException("Could not delete stale recordings. Reason: $result") + } + } + + override fun start() { + logger.info(logMarker, "Starting video recording") + lock.withLock { + if (isStarted) { + val message = "Video recording already started" + logger.error(logMarker, message) + throw SimulatorVideoRecordingException(message) + } + + delete() + + val properties = surfaceAttributes() + + if (!properties.pixelFormat.contains("BGRA")) { + throw RuntimeException("Unexpected pixel format ${properties.pixelFormat}") + } + + val frameWidth = properties.width + val frameHeight = properties.height + + val cmd = shell(videoRecordingCmd(fps = 5, frameWidth = frameWidth, frameHeight = frameHeight)) + + childProcess = childFactory(remote.hostName, remote.userName, cmd, mapOf(), + { logger.debug(logMarker, "$udid: VideoRecorder : ${it.trim()}") }, + { logger.warn(logMarker, "$udid: VideoRecorder : ${it.trim()}") } + ) + + logger.info(logMarker, "Started video recording") + isStarted = true + } + } + + override fun stop() { + lock.withLock { + if (!isStarted) { + val message = "Video recording has not yet started" + logger.warn(logMarker, message) + return + } + + try { + logger.info(logMarker, "Stopping video recording") + + // Regex.escape is incompatible with pkill regex, so let's not escape and hope + val pattern = """^$FFMPEG_PATH.*$uniqueTag""" + remote.execIgnoringErrors(listOf("pkill", "-f", pattern)) + + pollFor(recorderStopTimeout, "Stop video recording", false, Duration.ofMillis(500), logger, logMarker) { + logger.warn("${childProcess?.isAlive()}") + + childProcess?.isAlive() == false + } + } + finally { + childProcess?.kill() + childProcess = null + logger.info(logMarker, "Stopped video recording") + isStarted = false + } + } + } + + override fun getRecording(): ByteArray { + logger.info(logMarker, "Getting video recording") + + val videoFile = recordingLocation + + // TODO: is there a better way to read binary file over ssh without rsyncing? + // We should get rid of ssh and move to having 1 http server per 1 host and some proxy node to tie them together + // once we have proper deployment solution for our macOS machines + try { + val bytes = remote.captureFile(videoFile) + logger.info(logMarker, "Received video recording. Size ${bytes.size} bytes") + return bytes + } catch (e: RuntimeException) { + val message = "Could not read video file. Cause: ${e.message}" + logger.error(message) + throw SimulatorVideoRecordingException(message) + } + } + + override fun dispose() { + if (childProcess?.isAlive() != true) { + return + } + + logger.info(logMarker, "Terminating video recording process") + childProcess?.kill() + delete() + logger.info(logMarker, "Disposed video recording") + } + + private fun shell(command: String): List { + return if (remote.isLocalhost()) { + listOf("bash", "-c", command) + } else { + listOf(command) + } + } + + private fun videoRecordingCmd(fps: Int = 5, frameWidth: Int, frameHeight: Int, crf: Int = 35): String { + val fbsimctlStream = "$FBSIMCTL_BIN $udid stream --bgra --fps $fps -" + + val maxRecording = Duration.ofMinutes(15) // Video recording duration is capped + + val frameSize = "${frameWidth}x$frameHeight" + val recorder = "$FFMPEG_PATH -hide_banner -loglevel warning " + + "-f rawvideo " + + "-pixel_format bgra " + + "-s:v $frameSize " + + "-framerate $fps " + + "-i pipe:0 " + + "-f mp4 -vcodec h264 " + + "-t ${durationToString(maxRecording)} " + + "-crf $crf " + + "-metadata comment=$uniqueTag " + + "-y $recordingLocation" + + return "set -xeo pipefail; $fbsimctlStream | $recorder" + } + + private fun durationToString(duration: Duration): String { + return "%02d:%02d:%02d".format(duration.toHoursPart(), duration.toMinutesPart(), duration.toSecondsPart()) + } + + private fun surfaceAttributes(): IOSurfaceAttributes { + val cmd = shell("set -eo pipefail; $FBSIMCTL_BIN --debug-logging $udid stream --bgra --fps 1 - | exit") + + val rv = remote.execIgnoringErrors(cmd) + + + try { + return IOSurfaceAttributes.fromFbSimctlLog(rv.stdErr) + } catch(e: RuntimeException) { + throw(RuntimeException("Could not get IO surface attributes: $rv", e)) + } + } + + private companion object { + const val FFMPEG_PATH = "/usr/local/bin/ffmpeg" + val RECORDER_STOP_TIMEOUT: Duration = Duration.ofSeconds(3) + } +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecordingException.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecordingException.kt new file mode 100644 index 00000000..454cb7b1 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecordingException.kt @@ -0,0 +1,3 @@ +package com.badoo.automation.deviceserver.ios.simulator.video + +class SimulatorVideoRecordingException(message: String, cause: Throwable? = null) : RuntimeException(message, cause) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/VideoRecorder.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/VideoRecorder.kt index c6dd14ca..7e434506 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/VideoRecorder.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/VideoRecorder.kt @@ -4,7 +4,6 @@ interface VideoRecorder { fun start() fun stop() fun getRecording(): ByteArray - fun getRecordingLog(): String fun delete() fun dispose() } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/VideoRecordingException.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/VideoRecordingException.kt deleted file mode 100644 index ddf9f2da..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/VideoRecordingException.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.badoo.automation.deviceserver.ios.simulator.video - -class VideoRecordingException(message: String, cause: Throwable? = null) : RuntimeException(message, cause) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/AppInstaller.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/AppInstaller.kt deleted file mode 100644 index 6554b196..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/AppInstaller.kt +++ /dev/null @@ -1,152 +0,0 @@ -package com.badoo.automation.deviceserver.util - -import com.badoo.automation.deviceserver.LogMarkers -import com.badoo.automation.deviceserver.data.UDID -import com.badoo.automation.deviceserver.host.IRemote -import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlError -import net.logstash.logback.marker.MapEntriesAppendingMarker -import org.slf4j.LoggerFactory -import org.slf4j.Marker -import java.io.File -import java.lang.RuntimeException -import java.util.concurrent.* -import kotlin.system.measureNanoTime - -class AppInstaller( - private val remote: IRemote, - private val installExecutor: ExecutorService = Executors.newCachedThreadPool() -) { - private val logger = LoggerFactory.getLogger(javaClass.simpleName) - - private val commonLogMarkerDetails = mapOf( - LogMarkers.HOSTNAME to remote.hostName - ) - - fun installApplication(udid: UDID, appUrl: String, appBinaryPath: File, isRealDevice: Boolean): Future { - val logMarker = logMarker(udid) - logger.info(logMarker, "Installing app $appUrl on device $udid") - - return installExecutor.submit(Callable { - try { - return@Callable if (isRealDevice) { - performInstallRealDevice(logMarker, udid, appBinaryPath, appUrl) - } else { - performInstallSimulator(logMarker, udid, appBinaryPath, appUrl) - } - } catch (e: RuntimeException) { - val errorMessage = "Error happened while installing the app $appUrl on $udid. ${e.message}" - logger.error(logMarker, errorMessage, e) - return@Callable InstallResult(false, errorMessage) - } - }) - } - - fun uninstallApplication(udid: UDID, bundleId: String) { - val logMarker = logMarker(udid) - val uninstallTask = installExecutor.submit(Callable { - try { - logger.debug(logMarker, "Uninstalling application $bundleId from Simulator $udid") - val uninstallResult = remote.exec(listOf("/usr/bin/xcrun", "simctl", "uninstall", udid, bundleId), mapOf(), false, 60) - return@Callable uninstallResult.isSuccess - } catch (e: RuntimeException) { - logger.error(logMarker, "Error occured while uninstalling the app $bundleId on $udid", e) - return@Callable false - } - }) - - val result = uninstallTask.get() - if (!result) { - logger.error(logMarker, "Uninstall application $bundleId was unsuccessful.") - } - } - - private fun cleanup(udid: String, logMarker: Marker) { - val stopResult = remote.exec(listOf("/usr/bin/xcrun", "simctl", "spawn", udid, "launchctl", "stop", "com.apple.containermanagerd"), mapOf(), true, 60) - if (!stopResult.isSuccess){ - logger.error(logMarker, "Failed to stop com.apple.containermanagerd for $udid") - } - val deleteResult = remote.exec(listOf("/bin/rm", "-rf", "/Users/qa/$udid/data/Library/Caches/com.apple.containermanagerd"), mapOf(), true, 60) - if (!deleteResult.isSuccess){ - logger.error(logMarker, "Failed to clear cache of com.apple.containermanagerd for $udid") - } - val startResult = remote.exec(listOf("/usr/bin/xcrun", "simctl", "spawn", udid, "launchctl", "start", "com.apple.containermanagerd"), mapOf(), true, 60) - if (!startResult.isSuccess){ - logger.error(logMarker, "Failed to start com.apple.containermanagerd for $udid") - } - } - - private fun performInstallSimulator(logMarker: Marker, udid: UDID, appBinaryPath: File, appUrl: String): InstallResult { - logger.debug(logMarker, "Installing application $appUrl on simulator $udid") - - val nanos = measureNanoTime { - cleanup(udid, logMarker) - logger.debug(logMarker, "Will install application $appUrl on simulator $udid using xcrun simctl install ${appBinaryPath.absolutePath}") - val result = remote.exec(listOf("/usr/bin/xcrun", "simctl", "install", udid, appBinaryPath.absolutePath), mapOf(), true, 90L) - - if (!result.isSuccess) { - val errorMessage = "Failed to install application $appUrl to simulator $udid. Result: $result" - logger.error(logMarker, errorMessage) - return InstallResult(false, errorMessage) - } - } - - val seconds = TimeUnit.NANOSECONDS.toSeconds(nanos) - val measurement = mutableMapOf( - "action_name" to "install_application", - "duration" to seconds - ) - measurement.putAll(logMarkerDetails(udid)) - logger.debug(MapEntriesAppendingMarker(measurement), "Successfully installed application $appUrl on simulator $udid. Took $seconds seconds") - return InstallResult(true, null) - } - - private fun performInstallRealDevice(logMarker: Marker, udid: UDID, appBinaryPath: File, appUrl: String): InstallResult { - logger.debug(logMarker, "Installing application $appUrl on device $udid") - - val nanos = measureNanoTime { - logger.debug(logMarker, "Will install application $appUrl on device $udid using fbsimctl install ${appBinaryPath.absolutePath}") - try { - remote.fbsimctl.installApp(udid, appBinaryPath) - } catch (e: FBSimctlError) { - logger.error(logMarker, "Error happened while installing the app $appUrl on $udid", e) - return InstallResult(false, e.message) - } - } - - val seconds = TimeUnit.NANOSECONDS.toSeconds(nanos) - val measurement = mutableMapOf( - "action_name" to "install_application", - "duration" to seconds - ) - measurement.putAll(logMarkerDetails(udid)) - logger.debug(MapEntriesAppendingMarker(measurement), "Successfully installed application $appUrl on device $udid. Took $seconds seconds") - return InstallResult(true, null) - } - - private fun logMarker(udid: UDID) = MapEntriesAppendingMarker(logMarkerDetails(udid)) - - private fun logMarkerDetails(udid: UDID): Map { - return commonLogMarkerDetails + mapOf( - LogMarkers.DEVICE_REF to deviceRefFromUDID( - udid, - remote.publicHostName - ), LogMarkers.UDID to udid - ) - } - - fun isAppInstalledOnSimulator(udid: UDID, bundleId: String): Boolean { - val result = remote.execIgnoringErrors(listOf( - "/usr/bin/xcrun", - "simctl", - "get_app_container", - udid, - bundleId - )) - - return when(result.exitCode) { - 0 -> true - 2 -> false - else -> throw RuntimeException("Failed to check if app is installed. Result: $result") - } - } -} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClient.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClient.kt index 4e7a770e..c08ef1ac 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClient.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClient.kt @@ -6,12 +6,12 @@ import okhttp3.Request import java.net.* import java.util.concurrent.TimeUnit -class CustomHttpClient { +class CustomHttpClient(val client: OkHttpClient = defaultHttpClient) { companion object { - val client: OkHttpClient = OkHttpClient.Builder() - .connectTimeout(30, TimeUnit.SECONDS) - .writeTimeout(30, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) + private val defaultHttpClient: OkHttpClient = OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) .build() } @@ -42,4 +42,4 @@ class CustomHttpClient { } } -} +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/InfoPlist.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/InfoPlist.kt deleted file mode 100644 index e1a53af1..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/InfoPlist.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.badoo.automation.deviceserver.util - -import org.apache.commons.configuration2.plist.XMLPropertyListConfiguration -import java.io.BufferedReader -import java.io.File -import java.io.FileReader - -/** - * Info.plist - */ -class InfoPlist(file: File) { - private val config: XMLPropertyListConfiguration - - init { - val input = BufferedReader(FileReader(file)) - config = XMLPropertyListConfiguration() - config.read(input) - } - - fun bundleIdentifier(): String = config.getString("CFBundleIdentifier") - fun bundleExecutable(): String = config.getString("CFBundleExecutable") - fun bundleName(): String = config.getString("CFBundleName") -} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/InstallResult.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/InstallResult.kt deleted file mode 100644 index 07d59602..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/InstallResult.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.badoo.automation.deviceserver.util - -data class InstallResult( - val isSuccess: Boolean, - val errorMessage: String? -) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/ProvisioningProfile.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/ProvisioningProfile.kt deleted file mode 100644 index d865735c..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/ProvisioningProfile.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.badoo.automation.deviceserver.util - -import com.badoo.automation.deviceserver.command.ShellCommand -import org.apache.commons.configuration2.plist.XMLPropertyListConfiguration -import java.io.BufferedReader -import java.io.File -import java.io.StringReader - -/** - * embedded.mobileprovision - */ -class ProvisioningProfile(file: File) { - private val config: XMLPropertyListConfiguration - - init { - val result = ShellCommand().exec(listOf("/usr/bin/security", "cms", "-D", "-i", file.absolutePath)) - check(result.isSuccess) { - "Failed to read Provisioning Profile. ${result.stdOut}, ${result.stdErr}" - } - - val input = BufferedReader(StringReader(result.stdOut)) - config = XMLPropertyListConfiguration() - config.read(input) - } - - fun provisionedDevices(): List = config.getList("ProvisionedDevices") as List -} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/Support.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/Support.kt index 2f5ad8b6..85c41f59 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/Support.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/Support.kt @@ -8,8 +8,10 @@ import java.net.URI import java.time.Clock import java.time.Duration import java.time.LocalDateTime -import java.util.* -import java.util.concurrent.* +import java.util.concurrent.ExecutionException +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException fun executeWithTimeout(timeout: Duration, name: String, action: () -> T): T { val executor = Executors.newSingleThreadExecutor() @@ -79,4 +81,3 @@ fun uriWithPath(uri: URI, path: String): URI { } fun deviceRefFromUDID(udid: String, hostName: String): DeviceRef = "$udid-$hostName".replace(Regex("[^-\\w]"), "-") -fun deviceRef(): DeviceRef = UUID.randomUUID().toString() diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/WdaBundle.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/WdaBundle.kt deleted file mode 100644 index 69104060..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/WdaBundle.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.badoo.automation.deviceserver.util - -import java.io.File - -interface WdaBundle { - val bundleId: String - fun xctestRunnerPath(isLocalhost: Boolean): File - fun bundlePath(isLocalhost: Boolean): File - val provisionedDevices: List - val deviceInstrumentationPort: Int - val testIdentifier: String - val bundleName: String -} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/WdaDeviceBundlesProvider.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/WdaDeviceBundlesProvider.kt deleted file mode 100644 index cd0e503a..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/WdaDeviceBundlesProvider.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.badoo.automation.deviceserver.util - -import java.io.File -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.Paths -import java.util.stream.Collectors - -/** - * bundlePath=/Users/qa/.iosctl/device-agent-runner/devices/DeviceAgent-Runner.dev4.app - * bundleId=sh.calaba.DeviceAgent.dev - * provisionedDevices=listOf("udid1", "udid2") - */ -data class WdaDeviceBundle( - override val bundleId: String, - override val bundleName: String, - private val bundlePath: Path, // /app/wda/DeviceAgent.app - private val xctestRunnerPath: Path, // /app/wda/DeviceAgent.app/PlugIns/WebDriverAgentRunner.xctest - private val remoteBundlePath: Path, // /opt/wda/DeviceAgent.app - private val remoteXctestRunnerPath: Path, // /opt/wda/DeviceAgent.app/PlugIns/WebDriverAgentRunner.xctest - override val provisionedDevices: List, - override val deviceInstrumentationPort: Int, - override val testIdentifier: String -) : WdaBundle { - override fun xctestRunnerPath(isLocalhost: Boolean): File = - if (isLocalhost) xctestRunnerPath.toFile() else remoteXctestRunnerPath.toFile() - - override fun bundlePath(isLocalhost: Boolean): File = - if (isLocalhost) bundlePath.toFile() else remoteBundlePath.toFile() -} - -class WdaDeviceBundlesProvider(private val wdaDeviceBundlesBase: Path, private val remoteWdaDeviceBundleRoot: Path) { - fun getWdaDeviceBundles(): List { - val bundlePaths: Set = - Files.list(wdaDeviceBundlesBase).filter { Files.isDirectory(it) && it.fileName.toString().endsWith(".app") } - .collect(Collectors.toSet()) - - val wdaDeviceBundles = bundlePaths.map { bundlePath -> - val infoPlist = InfoPlist(bundlePath.resolve("Info.plist").toFile()) - val provisioningProfile = ProvisioningProfile(bundlePath.resolve("embedded.mobileprovision").toFile()) - val provisionedDevices = provisioningProfile.provisionedDevices() - val bundleId = infoPlist.bundleIdentifier() - val bundleName = infoPlist.bundleName() - val remoteBundlePath = remoteWdaDeviceBundleRoot.resolve(bundlePath.fileName) - val xcTestPath: Path = if (bundleId.contains("DeviceAgent")) DA_XCTEST else WDA_XCTEST - val deviceInstrumentationPort: Int = if (bundleId.contains("DeviceAgent")) DA_PORT else WDA_PORT - val xctestRunnerPath: Path = bundlePath.resolve(xcTestPath) - val remoteXctestRunnerPath: Path = remoteBundlePath.resolve(xcTestPath) - val testIdentifier: String = if (bundleId.contains("DeviceAgent")) "TestRunner/testRunner" else "UITestingUITests/testRunner" - - WdaDeviceBundle( - bundleId, - bundleName, - bundlePath, - xctestRunnerPath, - remoteBundlePath, - remoteXctestRunnerPath, - provisionedDevices, - deviceInstrumentationPort, - testIdentifier - ) - } - - return wdaDeviceBundles - } - - companion object { - private val WDA_XCTEST = Paths.get("PlugIns/WebDriverAgentRunner.xctest") - private val DA_XCTEST = Paths.get("PlugIns/DeviceAgent.xctest") - private const val WDA_PORT = 8100 - private const val DA_PORT = 27753 - } -} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/WdaSimulatorBundlesProvider.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/WdaSimulatorBundlesProvider.kt deleted file mode 100644 index 918a78d8..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/WdaSimulatorBundlesProvider.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.badoo.automation.deviceserver.util - -import java.io.File -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.Paths -import java.util.stream.Collectors - -/** - * bundlePath=/Users/qa/.iosctl/device-agent-runner/simulators/DeviceAgent-Runner.dev4.app - * bundleId=sh.calaba.DeviceAgent.dev - */ -data class WdaSimulatorBundle( - override val bundleId: String, - override val bundleName: String, - private val bundlePath: Path, // /app/wda/DeviceAgent.app - private val xctestRunnerPath: Path, // /app/wda/DeviceAgent.app/PlugIns/WebDriverAgentRunner.xctest - private val remoteBundlePath: Path, // /opt/wda/DeviceAgent.app - private val remoteXctestRunnerPath: Path, // /opt/wda/DeviceAgent.app/PlugIns/WebDriverAgentRunner.xctest - override val provisionedDevices: List, - override val deviceInstrumentationPort: Int, - override val testIdentifier: String -) : WdaBundle { - override fun xctestRunnerPath(isLocalhost: Boolean): File = - if (isLocalhost) xctestRunnerPath.toFile() else remoteXctestRunnerPath.toFile() - - override fun bundlePath(isLocalhost: Boolean): File = - if (isLocalhost) bundlePath.toFile() else remoteBundlePath.toFile() -} - -data class WdaSimulatorBundles( - val deviceAgentBundle: WdaSimulatorBundle, - val webDriverAgentBundle: WdaSimulatorBundle -) - -class WdaSimulatorBundlesProvider( - private val wdaSimulatorBundlesBase: Path, private val remoteBundleRoot: Path -) { - fun getWdaSimulatorBundles(): WdaSimulatorBundles { - val bundlePaths: Set = Files.list(wdaSimulatorBundlesBase) - .filter { Files.isDirectory(it) && it.fileName.toString().endsWith(".app") } - .collect(Collectors.toSet()) - val deviceAgentBundlePath = bundlePaths.find { it.toString().contains("DeviceAgent-Runner") } - ?: throw RuntimeException("Unable to find DeviceAgent-Runner at path $wdaSimulatorBundlesBase") - val webDriverAgentBundlePath = bundlePaths.find { it.toString().contains("WebDriverAgentRunner-Runner") } - ?: throw RuntimeException("Unable to find WebDriverAgentRunner-Runner at path $wdaSimulatorBundlesBase") - - return WdaSimulatorBundles( - deviceAgentBundle = createWdaSimulatorBundle(deviceAgentBundlePath), - webDriverAgentBundle = createWdaSimulatorBundle(webDriverAgentBundlePath) - ) - } - - private fun createWdaSimulatorBundle(bundlePath: Path): WdaSimulatorBundle { - val infoPlist = InfoPlist(bundlePath.resolve("Info.plist").toFile()) - val bundleId = infoPlist.bundleIdentifier() - val bundleName = infoPlist.bundleName() - val remoteBundlePath = remoteBundleRoot.resolve(bundlePath.fileName) - val xcTestPath: Path = if (bundlePath.toString().contains("DeviceAgent")) DA_XCTEST else WDA_XCTEST - val deviceInstrumentationPort: Int = if (bundleId.contains("DeviceAgent")) DA_PORT else WDA_PORT - val xctestRunnerPath: Path = bundlePath.resolve(xcTestPath) - val remoteXctestRunnerPath: Path = remoteBundlePath.resolve(xcTestPath) - val testIdentifier: String = if (bundleId.contains("DeviceAgent")) "TestRunner/testRunner" else "UITestingUITests/testRunner" - - return WdaSimulatorBundle( - bundleId, - bundleName, - bundlePath, - xctestRunnerPath, - remoteBundlePath, - remoteXctestRunnerPath, - listOf(), - deviceInstrumentationPort, - testIdentifier - ) - } - - companion object { - val WDA_XCTEST = Paths.get("PlugIns/WebDriverAgentRunner.xctest") - val DA_XCTEST = Paths.get("PlugIns/DeviceAgent.xctest") - private const val WDA_PORT = 8100 - private const val DA_PORT = 27753 - } -} diff --git a/device-server/src/main/resources/WebDriverAgent-RealDevice-Xcode13.template.xctestrun b/device-server/src/main/resources/WebDriverAgent-RealDevice-Xcode13.template.xctestrun deleted file mode 100644 index d6b32e6d..00000000 --- a/device-server/src/main/resources/WebDriverAgent-RealDevice-Xcode13.template.xctestrun +++ /dev/null @@ -1,94 +0,0 @@ - - - - - TestConfigurations - - - Name - Configuration 1 - TestTargets - - - BlueprintName - __BLUEPRINT_NAME__ - BundleIdentifiersForCrashReportEmphasis - - __DEVICE_AGENT_BUNDLE_ID__ - __DEVICE_AGENT_BUNDLE_ID__.xctrunner - - CommandLineArguments - - --port - __DEVICE_AGENT_PORT__ - --mjpeg-server-port - __DEVICE_AGENT_MJPEG_PORT__ - - DefaultTestExecutionTimeAllowance - 14400 - EnvironmentVariables - - EXAMPLE_VARIABLE - example_value - - IsUITestBundle - - IsXCTRunnerHostedTestBundle - - OnlyTestIdentifiers - - __TEST_IDENTIFIER__ - - ProductModuleName - __PRODUCT_MODULE_NAME__ - RunOrder - 0 - SystemAttachmentLifetime - keepNever - UITargetAppBundleIdentifier - __DEVICE_AGENT_BUNDLE_ID__ - TestHostBundleIdentifier - __DEVICE_AGENT_BUNDLE_ID__ - TestBundleDestinationRelativePath - __TESTBUNDLE_DESTINATION_RELATIVE_PATH__ - TestLanguage - - TestRegion - - TestTimeoutsEnabled - - TestingEnvironmentVariables - - DYLD_INSERT_LIBRARIES - /Developer/usr/lib/libMainThreadChecker.dylib - - UseDestinationArtifacts - - ToolchainsSettingValue - - UITargetAppCommandLineArguments - - UITargetAppEnvironmentVariables - - UITargetAppMainThreadCheckerEnabled - - UserAttachmentLifetime - deleteOnSuccess - - - - - TestPlan - - IsDefault - - Name - DeviceAgent - - __xctestrun_metadata__ - - FormatVersion - 2 - - - diff --git a/device-server/src/main/resources/WebDriverAgent-RealDevice-Xcode15.template.xctestrun b/device-server/src/main/resources/WebDriverAgent-RealDevice-Xcode15.template.xctestrun deleted file mode 100644 index a93f0892..00000000 --- a/device-server/src/main/resources/WebDriverAgent-RealDevice-Xcode15.template.xctestrun +++ /dev/null @@ -1,102 +0,0 @@ - - - - - TestConfigurations - - - Name - Configuration 1 - TestTargets - - - BlueprintName - __BLUEPRINT_NAME__ - BundleIdentifiersForCrashReportEmphasis - - __DEVICE_AGENT_BUNDLE_ID__ - __DEVICE_AGENT_BUNDLE_ID__.xctrunner - - CommandLineArguments - - --port - __DEVICE_AGENT_PORT__ - --mjpeg-server-port - __DEVICE_AGENT_MJPEG_PORT__ - - DefaultTestExecutionTimeAllowance - 14400 - EnvironmentVariables - - EXAMPLE_VARIABLE - example_value - - IsUITestBundle - - IsXCTRunnerHostedTestBundle - - OnlyTestIdentifiers - - __TEST_IDENTIFIER__ - - ProductModuleName - __PRODUCT_MODULE_NAME__ - RunOrder - 0 - SystemAttachmentLifetime - keepNever - UITargetAppBundleIdentifier - __DEVICE_AGENT_BUNDLE_ID__ - TestHostBundleIdentifier - __DEVICE_AGENT_BUNDLE_ID__ - - - TestBundlePath - __TESTHOST__/__TESTBUNDLE_DESTINATION_RELATIVE_PATH__ - TestHostPath - __DEVICE_AGENT_BINARY_PATH__ - UITargetAppPath - __DEVICE_AGENT_BINARY_PATH__ - - - TestLanguage - - TestRegion - - TestTimeoutsEnabled - - TestingEnvironmentVariables - - DYLD_INSERT_LIBRARIES - /Developer/usr/lib/libMainThreadChecker.dylib - - UseDestinationArtifacts - - ToolchainsSettingValue - - UITargetAppCommandLineArguments - - UITargetAppEnvironmentVariables - - UITargetAppMainThreadCheckerEnabled - - UserAttachmentLifetime - deleteOnSuccess - - - - - TestPlan - - IsDefault - - Name - DeviceAgent - - __xctestrun_metadata__ - - FormatVersion - 2 - - - diff --git a/device-server/src/main/resources/WebDriverAgent-Simulator.template.xctestrun b/device-server/src/main/resources/WebDriverAgent-Simulator.template.xctestrun deleted file mode 100644 index 2a8e92d6..00000000 --- a/device-server/src/main/resources/WebDriverAgent-Simulator.template.xctestrun +++ /dev/null @@ -1,79 +0,0 @@ - - - - - __BLUEPRINT_NAME__ - - BlueprintName - __BLUEPRINT_NAME__ - BundleIdentifiersForCrashReportEmphasis - - __DEVICE_AGENT_BUNDLE_ID__ - - CommandLineArguments - - --port - __DEVICE_AGENT_PORT__ - --mjpeg-server-port - __DEVICE_AGENT_MJPEG_PORT__ - - DefaultTestExecutionTimeAllowance - 14400 - EnvironmentVariables - - EXAMPLE_VARIABLE - example_value - - IsUITestBundle - - IsXCTRunnerHostedTestBundle - - OnlyTestIdentifiers - - __TEST_IDENTIFIER__ - - ProductModuleName - __PRODUCT_MODULE_NAME__ - RunOrder - 0 - SystemAttachmentLifetime - keepNever - TestBundlePath - __TESTHOST__/__TESTBUNDLE_DESTINATION_RELATIVE_PATH__ - TestHostBundleIdentifier - __DEVICE_AGENT_BUNDLE_ID__ - TestHostPath - __DEVICE_AGENT_BINARY_PATH__ - TestLanguage - - TestRegion - - TestTimeoutsEnabled - - TestingEnvironmentVariables - - DYLD_FRAMEWORK_PATH - - DYLD_LIBRARY_PATH - - XCODE_DBG_XPC_EXCLUSIONS - com.apple.dt.xctestSymbolicator - - ToolchainsSettingValue - - UITargetAppCommandLineArguments - - UITargetAppMainThreadCheckerEnabled - - UseUITargetAppProvidedByTests - - UserAttachmentLifetime - deleteOnSuccess - - __xctestrun_metadata__ - - FormatVersion - 1 - - - diff --git a/device-server/src/main/resources/record_video_x264.sh b/device-server/src/main/resources/record_video_x264.sh deleted file mode 100755 index 977e1a22..00000000 --- a/device-server/src/main/resources/record_video_x264.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/zsh - -# /opt/homebrew/bin — Homebrew PATH on Macs with Apple silicon -# /usr/local/bin — Homebrew PATH on Macs with Intel CPU - -export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:${PATH}" - -set -u -readonly UDID=${1} -readonly URL=${2} -readonly RECORDING=${3} -readonly RECORDING_LOG=${4} -readonly RECORDING_PID=${5} - -set -xe - -nohup \ - nice -n 10 \ - env PATH="${PATH}" \ - ffmpeg \ - -hide_banner \ - -loglevel info \ - -f mjpeg \ - -framerate 5 \ - -i "${URL}" \ - -vf 'pad=ceil(iw/2)*2:ceil(ih/2)*2' \ - -vf 'scale=400:-2' \ - -an \ - -threads 1 \ - -t "00:15:00" \ - -vcodec h264 \ - -preset ultrafast \ - -tune animation \ - -pix_fmt yuv420p \ - -metadata comment="${RECORDING}" \ - -y \ - "${RECORDING}" \ - &> "${RECORDING_LOG}" 2>&1 & -echo $! > "${RECORDING_PID}" diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/NodeConfigTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/NodeConfigTest.kt index 107956ea..1e3f1051 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/NodeConfigTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/NodeConfigTest.kt @@ -1,6 +1,6 @@ package com.badoo.automation.deviceserver -import com.badoo.automation.deviceserver.ios.device.ConfiguredDevice +import com.badoo.automation.deviceserver.ios.device.KnownDevice import org.junit.Assert import org.junit.Test @@ -34,8 +34,8 @@ class NodeConfigTest { concurrentBoots = 1, whitelistApps = setOf("bundle.id"), uninstallApps = true, - configuredDevices = setOf( - ConfiguredDevice( + knownDevices = listOf( + KnownDevice( "c865bdbe652d17cbe2c79566fb046b73fed66a38" ) ) @@ -56,9 +56,9 @@ class NodeConfigTest { concurrentBoots = 3, whitelistApps = emptySet(), uninstallApps = false, - configuredDevices = emptySet() + knownDevices = emptyList() ) Assert.assertEquals(expected, config) } -} +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/TestUtil.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/TestUtil.kt index 85a514f5..d7a66cf4 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/TestUtil.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/TestUtil.kt @@ -21,7 +21,6 @@ inline fun mockThis(): T = Mockito.mock(T::class.java) inline fun mockThis(name: String): T = Mockito.mock(T::class.java, name) inline fun mockThis(settings: MockSettings): T = Mockito.mock(T::class.java, settings) fun anyType(): T = Mockito.any() as T - /** * Parse a string of permissive JSON. */ @@ -29,16 +28,11 @@ fun json(json: String) = JsonMapper().readTree(json.byteInputStream()) fun deviceDTOStub(ref: DeviceRef): DeviceDTO { return DeviceDTO( - ref, DeviceState.NONE, - URI("http://fbsimctl/endpoint/for/testing"), - URI("http://wda/endpoint/for/testing"), - 0, - URI("http://calabash/endpoint/for/testing"), - 1, - 2, - URI("http://appium/endpoint/for/testing"), - DeviceInfo("", "", "iOS 16.4.1", "", ""), - Exception().toDto(), - capabilities = null - ) -} \ No newline at end of file + ref, DeviceState.NONE, + URI("http://fbsimctl/endpoint/for/testing"), + URI("http://wda/endpoint/for/testing"), + 0, 1, setOf(0, 1), + DeviceInfo("", "", "", "", ""), + Exception().toDto(), + capabilities = null) +} diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommandTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommandTest.kt index 5f080b53..b06033bb 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommandTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommandTest.kt @@ -2,29 +2,27 @@ package com.badoo.automation.deviceserver.command import com.badoo.automation.deviceserver.mockThis import com.nhaarman.mockito_kotlin.whenever -import com.zaxxer.nuprocess.NuProcess -import com.zaxxer.nuprocess.NuProcessBuilder import org.junit.Assert.assertEquals import org.junit.Before -import org.junit.Ignore import org.junit.Test -import org.mockito.Mock import org.mockito.MockitoAnnotations import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.io.OutputStream import java.io.PrintStream import java.time.Duration class RemoteShellCommandTest { private lateinit var systemErr: PrintStream private lateinit var systemOut: PrintStream - private lateinit var spyProcessBuilder: TestProcessBuilder private lateinit var remoteShell: IShellCommand private val remoteHost = "node" private val userName = "user" private val userAtHost = "user@node" - @Before fun setUp() { + @Before + fun setUp() { hideTestOutput() // comment out to debug MockitoAnnotations.initMocks(this) @@ -39,36 +37,25 @@ class RemoteShellCommandTest { System.setOut(testOut) } - @Ignore - @Test fun interactiveSshCommand() { - remoteShell = RemoteShellCommand( - remoteHost = remoteHost, - userName = userName, - connectionTimeout = 1 - ) - remoteShell.exec(listOf("fbsimctl"), timeOut = Duration.ofMillis(100)) - - val expectedCommand = listOf( - "/usr/bin/ssh", - "-o", "ConnectTimeout=1", - "-o", "PreferredAuthentications=publickey", - "-q", - "-t", "-t", - userAtHost, - "fbsimctl" - ) - - assertEquals(expectedCommand, spyProcessBuilder.command()) - } + @Test + fun sshCommandWithEnvironmentVariables() { + val processBuilder: ProcessBuilder = mockThis() + val process: Process = mockThis() + val outputStream: OutputStream = mockThis() + val stdOutStream: InputStream = mockThis() + val stdErrStream: InputStream = mockThis() + whenever(process.inputStream).thenReturn(stdOutStream) + whenever(process.outputStream).thenReturn(outputStream) + whenever(process.errorStream).thenReturn(stdErrStream) + whenever(processBuilder.start()).thenReturn(process) - @Ignore - @Test fun sshCommandWithEnvironmentVariables() { remoteShell = RemoteShellCommand( remoteHost = remoteHost, userName = userName, connectionTimeout = 1 ) - remoteShell.exec(listOf("fbsimctl", "udid='UDID'", "\$PWD"), timeOut = Duration.ofMillis(100)) + + val result = remoteShell.exec(listOf("fbsimctl", "udid='UDID'", "\$PWD"), timeOut = Duration.ofMillis(100), processBuilder = processBuilder) val expectedCommand = listOf( "/usr/bin/ssh", @@ -83,22 +70,7 @@ class RemoteShellCommandTest { "\$PWD" ) - assertEquals(expectedCommand, spyProcessBuilder.command()) + assertEquals(expectedCommand, result.cmd) } - private class TestProcessBuilder(cmd: List, env: Map) : NuProcessBuilder(cmd, env) { - val mockedProcess: NuProcess = mockThis() - init { - whenever(mockedProcess.pid).thenReturn(Int.MAX_VALUE) - } - - override fun start(): NuProcess { - return mockedProcess - } - } - - private fun nuProcessBuilderForTesting(cmd: List, env: Map): NuProcessBuilder { - spyProcessBuilder = TestProcessBuilder(cmd, env) - return spyProcessBuilder - } } diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ShellCommandTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ShellCommandTest.kt index d7c2a1d0..f780086b 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ShellCommandTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ShellCommandTest.kt @@ -15,7 +15,8 @@ class ShellCommandTest { private lateinit var systemOut: PrintStream private lateinit var shellCommand: ShellCommand - @Before fun setUp() { + @Before + fun setUp() { hideTestOutput() // comment out to debug shellCommand = ShellCommand(mapOf()) } @@ -29,8 +30,8 @@ class ShellCommandTest { System.setOut(testOut) } - - @Test fun testCommandWithRealProcess() { + @Test + fun testCommandWithRealProcess() { val result = shellCommand.exec(listOf("ls", "-lah")) assertThat("Wrong exit code", result.exitCode, equalTo(0)) assertThat("StdOut should not be empty", result.stdOut, not(emptyString())) diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ShellUtilsTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ShellUtilsTest.kt index a69d137d..777c37f1 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ShellUtilsTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ShellUtilsTest.kt @@ -9,4 +9,4 @@ class ShellUtilsTest { val actual = ShellUtils.escape("""{"version":1,"created":"2018-02-27T11:56:19"}""") assertEquals("""\{\"version\":1,\"created\":\"2018-02-27T11:56:19\"\}""", actual) } -} +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ZombieReaperTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ZombieReaperTest.kt new file mode 100644 index 00000000..9241b5dc --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ZombieReaperTest.kt @@ -0,0 +1,98 @@ +package com.badoo.automation.deviceserver.command + +import com.sun.jna.Library +import com.sun.jna.Native +import org.junit.After +import org.junit.Assert +import org.junit.Test +import kotlin.streams.toList + +class ZombieReaperTest { + private val reaper = ZombieReaper() + + @After + fun tearDown() { + cleanupChildProcesses() + assertChildProcessCount(0) + } + + @Test + fun testReapZombie() { + createZombie() + assertChildProcessCount(1) + + reaper.reapZombies() + + assertChildProcessCount(0) + } + + @Test + fun testReapOnlyZombies() { + createZombie() + createRegularChildProcess() + assertChildProcessCount(2) + + reaper.reapZombies() + + assertChildProcessCount(1) + } + + private fun assertChildProcessCount(expectedCount: Int) { + Assert.assertEquals( + "Wrong number of child processes", + expectedCount.toLong(), + ProcessHandle.current().children().count() + ) + } + + private fun cleanupChildProcesses() { + val pids = ProcessHandle.current().children().map { it.pid().toInt() }.toList() + + pids.forEach { pid -> + testCLibrary.kill(pid, SIGKILL) + } + + Thread.sleep(100L) // to ensure process has changed it's state + + reaper.reapZombies() + } + + private fun createRegularChildProcess(): Int { + val forkPID = testCLibrary.fork() + + if (forkPID == 0) { + Thread.sleep(2000L) // ensure no actions are taken by a fork + } + + return forkPID + } + + private fun createZombie(): Int { + val forkPID = testCLibrary.fork() + + if (forkPID == 0) { + Thread.sleep(2000L) // ensure no actions are taken by a fork + } else { + Thread.sleep(100L) // wait until initialized properly + killProcess(forkPID) + } + + return forkPID + } + + private fun killProcess(pid: Int) { + val signalResult = testCLibrary.kill(pid, SIGKILL) + Assert.assertEquals("Failed to send SIGKILL to process $pid", 0, signalResult) + Thread.sleep(50L) // to ensure process has changed it's state + } + + companion object { + private const val SIGKILL = 9 + private val testCLibrary: TestCLibrary = Native.load("c", TestCLibrary::class.java) + } +} + +private interface TestCLibrary : Library { + fun kill(pid: Int, signal: Int): Int + fun fork(): Int +} diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/DevicesControllerTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/DevicesControllerTest.kt index 1b687ca5..d5171e1c 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/DevicesControllerTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/DevicesControllerTest.kt @@ -5,7 +5,6 @@ import com.badoo.automation.deviceserver.deviceDTOStub import com.badoo.automation.deviceserver.host.management.DeviceManager import com.badoo.automation.deviceserver.json import com.badoo.automation.deviceserver.mockThis -import com.nhaarman.mockito_kotlin.doNothing import com.nhaarman.mockito_kotlin.whenever import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.sameInstance @@ -13,7 +12,6 @@ import org.junit.Assert.assertThat import org.junit.Test import org.mockito.Mockito.times import org.mockito.Mockito.verify -import java.io.File import java.net.URL private val happyEmpty: Map = mapOf() @@ -30,8 +28,7 @@ class DevicesControllerTest { private val headless = true private val desiredCaps = DesiredCapabilities(udid, model, os, headless) private val desiredCapsNoUdid = DesiredCapabilities(null, model, os) - private val expectedDeviceRef: DeviceRef = "hello" - private val expectedDeviceDTO = deviceDTOStub(expectedDeviceRef) + private val expectedDeviceDTO = deviceDTOStub("hello") @Test fun getStatus() { @@ -59,10 +56,10 @@ class DevicesControllerTest { fun createDevice() { whenever(deviceManager.createDeviceAsync(desiredCaps, null)).thenReturn(expectedDeviceDTO) - val actualDeviceRef = deviceServer.createDevice(desiredCaps, null) + val actualDeviceDTO = deviceServer.createDevice(desiredCaps, null) verify(deviceManager, times(1)).createDeviceAsync(desiredCaps, null) - assertThat(actualDeviceRef, equalTo(expectedDeviceDTO)) + assertThat(actualDeviceDTO, equalTo(expectedDeviceDTO)) } @Test @@ -70,10 +67,18 @@ class DevicesControllerTest { val desiredCapsWithEmptyUdid = DesiredCapabilities(null, model, os, headless) whenever(deviceManager.createDeviceAsync(desiredCapsWithEmptyUdid, null)).thenReturn(expectedDeviceDTO) - val actualDeviceRef = deviceServer.createDevice(desiredCapsNoUdid, null) + val actualDeviceDTO = deviceServer.createDevice(desiredCapsNoUdid, null) verify(deviceManager, times(1)).createDeviceAsync(desiredCapsWithEmptyUdid, null) - assertThat(actualDeviceRef, equalTo(expectedDeviceDTO)) + assertThat(actualDeviceDTO, equalTo(expectedDeviceDTO)) + } + + @Test + fun deleteDevice() { + val actualResult = deviceServer.deleteReleaseDevice(deviceRef) + + verify(deviceManager, times(1)).deleteReleaseDevice(deviceRef, "httpRequest") + assertThat(actualResult, equalTo(happyEmpty)) } @Test @@ -106,6 +111,22 @@ class DevicesControllerTest { assertThat(actualResult, equalTo(happyEmpty)) } + @Test + fun setAccessToCameraAndThings() { + val cameraAndThings = json( + """ + [ + {"bundle_id": "thingy_1"}, + {"bundle_id": "thingy_2"} + ]""" + ) + val actualResult = deviceServer.setAccessToCameraAndThings(deviceRef, cameraAndThings) + + verify(deviceManager).approveAccess(deviceRef, "thingy_1") + verify(deviceManager).approveAccess(deviceRef, "thingy_2") + assertThat(actualResult, equalTo(happyEmpty)) + } + @Test fun getEndpointFor() { val port = 1234 @@ -183,7 +204,7 @@ class DevicesControllerTest { @Test fun getDeviceState() { val expectedState = SimulatorStatusDTO( - true, true, true, true, DeviceState.NONE.value, null) + true, true, true, DeviceState.NONE.value, null) whenever(deviceManager.getDeviceState(deviceRef)).thenReturn(expectedState) val actualResult = deviceServer.getDeviceState(deviceRef) assertThat(actualResult, equalTo(expectedState)) @@ -200,34 +221,4 @@ class DevicesControllerTest { verify(deviceManager).setEnvironmentVariables(deviceRef, environmentVariables) assertThat(actualResult, equalTo(happyEmpty)) } - - @Test - fun getEnvironmentVariable() { - val environmentVariable = "ENV_NAME1" - val expectedValue = "ENV_VAL1" - whenever(deviceManager.getEnvironmentVariable(deviceRef, environmentVariable)).thenReturn(expectedValue) - val actualResult = deviceServer.getEnvironmentVariable(deviceRef, environmentVariable) - - assertThat(actualResult, equalTo(expectedValue)) - } - - @Test - fun pushFile() { - val fakeData = ByteArray(3) - val fakeFile = File("fakeFile").toPath() - doNothing().`when`(deviceManager).pushFile(deviceRef, fakeData, fakeFile) - val actualResult = deviceServer.pushFile(deviceRef, fakeData, fakeFile) - - verify(deviceManager, times(1)).pushFile(deviceRef, fakeData, fakeFile) - assertThat(actualResult, equalTo(happyEmpty)) - } - - @Test - fun deleteFile() { - val fakeFilePath = File("fakeFilePath").toPath() - doNothing().`when`(deviceManager).deleteFile(deviceRef, fakeFilePath) - deviceServer.deleteFile(deviceRef, fakeFilePath) - - verify(deviceManager, times(1)).deleteFile(deviceRef, fakeFilePath) - } } diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/StatusControllerTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/StatusControllerTest.kt index 7e3db170..6b395382 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/StatusControllerTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/StatusControllerTest.kt @@ -4,19 +4,15 @@ import com.badoo.automation.deviceserver.host.management.DeviceManager import com.badoo.automation.deviceserver.mockThis import org.hamcrest.CoreMatchers import org.junit.Assert -import org.junit.Ignore import org.junit.Test class StatusControllerTest { private var deviceManager: DeviceManager = mockThis() private var statusController = StatusController(deviceManager) @Test - @Ignore fun getServerStatus() { - val uptime = System.nanoTime() - Assert.assertThat( - statusController.getServerStatus(uptime), + statusController.getServerStatus(), CoreMatchers.equalTo(mapOf("status" to "ok", "deviceManager" to emptyMap() ))) } diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/DesiredCapabilitiesTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/DesiredCapabilitiesTest.kt index e219b9b9..3ff034c9 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/DesiredCapabilitiesTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/DesiredCapabilitiesTest.kt @@ -66,7 +66,7 @@ class DesiredCapabilitiesTest { val json = """{"use_wda": false}""" val actual = fromJson(json) - assertEquals(DesiredCapabilities(null, null, null, true, true, useWda = false, useAppium = false), actual) + assertEquals(DesiredCapabilities(null, null, null, true, true, useWda = false), actual) } @Test @@ -74,6 +74,6 @@ class DesiredCapabilitiesTest { val json = """{"use_wda": "false"}""" val actual = fromJson(json) - assertEquals(DesiredCapabilities(null, null, null, true, useWda = false, useAppium = false), actual) + assertEquals(DesiredCapabilities(null, null, null, true, useWda = false), actual) } } diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/PermissionSetTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/PermissionSetTest.kt index 66931bc8..16ba9bea 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/PermissionSetTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/PermissionSetTest.kt @@ -13,18 +13,18 @@ class PermissionSetTest { fun fromJsonParsesPermissionSet() { val json = """ { - "location": "revoke", - "calendar": "grant", - "reminders": "reset" + "location": "always", + "calendar": "yes", + "homekit": "unset" } """ val actual = fromJson(json) val expected = PermissionSet() - expected[PermissionType.Calendar] = PermissionAllowed.Grant - expected[PermissionType.Location] = PermissionAllowed.Revoke - expected[PermissionType.Reminders] = PermissionAllowed.Reset + expected[PermissionType.Calendar] = PermissionAllowed.Yes + expected[PermissionType.Location] = PermissionAllowed.Always + expected[PermissionType.HomeKit] = PermissionAllowed.Unset assertEquals(expected, actual) } diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeRegistryTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeRegistryTest.kt index 8e2cd7d1..0d6d36d8 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeRegistryTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeRegistryTest.kt @@ -27,8 +27,8 @@ class NodeRegistryTest { private val desiredCapabilities = DesiredCapabilities("udid", "model", "os", headless) private val nodeWrapper1: NodeWrapper = mockThis("wrapper1") private val nodeWrapper2: NodeWrapper = mockThis("wrapper2") - private val wrappedNode1: IDeviceNode = mockThis("node1") - private val wrappedNode2: IDeviceNode = mockThis("node2") + private val wrappedNode1: ISimulatorsNode = mockThis("node1") + private val wrappedNode2: ISimulatorsNode = mockThis("node2") private val capacityNotBusy = 0.8F private val capacityBusy = 0.3F @@ -78,7 +78,6 @@ class NodeRegistryTest { @Test fun createSimulatorByCapacity() { // arrange - val deviceTimeout = Duration.ofSeconds(0) whenever(nodeWrapper1.isAlive()).thenReturn(true) whenever(nodeWrapper2.isAlive()).thenReturn(true) whenever(wrappedNode1.createDeviceAsync(desiredCapabilities)).then { deviceDTOStub("") } @@ -88,7 +87,7 @@ class NodeRegistryTest { whenever(wrappedNode2.capacityRemaining(desiredCapabilities)).thenReturn(capacityNotBusy) // act - nodeRegistry.createDeviceAsync(desiredCapabilities, deviceTimeout, null) + nodeRegistry.createDeviceAsync(desiredCapabilities, null) // assert verify(activeDevices).registerDevice("", wrappedNode2, null) @@ -100,14 +99,14 @@ class NodeRegistryTest { whenever(nodeWrapper1.isAlive()).thenReturn(true) whenever(wrappedNode1.createDeviceAsync(desiredCapabilities)).then { deviceDTOStub("") } whenever(wrappedNode1.capacityRemaining(desiredCapabilities)).thenReturn(capacityNotBusy) - assertNotNull(nodeRegistry.createDeviceAsync(desiredCapabilities, Duration.ZERO, null)) + assertNotNull(nodeRegistry.createDeviceAsync(desiredCapabilities, null)) // act whenever(nodeWrapper1.isEnabled).thenReturn(false) // assert assertFailsWith { - nodeRegistry.createDeviceAsync(desiredCapabilities, Duration.ZERO, null) + nodeRegistry.createDeviceAsync(desiredCapabilities, null) } } @@ -125,3 +124,4 @@ class NodeRegistryTest { assertEquals(mapOf("total" to 0), nodeRegistry.capacitiesTotal(desiredCapabilities)) } } + diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeWrapperTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeWrapperTest.kt index e97bbb7f..189ebbb4 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeWrapperTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeWrapperTest.kt @@ -5,8 +5,10 @@ import com.badoo.automation.deviceserver.host.management.IHostFactory import com.badoo.automation.deviceserver.host.management.NodeRegistry import com.badoo.automation.deviceserver.host.management.NodeWrapper import com.badoo.automation.deviceserver.mockThis -import com.nhaarman.mockito_kotlin.* -import org.junit.Ignore +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.times +import com.nhaarman.mockito_kotlin.verify +import com.nhaarman.mockito_kotlin.whenever import org.junit.Test import org.mockito.Mockito import java.time.Duration @@ -17,7 +19,7 @@ import kotlin.test.assertTrue class NodeWrapperTest { private val registry: NodeRegistry = mockThis() private val hostFactory: IHostFactory = mockThis() - private val nodeMock: IDeviceNode = mockThis() + private val nodeMock: ISimulatorsNode = mockThis() private val config = NodeConfig("user", "localhost") @Test @@ -33,10 +35,9 @@ class NodeWrapperTest { nodeWrapper.stop() Thread.sleep(nodeCheckInterval * 2) // to ensure that healthChecking thread is not terminated by jvm, but by the nodeWrapper - verify(nodeMock, times(2)).isReachable() + verify(nodeWrapper, times(1)).isAlive() } - @Ignore @Test fun failsToInitOnFactoryError() { whenever(hostFactory.getHostFromConfig(any())).thenThrow(RuntimeException()) @@ -71,12 +72,14 @@ class NodeWrapperTest { @Test fun unregistersSelfIfUnreachableLongEnough() { whenever(hostFactory.getHostFromConfig(any())).thenReturn(nodeMock) - whenever(nodeMock.isReachable()).thenReturn(true, false, false, false, false) + whenever(nodeMock.isReachable()).thenReturn(true) val nodeWrapper = getWrapperWithMocks() nodeWrapper.start() + + whenever(nodeMock.isReachable()).thenReturn(false) nodeWrapper.startPeriodicHealthCheck() - Thread.sleep(1000) + Thread.sleep(100) verify(registry).removeIfPresent(nodeWrapper) } @@ -84,4 +87,4 @@ class NodeWrapperTest { private fun getWrapperWithMocks(nodeCheckInterval: Long) = NodeWrapper(config, hostFactory, registry, 2, Duration.ofMillis(nodeCheckInterval)) -} +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/RemoteTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/RemoteTest.kt index c7587f21..5edb026f 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/RemoteTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/RemoteTest.kt @@ -1,6 +1,5 @@ package com.badoo.automation.deviceserver.host -import com.badoo.automation.deviceserver.ApplicationConfiguration import com.badoo.automation.deviceserver.anyType import com.badoo.automation.deviceserver.command.CommandResult import com.badoo.automation.deviceserver.command.IShellCommand @@ -16,21 +15,10 @@ import org.mockito.ArgumentMatchers.* class RemoteTest { private val localExecutor: IShellCommand = mockThis() private val remoteExecutor: IShellCommand = mockThis() - private val applicationConfiguration: ApplicationConfiguration = mockThis() private lateinit var remote: Remote @Before fun setUp() { - whenever(remoteExecutor.exec(anyList(), anyMap(), anyType(), anyBoolean(), anyType(), anyType())).thenReturn( - CommandResult("", "", 0, pid = 1L) - ) - remote = Remote( - hostName = "host", - userName = "user", - publicHostName = "", - localExecutor = localExecutor, - remoteExecutor = remoteExecutor, - appConfig = applicationConfiguration - ) + remote = Remote("host", "user", "", localExecutor, remoteExecutor) } @Test @@ -49,4 +37,4 @@ class RemoteTest { assertTrue(remote.isReachable()) } -} +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorProviderTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorProviderTest.kt index 38d03671..6735e3ed 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorProviderTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorProviderTest.kt @@ -1,60 +1,105 @@ package com.badoo.automation.deviceserver.host -import com.badoo.automation.deviceserver.command.CommandResult import com.badoo.automation.deviceserver.data.DesiredCapabilities import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDevice import com.badoo.automation.deviceserver.mockThis +import com.nhaarman.mockito_kotlin.times +import com.nhaarman.mockito_kotlin.verify import com.nhaarman.mockito_kotlin.whenever -import org.hamcrest.CoreMatchers.sameInstance +import org.hamcrest.CoreMatchers.* import org.hamcrest.MatcherAssert.assertThat -import org.junit.Before -import org.junit.Ignore import org.junit.Test class SimulatorProviderTest { private val remote: IRemote = mockThis() private val headless = true private val fbSimctl: FBSimctl = mockThis() - private val provider = SimulatorProvider(remote, "/Users/qa/asdf") + + init { + whenever(remote.fbsimctl).thenReturn(fbSimctl) + } + + private val provider = SimulatorProvider(remote) + + private val fbSimctlDevice: FBSimctlDevice = mockThis() + private val dev1 = FBSimctlDevice("arch", "State", "model", "name", "udid-B", "iOS 11") private val dev2 = FBSimctlDevice("arch", "State", "model", "name", "udid-A", "os") - val anyListType = listOf("", "")::class - val anyMapType = mapOf()::class - val anyBooleanType = false::class - val anyLongType = 1L::class - @Before - fun setup() { - whenever(remote.fbsimctl).thenReturn(fbSimctl) - println(anyListType) - println(anyMapType) - println(anyBooleanType) + @Test + fun findByReturnsDeviceIfFound() { + whenever(fbSimctl.listDevice("udid")).thenReturn(fbSimctlDevice) + + val actual = provider.findBy("udid") + + assertThat(actual, sameInstance(fbSimctlDevice)) + } + + @Test + fun findByReturnsNullIfNotFound() { + whenever(fbSimctl.listDevice("udid")).thenReturn(null) + + val actual = provider.findBy("udid") + + assertThat(actual, nullValue()) + } + + @Test + fun listCachesResultAndStripsMissingModelOrOs() { + val expected: List = listOf( + dev1, + FBSimctlDevice("arch", "State", "", "name", "udid", "os"), + FBSimctlDevice("arch", "State", "model", "name", "udid", ""), + dev2 + ) + whenever(fbSimctl.listSimulators()) + .thenReturn(expected) + .thenThrow(RuntimeException("Expected first invocation to be cached")) + val actual = provider.list() + val actual2 = provider.list() + + assertThat(actual, equalTo(listOf(dev1, dev2))) + assertThat(actual2, equalTo(listOf(dev1, dev2))) + } + + @Test + fun createClearsCache() { + whenever(fbSimctl.create("model1", "os", false)).thenReturn(dev1) + whenever(fbSimctl.create("model2", "os", false)).thenReturn(dev2) + whenever(fbSimctl.listSimulators()) + .thenReturn(listOf(dev1)) + .thenReturn(listOf(dev1, dev2)) + .thenThrow(RuntimeException("Expected first invocation to be cached")) + val actual1 = provider.create("model1", "os", false) + provider.list() + val actual2 = provider.create("model2", "os", false) + provider.list() + provider.list() + + assertThat(actual1, sameInstance(dev1)) + assertThat(actual2, sameInstance(dev2)) + verify(fbSimctl, times(2)).listSimulators() } @Test fun matchByUuid() { - val result = CommandResult("udid-A_BACKUP\nudid-B_BACKUP\nudid-C_BACKUP\n", "", 0, true, listOf("/bin/ls"), 1) - whenever(remote.exec(command = listOf("/bin/ls", "-1", "/Users/qa/asdf"), env = mapOf(), returnFailure = false, timeOutSeconds = 60L)).thenReturn(result) - whenever(fbSimctl.listSimulators()).thenReturn(listOf(dev1)) - whenever(remote.fbsimctl.defaultDeviceSet()).thenReturn("/Users/qa/CoreSimulator") - val actual = provider.provideSimulator(DesiredCapabilities("udid-B", "model", "os", headless), emptySet()) + whenever(fbSimctl.listDevice("udid")).thenReturn(dev1) + val actual = provider.match(DesiredCapabilities("udid", "model", "os", headless), emptySet()) assertThat(actual, sameInstance(dev1)) } @Test fun matchByExistingDesiredCaps() { whenever(fbSimctl.listSimulators()).thenReturn(listOf(dev1)) - val result = CommandResult("udid-A_BACKUP\nudid-B_BACKUP\nudid-C_BACKUP\n", "", 0, true, listOf("/bin/ls"), 1) - whenever(remote.exec(command = listOf("/bin/ls", "-1", "/Users/qa/asdf"), env = mapOf(), returnFailure = false, timeOutSeconds = 60L)).thenReturn(result) - val actual = provider.provideSimulator(DesiredCapabilities(null, "model", "iOS 11", true), emptySet()) + val actual = provider.match(DesiredCapabilities(null, "model", "iOS 11", true), emptySet()) assertThat(actual, sameInstance(dev1)) } - @Test @Ignore + @Test fun matchByCreating() { - whenever(fbSimctl.create("model", "os")).thenReturn(dev2) - val actual = provider.provideSimulator(DesiredCapabilities(null, "model", "os", headless, existing = false), emptySet()) + whenever(fbSimctl.create("model", "os", true)).thenReturn(dev2) + val actual = provider.match(DesiredCapabilities(null, "model", "os", headless, existing = false), emptySet()) assertThat(actual, sameInstance(dev2)) } -} +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt index 2cf64320..5c9ae8f9 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt @@ -1,37 +1,27 @@ package com.badoo.automation.deviceserver.host -import com.badoo.automation.deviceserver.ApplicationConfiguration import com.badoo.automation.deviceserver.JsonMapper -import com.badoo.automation.deviceserver.command.CommandResult import com.badoo.automation.deviceserver.data.* import com.badoo.automation.deviceserver.host.management.ISimulatorHostChecker import com.badoo.automation.deviceserver.host.management.PortAllocator import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDevice import com.badoo.automation.deviceserver.ios.simulator.ISimulator -import com.badoo.automation.deviceserver.ios.simulator.video.FFMPEGVideoRecorder +import com.badoo.automation.deviceserver.ios.simulator.video.SimulatorVideoRecorder import com.badoo.automation.deviceserver.mockThis -import com.badoo.automation.deviceserver.util.WdaSimulatorBundle -import com.badoo.automation.deviceserver.util.WdaSimulatorBundles import com.nhaarman.mockito_kotlin.* import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.sameInstance import org.hamcrest.MatcherAssert.assertThat -import org.junit.Assert import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Ignore import org.junit.Test import java.io.File import java.net.URI -import java.nio.file.Paths -import java.util.concurrent.locks.ReentrantLock class SimulatorsNodeTest { private val iRemote: IRemote = mockThis() private val fbSimctl: FBSimctl = mockThis() - private val locationPermissionsLock: ReentrantLock = mockThis() init { whenever(iRemote.fbsimctl).thenReturn(fbSimctl) @@ -39,32 +29,9 @@ class SimulatorsNodeTest { private val hostChecker: ISimulatorHostChecker = mockThis() - private val daSimulatorBundle = WdaSimulatorBundle( - "DeviceAgent", - "DeviceAgent", - Paths.get("some/file/from/wdaPathProc"), - Paths.get("some/file/from/wdaPathProc/PlugIns/DeviceAgent.xctest"), - Paths.get("/remote/some/file/from/wdaPathProc"), - Paths.get("/remote/some/file/from/wdaPathProc/PlugIns/DeviceAgent.xctest"), - listOf(), - 123, - "DeviceAgent" - ) - private val wdaSimulatorBundle = WdaSimulatorBundle( - "DeviceAgent", - "DeviceAgent", - Paths.get("some/file/from/wdaPathProc"), - Paths.get("some/file/from/wdaPathProc/PlugIns/DeviceAgent.xctest"), - Paths.get("/remote/some/file/from/wdaPathProc"), - Paths.get("/remote/some/file/from/wdaPathProc/PlugIns/DeviceAgent.xctest"), - listOf(), - 123, - "DeviceAgent" - ) - - private val wdaSimulatorBundles = WdaSimulatorBundles(daSimulatorBundle, wdaSimulatorBundle) + private val wdaPath = File("some/file/from/wdaPathProc") - private val iSimulatorProvider: SimulatorProvider = mockThis() + private val iSimulatorProvider: ISimulatorProvider = mockThis() private val ref1: DeviceRef = "Udid1-rem-ote-node" @@ -84,11 +51,10 @@ class SimulatorsNodeTest { "Udid2", "Os") - private val portAllocator = PortAllocator(1, 20) + private val portAllocator = PortAllocator(1, 10) private val configuredSimulatorLimit = 3 - private val applicationConfiguration: ApplicationConfiguration = mockThis() private val simulatorFactory: ISimulatorFactory = mockThis() private val publicHostName = "hostname" private val simulatorsNode1 = SimulatorsNode( @@ -97,8 +63,7 @@ class SimulatorsNodeTest { hostChecker, configuredSimulatorLimit, 2, - wdaSimulatorBundles, - applicationConfiguration, + wdaPath, iSimulatorProvider, portAllocator, simulatorFactory @@ -116,25 +81,16 @@ class SimulatorsNodeTest { URI("http://fbsimctl"), URI("http://wda"), 4444, - URI("http://calabash"), - 3333, 5555, - URI("http://appium"), - DeviceInfo("", "", "iOS 16.4.1", "", ""), + setOf(1, 2, 3, 4, 37265), + DeviceInfo("", "", "", "", ""), null, - ActualCapabilities(true, true, false, false, true) + ActualCapabilities(true, true, true) ) private val expectedDeviceDTOJson = JsonMapper().toJson(expectedDeviceDTO) - @Before - fun setup() { - whenever(applicationConfiguration.simulatorBackupPath).thenReturn("/node/specific/device/set") - whenever(iSimulatorProvider.deviceSetPath).thenReturn("/node/specific/device/set") - } - @Test fun shouldPrepareNodeOnlyOnce() { - whenever(simulatorsNode1.remote.shell("/usr/bin/sw_vers -productVersion", returnOnFailure = false)).thenReturn(CommandResult("13.3.1", "", 0, true, ArrayList(), 0L)) simulatorsNode1.prepareNode() val inOrder = inOrder(hostChecker) @@ -152,11 +108,10 @@ class SimulatorsNodeTest { @Test(expected = RuntimeException::class) fun createDeviceAsyncFailsIfNoMatch() { - whenever(iSimulatorProvider.provideSimulator(desiredCapabilities, emptySet())).thenReturn(null) + whenever(iSimulatorProvider.match(desiredCapabilities, emptySet())).thenReturn(null) simulatorsNode.createDeviceAsync(desiredCapabilities) } - @Ignore @Test fun createDeviceAsyncSucceeds() { createDeviceForTest() @@ -165,15 +120,17 @@ class SimulatorsNodeTest { eq("Udid1-rem-ote-node"), eq(iRemote), eq(fbsimulatorDevice), - eq(DeviceAllocatedPorts(1, 2, 3, 4,5)), + eq(DeviceAllocatedPorts(1, 2, 3, 4)), eq("/node/specific/device/set"), - eq(wdaSimulatorBundles), + eq(File("some/file/from/wdaPathProc")), any(), eq(false), eq(false), - eq(false) + eq("FBSimctlDevice(arch=Arch, state=State, model=Model, name=Name, udid=Udid1, os=Os)") ) verify(simulatorMock).prepareAsync() + + assertThat(simulatorsNode.count(), equalTo(1)) } private fun createDeviceForTest(): DeviceDTO = @@ -194,7 +151,7 @@ class SimulatorsNodeTest { whenever(iRemote.publicHostName).thenReturn("rem.ote.node") whenever(iRemote.fbsimctl).thenReturn(fbSimctl) whenever(fbSimctl.defaultDeviceSet()).thenReturn("/node/specific/device/set") - var fbsimmock = whenever(iSimulatorProvider.provideSimulator(eq(desiredCapabilities), any())) + var fbsimmock = whenever(iSimulatorProvider.match(eq(desiredCapabilities), any())) simulatorMocks.forEach { pair -> fbsimmock = fbsimmock.thenReturn(pair.second) } @@ -207,22 +164,21 @@ class SimulatorsNodeTest { simulatorMocks.forEachIndexed { index, pair -> val it = pair.first whenever(it.ref).thenReturn("someref$index") - whenever(it.deviceState).thenReturn(DeviceState.CREATING) - whenever(it.deviceInfo).thenReturn(DeviceInfo("","","iOS 16.4.1","","")) + whenever(it.state).thenReturn(DeviceState.CREATING) + whenever(it.info).thenReturn(DeviceInfo("","","","","")) + whenever(it.userPorts).thenReturn(DeviceAllocatedPorts(1,2,3,4)) whenever(it.fbsimctlEndpoint).thenReturn(URI("http://fbsimctl")) whenever(it.wdaEndpoint).thenReturn(URI("http://wda")) whenever(it.calabashPort).thenReturn(4444 + index) - whenever(it.mjpegServerPort).thenReturn(3333 + index) - whenever(it.appiumPort).thenReturn(5555 + index) - whenever(it.calabashEndpoint).thenReturn(URI("http://calabash")) - whenever(it.appiumEndpoint).thenReturn(URI("http://appium")) + whenever(it.mjpegServerPort).thenReturn(5555 + index) + whenever(it.fbsimctlSubject).thenReturn("string representation of simulatorMock $index") } } @Test fun countStartsAtZero() { - assertThat(simulatorsNode.capacityRemaining(desiredCapabilities), equalTo(1F)) + assertThat(simulatorsNode.count(), equalTo(0)) } @Test @@ -280,6 +236,17 @@ class SimulatorsNodeTest { assertThat(simulatorsNode.capacityRemaining(desiredCapabilities), equalTo(2F/3)) } + @Test + fun approveAccess() { + createDeviceForTest() + + val bundleId = "somebundle" + + simulatorsNode.approveAccess(ref1, bundleId) + + verify(simulatorMock).approveAccess(bundleId) + } + @Test fun clearSafariCookies() { createDeviceForTest() @@ -310,13 +277,11 @@ class SimulatorsNodeTest { URI("http://fbsimctl"), URI("http://wda"), 4444, - URI("http://calabash"), - 3333, 5555, - URI("http://appium"), - DeviceInfo("", "", "iOS 16.4.1", "", ""), + setOf(1,2,3,4,37265), + DeviceInfo("", "", "", "", ""), null, - ActualCapabilities(true, true, false, false, true) + ActualCapabilities(true, true, true) ))) } @@ -346,31 +311,30 @@ class SimulatorsNodeTest { @Test fun deleteReleaseReleasesExistingRef() { - assertThat(simulatorsNode.capacityRemaining(desiredCapabilities), equalTo(1F)) createTwoDevicesForTest() - assertThat(simulatorsNode.capacityRemaining(desiredCapabilities), equalTo(1F/3)) - assertThat(portAllocator.available(), equalTo(10)) + assertThat(simulatorsNode.count(), equalTo(2)) + assertThat(portAllocator.available(), equalTo(2)) val actual = simulatorsNode.deleteRelease(ref1, "test") assertThat(actual, equalTo(true)) verify(simulatorMock).release(any()) - assertThat(simulatorsNode.capacityRemaining(desiredCapabilities), equalTo(1F/3*2)) - assertThat(portAllocator.available(), equalTo(15)) + assertThat(simulatorsNode.count(), equalTo(1)) + assertThat(portAllocator.available(), equalTo(6)) } - @Test @Ignore + @Test fun resetAsync() { createDeviceForTest() simulatorsNode.resetAsync(ref1) - Thread.sleep(1000) + verify(simulatorMock).resetAsync() } - @Ignore @Test + @Test fun state() { createDeviceForTest() - val expected = SimulatorStatusDTO(false, false, false, false, DeviceState.CREATING.value, null) + val expected = SimulatorStatusDTO(false, false, false, DeviceState.CREATING.value, null) whenever(simulatorMock.status()).thenReturn(expected) @@ -381,7 +345,7 @@ class SimulatorsNodeTest { verify(simulatorMock).status() } - private val videoRecorderMock = mockThis() + private val videoRecorderMock = mockThis() @Test fun videoRecorderDelete() { @@ -394,10 +358,34 @@ class SimulatorsNodeTest { verify(videoRecorderMock).delete() } + @Test + fun videoRecorderGet() { + createDeviceForTest() + + whenever(simulatorMock.videoRecorder).thenReturn(videoRecorderMock) + val bytes = ByteArray(23) + + whenever(videoRecorderMock.getRecording()).thenReturn(bytes) + + val byteArray = simulatorsNode.videoRecordingGet(ref1) + + assertThat(byteArray, sameInstance(bytes)) + } + + @Test + fun videoRecorderStart() { + createDeviceForTest() + + whenever(simulatorMock.videoRecorder).thenReturn(videoRecorderMock) + + simulatorsNode.videoRecordingStart(ref1) + + verify(videoRecorderMock).start() + } + @Test fun videoRecorderStop() { createDeviceForTest() - Thread.sleep(1000) whenever(simulatorMock.videoRecorder).thenReturn(videoRecorderMock) @@ -414,16 +402,4 @@ class SimulatorsNodeTest { verify(simulatorMock).setEnvironmentVariables(mapOf()) } - -// @Test - fun getEnvironmentVariable() { - createDeviceForTest() - var variableName = "ENV_VAR1s" - var expectedValue = "ENV_VAR1s" - - whenever(simulatorMock.getEnvironmentVariable(variableName)).thenReturn(expectedValue) - - val actual = simulatorsNode.getEnvironmentVariable(ref1, variableName) - Assert.assertThat(actual, equalTo(expectedValue)) - } } diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevicesTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevicesTest.kt index 502a617f..7fffe359 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevicesTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevicesTest.kt @@ -1,19 +1,20 @@ package com.badoo.automation.deviceserver.ios import com.badoo.automation.deviceserver.deviceDTOStub -import com.badoo.automation.deviceserver.host.IDeviceNode +import com.badoo.automation.deviceserver.host.ISimulatorsNode import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException import com.badoo.automation.deviceserver.mockThis import com.nhaarman.mockito_kotlin.whenever import org.junit.Assert.assertEquals import org.junit.Test +import java.time.Duration class ActiveDevicesTest { @Test fun deviceListShouldIgnoreDisconnectedDevices() { val activeDevices = ActiveDevices() - val node = mockThis() + val node = mockThis() whenever(node.getDeviceDTO("ref1")).thenReturn(deviceDTOStub("ref1")) whenever(node.getDeviceDTO("ref2")).thenThrow(DeviceNotFoundException("")) diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/DeviceManagerTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/DeviceManagerTest.kt index c29611ae..77d92a80 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/DeviceManagerTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/DeviceManagerTest.kt @@ -1,13 +1,23 @@ package com.badoo.automation.deviceserver.ios +import com.badoo.automation.deviceserver.DeviceServerConfig import com.badoo.automation.deviceserver.NodeConfig import com.badoo.automation.deviceserver.data.* import com.badoo.automation.deviceserver.deviceDTOStub -import com.badoo.automation.deviceserver.host.IDeviceNode +import com.badoo.automation.deviceserver.host.ISimulatorsNode +import com.badoo.automation.deviceserver.host.management.DeviceManager import com.badoo.automation.deviceserver.host.management.IHostFactory +import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException import com.badoo.automation.deviceserver.mockThis +import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.whenever +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.sameInstance +import org.junit.Test import org.mockito.Mockito +import org.mockito.Mockito.times +import org.mockito.Mockito.verify import java.net.URL class DeviceManagerTest { @@ -23,18 +33,18 @@ class DeviceManagerTest { private val hostTwo = mockHostWithTotalCapacity(2, true) private val hostsMap = mapOf( - "zero" to hostZero, "one" to hostOne, "two" to hostTwo, - "unreachable" to mockHostWithTotalCapacity(4, false) + "zero" to hostZero, "one" to hostOne, "two" to hostTwo, + "unreachable" to mockHostWithTotalCapacity(4, false) ) private val hostFactory: IHostFactory = object : IHostFactory { - override fun getHostFromConfig(config: NodeConfig): IDeviceNode { + override fun getHostFromConfig(config: NodeConfig): ISimulatorsNode { val nodeName = config.host return hostsMap[nodeName]!! } } - private fun mockHostWithTotalCapacity(total: Int, reachable: Boolean): IDeviceNode { - val m: IDeviceNode = Mockito.mock(IDeviceNode::class.java, "mockHost$total") + private fun mockHostWithTotalCapacity(total: Int, reachable: Boolean): ISimulatorsNode { + val m: ISimulatorsNode = Mockito.mock(ISimulatorsNode::class.java, "mockHost$total") whenever(m.totalCapacity(desiredCaps)).thenReturn(total) whenever(m.isReachable()).thenReturn(reachable) return m @@ -42,156 +52,145 @@ class DeviceManagerTest { private val activeDevices: ActiveDevices = mockThis() -// private val deviceManager = DeviceManager( -// DeviceServerConfig( -// emptyMap(), -// setOf() -// ), -// hostFactory, -// activeDevices -// ) - -// @Test -// fun getDeviceRefsEmpty() { -// hostsMap.forEach { _, mock -> whenever(mock.list()).thenReturn(emptyList()) } -// -// val actualRefs = deviceManager.getDeviceRefs() // .list() -// -// assertThat(actualRefs.size, equalTo(0)) -// } -// -// @Test -// fun deleteReleaseDeviceThatHasBeenReleased() { -// val sessionId = "defaultSessionId" -// whenever(activeDevices.getNodeFor(ref)).thenThrow( -// DeviceNotFoundException("Device [$ref] not found in [$sessionId] activeDevices") -// ) -// deviceManager.deleteReleaseDevice(ref, "httpRequest") -// verify(activeDevices, times(0)).unregisterDeleteDevice(any()) -// } -// -// private fun withDeviceOnHost(host: ISimulatorsNode, block: () -> Unit) { -// whenever(activeDevices.getNodeFor(ref)).thenReturn(host) -// block() -// } -// -// @Test -// fun getMethodReturningDeviceDTO() { -// withDeviceOnHost(hostTwo ) { -// whenever(hostTwo.getDeviceDTO(ref)).thenReturn(expectedDto) -// -// val actualDto = deviceManager.getGetDeviceDTO(ref) -// -// assertThat(actualDto, equalTo(expectedDto)) -// } -// } -// -// @Test -// fun clearSafariCookies() { -// withDeviceOnHost(hostTwo ) { -// deviceManager.clearSafariCookies(ref) -// verify(hostTwo).clearSafariCookies(ref) -// } -// } -// -// @Test -// fun resetAsyncDevice() { -// withDeviceOnHost(hostTwo ) { -// -// deviceManager.resetAsyncDevice(ref) -// -// verify(hostTwo).resetAsync(ref) -// } -// } -// -// @Test -// fun getEndpointFor() { -// withDeviceOnHost(hostTwo ) { -// whenever(hostTwo.endpointFor(ref, 1234)).thenReturn(someUrl) -// val actual = deviceManager.getEndpointFor(ref, 1234) -// assertThat(actual, equalTo(someUrl)) -// } -// } -// -// @Test -// fun getLastCrashLog() { -// withDeviceOnHost(hostTwo) { -// val crashLog = CrashLog("some/path", "stdout from cat of filename") -// whenever(hostTwo.lastCrashLog(ref)).thenReturn(crashLog) -// val actual = deviceManager.getLastCrashLog(ref) -// assertThat(actual, equalTo(crashLog)) -// } -// } -// -// @Test -// fun startVideo() { -// withDeviceOnHost(hostTwo) { -// deviceManager.startVideo(ref) -// verify(hostTwo).videoRecordingStart(ref) -// } -// } -// -// @Test -// fun stopVideo() { -// withDeviceOnHost(hostTwo) { -// deviceManager.stopVideo(ref) -// verify(hostTwo).videoRecordingStop(ref) -// } -// } -// -// @Test -// fun getVideo() { -// withDeviceOnHost(hostTwo) { -// val bytes = ByteArray(3) -// whenever(hostTwo.videoRecordingGet(ref)).thenReturn(bytes) -// val actual = deviceManager.getVideo(ref) -// assertThat(actual, sameInstance(bytes)) -// } -// } -// -// @Test -// fun deleteVideo() { -// withDeviceOnHost(hostTwo) { -// deviceManager.deleteVideo(ref) -// verify(hostTwo).videoRecordingDelete(ref) -// } -// } -// -// @Test -// fun getDeviceState() { // deviceStateDTO -// withDeviceOnHost(hostTwo ) { -// val deviceOrSimulatorStatusBloodyContradictoryNonsense = SimulatorStatusDTO( -// false, false, false, DeviceState.NONE.value, null) -// whenever(hostTwo.state(ref)).thenReturn(deviceOrSimulatorStatusBloodyContradictoryNonsense) -// val actual = deviceManager.getDeviceState(ref) -// assertThat(actual, equalTo(deviceOrSimulatorStatusBloodyContradictoryNonsense)) -// } -// } -// -// @Test -// fun setEnvironmentVariables() { -// withDeviceOnHost(hostTwo) { -// deviceManager.setEnvironmentVariables(ref, mapOf()) -// verify(hostTwo).setEnvironmentVariables(ref, mapOf()) -// } -// } -// -// @Test -// fun deleteFile() { -// withDeviceOnHost(hostTwo) { -// val fakeFilePath = File("filePath").toPath() -// deviceManager.deleteFile(ref, fakeFilePath) -// verify(hostTwo).deleteFile(ref, fakeFilePath) -// } -// } -// -// @Test -// fun pushFile() { -// withDeviceOnHost(hostTwo) { -// val fakeFile = File("filePath").toPath() -// val fakeData = ByteArray(3) -// deviceManager.pushFile(ref, fakeData, fakeFile) -// verify(hostTwo).pushFile(ref, fakeData, fakeFile) -// } -// } + private val deviceManager = DeviceManager( + DeviceServerConfig( + emptyMap(), + setOf() + ), + hostFactory, + activeDevices + ) + + @Test + fun getDeviceRefsEmpty() { + hostsMap.forEach { _, mock -> whenever(mock.list()).thenReturn(emptyList()) } + + val actualRefs = deviceManager.getDeviceRefs() // .list() + + assertThat(actualRefs.size, equalTo(0)) + } + + @Test + fun deleteReleaseDeviceThatHasBeenReleased() { + val sessionId = "defaultSessionId" + whenever(activeDevices.getNodeFor(ref)).thenThrow( + DeviceNotFoundException("Device [$ref] not found in [$sessionId] activeDevices") + ) + deviceManager.deleteReleaseDevice(ref, "httpRequest") + verify(activeDevices, times(0)).unregisterDeleteDevice(any()) + } + + private fun withDeviceOnHost(host: ISimulatorsNode, block: () -> Unit) { + whenever(activeDevices.getNodeFor(ref)).thenReturn(host) + block() + } + + @Test + fun getMethodReturningDeviceDTO() { + withDeviceOnHost(hostTwo) { + whenever(hostTwo.getDeviceDTO(ref)).thenReturn(expectedDto) + + val actualDto = deviceManager.getGetDeviceDTO(ref) + + assertThat(actualDto, equalTo(expectedDto)) + } + } + + @Test + fun clearSafariCookies() { + withDeviceOnHost(hostTwo) { + deviceManager.clearSafariCookies(ref) + verify(hostTwo).clearSafariCookies(ref) + } + } + + @Test + fun resetAsyncDevice() { + withDeviceOnHost(hostTwo) { + + deviceManager.resetAsyncDevice(ref) + + verify(hostTwo).resetAsync(ref) + } + } + + @Test + fun approveAccess() { + withDeviceOnHost(hostTwo) { + deviceManager.approveAccess(ref, bundleId) + verify(hostTwo).approveAccess(ref, bundleId) + } + } + + @Test + fun getEndpointFor() { + withDeviceOnHost(hostTwo) { + whenever(hostTwo.endpointFor(ref, 1234)).thenReturn(someUrl) + val actual = deviceManager.getEndpointFor(ref, 1234) + assertThat(actual, equalTo(someUrl)) + } + } + + @Test + fun getLastCrashLog() { + withDeviceOnHost(hostTwo) { + val crashLog = CrashLog("some/path", "stdout from cat of filename") + whenever(hostTwo.lastCrashLog(ref)).thenReturn(crashLog) + val actual = deviceManager.getLastCrashLog(ref) + assertThat(actual, equalTo(crashLog)) + } + } + + @Test + fun startVideo() { + withDeviceOnHost(hostTwo) { + deviceManager.startVideo(ref) + verify(hostTwo).videoRecordingStart(ref) + } + } + + @Test + fun stopVideo() { + withDeviceOnHost(hostTwo) { + deviceManager.stopVideo(ref) + verify(hostTwo).videoRecordingStop(ref) + } + } + + @Test + fun getVideo() { + withDeviceOnHost(hostTwo) { + val bytes = ByteArray(3) + whenever(hostTwo.videoRecordingGet(ref)).thenReturn(bytes) + val actual = deviceManager.getVideo(ref) + assertThat(actual, sameInstance(bytes)) + } + } + + @Test + fun deleteVideo() { + withDeviceOnHost(hostTwo) { + deviceManager.deleteVideo(ref) + verify(hostTwo).videoRecordingDelete(ref) + } + } + + @Test + fun getDeviceState() { // deviceStateDTO + withDeviceOnHost(hostTwo) { + val deviceOrSimulatorStatusBloodyContradictoryNonsense = SimulatorStatusDTO( + false, false, false, DeviceState.NONE.value, null) + whenever(hostTwo.state(ref)).thenReturn(deviceOrSimulatorStatusBloodyContradictoryNonsense) + val actual = deviceManager.getDeviceState(ref) + assertThat(actual, equalTo(deviceOrSimulatorStatusBloodyContradictoryNonsense)) + } + } + + @Test + fun setEnvironmentVariables() { + withDeviceOnHost(hostTwo) { + deviceManager.setEnvironmentVariables(ref, mapOf()) + verify(hostTwo).setEnvironmentVariables(ref, mapOf()) + } + } } diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/SessionTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/SessionTest.kt index 2ea5236b..502c7e6b 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/SessionTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/SessionTest.kt @@ -1,7 +1,7 @@ package com.badoo.automation.deviceserver.ios import com.badoo.automation.deviceserver.data.DeviceRef -import com.badoo.automation.deviceserver.host.IDeviceNode +import com.badoo.automation.deviceserver.host.ISimulatorsNode import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException import com.badoo.automation.deviceserver.mockThis import org.hamcrest.Matchers.* @@ -10,14 +10,12 @@ import org.junit.Test import java.time.Duration class SessionTest { - private var host1: IDeviceNode = mockThis() - private var host2: IDeviceNode = mockThis() + private var host1: ISimulatorsNode = mockThis() + private var host2: ISimulatorsNode = mockThis() private var sillySeconds: Long = 42L private val session = ActiveDevices(currentTimeSeconds = { sillySeconds++ }) - private val releaseAfterSecs = Duration.ofSeconds(5) - private val deviceRef1: DeviceRef = "hello-1" private val deviceRef2: DeviceRef = "hello-2" diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlTest.kt index c9eacb69..93fe3f6f 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlTest.kt @@ -11,7 +11,6 @@ import org.mockito.ArgumentMatchers.* import org.mockito.Mock import org.mockito.MockitoAnnotations import org.slf4j.Marker -import java.io.File class FBSimctlTest { @Mock private lateinit var executor: IShellCommand @@ -27,7 +26,7 @@ class FBSimctlTest { @Test fun mustTrimLastNewLine() { whenever(executor.exec(anyList(), anyMap(), anyType(), anyBoolean(), any(), anyType())).thenReturn(CommandResult(fbsimctlResponse, "", 0, pid = 1)) - val fbSimctl = FBSimctl(executor, File("/usr/local/bin"), FBSimctlResponseParser()) + val fbSimctl = FBSimctl(executor, FBSimctlResponseParser()) val deviceSets = fbSimctl.defaultDeviceSet() Assert.assertEquals("/a", deviceSets) } @@ -36,7 +35,7 @@ class FBSimctlTest { fun shouldThrowWhenNoDeviceSets() { whenever(executor.exec(anyList(), anyMap(), anyType(), anyBoolean(), any(), anyType())).thenReturn(CommandResult("\n", "", 0, pid = 1)) whenever(parser.parseDeviceSets(anyString())).thenReturn(emptyList()) - val fbSimctl = FBSimctl(executor, File("/usr/local/bin"), parser) + val fbSimctl = FBSimctl(executor, parser) fbSimctl.defaultDeviceSet() } -} +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FbSimctlResponseParserTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FbSimctlResponseParserTest.kt index f77b5386..a3f18bcd 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FbSimctlResponseParserTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FbSimctlResponseParserTest.kt @@ -60,7 +60,7 @@ class FbSimctlResponseParserTest { } @Test fun parseCreateDevice() { - val parsedValue = FBSimctlResponseParser().parseDeviceCreation(simulatorCreateStrings) + val parsedValue = FBSimctlResponseParser().parseDeviceCreation(simulatorCreateStrings, false) assertEquals("7CA9DCE7-22A2-434B-A9EE-3E2A497E3881", parsedValue.udid) } diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/XCRunSimctlResponseParserTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/XCRunSimctlResponseParserTest.kt deleted file mode 100644 index f752ce10..00000000 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/XCRunSimctlResponseParserTest.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.badoo.automation.deviceserver.ios.fbsimctl - -import XCRunSimctl -import org.junit.Assert.assertEquals -import org.junit.Test - -class XCRunSimctlResponseParserTest { - private val simulatorsRuntimesString = """ - { - "runtimes" : [ - { - "bundlePath" : "\/Applications\/Xcode_13_beta_4.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime", - "buildversion" : "19A5307d", - "runtimeRoot" : "\/Applications\/Xcode_13_beta_4.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot", - "identifier" : "com.apple.CoreSimulator.SimRuntime.iOS-15-0", - "version" : "15.0", - "isAvailable" : true, - "supportedDeviceTypes" : [ - { - "bundlePath" : "\/Applications\/Xcode_13_beta_4.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/DeviceTypes\/iPhone 12.simdevicetype", - "name" : "iPhone 12", - "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-12", - "productFamily" : "iPhone" - } - ], - "name" : "iOS 15.0" - } - ] - } - """.trimIndent() - - - @Test - fun parseDeviceTypes() { - deviceTypes.forEach { - assertEquals(it.value, XCRunSimctl.getDeviceModel(it.key)) - } - } - - companion object { - val deviceTypes: Map = mapOf( - "iPhone 4s" to "com.apple.CoreSimulator.SimDeviceType.iPhone-4s", - "iPhone 5" to "com.apple.CoreSimulator.SimDeviceType.iPhone-5", - "iPhone 5s" to "com.apple.CoreSimulator.SimDeviceType.iPhone-5s", - "iPhone 6 Plus" to "com.apple.CoreSimulator.SimDeviceType.iPhone-6-Plus", - "iPhone 6" to "com.apple.CoreSimulator.SimDeviceType.iPhone-6", - "iPhone 6s" to "com.apple.CoreSimulator.SimDeviceType.iPhone-6s", - "iPhone 6s Plus" to "com.apple.CoreSimulator.SimDeviceType.iPhone-6s-Plus", - "iPhone SE (1st generation)" to "com.apple.CoreSimulator.SimDeviceType.iPhone-SE", - "iPhone 7" to "com.apple.CoreSimulator.SimDeviceType.iPhone-7", - "iPhone 7 Plus" to "com.apple.CoreSimulator.SimDeviceType.iPhone-7-Plus", - "iPhone 8" to "com.apple.CoreSimulator.SimDeviceType.iPhone-8", - "iPhone 8 Plus" to "com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus", - "iPhone X" to "com.apple.CoreSimulator.SimDeviceType.iPhone-X", - "iPhone Xs" to "com.apple.CoreSimulator.SimDeviceType.iPhone-XS", - "iPhone Xs Max" to "com.apple.CoreSimulator.SimDeviceType.iPhone-XS-Max", - "iPhone Xʀ" to "com.apple.CoreSimulator.SimDeviceType.iPhone-XR", - "iPhone XR" to "com.apple.CoreSimulator.SimDeviceType.iPhone-XR", - "iPhone 11" to "com.apple.CoreSimulator.SimDeviceType.iPhone-11", - "iPhone 11 Pro" to "com.apple.CoreSimulator.SimDeviceType.iPhone-11-Pro", - "iPhone 11 Pro Max" to "com.apple.CoreSimulator.SimDeviceType.iPhone-11-Pro-Max", - "iPhone SE (2nd generation)" to "com.apple.CoreSimulator.SimDeviceType.iPhone-SE--2nd-generation-", - "iPhone 12 mini" to "com.apple.CoreSimulator.SimDeviceType.iPhone-12-mini", - "iPhone 12" to "com.apple.CoreSimulator.SimDeviceType.iPhone-12", - "iPhone 12 Pro" to "com.apple.CoreSimulator.SimDeviceType.iPhone-12-Pro", - "iPhone 12 Pro Max" to "com.apple.CoreSimulator.SimDeviceType.iPhone-12-Pro-Max", - "iPhone 13 Pro" to "com.apple.CoreSimulator.SimDeviceType.iPhone-13-Pro", - "iPhone 13 Pro Max" to "com.apple.CoreSimulator.SimDeviceType.iPhone-13-Pro-Max", - "iPhone 13 mini" to "com.apple.CoreSimulator.SimDeviceType.iPhone-13-mini", - "iPhone 13" to "com.apple.CoreSimulator.SimDeviceType.iPhone-13", - "iPod touch (7th generation)" to "com.apple.CoreSimulator.SimDeviceType.iPod-touch--7th-generation-", - "iPad 2" to "com.apple.CoreSimulator.SimDeviceType.iPad-2", - "iPad Retina" to "com.apple.CoreSimulator.SimDeviceType.iPad-Retina", - "iPad Air" to "com.apple.CoreSimulator.SimDeviceType.iPad-Air", - "iPad mini 2" to "com.apple.CoreSimulator.SimDeviceType.iPad-mini-2", - "iPad mini 3" to "com.apple.CoreSimulator.SimDeviceType.iPad-mini-3", - "iPad mini 4" to "com.apple.CoreSimulator.SimDeviceType.iPad-mini-4", - "iPad Air 2" to "com.apple.CoreSimulator.SimDeviceType.iPad-Air-2", - "iPad Pro (9.7-inch)" to "com.apple.CoreSimulator.SimDeviceType.iPad-Pro--9-7-inch-", - "iPad Pro (12.9-inch) (1st generation)" to "com.apple.CoreSimulator.SimDeviceType.iPad-Pro", - "iPad (5th generation)" to "com.apple.CoreSimulator.SimDeviceType.iPad--5th-generation-", - "iPad Pro (12.9-inch) (2nd generation)" to "com.apple.CoreSimulator.SimDeviceType.iPad-Pro--12-9-inch---2nd-generation-", - "iPad Pro (10.5-inch)" to "com.apple.CoreSimulator.SimDeviceType.iPad-Pro--10-5-inch-", - "iPad (6th generation)" to "com.apple.CoreSimulator.SimDeviceType.iPad--6th-generation-", - "iPad (7th generation)" to "com.apple.CoreSimulator.SimDeviceType.iPad--7th-generation-", - "iPad Pro (11-inch) (1st generation)" to "com.apple.CoreSimulator.SimDeviceType.iPad-Pro--11-inch-", - "iPad Pro (12.9-inch) (3rd generation)" to "com.apple.CoreSimulator.SimDeviceType.iPad-Pro--12-9-inch---3rd-generation-", - "iPad Pro (11-inch) (2nd generation)" to "com.apple.CoreSimulator.SimDeviceType.iPad-Pro--11-inch---2nd-generation-", - "iPad Pro (12.9-inch) (4th generation)" to "com.apple.CoreSimulator.SimDeviceType.iPad-Pro--12-9-inch---4th-generation-", - "iPad mini (5th generation)" to "com.apple.CoreSimulator.SimDeviceType.iPad-mini--5th-generation-", - "iPad Air (3rd generation)" to "com.apple.CoreSimulator.SimDeviceType.iPad-Air--3rd-generation-", - "iPad (8th generation)" to "com.apple.CoreSimulator.SimDeviceType.iPad--8th-generation-", - "iPad (9th generation)" to "com.apple.CoreSimulator.SimDeviceType.iPad-9th-generation", - "iPad Air (4th generation)" to "com.apple.CoreSimulator.SimDeviceType.iPad-Air--4th-generation-", - "iPad Pro (11-inch) (3rd generation)" to "com.apple.CoreSimulator.SimDeviceType.iPad-Pro-11-inch-3rd-generation", - "iPad Pro (12.9-inch) (5th generation)" to "com.apple.CoreSimulator.SimDeviceType.iPad-Pro-12-9-inch-5th-generation", - "iPad mini (6th generation)" to "com.apple.CoreSimulator.SimDeviceType.iPad-mini-6th-generation", - "Apple Watch - 38mm" to "com.apple.CoreSimulator.SimDeviceType.Apple-Watch-38mm", - "Apple Watch - 42mm" to "com.apple.CoreSimulator.SimDeviceType.Apple-Watch-42mm", - "Apple Watch Series 2 - 38mm" to "com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Series-2-38mm", - "Apple Watch Series 2 - 42mm" to "com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Series-2-42mm", - "Apple Watch Series 3 - 38mm" to "com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Series-3-38mm", - "Apple Watch Series 3 - 42mm" to "com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Series-3-42mm", - "Apple Watch Series 4 - 40mm" to "com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Series-4-40mm", - "Apple Watch Series 4 - 44mm" to "com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Series-4-44mm", - "Apple Watch Series 5 - 40mm" to "com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Series-5-40mm", - "Apple Watch Series 5 - 44mm" to "com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Series-5-44mm", - "Apple Watch SE - 40mm" to "com.apple.CoreSimulator.SimDeviceType.Apple-Watch-SE-40mm", - "Apple Watch SE - 44mm" to "com.apple.CoreSimulator.SimDeviceType.Apple-Watch-SE-44mm", - "Apple Watch Series 6 - 40mm" to "com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Series-6-40mm", - "Apple Watch Series 6 - 44mm" to "com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Series-6-44mm", - "Apple Watch Series 7 - 41mm" to "com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Series-7-41mm", - "Apple Watch Series 7 - 45mm" to "com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Series-7-45mm" - ) - } -} diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/DeviceFbsimctlProcTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/DeviceFbsimctlProcTest.kt index fdb55bde..2e828603 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/DeviceFbsimctlProcTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/DeviceFbsimctlProcTest.kt @@ -4,7 +4,6 @@ import com.badoo.automation.deviceserver.command.ChildProcess import com.badoo.automation.deviceserver.data.UDID import com.badoo.automation.deviceserver.host.IRemote import com.badoo.automation.deviceserver.ios.device.DeviceFbsimctlProc -import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl import com.nhaarman.mockito_kotlin.whenever import org.junit.Before import org.junit.Test @@ -21,17 +20,13 @@ class DeviceFbsimctlProcTest { private lateinit var endpoint: URI @Mock private lateinit var childProcess: ChildProcess - @Mock - private lateinit var fbsimctl: FBSimctl private lateinit var actualCommand: List @Before fun setUp() { MockitoAnnotations.initMocks(this) - whenever(fbsimctl.fbsimctlBinary).thenReturn("/usr/local/bin/fbsimctl") whenever(remote.hostName).thenReturn("hostName") whenever(remote.userName).thenReturn("userName") - whenever(remote.fbsimctl).thenReturn(fbsimctl) whenever(endpoint.port).thenReturn(1) } @@ -67,4 +62,4 @@ class DeviceFbsimctlProcTest { actualCommand = cmd return childProcess } -} +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProcTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProcTest.kt index 88ae3cea..8172a665 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProcTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProcTest.kt @@ -5,7 +5,6 @@ import com.badoo.automation.deviceserver.data.UDID import com.badoo.automation.deviceserver.host.IRemote import com.nhaarman.mockito_kotlin.whenever import org.junit.Before -import org.junit.Ignore import org.junit.Test import org.mockito.Mock import org.mockito.MockitoAnnotations @@ -26,4 +25,51 @@ class FbsimctlProcTest { whenever(remote.userName).thenReturn("userName") whenever(endpoint.port).thenReturn(1) } + + @Test + fun startHeadless() { + val headless = true + FbsimctlProc(remote, udid, endpoint, headless, this::childFactory).start() + val expectedCommand = listOf( + "/usr/local/bin/fbsimctl", + "UDID", + "boot", + "--direct-launch", + "--", + "listen", + "--http", + "1" + ) + + assertEquals(expectedCommand, actualCommand, "Wrong command") + } + @Test + fun startHeaded() { + val headless = false + FbsimctlProc(remote, udid, endpoint, headless, this::childFactory).start() + val expectedCommand = listOf( + "/usr/local/bin/fbsimctl", + "UDID", + "boot", + "--", + "listen", + "--http", + "1" + ) + + assertEquals(expectedCommand, actualCommand, "Wrong command") + } + + @Suppress("UNUSED_PARAMETER") + private fun childFactory( + remoteHost: String, + username: String, + cmd: List, + commandEnvironment: Map, + out_reader: ((line: String) -> Unit)?, + err_reader: ((line: String) -> Unit)? + ): ChildProcess { + actualCommand = cmd + return childProcess + } } \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningFileLoggingProcessListenerTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningFileLoggingProcessListenerTest.kt deleted file mode 100644 index cbf1fa78..00000000 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningFileLoggingProcessListenerTest.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.badoo.automation.deviceserver.ios.proc - -import org.junit.Assert.* -import org.junit.Test - -class LongRunningFileLoggingProcessListenerTest { - - @Test - fun a() { - - } -} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorBackupTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorBackupTest.kt index b70a2d5a..ea4d17df 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorBackupTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorBackupTest.kt @@ -12,7 +12,6 @@ import com.nhaarman.mockito_kotlin.whenever import org.hamcrest.Matchers.matchesPattern import org.junit.Assert.* import org.junit.Before -import org.junit.Ignore import org.junit.Test import org.mockito.ArgumentCaptor import org.mockito.Mock @@ -20,7 +19,6 @@ import org.mockito.Mockito.* import org.mockito.MockitoAnnotations import java.io.File -@Ignore class SimulatorBackupTest { private val metaJson = """ {"version":${SimulatorBackup.CURRENT_VERSION},"created":"2018-01-12 01:46:48 +0000"} @@ -33,13 +31,15 @@ class SimulatorBackupTest { private val resultStub = CommandResult("", "", 0, pid = 1) private val resultFailureStub = CommandResult("There is no such file or directory!", "", 1, pid = 1) @Mock private lateinit var config: ApplicationConfiguration - private val simulatorDirectory: File = File("/home/user/backup/M-Y-P-H-O-N-E") + @Mock + private lateinit var simulatorDirectory: File @Mock private lateinit var simulatorDataDirectory: File @Before fun setUp() { MockitoAnnotations.initMocks(this) + whenever(simulatorDirectory.absolutePath).thenReturn("simulatorDirectory") whenever(simulatorDataDirectory.absolutePath).thenReturn("simulatorDataDirectory") } @@ -49,7 +49,7 @@ class SimulatorBackupTest { whenever(remote.isDirectory(anyString())).thenReturn(true) whenever(remote.execIgnoringErrors(anyList(), anyMap(), anyLong())).thenReturn(resultWithMeta) - val backup: ISimulatorBackup = SimulatorBackup(remote, udid, deviceSetPath, simulatorDirectory, simulatorDataDirectory, config) + val backup: ISimulatorBackup = SimulatorBackup(remote, udid, deviceSetPath) assertTrue("Backup should exist", backup.isExist()) } @@ -58,22 +58,21 @@ class SimulatorBackupTest { whenever(remote.isDirectory(anyString())).thenReturn(true) whenever(remote.execIgnoringErrors(anyList(), anyMap(), anyLong())).thenReturn(resultStub) - val backup: ISimulatorBackup = SimulatorBackup(remote, udid, deviceSetPath, simulatorDirectory, simulatorDataDirectory, config) + val backup: ISimulatorBackup = SimulatorBackup(remote, udid, deviceSetPath) assertFalse("Backup should not exist", backup.isExist()) } - @Ignore @Test fun shouldCreateBackup() { whenever(remote.execIgnoringErrors(anyList(), anyMap(), anyLong())).thenReturn(resultStub) whenever(remote.shell(anyString(), anyBoolean())).thenReturn(resultStub) - SimulatorBackup(remote, udid, deviceSetPath, simulatorDirectory, simulatorDataDirectory, config).create() + SimulatorBackup(remote, udid, deviceSetPath).create() verify(remote, times(3)).execIgnoringErrors(captor.capture() ?: emptyList(), anyMap(), anyLong()) assertEquals("rm -rf $deviceSetPath/${udid}_BACKUP", captor.allValues[0].joinToString(" ")) - assertEquals("cp -Rp $deviceSetPath/$udid /home/user/backup/${udid}_BACKUP", captor.allValues[1].joinToString(" ")) + assertEquals("cp -R $deviceSetPath/$udid /home/user/backup/${udid}_BACKUP", captor.allValues[1].joinToString(" ")) assertEquals("mkdir -p $deviceSetPath/${udid}_BACKUP/data/device_server", captor.allValues[2].joinToString(" ")) val cmdCaptor = ArgumentCaptor.forClass("".javaClass) @@ -86,19 +85,19 @@ class SimulatorBackupTest { @Test(expected = SimulatorBackupError::class) fun shouldDeleteThrow() { whenever(remote.execIgnoringErrors(anyList(), anyMap(), anyLong())).thenReturn(resultFailureStub) - SimulatorBackup(remote, udid, deviceSetPath, simulatorDirectory, simulatorDataDirectory, config).delete() + SimulatorBackup(remote, udid, deviceSetPath).delete() } @Test(expected = SimulatorBackupError::class) fun shouldCreateThrow() { whenever(remote.execIgnoringErrors(anyList(), anyMap(), anyLong())).thenReturn(resultFailureStub) - SimulatorBackup(remote, udid, deviceSetPath, simulatorDirectory, simulatorDataDirectory, config).create() + SimulatorBackup(remote, udid, deviceSetPath).create() } @Test(expected = SimulatorBackupError::class) fun shouldRestoreThrow() { whenever(remote.execIgnoringErrors(anyList(), anyMap(), anyLong())).thenReturn(resultFailureStub) whenever(remote.shell(anyString(), anyBoolean())).thenReturn(resultFailureStub) - SimulatorBackup(remote, udid, deviceSetPath, simulatorDirectory, simulatorDataDirectory, config).restore() + SimulatorBackup(remote, udid, deviceSetPath).restore() } -} +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcessTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcessTest.kt index ac1676e0..516ca8a4 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcessTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcessTest.kt @@ -8,7 +8,8 @@ import com.badoo.automation.deviceserver.mockThis import com.nhaarman.mockito_kotlin.* import org.junit.Assert.assertEquals import org.junit.Test -import kotlin.test.assertNull +import java.lang.IllegalStateException +import kotlin.test.assertFailsWith class SimulatorProcessTest { private val udid: UDID = "ADB25768-5C9D-487E-A787-D271934B78B0" @@ -33,12 +34,14 @@ class SimulatorProcessTest { @Test fun testSimulatorProcessNotFound() { val simulatorProcess = SimulatorProcess(remote, udid, deviceRef) - val noSimulatorFoundCommandResult = CommandResult("", "", 0, pid = 1) + val noSimulatorFoundCommandResult = CommandResult("", "", 1, pid = 1) whenever(remote.execIgnoringErrors(any(), any(), any())) .thenReturn(noSimulatorFoundCommandResult) - assertNull(simulatorProcess.getSimulatorMainProcessPid()) + assertFailsWith { + simulatorProcess.getSimulatorMainProcessPid() + } } @Test diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/FileSystemTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/FileSystemTest.kt index 2f31225a..d262255a 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/FileSystemTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/FileSystemTest.kt @@ -3,18 +3,13 @@ package com.badoo.automation.deviceserver.ios.simulator.data import com.badoo.automation.deviceserver.command.CommandResult import com.badoo.automation.deviceserver.data.UDID import com.badoo.automation.deviceserver.host.IRemote -import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl -import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlAppInfo -import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlAppInfoBundle import com.badoo.automation.deviceserver.mockThis import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.whenever import org.junit.Before import org.junit.Test import java.io.File -import java.nio.file.Paths import kotlin.test.assertEquals -import kotlin.test.assertFails class FileSystemTest { private val udid: UDID = "udid" diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/SharedContainerTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/SharedContainerTest.kt deleted file mode 100644 index 4865a15c..00000000 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/SharedContainerTest.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.badoo.automation.deviceserver.ios.simulator.data - -import com.badoo.automation.deviceserver.command.CommandResult -import com.badoo.automation.deviceserver.host.IRemote -import com.badoo.automation.deviceserver.mockThis -import com.nhaarman.mockito_kotlin.any -import com.nhaarman.mockito_kotlin.doNothing -import com.nhaarman.mockito_kotlin.whenever -import org.junit.Ignore -import org.junit.Test -import org.mockito.Mockito -import java.io.File - -@Ignore -class SharedContainerTest { - private val remote: IRemote = mockThis() - - private val sharedContainerPathStub = File("/Users/qa/Library/Developer/CoreSimulator/Devices/UDID/data/") - - @Test(expected = DataContainerException::class) - fun shouldRaiseErrorOnDeletingOutsideSharedContainer() { - val container = SharedContainer( - remote = remote, - basePath = sharedContainerPathStub - ) - container.delete(File("/Users/qa/Library").toPath()) - } - - @Test - fun shouldDeleteFile() { - val container = SharedContainer( - remote = remote, - basePath = sharedContainerPathStub - ) - val fakeFailToDelete = File(sharedContainerPathStub.path.plus("/config.plist")) - whenever(remote.shell(any(), any())).thenReturn(CommandResult("", "", 0, pid = 1)) - - container.delete(fakeFailToDelete.toPath()) - Mockito.verify(remote, Mockito.times(1)).shell("rm -rf ${fakeFailToDelete.path}", false) - } - - @Test(expected = DataContainerException::class) - fun shouldRaiseErrorOnWritingOutsideSharedContainer() { - val container = SharedContainer( - remote = remote, - basePath = sharedContainerPathStub - ) - container.writeFile(ByteArray(3), File("/Users/qa/Library").toPath()) - } - - @Test - fun shouldPushFile() { - val container = SharedContainer( - remote = remote, - basePath = sharedContainerPathStub - ) - whenever(remote.isLocalhost()).thenReturn(false) - doNothing().`when`(remote).scpToRemoteHost(any(), any(), any()) - - val fakeFailLocation = File(sharedContainerPathStub.path.plus("/config.plist")) - - container.writeFile(ByteArray(3), fakeFailLocation.toPath()) - Mockito.verify(remote, Mockito.times(1)).scpToRemoteHost(any(), any(), any()) - } - - @Test(expected = DataContainerException::class) - fun shouldRaiseErrorOnReadingOutsideSharedContainer() { - val container = SharedContainer( - remote = remote, - basePath = sharedContainerPathStub - ) - container.readFile( File("/Users/qa/Library/fake_file.txt").toPath()) - } - - @Test - fun shouldReadFile() { - val container = SharedContainer( - remote = remote, - basePath = sharedContainerPathStub - ) - whenever(remote.isLocalhost()).thenReturn(false) - whenever(remote.captureFile(any())).thenReturn(ByteArray(2)) - - val fakeFailLocation = File(sharedContainerPathStub.path.plus("/config.plist")) - - container.readFile(fakeFailLocation.toPath()) - Mockito.verify(remote, Mockito.times(1)).captureFile(any()) - } -} diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClientTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClientTest.kt index 1c3d347a..1f848488 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClientTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClientTest.kt @@ -1,21 +1,30 @@ package com.badoo.automation.deviceserver.util +import com.badoo.automation.deviceserver.mockThis import com.badoo.automation.deviceserver.util.HttpCodes.* +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.doAnswer +import com.nhaarman.mockito_kotlin.whenever +import okhttp3.OkHttpClient import org.junit.Assert.assertEquals -import org.junit.Ignore import org.junit.Test +import java.io.IOException import java.net.URL +import java.net.UnknownHostException class CustomHttpClientTest { - @Ignore @Test fun unknownHost() { - val client = CustomHttpClient() + val httpClient = mockThis() + whenever(httpClient.newCall(any())).doAnswer { throw UnknownHostException() } + val client = CustomHttpClient(client = httpClient) val result = client.get(URL("http://1922.168.1.6")) assertEquals("Wrong code", OriginIsUnreachable.code, result.httpCode) } @Test fun connectionRefused() { - val client = CustomHttpClient() + val httpClient = mockThis() + whenever(httpClient.newCall(any())).doAnswer { throw IOException() } + val client = CustomHttpClient(client = httpClient) val result = client.get(URL("http://localhost:1")) assertEquals("Wrong code", WebServerIsDown.code, result.httpCode) } diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/util/SupportKtTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/util/SupportKtTest.kt index 2596a660..4799fb62 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/util/SupportKtTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/util/SupportKtTest.kt @@ -40,4 +40,10 @@ class SupportKtTest { } } } -} \ No newline at end of file + + @Test + fun testDeviceRefFromUDID() { + val deviceRef = deviceRefFromUDID("asDF-124", "my.host.name.domain() @") + assertEquals("Wrong device ref", "asDF-124-my-host-name-domain----", deviceRef) + } +}