[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.util.GradleVersion
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.util.concurrent.atomic.AtomicInteger
import kotlin.io.path.appendText
/**
* KmpIncrementalITBase is used as a "limited scope" for experimental test utils.
* If some of them can be reused widely, consider moving them to KGPBaseTest
* or the appropriate util package. //TODO: KT-63876
* Convenience class for KMP incremental compilation tests.
*
* Consider moving all general-purpose logic to `org.jetbrains.kotlin.gradle.testbase` package.
*/
abstract class KmpIncrementalITBase : KGPBaseTest() {
@@ -27,9 +25,11 @@ abstract class KmpIncrementalITBase : KGPBaseTest() {
protected open val gradleTask = "assemble"
protected open val projectName = "generic-kmp-app-plus-lib-with-tests"
protected open val mainCompileTasks = setOf(
":app:compileCommonMainKotlinMetadata",
":app:compileKotlinJvm",
":app:compileKotlinJs",
":app:compileKotlinNative",
":lib:compileCommonMainKotlinMetadata",
":lib:compileKotlinJvm",
":lib:compileKotlinJs",
":lib:compileKotlinNative",
@@ -48,30 +48,30 @@ abstract class KmpIncrementalITBase : KGPBaseTest() {
return subProject(subproject).kotlinSourcesDir(srcDir).resolve(name)
}
protected fun Path.addPrivateVal(): Path {
this@addPrivateVal.appendText("private val nothingMuch${changeCounter.incrementAndGet()} = 24")
return this@addPrivateVal
}
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 = {}) {
protected open fun TestProject.checkIncrementalBuild(
tasksExpectedToExecute: Set<String>,
assertions: BuildResult.() -> Unit = {}
) {
build(gradleTask, buildOptions = defaultBuildOptions.copy(logLevel = LogLevel.DEBUG)) {
assertSuccessOrUTD(
allTasks = mainCompileTasks,
executedTasks = executedTasks
)
incrementalPath?.let {
assertIncrementalCompilation(listOf(it).relativizeTo(projectPath))
}
assertTasksExecuted(tasksExpectedToExecute)
assertTasksUpToDate(mainCompileTasks - tasksExpectedToExecute)
assertions()
}
}
companion object {
private val changeCounter = AtomicInteger(0)
protected open fun TestProject.multiStepCheckIncrementalBuilds(
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.jetbrains.kotlin.gradle.mpp.KmpIncrementalITBase
import org.jetbrains.kotlin.gradle.testbase.*
import org.jetbrains.kotlin.test.TestMetadata
import org.junit.jupiter.api.DisplayName
/**
@@ -19,6 +20,7 @@ open class BasicIncrementalCompilationIT : KmpIncrementalITBase() {
@DisplayName("Base test case - local change, local recompilation")
@GradleTest
@TestMetadata("generic-kmp-app-plus-lib-with-tests")
fun testStrictlyLocalChange(gradleVersion: GradleVersion): Unit = withProject(gradleVersion) {
build("assemble")
@@ -26,84 +28,99 @@ open class BasicIncrementalCompilationIT : KmpIncrementalITBase() {
* Step 1: touch app:common, no abi change
*/
testCase(
incrementalPath = resolvePath("app", "commonMain", "Unused.kt").addPrivateVal(),
executedTasks = setOf(
val sourceInAppCommon = resolvePath("app", "commonMain", "Unused.kt").addPrivateVal()
checkIncrementalBuild(
tasksExpectedToExecute = setOf(
":app:compileCommonMainKotlinMetadata",
":app:compileKotlinJvm",
":app:compileKotlinJs",
":app:compileKotlinNative"
)
)
) {
assertIncrementalCompilation(listOf(sourceInAppCommon).relativizeTo(projectPath))
}
/**
* Step 2: touch app:jvm, no abi change
*/
val appJvmClassKt = resolvePath("app", "jvmMain", "UnusedJvm.kt").addPrivateVal()
testCase(
incrementalPath = null, //TODO: it just doesn't print "Incremental compilation completed", why? (KT-63476)
executedTasks = setOf(":app:compileKotlinJvm")
val sourceInAppJvm = resolvePath("app", "jvmMain", "UnusedJvm.kt").addPrivateVal()
checkIncrementalBuild(
tasksExpectedToExecute = 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
*/
testCase(
incrementalPath = resolvePath("app", "jsMain", "UnusedJs.kt").addPrivateVal(),
executedTasks = setOf(":app:compileKotlinJs"),
)
val sourceInAppJs = resolvePath("app", "jsMain", "UnusedJs.kt").addPrivateVal()
checkIncrementalBuild(
tasksExpectedToExecute = setOf(":app:compileKotlinJs"),
) {
assertIncrementalCompilation(listOf(sourceInAppJs).relativizeTo(projectPath))
}
/**
* Step 4: touch app:native, no abi change
*/
resolvePath("app", "nativeMain", "UnusedNative.kt").addPrivateVal()
testCase(
incrementalPath = null, // no native incremental compilation - see KT-62824
executedTasks = setOf(":app:compileKotlinNative"),
checkIncrementalBuild(
tasksExpectedToExecute = setOf(":app:compileKotlinNative"),
)
/**
* Step 5: touch lib:common, no abi change
*/
testCase(
incrementalPath = resolvePath("lib", "commonMain", "UsedInLibPlatformTests.kt").addPrivateVal(),
executedTasks = mainCompileTasks // TODO: KT-62642 - bad compile avoidance here
)
val sourceInLibCommon = resolvePath("lib", "commonMain", "UsedInLibPlatformTests.kt").addPrivateVal()
checkIncrementalBuild(
tasksExpectedToExecute = mainCompileTasks // TODO: KT-62642 - bad compile avoidance here
) {
assertIncrementalCompilation(listOf(sourceInLibCommon).relativizeTo(projectPath))
}
/**
* Step 6: touch lib:jvm, no abi change
*/
val libJvmUtilKt = resolvePath("lib", "jvmMain", "UsedInAppJvmAndLibTests.kt").addPrivateVal()
testCase(
incrementalPath = null, //TODO: it just doesn't print "Incremental compilation completed", why? (KT-63476)
executedTasks = setOf(":app:compileKotlinJvm", ":lib:compileKotlinJvm"),
val sourceInLibJvm = resolvePath("lib", "jvmMain", "UsedInAppJvmAndLibTests.kt").addPrivateVal()
checkIncrementalBuild(
tasksExpectedToExecute = setOf(
":app:compileKotlinJvm",
":lib:compileKotlinJvm"
),
) {
assertCompiledKotlinSources(listOf(libJvmUtilKt).relativizeTo(projectPath), output)
assertCompiledKotlinSources(listOf(sourceInLibJvm).relativizeTo(projectPath), output)
}
/**
* Step 7: touch lib:js, no abi change
*/
testCase(
incrementalPath = resolvePath("lib", "jsMain", "UsedInAppJsAndLibTests.kt").addPrivateVal(),
executedTasks = setOf(":app:compileKotlinJs", ":lib:compileKotlinJs"),
)
val sourceInLibJs = resolvePath("lib", "jsMain", "UsedInAppJsAndLibTests.kt").addPrivateVal()
checkIncrementalBuild(
tasksExpectedToExecute = setOf(
":app:compileKotlinJs",
":lib:compileKotlinJs"
),
) {
assertIncrementalCompilation(listOf(sourceInLibJs).relativizeTo(projectPath))
}
/**
* Step 8: touch lib:native, no abi change
*/
resolvePath("lib", "nativeMain", "UsedInAppNativeAndLibTests.kt").addPrivateVal()
testCase(
incrementalPath = null,
executedTasks = setOf(":app:compileKotlinNative", ":lib:compileKotlinNative"),
checkIncrementalBuild(
tasksExpectedToExecute = setOf(
":app:compileKotlinNative",
":lib:compileKotlinNative"
),
)
}
}
@@ -8,6 +8,7 @@ package org.jetbrains.kotlin.gradle.mpp.smoke
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.mpp.KmpIncrementalITBase
import org.jetbrains.kotlin.gradle.testbase.*
import org.jetbrains.kotlin.test.TestMetadata
import org.junit.jupiter.api.DisplayName
@DisplayName("Basic incremental scenarios with tests in KMP - K2")
@@ -15,6 +16,9 @@ import org.junit.jupiter.api.DisplayName
open class BasicTestIncrementalCompilationIT : KmpIncrementalITBase() {
override val mainCompileTasks: Set<String>
get() = setOf(
":app:compileCommonMainKotlinMetadata",
":lib:compileCommonMainKotlinMetadata",
":app:compileTestKotlinJvm",
":lib:compileTestKotlinJvm",
@@ -38,6 +42,7 @@ open class BasicTestIncrementalCompilationIT : KmpIncrementalITBase() {
@DisplayName("KMP tests are rebuilt when affected")
@GradleTest
@TestMetadata("generic-kmp-app-plus-lib-with-tests")
fun testAffectingTestDependencies(gradleVersion: GradleVersion): Unit = withProject(gradleVersion) {
build("build")
@@ -45,18 +50,21 @@ open class BasicTestIncrementalCompilationIT : KmpIncrementalITBase() {
* Step 1 - touch lib/common, affect all tests in app and lib
*/
testCase(
incrementalPath = resolvePath("lib", "commonMain", "UsedInLibPlatformTests.kt").addPrivateVal(),
executedTasks = mainCompileTasks,
)
val changedInLibCommon = resolvePath("lib", "commonMain", "UsedInLibPlatformTests.kt").addPrivateVal()
checkIncrementalBuild(
tasksExpectedToExecute = mainCompileTasks,
) {
assertIncrementalCompilation(listOf(changedInLibCommon).relativizeTo(projectPath))
}
/**
* Step 2 - touch app/common, affect all tests in app
*/
testCase(
incrementalPath = resolvePath("app", "commonMain", "Unused.kt").addPrivateVal(),
executedTasks = setOf(
val changedInAppCommon = resolvePath("app", "commonMain", "Unused.kt").addPrivateVal()
checkIncrementalBuild(
tasksExpectedToExecute = setOf(
":app:compileCommonMainKotlinMetadata",
":app:compileTestKotlinJvm",
":app:compileTestKotlinNative",
":app:compileTestKotlinJs",
@@ -64,16 +72,17 @@ open class BasicTestIncrementalCompilationIT : KmpIncrementalITBase() {
":app:jvmTest",
":app:nativeTest",
),
)
) {
assertIncrementalCompilation(listOf(changedInAppCommon).relativizeTo(projectPath))
}
/**
* Step 3 - touch app/jvm, affect jvm tests in app
*/
val touchedAppJvm = resolvePath("app", "jvmMain", "UnusedJvm.kt").addPrivateVal()
testCase(
incrementalPath = null,
executedTasks = setOf(
checkIncrementalBuild(
tasksExpectedToExecute = setOf(
":app:compileTestKotlinJvm",
":app:jvmTest",
),
@@ -85,22 +94,23 @@ open class BasicTestIncrementalCompilationIT : KmpIncrementalITBase() {
* Step 4 - touch app/js, affect js tests in app
*/
testCase(
incrementalPath = resolvePath("app", "jsMain", "UnusedJs.kt").addPrivateVal(),
executedTasks = setOf(
val changedInAppJs = resolvePath("app", "jsMain", "UnusedJs.kt").addPrivateVal()
checkIncrementalBuild(
tasksExpectedToExecute = setOf(
":app:compileTestKotlinJs",
":app:jsTest",
),
)
) {
assertIncrementalCompilation(listOf(changedInAppJs).relativizeTo(projectPath))
}
/**
* Step 5 - touch app/native, affect native tests in app
*/
resolvePath("app", "nativeMain", "UnusedNative.kt").addPrivateVal()
testCase(
incrementalPath = null,
executedTasks = setOf(
checkIncrementalBuild(
tasksExpectedToExecute = setOf(
":app:compileTestKotlinNative",
":app:nativeTest",
),
@@ -8,17 +8,15 @@ package org.jetbrains.kotlin.gradle.mpp.smoke
import org.gradle.api.logging.LogLevel
import org.gradle.util.GradleVersion
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 kotlin.io.path.appendText
/**
*
* Touch file with expect fun, target IC adds actual to the build set
* Variants: expect class
* Touch file with actual fun, target IC adds expect to the build set
* Variants: expect class
*
*/
@DisplayName("Incremental scenarios with expect/actual - K2")
@MppGradlePluginTests
@@ -28,17 +26,21 @@ open class ExpectActualIncrementalCompilationIT : KGPBaseTest() {
@DisplayName("File with actual declaration needs recompiling")
@GradleTest
@TestMetadata("expect-actual-fun-or-class-ic")
fun testRecompilationOfActualFun(gradleVersion: GradleVersion) {
nativeProject("expect-actual-fun-or-class-ic", gradleVersion) {
build("assemble")
listOf(
kotlinSourcesDir("jvmMain").resolve("ActualFunFoo.kt"),
kotlinSourcesDir("nativeMain").resolve("ActualFunFoo.kt"),
kotlinSourcesDir("jsMain").resolve("ActualFunFoo.kt")
).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)) {
assertTasksExecuted(":compileKotlinJvm", ":compileKotlinJs", ":compileKotlinNative")
@@ -55,15 +57,13 @@ open class ExpectActualIncrementalCompilationIT : KGPBaseTest() {
@DisplayName("File with expect declaration needs recompiling indirectly")
@GradleTest
@TestMetadata("expect-actual-fun-or-class-ic")
fun testRecompilationOfExpectFun(gradleVersion: GradleVersion) {
nativeProject("expect-actual-fun-or-class-ic", gradleVersion) {
build("assemble")
val unusedKtPath = kotlinSourcesDir("commonMain").resolve("Unused.kt")
unusedKtPath.replaceFirst(
"val secret = 1",
"val secret = \"k2\""
)
val commonSourceKt = kotlinSourcesDir("commonMain").resolve("UsedInFileWithExpectFun.kt")
commonSourceKt.replaceWithVersion("sourceCompatibleAbiChange")
build("assemble", buildOptions = defaultBuildOptions.copy(logLevel = LogLevel.DEBUG)) {
assertTasksExecuted(":compileKotlinJvm", ":compileKotlinJs", ":compileKotlinNative")
@@ -72,7 +72,7 @@ open class ExpectActualIncrementalCompilationIT : KGPBaseTest() {
kotlinSourcesDir("jvmMain").resolve("ActualFunFoo.kt"),
kotlinSourcesDir("jsMain").resolve("ActualFunFoo.kt"),
kotlinSourcesDir("commonMain").resolve("ExpectFunFoo.kt"),
unusedKtPath
commonSourceKt
).relativizeTo(projectPath)
)
}
@@ -81,11 +81,12 @@ open class ExpectActualIncrementalCompilationIT : KGPBaseTest() {
@DisplayName("File with expect class declaration needs recompiling")
@GradleTest
@TestMetadata("expect-actual-fun-or-class-ic")
fun testRecompilationOfExpectClass(gradleVersion: GradleVersion) {
nativeProject("expect-actual-fun-or-class-ic", gradleVersion) {
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)) {
assertTasksExecuted(":compileKotlinJvm", ":compileKotlinJs", ":compileKotlinNative")
@@ -5,62 +5,237 @@
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.jetbrains.kotlin.gradle.testbase.BuildOptions
import org.jetbrains.kotlin.gradle.testbase.GradleTest
import org.jetbrains.kotlin.gradle.testbase.KGPBaseTest
import org.jetbrains.kotlin.gradle.testbase.MppGradlePluginTests
import org.junit.jupiter.api.Disabled
import org.jetbrains.kotlin.gradle.mpp.KmpIncrementalITBase
import org.jetbrains.kotlin.gradle.testbase.*
import org.jetbrains.kotlin.gradle.util.replaceWithVersion
import org.jetbrains.kotlin.test.TestMetadata
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")
@MppGradlePluginTests
open class MultiModuleIncrementalCompilationIT : KGPBaseTest() {
override val defaultBuildOptions: BuildOptions
get() = super.defaultBuildOptions.copyEnsuringK2()
open class MultiModuleIncrementalCompilationIT : KmpIncrementalITBase() {
/**
* Tests api change across the module + sourceSet boundary
*/
@DisplayName("Verify IC builds on change in lib/commonMain")
@GradleTest
fun testTouchLibCommon(gradleVersion: GradleVersion) {
/**
* [Cross-Module] touch libCommon, affect appCommon, appJs
* Variants: 1. code-compatible ABI breakage (change deduced return type), 2. no ABI breakage, 3. error + fix
*/
@TestMetadata("generic-kmp-app-plus-lib-with-tests")
fun testTouchLibCommon(gradleVersion: GradleVersion) = withProject(gradleVersion) {
build("assemble")
/**
* 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")
@GradleTest
fun testTouchLibPlatform(gradleVersion: GradleVersion) {
/**
* [C-M] touch lbJs, affect appJs
* Variants: 1. code-compatible ABI breakage (change deduced return type), 2. no ABI breakage, 3. error + fix
*/
@TestMetadata("generic-kmp-app-plus-lib-with-tests")
fun testTouchLibPlatform(gradleVersion: GradleVersion) = withProject(gradleVersion) {
build("assemble")
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")
@GradleTest
fun testTouchAppCommon(gradleVersion: GradleVersion) {
/**
* [C-M] touch appCommon, rebuild appJs
* Variants: 1. code-compatible ABI breakage (change deduced return type), 2. no ABI breakage, 3. error + fix
*/
@TestMetadata("generic-kmp-app-plus-lib-with-tests")
fun testTouchAppCommon(gradleVersion: GradleVersion) = withProject(gradleVersion) {
build("assemble")
//TODO KT-56963 : confirm and create issues for these source-compatible changes
//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
val utilPath = resolvePath("app", "commonMain", "UsedInAppPlatform.kt")
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")
@GradleTest
fun testTouchAppPlatform(gradleVersion: GradleVersion) {
/**
* [C-M] touch appJs, rebuild appJs
* Variants: 1. code-compatible ABI breakage (change deduced return type), 2. no ABI breakage, 3. error + fix
*/
@TestMetadata("generic-kmp-app-plus-lib-with-tests")
fun testTouchAppPlatform(gradleVersion: GradleVersion) = withProject(gradleVersion) {
build("assemble")
/**
* 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 ->
assert(task(task)?.outcome == TaskOutcome.SUCCESS) {
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 ->
assert(task(task)?.outcome == TaskOutcome.UP_TO_DATE) {
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
* 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 java.io.File
import java.nio.file.Path
import kotlin.io.path.copyTo
import kotlin.io.path.readText
import kotlin.io.path.writeText
@@ -116,3 +117,9 @@ fun File.replaceText(regex: Regex, replacement: String) {
fun Path.replaceFirst(oldValue: String, newValue: String) {
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
fun fooImpl() = secret
fun fooImpl() = commonMainValue
@@ -1,3 +1,3 @@
class AppCommonUnusedClass {
open class AppCommonUnusedClass {
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 {
val line = appCommonFunForAppPlatformAndAppCommonTest()
val line = appCommonFunForAppPlatform()
}
@@ -0,0 +1,3 @@
class AppJsUnusedClass : AppCommonUnusedClass() {
fun foo() = 234
}
@@ -1,3 +1,3 @@
class AppJvmUnusedClassWithDependencies {
val line = appCommonFunForAppPlatformAndAppCommonTest()
val line = appCommonFunForAppPlatform()
}
@@ -0,0 +1,3 @@
class AppJvmUnusedClass : AppCommonUnusedClass() {
fun doBar() = 2
}
@@ -1,3 +1,3 @@
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
fun jsUtilReturns0() {
assertEquals(0, libJsPlatformUtil())
assertEquals(0, libJsPlatformUtil().toInt())
}
@Test
@@ -5,7 +5,7 @@ class LibJvmTest {
@Test
fun jvmUtilReturns400() {
assertEquals(400, libJvmPlatformUtil())
assertEquals(400, libJvmPlatformUtil().toInt())
}
@Test
@@ -5,7 +5,7 @@ class LibNativeTest {
@Test
fun nativeUtilReturns800() {
assertEquals(800, libNativePlatformUtil())
assertEquals(800, libNativePlatformUtil().toInt())
}
@Test