diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsGitIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsGitIT.kt index af8633035ac..14b07bdbea0 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsGitIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsGitIT.kt @@ -19,7 +19,6 @@ import org.gradle.util.GradleVersion import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_BUILD_TASK_NAME import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_GEN_TASK_NAME import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_IMPORT_TASK_NAME -import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_INSTALL_TASK_NAME import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_SETUP_BUILD_TASK_NAME import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_SPEC_TASK_NAME import org.jetbrains.kotlin.gradle.testbase.* @@ -31,10 +30,8 @@ import org.junit.jupiter.api.Assumptions import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.condition.OS -import java.nio.file.Path import java.util.* import kotlin.io.path.absolutePathString -import kotlin.io.path.readText import kotlin.test.assertTrue @OsCondition(supportedOn = [OS.MAC], enabledOnCI = [OS.MAC]) @@ -66,7 +63,6 @@ class CocoaPodsGitIT : KGPBaseTest() { private val podGenTaskName = ":$POD_GEN_TASK_NAME" private val podSetupBuildTaskName = ":$POD_SETUP_BUILD_TASK_NAME" private val podBuildTaskName = ":$POD_BUILD_TASK_NAME" - private val podInstallTaskName = ":$POD_INSTALL_TASK_NAME" private val defaultPodInstallSyntheticTaskName = ":podInstallSyntheticIos" private val defaultPodGenTaskName = podGenFullTaskName() @@ -516,34 +512,6 @@ class CocoaPodsGitIT : KGPBaseTest() { """.trimMargin() } - private fun BuildResult.podImportAsserts( - buildScript: Path, - projectName: String? = null, - ) { - - val buildScriptText = buildScript.readText() - val taskPrefix = projectName?.let { ":$it" } ?: "" - val podspec = "podspec" - - if ("noPodspec()" in buildScriptText) { - assertTasksSkipped("$taskPrefix:$podspec") - } - - if ("podfile" in buildScriptText) { - assertTasksExecuted("$taskPrefix$podInstallTaskName") - } else { - assertTasksSkipped("$taskPrefix$podInstallTaskName") - } - if (buildScriptText.matches("pod\\(.*\\)".toRegex())) { - assertTasksExecuted(listOf("$taskPrefix:$POD_GEN_TASK_NAME")) - } - - with(listOf(POD_SETUP_BUILD_TASK_NAME, POD_BUILD_TASK_NAME).map { "$taskPrefix:$it" }) { - if (buildScriptText.matches("pod\\(.*\\)".toRegex())) { - assertTasksExecuted(this) - } - } - } private fun isRepoAvailable(repos: List) = runBlocking { HttpClient(CIO).use { client -> diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsIT.kt index 2615a0df15f..4054843ef81 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsIT.kt @@ -1,449 +1,379 @@ /* - * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * 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.gradle.native -import org.jetbrains.kotlin.gradle.* +import org.gradle.testkit.runner.BuildResult +import org.gradle.util.GradleVersion import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.DUMMY_FRAMEWORK_TASK_NAME import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_IMPORT_TASK_NAME -import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_INSTALL_TASK_NAME import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_SPEC_TASK_NAME -import org.jetbrains.kotlin.gradle.testbase.ImportMode -import org.jetbrains.kotlin.gradle.testbase.cocoaPodsEnvironmentVariables -import org.jetbrains.kotlin.gradle.testbase.ensureCocoapodsInstalled -import org.jetbrains.kotlin.gradle.testbase.TestVersions.Gradle.G_8_1 -import org.jetbrains.kotlin.gradle.transformProjectWithPluginsDsl -import org.jetbrains.kotlin.gradle.util.modify +import org.jetbrains.kotlin.gradle.testbase.* +import org.jetbrains.kotlin.gradle.util.assertProcessRunResult +import org.jetbrains.kotlin.gradle.util.replaceText import org.jetbrains.kotlin.gradle.util.runProcess -import org.jetbrains.kotlin.konan.target.HostManager -import org.junit.Assume.assumeTrue -import org.junit.Before -import org.junit.BeforeClass -import org.junit.Test +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.condition.OS import java.io.File -import java.util.* -import java.util.concurrent.TimeUnit import java.util.zip.ZipFile +import kotlin.io.path.* import kotlin.test.* -class CocoaPodsIT : BaseGradleIT() { +@OsCondition(supportedOn = [OS.MAC], enabledOnCI = [OS.MAC]) +@DisplayName("K/N cocoapods tests") +@NativeGradlePluginTests +@GradleTestVersions(minVersion = TestVersions.Gradle.G_7_0) +@OptIn(EnvironmentalVariablesOverride::class) +class CocoaPodsIT : KGPBaseTest() { - override val defaultGradleVersion: GradleVersionRequired - get() = GradleVersionRequired.FOR_MPP_SUPPORT - - // We use Kotlin DSL. Earlier Gradle versions fail at accessors codegen. - private val gradleVersion = GradleVersionRequired.FOR_MPP_SUPPORT - - override fun defaultBuildOptions(): BuildOptions = - super.defaultBuildOptions().copy(customEnvironmentVariables = cocoaPodsEnvironmentVariables()) - - private val podfileImportDirectivePlaceholder = "" private val podfileImportPodPlaceholder = "#import_pod_directive" private val cocoapodsSingleKtPod = "native-cocoapods-single" private val cocoapodsMultipleKtPods = "native-cocoapods-multiple" - private val templateProjectName = "native-cocoapods-template" private val cocoapodsTestsProjectName = "native-cocoapods-tests" private val cocoapodsCommonizationProjectName = "native-cocoapods-commonization" + private val cocoapodsDependantPodsProjectName = "native-cocoapods-dependant-pods" private val dummyTaskName = ":$DUMMY_FRAMEWORK_TASK_NAME" private val podspecTaskName = ":$POD_SPEC_TASK_NAME" private val podImportTaskName = ":$POD_IMPORT_TASK_NAME" - private val podInstallTaskName = ":$POD_INSTALL_TASK_NAME" + private val podInstallTaskName = ":${KotlinCocoapodsPlugin.POD_INSTALL_TASK_NAME}" private val defaultPodName = "AFNetworking" - private val defaultPodInstallSyntheticTaskName = ":podInstallSyntheticIos" - private lateinit var hooks: CustomHooks - private lateinit var project: BaseGradleIT.Project - - @Before - fun configure() { - hooks = CustomHooks() - project = getProjectByName(templateProjectName).apply { - preparePodfile("ios-app", ImportMode.FRAMEWORKS) - } + @BeforeAll + fun setUp() { + ensureCocoapodsInstalled() } - @Test - fun testPodImportSingle() { - val project = getProjectByName(cocoapodsSingleKtPod) + @DisplayName("Pod import single") + @GradleTest + fun testPodImportSingle(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(cocoapodsSingleKtPod, gradleVersion) { - project.preparePodfile("ios-app", ImportMode.FRAMEWORKS) - - project.testImportWithAsserts() - - hooks.rewriteHooks { - podImportAsserts("kotlin-library") - } - project.testSynthetic(":kotlin-library:podImport") - } - - @Test - fun testPodImportMultiple() { - val project = getProjectByName(cocoapodsMultipleKtPods) - - project.preparePodfile("ios-app", ImportMode.FRAMEWORKS) - - project.testImportWithAsserts() - - hooks.rewriteHooks { - podImportAsserts("kotlin-library") - } - project.testSynthetic(":kotlin-library:podImport") - - hooks.rewriteHooks { - podImportAsserts("second-library") - } - project.testSynthetic(":second-library:podImport") - } - - @Test - fun testSyntheticProjectPodfileGeneration() { - val gradleProject = transformProjectWithPluginsDsl(cocoapodsSingleKtPod, gradleVersion) - gradleProject.gradleBuildScript().appendToCocoapodsBlock( - """ - ios.deploymentTarget = "14.1" - pod("SSZipArchive") - pod("AFNetworking", "~> 4.0.1") - pod("Alamofire") { - source = git("https://github.com/Alamofire/Alamofire.git") { - tag = "5.6.1" - } + buildWithCocoapodsWrapper(podImportTaskName) { + podImportAsserts(buildGradleKts) } - """.trimIndent() - ) - gradleProject.build("podInstallSyntheticIos", "-Pkotlin.native.cocoapods.generate.wrapper=true") { - assertSuccessful() - assertTasksExecuted(":podGenIos") - val podfileText = gradleProject.projectDir.resolve("build/cocoapods/synthetic/ios/Podfile").readText().trim() - assertTrue(podfileText.contains("platform :ios, '14.1'")) - assertTrue(podfileText.contains("pod 'SSZipArchive'")) - assertTrue(podfileText.contains("pod 'AFNetworking', '~> 4.0.1'")) - assertTrue(podfileText.contains("pod 'Alamofire', :git => 'https://github.com/Alamofire/Alamofire.git', :tag => '5.6.1'")) - assertTrue(podfileText.contains("config.build_settings['EXPANDED_CODE_SIGN_IDENTITY'] = \"\"")) - assertTrue(podfileText.contains("config.build_settings['CODE_SIGNING_REQUIRED'] = \"NO\"")) - assertTrue(podfileText.contains("config.build_settings['CODE_SIGNING_ALLOWED'] = \"NO\"")) - } - } - - @Test - fun testSyntheticProjectPodfilePostprocessing() { - project.gradleBuildScript().apply { - appendToCocoapodsBlock("""pod("ChatSDK", version = "5.2.1")""") - - appendText( - """ - - tasks.withType().configureEach { - doLast { - podfile.get().appendText("ENV['SWIFT_VERSION'] = '5'") - } - } - """.trimIndent() - ) - } - - project.build("podInstallSyntheticIos", "-Pkotlin.native.cocoapods.generate.wrapper=true") { - assertSuccessful() - assertFileContains(path = "build/cocoapods/synthetic/ios/Podfile", "ENV['SWIFT_VERSION'] = '5'") - } - } - - @Test - fun warnIfDeprecatedPodspecPathIsUsed() { - project = getProjectByName(cocoapodsSingleKtPod) - hooks.addHook { - assertContains( - listOf("Deprecated DSL found on ${project.projectDir.absolutePath}", "kotlin-library", "build.gradle.kts") - .joinToString(separator = File.separator) - ) - } - project.test(":kotlin-library:tasks") - } - - @Test - fun errorIfVersionIsNotSpecified() { - with(project.gradleBuildScript()) { - useLines { lines -> - lines.filter { line -> "version = \"1.0\"" !in line }.joinToString(separator = "\n") - }.also { writeText(it) } - } - hooks.addHook { - assertContains("Cocoapods Integration requires pod version to be specified.") - } - - project.build(POD_IMPORT_TASK_NAME, "-Pkotlin.native.cocoapods.generate.wrapper=true") { - assertFailed() - hooks.trigger(this) - } - } - - // up-to-date tests - - @Test - fun testDummyUTD() { - hooks.addHook { - assertTasksExecuted(dummyTaskName) - } - project.testWithWrapper(dummyTaskName) - - hooks.rewriteHooks { - assertTasksUpToDate(dummyTaskName) - } - project.testWithWrapper(dummyTaskName) - } - - @Test - fun testImportUTDAfterLinkingFramework() { - val linkTaskName = ":linkPodDebugFrameworkIOS" - project.gradleBuildScript().appendToCocoapodsBlock( - """ - framework { - baseName = "kotlin-library" + buildWithCocoapodsWrapper(":kotlin-library:podImport") { + podImportAsserts(subProject("kotlin-library").buildGradleKts, "kotlin-library") } - name = "kotlin-library" - podfile = project.file("ios-app/Podfile") - """.trimIndent() - ) - - - hooks.addHook { - assertTasksExecuted(dummyTaskName) - assertTasksExecuted(podInstallTaskName) } - project.testImport() - - hooks.rewriteHooks { - assertTasksExecuted(linkTaskName) - } - project.testWithWrapper(linkTaskName) - - hooks.rewriteHooks { - assertTasksUpToDate(dummyTaskName) - assertTasksUpToDate(podInstallTaskName) - } - project.testImport() } + @DisplayName("Pod import multiple") + @GradleTest + fun testPodImportMultiple(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(cocoapodsMultipleKtPods, gradleVersion) { - @Test - fun testChangeFrameworkTypeUTD() { - project.gradleBuildScript().appendToCocoapodsBlock( - """ - framework { - baseName = "kotlin-library" + buildWithCocoapodsWrapper(podImportTaskName) { + podImportAsserts(buildGradleKts) } - name = "kotlin-library" - podfile = project.file("ios-app/Podfile") - """.trimIndent() - ) - hooks.addHook { - assertTasksExecuted(dummyTaskName) - assertTasksExecuted(podInstallTaskName) + buildWithCocoapodsWrapper(":kotlin-library:podImport") { + podImportAsserts(subProject("kotlin-library").buildGradleKts, "kotlin-library") + } + + buildWithCocoapodsWrapper(":second-library:podImport") { + podImportAsserts(subProject("second-library").buildGradleKts, "second-library") + } } - project.testImport() - - hooks.rewriteHooks { - assertTasksUpToDate(dummyTaskName) - assertTasksUpToDate(podInstallTaskName) - } - project.testImport() - - project.gradleBuildScript().appendToFrameworkBlock("isStatic = true") - - hooks.rewriteHooks { - assertTasksExecuted(dummyTaskName) - assertTasksExecuted(podInstallTaskName) - } - project.testImport() - - hooks.rewriteHooks { - assertTasksUpToDate(dummyTaskName) - assertTasksUpToDate(podInstallTaskName) - } - project.testImport() } - @Test - fun testPodInstallInvalidatesUTD() { - with(project.gradleBuildScript()) { - addPod("AFNetworking") - } - - hooks.addHook { - assertTasksExecuted(defaultPodInstallSyntheticTaskName) - assertTrue { fileInWorkingDir("build/cocoapods/synthetic/ios/Pods/AFNetworking").deleteRecursively() } - } - project.testSynthetic(defaultPodInstallSyntheticTaskName) - - hooks.rewriteHooks { - assertTasksExecuted(defaultPodInstallSyntheticTaskName) - } - project.testSynthetic(defaultPodInstallSyntheticTaskName) - } - - @Test - fun testUTDPodspec() { - project.testWithWrapper(podspecTaskName) - hooks.addHook { - assertTasksExecuted(podspecTaskName) - } - with(project.gradleBuildScript()) { - appendToCocoapodsBlock("license = \"new license name\"") - } - project.testWithWrapper(podspecTaskName) - with(project.gradleBuildScript()) { - appendToCocoapodsBlock("license = \"new license name\"") - } - hooks.rewriteHooks { - assertTasksUpToDate(podspecTaskName) - } - project.testWithWrapper(podspecTaskName) - } - - @Test - fun testUTDPodspecDeploymentTarget() { - project.testWithWrapper(podspecTaskName) - hooks.addHook { - assertTasksExecuted(podspecTaskName) - } - with(project.gradleBuildScript()) { - appendToCocoapodsBlock("ios.deploymentTarget = \"12.5\"") - } - project.testWithWrapper(podspecTaskName) - hooks.rewriteHooks { - assertTasksUpToDate(podspecTaskName) - } - project.testWithWrapper(podspecTaskName) - } - - - @Test - fun testPodInstallWithoutPodFile() { - project.testSynthetic(podInstallTaskName) - } - - @Test - fun supportPodsWithDependencies() { - with(project.gradleBuildScript()) { - addPod("AlamofireImage") - } - project.testImportWithAsserts() - } - - @Test - fun testCustomPackageName() { - with(project.gradleBuildScript()) { - addPod("AFNetworking", "packageName = \"AFNetworking\"") - } - with(project) { - File(projectDir, "src/iosMain/kotlin/A.kt").modify { - it.replace( - "fun foo() {", """ - import AFNetworking - fun foo() { - """.trimIndent() + @DisplayName("Checking the warning about using deprecated podspec path") + @GradleTest + fun warnIfDeprecatedPodspecPathIsUsed(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(cocoapodsSingleKtPod, gradleVersion) { + build(":kotlin-library:tasks") { + assertOutputContains( + listOf("Deprecated DSL found on ${projectPath.toRealPath().absolutePathString()}", "kotlin-library", "build.gradle.kts") + .joinToString(separator = File.separator) ) - it.replace("println(\"hi!\")", "println(AFNetworking.AFNetworkingReachabilityNotificationStatusItem)") } - - testWithWrapper("assemble") } } - @Test - fun testCinteropExtraOpts() { - with(project) { - gradleBuildScript().addPod("AFNetworking", "extraOpts = listOf(\"-help\")") - hooks.addHook { - assertContains("Usage: cinterop options_list") + @DisplayName("Build with error if project version is not specified for cocoapods") + @GradleTest + fun errorIfVersionIsNotSpecified(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { + val filteredBuildScript = buildGradleKts.useLines { lines -> + lines.filter { line -> "version = \"1.0\"" !in line }.joinToString(separator = "\n") } - testWithWrapper("cinteropAFNetworkingIOS") - } - } + buildGradleKts.writeText(filteredBuildScript) - @Test - fun testCocoapodsWithRegularFrameworkDefinition() { - with(project) { - gradleBuildScript().appendToKotlinBlock("iosX64(\"iOS\") {binaries.framework{}}") - testImport() - } - } - - @Test - fun testSyncFramework() { - with(project) { - hooks.addHook { - assertTasksExecuted(":linkPodDebugFrameworkIOS") - assertTrue(fileInWorkingDir("build/cocoapods/framework/cocoapods.framework/cocoapods").exists()) - } - test( - "syncFramework", - "-Pkotlin.native.cocoapods.platform=iphonesimulator", - "-Pkotlin.native.cocoapods.archs=x86_64", - "-Pkotlin.native.cocoapods.configuration=Debug" + val buildOptions = this.buildOptions.copy( + nativeOptions = this.buildOptions.nativeOptions.copy( + cocoapodsGenerateWrapper = true + ) ) + + buildAndFail(POD_IMPORT_TASK_NAME, buildOptions = buildOptions) { + assertOutputContains("Cocoapods Integration requires pod version to be specified.") + } } } - @Test - fun testSyncFrameworkCustomXcodeConfiguration() { - with(project) { - gradleBuildScript().appendToCocoapodsBlock("xcodeConfigurationToNativeBuildType[\"CUSTOM\"] = org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType.DEBUG\n") - hooks.addHook { - assertTasksExecuted(":linkPodDebugFrameworkIOS") - assertTrue(fileInWorkingDir("build/cocoapods/framework/cocoapods.framework/cocoapods").exists()) + @DisplayName("Dummy UTD") + @GradleTest + fun testDummyUTD(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { + + buildWithCocoapodsWrapper(dummyTaskName) { + assertTasksExecuted(dummyTaskName) } - test( - "syncFramework", - "-Pkotlin.native.cocoapods.platform=iphonesimulator", - "-Pkotlin.native.cocoapods.archs=x86_64", - "-Pkotlin.native.cocoapods.configuration=CUSTOM" + buildWithCocoapodsWrapper(dummyTaskName) { + assertTasksUpToDate(dummyTaskName) + } + } + } + + @DisplayName("UTD after linking framework") + @GradleTest + fun testImportUTDAfterLinkingFramework(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { + val linkTaskName = ":linkPodDebugFrameworkIOS" + + buildGradleKts.addCocoapodsBlock( + """ + framework { + baseName = "kotlin-library" + } + name = "kotlin-library" + podfile = project.file("ios-app/Podfile") + """.trimIndent() ) - } - } - @Test - fun testSyncFrameworkInvalidArch() { - with(project) { - build( - "syncFramework", - "-Pkotlin.native.cocoapods.platform=iphoneos", - "-Pkotlin.native.cocoapods.archs=x86_64", - "-Pkotlin.native.cocoapods.configuration=Debug" - ) { - assertFailed() - assertContains("Architecture x86_64 is not supported for platform iphoneos") + buildWithCocoapodsWrapper(podImportTaskName) { + assertTasksExecuted(dummyTaskName) + assertTasksExecuted(podInstallTaskName) + } + + buildWithCocoapodsWrapper(linkTaskName) { + assertTasksExecuted(linkTaskName) + } + + buildWithCocoapodsWrapper(podImportTaskName) { + assertTasksUpToDate(dummyTaskName) + assertTasksUpToDate(podInstallTaskName) } } } - @Test - fun testSyncFrameworkMultiplePlatforms() { - with(project) { - build( - "syncFramework", - "-Pkotlin.native.cocoapods.platform=iphoneos iphonesimulator", - "-Pkotlin.native.cocoapods.archs=arm64", - "-Pkotlin.native.cocoapods.configuration=Debug" - ) { - assertFailed() - assertContains("kotlin.native.cocoapods.platform has to contain a single value only.") + @DisplayName("Changing framework type and checks UTD") + @GradleTest + fun testChangeFrameworkTypeUTD(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { + buildGradleKts.addCocoapodsBlock( + """ + framework { + baseName = "kotlin-library" + } + name = "kotlin-library" + podfile = project.file("ios-app/Podfile") + """.trimIndent() + ) + + buildWithCocoapodsWrapper(podImportTaskName) { + assertTasksExecuted(dummyTaskName) + assertTasksExecuted(podInstallTaskName) + } + + buildWithCocoapodsWrapper(podImportTaskName) { + assertTasksUpToDate(dummyTaskName) + assertTasksUpToDate(podInstallTaskName) + } + + buildGradleKts.addFrameworkBlock("isStatic = true") + buildWithCocoapodsWrapper(podImportTaskName) { + assertTasksExecuted(dummyTaskName) + assertTasksExecuted(podInstallTaskName) + } + + buildWithCocoapodsWrapper(podImportTaskName) { + assertTasksUpToDate(dummyTaskName) + assertTasksUpToDate(podInstallTaskName) + } + + } + } + + @DisplayName("UTD podspec") + @GradleTest + fun testUTDPodspec(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { + + buildWithCocoapodsWrapper(podspecTaskName) + + buildGradleKts.addCocoapodsBlock("license = \"new license name\"") + buildWithCocoapodsWrapper(podspecTaskName) { + assertTasksExecuted(podspecTaskName) + } + + buildGradleKts.addCocoapodsBlock("license = \"new license name\"") + buildWithCocoapodsWrapper(podspecTaskName) { + assertTasksUpToDate(podspecTaskName) } } } - @Test - fun testSyncFrameworkMultipleArchitecturesWithCustomName() { - with(project) { + @DisplayName("UTD with podspec deployment target") + @GradleTest + fun testUTDPodspecDeploymentTarget(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { + + buildWithCocoapodsWrapper(podspecTaskName) + + buildGradleKts.addCocoapodsBlock("ios.deploymentTarget = \"12.5\"") + buildWithCocoapodsWrapper(podspecTaskName) { + assertTasksExecuted(podspecTaskName) + } + + buildWithCocoapodsWrapper(podspecTaskName) { + assertTasksUpToDate(podspecTaskName) + } + } + } + + @DisplayName("Installing pod without pod file") + @GradleTest + fun testPodInstallWithoutPodFile(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { + buildWithCocoapodsWrapper(podInstallTaskName) + } + } + + @DisplayName("Pods with dependencies support") + @GradleTest + fun supportPodsWithDependencies(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { + buildGradleKts.addPod("AlamofireImage") + + buildWithCocoapodsWrapper(podImportTaskName) { + podImportAsserts(buildGradleKts) + } + } + } + + @DisplayName("Custom package name") + @GradleTest + fun testCustomPackageName(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { + + buildGradleKts.addPod("AFNetworking", "packageName = \"AFNetworking\"") + val srcFileForChanging = projectPath.resolve("src/iosMain/kotlin/A.kt") + srcFileForChanging.replaceText( + "println(\"hi!\")", "println(AFNetworking.AFNetworkingReachabilityNotificationStatusItem)" + ) + buildWithCocoapodsWrapper("assemble") + } + } + + @DisplayName("Cinterop extra opts") + @GradleTest + fun testCinteropExtraOpts(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { + buildGradleKts.addPod("AFNetworking", "extraOpts = listOf(\"-help\")") + buildWithCocoapodsWrapper("cinteropAFNetworkingIOS") { + assertOutputContains("Usage: cinterop options_list") + } + } + } + + @DisplayName("Cocoapods with regular framework definition") + @GradleTest + fun testCocoapodsWithRegularFrameworkDefinition(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { + buildGradleKts.addKotlinBlock("iosX64(\"iOS\") {binaries.framework{}}") + buildWithCocoapodsWrapper(podImportTaskName) + } + } + + @DisplayName("Checking sync framework") + @GradleTest + fun testSyncFramework(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { + + val buildOptions = this.buildOptions.copy( + nativeOptions = this.buildOptions.nativeOptions.copy( + cocoapodsPlatform = "iphonesimulator", + cocoapodsArchs = "x86_64", + cocoapodsConfiguration = "Debug" + ) + ) + build("syncFramework", buildOptions = buildOptions) { + assertTasksExecuted(":linkPodDebugFrameworkIOS") + assertFileInProjectExists("build/cocoapods/framework/cocoapods.framework/cocoapods") + } + } + } + + @DisplayName("Sync framework with custom Xcode configuration") + @GradleTest + fun testSyncFrameworkCustomXcodeConfiguration(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { + buildGradleKts.addCocoapodsBlock("xcodeConfigurationToNativeBuildType[\"CUSTOM\"] = org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType.DEBUG\n") + val buildOptions = this.buildOptions.copy( + nativeOptions = this.buildOptions.nativeOptions.copy( + cocoapodsPlatform = "iphonesimulator", + cocoapodsArchs = "x86_64", + cocoapodsConfiguration = "CUSTOM" + ) + ) + build("syncFramework", buildOptions = buildOptions) { + assertTasksExecuted(":linkPodDebugFrameworkIOS") + assertFileInProjectExists(("build/cocoapods/framework/cocoapods.framework/cocoapods")) + } + } + } + + @DisplayName("Checking sync framework with invalid platform") + @GradleTest + fun testSyncFrameworkInvalidArch(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { + + val buildOptions = this.buildOptions.copy( + nativeOptions = this.buildOptions.nativeOptions.copy( + cocoapodsPlatform = "iphoneos", + cocoapodsArchs = "x86_64", + cocoapodsConfiguration = "Debug" + ) + ) + buildAndFail("syncFramework", buildOptions = buildOptions) { + assertOutputContains("Architecture x86_64 is not supported for platform iphoneos") + } + } + } + + @DisplayName("Checking sync framework with multiple platforms") + @GradleTest + fun testSyncFrameworkMultiplePlatforms(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { + + val buildOptions = this.buildOptions.copy( + nativeOptions = this.buildOptions.nativeOptions.copy( + cocoapodsPlatform = "iphoneos iphonesimulator", + cocoapodsArchs = "arm64", + cocoapodsConfiguration = "Debug" + ) + ) + buildAndFail("syncFramework", buildOptions = buildOptions) { + assertOutputContains("kotlin.native.cocoapods.platform has to contain a single value only.") + } + } + } + + @DisplayName("Sync framework multiple achitectures with custom name") + @GradleTest + fun testSyncFrameworkMultipleArchitecturesWithCustomName(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { val frameworkName = "customSdk" - gradleBuildScript().appendText( + buildGradleKts.appendText( """ | |kotlin { @@ -456,32 +386,32 @@ class CocoaPodsIT : BaseGradleIT() { |} """.trimMargin() ) - hooks.addHook { + val buildOptions = this.buildOptions.copy( + nativeOptions = this.buildOptions.nativeOptions.copy( + cocoapodsPlatform = "iphonesimulator", + cocoapodsArchs = "arm64 x86_64", + cocoapodsConfiguration = "Debug", + cocoapodsGenerateWrapper = true + ) + ) + + build("syncFramework", buildOptions = buildOptions) { // Check that an output framework is a dynamic framework - val framework = fileInWorkingDir("build/cocoapods/framework/$frameworkName.framework/$frameworkName") - with(runProcess(listOf("file", framework.absolutePath), projectDir)) { - assertTrue(isSuccessful) - assertTrue(output.contains("universal binary with 2 architectures")) - assertTrue(output.contains("(for architecture x86_64)")) - assertTrue(output.contains("(for architecture arm64)")) + val framework = projectPath.resolve("build/cocoapods/framework/$frameworkName.framework/$frameworkName") + assertProcessRunResult(runProcess(listOf("file", framework.absolutePathString()), projectPath.toFile())) { + assertOutputContains("universal binary with 2 architectures") + assertOutputContains("(for architecture x86_64)") + assertOutputContains("(for architecture arm64)") } } - - test( - "syncFramework", - "-Pkotlin.native.cocoapods.platform=iphonesimulator", - "-Pkotlin.native.cocoapods.archs=arm64 x86_64", - "-Pkotlin.native.cocoapods.configuration=Debug", - "-Pkotlin.native.cocoapods.generate.wrapper=true" - ) } } - - @Test - fun testSyncFrameworkUseXcodeStyleErrorsWhenConfigurationFailed() { - with(project) { - gradleBuildScript().appendText( + @DisplayName("Xcode style errors when sync framework configuration failed") + @GradleTest + fun testSyncFrameworkUseXcodeStyleErrorsWhenConfigurationFailed(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { + buildGradleKts.appendText( """ kotlin { sourceSets["commonMain"].dependencies { @@ -490,634 +420,470 @@ class CocoaPodsIT : BaseGradleIT() { } """.trimIndent() ) - - build( - "syncFramework", - "-Pkotlin.native.cocoapods.platform=iphonesimulator", - "-Pkotlin.native.cocoapods.archs=x86_64", - "-Pkotlin.native.cocoapods.configuration=Debug" - ) { - assertFailed() - assertContains("error: Could not find com.example.unknown:dependency:0.0.1.") + val buildOptions = this.buildOptions.copy( + nativeOptions = this.buildOptions.nativeOptions.copy( + cocoapodsPlatform = "iphonesimulator", + cocoapodsArchs = "x86_64", + cocoapodsConfiguration = "Debug" + ) + ) + buildAndFail("syncFramework", buildOptions = buildOptions) { + assertOutputContains("error: Could not find com.example.unknown:dependency:0.0.1.") } } } - @Test - fun testSyncFrameworkUseXcodeStyleErrorsWhenCompilationFailed() { - with(project) { - projectDir.resolve("src/commonMain/kotlin/A.kt").appendText("this can't be compiled") - - build( - "syncFramework", - "-Pkotlin.native.cocoapods.platform=iphonesimulator", - "-Pkotlin.native.cocoapods.archs=x86_64", - "-Pkotlin.native.cocoapods.configuration=Debug", - ) { - assertFailed() - assertContains("/native-cocoapods-template/src/commonMain/kotlin/A.kt:5:2: error: Expecting a top level declaration") - assertContains("error: Compilation finished with errors") + @DisplayName("Xcode style errors when sync framework compilation failed") + @GradleTest + fun testSyncFrameworkUseXcodeStyleErrorsWhenCompilationFailed(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { + projectPath.resolve("src/commonMain/kotlin/A.kt").appendText("this can't be compiled") + val buildOptions = this.buildOptions.copy( + nativeOptions = this.buildOptions.nativeOptions.copy( + cocoapodsPlatform = "iphonesimulator", + cocoapodsArchs = "x86_64", + cocoapodsConfiguration = "Debug" + ) + ) + buildAndFail("syncFramework", buildOptions = buildOptions) { + assertOutputContains("/native-cocoapods-template/src/commonMain/kotlin/A.kt:5:2: error: Expecting a top level declaration") + assertOutputContains("error: Compilation finished with errors") } } } - @Test - fun testOtherTasksUseGradleStyleErrorsWhenCompilationFailed() { - with(project) { - projectDir.resolve("src/commonMain/kotlin/A.kt").appendText("this can't be compiled") - - build("linkPodDebugFrameworkIOS") { - assertFailed() - assertContains("e: file:///") - assertContains("/native-cocoapods-template/src/commonMain/kotlin/A.kt:5:2 Expecting a top level declaration") - assertNotContains("error: Compilation finished with errors") + @DisplayName("Other tasks use gradle style errors when compilation failed") + @GradleTest + fun testOtherTasksUseGradleStyleErrorsWhenCompilationFailed(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { + projectPath.resolve("src/commonMain/kotlin/A.kt").appendText("this can't be compiled") + val buildOptions = this.buildOptions.copy( + nativeOptions = this.buildOptions.nativeOptions.copy( + cocoapodsPlatform = "iphonesimulator", + cocoapodsArchs = "x86_64", + cocoapodsConfiguration = "Debug" + ) + ) + buildAndFail("linkPodDebugFrameworkIOS", buildOptions = buildOptions) { + assertOutputContains("e: file:///") + assertOutputContains("/native-cocoapods-template/src/commonMain/kotlin/A.kt:5:2 Expecting a top level declaration") + assertOutputDoesNotContain("error: Compilation finished with errors") } } } - @Test - fun testOtherTasksUseXcodeStyleErrorsWhenCompilationFailedAndOptionEnabled() { - with(project) { - projectDir.resolve("src/commonMain/kotlin/A.kt").appendText("this can't be compiled") - - build("linkPodDebugFrameworkIOS", "-Pkotlin.native.useXcodeMessageStyle=true") { - assertFailed() - assertContains("/native-cocoapods-template/src/commonMain/kotlin/A.kt:5:2: error: Expecting a top level declaration") - assertContains("error: Compilation finished with errors") + @DisplayName("Other tasks use Xcode style errors when compilation failed and `useXcodeMessageStyle` option enabled") + @GradleTest + fun testOtherTasksUseXcodeStyleErrorsWhenCompilationFailedAndOptionEnabled(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { + projectPath.resolve("src/commonMain/kotlin/A.kt").appendText("this can't be compiled") + val buildOptions = this.buildOptions.copy( + nativeOptions = this.buildOptions.nativeOptions.copy( + cocoapodsPlatform = "iphonesimulator", + cocoapodsArchs = "x86_64", + cocoapodsConfiguration = "Debug", + useXcodeMessageStyle = true + ) + ) + buildAndFail("linkPodDebugFrameworkIOS", buildOptions = buildOptions) { + assertOutputContains("/native-cocoapods-template/src/commonMain/kotlin/A.kt:5:2: error: Expecting a top level declaration") + assertOutputContains("error: Compilation finished with errors") } } } - @Test - fun testPodDependencyInUnitTests() { - getProjectByName(cocoapodsTestsProjectName).testWithWrapper(":iosX64Test") - } - - - @Test - fun testCinteropCommonizationOff() { - project = getProjectByName(cocoapodsCommonizationProjectName) - hooks.addHook { - assertTasksExecuted(":commonizeNativeDistribution") - assertTasksNotExecuted(":cinteropAFNetworkingIosArm64") - assertTasksNotExecuted(":cinteropAFNetworkingIosX64") - assertTasksNotExecuted(":commonizeCInterop") - } - project.testWithWrapper(":commonize") - } - - @Test - fun testCinteropCommonizationOn() { - project = getProjectByName(cocoapodsCommonizationProjectName) - project.gradleProperties().appendLine("kotlin.mpp.enableCInteropCommonization=true") - hooks.addHook { - assertTasksExecuted(":commonizeNativeDistribution") - assertTasksExecuted(":cinteropAFNetworkingIosArm64") - assertTasksExecuted(":cinteropAFNetworkingIosX64") - assertTasksExecuted(":commonizeCInterop") - } - project.testWithWrapper(":compileIosMainKotlinMetadata") - } - - @Test - fun testPodPublishing() { - //test that manually created frameworks are not included into cocoapods xcframework - project.gradleBuildScript().appendToKotlinBlock("iosX64(\"iOS\") {binaries.framework{}}") - project.build( - ":podPublishXCFramework", - "-Pkotlin.native.cocoapods.generate.wrapper=true", - ) { - assertSuccessful() - - assertTasksExecuted(":podPublishReleaseXCFramework") - assertTasksExecuted(":podPublishDebugXCFramework") - assertFileExists("build/cocoapods/publish/release/cocoapods.xcframework") - assertFileExists("build/cocoapods/publish/debug/cocoapods.xcframework") - assertFileExists("build/cocoapods/publish/release/cocoapods.podspec") - assertFileExists("build/cocoapods/publish/debug/cocoapods.podspec") - val actualPodspecContentWithoutBlankLines = fileInWorkingDir("build/cocoapods/publish/release/cocoapods.podspec").readText() - .lineSequence() - .filter { it.isNotBlank() } - .joinToString("\n") - - assertEquals(publishPodspecContent, actualPodspecContentWithoutBlankLines) + @DisplayName("Pod dependency in unit tests") + @GradleTest + fun testPodDependencyInUnitTests(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(cocoapodsTestsProjectName, gradleVersion) { + buildWithCocoapodsWrapper(":iosX64Test") } } - - @Test - fun testPodPublishingWithCustomProperties() { - - with(project.gradleBuildScript()) { - appendToCocoapodsBlock("name = \"CustomPod\"") - appendToCocoapodsBlock("version = \"2.0\"") - appendToCocoapodsBlock("publishDir = projectDir.resolve(\"CustomPublishDir\")") - appendToCocoapodsBlock("license = \"'MIT'\"") - appendToCocoapodsBlock("authors = \"{ 'Kotlin Dev' => 'kotlin.dev@jetbrains.com' }\"") - appendToCocoapodsBlock("extraSpecAttributes[\"social_media_url\"] = \"'https://twitter.com/kotlin'\"") - appendToCocoapodsBlock("extraSpecAttributes[\"vendored_frameworks\"] = \"'CustomFramework.xcframework'\"") - appendToCocoapodsBlock("extraSpecAttributes[\"libraries\"] = \"'xml'\"") - addPod(defaultPodName) + @DisplayName("Cinterop commonization off") + @GradleTest + fun testCinteropCommonizationOff(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(cocoapodsCommonizationProjectName, gradleVersion) { + buildWithCocoapodsWrapper(":commonize") { + assertTasksExecuted(":commonizeNativeDistribution") + assertTasksNotExecuted(":cinteropAFNetworkingIosArm64") + assertTasksNotExecuted(":cinteropAFNetworkingIosX64") + assertTasksNotExecuted(":commonizeCInterop") + } } - - hooks.addHook { - assertTasksExecuted(":podPublishReleaseXCFramework") - assertTasksExecuted(":podPublishDebugXCFramework") - assertFileExists("CustomPublishDir/release/cocoapods.xcframework") - assertFileExists("CustomPublishDir/debug/cocoapods.xcframework") - assertFileExists("CustomPublishDir/release/CustomPod.podspec") - assertFileExists("CustomPublishDir/debug/CustomPod.podspec") - val actualPodspecContentWithoutBlankLines = fileInWorkingDir("CustomPublishDir/release/CustomPod.podspec").readText() - .lineSequence() - .filter { it.isNotBlank() } - .joinToString("\n") - - assertEquals(publishPodspecCustomContent, actualPodspecContentWithoutBlankLines) - } - - project.testWithWrapper(":podPublishXCFramework") } - @Test - fun testPodInstallUpToDateCheck() { - project = getProjectByName(cocoapodsSingleKtPod) + @DisplayName("Cinterop commonization on") + @GradleTest + fun testCinteropCommonizationOn(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(cocoapodsCommonizationProjectName, gradleVersion) { + buildWithCocoapodsWrapper(":commonize", "-Pkotlin.mpp.enableCInteropCommonization=true") { + assertTasksExecuted(":commonizeNativeDistribution") + assertTasksExecuted(":cinteropAFNetworkingIosArm64") + assertTasksExecuted(":cinteropAFNetworkingIosX64") + assertTasksExecuted(":commonizeCInterop") + } + } + } + + @DisplayName("Checks pod publishing") + @GradleTest + fun testPodPublishing(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { + buildGradleKts.addKotlinBlock("iosX64(\"iOS\") {binaries.framework{}}") + buildWithCocoapodsWrapper(":podPublishXCFramework") { + assertTasksExecuted(":podPublishReleaseXCFramework") + assertTasksExecuted(":podPublishDebugXCFramework") + assertDirectoryInProjectExists("build/cocoapods/publish/release/cocoapods.xcframework") + assertDirectoryInProjectExists("build/cocoapods/publish/debug/cocoapods.xcframework") + assertFileInProjectExists("build/cocoapods/publish/release/cocoapods.podspec") + assertFileInProjectExists("build/cocoapods/publish/debug/cocoapods.podspec") + val actualPodspecContentWithoutBlankLines = + projectPath.resolve("build/cocoapods/publish/release/cocoapods.podspec").readText() + .lineSequence() + .filter { it.isNotBlank() } + .joinToString("\n") + + assertEquals(publishPodspecContent, actualPodspecContentWithoutBlankLines) + } + } + } + + @DisplayName("Checks pod publishing with custom properties") + @GradleTest + fun testPodPublishingWithCustomProperties(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { + buildGradleKts.addCocoapodsBlock("name = \"CustomPod\"") + buildGradleKts.addCocoapodsBlock("version = \"2.0\"") + buildGradleKts.addCocoapodsBlock("publishDir = projectDir.resolve(\"CustomPublishDir\")") + buildGradleKts.addCocoapodsBlock("license = \"'MIT'\"") + buildGradleKts.addCocoapodsBlock("authors = \"{ 'Kotlin Dev' => 'kotlin.dev@jetbrains.com' }\"") + buildGradleKts.addCocoapodsBlock("extraSpecAttributes[\"social_media_url\"] = \"'https://twitter.com/kotlin'\"") + buildGradleKts.addCocoapodsBlock("extraSpecAttributes[\"vendored_frameworks\"] = \"'CustomFramework.xcframework'\"") + buildGradleKts.addCocoapodsBlock("extraSpecAttributes[\"libraries\"] = \"'xml'\"") + buildGradleKts.addPod(defaultPodName) + + buildWithCocoapodsWrapper(":podPublishXCFramework") { + assertTasksExecuted(":podPublishReleaseXCFramework") + assertTasksExecuted(":podPublishDebugXCFramework") + assertDirectoryInProjectExists("CustomPublishDir/release/cocoapods.xcframework") + assertDirectoryInProjectExists("CustomPublishDir/debug/cocoapods.xcframework") + assertFileInProjectExists("CustomPublishDir/release/CustomPod.podspec") + assertFileInProjectExists("CustomPublishDir/debug/CustomPod.podspec") + val actualPodspecContentWithoutBlankLines = projectPath.resolve("CustomPublishDir/release/CustomPod.podspec").readText() + .lineSequence() + .filter { it.isNotBlank() } + .joinToString("\n") + + assertEquals(publishPodspecCustomContent, actualPodspecContentWithoutBlankLines) + } + } + } + + @DisplayName("Checks pod install UTD") + @GradleTest + fun testPodInstallUpToDateCheck(gradleVersion: GradleVersion) { val subProjectName = "kotlin-library" val subprojectPodImportTask = ":$subProjectName$podImportTaskName" val subprojectPodspecTask = ":$subProjectName$podspecTaskName" val subprojectPodInstallTask = ":$subProjectName$podInstallTaskName" - with(project) { - preparePodfile("ios-app", ImportMode.FRAMEWORKS) - gradleBuildScript(subProjectName).appendToCocoapodsBlock("ios.deploymentTarget = \"14.0\"") + nativeProjectWithCocoapodsAndIosAppPodFile(cocoapodsSingleKtPod, gradleVersion) { + buildGradleKts.addCocoapodsBlock("ios.deploymentTarget = \"14.0\"") + buildWithCocoapodsWrapper(subprojectPodImportTask) { + assertTasksExecuted(listOf(subprojectPodspecTask, subprojectPodInstallTask)) + } - build(subprojectPodImportTask, "-Pkotlin.native.cocoapods.generate.wrapper=true") { + subProject(subProjectName).buildGradleKts.addPod(defaultPodName) + buildWithCocoapodsWrapper(subprojectPodImportTask) { assertTasksExecuted(listOf(subprojectPodspecTask, subprojectPodInstallTask)) } - gradleBuildScript(subProjectName).addPod(defaultPodName) - build(subprojectPodImportTask, "-Pkotlin.native.cocoapods.generate.wrapper=true") { - assertTasksExecuted(listOf(subprojectPodspecTask, subprojectPodInstallTask)) - } - build(subprojectPodImportTask, "-Pkotlin.native.cocoapods.generate.wrapper=true") { - assertTasksNotExecuted(listOf(subprojectPodspecTask, subprojectPodInstallTask)) + + buildWithCocoapodsWrapper(subprojectPodImportTask) { + assertTasksUpToDate(subprojectPodspecTask, subprojectPodInstallTask) } + addPodToPodfile("ios-app", defaultPodName) - build(subprojectPodImportTask, "-Pkotlin.native.cocoapods.generate.wrapper=true") { - assertTasksNotExecuted(listOf(subprojectPodspecTask)) + buildWithCocoapodsWrapper(subprojectPodImportTask) { + assertTasksUpToDate(subprojectPodspecTask) assertTasksExecuted(listOf(subprojectPodInstallTask)) } } } - @Test - fun testCinteropKlibsProvideLinkerOptsToFramework() = with(project) { - gradleBuildScript().addPod("AFNetworking") - testWithWrapper(":cinteropAFNetworkingIOS") + @DisplayName("Cinterop Klibs provide linker opts to framework") + @GradleTest + fun testCinteropKlibsProvideLinkerOptsToFramework(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { + buildGradleKts.addPod("AFNetworking") + buildWithCocoapodsWrapper("cinteropAFNetworkingIOS") { + val cinteropKlib = projectPath.resolve("build/classes/kotlin/iOS/main/cinterop/cocoapods-cinterop-AFNetworking.klib") + val manifestLines = ZipFile(cinteropKlib.toFile()).use { zip -> + zip.getInputStream(zip.getEntry("default/manifest")).bufferedReader().use { it.readLines() } + } - val cinteropKlib = projectDir.resolve("build/classes/kotlin/iOS/main/cinterop/cocoapods-cinterop-AFNetworking.klib") - val manifestLines = ZipFile(cinteropKlib).use { zip -> - zip.getInputStream(zip.getEntry("default/manifest")).bufferedReader().use { it.readLines() } + assertContains(manifestLines, "linkerOpts=-framework AFNetworking") + } } - - assertContains(manifestLines, "linkerOpts=-framework AFNetworking") } - @Test - fun testLinkOnlyPods() = with(project) { - gradleBuildScript().appendToCocoapodsBlock( - """ - pod("AFNetworking") { linkOnly = true } - pod("SSZipArchive", linkOnly = true) - pod("SDWebImage/Core") - """.trimIndent() - ) - - build( - ":linkPodDebugFrameworkIOS", - "-Pkotlin.native.cocoapods.generate.wrapper=true", - ) { - assertSuccessful() - - assertTasksExecuted(":podBuildAFNetworkingIphonesimulator") - assertTasksExecuted(":podBuildSDWebImageIphonesimulator") - assertTasksExecuted(":podBuildSSZipArchiveIphonesimulator") - - assertTasksExecuted(":cinteropSDWebImageIOS") - assertTasksNotRegistered(":cinteropAFNetworkingIOS") - assertTasksNotRegistered(":cinteropSSZipArchiveIOS") - - assertContains( + @DisplayName("Link only pods") + @GradleTest + fun testLinkOnlyPods(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { + buildGradleKts.addCocoapodsBlock( """ - | -linker-option - | -framework - | -linker-option - | AFNetworking - """.trimMargin() + pod("AFNetworking") { linkOnly = true } + pod("SSZipArchive", linkOnly = true) + pod("SDWebImage/Core") + """.trimIndent() ) - assertContains( + buildAndAssertAllTasks( + notRegisteredTasks = listOf(":cinteropAFNetworkingIOS", ":cinteropSSZipArchiveIOS"), + buildOptions = this.buildOptions.copy( + nativeOptions = this.buildOptions.nativeOptions.copy( + cocoapodsGenerateWrapper = true + ) + ) + ) + + buildWithCocoapodsWrapper(":linkPodDebugFrameworkIOS") { + assertTasksExecuted(":podBuildAFNetworkingIphonesimulator") + assertTasksExecuted(":podBuildSDWebImageIphonesimulator") + assertTasksExecuted(":podBuildSSZipArchiveIphonesimulator") + + assertTasksExecuted(":cinteropSDWebImageIOS") + + // TODO(Dmitrii Krasnov): rewrite it, when GeneralNativeIT will be migrated to new test dsl + assertOutputContains( + """ + | -linker-option + | -framework + | -linker-option + | AFNetworking + """.trimMargin() + ) + + assertOutputContains( + """ + | -linker-option + | -framework + | -linker-option + | SSZipArchive + """.trimMargin() + ) + } + } + } + + @DisplayName("Usage link only with static framework produces message") + @GradleTest + fun testUsageLinkOnlyWithStaticFrameworkProducesMessage(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) { + buildGradleKts.addCocoapodsBlock( """ - | -linker-option - | -framework - | -linker-option - | SSZipArchive - """.trimMargin() + framework { + isStatic = true + } + + pod("AFNetworking") { linkOnly = true } + """.trimIndent() ) - } - } - - @Test - fun testUsageLinkOnlyWithStaticFrameworkProducesMessage() = with(project) { - gradleBuildScript().appendToCocoapodsBlock( - """ - framework { - isStatic = true + buildWithCocoapodsWrapper(":linkPodDebugFrameworkIOS") { + assertOutputContains("Dependency on 'AFNetworking' with option 'linkOnly=true' is unused for building static frameworks") } - - pod("AFNetworking") { linkOnly = true } - """.trimIndent() - ) - - build( - ":linkPodDebugFrameworkIOS", - "-Pkotlin.native.cocoapods.generate.wrapper=true", - ) { - assertSuccessful() - - assertContains("Dependency on 'AFNetworking' with option 'linkOnly=true' is unused for building static frameworks") } } - @Test - fun `hierarchy of dependant pods compiles successfully`() = with(getProjectByName("native-cocoapods-dependant-pods")) { - build( - ":compileKotlinIosX64", - "-Pkotlin.native.cocoapods.generate.wrapper=true", - ) { - assertSuccessful() + @DisplayName("Hierarchy of dependant pods compiles successfully") + @GradleTest + fun testHierarchyOfDependantPodsCompilesSuccessfully(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(cocoapodsDependantPodsProjectName, gradleVersion) { + buildWithCocoapodsWrapper(":compileKotlinIosX64") } } - @Test - fun `configuration fails when trying to depend on non-declared pod`() = with(getProjectByName("native-cocoapods-dependant-pods")) { - gradleBuildScript().appendToCocoapodsBlock( - """ - pod("Foo") { useInteropBindingFrom("JBNonExistent") } - """.trimIndent() - ) - - build( - ":help", - "-Pkotlin.native.cocoapods.generate.wrapper=true", - ) { - assertFailed() - assertContains("Couldn't find declaration of pod 'JBNonExistent' (interop-binding dependency of pod 'Foo')") - } - } - - @Test - fun `configuration fails when dependant pods are in the wrong order`() = with(getProjectByName("native-cocoapods-dependant-pods")) { - gradleBuildScript().appendToCocoapodsBlock( - """ - pod("Foo") { useInteropBindingFrom("Bar") } - pod("Bar") - """.trimIndent() - ) - - build( - ":help", - "-Pkotlin.native.cocoapods.generate.wrapper=true", - ) { - assertFailed() - assertContains("Couldn't find declaration of pod 'Bar' (interop-binding dependency of pod 'Foo')") - } - } - - @Test - fun `configuration fails when pod depends on itself`() = with(getProjectByName("native-cocoapods-dependant-pods")) { - gradleBuildScript().appendToCocoapodsBlock( - """ - pod("Foo") { useInteropBindingFrom("Foo") } - """.trimIndent() - ) - - build( - ":help", - "-Pkotlin.native.cocoapods.generate.wrapper=true", - ) { - assertFailed() - assertContains("Pod 'Foo' has an interop-binding dependency on itself") - } - } - - @Test - @Ignore // will be fixed in the next step - fun `test configuration cache works in a complex scenario with Gradle 8_1`() { - project = transformProjectWithPluginsDsl(templateProjectName, GradleVersionRequired.Exact(G_8_1)).apply { - preparePodfile("ios-app", ImportMode.FRAMEWORKS) - } - `test configuration cache works in a complex scenario`() - } - - @Test - fun `test configuration cache works in a complex scenario`() { - project.gradleBuildScript().appendToCocoapodsBlock("""pod("Base64", version = "1.1.2")""") - - val tasks = arrayOf( - ":podspec", - ":podImport", - ":podPublishDebugXCFramework", - ":podPublishReleaseXCFramework", - ":syncFramework", - ) - - val executableTasks = listOf( - ":podspec", - ":podPublishDebugXCFramework", - ":podPublishReleaseXCFramework", - ":linkPodDebugFrameworkIOS", - ) - - fun build(vararg tasks: String, check: CompiledProject.() -> Unit) { - project.build( - *tasks, - "-Pkotlin.native.cocoapods.generate.wrapper=true", - "-Pkotlin.native.cocoapods.platform=iphonesimulator", - "-Pkotlin.native.cocoapods.archs=x86_64", - "-Pkotlin.native.cocoapods.configuration=Debug", - options = defaultBuildOptions().copy(configurationCache = true), - check = check, + @DisplayName("Configuration fails when trying to depend on non-declared pod") + @GradleTest + fun testConfigurationFailsWhenTryingToDependOnNonDeclaredPod(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(cocoapodsDependantPodsProjectName, gradleVersion) { + buildGradleKts.addCocoapodsBlock( + """ + pod("Foo") { useInteropBindingFrom("JBNonExistent") } + """.trimIndent() ) - } - build(*tasks) { - assertSuccessful() - assertTasksExecuted(executableTasks) + val buildOptions = this.buildOptions.copy( + nativeOptions = this.buildOptions.nativeOptions.copy( + cocoapodsGenerateWrapper = true + ) + ) - assertContains("Calculating task graph as no configuration cache is available for tasks") - - assertContains("Configuration cache entry stored.") - } - - build("clean") { - assertSuccessful() - } - - build(*tasks) { - assertSuccessful() - - assertContains("Reusing configuration cache.") - } - - build(*tasks) { - assertSuccessful() - - assertTasksUpToDate(executableTasks) - } - } - - // test configuration phase - - private class CustomHooks { - private val hooks = mutableSetOf Unit>() - - fun addHook(hook: CompiledProject.() -> Unit) { - hooks.add(hook) - } - - fun rewriteHooks(hook: CompiledProject.() -> Unit) { - hooks.clear() - hooks.add(hook) - } - - fun trigger(project: CompiledProject) { - hooks.forEach { function -> - project.function() + buildAndFail(":help", buildOptions = buildOptions) { + assertOutputContains("Couldn't find declaration of pod 'JBNonExistent' (interop-binding dependency of pod 'Foo')") } } } - private fun Project.testImportWithAsserts( - repos: List = listOf(), - vararg args: String, - ) { - hooks.addHook { - podImportAsserts() - } - testImport(repos, *args) - } + @DisplayName("Configuration fails when dependant pods are in the wrong order") + @GradleTest + fun testConfigurationFailsWhenDependantPodsAreInTheWrongOrder(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(cocoapodsDependantPodsProjectName, gradleVersion) { + buildGradleKts.addCocoapodsBlock( + """ + pod("Foo") { useInteropBindingFrom("Bar") } + pod("Bar") + """.trimIndent() + ) - private fun Project.testImport( - repos: List = listOf(), - vararg args: String, - ) { - for (repo in repos) { - assumeTrue(isRepoAvailable(repo)) - } - testSynthetic(podImportTaskName, *args) - } + val buildOptions = this.buildOptions.copy( + nativeOptions = this.buildOptions.nativeOptions.copy( + cocoapodsGenerateWrapper = true + ) + ) - private fun Project.testSynthetic( - taskName: String, - vararg args: String, - ) { - testWithWrapper(taskName, *args) - } - - private fun Project.testWithWrapper( - taskName: String, - vararg args: String, - ) { - test(taskName, "-Pkotlin.native.cocoapods.generate.wrapper=true", *args) - } - - private fun Project.test( - taskName: String, - vararg args: String, - ) { - - // check that test executable - build(taskName, *args) { - //base checks - assertSuccessful() - hooks.trigger(this) + buildAndFail(":help", buildOptions = buildOptions) { + assertOutputContains("Couldn't find declaration of pod 'Bar' (interop-binding dependency of pod 'Foo')") + } } } - private fun getProjectByName(projectName: String) = transformProjectWithPluginsDsl(projectName, gradleVersion) + @DisplayName("Configuration fails when pod depends on itself") + @GradleTest + fun testConfigurationFailsWhenPodDependsOnItself(gradleVersion: GradleVersion) { + nativeProjectWithCocoapodsAndIosAppPodFile(cocoapodsDependantPodsProjectName, gradleVersion) { + buildGradleKts.addCocoapodsBlock( + """ + pod("Foo") { useInteropBindingFrom("Foo") } + """.trimIndent() + ) + val buildOptions = this.buildOptions.copy( + nativeOptions = this.buildOptions.nativeOptions.copy( + cocoapodsGenerateWrapper = true + ) + ) - // build script configuration phase - - private fun File.addPod(podName: String, configuration: String? = null) { - val pod = "pod(\"$podName\")" - val podBlock = configuration?.wrap(pod) ?: pod - appendToCocoapodsBlock(podBlock) + buildAndFail(":help", buildOptions = buildOptions) { + assertOutputContains("Pod 'Foo' has an interop-binding dependency on itself") + } + } } + @DisplayName("Configuration cache works in a complex scenario with Gradle 8_1") + @GradleTest + @GradleTestVersions(minVersion = TestVersions.Gradle.G_8_1) + @Disabled // will be fixed in the next step + fun testConfigurationCacheWorksInAComplexScenarioWithGradle8_1(gradleVersion: GradleVersion) { + testConfigurationCacheWorksInAComplexScenario(gradleVersion) + } - private fun File.appendToKotlinBlock(str: String) = appendLine(str.wrap("kotlin")) - - private fun File.appendToCocoapodsBlock(str: String) = appendToKotlinBlock(str.wrap("cocoapods")) - - private fun File.appendToFrameworkBlock(str: String) = appendToCocoapodsBlock(str.wrap("framework")) - - private fun String.wrap(s: String): String = """ - |$s { - | $this - |} - """.trimMargin() - - private fun File.appendLine(s: String) = appendText("\n$s") - // proposition phase - - private fun isRepoAvailable(repo: String): Boolean { - var responseCode = 0 - runCommand( - File("/"), - "curl", - "-s", - "-o", - "/dev/null", - "-w", - "%{http_code}", - "-L", - repo, - "--retry", "2" + @DisplayName("Configuration cache works in a complex scenario") + @GradleTestVersions(minVersion = TestVersions.Gradle.MAX_SUPPORTED) + @GradleTest + fun testConfigurationCacheWorksInAComplexScenario(gradleVersion: GradleVersion) { + val buildOptions = defaultBuildOptions.copy( + nativeOptions = defaultBuildOptions.nativeOptions.copy( + cocoapodsGenerateWrapper = true, + cocoapodsPlatform = "iphonesimulator", + cocoapodsArchs = "x86_64", + cocoapodsConfiguration = "Debug" + ), + configurationCache = true + ) + nativeProjectWithCocoapodsAndIosAppPodFile( + gradleVersion = gradleVersion, + buildOptions = buildOptions ) { - val (retCode, out, errorMessage) = this - assertEquals(0, retCode, errorMessage) - responseCode = out.toInt() - } - return responseCode == 200 - } + buildGradleKts.addCocoapodsBlock("""pod("Base64", version = "1.1.2")""") - private fun CompiledProject.podImportAsserts(projectName: String? = null) { + val tasks = arrayOf( + ":podspec", + ":podImport", + ":podPublishDebugXCFramework", + ":podPublishReleaseXCFramework", + ":syncFramework", + ) - val buildScriptText = project.gradleBuildScript(projectName).readText() - val taskPrefix = projectName?.let { ":$it" } ?: "" - val podspec = "podspec" - val podInstall = "podInstall" - assertSuccessful() + val executableTasks = listOf( + ":podspec", + ":podPublishDebugXCFramework", + ":podPublishReleaseXCFramework", + ":linkPodDebugFrameworkIOS", + ) - if ("noPodspec()" in buildScriptText) { - assertTasksSkipped("$taskPrefix:$podspec") - } + build(*tasks) { + assertTasksExecuted(executableTasks) - if ("podfile" in buildScriptText) { - assertTasksExecuted("$taskPrefix:$podInstall") - } else { - assertTasksSkipped("$taskPrefix:$podInstall") - } - assertTasksRegisteredByPrefix(listOf("$taskPrefix:${KotlinCocoapodsPlugin.POD_GEN_TASK_NAME}")) - if (buildScriptText.matches("pod\\(.*\\)".toRegex())) { - assertTasksExecutedByPrefix(listOf("$taskPrefix:${KotlinCocoapodsPlugin.POD_GEN_TASK_NAME}")) - } + assertOutputContains("Calculating task graph as no configuration cache is available for tasks") - with(listOf(KotlinCocoapodsPlugin.POD_SETUP_BUILD_TASK_NAME, KotlinCocoapodsPlugin.POD_BUILD_TASK_NAME).map { "$taskPrefix:$it" }) { - if (buildScriptText.matches("pod\\(.*\\)".toRegex())) { - assertTasksRegisteredByPrefix(this) - assertTasksExecutedByPrefix(this) + assertOutputContains("Configuration cache entry stored.") + } + + build("clean") + + build(*tasks) { + assertOutputContains("Reusing configuration cache.") + } + + build(*tasks) { + assertTasksUpToDate(executableTasks) } } } - private data class CommandResult( - val exitCode: Int, - val stdOut: String, - val stdErr: String, - ) - - private fun runCommand( - workingDir: File, - command: String, - vararg args: String, - timeoutSec: Long = 120, - inheritIO: Boolean = false, - block: CommandResult.() -> Unit, + private fun TestProject.buildWithCocoapodsWrapper( + vararg buildArguments: String, + assertions: BuildResult.() -> Unit = {}, ) { - val process = ProcessBuilder(command, *args).apply { - directory(workingDir) - environment().putAll(cocoaPodsEnvironmentVariables()) - if (inheritIO) { - inheritIO() - } - }.start() - - val isFinished = process.waitFor(timeoutSec, TimeUnit.SECONDS) - val stdOut = process.inputStream.bufferedReader().use { it.readText() } - val stdErr = process.errorStream.bufferedReader().use { it.readText() } - - if (!isFinished) { - process.destroyForcibly() - println("Stdout:\n$stdOut") - println("Stderr:\n$stdErr") - fail("Command '$command ${args.joinToString(" ")}' killed by timeout.".trimIndent()) - } - CommandResult(process.exitValue(), stdOut, stdErr).block() - } - - private fun Project.preparePodfile(iosAppLocation: String, mode: ImportMode) { - val iosAppDir = projectDir.resolve(iosAppLocation) - - // Set import mode for Podfile. - iosAppDir.resolve("Podfile").takeIf { it.exists() }?.modify { - it.replace(podfileImportDirectivePlaceholder, mode.directive) + val buildOptions = this.buildOptions.copy( + nativeOptions = this.buildOptions.nativeOptions.copy( + cocoapodsGenerateWrapper = true + ) + ) + build(*buildArguments, buildOptions = buildOptions) { + assertions() } } - private fun Project.addPodToPodfile(iosAppLocation: String, pod: String) { - val iosAppDir = projectDir.resolve(iosAppLocation) - iosAppDir.resolve("Podfile").takeIf { it.exists() }?.modify { - it.replace(podfileImportPodPlaceholder, "pod '$pod'") - } + private fun TestProject.addPodToPodfile(iosAppLocation: String, pod: String) { + projectPath + .resolve(iosAppLocation) + .resolve("Podfile") + .replaceText(podfileImportPodPlaceholder, "pod '$pod'") } - private val publishPodspecContent = """ - Pod::Spec.new do |spec| - spec.name = 'cocoapods' - spec.version = '1.0' - spec.homepage = 'https://github.com/JetBrains/kotlin' - spec.source = { :http=> ''} - spec.authors = '' - spec.license = '' - spec.summary = 'CocoaPods test library' - spec.vendored_frameworks = 'cocoapods.xcframework' - spec.libraries = 'c++' - spec.ios.deployment_target = '13.5' - end - """.trimIndent() + private val publishPodspecContent = + """ + Pod::Spec.new do |spec| + spec.name = 'cocoapods' + spec.version = '1.0' + spec.homepage = 'https://github.com/JetBrains/kotlin' + spec.source = { :http=> ''} + spec.authors = '' + spec.license = '' + spec.summary = 'CocoaPods test library' + spec.vendored_frameworks = 'cocoapods.xcframework' + spec.libraries = 'c++' + spec.ios.deployment_target = '13.5' + end + """.trimIndent() - private val publishPodspecCustomContent = """ - Pod::Spec.new do |spec| - spec.name = 'CustomPod' - spec.version = '2.0' - spec.homepage = 'https://github.com/JetBrains/kotlin' - spec.source = { :http=> ''} - spec.authors = { 'Kotlin Dev' => 'kotlin.dev@jetbrains.com' } - spec.license = 'MIT' - spec.summary = 'CocoaPods test library' - spec.ios.deployment_target = '13.5' - spec.dependency 'AFNetworking' - spec.social_media_url = 'https://twitter.com/kotlin' - spec.vendored_frameworks = 'CustomFramework.xcframework' - spec.libraries = 'xml' - end - """.trimIndent() - - companion object { - @BeforeClass - @JvmStatic - fun assumeItsMac() { - assumeTrue(HostManager.hostIsMac) - } - - @BeforeClass - @JvmStatic - fun checkCocoapodsInstalled() { - if (!HostManager.hostIsMac) { - return - } - - ensureCocoapodsInstalled() - } - } + private val publishPodspecCustomContent = + """ + Pod::Spec.new do |spec| + spec.name = 'CustomPod' + spec.version = '2.0' + spec.homepage = 'https://github.com/JetBrains/kotlin' + spec.source = { :http=> ''} + spec.authors = { 'Kotlin Dev' => 'kotlin.dev@jetbrains.com' } + spec.license = 'MIT' + spec.summary = 'CocoaPods test library' + spec.ios.deployment_target = '13.5' + spec.dependency 'AFNetworking' + spec.social_media_url = 'https://twitter.com/kotlin' + spec.vendored_frameworks = 'CustomFramework.xcframework' + spec.libraries = 'xml' + end + """.trimIndent() } \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsSyntheticIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsSyntheticIT.kt new file mode 100644 index 00000000000..082c3873fea --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsSyntheticIT.kt @@ -0,0 +1,115 @@ +/* + * 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.gradle.native + +import org.gradle.util.GradleVersion +import org.jetbrains.kotlin.gradle.testbase.* +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.condition.OS +import kotlin.io.path.appendText +import kotlin.test.assertTrue + +@OptIn(EnvironmentalVariablesOverride::class) +@OsCondition(supportedOn = [OS.MAC], enabledOnCI = [OS.MAC]) +@DisplayName("K/N tests with synthetic cocoapods") +@GradleTestVersions(minVersion = TestVersions.Gradle.G_7_0) +@NativeGradlePluginTests +class CocoaPodsSyntheticIT : KGPBaseTest() { + + override val defaultBuildOptions = super.defaultBuildOptions.copy( + nativeOptions = super.defaultBuildOptions.nativeOptions.copy( + cocoapodsGenerateWrapper = true + ) + ) + + private val cocoapodsSingleKtPod = "native-cocoapods-single" + private val templateProjectName = "native-cocoapods-template" + + private val defaultPodInstallSyntheticTaskName = ":podInstallSyntheticIos" + + private val environmentVariables = EnvironmentalVariables(cocoaPodsEnvironmentVariables()) + + @BeforeAll + fun setUp() { + ensureCocoapodsInstalled() + } + + @DisplayName("Synthetic project podfile generation") + @GradleTest + fun testSyntheticProjectPodfileGeneration(gradleVersion: GradleVersion) { + nativeProject(cocoapodsSingleKtPod, gradleVersion, environmentVariables = environmentVariables) { + buildGradleKts.addCocoapodsBlock( + """ + ios.deploymentTarget = "14.1" + pod("SSZipArchive") + pod("AFNetworking", "~> 4.0.1") + pod("Alamofire") { + source = git("https://github.com/Alamofire/Alamofire.git") { + tag = "5.6.1" + } + } + """.trimIndent() + ) + build(defaultPodInstallSyntheticTaskName) { + assertTasksExecuted(":podGenIos") + + val podfile = "build/cocoapods/synthetic/ios/Podfile" + assertFileInProjectContains( + podfile, + "platform :ios, '14.1'", + "pod 'SSZipArchive'", + "pod 'AFNetworking', '~> 4.0.1'", + "pod 'Alamofire', :git => 'https://github.com/Alamofire/Alamofire.git', :tag => '5.6.1'", + "config.build_settings['EXPANDED_CODE_SIGN_IDENTITY'] = \"\"", + "config.build_settings['CODE_SIGNING_REQUIRED'] = \"NO\"", + "config.build_settings['CODE_SIGNING_ALLOWED'] = \"NO\"" + ) + } + } + } + + @DisplayName("Synthetic project podfile postprocessing") + @GradleTest + fun testSyntheticProjectPodfilePostprocessing(gradleVersion: GradleVersion) { + nativeProject(templateProjectName, gradleVersion, environmentVariables = environmentVariables) { + preparePodfile("ios-app", ImportMode.FRAMEWORKS) + buildGradleKts.addCocoapodsBlock("""pod("ChatSDK", version = "5.2.1")""") + buildGradleKts.appendText( + """ + + tasks.withType().configureEach { + doLast { + podfile.get().appendText("ENV['SWIFT_VERSION'] = '5'") + } + } + """.trimIndent() + ) + + build(defaultPodInstallSyntheticTaskName) { + assertFileInProjectContains("build/cocoapods/synthetic/ios/Podfile", "ENV['SWIFT_VERSION'] = '5'") + } + } + } + + @DisplayName("Reinstalling pod invalidates UTD") + @GradleTest + fun testPodInstallInvalidatesUTD(gradleVersion: GradleVersion) { + nativeProject(templateProjectName, gradleVersion, environmentVariables = environmentVariables) { + preparePodfile("ios-app", ImportMode.FRAMEWORKS) + buildGradleKts.addPod("AFNetworking") + + build(defaultPodInstallSyntheticTaskName) { + assertTasksExecuted(defaultPodInstallSyntheticTaskName) + assertTrue { projectPath.resolve("build/cocoapods/synthetic/ios/Pods/AFNetworking").toFile().deleteRecursively() } + } + + build(defaultPodInstallSyntheticTaskName) { + assertTasksExecuted(defaultPodInstallSyntheticTaskName) + } + } + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/testbase/BuildOptions.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/testbase/BuildOptions.kt index 0d02d9d9a46..7d60af4dae3 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/testbase/BuildOptions.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/testbase/BuildOptions.kt @@ -76,6 +76,7 @@ data class BuildOptions( val platformLibrariesMode: String? = null, val reinstall: Boolean? = null, val restrictedDistribution: Boolean? = null, + val useXcodeMessageStyle: Boolean? = null, val version: String? = null, ) @@ -227,6 +228,9 @@ data class BuildOptions( nativeOptions.restrictedDistribution?.let { arguments.add("-Pkotlin.native.restrictedDistribution=${it}") } + nativeOptions.useXcodeMessageStyle?.let { + arguments.add("-Pkotlin.native.useXcodeMessageStyle=${it}") + } nativeOptions.version?.let { arguments.add("-Pkotlin.native.version=${it}") } diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/testbase/cocoapodsTestHelpers.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/testbase/cocoapodsTestHelpers.kt index 492023de8b1..73ad6f3a6b2 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/testbase/cocoapodsTestHelpers.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/testbase/cocoapodsTestHelpers.kt @@ -5,15 +5,21 @@ package org.jetbrains.kotlin.gradle.testbase +import org.gradle.testkit.runner.BuildResult import org.gradle.util.GradleVersion import org.jetbrains.kotlin.gradle.BaseGradleIT +import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin +import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_BUILD_TASK_NAME import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_INSTALL_TASK_NAME +import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_SETUP_BUILD_TASK_NAME +import org.jetbrains.kotlin.gradle.util.assertProcessRunResult import org.jetbrains.kotlin.gradle.util.replaceText import org.jetbrains.kotlin.gradle.util.runProcess import java.io.File import java.io.IOException import java.nio.file.Path import kotlin.io.path.* +import kotlin.test.assertTrue import kotlin.test.fail val String.normalizeCocoapadsFrameworkName: String @@ -204,8 +210,37 @@ fun KGPBaseTest.nativeProjectWithCocoapodsAndIosAppPodFile( } } +fun BuildResult.podImportAsserts( + buildScript: Path, + projectName: String? = null, +) { + + val buildScriptText = buildScript.readText() + val taskPrefix = projectName?.let { ":$it" } ?: "" + val podspec = "podspec" + + if ("noPodspec()" in buildScriptText) { + assertTasksSkipped("$taskPrefix:$podspec") + } + + if ("podfile" in buildScriptText) { + assertTasksExecuted("$taskPrefix$podInstallTaskName") + } else { + assertTasksSkipped("$taskPrefix$podInstallTaskName") + } + if (buildScriptText.matches("pod\\(.*\\)".toRegex())) { + assertTasksExecuted(listOf("$taskPrefix:${KotlinCocoapodsPlugin.POD_GEN_TASK_NAME}")) + } + + if (buildScriptText.matches("pod\\(.*\\)".toRegex())) { + assertTasksExecuted("$taskPrefix:$POD_SETUP_BUILD_TASK_NAME", "$taskPrefix:$POD_BUILD_TASK_NAME") + } +} + private val templateProjectName = "native-cocoapods-template" +private val podInstallTaskName = ":$POD_INSTALL_TASK_NAME" + private val shouldInstallLocalCocoapods: Boolean = System.getProperty("installCocoapods").toBoolean() private val cocoapodsInstallationRoot: Path by lazy { createTempDirectory("cocoapods") } private val cocoapodsBinPath: Path by lazy { cocoapodsInstallationRoot.resolve("bin") } @@ -228,8 +263,12 @@ private fun gem(vararg args: String): String { val command = listOf("gem", *args) println("Run command: ${command.joinToString(separator = " ")}") val result = runProcess(command, File("."), options = BaseGradleIT.BuildOptions(forceOutputToStdout = true)) - check(result.isSuccessful) { - "Process 'gem ${args.joinToString(separator = " ")}' exited with error code ${result.exitCode}. See log for details." + + assertProcessRunResult(result) { + assertTrue( + result.isSuccessful, + "Process 'gem ${args.joinToString(separator = " ")}' exited with error code ${result.exitCode}. See log for details." + ) } return result.output } diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/testbase/tasksAssertions.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/testbase/tasksAssertions.kt index c27eeab5be1..fc52b0484d6 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/testbase/tasksAssertions.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/testbase/tasksAssertions.kt @@ -168,9 +168,10 @@ fun BuildResult.assertNativeTasksClasspath( fun TestProject.buildAndAssertAllTasks( registeredTasks: List = emptyList(), notRegisteredTasks: List = emptyList(), + buildOptions: BuildOptions = this.buildOptions, environmentVariables: EnvironmentalVariables = EnvironmentalVariables() ) { - build("tasks", "--all", environmentVariables = environmentVariables) { + build("tasks", "--all", buildOptions = buildOptions, environmentVariables = environmentVariables) { assertTasksInBuildOutput(registeredTasks, notRegisteredTasks) } } diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/native-cocoapods-commonization/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/native-cocoapods-commonization/build.gradle.kts index e223985b2ea..b8b4aea43f4 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/native-cocoapods-commonization/build.gradle.kts +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/native-cocoapods-commonization/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - id("org.jetbrains.kotlin.multiplatform").version("") - id("org.jetbrains.kotlin.native.cocoapods").version("") + id("org.jetbrains.kotlin.multiplatform") + id("org.jetbrains.kotlin.native.cocoapods") } repositories { diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/native-cocoapods-dependant-pods/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/native-cocoapods-dependant-pods/build.gradle.kts index 7547313e1f1..0df5b84b355 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/native-cocoapods-dependant-pods/build.gradle.kts +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/native-cocoapods-dependant-pods/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - kotlin("multiplatform") version "" - kotlin("native.cocoapods") version "" + kotlin("multiplatform") + kotlin("native.cocoapods") } group = "org.jetbrains.kotlin.sample.native"