diff --git a/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt b/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt index cd00549..28e431a 100644 --- a/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt +++ b/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt @@ -759,166 +759,6 @@ internal class WdaRecovery( } } -class IosDriverBackend(private val udid: String) : DriverBackend { - private lateinit var driver: maestro.drivers.IOSDriver - private lateinit var localDevice: ios.LocalIOSDevice - private lateinit var installer: xcuitest.installer.LocalXCTestInstaller - private val recovery by lazy { - WdaRecovery( - isAlive = { runCatching { installer.isChannelAlive() }.getOrElse { false } }, - restart = { driver.open(); warmup() }, - ) - } - - init { - val reaped = reapOrphanIosRunners(udid) { command -> - runCatching { ProcessBuilder(command).start().waitFor() }.getOrDefault(1) - } - if (reaped) { - println("terminated orphaned XCTest runner session for $udid") - // Give the killed session a beat to tear down before installing ours. - Thread.sleep(1000) - } - val wdaPort = maestro.utils.SocketUtils.nextFreePort(22000, 23000) - val tempFileHandler = maestro.utils.TempFileHandler() - val simctlDevice = device.SimctlIOSDevice( - deviceId = udid, - tempFileHandler = tempFileHandler, - ) - val driverConfig = xcuitest.installer.LocalXCTestInstaller.IOSDriverConfig( - prebuiltRunner = true, - sourceDirectory = "driver-iPhoneSimulator", - context = xcuitest.installer.Context.CLI, - snapshotKeyHonorModalViews = null, - ) - installer = xcuitest.installer.LocalXCTestInstaller( - deviceId = udid, - host = "127.0.0.1", - deviceType = util.IOSDeviceType.SIMULATOR, - defaultPort = wdaPort, - iOSDriverConfig = driverConfig, - deviceController = simctlDevice, - tempFileHandler = tempFileHandler, - ) - val xcTestClient = xcuitest.XCTestClient("127.0.0.1", wdaPort) - val xcTestDriverClient = xcuitest.XCTestDriverClient( - installer = installer, - client = xcTestClient, - ) - val xcRunnerUtils = util.XCRunnerCLIUtils(tempFileHandler) - val xcTestDevice = ios.xctest.XCTestIOSDevice( - deviceId = udid, - client = xcTestDriverClient, - getInstalledApps = { xcRunnerUtils.listApps(udid) }, - ) - val device = ios.LocalIOSDevice( - deviceId = udid, - xcTestDevice = xcTestDevice, - deviceController = simctlDevice, - insights = maestro.utils.NoopInsights, - ) - localDevice = device - driver = maestro.drivers.IOSDriver(device, maestro.utils.NoopInsights) - driver.open() - warmup() - } - - private fun warmup() { - var warmupErr: Exception? = null - repeat(3) { attempt -> - try { - driver.contentDescriptor(false) - warmupErr = null - return@repeat - } catch (e: Exception) { - warmupErr = e - if (attempt < 2) Thread.sleep(500) - } - } - warmupErr?.let { throw IllegalStateException("WDA warmup failed after 3 attempts: $it") } - } - - private fun withReconnect(replay: Boolean = true, block: () -> T): T = - recovery.run(replay, block) - - override fun launch(bundleId: String, clearState: Boolean, env: Map) = withReconnect { - runCatching { driver.stopApp(bundleId) } - if (clearState) driver.clearAppState(bundleId) - driver.launchApp(bundleId, env) - } - - override fun terminate(bundleId: String) = withReconnect { driver.stopApp(bundleId) } - - override fun tap(x: Int, y: Int) = withReconnect(replay = false) { driver.tap(maestro.Point(x, y)) } - - // The second tap request is already queued at the XCTest runner while the - // first executes, so the on-device gap collapses to the runner's - // turnaround instead of a full transport round trip. Sequential requests - // leave a gap wide enough for the app to navigate between the taps. - override fun doubleTap(x: Int, y: Int): Unit = withReconnect(replay = false) { - overlappedDoubleTap { driver.tap(maestro.Point(x, y)) } - } - - override fun longPress(x: Int, y: Int) = withReconnect(replay = false) { driver.longPress(maestro.Point(x, y)) } - - override fun tapSelector(selector: String) = withReconnect(replay = false) { - val root = driver.contentDescriptor(false) - val bounds = findBoundsBySelector(root, selector) ?: return@withReconnect - driver.tap(maestro.Point((bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2)) - } - - override fun inputText(text: String) = withReconnect(replay = false) { driver.inputText(text) } - - override fun eraseText(characterCount: Int) = withReconnect(replay = false) { driver.eraseText(characterCount) } - - override fun swipe(fromX: Int, fromY: Int, toX: Int, toY: Int, durationMillis: Long) = withReconnect(replay = false) { - driver.swipe(maestro.Point(fromX, fromY), maestro.Point(toX, toY), maxOf(durationMillis, 250L)) - } - - override fun pressKey(key: String) = withReconnect(replay = false) { - StubDriverBackend.KEY_MAP[key]?.let { keyCode -> - keyCodeToMaestro(keyCode)?.let { driver.pressKey(it) } - } - Unit - } - - override fun screenshot(): Triple = withReconnect { - val buf = okio.Buffer() - driver.takeScreenshot(buf, false) - val bytes = buf.readByteArray() - Triple(bytes, pngWidth(bytes), pngHeight(bytes)) - } - - override fun hierarchy(): String = withReconnect { - com.fasterxml.jackson.module.kotlin.jacksonObjectMapper() - .writeValueAsString(iosAxElementToTreeNode(localDevice.viewHierarchy(false).axElement)) - } - - override fun recentLogs(sinceUnixMillis: Long, minLevel: String): List = emptyList() - - override fun waitForIdle(durationMillis: Long) = withReconnect { - driver.waitForAppToSettle(null, null, durationMillis.toInt()) - pollUntilStable(STABILITY_POLL_CAP_MILLIS) { - stabilitySnapshot( - com.fasterxml.jackson.module.kotlin.jacksonObjectMapper() - .writeValueAsString(driver.contentDescriptor(false)), - ) - } - Unit - } - - override fun healthy() = runCatching { driver.contentDescriptor(false); true }.getOrElse { false } - - override fun metrics(bundleId: String) = MetricsSample(0.0, 0L, 0L) - - // close stops the XCTest runner session (kills the xcodebuild process and - // uninstalls the runner app). Skipping this leaves an orphaned session - // that xcodebuild later restarts, killing the next run's session. - override fun close() { - runCatching { driver.close() } - } -} - private fun keyCodeToMaestro(adbKeyCode: String): maestro.KeyCode? { return when (adbKeyCode) { "KEYCODE_BACK" -> maestro.KeyCode.BACK