[IC] Multi-module incremental compilation smoke tests for KMP

Part 2. Main multi-module KMP smoke tests for incremental compilation

^KT-56963 Fixed
^KT-63876 Fixed

Merge-request: KT-MR-13193
Merged-by: Evgenii Mazhukin <evgenii.mazhukin@jetbrains.com>
This commit is contained in:
Evgenii Mazhukin
2023-12-01 15:46:51 +00:00
committed by Space Team
parent c4fe7e4453
commit befa921fd9
36 changed files with 426 additions and 143 deletions
@@ -9,15 +9,13 @@ import org.gradle.api.logging.LogLevel
import org.gradle.testkit.runner.BuildResult import org.gradle.testkit.runner.BuildResult
import org.gradle.util.GradleVersion import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.testbase.* import org.jetbrains.kotlin.gradle.testbase.*
import org.jetbrains.kotlin.gradle.testbase.relativizeTo import org.jetbrains.kotlin.gradle.util.replaceWithVersion
import java.nio.file.Path import java.nio.file.Path
import java.util.concurrent.atomic.AtomicInteger
import kotlin.io.path.appendText
/** /**
* KmpIncrementalITBase is used as a "limited scope" for experimental test utils. * Convenience class for KMP incremental compilation tests.
* If some of them can be reused widely, consider moving them to KGPBaseTest *
* or the appropriate util package. //TODO: KT-63876 * Consider moving all general-purpose logic to `org.jetbrains.kotlin.gradle.testbase` package.
*/ */
abstract class KmpIncrementalITBase : KGPBaseTest() { abstract class KmpIncrementalITBase : KGPBaseTest() {
@@ -27,9 +25,11 @@ abstract class KmpIncrementalITBase : KGPBaseTest() {
protected open val gradleTask = "assemble" protected open val gradleTask = "assemble"
protected open val projectName = "generic-kmp-app-plus-lib-with-tests" protected open val projectName = "generic-kmp-app-plus-lib-with-tests"
protected open val mainCompileTasks = setOf( protected open val mainCompileTasks = setOf(
":app:compileCommonMainKotlinMetadata",
":app:compileKotlinJvm", ":app:compileKotlinJvm",
":app:compileKotlinJs", ":app:compileKotlinJs",
":app:compileKotlinNative", ":app:compileKotlinNative",
":lib:compileCommonMainKotlinMetadata",
":lib:compileKotlinJvm", ":lib:compileKotlinJvm",
":lib:compileKotlinJs", ":lib:compileKotlinJs",
":lib:compileKotlinNative", ":lib:compileKotlinNative",
@@ -48,30 +48,30 @@ abstract class KmpIncrementalITBase : KGPBaseTest() {
return subProject(subproject).kotlinSourcesDir(srcDir).resolve(name) return subProject(subproject).kotlinSourcesDir(srcDir).resolve(name)
} }
protected fun Path.addPrivateVal(): Path { protected open fun TestProject.checkIncrementalBuild(
this@addPrivateVal.appendText("private val nothingMuch${changeCounter.incrementAndGet()} = 24") tasksExpectedToExecute: Set<String>,
return this@addPrivateVal assertions: BuildResult.() -> Unit = {}
} ) {
protected open fun BuildResult.assertSuccessOrUTD(allTasks: Set<String>, executedTasks: Set<String>) {
assertTasksExecuted(executedTasks)
assertTasksUpToDate(allTasks - executedTasks)
}
protected open fun TestProject.testCase(incrementalPath: Path? = null, executedTasks: Set<String>, assertions: BuildResult.() -> Unit = {}) {
build(gradleTask, buildOptions = defaultBuildOptions.copy(logLevel = LogLevel.DEBUG)) { build(gradleTask, buildOptions = defaultBuildOptions.copy(logLevel = LogLevel.DEBUG)) {
assertSuccessOrUTD( assertTasksExecuted(tasksExpectedToExecute)
allTasks = mainCompileTasks, assertTasksUpToDate(mainCompileTasks - tasksExpectedToExecute)
executedTasks = executedTasks
)
incrementalPath?.let {
assertIncrementalCompilation(listOf(it).relativizeTo(projectPath))
}
assertions() assertions()
} }
} }
companion object { protected open fun TestProject.multiStepCheckIncrementalBuilds(
private val changeCounter = AtomicInteger(0) incrementalPath: Path,
steps: List<String>,
tasksExpectedToExecuteOnEachStep: Set<String>,
afterEachStep: BuildResult.() -> Unit = {}
) {
for (step in steps) {
incrementalPath.replaceWithVersion(step)
build(gradleTask, buildOptions = defaultBuildOptions.copy(logLevel = LogLevel.DEBUG)) {
assertTasksExecuted(tasksExpectedToExecuteOnEachStep)
assertTasksUpToDate(mainCompileTasks - tasksExpectedToExecuteOnEachStep)
afterEachStep()
}
}
} }
} }
@@ -8,6 +8,7 @@ package org.jetbrains.kotlin.gradle.mpp.smoke
import org.gradle.util.GradleVersion import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.mpp.KmpIncrementalITBase import org.jetbrains.kotlin.gradle.mpp.KmpIncrementalITBase
import org.jetbrains.kotlin.gradle.testbase.* import org.jetbrains.kotlin.gradle.testbase.*
import org.jetbrains.kotlin.test.TestMetadata
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
/** /**
@@ -19,6 +20,7 @@ open class BasicIncrementalCompilationIT : KmpIncrementalITBase() {
@DisplayName("Base test case - local change, local recompilation") @DisplayName("Base test case - local change, local recompilation")
@GradleTest @GradleTest
@TestMetadata("generic-kmp-app-plus-lib-with-tests")
fun testStrictlyLocalChange(gradleVersion: GradleVersion): Unit = withProject(gradleVersion) { fun testStrictlyLocalChange(gradleVersion: GradleVersion): Unit = withProject(gradleVersion) {
build("assemble") build("assemble")
@@ -26,84 +28,99 @@ open class BasicIncrementalCompilationIT : KmpIncrementalITBase() {
* Step 1: touch app:common, no abi change * Step 1: touch app:common, no abi change
*/ */
testCase( val sourceInAppCommon = resolvePath("app", "commonMain", "Unused.kt").addPrivateVal()
incrementalPath = resolvePath("app", "commonMain", "Unused.kt").addPrivateVal(), checkIncrementalBuild(
executedTasks = setOf( tasksExpectedToExecute = setOf(
":app:compileCommonMainKotlinMetadata",
":app:compileKotlinJvm", ":app:compileKotlinJvm",
":app:compileKotlinJs", ":app:compileKotlinJs",
":app:compileKotlinNative" ":app:compileKotlinNative"
) )
) ) {
assertIncrementalCompilation(listOf(sourceInAppCommon).relativizeTo(projectPath))
}
/** /**
* Step 2: touch app:jvm, no abi change * Step 2: touch app:jvm, no abi change
*/ */
val appJvmClassKt = resolvePath("app", "jvmMain", "UnusedJvm.kt").addPrivateVal() val sourceInAppJvm = resolvePath("app", "jvmMain", "UnusedJvm.kt").addPrivateVal()
testCase( checkIncrementalBuild(
incrementalPath = null, //TODO: it just doesn't print "Incremental compilation completed", why? (KT-63476) tasksExpectedToExecute = setOf(":app:compileKotlinJvm")
executedTasks = setOf(":app:compileKotlinJvm")
) { ) {
assertCompiledKotlinSources(listOf(appJvmClassKt).relativizeTo(projectPath), output) //TODO: it just doesn't print "Incremental compilation completed", why? (KT-63476)
assertCompiledKotlinSources(listOf(sourceInAppJvm).relativizeTo(projectPath), output)
} }
/** /**
* Step 3: touch app:js, no abi change * Step 3: touch app:js, no abi change
*/ */
testCase( val sourceInAppJs = resolvePath("app", "jsMain", "UnusedJs.kt").addPrivateVal()
incrementalPath = resolvePath("app", "jsMain", "UnusedJs.kt").addPrivateVal(), checkIncrementalBuild(
executedTasks = setOf(":app:compileKotlinJs"), tasksExpectedToExecute = setOf(":app:compileKotlinJs"),
) ) {
assertIncrementalCompilation(listOf(sourceInAppJs).relativizeTo(projectPath))
}
/** /**
* Step 4: touch app:native, no abi change * Step 4: touch app:native, no abi change
*/ */
resolvePath("app", "nativeMain", "UnusedNative.kt").addPrivateVal() resolvePath("app", "nativeMain", "UnusedNative.kt").addPrivateVal()
testCase( checkIncrementalBuild(
incrementalPath = null, // no native incremental compilation - see KT-62824 tasksExpectedToExecute = setOf(":app:compileKotlinNative"),
executedTasks = setOf(":app:compileKotlinNative"),
) )
/** /**
* Step 5: touch lib:common, no abi change * Step 5: touch lib:common, no abi change
*/ */
testCase( val sourceInLibCommon = resolvePath("lib", "commonMain", "UsedInLibPlatformTests.kt").addPrivateVal()
incrementalPath = resolvePath("lib", "commonMain", "UsedInLibPlatformTests.kt").addPrivateVal(), checkIncrementalBuild(
executedTasks = mainCompileTasks // TODO: KT-62642 - bad compile avoidance here tasksExpectedToExecute = mainCompileTasks // TODO: KT-62642 - bad compile avoidance here
) ) {
assertIncrementalCompilation(listOf(sourceInLibCommon).relativizeTo(projectPath))
}
/** /**
* Step 6: touch lib:jvm, no abi change * Step 6: touch lib:jvm, no abi change
*/ */
val libJvmUtilKt = resolvePath("lib", "jvmMain", "UsedInAppJvmAndLibTests.kt").addPrivateVal() val sourceInLibJvm = resolvePath("lib", "jvmMain", "UsedInAppJvmAndLibTests.kt").addPrivateVal()
testCase( checkIncrementalBuild(
incrementalPath = null, //TODO: it just doesn't print "Incremental compilation completed", why? (KT-63476) tasksExpectedToExecute = setOf(
executedTasks = setOf(":app:compileKotlinJvm", ":lib:compileKotlinJvm"), ":app:compileKotlinJvm",
":lib:compileKotlinJvm"
),
) { ) {
assertCompiledKotlinSources(listOf(libJvmUtilKt).relativizeTo(projectPath), output) assertCompiledKotlinSources(listOf(sourceInLibJvm).relativizeTo(projectPath), output)
} }
/** /**
* Step 7: touch lib:js, no abi change * Step 7: touch lib:js, no abi change
*/ */
testCase( val sourceInLibJs = resolvePath("lib", "jsMain", "UsedInAppJsAndLibTests.kt").addPrivateVal()
incrementalPath = resolvePath("lib", "jsMain", "UsedInAppJsAndLibTests.kt").addPrivateVal(), checkIncrementalBuild(
executedTasks = setOf(":app:compileKotlinJs", ":lib:compileKotlinJs"), tasksExpectedToExecute = setOf(
) ":app:compileKotlinJs",
":lib:compileKotlinJs"
),
) {
assertIncrementalCompilation(listOf(sourceInLibJs).relativizeTo(projectPath))
}
/** /**
* Step 8: touch lib:native, no abi change * Step 8: touch lib:native, no abi change
*/ */
resolvePath("lib", "nativeMain", "UsedInAppNativeAndLibTests.kt").addPrivateVal() resolvePath("lib", "nativeMain", "UsedInAppNativeAndLibTests.kt").addPrivateVal()
testCase( checkIncrementalBuild(
incrementalPath = null, tasksExpectedToExecute = setOf(
executedTasks = setOf(":app:compileKotlinNative", ":lib:compileKotlinNative"), ":app:compileKotlinNative",
":lib:compileKotlinNative"
),
) )
} }
} }
@@ -8,6 +8,7 @@ package org.jetbrains.kotlin.gradle.mpp.smoke
import org.gradle.util.GradleVersion import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.mpp.KmpIncrementalITBase import org.jetbrains.kotlin.gradle.mpp.KmpIncrementalITBase
import org.jetbrains.kotlin.gradle.testbase.* import org.jetbrains.kotlin.gradle.testbase.*
import org.jetbrains.kotlin.test.TestMetadata
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
@DisplayName("Basic incremental scenarios with tests in KMP - K2") @DisplayName("Basic incremental scenarios with tests in KMP - K2")
@@ -15,6 +16,9 @@ import org.junit.jupiter.api.DisplayName
open class BasicTestIncrementalCompilationIT : KmpIncrementalITBase() { open class BasicTestIncrementalCompilationIT : KmpIncrementalITBase() {
override val mainCompileTasks: Set<String> override val mainCompileTasks: Set<String>
get() = setOf( get() = setOf(
":app:compileCommonMainKotlinMetadata",
":lib:compileCommonMainKotlinMetadata",
":app:compileTestKotlinJvm", ":app:compileTestKotlinJvm",
":lib:compileTestKotlinJvm", ":lib:compileTestKotlinJvm",
@@ -38,6 +42,7 @@ open class BasicTestIncrementalCompilationIT : KmpIncrementalITBase() {
@DisplayName("KMP tests are rebuilt when affected") @DisplayName("KMP tests are rebuilt when affected")
@GradleTest @GradleTest
@TestMetadata("generic-kmp-app-plus-lib-with-tests")
fun testAffectingTestDependencies(gradleVersion: GradleVersion): Unit = withProject(gradleVersion) { fun testAffectingTestDependencies(gradleVersion: GradleVersion): Unit = withProject(gradleVersion) {
build("build") build("build")
@@ -45,18 +50,21 @@ open class BasicTestIncrementalCompilationIT : KmpIncrementalITBase() {
* Step 1 - touch lib/common, affect all tests in app and lib * Step 1 - touch lib/common, affect all tests in app and lib
*/ */
testCase( val changedInLibCommon = resolvePath("lib", "commonMain", "UsedInLibPlatformTests.kt").addPrivateVal()
incrementalPath = resolvePath("lib", "commonMain", "UsedInLibPlatformTests.kt").addPrivateVal(), checkIncrementalBuild(
executedTasks = mainCompileTasks, tasksExpectedToExecute = mainCompileTasks,
) ) {
assertIncrementalCompilation(listOf(changedInLibCommon).relativizeTo(projectPath))
}
/** /**
* Step 2 - touch app/common, affect all tests in app * Step 2 - touch app/common, affect all tests in app
*/ */
testCase( val changedInAppCommon = resolvePath("app", "commonMain", "Unused.kt").addPrivateVal()
incrementalPath = resolvePath("app", "commonMain", "Unused.kt").addPrivateVal(), checkIncrementalBuild(
executedTasks = setOf( tasksExpectedToExecute = setOf(
":app:compileCommonMainKotlinMetadata",
":app:compileTestKotlinJvm", ":app:compileTestKotlinJvm",
":app:compileTestKotlinNative", ":app:compileTestKotlinNative",
":app:compileTestKotlinJs", ":app:compileTestKotlinJs",
@@ -64,16 +72,17 @@ open class BasicTestIncrementalCompilationIT : KmpIncrementalITBase() {
":app:jvmTest", ":app:jvmTest",
":app:nativeTest", ":app:nativeTest",
), ),
) ) {
assertIncrementalCompilation(listOf(changedInAppCommon).relativizeTo(projectPath))
}
/** /**
* Step 3 - touch app/jvm, affect jvm tests in app * Step 3 - touch app/jvm, affect jvm tests in app
*/ */
val touchedAppJvm = resolvePath("app", "jvmMain", "UnusedJvm.kt").addPrivateVal() val touchedAppJvm = resolvePath("app", "jvmMain", "UnusedJvm.kt").addPrivateVal()
testCase( checkIncrementalBuild(
incrementalPath = null, tasksExpectedToExecute = setOf(
executedTasks = setOf(
":app:compileTestKotlinJvm", ":app:compileTestKotlinJvm",
":app:jvmTest", ":app:jvmTest",
), ),
@@ -85,22 +94,23 @@ open class BasicTestIncrementalCompilationIT : KmpIncrementalITBase() {
* Step 4 - touch app/js, affect js tests in app * Step 4 - touch app/js, affect js tests in app
*/ */
testCase( val changedInAppJs = resolvePath("app", "jsMain", "UnusedJs.kt").addPrivateVal()
incrementalPath = resolvePath("app", "jsMain", "UnusedJs.kt").addPrivateVal(), checkIncrementalBuild(
executedTasks = setOf( tasksExpectedToExecute = setOf(
":app:compileTestKotlinJs", ":app:compileTestKotlinJs",
":app:jsTest", ":app:jsTest",
), ),
) ) {
assertIncrementalCompilation(listOf(changedInAppJs).relativizeTo(projectPath))
}
/** /**
* Step 5 - touch app/native, affect native tests in app * Step 5 - touch app/native, affect native tests in app
*/ */
resolvePath("app", "nativeMain", "UnusedNative.kt").addPrivateVal() resolvePath("app", "nativeMain", "UnusedNative.kt").addPrivateVal()
testCase( checkIncrementalBuild(
incrementalPath = null, tasksExpectedToExecute = setOf(
executedTasks = setOf(
":app:compileTestKotlinNative", ":app:compileTestKotlinNative",
":app:nativeTest", ":app:nativeTest",
), ),
@@ -8,17 +8,15 @@ package org.jetbrains.kotlin.gradle.mpp.smoke
import org.gradle.api.logging.LogLevel import org.gradle.api.logging.LogLevel
import org.gradle.util.GradleVersion import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.testbase.* import org.jetbrains.kotlin.gradle.testbase.*
import org.jetbrains.kotlin.gradle.util.replaceFirst import org.jetbrains.kotlin.gradle.util.replaceWithVersion
import org.jetbrains.kotlin.test.TestMetadata
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import kotlin.io.path.appendText
/** /**
*
* Touch file with expect fun, target IC adds actual to the build set * Touch file with expect fun, target IC adds actual to the build set
* Variants: expect class * Variants: expect class
* Touch file with actual fun, target IC adds expect to the build set * Touch file with actual fun, target IC adds expect to the build set
* Variants: expect class * Variants: expect class
*
*/ */
@DisplayName("Incremental scenarios with expect/actual - K2") @DisplayName("Incremental scenarios with expect/actual - K2")
@MppGradlePluginTests @MppGradlePluginTests
@@ -28,17 +26,21 @@ open class ExpectActualIncrementalCompilationIT : KGPBaseTest() {
@DisplayName("File with actual declaration needs recompiling") @DisplayName("File with actual declaration needs recompiling")
@GradleTest @GradleTest
@TestMetadata("expect-actual-fun-or-class-ic")
fun testRecompilationOfActualFun(gradleVersion: GradleVersion) { fun testRecompilationOfActualFun(gradleVersion: GradleVersion) {
nativeProject("expect-actual-fun-or-class-ic", gradleVersion) { nativeProject("expect-actual-fun-or-class-ic", gradleVersion) {
build("assemble") build("assemble")
listOf( listOf(
kotlinSourcesDir("jvmMain").resolve("ActualFunFoo.kt"), kotlinSourcesDir("jvmMain").resolve("ActualFunFoo.kt"),
kotlinSourcesDir("nativeMain").resolve("ActualFunFoo.kt"),
kotlinSourcesDir("jsMain").resolve("ActualFunFoo.kt") kotlinSourcesDir("jsMain").resolve("ActualFunFoo.kt")
).forEach { ).forEach {
it.replaceFirst("\"foo", "\"bar") // just touch the file. expected logic is this:
// actual declaration needs to be recompiled -> we add expect declaration to the list of compiled files
it.addPrivateVal()
} }
//TODO: KT-63970 - native compilation fails, if val is private
kotlinSourcesDir("nativeMain").resolve("ActualFunFoo.kt").addPublicVal()
build("assemble", buildOptions = defaultBuildOptions.copy(logLevel = LogLevel.DEBUG)) { build("assemble", buildOptions = defaultBuildOptions.copy(logLevel = LogLevel.DEBUG)) {
assertTasksExecuted(":compileKotlinJvm", ":compileKotlinJs", ":compileKotlinNative") assertTasksExecuted(":compileKotlinJvm", ":compileKotlinJs", ":compileKotlinNative")
@@ -55,15 +57,13 @@ open class ExpectActualIncrementalCompilationIT : KGPBaseTest() {
@DisplayName("File with expect declaration needs recompiling indirectly") @DisplayName("File with expect declaration needs recompiling indirectly")
@GradleTest @GradleTest
@TestMetadata("expect-actual-fun-or-class-ic")
fun testRecompilationOfExpectFun(gradleVersion: GradleVersion) { fun testRecompilationOfExpectFun(gradleVersion: GradleVersion) {
nativeProject("expect-actual-fun-or-class-ic", gradleVersion) { nativeProject("expect-actual-fun-or-class-ic", gradleVersion) {
build("assemble") build("assemble")
val unusedKtPath = kotlinSourcesDir("commonMain").resolve("Unused.kt") val commonSourceKt = kotlinSourcesDir("commonMain").resolve("UsedInFileWithExpectFun.kt")
unusedKtPath.replaceFirst( commonSourceKt.replaceWithVersion("sourceCompatibleAbiChange")
"val secret = 1",
"val secret = \"k2\""
)
build("assemble", buildOptions = defaultBuildOptions.copy(logLevel = LogLevel.DEBUG)) { build("assemble", buildOptions = defaultBuildOptions.copy(logLevel = LogLevel.DEBUG)) {
assertTasksExecuted(":compileKotlinJvm", ":compileKotlinJs", ":compileKotlinNative") assertTasksExecuted(":compileKotlinJvm", ":compileKotlinJs", ":compileKotlinNative")
@@ -72,7 +72,7 @@ open class ExpectActualIncrementalCompilationIT : KGPBaseTest() {
kotlinSourcesDir("jvmMain").resolve("ActualFunFoo.kt"), kotlinSourcesDir("jvmMain").resolve("ActualFunFoo.kt"),
kotlinSourcesDir("jsMain").resolve("ActualFunFoo.kt"), kotlinSourcesDir("jsMain").resolve("ActualFunFoo.kt"),
kotlinSourcesDir("commonMain").resolve("ExpectFunFoo.kt"), kotlinSourcesDir("commonMain").resolve("ExpectFunFoo.kt"),
unusedKtPath commonSourceKt
).relativizeTo(projectPath) ).relativizeTo(projectPath)
) )
} }
@@ -81,11 +81,12 @@ open class ExpectActualIncrementalCompilationIT : KGPBaseTest() {
@DisplayName("File with expect class declaration needs recompiling") @DisplayName("File with expect class declaration needs recompiling")
@GradleTest @GradleTest
@TestMetadata("expect-actual-fun-or-class-ic")
fun testRecompilationOfExpectClass(gradleVersion: GradleVersion) { fun testRecompilationOfExpectClass(gradleVersion: GradleVersion) {
nativeProject("expect-actual-fun-or-class-ic", gradleVersion) { nativeProject("expect-actual-fun-or-class-ic", gradleVersion) {
build("assemble") build("assemble")
kotlinSourcesDir("commonMain").resolve("ExpectClassBar.kt").appendText("val irrelevant = 2") kotlinSourcesDir("commonMain").resolve("ExpectClassBar.kt").addPrivateVal()
build("assemble", buildOptions = defaultBuildOptions.copy(logLevel = LogLevel.DEBUG)) { build("assemble", buildOptions = defaultBuildOptions.copy(logLevel = LogLevel.DEBUG)) {
assertTasksExecuted(":compileKotlinJvm", ":compileKotlinJs", ":compileKotlinNative") assertTasksExecuted(":compileKotlinJvm", ":compileKotlinJs", ":compileKotlinNative")
@@ -5,62 +5,237 @@
package org.jetbrains.kotlin.gradle.mpp.smoke package org.jetbrains.kotlin.gradle.mpp.smoke
import org.gradle.api.logging.LogLevel
import org.gradle.testkit.runner.BuildResult
import org.gradle.util.GradleVersion import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.testbase.BuildOptions import org.jetbrains.kotlin.gradle.mpp.KmpIncrementalITBase
import org.jetbrains.kotlin.gradle.testbase.GradleTest import org.jetbrains.kotlin.gradle.testbase.*
import org.jetbrains.kotlin.gradle.testbase.KGPBaseTest import org.jetbrains.kotlin.gradle.util.replaceWithVersion
import org.jetbrains.kotlin.gradle.testbase.MppGradlePluginTests import org.jetbrains.kotlin.test.TestMetadata
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
@Disabled("KT-56963") /**
* Multi-module KMP tests assert that IC is fine, when API changes between two modules, or common/platform parts of a module.
*/
@DisplayName("Basic multi-module incremental scenarios with KMP - K2") @DisplayName("Basic multi-module incremental scenarios with KMP - K2")
@MppGradlePluginTests @MppGradlePluginTests
open class MultiModuleIncrementalCompilationIT : KGPBaseTest() { open class MultiModuleIncrementalCompilationIT : KmpIncrementalITBase() {
override val defaultBuildOptions: BuildOptions
get() = super.defaultBuildOptions.copyEnsuringK2()
/**
* Tests api change across the module + sourceSet boundary
*/
@DisplayName("Verify IC builds on change in lib/commonMain") @DisplayName("Verify IC builds on change in lib/commonMain")
@GradleTest @GradleTest
fun testTouchLibCommon(gradleVersion: GradleVersion) { @TestMetadata("generic-kmp-app-plus-lib-with-tests")
/** fun testTouchLibCommon(gradleVersion: GradleVersion) = withProject(gradleVersion) {
* [Cross-Module] touch libCommon, affect appCommon, appJs build("assemble")
* Variants: 1. code-compatible ABI breakage (change deduced return type), 2. no ABI breakage, 3. error + fix
*/
/**
* Step 1: touch util used in app common
*/
val usedInAppCommon = resolvePath("lib", "commonMain", "UsedInAppCommon.kt")
multiStepCheckIncrementalBuilds(
incrementalPath = usedInAppCommon,
steps = listOf(
"1_addUnusedParameter",
"2_changeReturnType"
),
tasksExpectedToExecuteOnEachStep = mainCompileTasks,
afterEachStep = {
assertIncrementalCompilation(
listOf(
usedInAppCommon,
resolvePath("app", "commonMain", "DependsOnLibCommon.kt")
).relativizeTo(projectPath)
)
}
)
/**
* Step 2: touch class extended in app platform, then build each platform
*/
val usedInAppPlatform = resolvePath("lib", "commonMain", "UsedInAppPlatform.kt")
usedInAppPlatform.replaceWithVersion("1_addNewPublicApi")
fun testIndividualTarget(moduleTask: String, extraAssertions: BuildResult.() -> Unit = {}) {
build(":app:$moduleTask", buildOptions = defaultBuildOptions.copy(logLevel = LogLevel.DEBUG)) {
val targetTasks = setOf(
":app:$moduleTask",
":lib:$moduleTask"
)
assertTasksExecuted(targetTasks)
assertTasksAreNotInTaskGraph(*(mainCompileTasks - targetTasks).toTypedArray())
extraAssertions()
}
}
testIndividualTarget("compileKotlinJvm") {
assertCompiledKotlinSources(
listOf(
usedInAppPlatform,
resolvePath("app", "jvmMain", "DependsOnLibCommon.kt")
).relativizeTo(projectPath),
output
)
}
testIndividualTarget("compileKotlinJs") {
assertIncrementalCompilation(
listOf(
usedInAppPlatform,
resolvePath("app", "jsMain", "DependsOnLibCommon.kt")
).relativizeTo(projectPath)
)
}
testIndividualTarget("compileKotlinNative")
} }
/**
* Three platforms, two steps for each. Do source-compatible changes: first add default parameter,
* then change return type.
* lib/platform utils are used in app/platform with deduced return type.
*/
@DisplayName("Verify IC builds on change in lib/platformMain") @DisplayName("Verify IC builds on change in lib/platformMain")
@GradleTest @GradleTest
fun testTouchLibPlatform(gradleVersion: GradleVersion) { @TestMetadata("generic-kmp-app-plus-lib-with-tests")
/** fun testTouchLibPlatform(gradleVersion: GradleVersion) = withProject(gradleVersion) {
* [C-M] touch lbJs, affect appJs build("assemble")
* Variants: 1. code-compatible ABI breakage (change deduced return type), 2. no ABI breakage, 3. error + fix
*/
val commonSteps = listOf("1_addUnusedParameter", "2_changeReturnType")
/**
* Step 1 - jvm
*/
val jvmUtil = resolvePath("lib", "jvmMain", "UsedInAppJvmAndLibTests.kt")
multiStepCheckIncrementalBuilds(
incrementalPath = jvmUtil,
steps = commonSteps,
tasksExpectedToExecuteOnEachStep = setOf(
":app:compileKotlinJvm",
":lib:compileKotlinJvm"
),
afterEachStep = {
assertCompiledKotlinSources(
expectedSources = listOf(
jvmUtil,
resolvePath("app", "jvmMain", "DependsOnLibJvm.kt")
).relativizeTo(projectPath),
output = output
)
}
)
/**
* Step 2 - js
*/
val jsUtil = resolvePath("lib", "jsMain", "UsedInAppJsAndLibTests.kt")
multiStepCheckIncrementalBuilds(
incrementalPath = jsUtil,
steps = commonSteps,
tasksExpectedToExecuteOnEachStep = setOf(
":app:compileKotlinJs",
":lib:compileKotlinJs"
),
afterEachStep = {
assertIncrementalCompilation(
listOf(
jsUtil,
resolvePath("app", "jsMain", "DependsOnLibJs.kt")
).relativizeTo(projectPath)
)
}
)
/**
* Step 3 - native
*/
val nativeUtil = resolvePath("lib", "nativeMain", "UsedInAppNativeAndLibTests.kt")
multiStepCheckIncrementalBuilds(
incrementalPath = nativeUtil,
steps = commonSteps,
tasksExpectedToExecuteOnEachStep = setOf(
":app:compileKotlinNative",
":lib:compileKotlinNative"
)
)
} }
/**
* Main smoke tests for api changes on the source set boundary
*/
@DisplayName("Verify IC builds on change in app/commonMain") @DisplayName("Verify IC builds on change in app/commonMain")
@GradleTest @GradleTest
fun testTouchAppCommon(gradleVersion: GradleVersion) { @TestMetadata("generic-kmp-app-plus-lib-with-tests")
/** fun testTouchAppCommon(gradleVersion: GradleVersion) = withProject(gradleVersion) {
* [C-M] touch appCommon, rebuild appJs build("assemble")
* Variants: 1. code-compatible ABI breakage (change deduced return type), 2. no ABI breakage, 3. error + fix
*/
//TODO KT-56963 : confirm and create issues for these source-compatible changes val utilPath = resolvePath("app", "commonMain", "UsedInAppPlatform.kt")
//utilKtPath.replaceFirst("fun multiplyByTwo(n: Int): Int", "fun <T> multiplyByTwo(n: T): T") - breaks native
//utilKtPath.replaceFirst("fun multiplyByTwo(n: Int): Int", "fun multiplyByTwo(n: Int, unused: Int = 42): Int") - breaks js multiStepCheckIncrementalBuilds(
incrementalPath = utilPath,
steps = listOf(
"1_changeReturnType",
"2_addUnusedParameter"
// test changes in a different order for robustness
),
tasksExpectedToExecuteOnEachStep = setOf(
":app:compileCommonMainKotlinMetadata",
":app:compileKotlinJvm",
":app:compileKotlinJs",
":app:compileKotlinNative"
),
afterEachStep = {
assertIncrementalCompilation(
listOf(
utilPath,
resolvePath("app", "jsMain", "DependsOnAppCommon.kt"),
resolvePath("app", "jvmMain", "DependsOnAppCommon.kt")
).relativizeTo(projectPath)
)
}
)
} }
/**
* Platform changes in a non-dependency shouldn't affect anything else
*/
@DisplayName("Verify IC builds on change in app/platformMain") @DisplayName("Verify IC builds on change in app/platformMain")
@GradleTest @GradleTest
fun testTouchAppPlatform(gradleVersion: GradleVersion) { @TestMetadata("generic-kmp-app-plus-lib-with-tests")
/** fun testTouchAppPlatform(gradleVersion: GradleVersion) = withProject(gradleVersion) {
* [C-M] touch appJs, rebuild appJs build("assemble")
* Variants: 1. code-compatible ABI breakage (change deduced return type), 2. no ABI breakage, 3. error + fix
*/
/**
* Step 1 - jvm
*/
val changedJvmSource = resolvePath("app", "jvmMain", "UnusedJvm.kt")
.replaceWithVersion("addParent")
checkIncrementalBuild(
tasksExpectedToExecute = setOf(":app:compileKotlinJvm")
) {
assertCompiledKotlinSources(listOf(changedJvmSource).relativizeTo(projectPath), output)
}
/**
* Step 2 - js
*/
val changedJsSource = resolvePath("app", "jsMain", "UnusedJs.kt")
.replaceWithVersion("addParent")
checkIncrementalBuild(
tasksExpectedToExecute = setOf(":app:compileKotlinJs")
) {
assertIncrementalCompilation(listOf(changedJsSource).relativizeTo(projectPath))
}
/**
* Step 3 - native
*/
resolvePath("app", "nativeMain", "UnusedNative.kt")
.replaceWithVersion("addParent")
checkIncrementalBuild(
tasksExpectedToExecute = setOf(":app:compileKotlinNative")
)
} }
} }
@@ -0,0 +1,29 @@
/*
* 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 org.jetbrains.kotlin.gradle.testbase
import org.jetbrains.kotlin.gradle.testbase.TransformationsGeneratorState.changeCounter
import java.nio.file.Path
import java.util.concurrent.atomic.AtomicInteger
import kotlin.io.path.appendText
fun Path.addPrivateVal(): Path {
appendText("\nprivate val integerValue${changeCounter.incrementAndGet()} = 24\n")
return this
}
fun Path.addPublicVal(): Path {
appendText("\nval integerValue${changeCounter.incrementAndGet()} = 25\n")
return this
}
/**
* Ensures that generated names are unique.
*/
private object TransformationsGeneratorState {
val changeCounter = AtomicInteger(0)
}
@@ -35,7 +35,12 @@ fun BuildResult.assertTasksExecuted(vararg tasks: String) {
tasks.forEach { task -> tasks.forEach { task ->
assert(task(task)?.outcome == TaskOutcome.SUCCESS) { assert(task(task)?.outcome == TaskOutcome.SUCCESS) {
printBuildOutput() printBuildOutput()
"Task $task didn't have 'SUCCESS' state: ${task(task)?.outcome}" """
Task $task didn't have 'SUCCESS' state: ${task(task)?.outcome}
Actual task states:
${getActualTasksAsString()}
""".trimIndent()
} }
} }
} }
@@ -92,7 +97,12 @@ fun BuildResult.assertTasksUpToDate(vararg tasks: String) {
tasks.forEach { task -> tasks.forEach { task ->
assert(task(task)?.outcome == TaskOutcome.UP_TO_DATE) { assert(task(task)?.outcome == TaskOutcome.UP_TO_DATE) {
printBuildOutput() printBuildOutput()
"Task $task didn't have 'UP-TO-DATE' state: ${task(task)?.outcome}" """
Task $task didn't have 'UP-TO-DATE' state: ${task(task)?.outcome}
Actual task states:
${getActualTasksAsString()}
""".trimIndent()
} }
} }
} }
@@ -204,6 +214,13 @@ fun BuildResult.assertTasksInBuildOutput(
} }
} }
/**
* Returns printable list of tasks that actually executed.
*/
private fun BuildResult.getActualTasksAsString(): String {
return tasks.joinToString("\n") { "${it.path} - ${it.outcome}" }
}
/** /**
* Method parses the output of a 'tasks --all' build * Method parses the output of a 'tasks --all' build
* and returns a list of all the tasks mentioned in it. * and returns a list of all the tasks mentioned in it.
@@ -8,6 +8,7 @@ package org.jetbrains.kotlin.gradle.util
import org.jetbrains.kotlin.gradle.testbase.createTempDirDeleteOnExit import org.jetbrains.kotlin.gradle.testbase.createTempDirDeleteOnExit
import java.io.File import java.io.File
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.copyTo
import kotlin.io.path.readText import kotlin.io.path.readText
import kotlin.io.path.writeText import kotlin.io.path.writeText
@@ -116,3 +117,9 @@ fun File.replaceText(regex: Regex, replacement: String) {
fun Path.replaceFirst(oldValue: String, newValue: String) { fun Path.replaceFirst(oldValue: String, newValue: String) {
writeText(readText().replaceFirst(oldValue, newValue)) writeText(readText().replaceFirst(oldValue, newValue))
} }
fun Path.replaceWithVersion(versionSuffix: String): Path {
val otherVersion = resolveSibling("$fileName.$versionSuffix")
otherVersion.copyTo(this, overwrite = true)
return this
}
@@ -1,3 +1,3 @@
expect fun foo(): Any expect fun foo(): Any
fun fooImpl() = secret fun fooImpl() = commonMainValue
@@ -1,3 +1,3 @@
class AppCommonUnusedClass { open class AppCommonUnusedClass {
fun bar() = 234 fun bar() = 234
} }
@@ -1,10 +0,0 @@
import kotlin.test.Test
import kotlin.test.assertEquals
class AppCommonTest {
@Test
fun sayYesSaysYes() {
assertEquals("Yes", appCommonFunForAppPlatformAndAppCommonTest())
}
}
@@ -0,0 +1,10 @@
import kotlin.test.Test
import kotlin.test.assertEquals
class AppCommonTest {
@Test
fun threeIsThree() {
assertEquals(3, 3)
}
}
@@ -1,3 +1,3 @@
class AppJsUnusedClassWithDependencies { class AppJsUnusedClassWithDependencies {
val line = appCommonFunForAppPlatformAndAppCommonTest() val line = appCommonFunForAppPlatform()
} }
@@ -0,0 +1,3 @@
class AppJsUnusedClass : AppCommonUnusedClass() {
fun foo() = 234
}
@@ -1,3 +1,3 @@
class AppJvmUnusedClassWithDependencies { class AppJvmUnusedClassWithDependencies {
val line = appCommonFunForAppPlatformAndAppCommonTest() val line = appCommonFunForAppPlatform()
} }
@@ -0,0 +1,3 @@
class AppJvmUnusedClass : AppCommonUnusedClass() {
fun doBar() = 2
}
@@ -1,3 +1,3 @@
class AppNativeUnusedClassWithDependencies { class AppNativeUnusedClassWithDependencies {
val line = appCommonFunForAppPlatformAndAppCommonTest() val line = appCommonFunForAppPlatform()
} }
@@ -0,0 +1,3 @@
class AppNativeUnusedClass : AppCommonUnusedClass() {
fun queueSomething() = Unit
}
@@ -0,0 +1,6 @@
open class LibCommonClassForAppPlatform(
val length: Int,
val builder: String,
) {
val introduceNewPublicApi = "Yes, please"
}
@@ -5,7 +5,7 @@ class LibJsTest {
@Test @Test
fun jsUtilReturns0() { fun jsUtilReturns0() {
assertEquals(0, libJsPlatformUtil()) assertEquals(0, libJsPlatformUtil().toInt())
} }
@Test @Test
@@ -5,7 +5,7 @@ class LibJvmTest {
@Test @Test
fun jvmUtilReturns400() { fun jvmUtilReturns400() {
assertEquals(400, libJvmPlatformUtil()) assertEquals(400, libJvmPlatformUtil().toInt())
} }
@Test @Test
@@ -5,7 +5,7 @@ class LibNativeTest {
@Test @Test
fun nativeUtilReturns800() { fun nativeUtilReturns800() {
assertEquals(800, libNativePlatformUtil()) assertEquals(800, libNativePlatformUtil().toInt())
} }
@Test @Test