diff --git a/native/native.tests/build.gradle.kts b/native/native.tests/build.gradle.kts index 02c2ea3c2eb..2c4a7ac1fb9 100644 --- a/native/native.tests/build.gradle.kts +++ b/native/native.tests/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { testImplementation(project(":native:kotlin-native-utils")) testImplementation(project(":native:executors")) testImplementation(project(":kotlin-util-klib-abi")) + testImplementation(project(":native:swift:swift-export-standalone")) testImplementation(projectTests(":kotlin-util-klib-abi")) testApi(platform(libs.junit.bom)) testImplementation(libs.junit.jupiter.api) diff --git a/native/native.tests/testData/SwiftExport/smokes/smoke0.kt b/native/native.tests/testData/SwiftExport/smokes/smoke0.kt new file mode 100644 index 00000000000..25bb1365d6d --- /dev/null +++ b/native/native.tests/testData/SwiftExport/smokes/smoke0.kt @@ -0,0 +1,15 @@ +fun fooByte(): Byte { + return -1 +} + +fun fooShort(): Short { + return -1 +} + +fun fooInt(): Int { + return -1 +} + +fun fooLong(): Long { + return -1 +} \ No newline at end of file diff --git a/native/native.tests/testData/SwiftExport/smokes/smoke0.swift b/native/native.tests/testData/SwiftExport/smokes/smoke0.swift new file mode 100644 index 00000000000..40fa8aa180d --- /dev/null +++ b/native/native.tests/testData/SwiftExport/smokes/smoke0.swift @@ -0,0 +1,35 @@ +import Smokes + +extension Bool { + static func ^ (left: Bool, right: Bool) -> Bool { + return left != right + } +} + +func smoke() throws { + try assertEquals(actual: fooByte(), expected: -1) + try assertEquals(actual: fooShort(), expected: -1) + try assertEquals(actual: fooInt(), expected: -1) + try assertEquals(actual: fooLong(), expected: -1) + + try assertEquals(actual: org.kotlin.plus(a: 1, b: 2, c: 3), expected: 1 + 2 + 3) + + try assertEquals(actual: org.kotlin.logicalOr(a: true, b: true), expected: true || true) + try assertEquals(actual: org.kotlin.logicalOr(a: true, b: false), expected: true || false) + try assertEquals(actual: org.kotlin.logicalOr(a: false, b: false), expected: false || false) + + try assertEquals(actual: org.kotlin.xor(a: true, b: true), expected: true ^ true) + try assertEquals(actual: org.kotlin.xor(a: true, b: false), expected: true ^ false) + try assertEquals(actual: org.kotlin.xor(a: false, b: false), expected: false ^ false) +} + +class Smoke0Tests : TestProvider { + var tests: [TestCase] = [] + + init() { + providers.append(self) + tests = [ + TestCase(name: "Smokes", method: withAutorelease(smoke)), + ] + } +} \ No newline at end of file diff --git a/native/native.tests/testData/SwiftExport/smokes/smokes.kt b/native/native.tests/testData/SwiftExport/smokes/smokes.kt new file mode 100644 index 00000000000..a528aa859c9 --- /dev/null +++ b/native/native.tests/testData/SwiftExport/smokes/smokes.kt @@ -0,0 +1,8 @@ +package org.kotlin + +fun plus(a: Int, b: Int, c: Int) = + a + b + c + +fun logicalOr(a: Boolean, b: Boolean) = a || b + +fun xor(a: Boolean, b: Boolean) = a xor b \ No newline at end of file diff --git a/native/native.tests/tests-gen/org/jetbrains/kotlin/konan/test/blackbox/FirSwiftExportTestGenerated.java b/native/native.tests/tests-gen/org/jetbrains/kotlin/konan/test/blackbox/FirSwiftExportTestGenerated.java new file mode 100644 index 00000000000..d458b1ce4b2 --- /dev/null +++ b/native/native.tests/tests-gen/org/jetbrains/kotlin/konan/test/blackbox/FirSwiftExportTestGenerated.java @@ -0,0 +1,36 @@ +/* + * Copyright 2010-2024 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.konan.test.blackbox; + +import com.intellij.testFramework.TestDataPath; +import org.jetbrains.kotlin.test.util.KtTestUtil; +import org.junit.jupiter.api.Tag; +import org.jetbrains.kotlin.konan.test.blackbox.support.group.FirPipeline; +import org.jetbrains.kotlin.test.TestMetadata; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.regex.Pattern; + +/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.GenerateNativeTestsKt}. DO NOT MODIFY MANUALLY */ +@SuppressWarnings("all") +@TestMetadata("native/native.tests/testData/SwiftExport") +@TestDataPath("$PROJECT_ROOT") +@Tag("frontend-fir") +@FirPipeline() +public class FirSwiftExportTestGenerated extends AbstractNativeSwiftExportTest { + @Test + public void testAllFilesPresentInSwiftExport() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("native/native.tests/testData/SwiftExport"), Pattern.compile("^([^_](.+))$"), null, false); + } + + @Test + @TestMetadata("smokes") + public void testSmokes() throws Exception { + runTest("native/native.tests/testData/SwiftExport/smokes/"); + } +} diff --git a/native/native.tests/tests/org/jetbrains/kotlin/generators/tests/GenerateNativeTests.kt b/native/native.tests/tests/org/jetbrains/kotlin/generators/tests/GenerateNativeTests.kt index 45de31bb8db..ee6c0289569 100644 --- a/native/native.tests/tests/org/jetbrains/kotlin/generators/tests/GenerateNativeTests.kt +++ b/native/native.tests/tests/org/jetbrains/kotlin/generators/tests/GenerateNativeTests.kt @@ -499,6 +499,16 @@ fun main() { model("", extension = "swift", recursive = true) } } + testGroup("native/native.tests/tests-gen", "native/native.tests/testData") { + testClass( + suiteTestClassName = "FirSwiftExportTestGenerated", + annotations = listOf( + *frontendFir() + ), + ) { + model("SwiftExport", pattern = "^([^_](.+))$", recursive = false) + } + } } } diff --git a/native/native.tests/tests/org/jetbrains/kotlin/konan/test/blackbox/AbstractNativeSwiftExportTest.kt b/native/native.tests/tests/org/jetbrains/kotlin/konan/test/blackbox/AbstractNativeSwiftExportTest.kt new file mode 100644 index 00000000000..5ea2855c76b --- /dev/null +++ b/native/native.tests/tests/org/jetbrains/kotlin/konan/test/blackbox/AbstractNativeSwiftExportTest.kt @@ -0,0 +1,168 @@ +/* + * Copyright 2010-2024 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.konan.test.blackbox + +import com.intellij.testFramework.TestDataFile +import org.jetbrains.kotlin.konan.test.blackbox.support.PackageName +import org.jetbrains.kotlin.konan.test.blackbox.support.TestCase +import org.jetbrains.kotlin.konan.test.blackbox.support.TestCaseId +import org.jetbrains.kotlin.konan.test.blackbox.support.TestCompilerArgs +import org.jetbrains.kotlin.konan.test.blackbox.support.TestFile +import org.jetbrains.kotlin.konan.test.blackbox.support.TestKind +import org.jetbrains.kotlin.konan.test.blackbox.support.TestModule +import org.jetbrains.kotlin.konan.test.blackbox.support.TestName +import org.jetbrains.kotlin.konan.test.blackbox.support.compilation.SwiftCompilation +import org.jetbrains.kotlin.konan.test.blackbox.support.compilation.TestCompilationArtifact +import org.jetbrains.kotlin.konan.test.blackbox.support.compilation.TestCompilationFactory +import org.jetbrains.kotlin.konan.test.blackbox.support.compilation.TestCompilationResult.Companion.assertSuccess +import org.jetbrains.kotlin.konan.test.blackbox.support.runner.TestExecutable +import org.jetbrains.kotlin.konan.test.blackbox.support.runner.TestRunCheck +import org.jetbrains.kotlin.konan.test.blackbox.support.runner.TestRunChecks +import org.jetbrains.kotlin.konan.test.blackbox.support.settings.Timeouts +import org.jetbrains.kotlin.konan.test.blackbox.support.util.DEFAULT_MODULE_NAME +import org.jetbrains.kotlin.konan.test.blackbox.support.util.createModuleMap +import org.jetbrains.kotlin.konan.test.blackbox.support.util.createTestProvider +import org.jetbrains.kotlin.konan.test.blackbox.support.util.getAbsoluteFile +import org.jetbrains.kotlin.swiftexport.standalone.* +import org.jetbrains.kotlin.util.capitalizeDecapitalize.capitalizeAsciiOnly +import org.junit.jupiter.api.Assumptions +import org.junit.jupiter.api.Tag +import java.io.File + +@Tag("swiftexport") +abstract class AbstractNativeSwiftExportTest() : AbstractNativeSimpleTest() { + + private val testCompilationFactory = TestCompilationFactory() + private val testSuiteDir = File("native/native.tests/testData/framework") + + protected fun runTest(@TestDataFile testDir: String) { + Assumptions.assumeTrue(targets.testTarget.family.isAppleFamily) + val testPathFull = getAbsoluteFile(testDir) + val swiftExportOutput = runSwiftExport(testPathFull) + + val testName = testPathFull.name + val kotlinFiles = testPathFull.walk().filter { it.extension == "kt" }.map { testPathFull.resolve(it) }.toList() + val testCase = generateSwiftExportTestCase(testName, kotlinFiles + swiftExportOutput.kotlinBridges.toFile()) + val kotlinBinaryLibrary = testCompilationFactory.testCaseToBinaryLibrary( + testCase, testRunSettings, + kind = TestCompilationArtifact.BinaryLibrary.Kind.DYNAMIC, + ).result.assertSuccess().resultingArtifact + + val bridgeModuleFile = createModuleMap(buildDir, swiftExportOutput.cHeaderBridges.toFile()) + val swiftModuleName = testName.capitalizeAsciiOnly() + val swiftModule = compileSwiftModule(swiftModuleName, listOf(swiftExportOutput.swiftApi.toFile()), bridgeModuleFile, kotlinBinaryLibrary) + + val swiftTestFiles = testPathFull.walk().filter { it.extension == "swift" }.map { testPathFull.resolve(it) }.toList() + val testExecutable = compileTestExecutable(testName, swiftTestFiles, swiftModule.rootDir, swiftModuleName, bridgeModuleFile) + runExecutableAndVerify(testCase, testExecutable) + } + + private fun runSwiftExport(testPathFull: File): SwiftExportOutput { + val swiftExportInput = SwiftExportInput( + testPathFull.toPath(), + libraries = emptyList() + ) + val exportResultsPath = buildDir.toPath().resolve("swift_export_results") + val swiftExportOutput = SwiftExportOutput( + swiftApi = exportResultsPath.resolve("result.swift"), + kotlinBridges = exportResultsPath.resolve("result.kt"), + cHeaderBridges = exportResultsPath.resolve("result.h"), + ) + val swiftExportConfig = SwiftExportConfig( + settings = mapOf( + SwiftExportConfig.BRIDGE_MODULE_NAME to SwiftExportConfig.DEFAULT_BRIDGE_MODULE_NAME, + ), + logger = createDummyLogger() + ) + runSwiftExport(swiftExportInput, swiftExportConfig, swiftExportOutput) + return swiftExportOutput + } + + private fun compileSwiftModule( + swiftModuleName: String, + sources: List, + moduleMap: File, + binaryLibrary: TestCompilationArtifact.BinaryLibrary, + ): TestCompilationArtifact.Swift.Module { + val swiftModuleDir = buildDir.resolve("SwiftModules").resolve(swiftModuleName).also { it.mkdirs() } + val binaryLibraryName = binaryLibrary.libraryFile.nameWithoutExtension.substringAfter("lib") + return SwiftCompilation( + testRunSettings, + sources = sources, + TestCompilationArtifact.Swift.Module( + rootDir = swiftModuleDir, + moduleName = swiftModuleName + ), + swiftExtraOpts = listOf( + "-Xcc", "-fmodule-map-file=${moduleMap.absolutePath}", + "-L", binaryLibrary.libraryFile.parentFile.absolutePath, + "-l$binaryLibraryName", + "-emit-module", "-parse-as-library", "-emit-library", "-enable-library-evolution", + "-module-name", swiftModuleName, + ), + outputFile = { it.binaryLibrary }, + ).result.assertSuccess().resultingArtifact + } + + private fun compileTestExecutable( + testName: String, + testSources: List, + swiftModuleDir: File, + binaryLibraryName: String, + moduleMap: File, + ): TestExecutable { + val swiftExtraOpts = listOf( + "-I", swiftModuleDir.absolutePath, + "-L", swiftModuleDir.absolutePath, + "-l$binaryLibraryName", + "-Xcc", "-fmodule-map-file=${moduleMap.absolutePath}", + ) + val provider = createTestProvider(buildDir, testSources) + val success = SwiftCompilation( + testRunSettings, + testSources + listOf( + provider, + testSuiteDir.resolve("main.swift") + ), + TestCompilationArtifact.Executable(buildDir.resolve("swiftTestExecutable")), + swiftExtraOpts, + outputFile = { executable -> executable.executableFile } + ).result.assertSuccess() + return TestExecutable( + success.resultingArtifact, + success.loggedData, + listOf(TestName(testName)) + ) + } + + private fun generateSwiftExportTestCase(testName: String, sources: List): TestCase { + val module = TestModule.Exclusive(DEFAULT_MODULE_NAME, emptySet(), emptySet(), emptySet()) + sources.forEach { module.files += TestFile.createCommitted(it, module) } + + return TestCase( + id = TestCaseId.Named(testName), + kind = TestKind.STANDALONE_NO_TR, + modules = setOf(module), + freeCompilerArgs = TestCompilerArgs( + listOf( + "-opt-in", "kotlin.experimental.ExperimentalNativeApi", + "-opt-in", "kotlinx.cinterop.ExperimentalForeignApi" + ) + ), + nominalPackageName = PackageName(testName), + checks = TestRunChecks( + executionTimeoutCheck = TestRunCheck.ExecutionTimeout.ShouldNotExceed(testRunSettings.get().executionTimeout), + exitCodeCheck = TestRunCheck.ExitCode.Expected(0), + outputDataFile = null, + outputMatcher = null, + fileCheckMatcher = null, + ), + extras = TestCase.NoTestRunnerExtras(entryPoint = "main") + ).apply { + initialize(null, null) + } + } +} \ No newline at end of file diff --git a/native/native.tests/tests/org/jetbrains/kotlin/konan/test/blackbox/support/compilation/TestCompilationArtifact.kt b/native/native.tests/tests/org/jetbrains/kotlin/konan/test/blackbox/support/compilation/TestCompilationArtifact.kt index 36871a8c916..df7abcd6c7d 100644 --- a/native/native.tests/tests/org/jetbrains/kotlin/konan/test/blackbox/support/compilation/TestCompilationArtifact.kt +++ b/native/native.tests/tests/org/jetbrains/kotlin/konan/test/blackbox/support/compilation/TestCompilationArtifact.kt @@ -57,4 +57,11 @@ internal sealed interface TestCompilationArtifact { return expectedFile.takeIf { it.exists() } } } + + sealed interface Swift : TestCompilationArtifact { + class Module(val rootDir: File, val moduleName: String) : Swift { + override val logFile: File get() = rootDir.resolve("$moduleName.log") + val binaryLibrary: File get() = rootDir.resolve("lib$moduleName.dylib") + } + } }