migrated FatFrameworkIT to junit 5 and gradle TestKit

This commit is contained in:
Dmitrii Krasnov
2023-04-25 08:24:49 +02:00
committed by Space Team
parent 1591869991
commit 617dddba70
4 changed files with 197 additions and 175 deletions
@@ -1,42 +1,38 @@
/* /*
* 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. * 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 package org.jetbrains.kotlin.gradle.native
import org.jetbrains.kotlin.gradle.* import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.embedProject
import org.jetbrains.kotlin.gradle.tasks.FrameworkLayout import org.jetbrains.kotlin.gradle.tasks.FrameworkLayout
import org.jetbrains.kotlin.gradle.transformProjectWithPluginsDsl import org.jetbrains.kotlin.gradle.testbase.*
import org.jetbrains.kotlin.gradle.util.* import org.jetbrains.kotlin.gradle.util.*
import org.jetbrains.kotlin.konan.target.HostManager import org.junit.jupiter.api.DisplayName
import org.junit.Assume import org.junit.jupiter.api.condition.OS
import org.junit.BeforeClass
import org.junit.Test
import java.io.File
import java.util.* import java.util.*
import kotlin.io.path.absolutePathString
import kotlin.io.path.appendText
import kotlin.test.assertTrue import kotlin.test.assertTrue
class FatFrameworkIT : BaseGradleIT() { @OsCondition(supportedOn = [OS.MAC], enabledOnCI = [OS.MAC])
@DisplayName("Tests for K/N builds with Fat Framework")
@NativeGradlePluginTests
class FatFrameworkIT : KGPBaseTest() {
override val defaultGradleVersion: GradleVersionRequired @DisplayName("Fat framework for iOS smoke test")
get() = GradleVersionRequired.FOR_MPP_SUPPORT @GradleTest
fun smokeIos(gradleVersion: GradleVersion) {
@Test nativeProject("native-fat-framework/smoke", gradleVersion) {
fun smokeIos() {
transformProjectWithPluginsDsl(
"smoke",
directoryPrefix = "native-fat-framework"
).build("fat") {
checkSmokeBuild( checkSmokeBuild(
archs = listOf("x64", "arm64", "arm32"), archs = listOf("x64", "arm64", "arm32"),
targetPrefix = "ios", targetPrefix = "ios",
expectedPlistPlatform = "iPhoneOS" expectedPlistPlatform = "iPhoneOS"
) )
val binary = fileInWorkingDir("build/fat-framework/smoke.framework/smoke") val binary = projectPath.resolve("build/fat-framework/smoke.framework/smoke").absolutePathString()
with(runProcess(listOf("file", binary.absolutePath), project.projectDir)) { assertProcessRunResult(runProcess(listOf("file", binary), projectPath.toFile())) {
assertTrue(isSuccessful) assertTrue(isSuccessful)
assertTrue(output.contains("\\(for architecture x86_64\\):\\s+Mach-O 64-bit dynamically linked shared library x86_64".toRegex())) assertTrue(output.contains("\\(for architecture x86_64\\):\\s+Mach-O 64-bit dynamically linked shared library x86_64".toRegex()))
assertTrue(output.contains("\\(for architecture armv7\\):\\s+Mach-O dynamically linked shared library arm_v7".toRegex())) assertTrue(output.contains("\\(for architecture armv7\\):\\s+Mach-O dynamically linked shared library arm_v7".toRegex()))
@@ -45,216 +41,198 @@ class FatFrameworkIT : BaseGradleIT() {
} }
} }
@Test @DisplayName("Fat framework for watchOS smoke test")
fun smokeWatchos() { @GradleTest
with(transformProjectWithPluginsDsl("smoke", directoryPrefix = "native-fat-framework")) { fun smokeWatchos(gradleVersion: GradleVersion) {
nativeProject(
gradleBuildScript().modify { "native-fat-framework/smoke",
gradleVersion,
// TODO(d.krasnov): return default statisticsForceValidation, when KT-58104 will be fixed
buildOptions = defaultBuildOptions.copy(statisticsForceValidation = false)
) {
buildGradleKts.modify {
it.checkedReplace("iosArm32()", "watchosArm32()") it.checkedReplace("iosArm32()", "watchosArm32()")
.checkedReplace("iosArm64()", "watchosArm64(); watchosDeviceArm64()") .checkedReplace("iosArm64()", "watchosArm64(); watchosDeviceArm64()")
.checkedReplace("iosX64()", "watchosX64()") .checkedReplace("iosX64()", "watchosX64()")
} }
checkSmokeBuild(
build("fat") { archs = listOf("x64", "arm64", "arm32", "deviceArm64"),
checkSmokeBuild( targetPrefix = "watchos",
archs = listOf("x64", "arm64", "arm32", "deviceArm64"), expectedPlistPlatform = "WatchOS"
targetPrefix = "watchos", )
expectedPlistPlatform = "WatchOS" val binary = projectPath.resolve("build/fat-framework/smoke.framework/smoke").absolutePathString()
) assertProcessRunResult(runProcess(listOf("file", binary), projectPath.toFile())) {
assertTrue(isSuccessful)
val binary = fileInWorkingDir("build/fat-framework/smoke.framework/smoke") assertTrue(output.contains("\\(for architecture x86_64\\):\\s+Mach-O 64-bit dynamically linked shared library x86_64".toRegex()))
with(runProcess(listOf("file", binary.absolutePath), projectDir)) { assertTrue(output.contains("\\(for architecture armv7k\\):\\s+Mach-O dynamically linked shared library arm_v7k".toRegex()))
assertTrue(isSuccessful) assertTrue(output.contains("\\(for architecture arm64_32\\):\\s+Mach-O dynamically linked shared library arm64_32_v8".toRegex()))
assertTrue(output.contains("\\(for architecture x86_64\\):\\s+Mach-O 64-bit dynamically linked shared library x86_64".toRegex())) assertTrue(output.contains("\\(for architecture arm64\\):\\s+Mach-O 64-bit dynamically linked shared library arm64".toRegex()))
assertTrue(output.contains("\\(for architecture armv7k\\):\\s+Mach-O dynamically linked shared library arm_v7k".toRegex()))
assertTrue(output.contains("\\(for architecture arm64_32\\):\\s+Mach-O dynamically linked shared library arm64_32_v8".toRegex()))
assertTrue(output.contains("\\(for architecture arm64\\):\\s+Mach-O 64-bit dynamically linked shared library arm64".toRegex()))
}
} }
} }
} }
@Test @DisplayName("Fat framework with MacOS smoke test")
fun smokeMacos() { @GradleTest
with(transformProjectWithPluginsDsl("smoke", directoryPrefix = "native-fat-framework")) { fun smokeMacos(gradleVersion: GradleVersion) {
nativeProject(
gradleBuildScript().modify { "native-fat-framework/smoke",
gradleVersion
) {
buildGradleKts.modify {
it.checkedReplace("iosArm32()", "") it.checkedReplace("iosArm32()", "")
.checkedReplace("iosArm64()", "macosArm64()") .checkedReplace("iosArm64()", "macosArm64()")
.checkedReplace("iosX64()", "macosX64()") .checkedReplace("iosX64()", "macosX64()")
} }
checkSmokeBuild(
build("fat") { archs = listOf("x64", "arm64"),
checkSmokeBuild( targetPrefix = "macos",
archs = listOf("x64", "arm64"), expectedPlistPlatform = "MacOSX",
targetPrefix = "macos", true
expectedPlistPlatform = "MacOSX", )
true val binary = projectPath.resolve("build/fat-framework/smoke.framework/Versions/A/smoke").absolutePathString()
) assertProcessRunResult(runProcess(listOf("file", binary), projectPath.toFile())) {
assertTrue(isSuccessful)
val binary = fileInWorkingDir("build/fat-framework/smoke.framework/Versions/A/smoke") assertTrue(output.contains("\\(for architecture x86_64\\):\\s+Mach-O 64-bit dynamically linked shared library x86_64".toRegex()))
with(runProcess(listOf("file", binary.absolutePath), projectDir)) { assertTrue(output.contains("\\(for architecture arm64\\):\\s+Mach-O 64-bit dynamically linked shared library arm64".toRegex()))
assertTrue(isSuccessful)
assertTrue(output.contains("\\(for architecture x86_64\\):\\s+Mach-O 64-bit dynamically linked shared library x86_64".toRegex()))
assertTrue(output.contains("\\(for architecture arm64\\):\\s+Mach-O 64-bit dynamically linked shared library arm64".toRegex()))
}
} }
} }
} }
private fun CompiledProject.checkSmokeBuild( private fun TestProject.checkSmokeBuild(
archs: List<String>, archs: List<String>,
targetPrefix: String, targetPrefix: String,
expectedPlistPlatform: String, expectedPlistPlatform: String,
isMacosFramework: Boolean = false isMacosFramework: Boolean = false
) { ) {
assertSuccessful() build("fat") {
val linkTasks = archs.map { val linkTasks = archs.map {
":linkDebugFramework${targetPrefix.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }}${ ":linkDebugFramework${targetPrefix.capitalize()}${it.capitalize()}"
it.replaceFirstChar { }
if (it.isLowerCase()) it.titlecase(
Locale.getDefault()
) else it.toString()
}
}" }
assertTasksExecuted(linkTasks) assertTasksExecuted(linkTasks)
assertTasksExecuted(":fat") assertTasksExecuted(":fat")
val frameworkLayout = FrameworkLayout(fileInWorkingDir("build/fat-framework/smoke.framework"), isMacosFramework) val frameworkLayout = FrameworkLayout(projectPath.resolve("build/fat-framework/smoke.framework").toFile(), isMacosFramework)
fun File.projectRelative() = relativeTo(File(workingDir, project.projectName)).path assertFileExists(frameworkLayout.binary.toPath())
assertFileExists(frameworkLayout.header.toPath())
assertFileExists(frameworkLayout.dSYM.binary.toPath())
assertFileExists(frameworkLayout.binary.projectRelative()) assertFileContains(
assertFileExists(frameworkLayout.header.projectRelative()) frameworkLayout.header.toPath(),
assertFileExists(frameworkLayout.dSYM.binary.projectRelative()) "+ (int32_t)foo __attribute__((swift_name(\"foo()\")));"
)
val headerContent = frameworkLayout.header.readText() assertFileContains(
assertTrue( frameworkLayout.infoPlist.toPath(),
headerContent.contains("+ (int32_t)foo __attribute__((swift_name(\"foo()\")));"),
"Unexpected header content:\n$headerContent"
)
val plistContent = frameworkLayout.infoPlist
.readLines()
.joinToString(separator = "\n") { it.trim() }
assertTrue(
plistContent.contains(
""" """
<key>CFBundleSupportedPlatforms</key> | <key>CFBundleSupportedPlatforms</key>
<array> | <array>
<string>$expectedPlistPlatform</string> | <string>$expectedPlistPlatform</string>
</array> | </array>
""".trimIndent() """.trimMargin()
), .replace(Regex(" {4}"), "\t")
"Unexpected Info.plist content:\n$plistContent" )
) }
} }
@Test @DisplayName("Fat framework with duplicated iOS Arm 64")
fun testDuplicatedArchitecture() { @GradleTest
with(transformProjectWithPluginsDsl("smoke", directoryPrefix = "native-fat-framework")) { fun testDuplicatedArchitecture(gradleVersion: GradleVersion) {
gradleBuildScript().modify { nativeProject("native-fat-framework/smoke", gradleVersion) {
it + """ buildGradleKts.modify {
val anotherDeviceTarget = kotlin.iosArm64("another") { it +
binaries.framework("DEBUG") //language=kotlin
} """
fat.from(anotherDeviceTarget.binaries.getFramework("DEBUG")) val anotherDeviceTarget = kotlin.iosArm64("another") {
""".trimIndent() binaries.framework("DEBUG")
}
fat.from(anotherDeviceTarget.binaries.getFramework("DEBUG"))
""".trimIndent()
} }
build("fat") { buildAndFail("fat") {
assertFailed() assertOutputContains("This fat framework already has a binary for architecture `arm64`")
assertContains("This fat framework already has a binary for architecture `arm64`")
} }
} }
} }
@Test @DisplayName("Fat framework with incorrect 'osx' family")
fun testIncorrectFamily() { @GradleTest
with(transformProjectWithPluginsDsl("smoke", directoryPrefix = "native-fat-framework")) { fun testIncorrectFamily(gradleVersion: GradleVersion) {
gradleBuildScript().modify { nativeProject("native-fat-framework/smoke", gradleVersion) {
it + """ buildGradleKts.modify {
val macos = kotlin.macosX64 { it +
binaries.framework("DEBUG") //language=kotlin
} """
fat.from(macos.binaries.getFramework("DEBUG")) val macos = kotlin.macosX64 {
""".trimIndent() binaries.framework("DEBUG")
}
fat.from(macos.binaries.getFramework("DEBUG"))
""".trimIndent()
} }
build("fat") { buildAndFail("fat") {
assertFailed() assertOutputContains("Cannot add a binary with platform family 'osx' to the fat framework")
assertContains("Cannot add a binary with platform family 'osx' to the fat framework")
} }
} }
} }
@Test @DisplayName("Fat framework with custom base name")
fun testCustomName() { @GradleTest
with(transformProjectWithPluginsDsl("smoke", directoryPrefix = "native-fat-framework")) { fun testCustomName(gradleVersion: GradleVersion) {
gradleBuildScript().modify { nativeProject("native-fat-framework/smoke", gradleVersion) {
it.addBeforeSubstring("baseName = \"custom\"\n","from(frameworksToMerge)") buildGradleKts.modify {
it.addBeforeSubstring("baseName = \"custom\"\n", "from(frameworksToMerge)")
} }
build("fat") { build("fat") {
val binary = fileInWorkingDir("build/fat-framework/custom.framework/custom") val binary = projectPath.resolve("build/fat-framework/custom.framework/custom").absolutePathString()
with(runProcess(listOf("otool", "-D", binary.absolutePath), project.projectDir)) { assertProcessRunResult(runProcess(listOf("otool", "-D", binary), projectPath.toFile())) {
assertSuccessful() assertTrue(isSuccessful)
assertTrue { output.lines().any { it.contains("@rpath/custom.framework/custom") } } assertTrue { output.lines().any { it.contains("@rpath/custom.framework/custom") } }
} }
} }
} }
} }
@Test @DisplayName("Fat framework with different input types")
fun testDifferentTypes() { @GradleTest
with(transformProjectWithPluginsDsl("smoke", directoryPrefix = "native-fat-framework")) { fun testDifferentTypes(gradleVersion: GradleVersion) {
gradleBuildScript().modify { nativeProject("native-fat-framework/smoke", gradleVersion) {
buildGradleKts.modify {
it.checkedReplace("iosArm32()", "iosArm32{binaries.framework {isStatic = true}}") it.checkedReplace("iosArm32()", "iosArm32{binaries.framework {isStatic = true}}")
.checkedReplace("iosArm64()", "iosArm64{binaries.framework {isStatic = false}}") .checkedReplace("iosArm64()", "iosArm64{binaries.framework {isStatic = false}}")
.checkedReplace("iosX64()", "iosX64{binaries.framework {isStatic = false}}") .checkedReplace("iosX64()", "iosX64{binaries.framework {isStatic = false}}")
.addBeforeSubstring("//", "binaries.framework(listOf(DEBUG))") .addBeforeSubstring("//", "binaries.framework(listOf(DEBUG))")
} }
build("fat") { buildAndFail("fat") {
assertFailed() assertOutputContains("All input frameworks must be either static or dynamic")
assertContains("All input frameworks must be either static or dynamic")
} }
} }
} }
@Test @DisplayName("Fat framework with all static inputs")
fun testAllStatic() { @GradleTest
with(transformProjectWithPluginsDsl("smoke", directoryPrefix = "native-fat-framework")) { fun testAllStatic(gradleVersion: GradleVersion) {
gradleBuildScript().modify { nativeProject("native-fat-framework/smoke", gradleVersion) {
buildGradleKts.modify {
it.checkedReplace("iosArm32()", "iosArm32{binaries.framework {isStatic = true}}") it.checkedReplace("iosArm32()", "iosArm32{binaries.framework {isStatic = true}}")
.checkedReplace("iosArm64()", "iosArm64{binaries.framework {isStatic = true}}") .checkedReplace("iosArm64()", "iosArm64{binaries.framework {isStatic = true}}")
.checkedReplace("iosX64()", "iosX64{binaries.framework {isStatic = true}}") .checkedReplace("iosX64()", "iosX64{binaries.framework {isStatic = true}}")
.addBeforeSubstring("//", "binaries.framework(listOf(DEBUG))") .addBeforeSubstring("//", "binaries.framework(listOf(DEBUG))")
} }
build("fat") { build("fat")
assertSuccessful()
}
} }
} }
/** @DisplayName("Test that the configurations exposing the frameworks don't interfere with variant-aware dependency resolution")
* Test that the configurations exposing the frameworks don't interfere with variant-aware dependency resolution @GradleTest
*/ fun testDependencyResolution(gradleVersion: GradleVersion) {
@Test nativeProject("native-fat-framework/smoke", gradleVersion) {
fun testDependencyResolution() = with(transformProjectWithPluginsDsl("smoke", directoryPrefix = "native-fat-framework")) { val nestedProjectName = "nested"
setupWorkingDir() includeOtherProjectAsSubmodule("smoke", "native-fat-framework", nestedProjectName, true)
val nestedProjectName = "nested" buildGradleKts.appendText("dependencies { \"commonMainImplementation\"(project(\":$nestedProjectName\")) }")
embedProject(this, nestedProjectName) testResolveAllConfigurations()
gradleBuildScript(nestedProjectName).modify { it.replace(".version(\"$KOTLIN_VERSION\")", "") }
gradleBuildScript().appendText("dependencies { \"commonMainImplementation\"(project(\":$nestedProjectName\")) }")
testResolveAllConfigurations()
}
companion object {
@BeforeClass
@JvmStatic
fun assumeItsMac() {
Assume.assumeTrue(HostManager.hostIsMac)
} }
} }
} }
@@ -14,13 +14,15 @@ import org.jetbrains.kotlin.gradle.BaseGradleIT.Companion.acceptAndroidSdkLicens
import org.jetbrains.kotlin.gradle.model.ModelContainer import org.jetbrains.kotlin.gradle.model.ModelContainer
import org.jetbrains.kotlin.gradle.model.ModelFetcherBuildAction import org.jetbrains.kotlin.gradle.model.ModelFetcherBuildAction
import org.jetbrains.kotlin.gradle.native.disableKotlinNativeCaches import org.jetbrains.kotlin.gradle.native.disableKotlinNativeCaches
import org.jetbrains.kotlin.gradle.util.modify
import org.jetbrains.kotlin.gradle.report.BuildReportType import org.jetbrains.kotlin.gradle.report.BuildReportType
import org.jetbrains.kotlin.gradle.util.modify
import org.jetbrains.kotlin.konan.target.HostManager import org.jetbrains.kotlin.konan.target.HostManager
import org.jetbrains.kotlin.konan.target.presetName import org.jetbrains.kotlin.konan.target.presetName
import org.jetbrains.kotlin.test.util.KtTestUtil import org.jetbrains.kotlin.test.util.KtTestUtil
import java.io.File import java.io.File
import java.nio.file.* import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.io.path.* import kotlin.io.path.*
import kotlin.test.assertTrue import kotlin.test.assertTrue
@@ -316,7 +318,7 @@ class TestProject(
fun subProject(name: String) = GradleProject(name, projectPath.resolve(name)) fun subProject(name: String) = GradleProject(name, projectPath.resolve(name))
fun addKotlinCompilerArgumentsPlugin() { fun addKotlinCompilerArgumentsPlugin() {
if (buildOptions.languageVersion != null || buildOptions.languageApiVersion != null ) { if (buildOptions.languageVersion != null || buildOptions.languageApiVersion != null) {
projectPath.toFile().walkTopDown().forEach { file -> projectPath.toFile().walkTopDown().forEach { file ->
when { when {
file.name.equals("build.gradle") -> file.modify { file.name.equals("build.gradle") -> file.modify {
@@ -336,17 +338,28 @@ class TestProject(
} }
} }
/**
* Includes another project as a submodule in the current project.
* @param otherProjectName The name of the other project to include as a submodule.
* @param pathPrefix An optional prefix to prepend to the submodule's path. Defaults to an empty string.
* @param newSubmoduleName An optional new name for the submodule. Defaults to the otherProjectName.
* @param isKts Whether to update a .kts settings file instead of a .gradle settings file. Defaults to false.
*/
fun includeOtherProjectAsSubmodule( fun includeOtherProjectAsSubmodule(
otherProjectName: String, otherProjectName: String,
pathPrefix: String pathPrefix: String,
newSubmoduleName: String = otherProjectName,
isKts: Boolean = false
) { ) {
val otherProjectPath = "$pathPrefix/$otherProjectName".testProjectPath val otherProjectPath = "$pathPrefix/$otherProjectName".testProjectPath
otherProjectPath.copyRecursively(projectPath.resolve(otherProjectName)) otherProjectPath.copyRecursively(projectPath.resolve(newSubmoduleName))
settingsGradle.append( val gradleSettingToUpdate = if (isKts) settingsGradleKts else settingsGradle
gradleSettingToUpdate.append(
""" """
include ':$otherProjectName' include(":$newSubmoduleName")
""".trimIndent() """.trimIndent()
) )
} }
@@ -60,3 +60,34 @@ fun createGradleCommand(wrapperDir: File, tailParameters: List<String>): List<St
} }
val isWindows: Boolean = System.getProperty("os.name")!!.contains("Windows") val isWindows: Boolean = System.getProperty("os.name")!!.contains("Windows")
/**
* Asserts the result of running a process by calling a set of assertions on the result object.
* If any of the assertions fail, an [AssertionError] is thrown and the process output information is printed.
*
* @param result The result of running a process.
* @param assertions A lambda expression that performs a set of assertions on it.
*
* @throws AssertionError If any of the assertions fail.
*/
fun assertProcessRunResult(result: ProcessRunResult, assertions: ProcessRunResult.() -> Unit) {
try {
result.assertions()
} catch (e: AssertionError) {
println(
"""
|Process info:
|#######################
|$result
|#######################
|
|Process output:
|#######################
|${result.output}
|#######################
|
""".trimMargin()
)
throw e
}
}
@@ -2,7 +2,7 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.gradle.tasks.FatFrameworkTask import org.jetbrains.kotlin.gradle.tasks.FatFrameworkTask
plugins { plugins {
id("org.jetbrains.kotlin.multiplatform").version("<pluginMarkerVersion>") id("org.jetbrains.kotlin.multiplatform")
} }
repositories { repositories {