[MPP] Add intel macOS support and create simulators for the test run

^KT-61439
This commit is contained in:
Timofey Solonin
2023-08-24 12:11:18 +02:00
committed by Space Team
parent 9d00dfd76e
commit f20cc10660
5 changed files with 81 additions and 40 deletions
@@ -3,6 +3,7 @@ import org.jetbrains.kotlin.pill.PillExtension
plugins { plugins {
kotlin("jvm") kotlin("jvm")
kotlin("plugin.serialization")
id("jps-compatible") id("jps-compatible")
} }
@@ -85,6 +86,7 @@ dependencies {
testImplementation(kotlinStdlib("jdk8")) testImplementation(kotlinStdlib("jdk8"))
testImplementation(project(":kotlin-parcelize-compiler")) testImplementation(project(":kotlin-parcelize-compiler"))
testImplementation(commonDependency("org.jetbrains.intellij.deps", "trove4j")) testImplementation(commonDependency("org.jetbrains.intellij.deps", "trove4j"))
testImplementation(commonDependency("org.jetbrains.kotlinx", "kotlinx-serialization-json"))
testImplementation(libs.ktor.client.cio) testImplementation(libs.ktor.client.cio)
testImplementation(libs.ktor.client.mock) testImplementation(libs.ktor.client.mock)
testImplementation(libs.ktor.server.core) testImplementation(libs.ktor.server.core)
@@ -5,36 +5,34 @@
package org.jetbrains.kotlin.gradle.native package org.jetbrains.kotlin.gradle.native
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import org.gradle.util.GradleVersion import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.testbase.* import org.jetbrains.kotlin.gradle.testbase.*
import org.jetbrains.kotlin.gradle.util.assertProcessRunResult
import org.jetbrains.kotlin.gradle.util.runProcess import org.jetbrains.kotlin.gradle.util.runProcess
import org.jetbrains.kotlin.gradle.utils.XcodeUtils import org.jetbrains.kotlin.konan.target.HostManager
import org.jetbrains.kotlin.konan.target.KonanTarget import org.jetbrains.kotlin.konan.target.KonanTarget
import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.condition.OS import org.junit.jupiter.api.condition.OS
import java.io.File import java.io.File
import kotlin.test.assertEquals import java.util.UUID
@DisplayName("tests for the K/N XCode simulator test infrastructure") @DisplayName("tests for the K/N XCode simulator test infrastructure")
@NativeGradlePluginTests @NativeGradlePluginTests
@OsCondition(supportedOn = [OS.MAC], enabledOnCI = [OS.MAC]) @OsCondition(supportedOn = [OS.MAC], enabledOnCI = [OS.MAC])
class NativeXcodeSimulatorTestsIT : KGPBaseTest() { class NativeXcodeSimulatorTestsIT : KGPBaseTest() {
private val defaultIosSimulator by lazy {
XcodeUtils.getDefaultTestDeviceId(KonanTarget.IOS_SIMULATOR_ARM64) ?: error("No simulator found for iOS ARM64")
}
@DisplayName("A user-friendly error message is produced when the standalone mode is disabled and no simulator has booted") @DisplayName("A user-friendly error message is produced when the standalone mode is disabled and no simulator has booted")
@GradleTest @GradleTest
fun checkNoSimulatorErrorMessage(gradleVersion: GradleVersion) { fun checkNoSimulatorErrorMessage(gradleVersion: GradleVersion) {
shutDownSimulators() // based on the tests order there no simulator should be booted, but anyway to be sure val unbootedSimulator = createSimulator()
// Note the test still may fail if you have booted the required simulator manually before running the test
// We don't shut down such simulators in the tests
project("native-test-ios-https-request", gradleVersion) { project("native-test-ios-https-request", gradleVersion) {
buildGradleKts.append( buildGradleKts.append(
""" """
tasks.withType<org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeSimulatorTest> { tasks.withType<org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeSimulatorTest> {
device.set("$defaultIosSimulator") device.set("${unbootedSimulator.udid}")
standalone.set(false) standalone.set(false)
} }
""".trimIndent() """.trimIndent()
@@ -60,47 +58,81 @@ class NativeXcodeSimulatorTestsIT : KGPBaseTest() {
@GradleTest @GradleTest
fun checkSimulatorTestDoesNotFailInNonStandaloneMode(gradleVersion: GradleVersion) { fun checkSimulatorTestDoesNotFailInNonStandaloneMode(gradleVersion: GradleVersion) {
project("native-test-ios-https-request", gradleVersion) { project("native-test-ios-https-request", gradleVersion) {
bootXcodeSimulator(defaultIosSimulator) val simulator = createSimulator()
simulator.boot()
buildGradleKts.append( buildGradleKts.append(
""" """
tasks.withType<org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeSimulatorTest> { tasks.withType<org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeSimulatorTest> {
device.set("$defaultIosSimulator") device.set("${simulator.udid}")
standalone.set(false) standalone.set(false)
} }
""".trimIndent() """.trimIndent()
) )
build("check") build("check") {
when (HostManager.host) {
KonanTarget.MACOS_ARM64 -> assertTasksExecuted(":iosSimulatorArm64Test")
KonanTarget.MACOS_X64 -> assertTasksExecuted(":iosX64Test")
else -> error("Unexpected host")
}
}
} }
} }
@Serializable
private data class Device(val name: String, val udid: String)
@Serializable
private data class Simulators(val devices: Map<String, List<Device>>)
private val deviceIdentifier = "com.apple.CoreSimulator.SimDeviceType.iPhone-12-Pro-Max"
private val uuid = UUID.randomUUID()
private val testSimulatorName = "NativeXcodeSimulatorTestsIT_${uuid}_simulator"
@AfterAll @AfterAll
fun shutDownSimulators() { fun removeSimulatorsCreatedForTests() {
synchronized(bootedXcodeSimulators) { simulators().devices.values.toList().flatMap { it }.filter {
for (device in bootedXcodeSimulators) { it.name == testSimulatorName
val command = listOf("/usr/bin/xcrun", "simctl", "shutdown", device) }.forEach {
val processResult = runProcess(command, File(".")) processOutput(
assertEquals(0, processResult.exitCode) listOf("/usr/bin/xcrun", "simctl", "delete", it.udid)
bootedXcodeSimulators.remove(device) )
}
bootedXcodeSimulators.clear()
} }
} }
private val bootedXcodeSimulators: MutableSet<String> = HashSet() private fun simulators(): Simulators {
return Json {
ignoreUnknownKeys = true
}.decodeFromString<Simulators>(
processOutput(
listOf("/usr/bin/xcrun", "simctl", "list", "devices", "-j")
)
)
}
private fun bootXcodeSimulator(device: String) { private fun createSimulator(): Device {
synchronized(bootedXcodeSimulators) { return Device(
if (device !in bootedXcodeSimulators) { testSimulatorName,
val command = listOf("/usr/bin/xcrun", "simctl", "boot", device) processOutput(
val processResult = runProcess(command, File(".")) listOf("/usr/bin/xcrun", "simctl", "create", testSimulatorName, deviceIdentifier)
assert(processResult.exitCode == 0 || "current state: Booted" in processResult.output) { ).dropLast(1)
"Failed to boot a simulator $device and it wasn't started before, exit code = ${processResult.exitCode}" )
} }
if (processResult.exitCode == 0) {
// if the simulator was booted not by us, we should not shut it down private fun Device.boot() {
bootedXcodeSimulators.add(device) processOutput(
} listOf("/usr/bin/xcrun", "simctl", "bootstatus", udid, "-bd")
} )
}
private fun processOutput(arguments: List<String>): String {
val result = runProcess(
arguments, File("."),
redirectErrorStream = false,
)
assertProcessRunResult(
result
) {
assert(isSuccessful)
} }
return result.output
} }
} }
@@ -31,13 +31,14 @@ fun runProcess(
cmd: List<String>, cmd: List<String>,
workingDir: File, workingDir: File,
environmentVariables: Map<String, String> = mapOf(), environmentVariables: Map<String, String> = mapOf(),
options: BaseGradleIT.BuildOptions? = null options: BaseGradleIT.BuildOptions? = null,
redirectErrorStream: Boolean = true,
): ProcessRunResult { ): ProcessRunResult {
val builder = ProcessBuilder(cmd) val builder = ProcessBuilder(cmd)
builder.environment().putAll(environmentVariables) builder.environment().putAll(environmentVariables)
builder.directory(workingDir) builder.directory(workingDir)
// redirectErrorStream merges stdout and stderr, so it can be get from process.inputStream // redirectErrorStream merges stdout and stderr, so it can be get from process.inputStream
builder.redirectErrorStream(true) builder.redirectErrorStream(redirectErrorStream)
val process = builder.start() val process = builder.start()
// important to read inputStream, otherwise the process may hang on some systems // important to read inputStream, otherwise the process may hang on some systems
@@ -8,12 +8,18 @@ repositories {
} }
kotlin { kotlin {
iosSimulatorArm64() { iosSimulatorArm64 {
compilations.all() {
compilerOptions.options.apply {
optIn.add("kotlinx.cinterop.ExperimentalForeignApi")
}
}
}
iosX64 {
compilations.all() { compilations.all() {
compilerOptions.options.apply { compilerOptions.options.apply {
optIn.add("kotlinx.cinterop.ExperimentalForeignApi") optIn.add("kotlinx.cinterop.ExperimentalForeignApi")
} }
} }
} }
} }