migrated git connected cocoapods test to new test dsl (junit 5 and gradle test kit)

#KT-51553 In Progress
This commit is contained in:
Dmitrii Krasnov
2023-05-25 10:58:41 +02:00
committed by Space Team
parent 35b475f9f7
commit 267068546f
13 changed files with 1068 additions and 561 deletions
@@ -87,6 +87,7 @@ dependencies {
testImplementation(commonDependency("org.jetbrains.intellij.deps", "trove4j"))
testImplementation(commonDependency("io.ktor", "ktor-server-test-host"))
testImplementation(commonDependency("io.ktor", "ktor-server-core"))
testImplementation(commonDependency("io.ktor", "ktor-client-cio"))
testImplementation(commonDependency("io.ktor", "ktor-server-netty"))
testImplementation(commonDependency("io.ktor", "ktor-client-mock"))
@@ -8,13 +8,13 @@ package org.jetbrains.kotlin.gradle
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import io.ktor.application.*
import io.ktor.http.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.util.*
import io.ktor.util.collections.*
import org.gradle.util.GradleVersion
@@ -9,6 +9,7 @@ import org.gradle.api.JavaVersion
import org.gradle.testkit.runner.BuildResult
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.testbase.*
import org.jetbrains.kotlin.gradle.util.replaceText
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.condition.OS
import kotlin.io.path.absolutePathString
@@ -574,6 +575,8 @@ class AppleFrameworkIT : KGPBaseTest() {
)
}
subProject("iosApp").buildGradleKts.replaceText("<applePluginTestVersion>", "\"${TestVersions.AppleGradlePlugin.V222_0_21}\"")
build(*dependencyInsight("iosAppIosX64DebugImplementation")) {
assertContainsVariant("mainDynamicDebugFrameworkIos")
}
@@ -0,0 +1,509 @@
/*
* 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 io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.util.*
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import org.gradle.testkit.runner.BuildResult
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.*
import org.jetbrains.kotlin.gradle.util.assertProcessRunResult
import org.jetbrains.kotlin.gradle.util.capitalize
import org.jetbrains.kotlin.gradle.util.replaceText
import org.jetbrains.kotlin.gradle.util.runProcess
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])
@DisplayName("Git connected K/N tests with cocoapods")
@NativeGradlePluginTests
@GradleTestVersions(minVersion = TestVersions.Gradle.G_7_0)
@OptIn(EnvironmentalVariablesOverride::class)
class CocoaPodsGitIT : KGPBaseTest() {
override val defaultBuildOptions = super.defaultBuildOptions.copy(
nativeOptions = super.defaultBuildOptions.nativeOptions.copy(
cocoapodsGenerateWrapper = true
)
)
private val templateProjectName = "native-cocoapods-template"
private val groovyTemplateProjectName = "native-cocoapods-template-groovy"
private val defaultPodRepo = "https://github.com/AFNetworking/AFNetworking"
private val defaultPodName = "AFNetworking"
private val defaultTarget = "IOS"
private val defaultFamily = "ios"
private val defaultSDK = "iphonesimulator"
private val cinteropTaskName = ":cinterop"
private val defaultCinteropTaskName = cinteropTaskName + defaultPodName + defaultTarget
private val podImportTaskName = ":$POD_IMPORT_TASK_NAME"
private val podspecTaskName = ":$POD_SPEC_TASK_NAME"
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()
private val defaultBuildTaskName = podBuildFullTaskName()
private val defaultSetupBuildTaskName = podSetupBuildFullTaskName()
private fun podGenFullTaskName(familyName: String = defaultFamily) =
podGenTaskName + familyName.capitalize()
private fun podSetupBuildFullTaskName(podName: String = defaultPodName, sdkName: String = defaultSDK) =
podSetupBuildTaskName + podName.capitalize() + sdkName.capitalize()
private fun podBuildFullTaskName(podName: String = defaultPodName, sdkName: String = defaultSDK) =
podBuildTaskName + podName.capitalize() + sdkName.capitalize()
private fun cinteropFullTaskName(podName: String = defaultPodName, targetName: String = defaultTarget) =
cinteropTaskName + podName.capitalize() + targetName.capitalize()
@BeforeAll
fun setUp() {
ensureCocoapodsInstalled()
}
@DisplayName("Downloading pod from git without specifying neither tag not commit")
@GradleTest
fun testPodDownloadGitNoTagNorCommit(gradleVersion: GradleVersion) {
doTestGit(gradleVersion)
}
@DisplayName("Downloading pod from git with specifying tag")
@GradleTest
fun testPodDownloadGitTag(gradleVersion: GradleVersion) {
doTestGit(gradleVersion, tag = "4.0.0")
}
@DisplayName("Downloading pod from git with specifying commit")
@GradleTest
fun testPodDownloadGitCommit(gradleVersion: GradleVersion) {
doTestGit(gradleVersion, commit = "9c07ac0a5645abb58850253eeb109ed0dca515c1")
}
@DisplayName("Downloading pod from git with specifying branch")
@GradleTest
fun testPodDownloadGitBranch(gradleVersion: GradleVersion) {
doTestGit(gradleVersion, branch = "2974")
}
@DisplayName("Downloading pod's subspec from git")
@GradleTest
fun testPodDownloadGitSubspec(gradleVersion: GradleVersion) {
doTestGit(
gradleVersion,
repo = "https://github.com/SDWebImage/SDWebImage.git",
pod = "SDWebImage/MapKit",
tag = "5.9.2"
)
}
@DisplayName("Downloading pod from git with specifying branch and commit")
@GradleTest
fun testPodDownloadGitBranchAndCommit(gradleVersion: GradleVersion) {
doTestGit(
gradleVersion,
branch = "2974",
commit = "21637dd6164c0641e414bdaf3885af6f1ef15aee"
)
}
@DisplayName("Downloading pod from git (tag priority is bigger than branch priority)")
@GradleTest
fun testPodDownloadGitBranchAndTag(gradleVersion: GradleVersion) {
doTestGit(
gradleVersion,
tag = "4.0.0",
branch = "2974"
)
}
@DisplayName("Downloading pod from git with specifying tag for groovy build file")
@GradleTest
fun testGroovyDownloadAndImport(gradleVersion: GradleVersion) {
doTestGit(
gradleVersion,
groovyTemplateProjectName,
tag = "4.0.0",
isGradleBuildScript = true
)
}
@DisplayName("Checks that task cinterop is up to date during the second build")
@GradleTest
fun testCinteropUpToDate(gradleVersion: GradleVersion) {
doTestGit(gradleVersion) {
build(
"syncFramework",
buildOptions = defaultBuildOptions.copy(
nativeOptions = defaultBuildOptions.nativeOptions.copy(
cocoapodsGenerateWrapper = true,
cocoapodsArchs = "x86_64",
cocoapodsConfiguration = "Debug",
cocoapodsPlatform = "iphonesimulator",
)
)
) {
assertTasksUpToDate(
defaultCinteropTaskName
)
}
}
}
@DisplayName("UTD test")
@GradleTest
fun basicUTDTest(gradleVersion: GradleVersion) {
val tasks = listOf(
podspecTaskName,
defaultPodGenTaskName,
defaultPodInstallSyntheticTaskName,
defaultSetupBuildTaskName,
defaultBuildTaskName,
defaultCinteropTaskName,
)
doTestGit(
gradleVersion,
testImportAssertions = { assertTasksExecuted(tasks) }
) {
testImport {
assertTasksUpToDate(tasks)
}
}
}
@DisplayName("UTD with adding and removing pod")
@GradleTest
fun testUTDPodAdded(gradleVersion: GradleVersion) {
nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) {
buildGradleKts.addPod(defaultPodName, produceGitBlock())
testImport()
val anotherPodName = "Alamofire"
val anotherPodRepo = "https://github.com/Alamofire/Alamofire"
buildGradleKts.addPod(anotherPodName, produceGitBlock(anotherPodRepo))
testImport(listOf(defaultPodRepo, anotherPodRepo)) {
assertTasksExecuted(
podspecTaskName,
defaultPodGenTaskName,
podSetupBuildFullTaskName(anotherPodName),
podBuildFullTaskName(anotherPodName),
cinteropFullTaskName(anotherPodName)
)
assertTasksUpToDate(
defaultSetupBuildTaskName,
defaultBuildTaskName,
defaultCinteropTaskName
)
buildGradleKts.removePod(anotherPodName)
testImport {
assertOutputDoesNotContain(podBuildFullTaskName(anotherPodName))
assertOutputDoesNotContain(cinteropFullTaskName(anotherPodName))
assertTasksUpToDate(
defaultBuildTaskName,
defaultSetupBuildTaskName,
defaultCinteropTaskName
)
}
}
}
}
@DisplayName("UTD with adding and removing target")
@GradleTest
fun testUTDTargetAdded(gradleVersion: GradleVersion) {
nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) {
buildGradleKts.addPod(defaultPodName, produceGitBlock())
testImport()
buildGradleKts.addCocoapodsBlock("osx.deploymentTarget = \"10.15\"")
testImport()
val tasks = listOf(
podspecTaskName,
defaultPodGenTaskName,
defaultSetupBuildTaskName,
defaultBuildTaskName,
defaultCinteropTaskName
)
val anotherTarget = "MacosX64"
val anotherSdk = "macosx"
val anotherFamily = "macos"
buildGradleKts.addKotlinBlock(anotherTarget.replaceFirstChar { it.lowercase(Locale.getDefault()) } + "()")
testImport {
assertTasksExecuted(
podGenFullTaskName(anotherFamily),
podSetupBuildFullTaskName(sdkName = anotherSdk),
podBuildFullTaskName(sdkName = anotherSdk),
cinteropFullTaskName(targetName = anotherTarget)
)
assertTasksUpToDate(tasks)
}
buildGradleKts.replaceText(anotherTarget.replaceFirstChar { it.lowercase(Locale.getDefault()) } + "()", "")
testImport {
assertOutputDoesNotContain(podGenFullTaskName(anotherFamily))
assertOutputDoesNotContain(podSetupBuildFullTaskName(sdkName = anotherSdk))
assertOutputDoesNotContain(podBuildFullTaskName(sdkName = anotherSdk))
assertOutputDoesNotContain(cinteropFullTaskName(targetName = anotherTarget))
assertTasksUpToDate(tasks)
}
}
}
@DisplayName("UTD build")
@GradleTest
fun testUTDBuild(gradleVersion: GradleVersion) {
nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) {
buildGradleKts.addPod(defaultPodName, produceGitBlock())
testImport {
assertTasksExecuted(defaultBuildTaskName)
}
val anotherTarget = "MacosX64"
val anotherSdk = "macosx"
val anotherSdkDefaultPodTaskName = podBuildFullTaskName(sdkName = anotherSdk)
buildGradleKts.addCocoapodsBlock("osx.deploymentTarget = \"10.15\"")
buildGradleKts.addKotlinBlock(anotherTarget.replaceFirstChar { it.lowercase(Locale.getDefault()) } + "()")
testImport {
assertTasksUpToDate(defaultBuildTaskName)
assertTasksExecuted(anotherSdkDefaultPodTaskName)
}
testImport {
assertTasksUpToDate(defaultBuildTaskName, anotherSdkDefaultPodTaskName)
}
}
}
@DisplayName("Pod Build UTD after clean")
@GradleTest
fun testPodBuildUTDClean(gradleVersion: GradleVersion) {
nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) {
buildGradleKts.addPod(defaultPodName, produceGitBlock())
testImport {
assertTasksExecuted(defaultBuildTaskName)
}
build(":clean")
testImport {
assertTasksExecuted(defaultBuildTaskName)
}
}
}
@DisplayName("Link with use of dynamic framework ")
@GradleTest
fun testUseDynamicFramework(gradleVersion: GradleVersion) {
nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) {
buildGradleKts.addPod(defaultPodName, produceGitBlock())
buildGradleKts.addFrameworkBlock("isStatic = false")
build("linkPodDebugFrameworkIOS") {
val framework = projectPath.resolve("build/bin/iOS/podDebugFramework/cocoapods.framework/cocoapods")
val processRunResult = runProcess(
listOf("file", framework.absolutePathString()),
workingDir = projectPath.toFile(),
environmentVariables = environmentVariables.environmentalVariables
)
assertProcessRunResult(processRunResult) {
assertTrue(isSuccessful)
assertTrue(output.contains("dynamically linked shared library"))
}
}
}
}
@DisplayName("Link with use of static framework ")
@GradleTest
fun testUseStaticFramework(gradleVersion: GradleVersion) {
nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) {
buildGradleKts.addPod(defaultPodName, produceGitBlock())
buildGradleKts.addFrameworkBlock("isStatic = true")
build("linkPodDebugFrameworkIOS") {
val framework = projectPath.resolve("build/bin/iOS/podDebugFramework/cocoapods.framework/cocoapods")
val processRunResult = runProcess(
listOf("file", framework.absolutePathString()),
workingDir = projectPath.toFile(),
environmentVariables = environmentVariables.environmentalVariables
)
assertProcessRunResult(processRunResult) {
assertTrue(isSuccessful)
assertTrue(output.contains("current ar archive"))
}
}
}
}
@DisplayName("UTD with different spec repo")
@GradleTest
fun testUTDPodGen(gradleVersion: GradleVersion) {
nativeProjectWithCocoapodsAndIosAppPodFile(gradleVersion = gradleVersion) {
buildGradleKts.addPod(defaultPodName)
val repos = listOf(
"https://github.com/alozhkin/spec_repo_example",
"https://github.com/alozhkin/spec_repo_example_2"
)
isRepoAvailable(repos)
build(defaultPodGenTaskName) {
assertTasksExecuted(defaultPodGenTaskName)
}
buildGradleKts.addSpecRepo("https://github.com/alozhkin/spec_repo_example")
build(defaultPodGenTaskName) {
assertTasksExecuted(defaultPodGenTaskName)
}
buildGradleKts.addSpecRepo("https://github.com/alozhkin/spec_repo_example_2")
build(defaultPodGenTaskName) {
assertTasksExecuted(defaultPodGenTaskName)
}
build(defaultPodGenTaskName) {
assertTasksUpToDate(defaultPodGenTaskName)
}
}
}
private fun doTestGit(
gradleVersion: GradleVersion,
projectName: String = templateProjectName,
repo: String = defaultPodRepo,
pod: String = defaultPodName,
branch: String? = null,
commit: String? = null,
tag: String? = null,
isGradleBuildScript: Boolean = false,
testImportAssertions: BuildResult.() -> Unit = {},
block: TestProject.() -> Unit = {},
) {
nativeProjectWithCocoapodsAndIosAppPodFile(projectName, gradleVersion) {
val buildScript = if (isGradleBuildScript) buildGradle else buildGradleKts
buildScript.addPod(pod, produceGitBlock(repo, branch, commit, tag))
testImport(listOf(repo)) {
podImportAsserts(buildScript)
testImportAssertions()
}
block()
}
}
private fun produceGitBlock(
repo: String = defaultPodRepo,
branchName: String? = null,
commitName: String? = null,
tagName: String? = null,
): String {
val branch = if (branchName != null) "branch = \"$branchName\"" else ""
val commit = if (commitName != null) "commit = \"$commitName\"" else ""
val tag = if (tagName != null) "tag = \"$tagName\"" else ""
return """source = git("$repo") {
| $branch
| $commit
| $tag
|}
""".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<String>) = runBlocking {
HttpClient(CIO).use { client ->
val nonAvailableRepos = repos
.map { repo ->
async { repo to runCatching { client.get(repo).status }.recover { it }.getOrNull() }
}
.awaitAll()
.filter { (_, status) -> status != HttpStatusCode.OK }
Assumptions.assumeTrue(nonAvailableRepos.isEmpty()) {
"The following repositories of ${repos.joinToString()} are not available: ${nonAvailableRepos.joinToString()}"
}
}
}
private fun TestProject.testImport(
repos: List<String> = listOf(defaultPodRepo),
vararg args: String,
assertions: BuildResult.() -> Unit = {},
) {
isRepoAvailable(repos)
build(podImportTaskName, *args) {
assertions()
}
}
}
@@ -13,7 +13,9 @@ import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Compan
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.util.createTempDir
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.util.modify
import org.jetbrains.kotlin.gradle.util.runProcess
import org.jetbrains.kotlin.konan.target.HostManager
@@ -22,7 +24,6 @@ import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import java.io.File
import java.io.IOException
import java.util.*
import java.util.concurrent.TimeUnit
import java.util.zip.ZipFile
@@ -40,7 +41,7 @@ class CocoaPodsIT : BaseGradleIT() {
private val gradleVersion = GradleVersionRequired.FOR_MPP_SUPPORT
override fun defaultBuildOptions(): BuildOptions =
super.defaultBuildOptions().copy(customEnvironmentVariables = getEnvs())
super.defaultBuildOptions().copy(customEnvironmentVariables = cocoaPodsEnvironmentVariables())
private val podfileImportDirectivePlaceholder = "<import_mode_directive>"
private val podfileImportPodPlaceholder = "#import_pod_directive"
@@ -48,54 +49,24 @@ class CocoaPodsIT : BaseGradleIT() {
private val cocoapodsSingleKtPod = "native-cocoapods-single"
private val cocoapodsMultipleKtPods = "native-cocoapods-multiple"
private val templateProjectName = "native-cocoapods-template"
private val groovyTemplateProjectName = "native-cocoapods-template-groovy"
private val cocoapodsTestsProjectName = "native-cocoapods-tests"
private val cocoapodsCommonizationProjectName = "native-cocoapods-commonization"
private val dummyTaskName = ":$DUMMY_FRAMEWORK_TASK_NAME"
private val podspecTaskName = ":$POD_SPEC_TASK_NAME"
private val podGenTaskName = ":$POD_GEN_TASK_NAME"
private val podBuildTaskName = ":$POD_BUILD_TASK_NAME"
private val podSetupBuildTaskName = ":$POD_SETUP_BUILD_TASK_NAME"
private val podImportTaskName = ":$POD_IMPORT_TASK_NAME"
private val podInstallTaskName = ":$POD_INSTALL_TASK_NAME"
private val cinteropTaskName = ":cinterop"
private val defaultPodRepo = "https://github.com/AFNetworking/AFNetworking"
private val defaultPodName = "AFNetworking"
private val defaultTarget = "IOS"
private val defaultFamily = "ios"
private val defaultSDK = "iphonesimulator"
private val defaultPodGenTaskName = podGenFullTaskName()
private val defaultPodInstallSyntheticTaskName = ":podInstallSyntheticIos"
private val defaultBuildTaskName = podBuildFullTaskName()
private val defaultSetupBuildTaskName = podSetupBuildFullTaskName()
private val defaultCinteropTaskName = cinteropTaskName + defaultPodName + defaultTarget
private fun podGenFullTaskName(familyName: String = defaultFamily) =
podGenTaskName + familyName.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
private fun podSetupBuildFullTaskName(podName: String = defaultPodName, sdkName: String = defaultSDK) =
podSetupBuildTaskName + podName.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } + sdkName.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(
Locale.getDefault()
) else it.toString()
}
private fun podBuildFullTaskName(podName: String = defaultPodName, sdkName: String = defaultSDK) =
podBuildTaskName + podName.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } + sdkName.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(
Locale.getDefault()
) else it.toString()
}
private fun cinteropFullTaskName(podName: String = defaultPodName, targetName: String = defaultTarget) =
cinteropTaskName + podName.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } + targetName.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(
Locale.getDefault()
) else it.toString()
}
private lateinit var hooks: CustomHooks
private lateinit var project: BaseGradleIT.Project
@@ -154,7 +125,8 @@ class CocoaPodsIT : BaseGradleIT() {
@Test
fun testSyntheticProjectPodfileGeneration() {
val gradleProject = transformProjectWithPluginsDsl(cocoapodsSingleKtPod, gradleVersion)
gradleProject.gradleBuildScript().appendToCocoapodsBlock("""
gradleProject.gradleBuildScript().appendToCocoapodsBlock(
"""
ios.deploymentTarget = "14.1"
pod("SSZipArchive")
pod("AFNetworking", "~> 4.0.1")
@@ -163,7 +135,8 @@ class CocoaPodsIT : BaseGradleIT() {
tag = "5.6.1"
}
}
""".trimIndent())
""".trimIndent()
)
gradleProject.build("podInstallSyntheticIos", "-Pkotlin.native.cocoapods.generate.wrapper=true") {
assertSuccessful()
assertTasksExecuted(":podGenIos")
@@ -184,14 +157,16 @@ class CocoaPodsIT : BaseGradleIT() {
project.gradleBuildScript().apply {
appendToCocoapodsBlock("""pod("ChatSDK", version = "5.2.1")""")
appendText("""
appendText(
"""
tasks.withType<org.jetbrains.kotlin.gradle.targets.native.tasks.PodGenTask>().configureEach {
doLast {
podfile.get().appendText("ENV['SWIFT_VERSION'] = '5'")
}
}
""".trimIndent())
""".trimIndent()
)
}
project.build("podInstallSyntheticIos", "-Pkotlin.native.cocoapods.generate.wrapper=true") {
@@ -200,65 +175,6 @@ class CocoaPodsIT : BaseGradleIT() {
}
}
@Test
fun testPodDownloadGitNoTagNorCommit() {
doTestGit()
}
@Test
fun testPodDownloadGitTag() {
doTestGit(tag = "4.0.0")
}
@Test
fun testPodDownloadGitCommit() {
doTestGit(commit = "9c07ac0a5645abb58850253eeb109ed0dca515c1")
}
@Test
fun testPodDownloadGitBranch() {
doTestGit(branch = "2974")
}
@Test
fun testPodDownloadGitSubspec() {
doTestGit(
repo = "https://github.com/SDWebImage/SDWebImage.git",
pod = "SDWebImage/MapKit",
tag = "5.9.2"
)
}
@Test
fun testPodDownloadGitBranchAndCommit() {
val branch = "2974"
val commit = "21637dd6164c0641e414bdaf3885af6f1ef15aee"
with(project.gradleBuildScript()) {
addPod(defaultPodName, produceGitBlock(defaultPodRepo, branchName = branch, commitName = commit))
}
project.testImportWithAsserts(listOf(defaultPodRepo))
}
// tag priority is bigger than branch priority
@Test
fun testPodDownloadGitBranchAndTag() {
val branch = "2974"
val tag = "4.0.0"
with(project.gradleBuildScript()) {
addPod(defaultPodName, produceGitBlock(defaultPodRepo, branchName = branch, tagName = tag))
}
project.testImportWithAsserts(listOf(defaultPodRepo))
}
@Test
fun testDownloadAndImport() {
val tag = "4.0.0"
with(project.gradleBuildScript()) {
addPod(defaultPodName, produceGitBlock(defaultPodRepo, tagName = tag))
}
project.testImportWithAsserts(listOf(defaultPodRepo))
}
@Test
fun warnIfDeprecatedPodspecPathIsUsed() {
project = getProjectByName(cocoapodsSingleKtPod)
@@ -306,13 +222,15 @@ class CocoaPodsIT : BaseGradleIT() {
@Test
fun testImportUTDAfterLinkingFramework() {
val linkTaskName = ":linkPodDebugFrameworkIOS"
project.gradleBuildScript().appendToCocoapodsBlock("""
project.gradleBuildScript().appendToCocoapodsBlock(
"""
framework {
baseName = "kotlin-library"
}
name = "kotlin-library"
podfile = project.file("ios-app/Podfile")
""".trimIndent())
""".trimIndent()
)
hooks.addHook {
@@ -335,13 +253,15 @@ class CocoaPodsIT : BaseGradleIT() {
@Test
fun testChangeFrameworkTypeUTD() {
project.gradleBuildScript().appendToCocoapodsBlock("""
project.gradleBuildScript().appendToCocoapodsBlock(
"""
framework {
baseName = "kotlin-library"
}
name = "kotlin-library"
podfile = project.file("ios-app/Podfile")
""".trimIndent())
""".trimIndent()
)
hooks.addHook {
assertTasksExecuted(dummyTaskName)
@@ -371,31 +291,6 @@ class CocoaPodsIT : BaseGradleIT() {
}
@Test
fun basicUTDTest() {
val tasks = listOf(
podspecTaskName,
defaultPodGenTaskName,
defaultPodInstallSyntheticTaskName,
defaultSetupBuildTaskName,
defaultBuildTaskName,
defaultCinteropTaskName,
)
with(project.gradleBuildScript()) {
addPod(defaultPodName, produceGitBlock(defaultPodRepo))
}
hooks.addHook {
assertTasksExecuted(tasks)
}
project.testImportWithAsserts(listOf(defaultPodRepo))
hooks.rewriteHooks {
assertTasksUpToDate(tasks)
}
project.testImport(listOf(defaultPodRepo))
}
@Test
fun testSpecReposUTD() {
with(project.gradleBuildScript()) {
@@ -433,53 +328,6 @@ class CocoaPodsIT : BaseGradleIT() {
project.testSynthetic(defaultPodInstallSyntheticTaskName)
}
@Test
fun testUTDPodAdded() {
with(project.gradleBuildScript()) {
addPod(defaultPodName, produceGitBlock(defaultPodRepo))
}
project.testImport(listOf(defaultPodRepo))
val anotherPodName = "Alamofire"
val anotherPodRepo = "https://github.com/Alamofire/Alamofire"
with(project.gradleBuildScript()) {
addPod(anotherPodName, produceGitBlock(anotherPodRepo))
}
hooks.rewriteHooks {
assertTasksExecuted(
podspecTaskName,
defaultPodGenTaskName,
podSetupBuildFullTaskName(anotherPodName),
podBuildFullTaskName(anotherPodName),
cinteropFullTaskName(anotherPodName)
)
assertTasksUpToDate(
defaultSetupBuildTaskName,
defaultBuildTaskName,
defaultCinteropTaskName
)
}
project.testImport(listOf(defaultPodRepo, anotherPodRepo))
with(project.gradleBuildScript()) {
removePod(anotherPodName)
}
hooks.rewriteHooks {
assertTasksNotRegisteredByPrefix(
listOf(
podBuildFullTaskName(anotherPodName),
cinteropFullTaskName(anotherPodName)
)
)
assertTasksUpToDate(
defaultBuildTaskName,
defaultSetupBuildTaskName,
defaultCinteropTaskName
)
}
project.testImport(listOf(defaultPodRepo))
}
@Test
fun testImportSubspecs() {
with(project.gradleBuildScript()) {
@@ -489,62 +337,6 @@ class CocoaPodsIT : BaseGradleIT() {
project.testImport(listOf(defaultPodRepo))
}
@Test
fun testUTDTargetAdded() {
with(project.gradleBuildScript()) {
addPod(defaultPodName, produceGitBlock(defaultPodRepo))
appendToCocoapodsBlock("osx.deploymentTarget = \"10.15\"")
}
project.testImport(listOf(defaultPodRepo))
val anotherTarget = "MacosX64"
val anotherSdk = "macosx"
val anotherFamily = "macos"
with(project.gradleBuildScript()) {
appendToKotlinBlock(anotherTarget.replaceFirstChar { it.lowercase(Locale.getDefault()) } + "()")
}
hooks.rewriteHooks {
assertTasksExecuted(
podGenFullTaskName(anotherFamily),
podSetupBuildFullTaskName(sdkName = anotherSdk),
podBuildFullTaskName(sdkName = anotherSdk),
cinteropFullTaskName(targetName = anotherTarget)
)
assertTasksUpToDate(
podspecTaskName,
defaultPodGenTaskName,
defaultSetupBuildTaskName,
defaultBuildTaskName,
defaultCinteropTaskName
)
}
project.testImport(listOf(defaultPodRepo))
with(project.gradleBuildScript()) {
var text = readText()
text = text.replace(anotherTarget.replaceFirstChar { it.lowercase(Locale.getDefault()) } + "()", "")
writeText(text)
}
hooks.rewriteHooks {
assertTasksNotRegisteredByPrefix(
listOf(
podGenFullTaskName(anotherFamily),
podSetupBuildFullTaskName(sdkName = anotherSdk),
podBuildFullTaskName(sdkName = anotherSdk),
cinteropFullTaskName(targetName = anotherTarget)
)
)
assertTasksUpToDate(
podspecTaskName,
defaultPodGenTaskName,
defaultSetupBuildTaskName,
defaultBuildTaskName,
defaultCinteropTaskName
)
}
project.testImport(listOf(defaultPodRepo))
}
@Test
fun testUTDPodspec() {
project.testWithWrapper(podspecTaskName)
@@ -580,83 +372,6 @@ class CocoaPodsIT : BaseGradleIT() {
project.testWithWrapper(podspecTaskName)
}
@Test
fun testUTDPodGen() {
with(project.gradleBuildScript()) {
addPod(defaultPodName)
}
val repos = listOf(
"https://github.com/alozhkin/spec_repo_example",
"https://github.com/alozhkin/spec_repo_example_2"
)
for (repo in repos) {
assumeTrue(isRepoAvailable(repo))
}
hooks.addHook {
assertTasksExecuted(defaultPodGenTaskName)
}
project.testSynthetic(defaultPodGenTaskName)
with(project.gradleBuildScript()) {
addSpecRepo("https://github.com/alozhkin/spec_repo_example")
}
project.testSynthetic(defaultPodGenTaskName)
with(project.gradleBuildScript()) {
addSpecRepo("https://github.com/alozhkin/spec_repo_example_2")
}
project.testSynthetic(defaultPodGenTaskName)
hooks.rewriteHooks {
assertTasksUpToDate(defaultPodGenTaskName)
}
project.testSynthetic(defaultPodGenTaskName)
}
@Test
fun testUTDBuild() {
with(project.gradleBuildScript()) {
addPod(defaultPodName, produceGitBlock())
}
hooks.addHook {
assertTasksExecuted(defaultBuildTaskName)
}
project.testImport()
val anotherTarget = "MacosX64"
val anotherSdk = "macosx"
with(project.gradleBuildScript()) {
appendToCocoapodsBlock("osx.deploymentTarget = \"10.15\"")
appendToKotlinBlock(anotherTarget.replaceFirstChar { it.lowercase(Locale.getDefault()) } + "()")
}
val anotherSdkDefaultPodTaskName = podBuildFullTaskName(sdkName = anotherSdk)
hooks.rewriteHooks {
assertTasksUpToDate(defaultBuildTaskName)
assertTasksExecuted(anotherSdkDefaultPodTaskName)
}
project.testImport()
hooks.rewriteHooks {
assertTasksUpToDate(defaultBuildTaskName, anotherSdkDefaultPodTaskName)
}
project.testImport()
}
@Test
fun testPodBuildUTDClean() {
with(project.gradleBuildScript()) {
addPod(defaultPodName, produceGitBlock())
}
hooks.addHook {
assertTasksExecuted(defaultBuildTaskName)
}
project.testImport()
hooks.rewriteHooks {}
project.test(":clean")
hooks.addHook {
assertTasksExecuted(defaultBuildTaskName)
}
project.testImport()
}
@Test
fun testPodInstallWithoutPodFile() {
@@ -664,19 +379,6 @@ class CocoaPodsIT : BaseGradleIT() {
}
// groovy tests
@Test
fun testGroovyDownloadAndImport() {
val project = getProjectByName(groovyTemplateProjectName)
val tag = "4.0.0"
with(project.gradleBuildScript()) {
addPod(defaultPodName, produceGitBlock(defaultPodRepo, tagName = tag))
}
project.testImportWithAsserts(listOf(defaultPodRepo))
}
// other tests
@Test
@@ -742,47 +444,6 @@ class CocoaPodsIT : BaseGradleIT() {
}
}
@Test
fun testUseDynamicFramework() {
with(project) {
gradleBuildScript().addPod(defaultPodName, produceGitBlock(defaultPodRepo))
gradleBuildScript().appendToFrameworkBlock("isStatic = false")
hooks.addHook {
// Check that an output framework is a dynamic framework
val framework = fileInWorkingDir("build/bin/iOS/podDebugFramework/cocoapods.framework/cocoapods")
with(runProcess(listOf("file", framework.absolutePath), projectDir, environmentVariables = getEnvs())) {
assertTrue(isSuccessful)
assertTrue(output.contains("dynamically linked shared library"))
}
}
test(
"linkPodDebugFrameworkIOS",
"-Pkotlin.native.cocoapods.generate.wrapper=true"
)
}
}
@Test
fun testUseStaticFramework() {
with(project) {
gradleBuildScript().addPod(defaultPodName, produceGitBlock(defaultPodRepo))
gradleBuildScript().appendToFrameworkBlock("isStatic = true")
hooks.addHook {
// Check that an output framework is a static framework
val framework = fileInWorkingDir("build/bin/iOS/podDebugFramework/cocoapods.framework/cocoapods")
with(runProcess(listOf("file", framework.absolutePath), projectDir, environmentVariables = getEnvs())) {
assertTrue(isSuccessful)
kotlin.test.assertContains(output, "current ar archive")
}
}
test(
"linkPodDebugFrameworkIOS",
"-Pkotlin.native.cocoapods.generate.wrapper=true"
)
}
}
@Test
fun testCocoapodsWithRegularFrameworkDefinition() {
@@ -970,23 +631,6 @@ class CocoaPodsIT : BaseGradleIT() {
getProjectByName(cocoapodsTestsProjectName).testWithWrapper(":iosX64Test")
}
@Test
fun testCinteropUpToDate() {
project.gradleBuildScript().addPod(defaultPodName, produceGitBlock(defaultPodRepo))
project.testImport()
hooks.addHook {
assertTasksUpToDate(
defaultCinteropTaskName
)
}
project.test(
"syncFramework",
"-Pkotlin.native.cocoapods.platform=iphonesimulator",
"-Pkotlin.native.cocoapods.archs=x86_64",
"-Pkotlin.native.cocoapods.configuration=Debug",
"-Pkotlin.native.cocoapods.generate.wrapper=true"
)
}
@Test
fun testCinteropCommonizationOff() {
@@ -1116,11 +760,13 @@ class CocoaPodsIT : BaseGradleIT() {
@Test
fun testLinkOnlyPods() = with(project) {
gradleBuildScript().appendToCocoapodsBlock("""
gradleBuildScript().appendToCocoapodsBlock(
"""
pod("AFNetworking") { linkOnly = true }
pod("SSZipArchive", linkOnly = true)
pod("SDWebImage/Core")
""".trimIndent())
""".trimIndent()
)
build(
":linkPodDebugFrameworkIOS",
@@ -1136,31 +782,37 @@ class CocoaPodsIT : BaseGradleIT() {
assertTasksNotRegistered(":cinteropAFNetworkingIOS")
assertTasksNotRegistered(":cinteropSSZipArchiveIOS")
assertContains("""
assertContains(
"""
| -linker-option
| -framework
| -linker-option
| AFNetworking
""".trimMargin())
""".trimMargin()
)
assertContains("""
assertContains(
"""
| -linker-option
| -framework
| -linker-option
| SSZipArchive
""".trimMargin())
""".trimMargin()
)
}
}
@Test
fun testUsageLinkOnlyWithStaticFrameworkProducesMessage() = with(project) {
gradleBuildScript().appendToCocoapodsBlock("""
gradleBuildScript().appendToCocoapodsBlock(
"""
framework {
isStatic = true
}
pod("AFNetworking") { linkOnly = true }
""".trimIndent())
""".trimIndent()
)
build(
":linkPodDebugFrameworkIOS",
@@ -1184,9 +836,11 @@ class CocoaPodsIT : BaseGradleIT() {
@Test
fun `configuration fails when trying to depend on non-declared pod`() = with(getProjectByName("native-cocoapods-dependant-pods")) {
gradleBuildScript().appendToCocoapodsBlock("""
gradleBuildScript().appendToCocoapodsBlock(
"""
pod("Foo") { useInteropBindingFrom("JBNonExistent") }
""".trimIndent())
""".trimIndent()
)
build(
":help",
@@ -1199,10 +853,12 @@ class CocoaPodsIT : BaseGradleIT() {
@Test
fun `configuration fails when dependant pods are in the wrong order`() = with(getProjectByName("native-cocoapods-dependant-pods")) {
gradleBuildScript().appendToCocoapodsBlock("""
gradleBuildScript().appendToCocoapodsBlock(
"""
pod("Foo") { useInteropBindingFrom("Bar") }
pod("Bar")
""".trimIndent())
""".trimIndent()
)
build(
":help",
@@ -1215,9 +871,11 @@ class CocoaPodsIT : BaseGradleIT() {
@Test
fun `configuration fails when pod depends on itself`() = with(getProjectByName("native-cocoapods-dependant-pods")) {
gradleBuildScript().appendToCocoapodsBlock("""
gradleBuildScript().appendToCocoapodsBlock(
"""
pod("Foo") { useInteropBindingFrom("Foo") }
""".trimIndent())
""".trimIndent()
)
build(
":help",
@@ -1249,22 +907,10 @@ class CocoaPodsIT : BaseGradleIT() {
}
}
private fun doTestGit(
repo: String = defaultPodRepo,
pod: String = defaultPodName,
branch: String? = null,
commit: String? = null,
tag: String? = null
) {
with(project.gradleBuildScript()) {
addPod(pod, produceGitBlock(repo, branch, commit, tag))
}
project.testImportWithAsserts(listOf(repo))
}
private fun Project.testImportWithAsserts(
repos: List<String> = listOf(),
vararg args: String
vararg args: String,
) {
hooks.addHook {
podImportAsserts()
@@ -1274,7 +920,7 @@ class CocoaPodsIT : BaseGradleIT() {
private fun Project.testImport(
repos: List<String> = listOf(),
vararg args: String
vararg args: String,
) {
for (repo in repos) {
assumeTrue(isRepoAvailable(repo))
@@ -1284,21 +930,21 @@ class CocoaPodsIT : BaseGradleIT() {
private fun Project.testSynthetic(
taskName: String,
vararg args: String
vararg args: String,
) {
testWithWrapper(taskName, *args)
}
private fun Project.testWithWrapper(
taskName: String,
vararg args: String
vararg args: String,
) {
test(taskName, "-Pkotlin.native.cocoapods.generate.wrapper=true", *args)
}
private fun Project.test(
taskName: String,
vararg args: String
vararg args: String,
) {
// check that test executable
@@ -1320,25 +966,6 @@ class CocoaPodsIT : BaseGradleIT() {
appendToCocoapodsBlock(podBlock)
}
private fun File.removePod(podName: String) {
val text = readText()
val begin = text.indexOf("""pod("$podName")""")
require(begin != -1) { "Pod doesn't exist in file" }
var index = begin + """pod("$podName")""".length - 1
if (text.indexOf("""pod("$podName") {""", startIndex = begin) != -1) {
index += 2
var bracket = 1
while (bracket != 0) {
if (text[++index] == '{') {
bracket++
} else if (text[index] == '}') {
bracket--
}
}
}
writeText(text.removeRange(begin..index))
}
private fun File.addSpecRepo(specRepo: String) = appendToCocoapodsBlock("url(\"$specRepo\")".wrap("specRepos"))
private fun File.appendToKotlinBlock(str: String) = appendLine(str.wrap("kotlin"))
@@ -1355,23 +982,6 @@ class CocoaPodsIT : BaseGradleIT() {
private fun File.appendLine(s: String) = appendText("\n$s")
private fun produceGitBlock(
repo: String = defaultPodRepo,
branchName: String? = null,
commitName: String? = null,
tagName: String? = null
): String {
val branch = if (branchName != null) "branch = \"$branchName\"" else ""
val commit = if (commitName != null) "commit = \"$commitName\"" else ""
val tag = if (tagName != null) "tag = \"$tagName\"" else ""
return """source = git("$repo") {
| $branch
| $commit
| $tag
|}
""".trimMargin()
}
// proposition phase
@@ -1426,15 +1036,10 @@ class CocoaPodsIT : BaseGradleIT() {
}
}
private enum class ImportMode(val directive: String) {
FRAMEWORKS("use_frameworks!"),
MODULAR_HEADERS("use_modular_headers!")
}
private data class CommandResult(
val exitCode: Int,
val stdOut: String,
val stdErr: String
val stdErr: String,
)
private fun runCommand(
@@ -1443,11 +1048,11 @@ class CocoaPodsIT : BaseGradleIT() {
vararg args: String,
timeoutSec: Long = 120,
inheritIO: Boolean = false,
block: CommandResult.() -> Unit
block: CommandResult.() -> Unit,
) {
val process = ProcessBuilder(command, *args).apply {
directory(workingDir)
environment().putAll(getEnvs())
environment().putAll(cocoaPodsEnvironmentVariables())
if (inheritIO) {
inheritIO()
}
@@ -1523,79 +1128,12 @@ class CocoaPodsIT : BaseGradleIT() {
@BeforeClass
@JvmStatic
fun ensureCocoapodsInstalled() {
fun checkCocoapodsInstalled() {
if (!HostManager.hostIsMac) {
return
}
if (shouldInstallLocalCocoapods) {
val installDir = cocoapodsInstallationRoot.absolutePath
println("Installing CocoaPods...")
//https://github.com/ffi/ffi/issues/864#issuecomment-875242776
gem("install", "--install-dir", installDir, "ffi", "-v", "1.15.5", "--", "--enable-libffi-alloc")
gem("install", "--install-dir", installDir, "cocoapods", "-v", localCocoapodsVersion)
} else if (!isCocoapodsInstalled()) {
fail(
"""
Running CocoaPods integration tests requires cocoapods to be installed.
Please install them manually:
gem install cocoapods
Or re-run the tests with the 'installCocoapods=true' Gradle property.
""".trimIndent()
)
}
}
private const val localCocoapodsVersion = "1.11.0"
private val shouldInstallLocalCocoapods: Boolean = System.getProperty("installCocoapods").toBoolean()
private val cocoapodsInstallationRoot: File by lazy { createTempDir("cocoapods") }
private val cocoapodsBinPath: File by lazy {
cocoapodsInstallationRoot.resolve("bin")
}
private fun getEnvs(): Map<String, String> {
if (!shouldInstallLocalCocoapods) {
return emptyMap()
}
val path = cocoapodsBinPath.absolutePath + File.pathSeparator + System.getenv("PATH")
val gemPath = System.getenv("GEM_PATH")?.let {
cocoapodsInstallationRoot.absolutePath + File.pathSeparator + it
} ?: cocoapodsInstallationRoot.absolutePath
return mapOf(
"PATH" to path,
"GEM_PATH" to gemPath,
// CocoaPods 1.11 requires UTF-8 locale being set, more details: https://github.com/CocoaPods/CocoaPods/issues/10939
"LC_ALL" to "en_US.UTF-8"
)
}
private fun isCocoapodsInstalled(): Boolean {
// Do not use 'gem list' because the gem may be installed but PATH may miss its executables.
// Try to access the pod executable directly instead
return try {
val result = runProcess(
listOf("pod", "--version"),
File("."),
)
result.isSuccessful
} catch (e: IOException) {
false
}
}
private fun gem(vararg args: String): String {
val command = listOf("gem", *args)
println("Run command: ${command.joinToString(separator = " ")}")
val result = runProcess(command, File("."), options = BuildOptions(forceOutputToStdout = true))
check(result.isSuccessful) {
"Process 'gem ${args.joinToString(separator = " ")}' exited with error code ${result.exitCode}. See log for details."
}
return result.output
ensureCocoapodsInstalled()
}
}
}
@@ -6,10 +6,11 @@
package org.jetbrains.kotlin.gradle.native
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_INSTALL_TASK_NAME
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.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.condition.OS
import java.nio.file.Path
@@ -24,18 +25,16 @@ import kotlin.test.assertEquals
@OptIn(EnvironmentalVariablesOverride::class)
class CocoaPodsXcodeIT : KGPBaseTest() {
private val podfileImportDirectivePlaceholder = "<import_mode_directive>"
private val cocoapodsSingleKtPod = "native-cocoapods-single"
private val cocoapodsMultipleKtPods = "native-cocoapods-multiple"
private val templateProjectName = "native-cocoapods-template"
private val environmentVariables = EnvironmentalVariables(
mapOf(
// CocoaPods 1.11 requires UTF-8 locale being set, more details: https://github.com/CocoaPods/CocoaPods/issues/10939
"LC_ALL" to "en_US.UTF-8"
)
)
private val environmentVariables = EnvironmentalVariables(cocoaPodsEnvironmentVariables())
@BeforeAll
fun setUp() {
ensureCocoapodsInstalled()
}
@DisplayName("Checks xcodebuild for ios-app with a single framework")
@GradleTest
@@ -142,20 +141,6 @@ class CocoaPodsXcodeIT : KGPBaseTest() {
mapOf("kotlin-library" to "FirstMultiplatformLibrary", "second-library" to "SecondMultiplatformLibrary")
)
private enum class ImportMode(val directive: String) {
FRAMEWORKS("use_frameworks!"),
MODULAR_HEADERS("use_modular_headers!")
}
private fun TestProject.preparePodfile(iosAppLocation: String, mode: ImportMode) {
val iosAppDir = projectPath.resolve(iosAppLocation)
// Set import mode for Podfile.
iosAppDir.resolve("Podfile")
.takeIf { it.exists() }
?.replaceText(podfileImportDirectivePlaceholder, mode.directive)
}
private fun doTestXcode(
projectName: String,
gradleVersion: GradleVersion,
@@ -208,7 +193,7 @@ class CocoaPodsXcodeIT : KGPBaseTest() {
// Set import mode for Podfile.
preparePodfile(it, mode)
// Install pods.
build("$taskPrefix:podInstall", buildOptions = buildOptions)
build("$taskPrefix:$POD_INSTALL_TASK_NAME", buildOptions = buildOptions)
projectPath.resolve(it).apply {
// Run Xcode build.
@@ -68,6 +68,9 @@ data class BuildOptions(
data class NativeOptions(
val cacheKind: NativeCacheKind = NativeCacheKind.NONE,
val cocoapodsGenerateWrapper: Boolean? = null,
val cocoapodsPlatform: String? = null,
val cocoapodsConfiguration: String? = null,
val cocoapodsArchs: String? = null,
val distributionType: String? = null,
val distributionDownloadFromMaven: Boolean? = null,
val platformLibrariesMode: String? = null,
@@ -199,6 +202,15 @@ data class BuildOptions(
nativeOptions.cocoapodsGenerateWrapper?.let {
arguments.add("-Pkotlin.native.cocoapods.generate.wrapper=${it}")
}
nativeOptions.cocoapodsPlatform?.let {
arguments.add("-Pkotlin.native.cocoapods.platform=${it}")
}
nativeOptions.cocoapodsArchs?.let {
arguments.add("-Pkotlin.native.cocoapods.archs=${it}")
}
nativeOptions.cocoapodsConfiguration?.let {
arguments.add("-Pkotlin.native.cocoapods.configuration=${it}")
}
nativeOptions.distributionDownloadFromMaven?.let {
arguments.add("-Pkotlin.native.distribution.downloadFromMaven=${it}")
@@ -70,6 +70,10 @@ interface TestVersions {
;
}
object COCOAPODS {
const val VERSION = "1.11.0"
}
object AppleGradlePlugin {
const val V222_0_21 = "222.4550-0.21"
}
@@ -5,13 +5,25 @@
package org.jetbrains.kotlin.gradle.testbase
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.BaseGradleIT
import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.POD_INSTALL_TASK_NAME
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.appendText
import kotlin.io.path.*
import kotlin.test.fail
val String.normalizeCocoapadsFrameworkName: String
get() = replace('-', '_')
enum class ImportMode(val directive: String) {
FRAMEWORKS("use_frameworks!"),
MODULAR_HEADERS("use_modular_headers!")
}
fun TestProject.useCustomCocoapodsFrameworkName(
subprojectName: String,
frameworkName: String,
@@ -34,10 +46,189 @@ fun TestProject.useCustomCocoapodsFrameworkName(
}
}
/**
* Prepares the Podfile for an iOS app in the [TestProject]
*
* @param iosAppLocation The relative location of the iOS app directory within the [TestProject]
* @param mode The [ImportMode] to be set for the Podfile.
*
*/
fun TestProject.preparePodfile(iosAppLocation: String, mode: ImportMode) {
val iosAppDir = projectPath.resolve(iosAppLocation)
// Set import mode for Podfile.
iosAppDir.resolve("Podfile")
.takeIf { it.exists() }
?.replaceText(podfileImportDirectivePlaceholder, mode.directive)
}
/**
* Wraps the given string into a specRepos block and adds this block to the end of the [this] path.
*
* @param specRepo The code to be wrapped with the Cocoapods block.
*/
fun Path.addSpecRepo(specRepo: String) = addCocoapodsBlock("url(\"$specRepo\")".wrapIntoBlock("specRepos"))
/**
* Wraps the given string into a Cocoapods block and adds this block to the end of the [this] path.
*
* @param str The code to be wrapped with the Cocoapods block.
*/
fun Path.addCocoapodsBlock(str: String) = addKotlinBlock(str.wrapIntoBlock("cocoapods"))
private fun Path.addKotlinBlock(str: String) = appendLine(str.wrapIntoBlock("kotlin"))
/**
* Wraps the given string into a Kotlin block and adds this block to the end of the [this] path.
*
* @param str The code to be wrapped with the Cocoapods block.
*/
fun Path.addKotlinBlock(str: String) = appendLine(str.wrapIntoBlock("kotlin"))
private fun Path.addFrameworkBlock(str: String) = addCocoapodsBlock(str.wrapIntoBlock("framework"))
/**
* Wraps the given string into a Framework block and adds this block to the end of the [this] path.
*
* @param str The code to be wrapped with the Cocoapods block.
*/
fun Path.addFrameworkBlock(str: String) = addCocoapodsBlock(str.wrapIntoBlock("framework"))
private fun Path.appendLine(s: String) = appendText("\n$s")
/**
* Adds a Cocoapods dependency to [this] build script.
*
* @param podName The name of the Cocoapods dependency to be added.
* @param configuration The optional configuration string for the Cocoapods dependency.
*/
fun Path.addPod(podName: String, configuration: String? = null) {
val pod = "pod(\"$podName\")"
val podBlock = configuration?.wrapIntoBlock(pod) ?: pod
addCocoapodsBlock(podBlock)
}
/**
* Removes a Cocoapods dependency from [this] build script.
*
* @param podName The name of the Cocoapods dependency to be removes.
*/
fun Path.removePod(podName: String) {
val text = readText()
val begin = text.indexOf("""pod("$podName")""")
require(begin != -1) {
"""
Pod doesn't exist in file. File content is:
${text}
""".trimIndent()
}
var index = begin + """pod("$podName")""".length - 1
if (text.indexOf("""pod("$podName") {""", startIndex = begin) != -1) {
index += 2
var bracket = 1
while (bracket != 0) {
if (text[++index] == '{') {
bracket++
} else if (text[index] == '}') {
bracket--
}
}
}
writeText(text.removeRange(begin..index))
}
/**
* Method returns required environment variables for cocoapods tests with execution of [POD_INSTALL_TASK_NAME]
*/
fun cocoaPodsEnvironmentVariables(): Map<String, String> {
if (!shouldInstallLocalCocoapods) {
return emptyMap()
}
val path = cocoapodsBinPath.absolutePathString() + File.pathSeparator + System.getenv("PATH")
val gemPath = System.getenv("GEM_PATH")?.let {
cocoapodsInstallationRoot.absolutePathString() + File.pathSeparator + it
} ?: cocoapodsInstallationRoot.absolutePathString()
return mapOf(
"PATH" to path,
"GEM_PATH" to gemPath,
// CocoaPods 1.11 requires UTF-8 locale being set, more details: https://github.com/CocoaPods/CocoaPods/issues/10939
"LC_ALL" to "en_US.UTF-8"
)
}
/**
* This method checks if Cocoapods should be installed and verifies its installation status.
* If [shouldInstallLocalCocoapods] is true, it tries to install Cocoapods into the specified [cocoapodsInstallationRoot]
* if it is not already installed.
*
* @throws AssertionError if [shouldInstallLocalCocoapods] is false and cocoapods has not been installed
*/
fun ensureCocoapodsInstalled() {
if (shouldInstallLocalCocoapods) {
val installDir = cocoapodsInstallationRoot.absolutePathString()
println("Installing CocoaPods...")
//https://github.com/ffi/ffi/issues/864#issuecomment-875242776
gem("install", "--install-dir", installDir, "ffi", "-v", "1.15.5", "--", "--enable-libffi-alloc")
gem("install", "--install-dir", installDir, "cocoapods", "-v", TestVersions.COCOAPODS.VERSION)
} else if (!isCocoapodsInstalled()) {
fail(
"""
Running CocoaPods integration tests requires cocoapods to be installed.
Please install them manually:
gem install cocoapods
Or re-run the tests with the 'installCocoapods=true' Gradle property.
""".trimIndent()
)
}
}
@EnvironmentalVariablesOverride
fun KGPBaseTest.nativeProjectWithCocoapodsAndIosAppPodFile(
projectName: String = templateProjectName,
gradleVersion: GradleVersion,
buildOptions: BuildOptions = this.defaultBuildOptions,
projectBlock: TestProject.() -> Unit = {},
) {
nativeProject(
projectName,
gradleVersion,
buildOptions = buildOptions,
environmentVariables = EnvironmentalVariables(cocoaPodsEnvironmentVariables())
) {
preparePodfile("ios-app", ImportMode.FRAMEWORKS)
projectBlock()
}
}
private val templateProjectName = "native-cocoapods-template"
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") }
private fun isCocoapodsInstalled(): Boolean {
// Do not use 'gem list' because the gem may be installed but PATH may miss its executables.
// Try to access the pod executable directly instead
return try {
val result = runProcess(
listOf("pod", "--version"),
File("."),
)
result.isSuccessful
} catch (e: IOException) {
false
}
}
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."
}
return result.output
}
private fun Path.appendLine(s: String) = appendText("\n$s")
private const val podfileImportDirectivePlaceholder = "<import_mode_directive>"
@@ -1,7 +1,7 @@
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
plugins {
id("org.jetbrains.gradle.apple.applePlugin") version "222.4550-0.21"
id("org.jetbrains.gradle.apple.applePlugin") version <applePluginTestVersion>
}
apple {
@@ -1,6 +1,6 @@
plugins {
id("org.jetbrains.kotlin.multiplatform").version("<pluginMarkerVersion>")
id("org.jetbrains.kotlin.native.cocoapods").version("<pluginMarkerVersion>")
id("org.jetbrains.kotlin.multiplatform")
id("org.jetbrains.kotlin.native.cocoapods")
}
group = "com.example"