Add test to validate incremental changes in compiler plugin.

If compiler plugin was recompiled incrementally, other projects in the
same build should pick it up and regenerate sources.

^KT-38570 Fixed
This commit is contained in:
Yahor Berdnikau
2021-11-25 12:45:00 +01:00
parent 88f41d006a
commit d253f9ba0e
9 changed files with 237 additions and 0 deletions
@@ -0,0 +1,67 @@
/*
* Copyright 2010-2021 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
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.testbase.*
import org.junit.jupiter.api.DisplayName
import kotlin.io.path.exists
@DisplayName("Compiler plugin incremental compilation")
@OtherGradlePluginTests
class CompilerPluginsIncrementalIT : KGPBaseTest() {
override val defaultBuildOptions: BuildOptions
get() = super.defaultBuildOptions.copy(
incremental = true
)
@DisplayName("KT-38570: After changing compiler plugin code, next incremental build picks it up")
@GradleTest
internal fun afterChangeInPluginBuildDoesIncrementalProcessing(gradleVersion: GradleVersion) {
project("incrementalChangeInPlugin".prefix, gradleVersion) {
build("assemble")
val genDir = subProject("library").projectPath.resolve("build/sample-dir/plugin/test/gen")
assert(genDir.exists()) { "$genDir does not exists!" }
val generatedFiles = genDir.toFile()
.walkTopDown()
.asSequence()
.filterNot { it.isDirectory }
.associate {
it.absolutePath to it.readBytes()
}
subProject("plugin")
.kotlinSourcesDir()
.resolve("test/compiler/plugin/TestComponentRegistrar.kt")
.modify {
it.replace("world.", "something else.")
}
build("assemble") {
assert(genDir.exists()) { "$genDir does not exists!" }
}
genDir.toFile()
.walkTopDown()
.asSequence()
.filterNot { it.isDirectory }
.forEach {
assert(!it.readBytes().contentEquals(generatedFiles[it.absolutePath])) {
"""
File ${it.absolutePath} content is equals to previous one!
New content:
${it.readText()}
""".trimIndent()
}
}
}
}
private val String.prefix get() = "compilerPlugins/$this"
}
@@ -0,0 +1,14 @@
plugins {
id 'java'
id 'org.jetbrains.kotlin.jvm'
}
sourceCompatibility = 1.8
allprojects {
repositories {
mavenLocal()
mavenCentral()
google()
}
}
@@ -0,0 +1,7 @@
plugins {
id "org.jetbrains.kotlin.jvm"
}
dependencies {
kotlinCompilerPluginClasspath project(':plugin')
}
@@ -0,0 +1,8 @@
/*
* Copyright 2010-2021 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 library
class SomeClass
@@ -0,0 +1,8 @@
/*
* Copyright 2010-2021 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 library
interface SomeInterface
@@ -0,0 +1,13 @@
plugins {
id 'org.jetbrains.kotlin.jvm'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-gradle-plugin-api"
compileOnly "org.jetbrains.kotlin:kotlin-compiler-embeddable"
}
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
@@ -0,0 +1,112 @@
/*
* Copyright 2010-2021 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 test.compiler.plugin
import org.jetbrains.kotlin.analyzer.AnalysisResult
import org.jetbrains.kotlin.analyzer.AnalysisResult.RetryWithAdditionalRoots
import org.jetbrains.kotlin.cli.common.config.KotlinSourceRoot
import org.jetbrains.kotlin.cli.common.config.kotlinSourceRoots
import org.jetbrains.kotlin.com.intellij.mock.MockProject
import org.jetbrains.kotlin.com.intellij.openapi.project.Project
import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.descriptors.findClassAcrossModuleDependencies
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.resolve.BindingTrace
import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension
import java.io.File
class TestComponentRegistrar : ComponentRegistrar {
override fun registerProjectComponents(
project: MockProject,
configuration: CompilerConfiguration
) {
val kotlinSourceRoots = configuration.kotlinSourceRoots.ifEmpty { return }
val sourceGenFolder = createSourceGenFolder(kotlinSourceRoots)
var counter = 0
AnalysisHandlerExtension.registerExtension(project, object : AnalysisHandlerExtension {
private var didRecompile = false
override fun analysisCompleted(
project: Project,
module: ModuleDescriptor,
bindingTrace: BindingTrace,
files: Collection<KtFile>
): AnalysisResult? {
if (didRecompile) return null
didRecompile = true
val classes = files
.flatMap { ktFile ->
val packageName = ktFile.packageFqName.asString()
ktFile.findChildrenByClass(KtClassOrObject::class.java)
.map { packageName + "." + it.nameAsSafeName.asString() }
}
.mapNotNull {
module.findClassAcrossModuleDependencies(ClassId.topLevel(FqName(it)))
}
// Nothing to do.
if (classes.isEmpty()) return null
classes.forEach { classDescriptor ->
val className = classDescriptor.name.asString()
val directory = File(sourceGenFolder, "plugin/test/gen")
val file = File(directory, "$className.kt")
.also {
check(it.parentFile.exists() || it.parentFile.mkdirs()) {
"Could not generate package directory: $this"
}
}
file.writeText(
"""
package plugin.test.gen
val hello${counter++} = "world."
""".trimIndent()
)
}
// This restarts the analysis phase and will include our file.
return RetryWithAdditionalRoots(
bindingTrace.bindingContext, module, emptyList(), listOf(sourceGenFolder), addToEnvironment = true
)
}
})
}
private fun createSourceGenFolder(kotlinSourceRoots: List<KotlinSourceRoot>): File {
fun sampleDir(parent: File): File = File(parent, "sample-dir").also {
check(it.exists() || it.mkdirs()) {
"Could not create source generation directory: $it"
}
}
val oneSourceFile = kotlinSourceRoots.first().path.let { File(it) }
val parentSequence = generateSequence(oneSourceFile) { it.parentFile }
// Try to find the src dir.
parentSequence.firstOrNull { it.name == "src" }
?.let { return sampleDir(File(it.parentFile, "build")) }
// If the src dir is not part of the input (incremental build), look for the build dir
// directly.
parentSequence.firstOrNull { it.name == "build" }
?.let { return sampleDir(it) }
throw IllegalStateException(
"Could not create source generation directory: $oneSourceFile"
)
}
}
@@ -0,0 +1,6 @@
#
# Copyright 2010-2021 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.
#
test.compiler.plugin.TestComponentRegistrar