diff --git a/maestro-client/src/main/java/maestro/drivers/IOSDriver.kt b/maestro-client/src/main/java/maestro/drivers/IOSDriver.kt index 672a2c9cdf..377fa11725 100644 --- a/maestro-client/src/main/java/maestro/drivers/IOSDriver.kt +++ b/maestro-client/src/main/java/maestro/drivers/IOSDriver.kt @@ -42,7 +42,7 @@ class IOSDriver( private val iosDevice: IOSDevice, private val insights: Insights = NoopInsights, private val metricsProvider: Metrics = MetricsProvider.getInstance(), - ) : Driver { + ) : Driver { private val metrics = metricsProvider.withPrefix("maestro.driver").withTags(mapOf("platform" to "ios", "deviceId" to iosDevice.deviceId).filterValues { it != null }.mapValues { it.value!! }) diff --git a/maestro-client/src/test/java/maestro/ios/MockXCTestInstaller.kt b/maestro-client/src/test/java/maestro/ios/MockXCTestInstaller.kt index 80c3a8c21f..e46354cb0e 100644 --- a/maestro-client/src/test/java/maestro/ios/MockXCTestInstaller.kt +++ b/maestro-client/src/test/java/maestro/ios/MockXCTestInstaller.kt @@ -6,21 +6,18 @@ import xcuitest.installer.XCTestInstaller class MockXCTestInstaller( private val simulator: Simulator, + override val preBuiltRunner: Boolean = false, ) : XCTestInstaller { private var attempts = 0 - override fun start(): XCTestClient? { + override fun start(): XCTestClient { attempts++ for (i in 0..simulator.installationRetryCount) { assertThat(simulator.runningApps()).doesNotContain("dev.mobile.maestro-driver-iosUITests.xctrunner") } - return if (simulator.shouldInstall) { - simulator.installXCTestDriver() - XCTestClient("localhost", 22807) - } else { - null - } + simulator.installXCTestDriver() + return XCTestClient("localhost", 22807) } override fun uninstall(): Boolean { diff --git a/maestro-ios-driver/src/main/kotlin/util/CommandLineUtils.kt b/maestro-ios-driver/src/main/kotlin/util/CommandLineUtils.kt index aa1880f929..fd5493adcd 100644 --- a/maestro-ios-driver/src/main/kotlin/util/CommandLineUtils.kt +++ b/maestro-ios-driver/src/main/kotlin/util/CommandLineUtils.kt @@ -24,7 +24,7 @@ object CommandLineUtils { } else { ProcessBuilder(*parts.toTypedArray()) .redirectOutput(nullFile) - .redirectError(nullFile) + .redirectError(ProcessBuilder.Redirect.PIPE) } processBuilder.environment().putAll(params) @@ -42,7 +42,7 @@ object CommandLineUtils { .readUtf8() logger.error("Process failed with exit code ${process.exitValue()}") - logger.error(processOutput) + logger.error("Error output $processOutput") throw IllegalStateException(processOutput) } diff --git a/maestro-ios-driver/src/main/kotlin/util/LocalSimulatorUtils.kt b/maestro-ios-driver/src/main/kotlin/util/LocalSimulatorUtils.kt index 4607db6456..7eb59748d0 100644 --- a/maestro-ios-driver/src/main/kotlin/util/LocalSimulatorUtils.kt +++ b/maestro-ios-driver/src/main/kotlin/util/LocalSimulatorUtils.kt @@ -164,17 +164,24 @@ object LocalSimulatorUtils { fun terminate(deviceId: String, bundleId: String) { // Ignore error return: terminate will fail if the app is not running - ProcessBuilder( - listOf( - "xcrun", - "simctl", - "terminate", - deviceId, - bundleId + logger.info("[Start] Terminating app $bundleId") + runCatching { + runCommand( + listOf( + "xcrun", + "simctl", + "terminate", + deviceId, + bundleId + ) ) - ) - .start() - .waitFor() + }.onFailure { + if (it.message?.contains("found nothing to terminate") == false) { + logger.info("The bundle $bundleId is already terminated") + throw it + } + } + logger.info("[Done] Terminating app $bundleId") } private fun isAppRunning(deviceId: String, bundleId: String): Boolean { @@ -329,6 +336,23 @@ object LocalSimulatorUtils { ) } + fun launchUITestRunner( + deviceId: String, + port: Int, + ) { + runCommand( + listOf( + "xcrun", + "simctl", + "launch", + "--terminate-running-process", + deviceId, + "dev.mobile.maestro-driver-iosUITests.xctrunner" + ), + params = mapOf("SIMCTL_CHILD_PORT" to port.toString()) + ) + } + fun setLocation(deviceId: String, latitude: Double, longitude: Double) { runCommand( listOf( diff --git a/maestro-ios-driver/src/main/kotlin/xcuitest/XCTestDriverClient.kt b/maestro-ios-driver/src/main/kotlin/xcuitest/XCTestDriverClient.kt index 0be37a3eb5..c4bb2cd6d7 100644 --- a/maestro-ios-driver/src/main/kotlin/xcuitest/XCTestDriverClient.kt +++ b/maestro-ios-driver/src/main/kotlin/xcuitest/XCTestDriverClient.kt @@ -10,7 +10,6 @@ import okhttp3.RequestBody.Companion.toRequestBody import org.slf4j.LoggerFactory import xcuitest.api.* import xcuitest.installer.XCTestInstaller -import java.io.IOException import kotlin.time.Duration.Companion.seconds class XCTestDriverClient( @@ -42,11 +41,9 @@ class XCTestDriverClient( installer.uninstall() logger.trace("XCTest Runner uninstalled, will install and start it") - client = installer.start() ?: throw XCTestDriverUnreachable("Failed to start XCTest Driver") + client = installer.start() } - class XCTestDriverUnreachable(message: String) : IOException(message) - private val mapper = jacksonObjectMapper() fun viewHierarchy(installedApps: Set, excludeKeyboardElements: Boolean): ViewHierarchy { 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 8af21430dc..3e2163c449 100644 --- a/maestro-ios-driver/src/main/kotlin/xcuitest/installer/LocalXCTestInstaller.kt +++ b/maestro-ios-driver/src/main/kotlin/xcuitest/installer/LocalXCTestInstaller.kt @@ -13,6 +13,7 @@ import okio.source import org.apache.commons.io.FileUtils import org.rauschig.jarchivelib.ArchiverFactory import org.slf4j.LoggerFactory +import util.LocalSimulatorUtils import util.XCRunnerCLIUtils import xcuitest.XCTestClient import java.io.File @@ -30,7 +31,8 @@ class LocalXCTestInstaller( connectTimeout = 1.seconds, readTimeout = 100.seconds, ), - ) : XCTestInstaller { + override val preBuiltRunner: Boolean = false, +) : XCTestInstaller { private val logger = LoggerFactory.getLogger(LocalXCTestInstaller::class.java) private val metrics = metricsProvider.withPrefix("xcuitest.installer").withTags(mapOf("kind" to "local", "deviceId" to deviceId, "host" to host)) @@ -85,7 +87,7 @@ class LocalXCTestInstaller( } } - override fun start(): XCTestClient? { + override fun start(): XCTestClient { return metrics.measured("operation", mapOf("command" to "start")) { logger.info("start()") @@ -104,7 +106,7 @@ class LocalXCTestInstaller( logger.info("[Start] Install XCUITest runner on $deviceId") - startXCTestRunner() + startXCTestRunner(deviceId, preBuiltRunner) logger.info("[Done] Install XCUITest runner on $deviceId") val startTime = System.currentTimeMillis() @@ -176,7 +178,7 @@ class LocalXCTestInstaller( return checkSuccessful } - private fun startXCTestRunner() { + private fun startXCTestRunner(deviceId: String, preBuiltRunner: Boolean) { if (isChannelAlive()) { logger.info("UI Test runner already running, returning") return @@ -189,21 +191,28 @@ class LocalXCTestInstaller( logger.info("[Done] Writing xctest run file") logger.info("[Start] Writing maestro-driver-iosUITests-Runner app") - extractZipToApp("maestro-driver-iosUITests-Runner", UI_TEST_RUNNER_PATH) + val bundlePath = 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] Running XcUITest with `xcodebuild test-without-building`") - xcTestProcess = XCRunnerCLIUtils.runXcTestWithoutBuild( - deviceId = deviceId, - xcTestRunFilePath = xctestRunFile.absolutePath, - port = defaultPort, - enableXCTestOutputFileLogging = enableXCTestOutputFileLogging, - ) - logger.info("[Done] Running XcUITest with `xcodebuild test-without-building`") + if (preBuiltRunner) { + logger.info("Installing pre built driver without xcodebuild") + LocalSimulatorUtils.install(deviceId, bundlePath.toPath()) + LocalSimulatorUtils.launchUITestRunner(deviceId, defaultPort) + } else { + logger.info("Installing driver with xcodebuild") + logger.info("[Start] Running XcUITest with `xcodebuild test-without-building`") + xcTestProcess = XCRunnerCLIUtils.runXcTestWithoutBuild( + deviceId = this.deviceId, + xcTestRunFilePath = xctestRunFile.absolutePath, + port = defaultPort, + enableXCTestOutputFileLogging = enableXCTestOutputFileLogging, + ) + logger.info("[Done] Running XcUITest with `xcodebuild test-without-building`") + } } override fun close() { @@ -214,18 +223,21 @@ 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) + LocalSimulatorUtils.terminate(deviceId = deviceId, bundleId = UI_TEST_RUNNER_APP_BUNDLE_ID) + XCRunnerCLIUtils.uninstall(bundleId = UI_TEST_RUNNER_APP_BUNDLE_ID, deviceId = deviceId) logger.info("[Done] Cleaning up the ui test runner files") } - private fun extractZipToApp(appFileName: String, srcAppPath: String) { - val appFile = File("$tempDir/Debug-iphonesimulator").apply { mkdir() } + private fun extractZipToApp(appFileName: String, srcAppPath: String): File { + val bundlePath = File("$tempDir/Debug-iphonesimulator").apply { mkdir() } val appZip = File("$tempDir/$appFileName.zip") writeFileToDestination(srcAppPath, appZip) ArchiverFactory.createArchiver(appZip).apply { - extract(appZip, appFile) + extract(appZip, bundlePath) } + + return File(bundlePath.path + "/$appFileName.app") } private fun writeFileToDestination(srcPath: String, destFile: File) { diff --git a/maestro-ios-driver/src/main/kotlin/xcuitest/installer/XCTestInstaller.kt b/maestro-ios-driver/src/main/kotlin/xcuitest/installer/XCTestInstaller.kt index 583bf68503..eeaae97c18 100644 --- a/maestro-ios-driver/src/main/kotlin/xcuitest/installer/XCTestInstaller.kt +++ b/maestro-ios-driver/src/main/kotlin/xcuitest/installer/XCTestInstaller.kt @@ -3,7 +3,9 @@ package xcuitest.installer import xcuitest.XCTestClient interface XCTestInstaller: AutoCloseable { - fun start(): XCTestClient? + val preBuiltRunner: Boolean + + fun start(): XCTestClient /** * Attempts to uninstall the XCTest Runner. diff --git a/maestro-ios-driver/src/main/resources/maestro-driver-ios.zip b/maestro-ios-driver/src/main/resources/maestro-driver-ios.zip index 0ddedc6882..f68c8f18e5 100644 Binary files a/maestro-ios-driver/src/main/resources/maestro-driver-ios.zip and b/maestro-ios-driver/src/main/resources/maestro-driver-ios.zip differ diff --git a/maestro-ios-driver/src/main/resources/maestro-driver-iosUITests-Runner.zip b/maestro-ios-driver/src/main/resources/maestro-driver-iosUITests-Runner.zip index 3bf25f634c..bf87fc888c 100644 Binary files a/maestro-ios-driver/src/main/resources/maestro-driver-iosUITests-Runner.zip and b/maestro-ios-driver/src/main/resources/maestro-driver-iosUITests-Runner.zip differ diff --git a/maestro-ios-xctest-runner/build-maestro-ios-runner.sh b/maestro-ios-xctest-runner/build-maestro-ios-runner.sh index 4ef34f5617..b003c9f296 100755 --- a/maestro-ios-xctest-runner/build-maestro-ios-runner.sh +++ b/maestro-ios-xctest-runner/build-maestro-ios-runner.sh @@ -8,29 +8,24 @@ fi rm -rf ./build/Products -xcodebuild \ - ARCHS="x86_64 arm64" \ - ONLY_ACTIVE_ARCH=NO \ - -project ./maestro-ios-xctest-runner/maestro-driver-ios.xcodeproj \ - -scheme maestro-driver-ios \ - -sdk iphonesimulator \ - -destination "generic/platform=iOS Simulator" \ - -IDEBuildLocationStyle=Custom \ - -IDECustomBuildLocationType=Absolute \ - -IDECustomBuildProductsPath="$PWD/build/Products" \ - build-for-testing +xcodebuild clean build-for-testing \ + -project ./maestro-ios-xctest-runner/maestro-driver-ios.xcodeproj \ + -derivedDataPath "$PWD/build/Products" \ + -scheme maestro-driver-ios \ + -destination "generic/platform=iOS Simulator" \ + CODE_SIGNING_ALLOWED=NO ARCHS="x86_64 arm64" COMPILER_INDEX_STORE_ENABLE=NO ## Remove intermediates, output and copy runner in maestro-ios-driver cp -r \ - ./build/Products/Debug-iphonesimulator/maestro-driver-iosUITests-Runner.app \ + ./build/Products/Build/Products/Debug-iphonesimulator/maestro-driver-iosUITests-Runner.app \ ./maestro-ios-driver/src/main/resources/maestro-driver-iosUITests-Runner.app cp -r \ - ./build/Products/Debug-iphonesimulator/maestro-driver-ios.app \ + ./build/Products/Build/Products/Debug-iphonesimulator/maestro-driver-ios.app \ ./maestro-ios-driver/src/main/resources/maestro-driver-ios.app cp \ - ./build/Products/*.xctestrun \ + ./build/Products/Build/Products/*.xctestrun \ ./maestro-ios-driver/src/main/resources/maestro-driver-ios-config.xctestrun (cd ./maestro-ios-driver/src/main/resources && zip -r maestro-driver-iosUITests-Runner.zip ./maestro-driver-iosUITests-Runner.app)