From 9f2c2b7265190bdf9e5218f430fb644518687e1e Mon Sep 17 00:00:00 2001 From: Sebastian Sellmair Date: Thu, 3 Aug 2023 12:20:37 +0200 Subject: [PATCH] [Gradle] Implement tests for External target publications ^KT-60159 Verification Pending --- .../gradle/android/ExternalAndroidTargetIT.kt | 42 +++++- .../build.gradle.kts | 43 ++++++ .../lib/build.gradle.kts | 36 +++++ .../lib/src/commonMain/kotlin/CommonMain.kt | 8 + .../settings.gradle.kts | 2 + .../src/androidMain/kotlin/AndroidMain.kt | 23 +++ .../kotlin/AndroidTestOnJvm.kt | 14 ++ .../src/commonMain/kotlin/CommonMain.kt | 10 ++ .../unitTests/ExternalKotlinTargetApiTests.kt | 142 ++++++++++++++++-- 9 files changed, 303 insertions(+), 17 deletions(-) create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/build.gradle.kts create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/lib/build.gradle.kts create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/lib/src/commonMain/kotlin/CommonMain.kt create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/settings.gradle.kts create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/src/androidMain/kotlin/AndroidMain.kt create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/src/androidTestOnJvm/kotlin/AndroidTestOnJvm.kt create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/src/commonMain/kotlin/CommonMain.kt diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/android/ExternalAndroidTargetIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/android/ExternalAndroidTargetIT.kt index 0c5e84bdb2c..faf209d705f 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/android/ExternalAndroidTargetIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/android/ExternalAndroidTargetIT.kt @@ -6,12 +6,18 @@ package org.jetbrains.kotlin.gradle.android import org.gradle.util.GradleVersion -import org.jetbrains.kotlin.gradle.idea.tcs.IdeaKotlinSourceDependency.Type.Regular -import org.jetbrains.kotlin.gradle.idea.testFixtures.tcs.* +import org.jetbrains.kotlin.gradle.idea.testFixtures.tcs.assertMatches +import org.jetbrains.kotlin.gradle.idea.testFixtures.tcs.binaryCoordinates +import org.jetbrains.kotlin.gradle.idea.testFixtures.tcs.dependsOnDependency +import org.jetbrains.kotlin.gradle.idea.testFixtures.tcs.friendSourceDependency import org.jetbrains.kotlin.gradle.testbase.* import org.jetbrains.kotlin.gradle.util.jetbrainsAnnotationDependencies import org.jetbrains.kotlin.gradle.util.kotlinStdlibDependencies import org.jetbrains.kotlin.gradle.util.resolveIdeDependencies +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path +import kotlin.io.path.readText +import kotlin.test.fail @GradleTestVersions(minVersion = TestVersions.Gradle.G_8_1) @AndroidTestVersions(minVersion = TestVersions.AGP.AGP_82) @@ -81,4 +87,36 @@ class ExternalAndroidTargetIT : KGPBaseTest() { } } } + + @GradleAndroidTest + fun `test - simple project - pom dependencies rewritten`( + gradleVersion: GradleVersion, androidVersion: String, jdkVersion: JdkVersions.ProvidedJdk, @TempDir localRepoDir: Path, + ) { + project( + "externalAndroidTarget-project2project", + gradleVersion, + buildOptions = defaultBuildOptions.copy(androidVersion = androidVersion), + buildJdk = jdkVersion.location, + localRepoDir = localRepoDir + ) { + build("publish", buildOptions = buildOptions.copy(configurationCache = true)) { + val pomFile = localRepoDir.resolve("app/app-android/1.0/app-android-1.0.pom") + assertFileExists(pomFile) + + fun String.removeWhiteSpaces() = replace("\\s+".toRegex(), "") + val pomText = pomFile.readText() + val expectedDependency = """ + + sample + tcs-android + 2.0 + compile + + """.trimIndent() + + if (expectedDependency.removeWhiteSpaces() !in pomText.removeWhiteSpaces()) + fail("Expected to find\n$expectedDependency\nIn POM file\n$pomText") + } + } + } } diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/build.gradle.kts new file mode 100644 index 00000000000..be11214d48c --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + kotlin("multiplatform") + id("com.android.kotlin.multiplatform.library") + `maven-publish` +} + +group = "app" +version = "1.0" + +repositories { + mavenLocal() + mavenCentral() + google() +} + +kotlin { + linuxX64() + linuxArm64() + + androidLibrary { + compileSdk = 33 + namespace = "org.jetbrains.sample" + + withAndroidTestOnJvm() + sourceSets.getByName("androidTestOnJvm").dependencies { + implementation("junit:junit:4.13.2") + } + } + + sourceSets { + commonMain { + dependencies { + api(project(":lib")) + } + } + } +} + +publishing { + repositories { + maven("") + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/lib/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/lib/build.gradle.kts new file mode 100644 index 00000000000..7c9b487a6ba --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/lib/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + kotlin("multiplatform") + id("com.android.kotlin.multiplatform.library") + `maven-publish` +} + +repositories { + mavenLocal() + mavenCentral() + google() +} + +kotlin { + linuxX64() + linuxArm64() + + androidLibrary { + compileSdk = 33 + namespace = "org.jetbrains.sample" + } +} + +publishing { + repositories { + maven("") + } + + publications { + val android by getting { + this as MavenPublication + artifactId = "tcs-android" + version = "2.0" + groupId = "sample" + } + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/lib/src/commonMain/kotlin/CommonMain.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/lib/src/commonMain/kotlin/CommonMain.kt new file mode 100644 index 00000000000..4b52e0b66e6 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/lib/src/commonMain/kotlin/CommonMain.kt @@ -0,0 +1,8 @@ +/* + * 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 lib + +fun commonMain() = 42 \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/settings.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/settings.gradle.kts new file mode 100644 index 00000000000..e1a72365a33 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "app" +include(":lib") \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/src/androidMain/kotlin/AndroidMain.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/src/androidMain/kotlin/AndroidMain.kt new file mode 100644 index 00000000000..f552460029e --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/src/androidMain/kotlin/AndroidMain.kt @@ -0,0 +1,23 @@ +/* + * 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. + */ + +import android.content.Context +import android.util.Log + +class AndroidMain(val context: Context) { + fun useContext() { + context.getSystemService(Context.LOCATION_SERVICE) + } + + fun useLog() { + Log.d("test", CommonMain.toString()) + } + + companion object { + fun useCommonMain() { + println("useCommonMain: ${CommonMain}") + } + } +} diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/src/androidTestOnJvm/kotlin/AndroidTestOnJvm.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/src/androidTestOnJvm/kotlin/AndroidTestOnJvm.kt new file mode 100644 index 00000000000..c8c920dd897 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/src/androidTestOnJvm/kotlin/AndroidTestOnJvm.kt @@ -0,0 +1,14 @@ +import org.junit.Test + +/* + * 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. + */ + +class AndroidTestOnJvm { + @Test + fun run() { + AndroidMain.useCommonMain() + org.junit.Assert.assertEquals("CommonMain", CommonMain.toString()) + } +} diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/src/commonMain/kotlin/CommonMain.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/src/commonMain/kotlin/CommonMain.kt new file mode 100644 index 00000000000..b91594832d4 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/externalAndroidTarget-project2project/src/commonMain/kotlin/CommonMain.kt @@ -0,0 +1,10 @@ +/* + * 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. + */ + +object CommonMain { + override fun toString(): String { + return "CommonMain" + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/ExternalKotlinTargetApiTests.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/ExternalKotlinTargetApiTests.kt index bb852aeb8ba..df33162b562 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/ExternalKotlinTargetApiTests.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/ExternalKotlinTargetApiTests.kt @@ -10,18 +10,18 @@ package org.jetbrains.kotlin.gradle.unitTests import org.gradle.api.attributes.Attribute import org.gradle.api.attributes.Category import org.gradle.api.attributes.DocsType +import org.gradle.api.internal.component.SoftwareComponentInternal import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension -import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType -import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle -import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree +import org.jetbrains.kotlin.gradle.plugin.* import org.jetbrains.kotlin.gradle.plugin.hierarchy.KotlinSourceSetTreeClassifier import org.jetbrains.kotlin.gradle.plugin.hierarchy.orNull -import org.jetbrains.kotlin.gradle.plugin.launchInStage import org.jetbrains.kotlin.gradle.plugin.mpp.external.ExternalKotlinCompilationDescriptor.CompilationAssociator import org.jetbrains.kotlin.gradle.plugin.mpp.external.ExternalKotlinCompilationDescriptor.CompilationFactory import org.jetbrains.kotlin.gradle.plugin.mpp.external.ExternalKotlinCompilationDescriptorBuilder import org.jetbrains.kotlin.gradle.plugin.mpp.external.createCompilation import org.jetbrains.kotlin.gradle.plugin.mpp.external.createExternalKotlinTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.internal +import org.jetbrains.kotlin.gradle.plugin.mpp.kotlinProjectStructureMetadata import org.jetbrains.kotlin.gradle.util.* import org.jetbrains.kotlin.gradle.utils.property import org.jetbrains.kotlin.gradle.utils.toMap @@ -35,15 +35,21 @@ class ExternalKotlinTargetApiTests { @Test - fun `test - sourceSetClassifier - default`() = buildProjectWithMPP().runLifecycleAwareTest { + fun `test - sourceSetClassifier - default`() = project.runLifecycleAwareTest { val target = kotlin.createExternalKotlinTarget { defaults() } - val compilation = target.createCompilation { defaults(kotlin) } + val mainCompilation = target.createCompilation { defaults(kotlin) } + val fakeCompilation = target.createCompilation { + defaults(kotlin) + compilationName = "fake" + defaultSourceSet = kotlin.sourceSets.create("fake") + } - assertEquals(KotlinSourceSetTree("fake"), KotlinSourceSetTree.orNull(compilation)) + assertEquals(KotlinSourceSetTree.main, KotlinSourceSetTree.orNull(mainCompilation)) + assertEquals(KotlinSourceSetTree("fake"), KotlinSourceSetTree.orNull(fakeCompilation)) } @Test - fun `test - sourceSetClassifier - custom name`() = buildProjectWithMPP().runLifecycleAwareTest { + fun `test - sourceSetClassifier - custom name`() = project.runLifecycleAwareTest { val target = kotlin.createExternalKotlinTarget { defaults() } val compilation = target.createCompilation { defaults(kotlin) @@ -54,7 +60,8 @@ class ExternalKotlinTargetApiTests { } @Test - fun `test - sourceSetClassifier - custom property`() = buildProjectWithMPP().runLifecycleAwareTest { + fun `test - sourceSetClassifier - custom property`() = project.runLifecycleAwareTest { + val kotlin = multiplatformExtension val myProperty = project.objects.property() val nullProperty = project.objects.property() @@ -132,8 +139,9 @@ class ExternalKotlinTargetApiTests { } @Test - fun `test - sourcesElements - default configuration`() = buildProjectWithMPP().runLifecycleAwareTest { + fun `test - sourcesElements - default configuration`() = project.runLifecycleAwareTest { val target = kotlin.createExternalKotlinTarget { defaults() } + target.createCompilation { defaults(kotlin) } assertNotEquals(target.sourcesElementsConfiguration, target.sourcesElementsPublishedConfiguration) @@ -165,7 +173,7 @@ class ExternalKotlinTargetApiTests { } @Test - fun `test - sourcesElements - configure`() = buildProjectWithMPP().runLifecycleAwareTest { + fun `test - sourcesElements - configure`() = project.runLifecycleAwareTest { val testAttribute = Attribute.of("for.test", String::class.java) val target = kotlin.createExternalKotlinTarget { @@ -180,6 +188,7 @@ class ExternalKotlinTargetApiTests { configuration.attributes.attribute(testAttribute, "sourcesElements-published") } } + target.createCompilation { defaults(kotlin) } /* Check traces left before */ assertEquals("sourcesElements", target.sourcesElementsConfiguration.attributes.getAttribute(testAttribute)) @@ -187,22 +196,125 @@ class ExternalKotlinTargetApiTests { } @Test - fun `test - sourcesElements - publication`() = buildProjectWithMPP().runLifecycleAwareTest { + fun `test - sourcesElements - publication`() = project.runLifecycleAwareTest { val target = kotlin.createExternalKotlinTarget { defaults() } - val component = target.delegate.components.singleOrNull() ?: fail("Expected single 'component' for external target") - component.usages.find { it.name == target.sourcesElementsPublishedConfiguration.name } + // Main/publishable compilation is required to have usages + target.createCompilation { defaults(kotlin) } + + KotlinPluginLifecycle.Stage.AfterFinaliseCompilations.await() + val component = target.delegate.kotlinComponents.singleOrNull() ?: fail("Expected single 'component' for external target") + + component.internal.usages.find { it.dependencyConfigurationName == target.sourcesElementsPublishedConfiguration.name } ?: fail("Missing sourcesElements usage") } @Test - fun `test - sourcesElements - publication - withSourcesJar set to false`() = buildProjectWithMPP().runLifecycleAwareTest { + fun `test - sourcesElements - publication - withSourcesJar set to false`() = project.runLifecycleAwareTest { val target = kotlin.createExternalKotlinTarget { defaults() } target.withSourcesJar(false) + target.createCompilation { defaults(kotlin) } + val component = target.delegate.components.singleOrNull() ?: fail("Expected single 'component' for external target") val sourcesUsage = component.usages.find { it.name.contains("sources", true) } if (sourcesUsage != null) { fail("Unexpected usage '${sourcesUsage.name} in target publication") } } + + @Test + fun `test - gradle usage component contains the same usages as kotlin component`() = project.runLifecycleAwareTest { + val target = kotlin.createExternalKotlinTarget { defaults() } + target.createCompilation { defaults(kotlin) } + + val kotlinComponent = target.delegate.kotlinComponents.singleOrNull() + ?: fail("Expected single 'kotlinComponent' for external target") + + val gradleComponent = target.delegate.components.singleOrNull() + ?: fail("Expected single 'component' for external target") + + configurationResult.await() + + val kotlinUsagesNames = kotlinComponent.internal.usages.map { it.dependencyConfigurationName } + val gradleUsagesNames = (gradleComponent as SoftwareComponentInternal).usages.map { it.name } + + if (kotlinUsagesNames.toSet() != gradleUsagesNames.toSet()) + fail("Kotlin Usages ($kotlinUsagesNames) from Kotlin Component didn't match Usages from Gradle Component ($gradleUsagesNames)") + } + + @Test + fun `test - gradle usage component contains the same usages as kotlin component after project evaluation`() { + val target = kotlin.createExternalKotlinTarget { defaults() } + target.createCompilation { defaults(kotlin) } + project.evaluate() + + val kotlinComponent = target.kotlinComponents.singleOrNull() ?: fail("Expected single 'kotlinComponent' for external target") + val gradleComponent = target.components.singleOrNull() ?: fail("Expected single 'component' for external target") + + if (kotlinComponent !is SoftwareComponentInternal) error("Expected 'kotlinComponent' to be 'SoftwareComponentInternal'") + + // When kotlin usages is available corresponding gradle component usages should be also available + val kotlinUsagesNames = kotlinComponent.usages.map { it.name } + val gradleUsagesNames = gradleComponent.usages.map { it.name } + + if (kotlinUsagesNames.toSet() != gradleUsagesNames.toSet()) + fail("Kotlin Usages ($kotlinUsagesNames) from Kotlin Component didn't match Usages from Gradle Component ($gradleUsagesNames)") + } + + @Test + fun `test - project structure metadata contains external target variants`() { + val target = kotlin.createExternalKotlinTarget { defaults() } + val mainCompilation = target.createCompilation { defaults(kotlin) } + val testCompilation = target.createCompilation { + defaults(kotlin) + compilationName = "test" + defaultSourceSet = kotlin.sourceSets.create("fakeTest") + } + + kotlin.linuxX64() + kotlin.macosX64() + + fun KotlinHierarchyBuilder.withFakeTarget() = withCompilations { it == mainCompilation || it == testCompilation } + kotlin.applyHierarchyTemplate { + sourceSetTrees(KotlinSourceSetTree.main, KotlinSourceSetTree.test) + common { + group("linuxAndFake") { + withLinux() + withFakeTarget() + } + + withMacos() + } + } + + project.evaluate() + + val projectStructureMetadata = kotlin.kotlinProjectStructureMetadata + + val fakeApiElementsSourceSets = projectStructureMetadata.sourceSetNamesByVariantName["fakeApiElements"] + ?: fail("Expected 'fakeApiElements' variant") + assertEquals(setOf("commonMain", "linuxAndFakeMain"), fakeApiElementsSourceSets) + + val fakeRuntimeElementsSourceSets = projectStructureMetadata.sourceSetNamesByVariantName["fakeRuntimeElements"] + ?: fail("Expected 'fakeRuntimeElements' variant") + assertEquals(setOf("commonMain", "linuxAndFakeMain"), fakeRuntimeElementsSourceSets) + } + + @Test + fun `test - published component contain user defined attributes`() { + val userAttribute = Attribute.of("userAttribute", String::class.java) + + val target = kotlin.createExternalKotlinTarget { defaults() } + target.createCompilation { defaults(kotlin) } + target.attributes.attribute(userAttribute, "foo") + + project.evaluate() + + val component = target.internal.components.singleOrNull() ?: fail("Expected single component") + if (component.usages.isEmpty()) fail("Expected at least one usage") + component.usages.forEach { usage -> + if (!usage.attributes.contains(userAttribute)) fail("Usage '${usage.name}' does not contain user attribute") + assertEquals("foo", usage.attributes.getAttribute(userAttribute)) + } + } }