[Gradle, MPP] Test sources publication as gradle variants

^KT-36943
This commit is contained in:
Anton Lakotka
2022-11-29 14:56:58 +01:00
committed by teamcity
parent 3a418bfe66
commit fd7ceabab6
6 changed files with 396 additions and 2 deletions
@@ -797,6 +797,63 @@ class HierarchicalMppIT : KGPBaseTest() {
actualSourcePublicationLayoutBySourcesFile.stringifyForBeautifulDiff() actualSourcePublicationLayoutBySourcesFile.stringifyForBeautifulDiff()
) )
} }
project(
"mpp-sources-publication/consumer",
gradleVersion = gradleVersion,
localRepoDir = tempDir
) {
buildGradleKts.appendText("""
testResolutionToSourcesVariant(
"common",
KotlinPlatformType.common,
includeDisambiguation = false
)
testResolutionToSourcesVariant(
"jvm",
KotlinPlatformType.jvm
)
testResolutionToSourcesVariant(
"jvm2",
KotlinPlatformType.jvm
)
testResolutionToSourcesVariant(
"linuxX64",
KotlinPlatformType.native,
nativePlatform = "linux_x64"
)
""".trimIndent())
val expectedReports = mapOf(
"common" to SourcesVariantResolutionReport(
files = listOf("lib-kotlin-1.0-sources.jar"),
dependencyToVariant = mapOf("test:lib:1.0" to "metadataSourcesElements")
),
"jvm" to SourcesVariantResolutionReport(
files = listOf("lib-jvm-1.0-sources.jar"),
dependencyToVariant = mapOf("test:lib:1.0" to "jvmSourcesElements-published",
"test:lib-jvm:1.0" to "jvmSourcesElements-published")
),
"jvm2" to SourcesVariantResolutionReport(
files = listOf("lib-jvm2-1.0-sources.jar"),
dependencyToVariant = mapOf("test:lib:1.0" to "jvm2SourcesElements-published",
"test:lib-jvm2:1.0" to "jvm2SourcesElements-published")
),
"linuxX64" to SourcesVariantResolutionReport(
files = listOf("lib-linuxx64-1.0-sources.jar"),
dependencyToVariant = mapOf("test:lib:1.0" to "linuxX64SourcesElements-published",
"test:lib-linuxx64:1.0" to "linuxX64SourcesElements-published")
),
)
build("help") { // evaluate only
val actualReports = SourcesVariantResolutionReport.parse(output, expectedReports.keys)
assertEquals(expectedReports, actualReports)
}
}
} }
@GradleTest @GradleTest
@@ -1083,4 +1140,43 @@ class HierarchicalMppIT : KGPBaseTest() {
} }
} }
} }
private data class SourcesVariantResolutionReport(
val files: List<String>,
val dependencyToVariant: Map<String, String>
) {
companion object {
fun parse(output: String, targetNames: Iterable<String>): Map<String, SourcesVariantResolutionReport> {
val lines = output.lines()
return targetNames.associateWith { targetName -> lines.parseForTarget(targetName) }
}
private fun List<String>.parseForTarget(targetName: String) = SourcesVariantResolutionReport(
files = parseFiles(targetName),
dependencyToVariant = parseResolvedDependencies(targetName)
)
private fun List<String>.betweenMarkers(
start: String,
end: String
): List<String> {
val startPos = indexOf(start)
val endPos = indexOf(end)
return subList(startPos + 1, endPos)
}
private fun List<String>.parseFiles(targetName: String): List<String> =
betweenMarkers(
"<RESOLVED SOURCES FILE $targetName>",
"</RESOLVED SOURCES FILE $targetName>"
)
private fun List<String>.parseResolvedDependencies(targetName: String): Map<String, String> =
betweenMarkers(
"<RESOLVED DEPENDENCIES OF $targetName>",
"</RESOLVED DEPENDENCIES OF $targetName>"
).associate { it.split(" => ").let { it[0] to it[1] } }
}
}
} }
@@ -196,6 +196,19 @@ class KotlinAndroidMppIT : KGPBaseTest() {
agpVersion: String, agpVersion: String,
jdkVersion: JdkVersions.ProvidedJdk, jdkVersion: JdkVersions.ProvidedJdk,
) { ) {
val androidSourcesElementsAttributes = arrayOf(
"org.gradle.category" to "documentation",
"org.gradle.dependency.bundling" to "external",
"org.gradle.docstype" to "sources",
"org.gradle.libraryelements" to "jar",
"org.gradle.usage" to "java-runtime",
"org.jetbrains.kotlin.platform.type" to "androidJvm",
// user-specific attributes that added manually in build script
"com.example.compilation" to "release",
"com.example.target" to "androidLib",
)
project( project(
"new-mpp-android", "new-mpp-android",
gradleVersion, gradleVersion,
@@ -222,6 +235,12 @@ class KotlinAndroidMppIT : KGPBaseTest() {
build("publish") { build("publish") {
assertFileExists(groupDir.resolve("lib-androidlib/1.0/lib-androidlib-1.0.aar")) assertFileExists(groupDir.resolve("lib-androidlib/1.0/lib-androidlib-1.0.aar"))
assertFileExists(groupDir.resolve("lib-androidlib/1.0/lib-androidlib-1.0-sources.jar")) assertFileExists(groupDir.resolve("lib-androidlib/1.0/lib-androidlib-1.0-sources.jar"))
assertGradleVariant(
groupDir.resolve("lib-androidlib/1.0/lib-androidlib-1.0.module"),
"releaseSourcesElements-published"
) {
assertAttributesEquals(*androidSourcesElementsAttributes)
}
assertFileInProjectNotExists("$groupDir/lib-androidlib-debug") assertFileInProjectNotExists("$groupDir/lib-androidlib-debug")
} }
groupDir.deleteRecursively() groupDir.deleteRecursively()
@@ -237,8 +256,24 @@ class KotlinAndroidMppIT : KGPBaseTest() {
build("publish") { build("publish") {
assertFileExists(groupDir.resolve("lib-androidlib/1.0/lib-androidlib-1.0.aar")) assertFileExists(groupDir.resolve("lib-androidlib/1.0/lib-androidlib-1.0.aar"))
assertFileExists(groupDir.resolve("lib-androidlib/1.0/lib-androidlib-1.0-sources.jar")) assertFileExists(groupDir.resolve("lib-androidlib/1.0/lib-androidlib-1.0-sources.jar"))
assertGradleVariant(
groupDir.resolve("lib-androidlib/1.0/lib-androidlib-1.0.module"),
"releaseSourcesElements-published"
) {
assertAttributesEquals(*androidSourcesElementsAttributes)
}
assertFileExists(groupDir.resolve("lib-androidlib-debug/1.0/lib-androidlib-debug-1.0.aar")) assertFileExists(groupDir.resolve("lib-androidlib-debug/1.0/lib-androidlib-debug-1.0.aar"))
assertFileExists(groupDir.resolve("lib-androidlib-debug/1.0/lib-androidlib-debug-1.0-sources.jar")) assertFileExists(groupDir.resolve("lib-androidlib-debug/1.0/lib-androidlib-debug-1.0-sources.jar"))
assertGradleVariant(
groupDir.resolve("lib-androidlib-debug/1.0/lib-androidlib-debug-1.0.module"),
"debugSourcesElements-published"
) {
assertAttributesEquals(
*androidSourcesElementsAttributes,
"com.example.compilation" to "debug",
"com.android.build.api.attributes.BuildTypeAttr" to "debug"
)
}
} }
groupDir.deleteRecursively() groupDir.deleteRecursively()
@@ -255,6 +290,22 @@ class KotlinAndroidMppIT : KGPBaseTest() {
assertFileExists(groupDir.resolve("lib-androidlib/1.0/lib-androidlib-1.0-sources.jar")) assertFileExists(groupDir.resolve("lib-androidlib/1.0/lib-androidlib-1.0-sources.jar"))
assertFileExists(groupDir.resolve("lib-androidlib/1.0/lib-androidlib-1.0-debug.aar")) assertFileExists(groupDir.resolve("lib-androidlib/1.0/lib-androidlib-1.0-debug.aar"))
assertFileExists(groupDir.resolve("lib-androidlib/1.0/lib-androidlib-1.0-debug-sources.jar")) assertFileExists(groupDir.resolve("lib-androidlib/1.0/lib-androidlib-1.0-debug-sources.jar"))
assertGradleVariant(
groupDir.resolve("lib-androidlib/1.0/lib-androidlib-1.0.module"),
"releaseSourcesElements-published"
) {
assertAttributesEquals(*androidSourcesElementsAttributes)
}
assertGradleVariant(
groupDir.resolve("lib-androidlib/1.0/lib-androidlib-1.0.module"),
"debugSourcesElements-published"
) {
assertAttributesEquals(
*androidSourcesElementsAttributes,
"com.example.compilation" to "debug",
"com.android.build.api.attributes.BuildTypeAttr" to "debug"
)
}
} }
groupDir.deleteRecursively() groupDir.deleteRecursively()
@@ -267,11 +318,45 @@ class KotlinAndroidMppIT : KGPBaseTest() {
""".trimIndent() """.trimIndent()
) )
build("publish") { build("publish") {
listOf("foobar", "foobaz").forEach { flavor -> listOf("fooBar", "fooBaz").forEach { flavorName ->
val flavor = flavorName.lowercase()
val flavorAttributes = if (AGPVersion.fromString(agpVersion) >= AGPVersion.v7_0_0) {
arrayOf(
"foo" to flavorName,
"com.android.build.api.attributes.ProductFlavor:foo" to flavorName
)
} else {
arrayOf(
"foo" to flavorName
)
}
assertFileExists(groupDir.resolve("lib-androidlib-$flavor/1.0/lib-androidlib-$flavor-1.0.aar")) assertFileExists(groupDir.resolve("lib-androidlib-$flavor/1.0/lib-androidlib-$flavor-1.0.aar"))
assertFileExists(groupDir.resolve("lib-androidlib-$flavor/1.0/lib-androidlib-$flavor-1.0-sources.jar")) assertFileExists(groupDir.resolve("lib-androidlib-$flavor/1.0/lib-androidlib-$flavor-1.0-sources.jar"))
assertFileExists(groupDir.resolve("lib-androidlib-$flavor/1.0/lib-androidlib-$flavor-1.0-debug.aar")) assertFileExists(groupDir.resolve("lib-androidlib-$flavor/1.0/lib-androidlib-$flavor-1.0-debug.aar"))
assertFileExists(groupDir.resolve("lib-androidlib-$flavor/1.0/lib-androidlib-$flavor-1.0-debug-sources.jar")) assertFileExists(groupDir.resolve("lib-androidlib-$flavor/1.0/lib-androidlib-$flavor-1.0-debug-sources.jar"))
assertGradleVariant(
groupDir.resolve("lib-androidlib-$flavor/1.0/lib-androidlib-$flavor-1.0.module"),
"${flavorName}ReleaseSourcesElements-published"
) {
assertAttributesEquals(
*androidSourcesElementsAttributes,
*flavorAttributes,
"com.example.compilation" to "${flavorName}Release",
)
}
assertGradleVariant(
groupDir.resolve("lib-androidlib-$flavor/1.0/lib-androidlib-$flavor-1.0.module"),
"${flavorName}DebugSourcesElements-published"
) {
assertAttributesEquals(
*androidSourcesElementsAttributes,
*flavorAttributes,
"com.example.compilation" to "${flavorName}Debug",
"com.android.build.api.attributes.BuildTypeAttr" to "debug"
)
}
} }
} }
groupDir.deleteRecursively() groupDir.deleteRecursively()
@@ -285,11 +370,47 @@ class KotlinAndroidMppIT : KGPBaseTest() {
""".trimIndent() """.trimIndent()
) )
build("publish") { build("publish") {
listOf("foobar", "foobaz").forEach { flavor -> listOf("fooBar", "fooBaz").forEach { flavorName ->
val flavor = flavorName.lowercase()
val flavorAttributes = if (AGPVersion.fromString(agpVersion) >= AGPVersion.v7_0_0) {
arrayOf(
"foo" to flavorName,
"com.android.build.api.attributes.ProductFlavor:foo" to flavorName
)
} else {
arrayOf(
"foo" to flavorName
)
}
listOf("-debug", "").forEach { buildType -> listOf("-debug", "").forEach { buildType ->
assertFileExists(groupDir.resolve("lib-androidlib-$flavor$buildType/1.0/lib-androidlib-$flavor$buildType-1.0.aar")) assertFileExists(groupDir.resolve("lib-androidlib-$flavor$buildType/1.0/lib-androidlib-$flavor$buildType-1.0.aar"))
assertFileExists(groupDir.resolve("lib-androidlib-$flavor$buildType/1.0/lib-androidlib-$flavor$buildType-1.0-sources.jar")) assertFileExists(groupDir.resolve("lib-androidlib-$flavor$buildType/1.0/lib-androidlib-$flavor$buildType-1.0-sources.jar"))
} }
assertGradleVariant(
groupDir.resolve("lib-androidlib-$flavor/1.0/lib-androidlib-$flavor-1.0.module"),
"${flavorName}ReleaseSourcesElements-published"
) {
assertAttributesEquals(
*androidSourcesElementsAttributes,
*flavorAttributes,
"com.example.compilation" to "${flavorName}Release",
)
}
assertGradleVariant(
groupDir.resolve("lib-androidlib-$flavor-debug/1.0/lib-androidlib-$flavor-debug-1.0.module"),
"${flavorName}DebugSourcesElements-published"
) {
assertAttributesEquals(
*androidSourcesElementsAttributes,
*flavorAttributes,
"com.example.compilation" to "${flavorName}Debug",
"com.android.build.api.attributes.BuildTypeAttr" to "debug"
)
}
} }
} }
} }
@@ -5,11 +5,14 @@
package org.jetbrains.kotlin.gradle.testbase package org.jetbrains.kotlin.gradle.testbase
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.exists import kotlin.io.path.exists
import kotlin.io.path.isDirectory import kotlin.io.path.isDirectory
import kotlin.io.path.readText import kotlin.io.path.readText
import kotlin.test.assertEquals
import kotlin.test.asserter import kotlin.test.asserter
/** /**
@@ -210,3 +213,33 @@ fun assertContainsFiles(expected: Iterable<Path>, actual: Iterable<Path>, messag
"Actual set: ${actualSet.sorted().joinToString(", ")}\n" "Actual set: ${actualSet.sorted().joinToString(", ")}\n"
}, actualSet.containsAll(expectedSet)) }, actualSet.containsAll(expectedSet))
} }
class GradleVariantAssertions(
val variantJson: JsonObject
) {
fun assertAttributesEquals(expected: Map<String, String>) {
val attributesJson = variantJson.getAsJsonObject("attributes")
val actual = attributesJson.keySet().associateWith { attributesJson.get(it).asString }
assertEquals(expected.toSortedStringWithLines(), actual.toSortedStringWithLines())
}
fun assertAttributesEquals(vararg expected: Pair<String, String>) = assertAttributesEquals(expected.toMap())
}
private fun Map<String, Any?>.toSortedStringWithLines() = entries
.sortedBy { it.key }
.joinToString("\n") { (key, value) -> "'$key' => '$value'" }
fun assertGradleVariant(gradleModuleFile: Path, variantName: String, code: GradleVariantAssertions.() -> Unit) {
val moduleJson = JsonParser.parseString(gradleModuleFile.readText()).asJsonObject
val variants = moduleJson.getAsJsonArray("variants")
val variantJson = variants.find { it.asJsonObject.get("name").asString == variantName }
if (variantJson == null) {
val existingVariants = variants.map { it.asJsonObject.get("name").asString }
throw AssertionError("Variant with name '$variantName' doesn't exist; Existing variants: $existingVariants")
}
GradleVariantAssertions(variantJson.asJsonObject).apply(code)
}
@@ -0,0 +1,63 @@
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
plugins {
kotlin("multiplatform") apply false
`maven-publish`
}
repositories {
mavenLocal()
maven("<localRepo>")
}
val disambiguationAttribute = Attribute.of("disambiguationAttribute", String::class.java)
fun testResolutionToSourcesVariant(
targetName: String,
platformType: KotlinPlatformType,
nativePlatform: String? = null,
includeDisambiguation: Boolean = true
) {
val configuration = configurations.create("${targetName}Sources") {
isCanBeResolved = true
isCanBeConsumed = false
if (platformType == KotlinPlatformType.jvm) {
attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named("java-runtime"))
} else {
attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named("kotlin-runtime"))
}
attributes.attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.DOCUMENTATION))
attributes.attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named(DocsType.SOURCES))
attributes.attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL))
attributes.attribute(KotlinPlatformType.attribute, platformType)
if (nativePlatform != null) {
attributes.attribute(KotlinNativeTarget.konanTargetAttribute, nativePlatform)
}
if (includeDisambiguation) {
attributes.attribute(disambiguationAttribute, targetName)
}
project.dependencies.add(name, "test:lib:1.0")
}
val files = configuration.resolve()
println("<RESOLVED SOURCES FILE $targetName>")
println(files.joinToString("\n") { it.name })
println("</RESOLVED SOURCES FILE $targetName>")
println("<RESOLVED DEPENDENCIES OF $targetName>")
val resolvedVariants = configuration
.incoming
.resolutionResult
.allDependencies
.filterIsInstance<ResolvedDependencyResult>()
.map { it.requested.displayName to it.resolvedVariant.displayName }
.joinToString("\n") { "${it.first} => ${it.second}" }
println(resolvedVariants)
println("</RESOLVED DEPENDENCIES OF $targetName>")
}
@@ -7,12 +7,19 @@
package org.jetbrains.kotlin.gradle package org.jetbrains.kotlin.gradle
import org.gradle.api.attributes.Attribute
import org.gradle.api.attributes.AttributeContainer
import org.gradle.api.component.ComponentWithVariants
import org.gradle.api.component.SoftwareComponent
import org.gradle.api.internal.component.SoftwareComponentInternal
import org.gradle.api.internal.project.ProjectInternal import org.gradle.api.internal.project.ProjectInternal
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.publish.PublishingExtension import org.gradle.api.publish.PublishingExtension
import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.publish.maven.MavenPublication
import org.gradle.testfixtures.ProjectBuilder import org.gradle.testfixtures.ProjectBuilder
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
import kotlin.test.fail import kotlin.test.fail
@@ -56,4 +63,78 @@ class MppPublicationTest {
assertTrue(sources.isNotEmpty(), "Expected at least one sources artifact for ${publication.name}") assertTrue(sources.isNotEmpty(), "Expected at least one sources artifact for ${publication.name}")
} }
} }
@Test
fun `jvm sources elements has same attributes as java sources elements from default java gradle plugin`() {
val javaProject = ProjectBuilder.builder().build().run {
// Given
plugins.apply("java")
plugins.apply("maven-publish")
extensions.getByType(JavaPluginExtension::class.java).run {
withSourcesJar()
}
// When
this as ProjectInternal
this.evaluate()
}
val javaSourcesElementsAttributes = javaProject
.components
.getByName("java")
.attributesOfUsageContext("sourcesElements")
project.evaluate()
val kotlinComponent = project.components.getByName("kotlin") as ComponentWithVariants
val jvmComponent = kotlinComponent.variants.first { it.name == "jvm" }
val jvmSourcesElementsAttributes = jvmComponent.attributesOfUsageContext("jvmSourcesElements-published")
val extraExpectedAttributes = mapOf(
"org.jetbrains.kotlin.platform.type" to "jvm", // target disambiguation attribute
"org.gradle.libraryelements" to "jar" // is set by default by kgp, when it sets usage as `java-runtime-jars`
// see [KotlinUsages.producerApiUsage]
)
val expectedAttributes = javaSourcesElementsAttributes.toMapOfStrings() + extraExpectedAttributes
val actualAttributes = jvmSourcesElementsAttributes.toMapOfStrings()
assertEquals(expectedAttributes, actualAttributes)
}
@Test
fun `sources elements includes user-defined attributes`() {
val userAttribute = Attribute.of("userAttribute", String::class.java)
kotlin.run {
targets.all { target -> target.attributes { attribute(userAttribute, target.name) } }
js(IR)
linuxX64()
iosX64()
}
project.evaluate()
val kotlinComponent = project.components.getByName("kotlin") as ComponentWithVariants
val sourcesElements = listOf("jvm", "js", "linuxX64", "iosX64")
.map { targetName ->
targetName to kotlinComponent.variants
.first { it.name == targetName }
.attributesOfUsageContext("${targetName}SourcesElements-published")
}
for ( (targetName, sourceElements) in sourcesElements) {
assertTrue(
message = "Sources Elements of target $targetName doesn't have 'userAttribute'"
) { sourceElements.attributes.toMapOfStrings().containsKey("userAttribute") }
}
}
private fun SoftwareComponent.attributesOfUsageContext(usageContextName: String): AttributeContainer {
this as SoftwareComponentInternal
return usages.first { it.name == usageContextName }.attributes
}
private fun AttributeContainer.toMapOfStrings(): Map<String, String> = keySet()
.associate { key -> key.name to getAttribute(key).toString() }
} }