[K/N][test] XCTest support
* Adds xctest wrapper and launcher
* Supports running xctest bundles under the simulator and host xctest
* Initial support of xctest in the native.tests project with stdlib
and kotlin.test
This is a part of ^KT-58928
diff --git a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/Utils.kt b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/Utils.kt
index a2b3958..5cc9d24 100644
--- a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/Utils.kt
+++ b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/Utils.kt
@@ -188,7 +188,7 @@
project.tasks.getByName(taskName).dependsOnDist()
}
-fun TaskProvider<Task>.dependsOnDist() {
+fun TaskProvider<out Task>.dependsOnDist() {
configure {
dependsOnDist()
}
@@ -224,6 +224,10 @@
fun Task.dependsOnDist() {
val target = project.testTarget
+ dependsOnDist(target)
+}
+
+fun Task.dependsOnDist(target: KonanTarget) {
if (project.isDefaultNativeHome) {
dependsOn(":kotlin-native:dist")
if (target != HostManager.host) {
@@ -252,6 +256,12 @@
}
}
+fun Task.dependsOnPlatformLibs(target: KonanTarget) {
+ if (project.isDefaultNativeHome) {
+ dependsOn(":kotlin-native:${target.name}PlatformLibs")
+ }
+}
+
fun Task.konanOldPluginTaskDependenciesWalker(index: Int = 0, walker: Task.(Int) -> Unit) {
walker(index + 1)
dependsOn.forEach {
diff --git a/kotlin-native/runtime/src/main/cpp/Runtime.h b/kotlin-native/runtime/src/main/cpp/Runtime.h
index b7b073f..4cb16be 100644
--- a/kotlin-native/runtime/src/main/cpp/Runtime.h
+++ b/kotlin-native/runtime/src/main/cpp/Runtime.h
@@ -7,6 +7,7 @@
#define RUNTIME_RUNTIME_H
#include "Porting.h"
+#include "Memory.h"
#ifdef __cplusplus
extern "C" {
diff --git a/kotlin-native/utilities/xctest-runner/build.gradle.kts b/kotlin-native/utilities/xctest-runner/build.gradle.kts
new file mode 100644
index 0000000..5079ddf
--- /dev/null
+++ b/kotlin-native/utilities/xctest-runner/build.gradle.kts
@@ -0,0 +1,213 @@
+import org.jetbrains.kotlin.*
+import org.jetbrains.kotlin.bitcode.CompileToBitcodeExtension
+import org.jetbrains.kotlin.cpp.CppUsage
+import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
+import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinUsages
+import org.jetbrains.kotlin.gradle.tasks.CInteropProcess
+import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile
+import org.jetbrains.kotlin.konan.target.*
+import java.io.ByteArrayOutputStream
+import java.util.*
+
+plugins {
+ id("kotlin.native.build-tools-conventions")
+ kotlin("multiplatform")
+ id("compile-to-bitcode")
+}
+
+group = "org.jetbrains.kotlin.native.test.xctest"
+
+val distDir: File by project
+val konanHome: String by extra(distDir.absolutePath)
+// Set native home for KGP
+extra["kotlin.native.home"] = konanHome
+
+with(PlatformInfo) {
+ if (isMac()) {
+ checkXcodeVersion(project)
+ }
+}
+
+/**
+ * Path to the target SDK platform.
+ *
+ * By default, K/N includes only SDK frameworks.
+ * It's required to get the Library frameworks path where the `XCTest.framework` is located.
+ */
+fun targetPlatform(target: String): String {
+ val out = ByteArrayOutputStream()
+ val result = project.exec {
+ executable = "/usr/bin/xcrun"
+ args = listOf("--sdk", target, "--show-sdk-platform-path")
+ standardOutput = out
+ }
+
+ check(result.exitValue == 0) {
+ "xcrun ended unsuccessfully. See the output: $out"
+ }
+
+ return out.toString().trim()
+}
+
+val targets = listOf(
+ KonanTarget.MACOS_X64,
+ KonanTarget.MACOS_ARM64,
+ KonanTarget.IOS_X64,
+ KonanTarget.IOS_SIMULATOR_ARM64,
+ KonanTarget.IOS_ARM64
+).filter {
+ it in platformManager.enabled
+}
+
+/*
+ * Double laziness: lazily create functions that execute `/usr/bin/xcrun` and return
+ * a path to the Developer frameworks.
+ */
+val developerFrameworks: Map<KonanTarget, () -> String> by lazy {
+ platformManager.enabled
+ .filter { it.family.isAppleFamily }
+ .associateWith { target ->
+ val configurable = platformManager.platform(target).configurables as AppleConfigurables
+ val platform = configurable.platformName().lowercase()
+ fun(): String = "${targetPlatform(platform)}/Developer/Library/Frameworks/"
+ }
+}
+
+/**
+ * Gets a path to the developer frameworks location.
+ */
+fun KonanTarget.getDeveloperFramework(): String = developerFrameworks[this]?.let { it() } ?: error("Not supported target $this")
+
+if (PlatformInfo.isMac()) {
+ kotlin {
+ val nativeTargets = listOf(
+ macosX64(KonanTarget.MACOS_X64.name),
+ macosArm64(KonanTarget.MACOS_ARM64.name),
+ iosX64(KonanTarget.IOS_X64.name),
+ iosArm64(KonanTarget.IOS_ARM64.name),
+ iosSimulatorArm64(KonanTarget.IOS_SIMULATOR_ARM64.name)
+ )
+
+ nativeTargets.forEach {
+ it.compilations.all {
+ cinterops {
+ register("XCTest") {
+ compilerOpts("-iframework", konanTarget.getDeveloperFramework())
+ }
+ }
+ }
+ }
+ sourceSets.all {
+ languageSettings.apply {
+ // Oh, yeah! So much experimental, so wow!
+ optIn("kotlinx.cinterop.BetaInteropApi")
+ optIn("kotlinx.cinterop.ExperimentalForeignApi")
+ optIn("kotlin.experimental.ExperimentalNativeApi")
+ }
+ }
+ }
+}
+
+// Due to KT-42056 and KT-48410, it is not possible to set dependencies on dist when opened in the IDEA.
+// IDEA sync makes cinterop tasks eagerly resolve dependencies, effectively running the dist-build in configuration time
+if (!project.isIdeaActive) {
+ targets.forEach {
+ val targetName = it.name.capitalize()
+ tasks.named<KotlinNativeCompile>("compileKotlin$targetName") {
+ dependsOnDist(it)
+ dependsOnPlatformLibs(it)
+ }
+
+ tasks.named<CInteropProcess>("cinteropXCTest$targetName") {
+ dependsOnDist(it)
+ dependsOnPlatformLibs(it)
+ }
+ }
+} else {
+ tasks.named("prepareKotlinIdeaImport") {
+ enabled = false
+ }
+}
+
+bitcode {
+ targets.map { it.withSanitizer() }
+ .forEach { targetWithSanitizer ->
+ target(targetWithSanitizer) {
+ module("xctest") {
+ compilerArgs.set(
+ listOf(
+ "-iframework", target.getDeveloperFramework(),
+ "--std=c++17",
+ )
+ )
+ // Uses headers from the K/N runtime
+ headersDirs.from(project(":kotlin-native:runtime").files("src/main/cpp"))
+
+ sourceSets { main {} }
+ onlyIf { target.family.isAppleFamily }
+ }
+ }
+ }
+}
+
+val xcTestLauncherBitcode by configurations.creating {
+ isCanBeConsumed = false
+ isCanBeResolved = true
+ attributes {
+ attribute(CppUsage.USAGE_ATTRIBUTE, objects.named(CppUsage.LLVM_BITCODE))
+ }
+}
+
+val xcTestArtifactsConfig by configurations.creating {
+ attributes {
+ attribute(Usage.USAGE_ATTRIBUTE, objects.named(KotlinUsages.KOTLIN_API))
+ // Native target-specific
+ attribute(KotlinPlatformType.attribute, KotlinPlatformType.native)
+ }
+}
+
+dependencies {
+ xcTestLauncherBitcode(project)
+}
+
+// Each platform has three artifacts: cinterop, regular klib and bc. Add bc file to the klib
+targets.forEach { target ->
+ val targetName = target.name
+
+ val bitcodeArtifacts = xcTestLauncherBitcode.incoming.artifactView {
+ attributes {
+ attribute(TargetWithSanitizer.TARGET_ATTRIBUTE, target.withSanitizer())
+ }
+ }
+
+ tasks.register("${targetName}XCTestLauncher") {
+ description = "Build native launcher for $targetName"
+ group = CompileToBitcodeExtension.BUILD_TASK_GROUP
+ dependsOn(bitcodeArtifacts.files)
+ }
+
+ val runnerKlibProducer = tasks.register<Zip>("${targetName}XCTestRunner") {
+ val klibTask = tasks.named<KotlinNativeCompile>("compileKotlin${targetName.capitalize()}")
+ dependsOn(klibTask)
+
+ archiveFileName.set("${targetName}XCTest.klib")
+ destinationDirectory.set(layout.buildDirectory)
+
+ from(zipTree(klibTask.get().outputFile))
+ from(bitcodeArtifacts.files) {
+ into("default/targets/$targetName/native")
+ }
+ }
+
+ artifacts {
+ add(xcTestArtifactsConfig.name, runnerKlibProducer) {
+ classifier = targetName
+ }
+ add(xcTestArtifactsConfig.name, tasks.named<CInteropProcess>("cinteropXCTest${targetName.capitalize()}")) {
+ classifier = targetName
+ }
+ add(xcTestArtifactsConfig.name, File(target.getDeveloperFramework())) {
+ classifier = "${targetName}Frameworks"
+ }
+ }
+}
diff --git a/kotlin-native/utilities/xctest-runner/gradle.properties b/kotlin-native/utilities/xctest-runner/gradle.properties
new file mode 100644
index 0000000..bd7059f
--- /dev/null
+++ b/kotlin-native/utilities/xctest-runner/gradle.properties
@@ -0,0 +1,6 @@
+# Disable commonizer. It writes its data to dist, that's not desirable during the build and messes with types
+kotlin.mpp.enableNativeDistributionCommonizationCache = false
+kotlin.mpp.enableGranularSourceSetsMetadata = false
+kotlin.mpp.enableCInteropCommonization = false
+kotlin.internal.mpp.hierarchicalStructureByDefault = false
+kotlin.mpp.enableCompatibilityMetadataVariant = false
\ No newline at end of file
diff --git a/kotlin-native/utilities/xctest-runner/src/commonMain/kotlin/NativeTestRunner.kt b/kotlin-native/utilities/xctest-runner/src/commonMain/kotlin/NativeTestRunner.kt
new file mode 100644
index 0000000..c4ce73d
--- /dev/null
+++ b/kotlin-native/utilities/xctest-runner/src/commonMain/kotlin/NativeTestRunner.kt
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
+
+import kotlinx.cinterop.*
+import kotlin.native.internal.test.*
+import platform.Foundation.*
+import platform.Foundation.NSError
+import platform.Foundation.NSInvocation
+import platform.Foundation.NSString
+import platform.Foundation.NSMethodSignature
+import platform.UniformTypeIdentifiers.UTTypeSourceCode
+import platform.XCTest.*
+import platform.objc.*
+
+/**
+ * An XCTest equivalent of the K/N TestCase.
+ */
+@ExportObjCClass(name = "Kotlin/Native::Test")
+class XCTestCaseRunner(
+ invocation: NSInvocation,
+ val testName: String,
+ val testCase: TestCase,
+) : XCTestCase(invocation) {
+ // Sets XCTest to continue running after failure to match Kotlin Test
+ override fun continueAfterFailure(): Boolean = true
+
+ private val ignored = testCase.ignored || testCase.suite.ignored
+
+ @ObjCAction
+ fun run() {
+ if (ignored) {
+ // FIXME: It is not possible to use XCTSkip() due to the KT-43719 and not implemented exception importing.
+ // Using `_XCTSkipHandler(...)` fails with
+ // Uncaught Kotlin exception: kotlinx.cinterop.ForeignException: _XCTSkipFailureException:: Test skipped
+ // _XCTSkipHandler(testName, 0U, "Test $testName is ignored")
+ // So, just skip the test. It will be seen as passed in XCode, but K/N TestListener should correctly process that.
+ return
+ }
+ try {
+ testCase.doRun()
+ } catch (throwable: Throwable) {
+ val type = when (throwable) {
+ is AssertionError -> XCTIssueTypeAssertionFailure
+ else -> XCTIssueTypeUncaughtException
+ }
+
+ val stackTrace = throwable.getStackTrace()
+ val failedStackLine = stackTrace.first {
+ // try to filter out kotlin.Exceptions and kotlin.test.Assertion inits
+ !it.contains("kfun:kotlin.")
+ }
+ // Find path and line number to create source location
+ val matchResult = Regex("^\\d+ +.* \\((.*):(\\d+):.*\\)$").find(failedStackLine)
+ val sourceLocation = if (matchResult != null) {
+ val (file, line) = matchResult.destructured
+ XCTSourceCodeLocation(file, line.toLong())
+ } else {
+ // No debug info to get the path. Still have to record location
+ XCTSourceCodeLocation(testCase.suite.name, 0L)
+ }
+
+ @Suppress("CAST_NEVER_SUCCEEDS")
+ val stackAsPayload = (stackTrace.joinToString("\n") as? NSString)?.dataUsingEncoding(NSUTF8StringEncoding)
+ val stackTraceAttachment = XCTAttachment.attachmentWithUniformTypeIdentifier(
+ identifier = UTTypeSourceCode.identifier,
+ name = "Kotlin stacktrace (full)",
+ payload = stackAsPayload,
+ userInfo = null
+ )
+
+ val issue = XCTIssue(
+ type = type,
+ compactDescription = "$throwable in $testName",
+ detailedDescription = buildString {
+ appendLine("Test '$testName' from '${testCase.suite.name}' failed with $throwable")
+ throwable.cause?.let { appendLine("(caused by ${throwable.cause})") }
+ },
+ sourceCodeContext = XCTSourceCodeContext(
+ callStackAddresses = throwable.getStackTraceAddresses(),
+ location = sourceLocation
+ ),
+ associatedError = NSErrorWithKotlinException(throwable),
+ attachments = listOf(stackTraceAttachment)
+ )
+ testRun?.recordIssue(issue) ?: error("TestRun for the test $testName not found")
+ }
+ }
+
+ override fun setUp() {
+ if (!ignored) testCase.doBefore()
+ }
+
+ override fun tearDown() {
+ if (!ignored) testCase.doAfter()
+ }
+
+ override fun description(): String = buildString {
+ append(testName)
+ if (ignored) append("(ignored)")
+ }
+
+ override fun name() = testName
+
+ companion object : XCTestCaseMeta() {
+ /**
+ * This method is invoked by the XCTest when it discovered XCTestCase instance
+ * that contains test method.
+ */
+ override fun testCaseWithInvocation(invocation: NSInvocation?): XCTestCase {
+ error(
+ """
+ This should not happen by default.
+ Got invocation: ${invocation?.description}
+ with selector @sel(${NSStringFromSelector(invocation?.selector)})
+ """.trimIndent()
+ )
+ }
+
+ // region Dynamic run methods creation
+
+ /**
+ * Creates and adds method to the metaclass with implementation block
+ * that holds an XCTestCase instance to be run
+ */
+ private fun createRunMethod(selector: SEL) {
+ val result = class_addMethod(
+ cls = this.`class`(),
+ name = selector,
+ imp = imp_implementationWithBlock(this::runner),
+ types = "v@:" // See ObjC' type encodings: v (returns void), @ (id self), : (SEL _cmd)
+ )
+ check(result) {
+ "Internal error: was unable to add method with selector $selector"
+ }
+ }
+
+ /**
+ * Disposes the implementation block for given selector
+ */
+ private fun dispose(selector: SEL) {
+ val imp = class_getMethodImplementation(
+ cls = this.`class`(),
+ name = selector
+ )
+ val result = imp_removeBlock(imp)
+ check(result) {
+ "Internal error: was unable to remove block for $selector"
+ }
+ }
+
+ // TODO: Clean up those methods? When/where should this be invoked?
+ private fun disposeRunMethods() {
+ testMethodsNames().forEach {
+ val selector = NSSelectorFromString(it)
+ dispose(selector)
+ }
+ }
+
+ @Suppress("UNUSED_PARAMETER")
+ private fun runner(runner: XCTestCaseRunner, cmd: SEL) = runner.run()
+ // endregion
+
+ /**
+ * Creates Test invocations for each test method to make them resolvable by the XCTest machinery.
+ *
+ * @see NSInvocation
+ */
+ override fun testInvocations(): List<NSInvocation> = testMethodsNames().map {
+ val selector = NSSelectorFromString(it)
+ createRunMethod(selector)
+ this.instanceMethodSignatureForSelector(selector)?.let { signature ->
+ // Those casts can never succeed ¯\_(ツ)_/¯
+ @Suppress("CAST_NEVER_SUCCEEDS")
+ val invocation = NSInvocation.invocationWithMethodSignature(signature as NSMethodSignature)
+ invocation.setSelector(selector)
+ invocation
+ } ?: error("Not able to create NSInvocation for method $it")
+ }
+ }
+}
+
+private typealias SEL = COpaquePointer?
+
+/**
+ * This is a NSError-wrapper of Kotlin exception used to pass it through the XCTIssue.
+ * See [NativeTestObserver] for the usage.
+ */
+internal class NSErrorWithKotlinException(val kotlinException: Throwable) : NSError(NSCocoaErrorDomain, NSValidationErrorMinimum, null)
+
+/**
+ * XCTest equivalent of K/N TestSuite.
+ */
+class XCTestSuiteRunner(val testSuite: TestSuite) : XCTestSuite(testSuite.name) {
+ private val ignoredSuite: Boolean
+ get() = testSuite.ignored || testSuite.testCases.all { it.value.ignored }
+
+ override fun setUp() {
+ if (!ignoredSuite) testSuite.doBeforeClass()
+ }
+
+ override fun tearDown() {
+ if (!ignoredSuite) testSuite.doAfterClass()
+ }
+}
diff --git a/kotlin-native/utilities/xctest-runner/src/commonMain/kotlin/TestObserver.kt b/kotlin-native/utilities/xctest-runner/src/commonMain/kotlin/TestObserver.kt
new file mode 100644
index 0000000..d9ecc3d6
--- /dev/null
+++ b/kotlin-native/utilities/xctest-runner/src/commonMain/kotlin/TestObserver.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
+
+import kotlin.native.internal.test.*
+import kotlin.time.*
+import kotlin.time.Duration
+import kotlin.time.DurationUnit
+import platform.Foundation.NSError
+import platform.darwin.NSObject
+import platform.XCTest.*
+
+/**
+ * Test execution observation.
+ *
+ * Logs tests and notifies listeners set with [testSettings].
+ * @see TestSettings
+ */
+internal class NativeTestObserver(private val testSettings: TestSettings) : NSObject(), XCTestObservationProtocol {
+ private val listeners: Set<TestListener> = testSettings.listeners
+
+ private val logger: TestLogger = testSettings.logger
+
+ private inline fun sendToListeners(event: TestListener.() -> Unit) {
+ logger.event()
+ listeners.forEach(event)
+ }
+
+ private fun XCTest.getTestDuration(): Duration =
+ testRun?.totalDuration
+ ?.toDuration(DurationUnit.SECONDS)
+ ?: Duration.ZERO
+
+ override fun testCase(testCase: XCTestCase, didRecordIssue: XCTIssue) {
+ val duration = testCase.getTestDuration()
+ val error = didRecordIssue.associatedError as NSError
+ val throwable = if (error is NSErrorWithKotlinException) {
+ error.kotlinException
+ } else {
+ Throwable(didRecordIssue.compactDescription)
+ }
+ if (testCase is XCTestCaseRunner) {
+ sendToListeners { fail(testCase.testCase, throwable, duration.inWholeMilliseconds) }
+ }
+ }
+
+ override fun testCase(testCase: XCTestCase, didRecordExpectedFailure: XCTExpectedFailure) {
+ logger.log("TestCase: $testCase got expected failure: ${didRecordExpectedFailure.failureReason}")
+ this.testCase(testCase, didRecordExpectedFailure.issue)
+ }
+
+ override fun testCaseDidFinish(testCase: XCTestCase) {
+ val duration = testCase.getTestDuration()
+ if (testCase.testRun?.hasSucceeded == true) {
+ if (testCase is XCTestCaseRunner) {
+ val test = testCase.testCase
+ if (!test.ignored) sendToListeners { pass(test, duration.inWholeMilliseconds) }
+ }
+ }
+ }
+
+ override fun testCaseWillStart(testCase: XCTestCase) {
+ if (testCase is XCTestCaseRunner) {
+ val test = testCase.testCase
+ if (test.ignored) {
+ sendToListeners { ignore(test) }
+ } else {
+ sendToListeners { start(test) }
+ }
+ }
+ }
+
+ override fun testSuite(testSuite: XCTestSuite, didRecordIssue: XCTIssue) {
+ logger.log("TestSuite ${testSuite.name} recorded issue: ${didRecordIssue.compactDescription}")
+ }
+
+ override fun testSuite(testSuite: XCTestSuite, didRecordExpectedFailure: XCTExpectedFailure) {
+ logger.log("TestSuite ${testSuite.name} got expected failure: ${didRecordExpectedFailure.failureReason}")
+ this.testSuite(testSuite, didRecordExpectedFailure.issue)
+ }
+
+ override fun testSuiteDidFinish(testSuite: XCTestSuite) {
+ val duration = testSuite.getTestDuration().inWholeMilliseconds
+ if (testSuite is XCTestSuiteRunner) {
+ sendToListeners { finishSuite(testSuite.testSuite, duration) }
+ } else if (testSuite.name == TOP_LEVEL_SUITE) {
+ sendToListeners {
+ finishIteration(testSettings, 0, duration) // test iterations are not supported
+ finishTesting(testSettings, duration)
+ }
+ }
+ }
+
+ override fun testSuiteWillStart(testSuite: XCTestSuite) {
+ if (testSuite is XCTestSuiteRunner) {
+ sendToListeners { startSuite(testSuite.testSuite) }
+ } else if (testSuite.name == TOP_LEVEL_SUITE) {
+ sendToListeners {
+ startTesting(testSettings)
+ startIteration(testSettings, 0, testSettings.testSuites) // test iterations are not supported
+ }
+ }
+ }
+
+ override fun debugDescription() = "Native test listener with test settings $testSettings"
+}
\ No newline at end of file
diff --git a/kotlin-native/utilities/xctest-runner/src/commonMain/kotlin/configuration.kt b/kotlin-native/utilities/xctest-runner/src/commonMain/kotlin/configuration.kt
new file mode 100644
index 0000000..3325ecb
--- /dev/null
+++ b/kotlin-native/utilities/xctest-runner/src/commonMain/kotlin/configuration.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
+
+import kotlinx.cinterop.*
+import kotlin.native.internal.test.*
+import platform.Foundation.NSInvocation
+import platform.Foundation.NSBundle
+import platform.Foundation.NSStringFromSelector
+import platform.XCTest.*
+import platform.objc.*
+
+internal const val TOP_LEVEL_SUITE = "Kotlin/Native test suite"
+
+// Name of the key that contains arguments used to set [TestSettings]
+private const val TEST_ARGUMENTS_KEY = "KotlinNativeTestArgs"
+
+// Test settings should be initialized by the setup method
+// It stores settings with the filtered test suites, loggers and listeners.
+private var testSettings: TestSettings? = null
+
+internal fun testMethodsNames(): List<String> = testSettings?.testSuites
+ ?.toList()
+ ?.flatMap { testSuite ->
+ testSuite.testCases.values.map { "$testSuite.${it.name}" }
+ } ?: error("TestSettings isn't initialized")
+
+@Suppress("unused")
+@kotlin.native.internal.ExportForCppRuntime("Konan_create_testSuite")
+internal fun setupXCTestSuite(): XCTestSuite {
+ val nativeTestSuite = XCTestSuite.testSuiteWithName(TOP_LEVEL_SUITE)
+
+ // Get test arguments from the Info.plist to create test settings
+ val plistTestArgs = NSBundle.allBundles.mapNotNull {
+ (it as? NSBundle)?.infoDictionary?.get(TEST_ARGUMENTS_KEY)
+ }.singleOrNull() as? String
+ val args = plistTestArgs?.split(" ")?.toTypedArray() ?: emptyArray<String>()
+
+ // Initialize settings with the given args
+ testSettings = TestProcessor(GeneratedSuites.suites, args).process()
+
+ checkNotNull(testSettings) {
+ "Test settings wasn't set. Check provided arguments and suites"
+ }
+
+ // Set test observer that will log test execution
+ testSettings?.let {
+ XCTestObservationCenter.sharedTestObservationCenter.addTestObserver(NativeTestObserver(it))
+ }
+
+ if (testSettings?.runTests == true) {
+ // Generate and add tests to the main suite
+ testSettings?.testSuites?.generate()?.forEach {
+ nativeTestSuite.addTest(it)
+ }
+
+ // Tests created (self-check)
+ @Suppress("UNCHECKED_CAST")
+ check(testSettings?.testSuites?.size == (nativeTestSuite.tests as List<XCTest>).size) {
+ "The amount of generated XCTest suites should be equal to Kotlin test suites"
+ }
+ }
+
+ return nativeTestSuite
+}
+
+private fun Collection<TestSuite>.generate(): List<XCTestSuite> {
+ val testInvocations = XCTestCaseRunner.testInvocations()
+ return this.map { suite ->
+ val xcSuite = XCTestSuiteRunner(suite)
+ suite.testCases.values.map { testCase ->
+ testInvocations.filter {
+ it.selectorString() == "${suite.name}.${testCase.name}"
+ }.map { invocation ->
+ XCTestCaseRunner(
+ invocation = invocation,
+ testName = "${suite.name}.${testCase.name}",
+ testCase = testCase
+ )
+ }.single()
+ }.forEach {
+ xcSuite.addTest(it)
+ }
+ xcSuite
+ }
+}
+
+private fun NSInvocation.selectorString() = NSStringFromSelector(selector)
diff --git a/kotlin-native/utilities/xctest-runner/src/commonTest/kotlin/Test.kt b/kotlin-native/utilities/xctest-runner/src/commonTest/kotlin/Test.kt
new file mode 100644
index 0000000..9f8577c
--- /dev/null
+++ b/kotlin-native/utilities/xctest-runner/src/commonTest/kotlin/Test.kt
@@ -0,0 +1,125 @@
+import kotlin.native.concurrent.Worker
+import kotlin.test.*
+import kotlin.time.Duration.Companion.seconds
+import kotlin.time.DurationUnit
+
+@BeforeClass
+fun testTopBeforeClass() {
+ println("Top level BEFORE CLASS")
+}
+
+@AfterClass
+fun testTopAfterClass() {
+ println("Top level AFTER CLASS")
+}
+
+@BeforeTest
+fun testTopLevelBefore() {
+ println("Top level BEFORE")
+}
+
+@AfterTest
+fun testTopLevelAfter() {
+ println("Top level AFTER")
+}
+
+@Test
+fun testTopOne() {
+ println("Top level ONE")
+}
+
+@Test
+fun testTopTwo() {
+ println("Top level TWO")
+}
+
+@Test
+@Ignore
+fun testTopIgnored() {
+ println("Top level IGNORED")
+}
+
+class MyTest {
+ companion object {
+ @BeforeClass
+ fun beforeClass() {
+ println("Setup @BeforeClass")
+ }
+
+ @AfterClass
+ fun afterClass() {
+ println("After @AfterClass")
+ }
+ }
+
+ @BeforeTest
+ fun beforeTest() {
+ println("Setup @BeforeTest")
+ }
+
+ @Test
+ fun testABC() {
+ println("Test @Test ABC")
+ // Wait a sec to see that test is actually working in the suite, not during creation
+ Worker.current.park(1.seconds.toLong(DurationUnit.MICROSECONDS))
+ }
+
+ @Test
+ fun testOther() {
+ println("Test @Test Other")
+ }
+
+ @Test
+ fun testFailed() {
+ println("Failed started")
+ assertTrue(false, "Kotlin assertion failed")
+ println("Failed ended")
+ }
+
+ @Test
+ @Ignore
+ fun testIgnored() {
+ println("Ignored test")
+ }
+
+ @AfterTest
+ fun afterTest() {
+ println("After @AfterTest")
+ }
+}
+
+@Ignore
+class IgnoredSuite {
+ @BeforeTest
+ fun beforeTest() {
+ println("Setup @BeforeTest")
+ }
+
+ @Test
+ fun testTest() {
+ println("Test @Test Test")
+ }
+
+ @AfterTest
+ fun afterTest() {
+ println("After @AfterTest")
+ }
+}
+
+class SuiteWithIgnoredCases {
+ @BeforeTest
+ fun beforeTest() {
+ println("Setup @BeforeTest")
+ }
+
+ @Test
+ @Ignore
+ fun testIgnored() {
+ println("Ignored test")
+ }
+
+ @AfterTest
+ fun afterTest() {
+ println("After @AfterTest")
+ }
+}
\ No newline at end of file
diff --git a/kotlin-native/utilities/xctest-runner/src/nativeInterop/cinterop/XCTest.def b/kotlin-native/utilities/xctest-runner/src/nativeInterop/cinterop/XCTest.def
new file mode 100644
index 0000000..8c8f2ad
--- /dev/null
+++ b/kotlin-native/utilities/xctest-runner/src/nativeInterop/cinterop/XCTest.def
@@ -0,0 +1,9 @@
+depends = Foundation darwin posix
+language = Objective-C
+package = platform.XCTest
+modules = XCTest
+
+compilerOpts = -framework XCTest
+linkerOpts = -framework XCTest
+
+foreignExceptionMode = objc-wrap
diff --git a/kotlin-native/utilities/xctest-runner/src/xctest/cpp/TestLauncher.mm b/kotlin-native/utilities/xctest-runner/src/xctest/cpp/TestLauncher.mm
new file mode 100644
index 0000000..2cd9f6d
--- /dev/null
+++ b/kotlin-native/utilities/xctest-runner/src/xctest/cpp/TestLauncher.mm
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+#import <XCTest/XCTest.h>
+
+#import "Common.h"
+#import "Runtime.h"
+#import "ObjCExport.h"
+
+extern "C" OBJ_GETTER0(Konan_create_testSuite);
+
+@interface XCTestLauncher : XCTestCase
+@end
+
+@implementation XCTestLauncher
+/**
+ * Test suites factory.
+ *
+ * This is a starting point for XCTest to get the test suites with test cases.
+ * K/N dynamically adds test suites for Kotlin tests.
+ * See `setupXCTestSuite` Kotlin method.
+ */
++ (id)defaultTestSuite {
+ Kotlin_initRuntimeIfNeeded();
+ Kotlin_mm_switchThreadStateRunnable();
+ KRef result = nil;
+ Konan_create_testSuite(&result);
+ id retainedResult = Kotlin_ObjCExport_refToRetainedObjC(result);
+ Kotlin_mm_switchThreadStateNative();
+ return retainedResult;
+}
+
+- (void)dealloc {
+ Kotlin_shutdownRuntime();
+ [super dealloc];
+}
+@end
diff --git a/native/executors/src/main/kotlin/org/jetbrains/kotlin/native/executors/XCTestExecutor.kt b/native/executors/src/main/kotlin/org/jetbrains/kotlin/native/executors/XCTestExecutor.kt
new file mode 100644
index 0000000..b61c56d
--- /dev/null
+++ b/native/executors/src/main/kotlin/org/jetbrains/kotlin/native/executors/XCTestExecutor.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package org.jetbrains.kotlin.native.executors
+
+import org.jetbrains.kotlin.konan.target.*
+import java.io.ByteArrayOutputStream
+import java.io.File
+import java.nio.file.Files
+import kotlin.io.path.exists
+
+abstract class AbstractXCTestExecutor(
+ private val configurables: AppleConfigurables,
+ private val executor: Executor
+) : Executor {
+ private val hostExecutor = HostExecutor()
+
+ private val target by configurables::target
+
+ private fun targetPlatform(): String {
+ val xcodeTarget = when (target) {
+ KonanTarget.MACOS_X64, KonanTarget.MACOS_ARM64 -> "macosx"
+ KonanTarget.IOS_X64, KonanTarget.IOS_SIMULATOR_ARM64 -> "iphonesimulator"
+ KonanTarget.IOS_ARM64 -> "iphoneos"
+ else -> error("Target $target is not supported buy the executor")
+ }
+
+ val stdout = ByteArrayOutputStream()
+ val request = ExecuteRequest(
+ "/usr/bin/xcrun",
+ args = mutableListOf("--sdk", xcodeTarget, "--show-sdk-platform-path"),
+ stdout = stdout
+ )
+ hostExecutor.execute(request).assertSuccess()
+
+ return stdout.toString("UTF-8").trim()
+ }
+
+ private val frameworkPath: String
+ get() = "${targetPlatform()}/Developer/Library/Frameworks/"
+
+ private val xcTestExecutablePath: String
+ get() = "${targetPlatform()}/Developer/Library/Xcode/Agents/xctest"
+
+ override fun execute(request: ExecuteRequest): ExecuteResponse {
+ val bundlePath = if (request.args.isNotEmpty()) {
+ // Copy the bundle to a temp dir
+ val dir = Files.createTempDirectory("tmp-xctest-runner")
+ dir.toFile().deleteOnExit()
+ val newBundlePath = File(request.executableAbsolutePath).run {
+ val newPath = dir.resolve(name)
+ copyRecursively(newPath.toFile())
+ newPath
+ }
+ check(newBundlePath.exists())
+
+ // Passing arguments to the XCTest-runner using Info.plist file.
+ val infoPlist = newBundlePath.toFile()
+ .walk()
+ .firstOrNull { it.name == "Info.plist" }
+ ?.absolutePath
+ checkNotNull(infoPlist) { "Info.plist of xctest-bundle wasn't found. Check the bundle contents and location "}
+
+ val writeArgsRequest = ExecuteRequest(
+ executableAbsolutePath = "/usr/libexec/PlistBuddy",
+ args = mutableListOf("-c", "Add :KotlinNativeTestArgs string ${request.args.joinToString(" ")}", infoPlist)
+ )
+ val writeResponse = hostExecutor.execute(writeArgsRequest)
+ writeResponse.assertSuccess()
+
+ newBundlePath.toString()
+ } else {
+ request.executableAbsolutePath
+ }
+
+ val response = executor.execute(request.copying {
+ environment["DYLD_FRAMEWORK_PATH"] = frameworkPath
+ executableAbsolutePath = xcTestExecutablePath
+ args.clear()
+ args.add(bundlePath)
+ })
+
+ if (request.executableAbsolutePath != bundlePath) {
+ // Remove the copied bundle after the run
+ File(bundlePath).deleteRecursively()
+ }
+ return response
+ }
+}
+
+class XCTestHostExecutor(configurables: AppleConfigurables) : AbstractXCTestExecutor(configurables, HostExecutor())
+
+class XCTestSimulatorExecutor(configurables: AppleConfigurables) :
+ AbstractXCTestExecutor(configurables, XcodeSimulatorExecutor(configurables))
\ No newline at end of file
diff --git a/native/native.tests/build.gradle.kts b/native/native.tests/build.gradle.kts
index 36b677c..11243d1 100644
--- a/native/native.tests/build.gradle.kts
+++ b/native/native.tests/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.konan.target.HostManager
+
plugins {
kotlin("jvm")
id("jps-compatible")
@@ -33,14 +35,49 @@
}
}
+if (kotlinBuildProperties.isKotlinNativeEnabled &&
+ HostManager.hostIsMac &&
+ project.hasProperty(TestProperty.XCTEST_FRAMEWORK.fullName)
+) {
+ val xcTestConfig = configurations.detachedConfiguration(
+ dependencies.project(path = ":kotlin-native:utilities:xctest-runner", configuration = "xcTestArtifactsConfig")
+ )
+ // Set test tasks dependency on this config
+ tasks.withType<Test>().configureEach {
+ if (name.endsWith("XCTest")) {
+ dependsOn(xcTestConfig)
+ }
+ }
+
+ val testTarget = project.findProperty(TestProperty.TEST_TARGET.fullName)?.toString() ?: HostManager.hostName
+ // Set XCTest runner and cinterop klibs location
+ project.extra.set(
+ TestProperty.CUSTOM_KLIBS.fullName,
+ xcTestConfig.resolvedConfiguration
+ .resolvedArtifacts
+ .filter { it.classifier == testTarget }
+ .map { it.file }
+ .joinToString(separator = File.pathSeparator)
+ )
+ // Set XCTest.framework location (Developer Frameworks directory)
+ project.setProperty(
+ TestProperty.XCTEST_FRAMEWORK.fullName,
+ xcTestConfig.resolvedConfiguration
+ .resolvedArtifacts
+ .filter { it.classifier == "${testTarget}Frameworks" }
+ .map { it.file }
+ .singleOrNull()
+ )
+}
+
testsJar {}
// Tasks that run different sorts of tests. Most frequent use case: running specific tests at TeamCity.
val infrastructureTest = nativeTest("infrastructureTest", "infrastructure")
val codegenBoxTest = nativeTest("codegenBoxTest", "codegen & !frontend-fir")
val codegenBoxK2Test = nativeTest("codegenBoxK2Test", "codegen & frontend-fir")
-val stdlibTest = nativeTest("stdlibTest", "stdlib & !frontend-fir")
-val stdlibK2Test = nativeTest("stdlibK2Test", "stdlib & frontend-fir")
+val stdlibTest = nativeTest("stdlibTest", "stdlib & !frontend-fir & !xctest")
+val stdlibK2Test = nativeTest("stdlibK2Test", "stdlib & frontend-fir & !xctest")
val kotlinTestLibraryTest = nativeTest("kotlinTestLibraryTest", "kotlin-test & !frontend-fir")
val kotlinTestLibraryK2Test = nativeTest("kotlinTestLibraryK2Test", "kotlin-test & frontend-fir")
val partialLinkageTest = nativeTest("partialLinkageTest", "partial-linkage")
@@ -49,6 +86,12 @@
val cachesTest = nativeTest("cachesTest", "caches")
val klibTest = nativeTest("klibTest", "klib")
+// xctest tasks
+val stdlibTestWithXCTest = nativeTest("stdlibTestWithXCTest", "stdlib & !frontend-fir & xctest")
+val stdlibK2TestWithXCTest = nativeTest("stdlibK2TestWithXCTest", "stdlib & frontend-fir & xctest")
+val kotlinTestLibraryTestWithXCTest = nativeTest("kotlinTestLibraryTestWithXCTest", "kotlin-test & !frontend-fir & xctest")
+val kotlinTestLibraryK2TestWithXCTest = nativeTest("kotlinTestLibraryK2TestWithXCTest", "kotlin-test & frontend-fir & xctest")
+
val testTags = findProperty("kotlin.native.tests.tags")?.toString()
// Note: arbitrary JUnit tag expressions can be used in this property.
// See https://junit.org/junit5/docs/current/user-guide/#running-tests-tag-expressions
diff --git a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/KotlinTestLibraryTest.kt b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/KotlinTestLibraryTest.kt
index 492567b..c17a8f6 100644
--- a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/KotlinTestLibraryTest.kt
+++ b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/KotlinTestLibraryTest.kt
@@ -63,3 +63,38 @@
@TestFactory
fun worker() = dynamicTestCase(TestCaseId.Named("worker"))
}
+
+@Tag("kotlin-test")
+@Tag("xctest")
+@PredefinedTestCases(
+ TC(
+ name = "defaultXCTest",
+ runnerType = TestRunnerType.DEFAULT,
+ freeCompilerArgs = [STDLIB_IS_A_FRIEND],
+ sourceLocations = ["libraries/kotlin.test/common/src/test/kotlin/**.kt"]
+ )
+)
+@UsePartialLinkage(UsePartialLinkage.Mode.DISABLED)
+class KotlinTestLibraryTestWithXCTest : AbstractNativeBlackBoxTest() {
+ @TestFactory
+ fun default() = dynamicTestCase(TestCaseId.Named("defaultXCTest"))
+}
+
+@Tag("kotlin-test")
+@Tag("frontend-fir")
+@Tag("xctest")
+@PredefinedTestCases(
+ TC(
+ name = "defaultXCTest",
+ runnerType = TestRunnerType.DEFAULT,
+ freeCompilerArgs = [STDLIB_IS_A_FRIEND],
+ sourceLocations = ["libraries/kotlin.test/common/src/test/kotlin/**.kt"]
+ )
+)
+@FirPipeline
+@UsePartialLinkage(UsePartialLinkage.Mode.DISABLED)
+class FirKotlinTestLibraryTestWithXCTest : AbstractNativeBlackBoxTest() {
+ @TestFactory
+ fun default() = dynamicTestCase(TestCaseId.Named("defaultXCTest"))
+}
+
diff --git a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/StdlibTest.kt b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/StdlibTest.kt
index b91058b..f371f4d 100644
--- a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/StdlibTest.kt
+++ b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/StdlibTest.kt
@@ -7,6 +7,8 @@
package org.jetbrains.kotlin.konan.blackboxtest
+import org.jetbrains.kotlin.konan.blackboxtest.support.*
+import org.jetbrains.kotlin.konan.blackboxtest.support.ClassLevelProperty
import org.jetbrains.kotlin.konan.blackboxtest.support.TestCaseId
import org.jetbrains.kotlin.konan.blackboxtest.support.TestRunnerType
import org.jetbrains.kotlin.konan.blackboxtest.support.group.FirPipeline
@@ -111,6 +113,63 @@
fun worker() = dynamicTestCase(TestCaseId.Named("worker"))
}
+@Tag("stdlib")
+@Tag("xctest")
+@PredefinedTestCases(
+ TC(
+ name = "xctest",
+ runnerType = TestRunnerType.DEFAULT,
+ freeCompilerArgs = [ENABLE_MPP, STDLIB_IS_A_FRIEND, ENABLE_X_STDLIB_API, ENABLE_X_ENCODING_API, ENABLE_RANGE_UNTIL],
+ sourceLocations = [
+ "libraries/stdlib/test/**.kt",
+ "libraries/stdlib/common/test/**.kt",
+ "kotlin-native/backend.native/tests/stdlib_external/text/**.kt",
+ "kotlin-native/backend.native/tests/stdlib_external/utils.kt",
+ "kotlin-native/backend.native/tests/stdlib_external/jsCollectionFactoriesActuals.kt"
+ ],
+ ignoredTests = [DISABLED_STDLIB_TEST]
+ )
+)
+@UsePartialLinkage(UsePartialLinkage.Mode.DISABLED)
+class StdlibTestWithXCTest : AbstractNativeBlackBoxTest() {
+ @TestFactory
+ fun xctest() = dynamicTestCase(TestCaseId.Named("xctest"))
+}
+
+@Tag("stdlib")
+@Tag("xctest")
+@Tag("frontend-fir")
+@PredefinedTestCases(
+ TC(
+ name = "xctest",
+ runnerType = TestRunnerType.DEFAULT,
+ freeCompilerArgs = [
+ ENABLE_MPP, STDLIB_IS_A_FRIEND, ENABLE_X_STDLIB_API, ENABLE_X_ENCODING_API, ENABLE_RANGE_UNTIL,
+ "-Xcommon-sources=libraries/stdlib/common/test/jsCollectionFactories.kt",
+ "-Xcommon-sources=libraries/stdlib/common/test/testUtils.kt",
+ "-Xcommon-sources=libraries/stdlib/test/testUtils.kt",
+ "-Xcommon-sources=libraries/stdlib/test/text/StringEncodingTest.kt",
+ ],
+ sourceLocations = [
+ "libraries/stdlib/test/**.kt",
+ "libraries/stdlib/common/test/**.kt",
+ "kotlin-native/backend.native/tests/stdlib_external/text/**.kt",
+ "kotlin-native/backend.native/tests/stdlib_external/utils.kt",
+ "kotlin-native/backend.native/tests/stdlib_external/jsCollectionFactoriesActuals.kt"
+ ],
+ ignoredFiles = [
+ DISABLED_K2_ARRAYS,
+ ],
+ ignoredTests = [DISABLED_STDLIB_TEST]
+ )
+)
+@FirPipeline
+@UsePartialLinkage(UsePartialLinkage.Mode.DISABLED)
+class FirStdlibTestWithXCTest : AbstractNativeBlackBoxTest() {
+ @TestFactory
+ fun xctest() = dynamicTestCase(TestCaseId.Named("xctest"))
+}
+
private const val ENABLE_MPP = "-Xmulti-platform"
internal const val STDLIB_IS_A_FRIEND = "-friend-modules=$KOTLIN_NATIVE_DISTRIBUTION/klib/common/stdlib"
private const val ENABLE_X_STDLIB_API = "-opt-in=kotlin.ExperimentalStdlibApi"
diff --git a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/ConfigurationProperties.kt b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/ConfigurationProperties.kt
index 8bfde34..094daa2 100644
--- a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/ConfigurationProperties.kt
+++ b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/ConfigurationProperties.kt
@@ -69,7 +69,7 @@
EXECUTION_TIMEOUT("executionTimeout"),
SANITIZER("sanitizer"),
COMPILER_OUTPUT_INTERCEPTOR("compilerOutputInterceptor"),
-
+ XCTEST_FRAMEWORK("xctest"),
;
internal val propertyName = fullPropertyName(shortName)
diff --git a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/NativeTestSupport.kt b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/NativeTestSupport.kt
index 1a7d442..9c70b57 100644
--- a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/NativeTestSupport.kt
+++ b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/NativeTestSupport.kt
@@ -199,6 +199,7 @@
output += computePipelineType(testClass.get())
output += computeUsedPartialLinkageConfig(enclosingTestClass)
output += computeCompilerOutputInterceptor(enforcedProperties)
+ output += computeXCTestRunner(enforcedProperties)
return nativeTargets
}
@@ -251,7 +252,7 @@
enforcedProperties: EnforcedProperties,
distribution: Distribution,
kotlinNativeTargets: KotlinNativeTargets,
- optimizationMode: OptimizationMode
+ optimizationMode: OptimizationMode,
): CacheMode {
val cacheMode = ClassLevelProperty.CACHE_MODE.readValue(
enforcedProperties,
@@ -323,6 +324,14 @@
return Timeouts(executionTimeout)
}
+ private fun computeXCTestRunner(enforcedProperties: EnforcedProperties) = XCTestRunner(
+ ClassLevelProperty.XCTEST_FRAMEWORK.readValue(
+ enforcedProperties,
+ { File(it).absolutePath },
+ default = ""
+ )
+ )
+
/*************** Test class settings (for black box tests only) ***************/
private fun ExtensionContext.getOrCreateTestClassSettings(): TestClassSettings =
@@ -422,7 +431,7 @@
private fun computeGeneratedSourceDirs(
baseDirs: BaseDirs,
targets: KotlinNativeTargets,
- enclosingTestClass: Class<*>
+ enclosingTestClass: Class<*>,
): GeneratedSources {
val testSourcesDir = baseDirs.testBuildDir
.resolve("bb.src") // "bb" for black box
@@ -440,7 +449,7 @@
private fun computeBinariesForBlackBoxTests(
baseDirs: BaseDirs,
targets: KotlinNativeTargets,
- enclosingTestClass: Class<*>
+ enclosingTestClass: Class<*>,
): Binaries {
val testBinariesDir = baseDirs.testBuildDir
.resolve("bb.out") // "bb" for black box
diff --git a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/compilation/TestCompilation.kt b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/compilation/TestCompilation.kt
index be25914..ab19a66 100644
--- a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/compilation/TestCompilation.kt
+++ b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/compilation/TestCompilation.kt
@@ -479,6 +479,108 @@
}
}
+internal class TestBundleCompilation(
+ val settings: Settings,
+ freeCompilerArgs: TestCompilerArgs,
+ sourceModules: Collection<TestModule>,
+ private val extras: Extras,
+ dependencies: Iterable<TestCompilationDependency<*>>,
+ expectedArtifact: XCTestBundle,
+ private val tryPassSystemCacheDirectory: Boolean = true,
+) : SourceBasedCompilation<XCTestBundle>(
+ targets = settings.get(),
+ home = settings.get(),
+ classLoader = settings.get(),
+ optimizationMode = settings.get(),
+ compilerOutputInterceptor = settings.get(),
+ threadStateChecker = settings.get(),
+ sanitizer = settings.get(),
+ gcType = settings.get(),
+ gcScheduler = settings.get(),
+ allocator = settings.get(),
+ pipelineType = settings.getStageDependentPipelineType(),
+ freeCompilerArgs = freeCompilerArgs,
+ compilerPlugins = settings.get(),
+ sourceModules = sourceModules,
+ dependencies = CategorizedDependencies(dependencies),
+ expectedArtifact = expectedArtifact
+) {
+ // TODO: Enabling caches lead to link failure
+ // Undefined symbols for architecture x86_64:
+ // "_dns_class_number", referenced from:
+ // _platform_darwin_dns_class_number_wrapper379 in liborg.jetbrains.kotlin.native.platform.darwin-cache.a(result.o)
+ private val cacheMode: CacheMode = CacheMode.WithoutCache // settings.get()
+ override val binaryOptions = BinaryOptions.RuntimeAssertionsMode.chooseFor(cacheMode)
+
+ private val partialLinkageConfig: UsedPartialLinkageConfig = settings.get()
+
+ override fun applySpecificArgs(argsBuilder: ArgsBuilder): Unit = with(argsBuilder) {
+ add(
+ "-produce", "test_bundle",
+ "-linker-option", "-F" + settings.get<XCTestRunner>().frameworksPath,
+ "-output", expectedArtifact.bundleDir.path
+ )
+ when (extras) {
+ is NoTestRunnerExtras -> error("It doesn't suit")
+ is WithTestRunnerExtras -> {
+ val testDumpFile: File? = if (sourceModules.isEmpty()
+ && dependencies.includedLibraries.isNotEmpty()
+ && cacheMode.useStaticCacheForUserLibraries
+ ) {
+ // If there are no source modules passed to the compiler, but there is an included library with the static cache, then
+ // this should be two-stage test mode: Test functions are already stored in the included library, and they should
+ // already have been dumped during generation of library's static cache.
+ null // No, don't need to dump tests.
+ } else {
+ expectedArtifact.testDumpFile // Yes, need to dump tests.
+ }
+ applyTestRunnerSpecificArgs(extras, testDumpFile)
+ }
+ }
+ applyPartialLinkageArgs(partialLinkageConfig)
+ super.applySpecificArgs(argsBuilder)
+ }
+
+ override fun applyDependencies(argsBuilder: ArgsBuilder): Unit = with(argsBuilder) {
+ super.applyDependencies(argsBuilder)
+ cacheMode.staticCacheForDistributionLibrariesRootDir
+ ?.takeIf { tryPassSystemCacheDirectory }
+ ?.let { cacheRootDir -> add("-Xcache-directory=$cacheRootDir") }
+ add(dependencies.uniqueCacheDirs) { libraryCacheDir -> "-Xcache-directory=${libraryCacheDir.path}" }
+ }
+
+ override fun postCompileCheck() {
+ expectedArtifact.assertTestDumpFileNotEmptyIfExists()
+ }
+
+ companion object {
+ internal fun ArgsBuilder.applyTestRunnerSpecificArgs(extras: WithTestRunnerExtras, testDumpFile: File?) {
+ val testRunnerArg = when (extras.runnerType) {
+ TestRunnerType.DEFAULT -> "-generate-test-runner"
+ TestRunnerType.WORKER, TestRunnerType.NO_EXIT -> error("Those runners don't work here")
+ }
+ add(testRunnerArg)
+ testDumpFile?.let { add("-Xdump-tests-to=$it") }
+ }
+
+ internal fun XCTestBundle.assertTestDumpFileNotEmptyIfExists() {
+ if (testDumpFile.exists()) {
+ testDumpFile.useLines { lines ->
+ assertTrue(lines.filter(String::isNotBlank).any()) { "Test dump file is empty: $testDumpFile" }
+ }
+ }
+ }
+
+ internal fun ArgsBuilder.applyPartialLinkageArgs(partialLinkageConfig: UsedPartialLinkageConfig) {
+ with(partialLinkageConfig.config) {
+ add("-Xpartial-linkage=${mode.name.lowercase()}")
+ if (mode.isEnabled)
+ add("-Xpartial-linkage-loglevel=${logLevel.name.lowercase()}")
+ }
+ }
+ }
+}
+
internal class CategorizedDependencies(uncategorizedDependencies: Iterable<TestCompilationDependency<*>>) {
val failures: Set<TestCompilationResult.Failure> by lazy {
uncategorizedDependencies.flatMapToSet { dependency ->
diff --git a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/compilation/TestCompilationArtifact.kt b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/compilation/TestCompilationArtifact.kt
index d20658d..2c55abf 100644
--- a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/compilation/TestCompilationArtifact.kt
+++ b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/compilation/TestCompilationArtifact.kt
@@ -6,6 +6,7 @@
package org.jetbrains.kotlin.konan.blackboxtest.support.compilation
import java.io.File
+import java.nio.file.*
internal sealed interface TestCompilationArtifact {
val logFile: File
@@ -28,7 +29,12 @@
data class ObjCFramework(private val buildDir: File, val frameworkName: String) : TestCompilationArtifact {
val frameworkDir: File get() = buildDir.resolve("$frameworkName.framework")
override val logFile: File get() = frameworkDir.resolveSibling("${frameworkDir.name}.log")
- val headersDir: File get () = frameworkDir.resolve("Headers")
+ val headersDir: File get() = frameworkDir.resolve("Headers")
val mainHeader: File get() = headersDir.resolve("$frameworkName.h")
}
+
+ data class XCTestBundle(val bundleDir: File) : TestCompilationArtifact {
+ override val logFile: File get() = bundleDir.resolveSibling("${bundleDir.name}.log")
+ val testDumpFile: File get() = bundleDir.resolveSibling("${bundleDir.name}.dump")
+ }
}
diff --git a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/compilation/TestCompilationFactory.kt b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/compilation/TestCompilationFactory.kt
index 1e1a8c3..7a52621 100644
--- a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/compilation/TestCompilationFactory.kt
+++ b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/compilation/TestCompilationFactory.kt
@@ -18,6 +18,7 @@
import org.jetbrains.kotlin.konan.blackboxtest.support.compilation.TestCompilationDependencyType.*
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.*
import org.jetbrains.kotlin.konan.blackboxtest.support.util.*
+import org.jetbrains.kotlin.konan.target.CompilerOutputKind
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertTrue
import org.jetbrains.kotlin.utils.addIfNotNull
import java.io.File
@@ -26,10 +27,12 @@
private val cachedKlibCompilations = ThreadSafeCache<KlibCacheKey, KlibCompilations>()
private val cachedExecutableCompilations = ThreadSafeCache<ExecutableCacheKey, TestCompilation<Executable>>()
private val cachedObjCFrameworkCompilations = ThreadSafeCache<ObjCFrameworkCacheKey, ObjCFrameworkCompilation>()
+ private val cachedTestBundleCompilations = ThreadSafeCache<TestBundleCacheKey, TestBundleCompilation>()
private data class KlibCacheKey(val sourceModules: Set<TestModule>, val freeCompilerArgs: TestCompilerArgs)
private data class ExecutableCacheKey(val sourceModules: Set<TestModule>)
private data class ObjCFrameworkCacheKey(val sourceModules: Set<TestModule>)
+ private data class TestBundleCacheKey(val sourceModules: Set<TestModule>)
// A pair of compilations for a KLIB itself and for its static cache that are created together.
private data class KlibCompilations(val klib: TestCompilation<KLIB>, val staticCache: TestCompilation<KLIBStaticCache>?)
@@ -138,6 +141,39 @@
}
}
+ fun testCasesToTestBundle(testCases: Collection<TestCase>, settings: Settings): TestCompilation<XCTestBundle> {
+ val rootModules = testCases.flatMapToSet { testCase -> testCase.rootModules }
+ val cacheKey = TestBundleCacheKey(rootModules)
+
+ // Fast pass.
+ cachedTestBundleCompilations[cacheKey]?.let { return it }
+
+ // Long pass.
+ val freeCompilerArgs = rootModules.first().testCase.freeCompilerArgs // Should be identical inside the same test case group.
+ val extras = testCases.first().extras // Should be identical inside the same test case group.
+ val executableArtifact = XCTestBundle(settings.artifactFileForXCTestBundle(rootModules))
+
+ val (
+ dependenciesToCompileExecutable: Iterable<CompiledDependency<*>>,
+ sourceModulesToCompileExecutable: Set<TestModule.Exclusive>
+ ) = getDependenciesAndSourceModules(settings, rootModules, freeCompilerArgs) {
+ // TODO: should also include caches but the test compilation architecture is not configurable enough
+ // to de-duplicate code used for executable and other compiler outputs
+ ProduceStaticCache.No
+ }
+
+ return cachedTestBundleCompilations.computeIfAbsent(cacheKey) {
+ TestBundleCompilation(
+ settings = settings,
+ freeCompilerArgs = freeCompilerArgs,
+ sourceModules = sourceModulesToCompileExecutable,
+ extras = extras,
+ dependencies = dependenciesToCompileExecutable,
+ expectedArtifact = executableArtifact
+ )
+ }
+ }
+
private fun getDependenciesAndSourceModules(
settings: Settings,
rootModules: Set<TestModule.Exclusive>,
@@ -267,6 +303,18 @@
private fun Settings.artifactFileForExecutable(module: TestModule.Exclusive) =
singleModuleArtifactFile(module, get<KotlinNativeTargets>().testTarget.family.exeSuffix)
+ private fun Settings.artifactFileForXCTestBundle(modules: Set<TestModule.Exclusive>) = when (modules.size) {
+ 1 -> artifactFileForXCTestBundle(modules.first())
+ else -> multiModuleArtifactFile(modules, xctestExtension())
+ }
+
+ private fun Settings.artifactFileForXCTestBundle(module: TestModule.Exclusive) =
+ singleModuleArtifactFile(module, xctestExtension())
+
+ private fun Settings.xctestExtension(): String = CompilerOutputKind.TEST_BUNDLE
+ .suffix(get<KotlinNativeTargets>().testTarget)
+ .substringAfterLast(".")
+
private fun Settings.artifactFileForKlib(modules: Set<TestModule>, freeCompilerArgs: TestCompilerArgs): File =
when (modules.size) {
1 -> when (val module = modules.first()) {
diff --git a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/group/PredefinedTestCaseGroupProvider.kt b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/group/PredefinedTestCaseGroupProvider.kt
index 8a196d5..902f7f1 100644
--- a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/group/PredefinedTestCaseGroupProvider.kt
+++ b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/group/PredefinedTestCaseGroupProvider.kt
@@ -8,12 +8,14 @@
import org.jetbrains.kotlin.konan.blackboxtest.support.*
import org.jetbrains.kotlin.konan.blackboxtest.support.TestCase.WithTestRunnerExtras
import org.jetbrains.kotlin.konan.blackboxtest.support.runner.TestRunChecks
+import org.jetbrains.kotlin.konan.blackboxtest.support.settings.CustomKlibs
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.KotlinNativeHome
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.Settings
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.Timeouts
import org.jetbrains.kotlin.konan.blackboxtest.support.util.ThreadSafeCache
import org.jetbrains.kotlin.konan.blackboxtest.support.util.expandGlobTo
import org.jetbrains.kotlin.konan.blackboxtest.support.util.getAbsoluteFile
+import org.jetbrains.kotlin.konan.blackboxtest.support.util.mapToSet
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertTrue
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
import java.io.File
@@ -72,7 +74,10 @@
ignoredTests = predefinedTestCase.ignoredTests.toSet()
)
)
- testCase.initialize(null, null)
+ testCase.initialize(
+ givenModules = settings.get<CustomKlibs>().klibs.mapToSet(TestModule::Given),
+ findSharedModule = null
+ )
TestCaseGroup.Default(disabledTestCaseIds = emptySet(), testCases = listOf(testCase))
}
diff --git a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/runner/RunnerWithExecutor.kt b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/runner/RunnerWithExecutor.kt
index 7ee2f9f..8a88780 100644
--- a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/runner/RunnerWithExecutor.kt
+++ b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/runner/RunnerWithExecutor.kt
@@ -42,6 +42,7 @@
val request = ExecuteRequest(
executableAbsolutePath = executable.executableFile.absolutePath,
args = programArgs,
+ workingDirectory = executable.executableFile.parentFile,
stdin = stdin,
stdout = stdout,
stderr = stderr,
diff --git a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/runner/TestRunProvider.kt b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/runner/TestRunProvider.kt
index b232954..d37d7f8 100644
--- a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/runner/TestRunProvider.kt
+++ b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/runner/TestRunProvider.kt
@@ -11,11 +11,14 @@
import org.jetbrains.kotlin.konan.blackboxtest.support.TestCase.NoTestRunnerExtras
import org.jetbrains.kotlin.konan.blackboxtest.support.TestCase.WithTestRunnerExtras
import org.jetbrains.kotlin.konan.blackboxtest.support.compilation.TestCompilation
+import org.jetbrains.kotlin.konan.blackboxtest.support.compilation.TestCompilationArtifact
import org.jetbrains.kotlin.konan.blackboxtest.support.compilation.TestCompilationArtifact.Executable
import org.jetbrains.kotlin.konan.blackboxtest.support.compilation.TestCompilationFactory
+import org.jetbrains.kotlin.konan.blackboxtest.support.compilation.TestCompilationResult
import org.jetbrains.kotlin.konan.blackboxtest.support.compilation.TestCompilationResult.Companion.assertSuccess
import org.jetbrains.kotlin.konan.blackboxtest.support.group.TestCaseGroupProvider
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.Settings
+import org.jetbrains.kotlin.konan.blackboxtest.support.settings.XCTestRunner
import org.jetbrains.kotlin.konan.blackboxtest.support.util.ThreadSafeCache
import org.jetbrains.kotlin.konan.blackboxtest.support.util.TreeNode
import org.jetbrains.kotlin.konan.blackboxtest.support.util.buildTree
@@ -33,14 +36,16 @@
) : BaseTestRunProvider(), ExtensionContext.Store.CloseableResource {
private val compilationFactory = TestCompilationFactory()
private val cachedCompilations = ThreadSafeCache<TestCompilationCacheKey, TestCompilation<Executable>>()
+ private val cachedXCTestCompilations = ThreadSafeCache<TestCompilationCacheKey, TestCompilation<TestCompilationArtifact.XCTestBundle>>()
/**
* Produces a single [TestRun] per [TestCase]. So-called "one test case/one test run" mode.
*
* If [TestCase] contains multiple functions annotated with [kotlin.test.Test], then all these functions will be executed
- * in one shot. If either function will fail, the whole JUnit test will be considered as failed.
+ * in one shot. If either function fails, the whole JUnit test will be considered as failed.
*
* Example:
+ * ```
* //+++ testData file (foo.kt): +++//
* @kotlin.test.Test
* fun one() { /* ... */ }
@@ -58,11 +63,12 @@
* // If either of test functions fails, the whole "testFoo()" JUnit test is marked as failed.
* }
* }
+ * ```
*/
fun getSingleTestRun(
testCaseId: TestCaseId,
settings: Settings
- ): TestRun = withTestExecutable(testCaseId, settings) { testCase, executable ->
+ ): TestRun = withTestSettingsDispatched(testCaseId, settings) { testCase, executable ->
createSingleTestRun(testCase, executable)
}
@@ -72,10 +78,11 @@
* If [TestCase] contains multiple functions annotated with [kotlin.test.Test], then a separate [TestRun] will be produced
* for each such function.
*
- * This allows to have a better granularity in tests. So that every individual test method inside [TestCase] will be considered
+ * This allows having a better granularity in tests. So that every test method inside [TestCase] will be considered
* as an individual JUnit test, and will be presented as a separate row in JUnit test report.
*
* Example:
+ * ```
* //+++ testData file (foo.kt): +++//
* @kotlin.test.Test
* fun one() { /* ... */ }
@@ -95,11 +102,12 @@
* // in the test report, and "testFoo.two" will be presented as passed.
* }
* }
+ * ```
*/
fun getTestRuns(
testCaseId: TestCaseId,
settings: Settings
- ): Collection<TreeNode<TestRun>> = withTestExecutable(testCaseId, settings) { testCase, executable ->
+ ): Collection<TreeNode<TestRun>> = withTestSettingsDispatched(testCaseId, settings) { testCase, executable ->
fun createTestRun(testRunName: String, testName: TestName?) = createTestRun(testCase, executable, testRunName, testName)
when (testCase.kind) {
@@ -117,6 +125,18 @@
}
}
+ private fun <T> withTestSettingsDispatched(
+ testCaseId: TestCaseId,
+ settings: Settings,
+ action: (TestCase, TestExecutable) -> T
+ ): T {
+ return if (settings.get<XCTestRunner>().isEnabled) {
+ withTestBundle(testCaseId, settings, action)
+ } else {
+ withTestExecutable(testCaseId, settings, action)
+ }
+ }
+
private fun <T> withTestExecutable(
testCaseId: TestCaseId,
settings: Settings,
@@ -162,6 +182,54 @@
return action(testCase, executable)
}
+ private fun <T> withTestBundle(
+ testCaseId: TestCaseId,
+ settings: Settings,
+ action: (TestCase, TestExecutable) -> T
+ ): T {
+ val testCaseGroup = testCaseGroupProvider.getTestCaseGroup(testCaseId.testCaseGroupId, settings)
+ ?: fail { "No test case for $testCaseId" }
+
+ assumeTrue(testCaseGroup.isEnabled(testCaseId), "Test case is disabled")
+
+ val testCase = testCaseGroup.getByName(testCaseId) ?: fail { "No test case for $testCaseId" }
+
+ val testCompilation = when (testCase.kind) {
+ TestKind.STANDALONE -> {
+ // Create a separate compilation for each standalone test case.
+ cachedXCTestCompilations.computeIfAbsent(
+ TestCompilationCacheKey.Standalone(testCaseId)
+ ) {
+ compilationFactory.testCasesToTestBundle(listOf(testCase), settings)
+ }
+ }
+ TestKind.REGULAR -> {
+ // Group regular test cases by compiler arguments.
+ val testRunnerType = testCase.extras<WithTestRunnerExtras>().runnerType
+ cachedXCTestCompilations.computeIfAbsent(
+ TestCompilationCacheKey.Grouped(
+ testCaseGroupId = testCaseId.testCaseGroupId,
+ freeCompilerArgs = testCase.freeCompilerArgs,
+ sharedModules = testCase.sharedModules,
+ runnerType = testRunnerType
+ )
+ ) {
+ val testCases = testCaseGroup.getRegularOnly(testCase.freeCompilerArgs, testCase.sharedModules, testRunnerType)
+ assertTrue(testCases.isNotEmpty())
+ compilationFactory.testCasesToTestBundle(testCases, settings)
+ }
+ }
+ else -> error("Test kind ${testCase.kind} is not supported yet in XCTest runner")
+ }
+
+ val compilationResult = testCompilation.result.assertSuccess() // <-- Compilation happens here.
+ // FIXME: temp adapter.
+ val adapter = TestCompilationResult.Success(Executable(compilationResult.resultingArtifact.bundleDir), compilationResult.loggedData)
+ val executable = TestExecutable.fromCompilationResult(testCase, adapter)
+
+ return action(testCase, executable)
+ }
+
private fun Collection<TestName>.filterIrrelevant(testCase: TestCase) =
if (testCase.kind == TestKind.REGULAR)
filter { testName -> testName.packageName.startsWith(testCase.nominalPackageName) }
diff --git a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/runner/TestRunners.kt b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/runner/TestRunners.kt
index 55301d5..060b56f 100644
--- a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/runner/TestRunners.kt
+++ b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/runner/TestRunners.kt
@@ -6,16 +6,14 @@
package org.jetbrains.kotlin.konan.blackboxtest.support.runner
import org.jetbrains.kotlin.konan.blackboxtest.support.TestName
+import org.jetbrains.kotlin.konan.blackboxtest.support.settings.*
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.ForcedNoopTestRunner
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.KotlinNativeHome
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.KotlinNativeTargets
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.Settings
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.Timeouts
-import org.jetbrains.kotlin.native.executors.Executor
-import org.jetbrains.kotlin.native.executors.EmulatorExecutor
-import org.jetbrains.kotlin.native.executors.XcodeSimulatorExecutor
import org.jetbrains.kotlin.konan.target.*
-import org.jetbrains.kotlin.native.executors.RosettaExecutor
+import org.jetbrains.kotlin.native.executors.*
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
import java.util.concurrent.ConcurrentHashMap
@@ -24,13 +22,26 @@
if (get<ForcedNoopTestRunner>().value) {
NoopTestRunner
} else with(get<KotlinNativeTargets>()) {
- if (testTarget == hostTarget) {
+ val nativeHome = get<KotlinNativeHome>()
+ val distribution = Distribution(nativeHome.dir.path)
+ val configurables = PlatformManager(distribution, true).platform(testTarget).configurables
+
+ if (get<XCTestRunner>().isEnabled) {
+ // Forcibly run tests with XCTest
+ check(configurables is AppleConfigurables) {
+ "Running tests with XCTest is not supported on non-Apple $configurables"
+ }
+ val executor = cached(
+ if (testTarget == hostTarget) {
+ XCTestHostExecutor(configurables)
+ } else {
+ XCTestSimulatorExecutor(configurables)
+ }
+ )
+ RunnerWithExecutor(executor, testRun)
+ } else if (testTarget == hostTarget) {
LocalTestRunner(testRun)
} else {
- val nativeHome = get<KotlinNativeHome>()
- val distribution = Distribution(nativeHome.dir.path)
- val configurables = PlatformManager(distribution, true).platform(testTarget).configurables
-
val executor = cached(
when {
configurables is ConfigurablesWithEmulator -> EmulatorExecutor(configurables)
diff --git a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/settings/TestProcessSettings.kt b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/settings/TestProcessSettings.kt
index aa5a8d4..d639041 100644
--- a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/settings/TestProcessSettings.kt
+++ b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/settings/TestProcessSettings.kt
@@ -290,3 +290,12 @@
DEFAULT,
NONE
}
+
+/**
+ * XCTestRunner setting.
+ *
+ * @param frameworksPath is a linker path to the developer frameworks location.
+ */
+internal class XCTestRunner(val frameworksPath: String) {
+ val isEnabled: Boolean = frameworksPath.isNotEmpty()
+}
diff --git a/repo/gradle-build-conventions/buildsrc-compat/src/main/kotlin/common-configuration.gradle.kts b/repo/gradle-build-conventions/buildsrc-compat/src/main/kotlin/common-configuration.gradle.kts
index 3fd30e4..8e362e3 100644
--- a/repo/gradle-build-conventions/buildsrc-compat/src/main/kotlin/common-configuration.gradle.kts
+++ b/repo/gradle-build-conventions/buildsrc-compat/src/main/kotlin/common-configuration.gradle.kts
@@ -138,10 +138,13 @@
}
// Workaround to avoid remote build cache misses due to absolute paths in relativePathBaseArg
- doFirst {
- if (relativePathBaseArg != null) {
- @Suppress("DEPRECATION")
- kotlinOptions.freeCompilerArgs += relativePathBaseArg
+ if (project.path != ":kotlin-native:utilities:xctest-runner") {
+ // In xctest-runner project
+ doFirst {
+ if (relativePathBaseArg != null) {
+ @Suppress("DEPRECATION")
+ kotlinOptions.freeCompilerArgs += relativePathBaseArg
+ }
}
}
}
diff --git a/repo/gradle-build-conventions/buildsrc-compat/src/main/kotlin/nativeTest.kt b/repo/gradle-build-conventions/buildsrc-compat/src/main/kotlin/nativeTest.kt
index 379b6e2..17f0404 100644
--- a/repo/gradle-build-conventions/buildsrc-compat/src/main/kotlin/nativeTest.kt
+++ b/repo/gradle-build-conventions/buildsrc-compat/src/main/kotlin/nativeTest.kt
@@ -6,7 +6,7 @@
import org.gradle.kotlin.dsl.project
import java.io.File
-private enum class TestProperty(shortName: String) {
+enum class TestProperty(shortName: String) {
// Use a separate Gradle property to pass Kotlin/Native home to tests: "kotlin.internal.native.test.nativeHome".
// Don't use "kotlin.native.home" and similar properties for this purpose, as these properties may have undesired
// effect on other Gradle tasks (ex: :kotlin-native:dist) that might be executed along with test task.
@@ -26,7 +26,8 @@
CACHE_MODE("cacheMode"),
EXECUTION_TIMEOUT("executionTimeout"),
SANITIZER("sanitizer"),
- TEAMCITY("teamcity");
+ TEAMCITY("teamcity"),
+ XCTEST_FRAMEWORK("xctest");
val fullName = "kotlin.internal.native.test.$shortName"
}
@@ -190,6 +191,7 @@
compute(CACHE_MODE)
compute(EXECUTION_TIMEOUT)
compute(SANITIZER)
+ compute(XCTEST_FRAMEWORK)
// Pass whether tests are running at TeamCity.
computePrivate(TEAMCITY) { kotlinBuildProperties.isTeamcityBuild.toString() }
diff --git a/settings.gradle b/settings.gradle
index fcb0827..2ab3216 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -850,6 +850,7 @@
include ':kotlin-native:Interop:Skia'
include ':kotlin-native:utilities:basic-utils'
include ':kotlin-native:utilities:cli-runner'
+ include ':kotlin-native:utilities:xctest-runner'
include ':kotlin-native:klib'
include ':kotlin-native:common'
include ':kotlin-native:runtime'