Skip to content

Commit

Permalink
[MA 2992] Prebuilt iOS driver without xcodebuild (#2275)
Browse files Browse the repository at this point in the history
  • Loading branch information
amanjeetsingh150 authored Jan 30, 2025
1 parent bb447b1 commit d702a0c
Show file tree
Hide file tree
Showing 10 changed files with 83 additions and 56 deletions.
2 changes: 1 addition & 1 deletion maestro-client/src/main/java/maestro/drivers/IOSDriver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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!! })

Expand Down
11 changes: 4 additions & 7 deletions maestro-client/src/test/java/maestro/ios/MockXCTestInstaller.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions maestro-ios-driver/src/main/kotlin/util/CommandLineUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ object CommandLineUtils {
} else {
ProcessBuilder(*parts.toTypedArray())
.redirectOutput(nullFile)
.redirectError(nullFile)
.redirectError(ProcessBuilder.Redirect.PIPE)
}

processBuilder.environment().putAll(params)
Expand All @@ -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)
}
Expand Down
44 changes: 34 additions & 10 deletions maestro-ios-driver/src/main/kotlin/util/LocalSimulatorUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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<String>, excludeKeyboardElements: Boolean): ViewHierarchy {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))
Expand Down Expand Up @@ -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()")

Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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() {
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Binary file modified maestro-ios-driver/src/main/resources/maestro-driver-ios.zip
Binary file not shown.
Binary file not shown.
23 changes: 9 additions & 14 deletions maestro-ios-xctest-runner/build-maestro-ios-runner.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit d702a0c

Please sign in to comment.