From ae84916d9061903760039500377da77bb0c01da8 Mon Sep 17 00:00:00 2001 From: Amanjeet Singh Date: Tue, 26 Nov 2024 20:15:00 +0530 Subject: [PATCH] Ensure only one xcodebuild process, cleanup unwanted retries (#2097) --- .../src/main/java/maestro/Errors.kt | 1 - .../main/java/maestro/drivers/IOSDriver.kt | 53 +++----- .../xctestdriver/XCTestDriverClientTest.kt | 87 ------------ .../kotlin/xcuitest/XCTestDriverClient.kt | 94 +------------ .../xcuitest/api/NetworkErrorHandler.kt | 127 ------------------ .../xcuitest/api/OkHttpClientInstance.kt | 16 +++ .../installer/LocalXCTestInstaller.kt | 100 +++++++------- .../main/java/ios/xctest/XCTestIOSDevice.kt | 1 + 8 files changed, 90 insertions(+), 389 deletions(-) delete mode 100644 maestro-ios-driver/src/main/kotlin/xcuitest/api/NetworkErrorHandler.kt create mode 100644 maestro-ios-driver/src/main/kotlin/xcuitest/api/OkHttpClientInstance.kt diff --git a/maestro-client/src/main/java/maestro/Errors.kt b/maestro-client/src/main/java/maestro/Errors.kt index 0f7b6dab2b..a572635ad2 100644 --- a/maestro-client/src/main/java/maestro/Errors.kt +++ b/maestro-client/src/main/java/maestro/Errors.kt @@ -63,5 +63,4 @@ sealed class MaestroException(override val message: String) : RuntimeException(m sealed class MaestroDriverStartupException(override val message: String): RuntimeException() { class AndroidDriverTimeoutException(message: String): MaestroDriverStartupException(message) class AndroidInstrumentationSetupFailure(message: String): MaestroDriverStartupException(message) - class IOSDriverTimeoutException(message: String): MaestroDriverStartupException(message) } diff --git a/maestro-client/src/main/java/maestro/drivers/IOSDriver.kt b/maestro-client/src/main/java/maestro/drivers/IOSDriver.kt index 41381bf6e5..b3b67ef4ad 100644 --- a/maestro-client/src/main/java/maestro/drivers/IOSDriver.kt +++ b/maestro-client/src/main/java/maestro/drivers/IOSDriver.kt @@ -26,7 +26,6 @@ import hierarchy.AXElement import ios.IOSDevice import ios.IOSDeviceErrors import maestro.* -import maestro.MaestroDriverStartupException.* import maestro.UiElement.Companion.toUiElement import maestro.UiElement.Companion.toUiElementOrNull import maestro.utils.* @@ -35,6 +34,7 @@ import okio.source import org.slf4j.LoggerFactory import util.XCRunnerCLIUtils import java.io.File +import java.net.SocketTimeoutException import java.util.UUID import kotlin.collections.set @@ -51,7 +51,7 @@ class IOSDriver( } override fun open() { - awaitLaunch() + iosDevice.open() } override fun close() { @@ -63,7 +63,7 @@ class IOSDriver( } override fun deviceInfo(): DeviceInfo { - return runDeviceCall { iosDevice.deviceInfo().toCommonDeviceInfo() } + return runDeviceCall("deviceInfo") { iosDevice.deviceInfo().toCommonDeviceInfo() } } override fun launchApp( @@ -96,11 +96,11 @@ class IOSDriver( } override fun tap(point: Point) { - runDeviceCall { iosDevice.tap(point.x, point.y) } + runDeviceCall("tap") { iosDevice.tap(point.x, point.y) } } override fun longPress(point: Point) { - runDeviceCall { iosDevice.longPress(point.x, point.y, 3000) } + runDeviceCall("longPress") { iosDevice.longPress(point.x, point.y, 3000) } } override fun pressKey(code: KeyCode) { @@ -114,7 +114,7 @@ class IOSDriver( KeyCode.LOCK to "lock", ) - runDeviceCall { + runDeviceCall("pressKey") { keyCodeNameMap[code]?.let { name -> iosDevice.pressKey(name) } @@ -126,7 +126,7 @@ class IOSDriver( } override fun contentDescriptor(excludeKeyboardElements: Boolean): TreeNode { - return runDeviceCall { viewHierarchy(excludeKeyboardElements) } + return runDeviceCall("contentDescriptor") { viewHierarchy(excludeKeyboardElements) } } private fun viewHierarchy(excludeKeyboardElements: Boolean): TreeNode { @@ -194,7 +194,7 @@ class IOSDriver( } override fun isKeyboardVisible(): Boolean { - return runDeviceCall { iosDevice.isKeyboardVisible() } + return runDeviceCall("isKeyboardVisible") { iosDevice.isKeyboardVisible() } } override fun swipe( @@ -206,7 +206,7 @@ class IOSDriver( val startPoint = start.coerceIn(maxWidth = deviceInfo.widthGrid, maxHeight = deviceInfo.heightGrid) val endPoint = end.coerceIn(maxWidth = deviceInfo.widthGrid, maxHeight = deviceInfo.heightGrid) - runDeviceCall { + runDeviceCall("swipe") { waitForAppToSettle(null, null) iosDevice.scroll( xStart = startPoint.x.toDouble(), @@ -360,7 +360,7 @@ class IOSDriver( } override fun takeScreenshot(out: Sink, compressed: Boolean) { - runDeviceCall { iosDevice.takeScreenshot(out, compressed) } + runDeviceCall("takeScreenshot") { iosDevice.takeScreenshot(out, compressed) } } override fun startScreenRecording(out: Sink): ScreenRecording { @@ -372,7 +372,7 @@ class IOSDriver( override fun inputText(text: String) { // silently fail if no XCUIElement has focus - runDeviceCall { iosDevice.input(text = text) } + runDeviceCall("inputText") { iosDevice.input(text = text) } } override fun openLink(link: String, appId: String?, autoVerify: Boolean, browser: Boolean) { @@ -384,7 +384,7 @@ class IOSDriver( } override fun eraseText(charactersToErase: Int) { - runDeviceCall { iosDevice.eraseText(charactersToErase) } + runDeviceCall("eraseText") { iosDevice.eraseText(charactersToErase) } } override fun setProxy(host: String, port: Int) { @@ -421,7 +421,7 @@ class IOSDriver( } override fun setPermissions(appId: String, permissions: Map) { - runDeviceCall { + runDeviceCall("setPermissions") { iosDevice.setPermissions(appId, permissions) } } @@ -456,43 +456,24 @@ class IOSDriver( } private fun isScreenStatic(): Boolean { - return runDeviceCall { iosDevice.isScreenStatic() } + return runDeviceCall("isScreenStatic") { iosDevice.isScreenStatic() } } - private fun awaitLaunch() { - val startTime = System.currentTimeMillis() - - while (System.currentTimeMillis() - startTime < getStartupTimeout()) { - runCatching { - iosDevice.open() - return - } - Thread.sleep(100) - } - - throw IOSDriverTimeoutException("Maestro iOS driver did not start up in time") - } - - private fun runDeviceCall(call: () -> T): T { + private fun runDeviceCall(callName: String, call: () -> T): T { return try { call() + } catch (socketTimeoutException: SocketTimeoutException) { + throw MaestroException.DriverTimeout("iOS driver timed out while doing $callName call") } catch (appCrashException: IOSDeviceErrors.AppCrash) { throw MaestroException.AppCrash(appCrashException.errorMessage) } } - private fun getStartupTimeout(): Long = runCatching { - System.getenv(MAESTRO_DRIVER_STARTUP_TIMEOUT).toLong() - }.getOrDefault(SERVER_LAUNCH_TIMEOUT_MS) - companion object { const val NAME = "iOS Simulator" private val LOGGER = LoggerFactory.getLogger(IOSDevice::class.java) - private const val SERVER_LAUNCH_TIMEOUT_MS = 15000L - private const val MAESTRO_DRIVER_STARTUP_TIMEOUT = "MAESTRO_DRIVER_STARTUP_TIMEOUT" - private const val ELEMENT_TYPE_CHECKBOX = 12 private const val ELEMENT_TYPE_SWITCH = 40 private const val ELEMENT_TYPE_TOGGLE = 41 diff --git a/maestro-client/src/test/java/maestro/xctestdriver/XCTestDriverClientTest.kt b/maestro-client/src/test/java/maestro/xctestdriver/XCTestDriverClientTest.kt index 89bf0a9544..513dd5e682 100644 --- a/maestro-client/src/test/java/maestro/xctestdriver/XCTestDriverClientTest.kt +++ b/maestro-client/src/test/java/maestro/xctestdriver/XCTestDriverClientTest.kt @@ -19,33 +19,6 @@ import java.net.InetAddress class XCTestDriverClientTest { - @Test - fun `it should return correct message in case of TimeoutException with 3 retries`() { - // given - val mockWebServer = MockWebServer() - // do not enqueue any response - mockWebServer.start(InetAddress.getByName( "localhost"), 22087) - val httpUrl = mockWebServer.url("/deviceInfo") - - // when - val simulator = MockXCTestInstaller.Simulator( - installationRetryCount = 0, - shouldInstall = false - ) - val mockXCTestInstaller = MockXCTestInstaller(simulator) - val xcTestDriverClient = XCTestDriverClient( - mockXCTestInstaller, - XCTestClient("localhost", 22087) - ) - - // then - assertThrows { - xcTestDriverClient.deviceInfo(httpUrl) - } - mockXCTestInstaller.assertInstallationRetries(5) - mockWebServer.shutdown() - } - @Test fun `it should return the 4xx response as is without retrying`() { // given @@ -138,66 +111,6 @@ class XCTestDriverClientTest { mockWebServer.shutdown() } - @Test - fun `it should return correct message in case of UnknownHostException without retries`() { - // given - val mockWebServer = MockWebServer() - mockWebServer.enqueue( - MockResponse() - .setSocketPolicy(SocketPolicy.DISCONNECT_AT_START) - ) - mockWebServer.start(InetAddress.getByName( "localhost"), 22087) - val httpUrl = mockWebServer.url("http://nonexistent-domain.local") - - // when - val simulator = MockXCTestInstaller.Simulator( - installationRetryCount = 0, - shouldInstall = false - ) - val mockXCTestInstaller = MockXCTestInstaller(simulator) - val xcTestDriverClient = XCTestDriverClient( - mockXCTestInstaller, - XCTestClient("localhost", 22087) - ) - - // then - assertThrows { - xcTestDriverClient.deviceInfo(httpUrl) - } - mockXCTestInstaller.assertInstallationRetries(0) - mockWebServer.shutdown() - } - - @Test - fun `it should return correct message in case of ConnectExceptions with 3 retries`() { - // given - val mockWebServer = MockWebServer() - mockWebServer.enqueue( - MockResponse() - .setSocketPolicy(SocketPolicy.DISCONNECT_DURING_REQUEST_BODY) - ) - mockWebServer.start(InetAddress.getByName( "localhost"), 22087) - val httpUrl = mockWebServer.url("/deviceInfo") - mockWebServer.shutdown() - - // when - val simulator = MockXCTestInstaller.Simulator( - installationRetryCount = 0, - shouldInstall = false - ) - val mockXCTestInstaller = MockXCTestInstaller(simulator) - val xcTestDriverClient = XCTestDriverClient( - mockXCTestInstaller, - XCTestClient("localhost", 22087) - ) - - // then - assertThrows { - xcTestDriverClient.deviceInfo(httpUrl) - } - mockXCTestInstaller.assertInstallationRetries(5) - } - companion object { @JvmStatic diff --git a/maestro-ios-driver/src/main/kotlin/xcuitest/XCTestDriverClient.kt b/maestro-ios-driver/src/main/kotlin/xcuitest/XCTestDriverClient.kt index a491a26f73..ceb555fa27 100644 --- a/maestro-ios-driver/src/main/kotlin/xcuitest/XCTestDriverClient.kt +++ b/maestro-ios-driver/src/main/kotlin/xcuitest/XCTestDriverClient.kt @@ -6,21 +6,19 @@ import maestro.utils.network.XCUITestServerError import okhttp3.* import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody -import okhttp3.ResponseBody.Companion.toResponseBody import okhttp3.logging.HttpLoggingInterceptor import org.slf4j.LoggerFactory import xcuitest.api.* -import xcuitest.api.NetworkErrorHandler.Companion.RETRY_RESPONSE_CODE import xcuitest.installer.XCTestInstaller import java.io.IOException -import java.net.ConnectException -import java.net.SocketTimeoutException -import java.net.UnknownHostException import java.util.concurrent.TimeUnit class XCTestDriverClient( private val installer: XCTestInstaller, - private val httpInterceptor: HttpLoggingInterceptor? = null + private val okHttpClient: OkHttpClient = OkHttpClient.Builder() + .connectTimeout(1, TimeUnit.SECONDS) + .readTimeout(200, TimeUnit.SECONDS) + .build() ) { private val logger = LoggerFactory.getLogger(XCTestDriverClient::class.java) @@ -32,13 +30,10 @@ class XCTestDriverClient( private var isShuttingDown = false - private val networkErrorHandler by lazy { NetworkErrorHandler(installer) } - init { Runtime.getRuntime().addShutdownHook(Thread { isShuttingDown = true }) - httpInterceptor?.level = HttpLoggingInterceptor.Level.BODY } fun restartXCTestRunner() { @@ -49,18 +44,6 @@ class XCTestDriverClient( client = installer.start() ?: throw XCTestDriverUnreachable("Failed to start XCTest Driver") } - private val okHttpClient = OkHttpClient.Builder() - .connectTimeout(1, TimeUnit.SECONDS) - .readTimeout(200, TimeUnit.SECONDS) - .apply { - httpInterceptor?.let { - this.addInterceptor(it) - } - } - .addRetryInterceptor() - .addRetryAndShutdownInterceptor() - .build() - class XCTestDriverUnreachable(message: String) : IOException(message) private val mapper = jacksonObjectMapper() @@ -272,12 +255,6 @@ class XCTestDriverClient( responseBodyAsString ) } - code == NetworkErrorHandler.NO_RETRY_RESPONSE_CODE -> { - logger.error("Request for $pathString failed, because of XCUITest server got crashed/exit, body: $responseBodyAsString") - throw XCUITestServerError.NetworkError( - "Request for $pathString failed, because of XCUITest server got crashed/exit, body: $responseBodyAsString" - ) - } error.errorMessage.contains("Lost connection to the application.*".toRegex()) -> { logger.error("Request for $pathString failed, because of app crash, body: $responseBodyAsString") throw XCUITestServerError.AppCrash( @@ -311,67 +288,4 @@ class XCTestDriverClient( } } - private fun OkHttpClient.Builder.addRetryInterceptor() = addInterceptor(Interceptor { chain -> - val response = try { - chain.proceed(chain.request()) - } catch (ioException: IOException) { - val networkException = mapNetworkException(ioException) - return@Interceptor networkErrorHandler.retryConnection(chain, networkException) { - client = it - } - } - - return@Interceptor when (response.code) { - RETRY_RESPONSE_CODE -> { - networkErrorHandler.retryConnection(chain.call(), response) { - logger.info("Reinitialized the xctest client after reestablishing connection") - client = it - } - } - else -> { - networkErrorHandler.resetRetryCount() - response - } - } - }) - - private fun OkHttpClient.Builder.addRetryAndShutdownInterceptor() = addNetworkInterceptor(Interceptor { - val request = it.request() - try { - it.proceed(request) - } catch (ioException: IOException) { - // Fake an Ok response when shutting down and receiving an error - // to prevent a stack trace in the cli when running maestro studio. - - if (isShuttingDown) { - val message = "Shutting down xctest server" - val responseBody = """ - { "message" : "$message" } - """.trimIndent().toResponseBody("application/json; charset=utf-8".toMediaType()) - - Response.Builder() - .request(it.request()) - .protocol(Protocol.HTTP_1_1) - .message(message) - .body(responseBody) - .code(200) - .build() - } else { - val networkException = mapNetworkException(ioException) - return@Interceptor networkErrorHandler.getRetrialResponse(networkException, request) - } - } - }) - - private fun mapNetworkException(e: IOException): NetworkException { - return when (e) { - is SocketTimeoutException -> NetworkException.TimeoutException("Socket timeout") - is ConnectException -> NetworkException.ConnectionException("Connection error") - is UnknownHostException -> NetworkException.UnknownHostException("Unknown host") - else -> { - logger.info("Exception $e is not mapped io exception") - NetworkException.UnknownNetworkException(e.message ?: e.stackTraceToString()) - } - } - } } diff --git a/maestro-ios-driver/src/main/kotlin/xcuitest/api/NetworkErrorHandler.kt b/maestro-ios-driver/src/main/kotlin/xcuitest/api/NetworkErrorHandler.kt deleted file mode 100644 index e96e9d8a93..0000000000 --- a/maestro-ios-driver/src/main/kotlin/xcuitest/api/NetworkErrorHandler.kt +++ /dev/null @@ -1,127 +0,0 @@ -package xcuitest.api - -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import okhttp3.* -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.ResponseBody.Companion.toResponseBody -import org.slf4j.LoggerFactory -import util.PrintUtils -import xcuitest.XCTestClient -import xcuitest.api.NetworkException.Companion.toUserNetworkException -import xcuitest.installer.XCTestInstaller - -class NetworkErrorHandler( - private val xcTestInstaller: XCTestInstaller, -) { - private val logger = LoggerFactory.getLogger(NetworkErrorHandler::class.java) - - private var retry = 0 - - companion object { - const val RETRY_RESPONSE_CODE = 503 - const val NO_RETRY_RESPONSE_CODE = 502 - private const val MAX_RETRY = 5 - private val mapper = jacksonObjectMapper() - } - - fun getRetrialResponse(networkException: NetworkException, request: Request): Response { - val userNetworkModel = networkException.toUserNetworkException() - val error = Error(errorMessage = userNetworkModel.userFriendlyMessage, errorCode = "network-error") - val json = mapper.writeValueAsString(error) - val responseBody = json.toResponseBody("application/json; charset=utf-8".toMediaType()) - logger.info("Got Network exception in network layer: $networkException") - return if (networkException.shouldRetryDriverInstallation()) { - logger.info("Retrying the installation of driver from network layer by returning fake response code $RETRY_RESPONSE_CODE") - Response.Builder() - .request(request) - .protocol(Protocol.HTTP_1_1) - .message(userNetworkModel.userFriendlyMessage) - .body(responseBody) - .code(RETRY_RESPONSE_CODE) - .build() - } else { - logger.info("Not retrying the installation of driver from network layer") - logger.info("Network exception $networkException and mapped user network exception $userNetworkModel") - Response.Builder() - .request(request) - .protocol(Protocol.HTTP_1_1) - .message(userNetworkModel.userFriendlyMessage) - .body(responseBody) - .code(NO_RETRY_RESPONSE_CODE) - .build() - } - } - - private fun NetworkException.shouldRetryDriverInstallation(): Boolean { - return when (this) { - is NetworkException.ConnectionException, - is NetworkException.TimeoutException, - is NetworkException.UnknownNetworkException -> true - is NetworkException.UnknownHostException -> false - } - } - - fun retryConnection( - call: Call, - response: Response, - reInitializeInstaller: (XCTestClient) -> Unit - ): Response { - return if (retry < MAX_RETRY) { - xcTestInstaller.start()?.let { - reInitializeInstaller(it) - } - response.close() - retry++ - logger.info("ℹ️ Retrying connection to the XCUITest server for ${retry}...") - PrintUtils.log("ℹ️ Retrying connection to the XCUITest server for ${retry}...") - call.clone().execute() - } else { - logger.error("⚠️ Error: ${response.message}") - PrintUtils.log("⚠️ Error: ${response.message}") - resetRetryCount() - return Response.Builder() - .request(call.request()) - .protocol(Protocol.HTTP_1_1) - .message(response.message) - .body(response.body) - .code(NO_RETRY_RESPONSE_CODE) - .build() - } - } - - fun retryConnection( - chain: Interceptor.Chain, - networkException: NetworkException, - reInitializeInstaller: (XCTestClient) -> Unit - ): Response { - logger.info("Got Network exception in application layer: $networkException") - return if (networkException.shouldRetryDriverInstallation() && retry < MAX_RETRY) { - xcTestInstaller.start()?.let { - reInitializeInstaller(it) - } - retry++ - logger.info("ℹ️ Retrying connection to the XCUITest server for ${retry}...") - PrintUtils.log("ℹ️ Retrying connection to the XCUITest server for ${retry}...") - chain.call().clone().execute() - } else { - val userNetworkException = networkException.toUserNetworkException() - val error = Error(errorMessage = userNetworkException.userFriendlyMessage, errorCode = "network-error") - val json = mapper.writeValueAsString(error) - val responseBody = json.toResponseBody("application/json; charset=utf-8".toMediaType()) - logger.error("⚠️ Error: ${userNetworkException.userFriendlyMessage}") - PrintUtils.log("⚠️ Error: ${userNetworkException.userFriendlyMessage}") - resetRetryCount() - return Response.Builder() - .request(chain.request()) - .protocol(Protocol.HTTP_1_1) - .message(userNetworkException.userFriendlyMessage) - .body(responseBody) - .code(NO_RETRY_RESPONSE_CODE) - .build() - } - } - - fun resetRetryCount() { - retry = 0 - } -} diff --git a/maestro-ios-driver/src/main/kotlin/xcuitest/api/OkHttpClientInstance.kt b/maestro-ios-driver/src/main/kotlin/xcuitest/api/OkHttpClientInstance.kt new file mode 100644 index 0000000000..203ae8486d --- /dev/null +++ b/maestro-ios-driver/src/main/kotlin/xcuitest/api/OkHttpClientInstance.kt @@ -0,0 +1,16 @@ +package xcuitest.api + +import okhttp3.ConnectionPool +import okhttp3.OkHttpClient +import java.util.concurrent.TimeUnit + +object OkHttpClientInstance { + + fun get(): OkHttpClient { + return OkHttpClient.Builder() + .connectionPool(ConnectionPool(225, 10, TimeUnit.MINUTES)) + .connectTimeout(1, TimeUnit.SECONDS) + .readTimeout(200, TimeUnit.SECONDS) + .build() + } +} \ No newline at end of file diff --git a/maestro-ios-driver/src/main/kotlin/xcuitest/installer/LocalXCTestInstaller.kt b/maestro-ios-driver/src/main/kotlin/xcuitest/installer/LocalXCTestInstaller.kt index cca58defb8..0d7296cd50 100644 --- a/maestro-ios-driver/src/main/kotlin/xcuitest/installer/LocalXCTestInstaller.kt +++ b/maestro-ios-driver/src/main/kotlin/xcuitest/installer/LocalXCTestInstaller.kt @@ -21,6 +21,10 @@ class LocalXCTestInstaller( private val host: String = "[::1]", private val enableXCTestOutputFileLogging: Boolean, private val defaultPort: Int, + private val okHttpClient: OkHttpClient = OkHttpClient.Builder() + .connectTimeout(1, TimeUnit.SECONDS) + .readTimeout(100, TimeUnit.SECONDS) + .build() ) : XCTestInstaller { private val logger = LoggerFactory.getLogger(LocalXCTestInstaller::class.java) @@ -45,6 +49,8 @@ class LocalXCTestInstaller( return false } + if (!isChannelAlive()) return false + fun killXCTestRunnerProcess() { logger.trace("Will attempt to stop all alive XCTest Runner processes before uninstalling") @@ -68,7 +74,6 @@ class LocalXCTestInstaller( killXCTestRunnerProcess() logger.trace("Uninstalling XCTest Runner from device $deviceId") - XCRunnerCLIUtils.uninstall(UI_TEST_RUNNER_APP_BUNDLE_ID, deviceId) return true } @@ -88,29 +93,31 @@ class LocalXCTestInstaller( throw IllegalStateException("XCTest was not started manually") } - uninstall() - repeat(3) { i -> - logger.info("[Start] Install XCUITest runner on $deviceId") - startXCTestRunner() - logger.info("[Done] Install XCUITest runner on $deviceId") - - logger.info("[Start] Ensure XCUITest runner is running on $deviceId") - if (ensureOpen()) { - logger.info("[Done] Ensure XCUITest runner is running on $deviceId") - return XCTestClient(host, defaultPort) - } else { - uninstall() - logger.info("[Failed] Ensure XCUITest runner is running on $deviceId") - logger.info("[Retry] Retrying setup() ${i}th time") + logger.info("[Start] Install XCUITest runner on $deviceId") + startXCTestRunner() + logger.info("[Done] Install XCUITest runner on $deviceId") + + val startTime = System.currentTimeMillis() + + while (System.currentTimeMillis() - startTime < getStartupTimeout()) { + runCatching { + if (isChannelAlive()) return XCTestClient(host, defaultPort) } + Thread.sleep(500) } - return null + + throw IOSDriverTimeoutException("iOS driver not ready in time, consider increasing timeout by configuring MAESTRO_DRIVER_STARTUP_TIMEOUT env variable") } + class IOSDriverTimeoutException(message: String): RuntimeException(message) + + private fun getStartupTimeout(): Long = runCatching { + System.getenv(MAESTRO_DRIVER_STARTUP_TIMEOUT).toLong() + }.getOrDefault(SERVER_LAUNCH_TIMEOUT_MS) + override fun isChannelAlive(): Boolean { - val appAlive = XCRunnerCLIUtils.isAppAlive(UI_TEST_RUNNER_APP_BUNDLE_ID, deviceId) - return appAlive && xcTestDriverStatusCheck() + return xcTestDriverStatusCheck() } private fun ensureOpen(): Boolean { @@ -133,18 +140,16 @@ class LocalXCTestInstaller( .port(defaultPort) } - val url = xctestAPIBuilder("status") - .build() - - val request = Request.Builder() - .get() - .url(url) - .build() - - val okHttpClient = OkHttpClient.Builder() - .connectTimeout(1, TimeUnit.SECONDS) - .readTimeout(100, TimeUnit.SECONDS) - .build() + val url by lazy { + xctestAPIBuilder("status") + .build() + } + val request by lazy { + Request.Builder() + .get() + .url(url) + .build() + } val checkSuccessful = try { okHttpClient.newCall(request).execute().use { @@ -160,10 +165,10 @@ class LocalXCTestInstaller( } private fun startXCTestRunner() { - val processOutput = ProcessBuilder(listOf("xcrun", "simctl", "spawn", deviceId, "launchctl", "list")) - .start() - .inputStream.source().buffer().readUtf8() - .trim() + if (isChannelAlive()) { + logger.info("UI Test runner already running, returning") + return + } logger.info("[Start] Writing xctest run file") val tempDir = File(tempDir).apply { mkdir() } @@ -171,20 +176,13 @@ class LocalXCTestInstaller( writeFileToDestination(XCTEST_RUN_PATH, xctestRunFile) logger.info("[Done] Writing xctest run file") - if (processOutput.contains(UI_TEST_RUNNER_APP_BUNDLE_ID)) { - logger.info("UI Test runner already running, stopping it") - uninstall() - } else { - logger.info("Not able to find ui test runner app running, going to install now") + logger.info("[Start] Writing maestro-driver-iosUITests-Runner app") + extractZipToApp("maestro-driver-iosUITests-Runner", UI_TEST_RUNNER_PATH) + logger.info("[Done] Writing maestro-driver-iosUITests-Runner app") - logger.info("[Start] Writing maestro-driver-iosUITests-Runner app") - extractZipToApp("maestro-driver-iosUITests-Runner", UI_TEST_RUNNER_PATH) - logger.info("[Done] Writing maestro-driver-iosUITests-Runner app") - - logger.info("[Start] Writing maestro-driver-ios app") - extractZipToApp("maestro-driver-ios", UI_TEST_HOST_PATH) - logger.info("[Done] Writing maestro-driver-ios app") - } + logger.info("[Start] Writing maestro-driver-ios app") + extractZipToApp("maestro-driver-ios", UI_TEST_HOST_PATH) + logger.info("[Done] Writing maestro-driver-ios app") logger.info("[Start] Running XcUITest with `xcodebuild test-without-building`") xcTestProcess = XCRunnerCLIUtils.runXcTestWithoutBuild( @@ -204,6 +202,7 @@ class LocalXCTestInstaller( logger.info("[Start] Cleaning up the ui test runner files") FileUtils.cleanDirectory(File(tempDir)) uninstall() + XCRunnerCLIUtils.uninstall(UI_TEST_RUNNER_APP_BUNDLE_ID, deviceId) logger.info("[Done] Cleaning up the ui test runner files") } @@ -229,6 +228,11 @@ class LocalXCTestInstaller( private const val UI_TEST_RUNNER_PATH = "/maestro-driver-iosUITests-Runner.zip" private const val XCTEST_RUN_PATH = "/maestro-driver-ios-config.xctestrun" private const val UI_TEST_HOST_PATH = "/maestro-driver-ios.zip" - private const val UI_TEST_RUNNER_APP_BUNDLE_ID = "dev.mobile.maestro-driver-iosUITests.xctrunner" + private const val UI_TEST_RUNNER_APP_BUNDLE_ID = + "dev.mobile.maestro-driver-iosUITests.xctrunner" + + private const val SERVER_LAUNCH_TIMEOUT_MS = 15000L + private const val MAESTRO_DRIVER_STARTUP_TIMEOUT = "MAESTRO_DRIVER_STARTUP_TIMEOUT" } + } diff --git a/maestro-ios/src/main/java/ios/xctest/XCTestIOSDevice.kt b/maestro-ios/src/main/java/ios/xctest/XCTestIOSDevice.kt index da2577e6d2..91b5506c28 100644 --- a/maestro-ios/src/main/java/ios/xctest/XCTestIOSDevice.kt +++ b/maestro-ios/src/main/java/ios/xctest/XCTestIOSDevice.kt @@ -13,6 +13,7 @@ import okio.buffer import org.slf4j.LoggerFactory import xcuitest.XCTestDriverClient import java.io.InputStream +import java.net.SocketTimeoutException import java.util.UUID class XCTestIOSDevice(