[Gradle] Implement initial MppCompositeBuildIT

^KT-52172 Verification Pending
This commit is contained in:
Sebastian Sellmair
2022-12-09 15:19:42 +01:00
committed by Space Team
parent d12fbcf574
commit 7f14255be8
21 changed files with 288 additions and 68 deletions
@@ -11,3 +11,9 @@ interface IdeaKotlinDependencyMatcher {
val description: String
fun matches(dependency: IdeaKotlinDependency): Boolean
}
fun IdeaKotlinDependencyMatcher(description: String, matches: (dependency: IdeaKotlinDependency) -> Boolean) =
object : IdeaKotlinDependencyMatcher {
override val description: String = description
override fun matches(dependency: IdeaKotlinDependency): Boolean = matches(dependency)
}
@@ -12,6 +12,7 @@ import java.io.File
internal class IdeaKotlinProjectArtifactDependencyMatcher(
val type: IdeaKotlinSourceDependency.Type,
val buildId: String,
val projectPath: String,
val artifactFilePath: FilePathRegex
) : IdeaKotlinDependencyMatcher {
@@ -21,6 +22,7 @@ internal class IdeaKotlinProjectArtifactDependencyMatcher(
override fun matches(dependency: IdeaKotlinDependency): Boolean {
if (dependency !is IdeaKotlinProjectArtifactDependency) return false
return dependency.type == type &&
dependency.coordinates.project.buildId == buildId &&
dependency.coordinates.project.projectPath == projectPath &&
artifactFilePath.matches(dependency.coordinates.artifactFile)
@@ -10,6 +10,7 @@ import org.jetbrains.kotlin.gradle.idea.tcs.IdeaKotlinSourceDependency
internal class IdeaKotlinSourceDependencyMatcher(
val type: IdeaKotlinSourceDependency.Type,
val buildId: String,
val projectPath: String,
val sourceSetName: String
) : IdeaKotlinDependencyMatcher {
@@ -19,6 +20,7 @@ internal class IdeaKotlinSourceDependencyMatcher(
override fun matches(dependency: IdeaKotlinDependency): Boolean {
if (dependency !is IdeaKotlinSourceDependency) return false
return dependency.type == type &&
dependency.coordinates.buildId == buildId &&
dependency.coordinates.projectPath == projectPath &&
dependency.coordinates.sourceSetName == sourceSetName
}
@@ -7,6 +7,9 @@ package org.jetbrains.kotlin.gradle.idea.testFixtures.tcs
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.idea.tcs.IdeaKotlinBinaryCoordinates
import org.gradle.api.artifacts.component.BuildIdentifier
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.internal.build.BuildState
import org.jetbrains.kotlin.gradle.idea.tcs.IdeaKotlinDependency
import org.jetbrains.kotlin.gradle.idea.tcs.IdeaKotlinSourceDependency
@@ -22,14 +25,16 @@ fun buildIdeaKotlinDependencyMatchers(notation: Any?): List<IdeaKotlinDependency
}
fun ideSourceDependency(type: IdeaKotlinSourceDependency.Type, project: Project, sourceSetName: String): IdeaKotlinDependencyMatcher {
return IdeaKotlinSourceDependencyMatcher(type, project.path, sourceSetName)
return IdeaKotlinSourceDependencyMatcher(type, project.currentBuildId().name, project.path, sourceSetName)
}
fun ideSourceDependency(type: IdeaKotlinSourceDependency.Type, path: String): IdeaKotlinDependencyMatcher {
val segments = path.split("/")
val projectPath = segments.dropLast(1).joinToString("/")
val buildAndProjectPath = segments.dropLast(1).joinToString(":").split("::", limit = 2)
val buildId = if (buildAndProjectPath.size == 2) buildAndProjectPath.first() else ":"
val projectPath = if (buildAndProjectPath.size == 2) ":" + buildAndProjectPath.last() else buildAndProjectPath.last()
val sourceSetName = segments.last()
return IdeaKotlinSourceDependencyMatcher(type, projectPath, sourceSetName)
return IdeaKotlinSourceDependencyMatcher(type, buildId, projectPath, sourceSetName)
}
fun regularSourceDependency(path: String) = ideSourceDependency(IdeaKotlinSourceDependency.Type.Regular, path)
@@ -42,12 +47,20 @@ fun dependsOnDependency(project: Project, sourceSetName: String) =
fun dependsOnDependency(path: String) = ideSourceDependency(IdeaKotlinSourceDependency.Type.DependsOn, path)
fun projectArtifactDependency(
type: IdeaKotlinSourceDependency.Type = IdeaKotlinSourceDependency.Type.Regular, projectPath: String, artifactFilePath: FilePathRegex
): IdeaKotlinDependencyMatcher = IdeaKotlinProjectArtifactDependencyMatcher(
type = type,
projectPath = projectPath,
artifactFilePath = artifactFilePath
)
type: IdeaKotlinSourceDependency.Type = IdeaKotlinSourceDependency.Type.Regular,
buildIdAndProjectPath: String, artifactFilePath: FilePathRegex
): IdeaKotlinDependencyMatcher {
val slicedProjectPath = buildIdAndProjectPath.split("::", limit = 2)
val buildId = if (slicedProjectPath.size == 2) slicedProjectPath.first() else ":"
val projectPath = if (slicedProjectPath.size == 2) ":" + slicedProjectPath.last() else slicedProjectPath.last()
return IdeaKotlinProjectArtifactDependencyMatcher(
type = type,
buildId = buildId,
projectPath = projectPath,
artifactFilePath = artifactFilePath
)
}
fun binaryCoordinates(regex: Regex): IdeaKotlinDependencyMatcher {
return IdeaBinaryCoordinatesMatcher(regex)
@@ -60,4 +73,8 @@ fun binaryCoordinates(literal: String): IdeaKotlinDependencyMatcher {
fun anyDependency(): IdeaKotlinDependencyMatcher = object : IdeaKotlinDependencyMatcher {
override val description: String get() = "any"
override fun matches(dependency: IdeaKotlinDependency): Boolean = true
}
}
/* Duplicated: Aks Gradle for public API? */
private fun Project.currentBuildId(): BuildIdentifier =
(project as ProjectInternal).services.get(BuildState::class.java).buildIdentifier
@@ -5,13 +5,9 @@
package org.jetbrains.kotlin.gradle
import org.gradle.testkit.runner.BuildResult
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.commonizer.CommonizerTarget
import org.jetbrains.kotlin.commonizer.identityString
import org.jetbrains.kotlin.gradle.idea.proto.tcs.IdeaKotlinDependency
import org.jetbrains.kotlin.gradle.idea.serialize.IdeaKotlinSerializationContext
import org.jetbrains.kotlin.gradle.idea.serialize.IdeaKotlinSerializationLogger
import org.jetbrains.kotlin.gradle.idea.tcs.IdeaKotlinDependency
import org.jetbrains.kotlin.gradle.idea.tcs.IdeaKotlinDependency.Companion.CLASSPATH_BINARY_TYPE
import org.jetbrains.kotlin.gradle.idea.tcs.IdeaKotlinResolvedBinaryDependency
@@ -21,11 +17,10 @@ import org.jetbrains.kotlin.gradle.idea.tcs.extras.isNativeStdlib
import org.jetbrains.kotlin.gradle.idea.tcs.extras.klibExtra
import org.jetbrains.kotlin.gradle.idea.testFixtures.tcs.assertMatches
import org.jetbrains.kotlin.gradle.idea.testFixtures.tcs.binaryCoordinates
import org.jetbrains.kotlin.gradle.plugin.ide.kotlinExtrasSerialization
import org.jetbrains.kotlin.gradle.testbase.*
import org.jetbrains.kotlin.gradle.util.resolveIdeDependencies
import org.jetbrains.kotlin.konan.target.KonanTarget.*
import org.junit.jupiter.api.DisplayName
import java.io.File
import kotlin.test.assertEquals
import kotlin.test.fail
@@ -91,50 +86,3 @@ class MppIdeDependencyResolutionIT : KGPBaseTest() {
}
}
}
/* Test Utils / Test Infrastructure Implementation */
private fun TestProject.resolveIdeDependencies(
subproject: String? = null,
assertions: BuildResult.(dependencies: IdeaKotlinDependenciesContainer) -> Unit
) {
build("${subproject.orEmpty()}:resolveIdeDependencies") {
val subprojectPathPrefix = subproject?.removePrefix(":")?.replace(":", "/")?.plus("/") ?: ""
val output = projectPath.resolve("${subprojectPathPrefix}build/ide/dependencies/proto").toFile()
if (!output.isDirectory) fail("Missing output directory: $output")
val dependenciesBySourceSetName = output.listFiles().orEmpty().associate { sourceSetDirectory ->
if (!sourceSetDirectory.isDirectory) fail("Expected $sourceSetDirectory to be directory")
val serializedDependencyFiles = sourceSetDirectory.listFiles().orEmpty()
val deserializedDependencies = serializedDependencyFiles.map { dependencyFile ->
deserializeIdeaKotlinDependencyOrFail(dependencyFile)
}
sourceSetDirectory.name to deserializedDependencies.toSet()
}
assertions(IdeaKotlinDependenciesContainer(dependenciesBySourceSetName))
}
}
private fun deserializeIdeaKotlinDependencyOrFail(file: File): IdeaKotlinDependency {
return GradleIntegrationTestIdeaKotlinSerializationContext.IdeaKotlinDependency(file.readBytes())
?: fail("Failed to deserialize dependency. $file")
}
private object GradleIntegrationTestIdeaKotlinSerializationContext : IdeaKotlinSerializationContext {
override val extrasSerializationExtension = kotlinExtrasSerialization
override val logger: IdeaKotlinSerializationLogger = object : IdeaKotlinSerializationLogger {
override fun report(severity: IdeaKotlinSerializationLogger.Severity, message: String, cause: Throwable?) {
println("$severity: $message")
if (cause != null) println(cause.stackTraceToString())
}
}
}
private class IdeaKotlinDependenciesContainer(
private val dependencies: Map<String, Set<IdeaKotlinDependency>>
) {
operator fun get(sourceSetName: String) = dependencies[sourceSetName]
?: fail("SourceSet with name $sourceSetName not found. Found: ${dependencies.keys}")
}
@@ -0,0 +1,83 @@
/*
* Copyright 2010-2022 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.mpp
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.testbase.*
import org.jetbrains.kotlin.gradle.util.kotilnNativeDistributionDependencies
import org.jetbrains.kotlin.gradle.util.kotlinStdlibDependencies
import org.jetbrains.kotlin.gradle.util.replaceText
import org.jetbrains.kotlin.gradle.util.resolveIdeDependencies
import org.junit.jupiter.api.DisplayName
import kotlin.io.path.absolutePathString
@MppGradlePluginTests
@DisplayName("Tests for multiplatform with composite builds")
class MppCompositeBuildIT : KGPBaseTest() {
@GradleTest
fun `test - simple composite build - ide dependencies`(gradleVersion: GradleVersion) {
val producer = project("mpp-composite-build/simple/producerBuild", gradleVersion)
project("mpp-composite-build/simple/consumerBuild", gradleVersion) {
settingsGradleKts.toFile().replaceText("<producer_path>", producer.projectPath.absolutePathString())
resolveIdeDependencies(":consumerA") { dependencies ->
dependencies["commonMain"].assertMatches(
regularSourceDependency("producerBuild::producerA/commonMain"),
kotlinStdlibDependencies
)
dependencies["nativeMain"].assertMatches(
dependsOnDependency(":consumerA/commonMain"),
regularSourceDependency("producerBuild::producerA/commonMain"),
regularSourceDependency("producerBuild::producerA/nativeMain"),
regularSourceDependency("producerBuild::producerA/linuxMain"),
kotilnNativeDistributionDependencies
)
dependencies["linuxMain"].assertMatches(
dependencies["nativeMain"],
dependsOnDependency(":consumerA/nativeMain"),
)
dependencies["linuxX64Main"].assertMatches(
dependsOnDependency(":consumerA/commonMain"),
dependsOnDependency(":consumerA/nativeMain"),
dependsOnDependency(":consumerA/linuxMain"),
projectArtifactDependency(Regular, "producerBuild::producerA", FilePathRegex(".*/linuxX64/main/klib/producerA.klib")),
kotilnNativeDistributionDependencies
)
}
}
}
@GradleTest
fun `test - simple composite build - assemble`(gradleVersion: GradleVersion) {
val producer = project("mpp-composite-build/simple/producerBuild", gradleVersion)
project("mpp-composite-build/simple/consumerBuild", gradleVersion) {
settingsGradleKts.toFile().replaceText("<producer_path>", producer.projectPath.absolutePathString())
build("cleanNativeDistributionCommonization")
build("assemble") {
assertTasksExecuted(":consumerA:compileCommonMainKotlinMetadata")
assertTasksExecuted(":consumerA:compileNativeMainKotlinMetadata")
assertTasksExecuted(":consumerA:compileNativeMainKotlinMetadata")
assertTasksExecuted(":consumerA:compileKotlinLinuxX64")
assertTasksExecuted(":consumerA:compileKotlinJvm")
}
build("assemble") {
assertTasksUpToDate(":consumerA:compileCommonMainKotlinMetadata")
assertTasksUpToDate(":consumerA:compileNativeMainKotlinMetadata")
assertTasksUpToDate(":consumerA:compileNativeMainKotlinMetadata")
assertTasksUpToDate(":consumerA:compileKotlinLinuxX64")
assertTasksUpToDate(":consumerA:compileKotlinJvm")
}
}
}
}
@@ -0,0 +1,17 @@
/*
* Copyright 2010-2022 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.util
import org.jetbrains.kotlin.gradle.idea.tcs.IdeaKotlinResolvedBinaryDependency
import org.jetbrains.kotlin.gradle.idea.tcs.extras.isNativeDistribution
import org.jetbrains.kotlin.gradle.idea.testFixtures.tcs.IdeaKotlinDependencyMatcher
import org.jetbrains.kotlin.gradle.idea.testFixtures.tcs.binaryCoordinates
val kotlinStdlibDependencies = binaryCoordinates(Regex(".*kotlin-stdlib.*"))
val kotilnNativeDistributionDependencies = IdeaKotlinDependencyMatcher("native distribution") { dependency ->
dependency is IdeaKotlinResolvedBinaryDependency && dependency.isNativeDistribution
}
@@ -97,3 +97,11 @@ private fun normalizeTail(prefixEnd: Int, path: String, separator: Boolean): Str
return result.toString()
}
fun File.replaceText(oldValue: String, newValue: String) {
writeText(readText().replace(oldValue, newValue))
}
fun File.replaceText(regex: Regex, replacement: String) {
writeText(readText().replace(regex, replacement))
}
@@ -0,0 +1,65 @@
/*
* Copyright 2010-2022 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.util
import org.gradle.testkit.runner.BuildResult
import org.jetbrains.kotlin.gradle.idea.proto.tcs.IdeaKotlinDependency
import org.jetbrains.kotlin.gradle.idea.serialize.IdeaKotlinSerializationContext
import org.jetbrains.kotlin.gradle.idea.serialize.IdeaKotlinSerializationLogger
import org.jetbrains.kotlin.gradle.idea.tcs.IdeaKotlinDependency
import org.jetbrains.kotlin.gradle.plugin.ide.kotlinExtrasSerialization
import org.jetbrains.kotlin.gradle.testbase.TestProject
import org.jetbrains.kotlin.gradle.testbase.build
import java.io.File
import kotlin.test.fail
/* Test Utils / Test Infrastructure Implementation */
internal fun TestProject.resolveIdeDependencies(
subproject: String? = null,
assertions: BuildResult.(dependencies: IdeaKotlinDependenciesContainer) -> Unit
) {
build("${subproject.orEmpty()}:resolveIdeDependencies") {
val subprojectPathPrefix = subproject?.removePrefix(":")?.replace(":", "/")?.plus("/") ?: ""
val output = projectPath.resolve("${subprojectPathPrefix}build/ide/dependencies/proto").toFile()
if (!output.isDirectory) fail("Missing output directory: $output")
val dependenciesBySourceSetName = output.listFiles().orEmpty().associate { sourceSetDirectory ->
if (!sourceSetDirectory.isDirectory) fail("Expected $sourceSetDirectory to be directory")
val serializedDependencyFiles = sourceSetDirectory.listFiles().orEmpty()
val deserializedDependencies = serializedDependencyFiles.map { dependencyFile ->
deserializeIdeaKotlinDependencyOrFail(dependencyFile)
}
sourceSetDirectory.name to deserializedDependencies.toSet()
}
assertions(IdeaKotlinDependenciesContainer(dependenciesBySourceSetName))
}
}
private fun deserializeIdeaKotlinDependencyOrFail(file: File): IdeaKotlinDependency {
return GradleIntegrationTestIdeaKotlinSerializationContext.IdeaKotlinDependency(file.readBytes())
?: fail("Failed to deserialize dependency. $file")
}
private object GradleIntegrationTestIdeaKotlinSerializationContext : IdeaKotlinSerializationContext {
override val extrasSerializationExtension = kotlinExtrasSerialization
override val logger: IdeaKotlinSerializationLogger = object : IdeaKotlinSerializationLogger {
override fun report(severity: IdeaKotlinSerializationLogger.Severity, message: String, cause: Throwable?) {
println("$severity: $message")
if (cause != null) println(cause.stackTraceToString())
}
}
}
class IdeaKotlinDependenciesContainer(
private val dependencies: Map<String, Set<IdeaKotlinDependency>>
) {
operator fun get(sourceSetName: String) = dependencies[sourceSetName]
?: fail("SourceSet with name $sourceSetName not found. Found: ${dependencies.keys}")
}
@@ -0,0 +1,16 @@
@file:Suppress("OPT_IN_USAGE")
plugins {
kotlin("multiplatform")
}
kotlin {
targetHierarchy.default()
jvm()
linuxX64()
linuxArm64()
sourceSets.commonMain.get().dependencies {
implementation("org.jetbrains.sample:producerA:1.0.0-SNAPSHOT")
}
}
@@ -0,0 +1,7 @@
object ConsumerALinuxX64 {
init {
ProducerACommon
ProducerANative
ProducerALinuxX64
}
}
@@ -0,0 +1,14 @@
dependencyResolutionManagement {
repositories {
mavenLocal()
mavenCentral()
}
}
include(":consumerA")
includeBuild("<producer_path>") {
dependencySubstitution {
substitute(module("org.jetbrains.sample:producerA")).using(project(":producerA"))
}
}
@@ -0,0 +1,17 @@
@file:Suppress("OPT_IN_USAGE")
plugins {
kotlin("multiplatform")
`maven-publish`
}
group = "org.jetbrains.sample"
version = "1.0.0-SNAPSHOT"
kotlin {
targetHierarchy.default()
jvm()
linuxX64()
linuxArm64()
}
@@ -0,0 +1,8 @@
dependencyResolutionManagement {
repositories {
mavenLocal()
mavenCentral()
}
}
include(":producerA")
@@ -171,8 +171,8 @@ class IdeSourceDependencyResolutionTest {
consumer.resolveDependencies("jvmAndAndroidMain").assertMatches(
regularSourceDependency(":producer/commonMain"),
regularSourceDependency(":producer/jvmAndAndroidMain"),
projectArtifactDependency(Regular, ":producer", FilePathRegex(".*/producer/build/libs/producer-jvm.jar")),
dependsOnDependency(":consumer/commonMain")
dependsOnDependency(":consumer/commonMain"),
projectArtifactDependency(Regular, ":producer", FilePathRegex(".*/build/libs/producer-jvm.jar"))
)
}
@@ -5,11 +5,8 @@
package org.jetbrains.kotlin.gradle.ide
import org.jetbrains.kotlin.gradle.idea.tcs.IdeaKotlinSourceDependency
import org.jetbrains.kotlin.gradle.idea.testFixtures.tcs.ideSourceDependency
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.sources.internal
import org.jetbrains.kotlin.gradle.plugin.sources.project
fun dependsOnDependency(sourceSet: KotlinSourceSet) =
org.jetbrains.kotlin.gradle.idea.testFixtures.tcs.dependsOnDependency(sourceSet.internal.project, sourceSet.name)