From b5cb1fddff8b434bbd313b27c5c07a679e0c2731 Mon Sep 17 00:00:00 2001 From: Sergey Bogolepov Date: Fri, 9 Feb 2024 15:50:51 +0200 Subject: [PATCH] [KT-65126] Initial version of Swift export integration tests Here we reuse infrastructure from existing ObjC Framework tests. It is not the prettiest approach, and ideally we would like to use XCTest, but for now we need to quickly kick-start end-to-end tests. --- native/native.tests/build.gradle.kts | 1 + .../testData/SwiftExport/smokes/smoke0.kt | 15 ++ .../testData/SwiftExport/smokes/smoke0.swift | 35 ++++ .../testData/SwiftExport/smokes/smokes.kt | 8 + .../blackbox/FirSwiftExportTestGenerated.java | 36 ++++ .../generators/tests/GenerateNativeTests.kt | 10 ++ .../blackbox/AbstractNativeSwiftExportTest.kt | 168 ++++++++++++++++++ .../compilation/TestCompilationArtifact.kt | 7 + 8 files changed, 280 insertions(+) create mode 100644 native/native.tests/testData/SwiftExport/smokes/smoke0.kt create mode 100644 native/native.tests/testData/SwiftExport/smokes/smoke0.swift create mode 100644 native/native.tests/testData/SwiftExport/smokes/smokes.kt create mode 100644 native/native.tests/tests-gen/org/jetbrains/kotlin/konan/test/blackbox/FirSwiftExportTestGenerated.java create mode 100644 native/native.tests/tests/org/jetbrains/kotlin/konan/test/blackbox/AbstractNativeSwiftExportTest.kt 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") + } + } }