Migrate KaptIncrementalIT to new test DSL

^KT-45745 In Progress
This commit is contained in:
Yahor Berdnikau
2022-01-27 11:28:51 +01:00
committed by TeamCityServer
parent a0cb7c8173
commit 9ca2ce6647
@@ -1,338 +1,379 @@
package org.jetbrains.kotlin.gradle package org.jetbrains.kotlin.gradle
import org.gradle.api.logging.LogLevel
import org.gradle.api.logging.configuration.WarningMode import org.gradle.api.logging.configuration.WarningMode
import org.jetbrains.kotlin.gradle.util.* import org.gradle.testkit.runner.BuildResult
import org.junit.Test import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.testbase.*
import org.junit.jupiter.api.DisplayName
import java.nio.file.Path
import java.util.*
import kotlin.io.path.createDirectories
import kotlin.io.path.deleteIfExists
import kotlin.io.path.writeText
import kotlin.test.assertEquals import kotlin.test.assertEquals
open class KaptIncrementalIT : BaseGradleIT() { @DisplayName("Kapt incremental compilation")
@OtherGradlePluginTests
open class KaptIncrementalIT : KGPBaseTest() {
companion object { companion object {
private val EXAMPLE_ANNOTATION_REGEX = "@(field:)?example.ExampleAnnotation".toRegex() private val EXAMPLE_ANNOTATION_REGEX = "@(field:)?example.ExampleAnnotation".toRegex()
const val PROJECT_NAME = "kaptIncrementalCompilationProject"
const val KAPT3_STUBS_PATH = "build/tmp/kapt3/stubs/main"
} }
open fun getProject() =
Project(
"kaptIncrementalCompilationProject",
GradleVersionRequired.None
).apply { setupWorkingDir() }
private val annotatedElements = private val annotatedElements =
arrayOf("A", "funA", "valA", "funUtil", "valUtil", "B", "funB", "valB", "useB") arrayOf("A", "funA", "valA", "funUtil", "valUtil", "B", "funB", "valB", "useB")
override fun defaultBuildOptions(): BuildOptions { override val defaultBuildOptions = super.defaultBuildOptions.copy(
return super.defaultBuildOptions().copy(incremental = true, warningMode = WarningMode.Fail) incremental = true,
} warningMode = WarningMode.Fail,
kaptOptions = BuildOptions.KaptOptions(incrementalKapt = true)
)
@Test @DisplayName("After adding new line compilation is incremental")
fun testAddNewLine() { @GradleTest
val project = getProject() fun testAddNewLine(gradleVersion: GradleVersion) {
project(PROJECT_NAME, gradleVersion) {
build("clean", "build")
project.build("clean", "build") { javaSourcesDir().resolve("bar/useB.kt").modify { "\n$it" }
assertSuccessful() build("build") {
} assertTasksExecuted(":kaptGenerateStubsKotlin", ":compileKotlin")
assertTasksUpToDate(":kaptKotlin")
project.projectFile("useB.kt").modify { "\n$it" } assertTasksUpToDate(":compileJava")
project.build("build") { }
assertSuccessful()
assertTasksExecuted(":kaptGenerateStubsKotlin", ":compileKotlin")
assertTasksUpToDate(":kaptKotlin")
assertTasksUpToDate(":compileJava")
} }
} }
@Test @DisplayName("On rebuild without changes tasks should be UP-TO-DATE")
fun testBasic() { @GradleTest
val project = getProject() fun testBasic(gradleVersion: GradleVersion) {
project(
PROJECT_NAME,
gradleVersion,
buildOptions = defaultBuildOptions.copy(logLevel = LogLevel.DEBUG)
) {
build("build") {
checkGenerated(kaptGeneratedToPath, *annotatedElements)
checkNotGenerated(kaptGeneratedToPath, "notAnnotatedFun")
assertOutputContains("foo.ATest PASSED")
}
project.build("build") { build("build") {
assertSuccessful() assertTasksUpToDate(
checkGenerated(*annotatedElements) ":compileKotlin",
checkNotGenerated("notAnnotatedFun") ":compileJava"
assertContains("foo.ATest PASSED") )
}
project.build("build") { assertTasksUpToDate(
assertSuccessful() ":kaptKotlin",
assertTasksUpToDate( ":kaptGenerateStubsKotlin"
":compileKotlin", )
":compileJava" }
)
assertTasksUpToDate(
":kaptKotlin",
":kaptGenerateStubsKotlin"
)
} }
} }
@Test @DisplayName("Successfully rebuild after error")
fun testCompileError() { @GradleTest
val project = getProject() fun testCompileError(gradleVersion: GradleVersion) {
project(PROJECT_NAME, gradleVersion) {
build("assemble")
project.build("build") { val bKt = javaSourcesDir().resolve("bar/B.kt")
assertSuccessful() val errorKt = bKt.resolveSibling("error.kt")
} errorKt.writeText("<COMPILE_ERROR_MARKER>")
val bKt = project.projectDir.getFileByName("B.kt") buildAndFail("assemble") {
val errorKt = bKt.resolveSibling("error.kt") assertTasksFailed(":kaptGenerateStubsKotlin")
errorKt.writeText("<COMPILE_ERROR_MARKER>") }
project.build("build") { errorKt.deleteIfExists()
assertFailed() bKt.modify { "$it\n" }
assertTasksFailed(":kaptGenerateStubsKotlin") build("assemble", buildOptions = buildOptions.copy(logLevel = LogLevel.DEBUG)) {
} assertCompiledKotlinSources(listOf(projectPath.relativize(bKt)), output)
assertTasksExecuted(":kaptGenerateStubsKotlin", ":compileKotlin")
errorKt.delete() }
bKt.modify { "$it\n" }
project.build("build") {
assertSuccessful()
assertCompiledKotlinSources(project.relativize(bKt))
assertTasksExecuted(":kaptGenerateStubsKotlin", ":compileKotlin")
} }
} }
@Test @DisplayName("Change in the function body without changing the signature")
fun testChangeFunctionBodyWithoutChangingSignature() { @GradleTest
val project = getProject() fun testChangeFunctionBodyWithoutChangingSignature(gradleVersion: GradleVersion) {
project(PROJECT_NAME, gradleVersion) {
build("build", buildOptions = buildOptions.copy(logLevel = LogLevel.DEBUG)) {
checkGenerated(kaptGeneratedToPath, *annotatedElements)
checkNotGenerated(kaptGeneratedToPath, "notAnnotatedFun")
assertOutputContains("foo.ATest PASSED")
}
project.build("build") { val utilKt = javaSourcesDir().resolve("baz/util.kt")
assertSuccessful() utilKt.modify { oldContent ->
checkGenerated(*annotatedElements) assert(oldContent.contains("2 * 2 == 4"))
checkNotGenerated("notAnnotatedFun") oldContent.replace("2 * 2 == 4", "2 * 2 == 5")
assertContains("foo.ATest PASSED") }
}
val utilKt = project.projectDir.getFileByName("util.kt") build("assemble") {
utilKt.modify { oldContent -> assertTasksExecuted(":kaptGenerateStubsKotlin")
assert(oldContent.contains("2 * 2 == 4")) assertTasksUpToDate(":kaptKotlin")
oldContent.replace("2 * 2 == 4", "2 * 2 == 5") }
}
project.build("build") {
assertSuccessful()
assertTasksExecuted(":kaptGenerateStubsKotlin")
assertTasksUpToDate(":kaptKotlin")
} }
} }
@Test @DisplayName("Adding new annotated element")
fun testAddAnnotatedElement() { @GradleTest
val project = getProject() fun testAddAnnotatedElement(gradleVersion: GradleVersion) {
project.build("build") { project(PROJECT_NAME, gradleVersion) {
assertSuccessful() build("assemble")
}
val utilKt = project.projectDir.getFileByName("util.kt") val utilKt = javaSourcesDir().resolve("baz/util.kt")
utilKt.modify { oldContent -> utilKt.modify { oldContent ->
"""$oldContent
@example.ExampleAnnotation
fun newUtilFun() {}"""
}
project.build("build") {
assertSuccessful()
assertKapt3FullyExecuted()
// todo: for kapt with stubs check compileKotlin and compileKotlinAfterJava separately
assertCompiledKotlinSourcesHandleKapt3(project.relativize(utilKt))
checkGenerated(*(annotatedElements + arrayOf("newUtilFun")))
}
}
@Test
fun testAddAnnotation() {
val project = getProject()
project.build("build") {
assertSuccessful()
}
val utilKt = project.projectDir.getFileByName("util.kt")
utilKt.modify { it.replace("fun notAnnotatedFun", "@example.ExampleAnnotation fun notAnnotatedFun") }
project.build("build") {
assertSuccessful()
assertKapt3FullyExecuted()
assertCompiledKotlinSources(project.relativize(utilKt))
checkGenerated(*(annotatedElements + arrayOf("notAnnotatedFun")))
}
}
@Test
fun testRemoveSourceFile() {
val project = getProject()
val kapt3IncDataPath = "build/tmp/kapt3/incrementalData/main"
val kapt3StubsPath = "build/tmp/kapt3/stubs/main"
project.build("build") {
assertSuccessful()
assertKapt3FullyExecuted()
assertFileExists("$kapt3IncDataPath/bar/B.class")
assertFileExists("$kapt3IncDataPath/bar/UseBKt.class")
assertFileExists("$kapt3StubsPath/bar/B.java")
assertFileExists("$kapt3StubsPath/bar/B.kapt_metadata")
assertFileExists("$kapt3StubsPath/bar/UseBKt.java")
assertFileExists("$kapt3StubsPath/bar/UseBKt.kapt_metadata")
}
with(project.projectDir) {
getFileByName("B.kt").delete()
getFileByName("useB.kt").delete()
}
project.build("build") {
assertFailed()
assertNoSuchFile("$kapt3IncDataPath/bar/B.class")
assertNoSuchFile("$kapt3IncDataPath/bar/UseBKt.class")
assertNoSuchFile("$kapt3StubsPath/bar/B.java")
assertNoSuchFile("$kapt3StubsPath/bar/B.kaptMetadata")
assertNoSuchFile("$kapt3StubsPath/bar/UseBKt.java")
assertNoSuchFile("$kapt3StubsPath/bar/UseBKt.kaptMetadata")
}
project.projectDir.getFileByName("JavaClass.java").delete()
project.build("build") {
assertSuccessful()
assertKapt3FullyExecuted()
assertCompiledKotlinSourcesHandleKapt3(project.relativize(project.projectDir.allKotlinFiles()))
val affectedElements = arrayOf("B", "funB", "valB", "useB")
checkGenerated(*(annotatedElements.toSet() - affectedElements).toTypedArray())
checkNotGenerated(*affectedElements)
}
}
@Test
fun testRemoveAllKotlinSources() {
val project = getProject()
val kapt3StubsPath = "build/tmp/kapt3/stubs/main"
project.build("build") {
assertSuccessful()
assertFileExists("$kapt3StubsPath/bar/UseBKt.java")
}
with(project.projectDir) {
resolve("src/").deleteRecursively()
resolve("src/main/java/bar").mkdirs()
resolve("src/main/java/bar/MyClass.java").writeText(
""" """
package bar; $oldContent
public class MyClass {}
""".trimIndent()
)
}
project.build("build") { @example.ExampleAnnotation
assertSuccessful() fun newUtilFun() {}
""".trimIndent()
}
// Make sure all generated stubs are removed (except for NonExistentClass). build("assemble", buildOptions = buildOptions.copy(logLevel = LogLevel.DEBUG)) {
assertEquals( assertKapt3FullyExecuted()
listOf(fileInWorkingDir("$kapt3StubsPath/error/NonExistentClass.java").canonicalPath),
fileInWorkingDir(kapt3StubsPath).walk().filter { it.extension == "java" }.map { it.canonicalPath }.toList() assertCompiledKotlinSourcesHandleKapt3(this, listOf(projectPath.relativize(utilKt)))
) checkGenerated(kaptGeneratedToPath, *(annotatedElements + arrayOf("newUtilFun")))
// Make sure all compiled kt files are cleaned up. }
assertEquals(emptyList(), fileInWorkingDir("build/classes/kotlin").walk().filter { it.extension == "class" }.toList())
} }
} }
@Test @DisplayName("Adding new annotation triggers kapt run")
fun testRemoveAnnotations() { @GradleTest
val project = getProject() fun testAddAnnotation(gradleVersion: GradleVersion) {
project.build("build") { project(PROJECT_NAME, gradleVersion) {
assertSuccessful() build("assemble")
}
val bKt = project.projectDir.getFileByName("B.kt") val utilKt = javaSourcesDir().resolve("baz/util.kt")
bKt.modify { it.replace(EXAMPLE_ANNOTATION_REGEX, "") } utilKt.modify {
val affectedElements = arrayOf("B", "funB", "valB") it.replace("fun notAnnotatedFun", "@example.ExampleAnnotation fun notAnnotatedFun")
}
project.build("build") { build("assemble", buildOptions = buildOptions.copy(logLevel = LogLevel.DEBUG)) {
assertSuccessful() assertKapt3FullyExecuted()
assertCompiledKotlinSources(listOf(projectPath.relativize(utilKt)), output)
assertKapt3FullyExecuted() checkGenerated(kaptGeneratedToPath, *(annotatedElements + arrayOf("notAnnotatedFun")))
}
val useBKt = project.projectDir.getFileByName("useB.kt")
assertCompiledKotlinSources(project.relativize(bKt, useBKt), tasks = listOf("kaptGenerateStubsKotlin"))
// java removal is detected
assertCompiledKotlinSources(
project.relativize(project.projectDir.allKotlinFiles()),
tasks = listOf("compileKotlin")
)
checkGenerated(*(annotatedElements.toSet() - affectedElements).toTypedArray())
checkNotGenerated(*affectedElements)
} }
} }
@Test @DisplayName("Kapt run is incremental after source file was removed")
fun testChangeAnnotatedPropertyType() { @GradleTest
val project = getProject() fun testRemoveSourceFile(gradleVersion: GradleVersion) {
project.build("build") { project(PROJECT_NAME, gradleVersion) {
assertSuccessful() val kapt3IncDataPath = "build/tmp/kapt3/incrementalData/main"
} val kapt3StubsPath = "build/tmp/kapt3/stubs/main"
val bKt = project.projectDir.getFileByName("B.kt") build("assemble") {
val useBKt = project.projectDir.getFileByName("useB.kt") assertKapt3FullyExecuted()
bKt.modify { it.replace("val valB = \"text\"", "val valB = 4") }
project.build("build") { assertFileInProjectExists("$kapt3IncDataPath/bar/B.class")
assertSuccessful() assertFileInProjectExists("$kapt3IncDataPath/bar/UseBKt.class")
assertKapt3FullyExecuted() assertFileInProjectExists("$kapt3StubsPath/bar/B.java")
assertCompiledKotlinSourcesHandleKapt3(project.relativize(bKt, useBKt)) assertFileInProjectExists("$kapt3StubsPath/bar/B.kapt_metadata")
checkGenerated(*annotatedElements) assertFileInProjectExists("$kapt3StubsPath/bar/UseBKt.java")
assertFileInProjectExists("$kapt3StubsPath/bar/UseBKt.kapt_metadata")
}
with(javaSourcesDir()) {
resolve("bar/B.kt").deleteIfExists()
resolve("bar/useB.kt").deleteIfExists()
}
buildAndFail("assemble") {
assertFileInProjectNotExists("$kapt3IncDataPath/bar/B.class")
assertFileInProjectNotExists("$kapt3IncDataPath/bar/UseBKt.class")
assertFileInProjectNotExists("$kapt3StubsPath/bar/B.java")
assertFileInProjectNotExists("$kapt3StubsPath/bar/B.kaptMetadata")
assertFileInProjectNotExists("$kapt3StubsPath/bar/UseBKt.java")
assertFileInProjectNotExists("$kapt3StubsPath/bar/UseBKt.kaptMetadata")
}
javaSourcesDir().resolve("foo/JavaClass.java").deleteIfExists()
build("assemble", buildOptions = buildOptions.copy(logLevel = LogLevel.DEBUG)) {
assertKapt3FullyExecuted()
assertCompiledKotlinSourcesHandleKapt3(
this,
projectPath.allKotlinFiles.map { projectPath.relativize(it) }
)
val affectedElements = arrayOf("B", "funB", "valB", "useB")
checkGenerated(kaptGeneratedToPath, *(annotatedElements.toSet() - affectedElements).toTypedArray())
checkNotGenerated(kaptGeneratedToPath, *affectedElements)
}
} }
} }
@Test @DisplayName("Incremental kapt run is correct after removing all Kotlin sources")
fun testChangeInlineDelegate() { @GradleTest
val project = getProject() fun testRemoveAllKotlinSources(gradleVersion: GradleVersion) {
project.build("build") { project(PROJECT_NAME, gradleVersion) {
assertSuccessful() build("assemble") {
} assertFileInProjectExists("$KAPT3_STUBS_PATH/bar/UseBKt.java")
}
val file = project.projectDir.getFileByName("Usage.kt") with(projectPath) {
file.modify { "$it//" } resolve("src/").deleteRecursively()
resolve("src/main/java/bar").createDirectories()
resolve("src/main/java/bar/MyClass.java").writeText(
"""
package bar;
public class MyClass {}
""".trimIndent()
)
}
project.build("build") { build("assemble") {
assertSuccessful() // Make sure all generated stubs are removed (except for NonExistentClass).
assertTasksExecuted(":kaptGenerateStubsKotlin", ":compileKotlin") assertEquals(
listOf(projectPath.resolve("$KAPT3_STUBS_PATH/error/NonExistentClass.java").toRealPath().toString()),
projectPath
.resolve(KAPT3_STUBS_PATH)
.toFile()
.walk()
.filter { it.extension == "java" }
.map { it.canonicalPath }
.toList()
)
// Make sure all compiled kt files are cleaned up.
assertEquals(
emptyList(),
projectPath
.resolve("build/classes/kotlin")
.toFile()
.walk()
.filter { it.extension == "class" }
.toList()
)
}
} }
} }
private fun CompiledProject.assertCompiledKotlinSourcesHandleKapt3( @DisplayName("On all annotations remove kapt and compile runs incremenatally")
sources: Iterable<String>, @GradleTest
weakTesting: Boolean = false fun testRemoveAnnotations(gradleVersion: GradleVersion) {
project(PROJECT_NAME, gradleVersion) {
build("assemble")
val bKt = javaSourcesDir().resolve("bar/B.kt")
bKt.modify { it.replace(EXAMPLE_ANNOTATION_REGEX, "") }
val affectedElements = arrayOf("B", "funB", "valB")
build("assemble", buildOptions = buildOptions.copy(logLevel = LogLevel.DEBUG)) {
assertKapt3FullyExecuted()
val useBKt = javaSourcesDir().resolve("bar/useB.kt")
assertCompiledKotlinSources(
listOf(projectPath.relativize(bKt), projectPath.relativize(useBKt)),
getOutputForTask("kaptGenerateStubs"),
errorMessageSuffix = " in task 'kaptGenerateStubs'"
)
// java removal is detected
assertCompiledKotlinSources(
projectPath.allKotlinFiles.map { projectPath.relativize(it) },
output
)
checkGenerated(
kaptGeneratedToPath,
*(annotatedElements.toSet() - affectedElements).toTypedArray()
)
checkNotGenerated(kaptGeneratedToPath, *affectedElements)
}
}
}
@DisplayName("Changing annotated property type is handled correctly")
@GradleTest
fun testChangeAnnotatedPropertyType(gradleVersion: GradleVersion) {
project(PROJECT_NAME, gradleVersion) {
build("assemble")
val bKt = javaSourcesDir().resolve("bar/B.kt")
val useBKt = javaSourcesDir().resolve("bar/useB.kt")
bKt.modify { it.replace("val valB = \"text\"", "val valB = 4") }
build("assemble", buildOptions = buildOptions.copy(logLevel = LogLevel.DEBUG)) {
assertKapt3FullyExecuted()
assertCompiledKotlinSourcesHandleKapt3(
this,
listOf(bKt, useBKt).map { projectPath.relativize(it) }
)
checkGenerated(kaptGeneratedToPath, *annotatedElements)
}
}
}
@DisplayName("Change in inline delegate is handled correctly")
@GradleTest
fun testChangeInlineDelegate(gradleVersion: GradleVersion) {
project(PROJECT_NAME, gradleVersion) {
build("assemble")
val file = javaSourcesDir().resolve("delegate/Usage.kt")
file.modify { "$it//" }
build("assemble") {
assertTasksExecuted(":kaptGenerateStubsKotlin", ":compileKotlin")
}
}
}
private fun TestProject.assertCompiledKotlinSourcesHandleKapt3(
buildResult: BuildResult,
sources: List<Path>
) { ) {
assertCompiledKotlinSources( assertCompiledKotlinSources(
sources, weakTesting, sources,
tasks = listOf("compileKotlin", "kaptGenerateStubsKotlin") buildResult.getOutputForTask("kaptGenerateStubsKotlin"),
errorMessageSuffix = " in task 'kaptGenerateStubsKotlin"
)
assertCompiledKotlinSources(
sources,
buildResult.getOutputForTask("compileKotlin"),
errorMessageSuffix = " in task 'compileKotlin'"
) )
} }
private fun CompiledProject.assertKapt3FullyExecuted() { private fun BuildResult.assertKapt3FullyExecuted() {
assertTasksExecuted(":kaptKotlin", ":kaptGenerateStubsKotlin") assertTasksExecuted(":kaptKotlin", ":kaptGenerateStubsKotlin")
} }
private fun CompiledProject.checkGenerated(vararg annotatedElementNames: String) { private fun TestProject.checkGenerated(
generateToPath: Path,
vararg annotatedElementNames: String
) {
getGeneratedFileNames(*annotatedElementNames).forEach { getGeneratedFileNames(*annotatedElementNames).forEach {
val file = project.projectDir.getFileByName(it) assertFileExistsInTree(generateToPath, it)
assert(file.isFile) { "$file must exist" }
} }
} }
private fun CompiledProject.checkNotGenerated(vararg annotatedElementNames: String) { private fun TestProject.checkNotGenerated(
generateToPath: Path,
vararg annotatedElementNames: String
) {
getGeneratedFileNames(*annotatedElementNames).forEach { getGeneratedFileNames(*annotatedElementNames).forEach {
val file = project.projectDir.findFileByName(it) assertFileNotExistsInTree(generateToPath, it)
assert(file == null) { "$file must not exist" }
} }
} }
private fun getGeneratedFileNames(vararg annotatedElementNames: String): Iterable<String> { @OptIn(ExperimentalStdlibApi::class)
val names = annotatedElementNames.map { it.capitalize() + "Generated" } private fun getGeneratedFileNames(vararg annotatedElementNames: String) =
return names.map { it + ".java" } annotatedElementNames
} .map { name ->
name.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
} + "Generated.java"
}
val TestProject.kaptGeneratedToPath get() = projectPath.resolve("build/generated/source/kapt")
} }