[K/N][test] Improve simulator executor

Makes executor be able to find and download necessary runtimes and
create device with appropriate runtime available. Replaces device names
with identifiers


Co-authored-by: Alexander Shabalin <Alexander.Shabalin@jetbrains.com>

Merge-request: KT-MR-7919
Merged-by: Pavel Punegov <Pavel.Punegov@jetbrains.com>
This commit is contained in:
Pavel Punegov
2022-12-08 11:11:00 +00:00
committed by Space Team
parent c8e9cfde0b
commit 7ee877f9aa
3 changed files with 169 additions and 30 deletions
@@ -99,8 +99,9 @@ fun create(project: Project): ExecutorService {
configurables is WasmConfigurables -> WasmExecutor(configurables).service(project)
configurables is ConfigurablesWithEmulator && testTarget != HostManager.host -> EmulatorExecutor(configurables).service(project)
configurables is AppleConfigurables && configurables.targetTriple.isSimulator -> XcodeSimulatorExecutor(configurables).apply {
// Property can specify device identifier to be run on. For example, `com.apple.CoreSimulator.SimDeviceType.iPhone-11`
project.findProperty("iosDevice")?.toString()?.let {
deviceName = it
deviceId = it
}
}.service(project)
supportsRunningTestsOnDevice(testTarget) -> deviceLauncher(project)
@@ -20,26 +20,57 @@ private fun compareStringsAsVersions(version1: String, version2: String): Int {
return splitVersion1.size.compareTo(splitVersion2.size)
}
/**
* Returns parsed output of `xcrun simctl list runtimes -j`.
*/
private fun Xcode.getSimulatorRuntimeDescriptors(): List<SimulatorRuntimeDescriptor> = gson.fromJson(simulatorRuntimes, ListRuntimesReport::class.java).runtimes
/**
* Returns first available simulator runtime for [target] with at least [osMinVersion] OS version.
* */
fun Xcode.getLatestSimulatorRuntimeFor(family: Family, osMinVersion: String): SimulatorRuntimeDescriptor? {
val osName = when (family) {
internal fun simulatorOsName(family: Family): String {
return when (family) {
Family.IOS -> "iOS"
Family.WATCHOS -> "watchOS"
Family.TVOS -> "tvOS"
else -> error("Unexpected simulator OS: $family")
}
return getSimulatorRuntimeDescriptors().firstOrNull {
}
/**
* Returns parsed output of `xcrun simctl list runtimes -j`.
*/
private fun Xcode.getSimulatorRuntimeDescriptors(): List<SimulatorRuntimeDescriptor> =
gson.fromJson(simulatorRuntimes, ListRuntimesReport::class.java).runtimes
private fun getSimulatorRuntimeDescriptors(json: String): List<SimulatorRuntimeDescriptor> =
gson.fromJson(json, ListRuntimesReport::class.java).runtimes
private fun getLatestSimulatorRuntimeFor(
descriptors: List<SimulatorRuntimeDescriptor>,
family: Family,
osMinVersion: String
): SimulatorRuntimeDescriptor? {
val osName = simulatorOsName(family)
return descriptors.firstOrNull {
it.checkAvailability() && it.name.startsWith(osName) && compareStringsAsVersions(it.version, osMinVersion) >= 0
}
}
private fun getSimulatorRuntimesFor(
descriptors: List<SimulatorRuntimeDescriptor>,
family: Family,
osMinVersion: String
): List<SimulatorRuntimeDescriptor> {
val osName = simulatorOsName(family)
return descriptors.filter {
it.checkAvailability() && it.name.startsWith(osName) && compareStringsAsVersions(it.version, osMinVersion) >= 0
}
}
/**
* Returns first available simulator runtime for [target] with at least [osMinVersion] OS version.
* */
fun Xcode.getLatestSimulatorRuntimeFor(family: Family, osMinVersion: String): SimulatorRuntimeDescriptor? =
getLatestSimulatorRuntimeFor(getSimulatorRuntimeDescriptors(), family, osMinVersion)
fun getLatestSimulatorRuntimeFor(json: String, family: Family, osMinVersion: String): SimulatorRuntimeDescriptor? =
getLatestSimulatorRuntimeFor(getSimulatorRuntimeDescriptors(json), family, osMinVersion)
fun getSimulatorRuntimesFor(json: String, family: Family, osMinVersion: String): List<SimulatorRuntimeDescriptor> =
getSimulatorRuntimesFor(getSimulatorRuntimeDescriptors(json), family, osMinVersion)
// Result of `xcrun simctl list runtimes -j`.
data class ListRuntimesReport(
@Expose val runtimes: List<SimulatorRuntimeDescriptor>
@@ -53,7 +84,8 @@ data class SimulatorRuntimeDescriptor(
@Expose val availability: String? = null,
@Expose val name: String,
@Expose val identifier: String,
@Expose val buildversion: String
@Expose val buildversion: String,
@Expose val supportedDeviceTypes: List<DeviceType>
) {
/**
* Different Xcode/macOS combinations give different fields that checks
@@ -65,3 +97,33 @@ data class SimulatorRuntimeDescriptor(
return false
}
}
data class DeviceType(
@Expose val bundlePath: String,
@Expose val name: String,
@Expose val identifier: String,
@Expose val productFamily: String
)
/**
* Returns map of simulator devices from the json input
*/
fun getSimulatorDevices(json: String): Map<String, List<SimulatorDeviceDescriptor>> =
gson.fromJson(json, ListDevicesReport::class.java).devices
// Result of `xcrun simctl list devices -j`
data class ListDevicesReport(
@Expose val devices: Map<String, List<SimulatorDeviceDescriptor>>
)
data class SimulatorDeviceDescriptor(
@Expose val lastBootedAt: String?,
@Expose val dataPath: String,
@Expose val dataPathSize: Long,
@Expose val logPath: String,
@Expose val udid: String,
@Expose val isAvailable: Boolean?,
@Expose val deviceTypeIdentifier: String,
@Expose val state: String,
@Expose val name: String
)
@@ -6,13 +6,15 @@
package org.jetbrains.kotlin.executors
import org.jetbrains.kotlin.konan.target.*
import org.jetbrains.kotlin.*
import java.io.ByteArrayOutputStream
import java.io.File
import java.util.logging.Logger
private fun defaultDeviceName(target: KonanTarget) = when (target.family) {
Family.TVOS -> "Apple TV 4K"
Family.IOS -> "iPhone 11"
Family.WATCHOS -> "Apple Watch Series 6 " + (if (Xcode.findCurrent().version.startsWith("14")) "(40mm)" else "- 40mm")
private fun defaultDeviceId(target: KonanTarget) = when (target.family) {
Family.TVOS -> "com.apple.CoreSimulator.SimDeviceType.Apple-TV-4K-4K"
Family.IOS -> "com.apple.CoreSimulator.SimDeviceType.iPhone-14"
Family.WATCHOS -> "com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Series-6-40mm"
else -> error("Unexpected simulation target: $target")
}
@@ -33,10 +35,12 @@ private fun Executor.run(executableAbsolutePath: String, vararg args: String) =
*/
class XcodeSimulatorExecutor(
private val configurables: AppleConfigurables,
var deviceName: String = defaultDeviceName(configurables.target),
var deviceId: String = defaultDeviceId(configurables.target),
) : Executor {
private val hostExecutor: Executor = HostExecutor()
private val logger = Logger.getLogger(this::class.java.name)
private val target by configurables::target
init {
@@ -62,20 +66,91 @@ class XcodeSimulatorExecutor(
else -> error("${target.architecture} can't be used in simulator.")
}.toTypedArray()
private var _deviceNameChecked: String? = null
private fun simctl(vararg args: String): String {
val out = hostExecutor.run("/usr/bin/xcrun", *arrayOf("simctl", *args))
return out.toString("UTF-8").trim()
}
private var deviceChecked: SimulatorDeviceDescriptor? = null
private fun ensureSimulatorExists() {
// Already ensured that simulator for `deviceName` exists.
if (deviceName == _deviceNameChecked)
// Already ensured that simulator for `deviceId` exists.
if (deviceId == deviceChecked?.deviceTypeIdentifier) {
logger.info("Device already exists: ${deviceChecked?.deviceTypeIdentifier} with name ${deviceChecked?.name}")
return
// Find out if the default device is available
val out = hostExecutor.run("/usr/bin/xcrun", "simctl", "list", "devices", "available")
val deviceNameExists = out.toString("UTF-8").trim().contains(deviceName)
// Create if it's not available
if (!deviceNameExists) {
hostExecutor.run("/usr/bin/xcrun", "simctl", "create", deviceName, deviceName)
}
// If successfully created, remember that.
_deviceNameChecked = deviceName
logger.info("Device was not cheked before. Find the device with id: $deviceId")
val simulatorRuntime = getSimulatorRuntime()
logger.info("Runtime used for the $deviceId is $simulatorRuntime")
val device = getDeviceFor(simulatorRuntime)
logger.info("Found device: $device")
// If successfully found, remember that.
deviceChecked = device
}
private fun getDeviceFor(simulatorRuntime: SimulatorRuntimeDescriptor): SimulatorDeviceDescriptor {
val runtimeIdentifier = simulatorRuntime.identifier
val deviceOrNull = {
getSimulatorDevices(simctl("list", "devices", "--json"))[runtimeIdentifier]
?.find { it.deviceTypeIdentifier == deviceId && it.isAvailable == true }
}
// If the device already exists, nothing to do.
deviceOrNull()?.let { return it }
// Create the device
logger.info("Current runtime doesn't have $deviceId available")
val deviceType = simulatorRuntime.supportedDeviceTypes.find { it.identifier == deviceId }
checkNotNull(deviceType) {
"""
Default device $deviceId is not available for the runtime ${simulatorRuntime.name}
Supported devices: ${simulatorRuntime.supportedDeviceTypes.map { it.identifier }.joinToString(", ")}
""".trimIndent()
}
val out = simctl("create", deviceType.name, deviceType.identifier, runtimeIdentifier)
logger.info("Create device $deviceId: simctl output is: $out")
return checkNotNull(deviceOrNull()) { "Unable to get or create simulator device $deviceId for $target with runtime ${simulatorRuntime.name}" }
}
private fun getSimulatorRuntime(): SimulatorRuntimeDescriptor {
val simulatorRuntimeOrNull = {
getSimulatorRuntimesFor(
json = simctl("list", "runtimes", "--json"),
family = target.family,
osMinVersion = configurables.osVersionMin
).firstOrNull {
it.supportedDeviceTypes.any { deviceType -> deviceType.identifier == deviceId }
}
}
// If the simulator runtime already exists, nothing to do.
simulatorRuntimeOrNull()?.let { return it }
// Download this platform if not available
logger.info("Runtime for the $deviceId is not available. Downloading runtimes for $target with Xcode")
downloadRuntimeFor(simulatorOsName(target.family))
return checkNotNull(simulatorRuntimeOrNull()) { "Runtime is not available for the selected $deviceId. Check Xcode installation" }
}
private fun downloadRuntimeFor(osName: String) {
val version = Xcode.findCurrent().version
val versionSplit = version.split(".")
check(versionSplit.size >= 2) {
"Unrecognised version of Xcode $version was split to $versionSplit"
}
val major = versionSplit[0].toInt()
val minor = versionSplit[1].toInt()
check(major >= 14) {
"Was unable to get the required runtimes running on Xcode $version. Check the Xcode installation"
}
if (minor >= 1) {
// Option -downloadPlatform NAME available only since 14.1
hostExecutor.run("/usr/bin/xcrun", "xcodebuild", "-downloadPlatform", osName)
} else {
// Have to download all platforms :(
hostExecutor.run("/usr/bin/xcrun", "xcodebuild", "-downloadAllPlatforms")
}
}
override fun execute(request: ExecuteRequest): ExecuteResponse {
@@ -85,11 +160,12 @@ class XcodeSimulatorExecutor(
"SIMCTL_CHILD_" + it.key
}
val workingDirectory = request.workingDirectory ?: File(request.executableAbsolutePath).parentFile
val name = deviceChecked?.name ?: error("No device available for $deviceId")
// Starting Xcode 11 `simctl spawn` requires explicit `--standalone` flag.
return hostExecutor.execute(request.copying {
this.executableAbsolutePath = "/usr/bin/xcrun"
this.workingDirectory = workingDirectory
this.args.addAll(0, listOf("simctl", "spawn", "--standalone", *archSpecification, deviceName, executable))
this.args.addAll(0, listOf("simctl", "spawn", "--standalone", *archSpecification, name, executable))
this.environment.clear()
this.environment.putAll(env)
})