From fd7ceabab6102a79b90c3b4aac860ea284bb1ea1 Mon Sep 17 00:00:00 2001 From: Anton Lakotka Date: Tue, 29 Nov 2022 14:56:58 +0100 Subject: [PATCH] [Gradle, MPP] Test sources publication as gradle variants ^KT-36943 --- .../kotlin/gradle/HierarchicalMppIT.kt | 96 ++++++++++++++ .../gradle/android/KotlinAndroidMppIT.kt | 125 +++++++++++++++++- .../kotlin/gradle/testbase/fileAssertions.kt | 33 +++++ .../consumer/build.gradle.kts | 63 +++++++++ .../{ => consumer}/settings.gradle.kts | 0 .../kotlin/gradle/MppPublicationTest.kt | 81 ++++++++++++ 6 files changed, 396 insertions(+), 2 deletions(-) create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/mpp-sources-publication/consumer/build.gradle.kts rename libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/mpp-sources-publication/{ => consumer}/settings.gradle.kts (100%) diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/HierarchicalMppIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/HierarchicalMppIT.kt index b5c4754d50b..a7a53427d4b 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/HierarchicalMppIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/HierarchicalMppIT.kt @@ -797,6 +797,63 @@ class HierarchicalMppIT : KGPBaseTest() { 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 @@ -1083,4 +1140,43 @@ class HierarchicalMppIT : KGPBaseTest() { } } } + + private data class SourcesVariantResolutionReport( + val files: List, + val dependencyToVariant: Map + ) { + companion object { + fun parse(output: String, targetNames: Iterable): Map { + val lines = output.lines() + return targetNames.associateWith { targetName -> lines.parseForTarget(targetName) } + } + + private fun List.parseForTarget(targetName: String) = SourcesVariantResolutionReport( + files = parseFiles(targetName), + dependencyToVariant = parseResolvedDependencies(targetName) + ) + + private fun List.betweenMarkers( + start: String, + end: String + ): List { + val startPos = indexOf(start) + val endPos = indexOf(end) + + return subList(startPos + 1, endPos) + } + + private fun List.parseFiles(targetName: String): List = + betweenMarkers( + "", + "" + ) + + private fun List.parseResolvedDependencies(targetName: String): Map = + betweenMarkers( + "", + "" + ).associate { it.split(" => ").let { it[0] to it[1] } } + } + } } diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/android/KotlinAndroidMppIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/android/KotlinAndroidMppIT.kt index 676d77a52fb..67db9bcf546 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/android/KotlinAndroidMppIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/android/KotlinAndroidMppIT.kt @@ -196,6 +196,19 @@ class KotlinAndroidMppIT : KGPBaseTest() { agpVersion: String, 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( "new-mpp-android", gradleVersion, @@ -222,6 +235,12 @@ class KotlinAndroidMppIT : KGPBaseTest() { 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-sources.jar")) + assertGradleVariant( + groupDir.resolve("lib-androidlib/1.0/lib-androidlib-1.0.module"), + "releaseSourcesElements-published" + ) { + assertAttributesEquals(*androidSourcesElementsAttributes) + } assertFileInProjectNotExists("$groupDir/lib-androidlib-debug") } groupDir.deleteRecursively() @@ -237,8 +256,24 @@ class KotlinAndroidMppIT : KGPBaseTest() { 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-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-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() @@ -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-debug.aar")) 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() @@ -267,11 +318,45 @@ class KotlinAndroidMppIT : KGPBaseTest() { """.trimIndent() ) 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-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-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() @@ -285,11 +370,47 @@ class KotlinAndroidMppIT : KGPBaseTest() { """.trimIndent() ) 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 -> 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")) } + + 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" + ) + } } } } diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/testbase/fileAssertions.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/testbase/fileAssertions.kt index 37d8ad5ca5e..40e2c5e73ce 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/testbase/fileAssertions.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/testbase/fileAssertions.kt @@ -5,11 +5,14 @@ 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.Path import kotlin.io.path.exists import kotlin.io.path.isDirectory import kotlin.io.path.readText +import kotlin.test.assertEquals import kotlin.test.asserter /** @@ -210,3 +213,33 @@ fun assertContainsFiles(expected: Iterable, actual: Iterable, messag "Actual set: ${actualSet.sorted().joinToString(", ")}\n" }, actualSet.containsAll(expectedSet)) } + +class GradleVariantAssertions( + val variantJson: JsonObject +) { + fun assertAttributesEquals(expected: Map) { + val attributesJson = variantJson.getAsJsonObject("attributes") + val actual = attributesJson.keySet().associateWith { attributesJson.get(it).asString } + + assertEquals(expected.toSortedStringWithLines(), actual.toSortedStringWithLines()) + } + + fun assertAttributesEquals(vararg expected: Pair) = assertAttributesEquals(expected.toMap()) +} + +private fun Map.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) +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/mpp-sources-publication/consumer/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/mpp-sources-publication/consumer/build.gradle.kts new file mode 100644 index 00000000000..25fc4e84fed --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/mpp-sources-publication/consumer/build.gradle.kts @@ -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("") +} + +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("") + println(files.joinToString("\n") { it.name }) + println("") + + println("") + val resolvedVariants = configuration + .incoming + .resolutionResult + .allDependencies + .filterIsInstance() + .map { it.requested.displayName to it.resolvedVariant.displayName } + .joinToString("\n") { "${it.first} => ${it.second}" } + println(resolvedVariants) + println("") +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/mpp-sources-publication/settings.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/mpp-sources-publication/consumer/settings.gradle.kts similarity index 100% rename from libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/mpp-sources-publication/settings.gradle.kts rename to libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/mpp-sources-publication/consumer/settings.gradle.kts diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/MppPublicationTest.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/MppPublicationTest.kt index 6ee670ad0ce..cb32914e5d4 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/MppPublicationTest.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/MppPublicationTest.kt @@ -7,12 +7,19 @@ 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.plugins.JavaPluginExtension import org.gradle.api.publish.PublishingExtension import org.gradle.api.publish.maven.MavenPublication import org.gradle.testfixtures.ProjectBuilder import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertTrue import kotlin.test.fail @@ -56,4 +63,78 @@ class MppPublicationTest { 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 = keySet() + .associate { key -> key.name to getAttribute(key).toString() } + }