KT-45777: Move classpath diffing to incremental Kotlin compiler (1/2)

This commit only changes the files' paths, the next commit will update
the files' contents.

See the next commit for more context.
This commit is contained in:
Hung Nguyen
2021-11-04 20:22:21 +00:00
committed by teamcityserver
parent c46e9943cc
commit dfaf195e1d
102 changed files with 24 additions and 24 deletions
@@ -0,0 +1,337 @@
/*
* 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.incremental.classpathDiff
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.Util.compileAll
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.Util.snapshot
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.Util.snapshotAll
import org.jetbrains.kotlin.incremental.ClasspathChanges
import org.jetbrains.kotlin.incremental.LookupSymbol
import org.jetbrains.kotlin.resolve.sam.SAM_LOOKUP_NAME
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.io.File
import kotlin.test.fail
abstract class ClasspathChangesComputerTest : ClasspathSnapshotTestCommon() {
// TODO Add more test cases:
// - private/non-private fields
// - inline functions
// - changing supertype by adding somethings that changes/does not change the supertype ABI
// - adding an annotation
@Test
abstract fun testAbiChange_changePublicMethodSignature()
@Test
abstract fun testNonAbiChange_changeMethodImplementation()
@Test
abstract fun testVariousAbiChanges()
@Test
abstract fun testImpactAnalysis()
}
class KotlinOnlyClasspathChangesComputerTest : ClasspathChangesComputerTest() {
@Test
override fun testAbiChange_changePublicMethodSignature() {
val sourceFile = SimpleKotlinClass(tmpDir)
val previousSnapshot = sourceFile.compileAndSnapshot()
val currentSnapshot = sourceFile.changePublicMethodSignature().compileAndSnapshot()
val changes = ClasspathChangesComputer.computeClassChanges(listOf(currentSnapshot), listOf(previousSnapshot)).normalize()
Changes(
lookupSymbols = setOf(
LookupSymbol(name = "publicFunction", scope = "com.example.SimpleKotlinClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.SimpleKotlinClass")
),
fqNames = setOf("com.example.SimpleKotlinClass")
).assertEquals(changes)
}
@Test
override fun testNonAbiChange_changeMethodImplementation() {
val sourceFile = SimpleKotlinClass(tmpDir)
val previousSnapshot = sourceFile.compileAndSnapshot()
val currentSnapshot = sourceFile.changeMethodImplementation().compileAndSnapshot()
val changes = ClasspathChangesComputer.computeClassChanges(listOf(currentSnapshot), listOf(previousSnapshot)).normalize()
Changes(emptySet(), emptySet()).assertEquals(changes)
}
@Test
override fun testVariousAbiChanges() {
val classpathSourceDir = File(testDataDir, "../ClasspathChangesComputerTest/testVariousAbiChanges/src/kotlin").canonicalFile
val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir)
val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir)
val changes = ClasspathChangesComputer.compute(currentSnapshot, previousSnapshot).normalize()
Changes(
lookupSymbols = setOf(
// ModifiedClassUnchangedMembers
LookupSymbol(name = "ModifiedClassUnchangedMembers", scope = "com.example"),
// ModifiedClassChangedMembers
LookupSymbol(name = "modifiedProperty", scope = "com.example.ModifiedClassChangedMembers"),
LookupSymbol(name = "addedProperty", scope = "com.example.ModifiedClassChangedMembers"),
LookupSymbol(name = "removedProperty", scope = "com.example.ModifiedClassChangedMembers"),
LookupSymbol(name = "modifiedFunction", scope = "com.example.ModifiedClassChangedMembers"),
LookupSymbol(name = "addedFunction", scope = "com.example.ModifiedClassChangedMembers"),
LookupSymbol(name = "removedFunction", scope = "com.example.ModifiedClassChangedMembers"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.ModifiedClassChangedMembers"),
// AddedClass
LookupSymbol(name = "AddedClass", scope = "com.example"),
// RemovedClass
LookupSymbol(name = "RemovedClass", scope = "com.example"),
// Top-level properties and functions
LookupSymbol(name = "modifiedTopLevelProperty", scope = "com.example"),
LookupSymbol(name = "addedTopLevelProperty", scope = "com.example"),
LookupSymbol(name = "removedTopLevelProperty", scope = "com.example"),
LookupSymbol(name = "movedTopLevelProperty", scope = "com.example"),
LookupSymbol(name = "modifiedTopLevelFunction", scope = "com.example"),
LookupSymbol(name = "addedTopLevelFunction", scope = "com.example"),
LookupSymbol(name = "removedTopLevelFunction", scope = "com.example"),
LookupSymbol(name = "movedTopLevelFunction", scope = "com.example"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example")
),
fqNames = setOf(
"com.example.ModifiedClassUnchangedMembers",
"com.example.ModifiedClassChangedMembers",
"com.example.AddedClass",
"com.example.RemovedClass",
"com.example"
)
).assertEquals(changes)
}
@Test
override fun testImpactAnalysis() {
val classpathSourceDir =
File(testDataDir, "../ClasspathChangesComputerTest/testImpactAnalysis_KotlinOrJava/src/kotlin").canonicalFile
val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir)
val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir)
val changes = ClasspathChangesComputer.compute(currentSnapshot, previousSnapshot).normalize()
Changes(
lookupSymbols = setOf(
LookupSymbol(name = "changedProperty", scope = "com.example.ChangedSuperClass"),
LookupSymbol(name = "changedProperty", scope = "com.example.SubClass"),
LookupSymbol(name = "changedProperty", scope = "com.example.SubSubClass"),
LookupSymbol(name = "changedFunction", scope = "com.example.ChangedSuperClass"),
LookupSymbol(name = "changedFunction", scope = "com.example.SubClass"),
LookupSymbol(name = "changedFunction", scope = "com.example.SubSubClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.ChangedSuperClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.SubClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.SubSubClass")
),
fqNames = setOf(
"com.example.ChangedSuperClass",
"com.example.SubClass",
"com.example.SubSubClass"
)
).assertEquals(changes)
}
}
@RunWith(Parameterized::class)
class JavaOnlyClasspathChangesComputerTest(private val protoBased: Boolean) : ClasspathChangesComputerTest() {
companion object {
@Parameterized.Parameters(name = "protoBased={0}")
@JvmStatic
fun parameters() = listOf(true, false)
}
@Test
override fun testAbiChange_changePublicMethodSignature() {
val sourceFile = SimpleJavaClass(tmpDir)
val previousSnapshot = sourceFile.compile().snapshot(protoBased)
val currentSnapshot = sourceFile.changePublicMethodSignature().compile().snapshot(protoBased)
val changes = ClasspathChangesComputer.computeClassChanges(listOf(currentSnapshot), listOf(previousSnapshot)).normalize()
Changes(
lookupSymbols = setOf(
LookupSymbol(name = "publicMethod", scope = "com.example.SimpleJavaClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.SimpleJavaClass")
),
fqNames = setOf("com.example.SimpleJavaClass")
).assertEquals(changes)
}
@Test
override fun testNonAbiChange_changeMethodImplementation() {
val sourceFile = SimpleJavaClass(tmpDir)
val previousSnapshot = sourceFile.compile().snapshot(protoBased)
val currentSnapshot = sourceFile.changeMethodImplementation().compile().snapshot(protoBased)
val changes = ClasspathChangesComputer.computeClassChanges(listOf(currentSnapshot), listOf(previousSnapshot)).normalize()
Changes(emptySet(), emptySet()).assertEquals(changes)
}
@Test
override fun testVariousAbiChanges() {
val classpathSourceDir = File(testDataDir, "../ClasspathChangesComputerTest/testVariousAbiChanges/src/java").canonicalFile
val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir, protoBased)
val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir, protoBased)
val changes = ClasspathChangesComputer.compute(currentSnapshot, previousSnapshot).normalize()
Changes(
lookupSymbols = setOf(
// ModifiedClassUnchangedMembers
LookupSymbol(name = "ModifiedClassUnchangedMembers", scope = "com.example"),
// ModifiedClassChangedMembers
LookupSymbol(name = "modifiedField", scope = "com.example.ModifiedClassChangedMembers"),
LookupSymbol(name = "removedField", scope = "com.example.ModifiedClassChangedMembers"),
LookupSymbol(name = "modifiedMethod", scope = "com.example.ModifiedClassChangedMembers"),
LookupSymbol(name = "removedMethod", scope = "com.example.ModifiedClassChangedMembers"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.ModifiedClassChangedMembers"),
// RemovedClass
LookupSymbol(name = "RemovedClass", scope = "com.example")
) + if (protoBased) {
setOf(
// ModifiedClassChangedMembers
LookupSymbol(name = "addedField", scope = "com.example.ModifiedClassChangedMembers"),
LookupSymbol(name = "addedMethod", scope = "com.example.ModifiedClassChangedMembers"),
// AddedClass
LookupSymbol(name = "AddedClass", scope = "com.example"),
)
} else {
emptySet()
},
fqNames = setOf(
"com.example.ModifiedClassUnchangedMembers",
"com.example.ModifiedClassChangedMembers",
"com.example.RemovedClass"
) + if (protoBased) {
setOf("com.example.AddedClass")
} else {
emptySet()
}
).assertEquals(changes)
}
@Test
override fun testImpactAnalysis() {
val classpathSourceDir = File(testDataDir, "../ClasspathChangesComputerTest/testImpactAnalysis_KotlinOrJava/src/java").canonicalFile
val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir, protoBased)
val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir, protoBased)
val changes = ClasspathChangesComputer.compute(currentSnapshot, previousSnapshot).normalize()
Changes(
lookupSymbols = setOf(
LookupSymbol(name = "changedField", scope = "com.example.ChangedSuperClass"),
LookupSymbol(name = "changedField", scope = "com.example.SubClass"),
LookupSymbol(name = "changedField", scope = "com.example.SubSubClass"),
LookupSymbol(name = "changedMethod", scope = "com.example.ChangedSuperClass"),
LookupSymbol(name = "changedMethod", scope = "com.example.SubClass"),
LookupSymbol(name = "changedMethod", scope = "com.example.SubSubClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.ChangedSuperClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.SubClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.SubSubClass")
),
fqNames = setOf(
"com.example.ChangedSuperClass",
"com.example.SubClass",
"com.example.SubSubClass"
)
).assertEquals(changes)
}
}
class KotlinAndJavaClasspathChangesComputerTest : ClasspathSnapshotTestCommon() {
@Test
fun testImpactAnalysis() {
val classpathSourceDir = File(testDataDir, "../ClasspathChangesComputerTest/testImpactAnalysis_KotlinAndJava/src").canonicalFile
val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir)
val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir)
val changes = ClasspathChangesComputer.compute(currentSnapshot, previousSnapshot).normalize()
Changes(
lookupSymbols = setOf(
LookupSymbol(name = "changedProperty", scope = "com.example.ChangedKotlinSuperClass"),
LookupSymbol(name = "changedProperty", scope = "com.example.KotlinSubClassOfKotlinSuperClass"),
LookupSymbol(name = "changedProperty", scope = "com.example.JavaSubClassOfKotlinSuperClass"),
LookupSymbol(name = "changedFunction", scope = "com.example.ChangedKotlinSuperClass"),
LookupSymbol(name = "changedFunction", scope = "com.example.KotlinSubClassOfKotlinSuperClass"),
LookupSymbol(name = "changedFunction", scope = "com.example.JavaSubClassOfKotlinSuperClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.ChangedKotlinSuperClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.KotlinSubClassOfKotlinSuperClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.JavaSubClassOfKotlinSuperClass"),
LookupSymbol(name = "changedField", scope = "com.example.ChangedJavaSuperClass"),
LookupSymbol(name = "changedField", scope = "com.example.KotlinSubClassOfJavaSuperClass"),
LookupSymbol(name = "changedField", scope = "com.example.JavaSubClassOfJavaSuperClass"),
LookupSymbol(name = "changedMethod", scope = "com.example.ChangedJavaSuperClass"),
LookupSymbol(name = "changedMethod", scope = "com.example.KotlinSubClassOfJavaSuperClass"),
LookupSymbol(name = "changedMethod", scope = "com.example.JavaSubClassOfJavaSuperClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.ChangedJavaSuperClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.KotlinSubClassOfJavaSuperClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.JavaSubClassOfJavaSuperClass")
),
fqNames = setOf(
"com.example.ChangedKotlinSuperClass",
"com.example.KotlinSubClassOfKotlinSuperClass",
"com.example.JavaSubClassOfKotlinSuperClass",
"com.example.ChangedJavaSuperClass",
"com.example.KotlinSubClassOfJavaSuperClass",
"com.example.JavaSubClassOfJavaSuperClass"
)
).assertEquals(changes)
}
}
private fun snapshotClasspath(classpathSourceDir: File, tmpDir: TemporaryFolder, protoBased: Boolean = true): ClasspathSnapshot {
val classpath = mutableListOf<File>()
val classpathEntrySnapshots = classpathSourceDir.listFiles()!!.sortedBy { it.name }.map { classpathEntrySourceDir ->
val classFiles = compileAll(classpathEntrySourceDir, classpath, tmpDir)
classpath.addAll(listOfNotNull(classFiles.firstOrNull()?.classRoot))
val relativePaths = classFiles.map { it.unixStyleRelativePath }
val classSnapshots = classFiles.snapshotAll(protoBased)
ClasspathEntrySnapshot(
classSnapshots = relativePaths.zip(classSnapshots).toMap(LinkedHashMap())
)
}
return ClasspathSnapshot(classpathEntrySnapshots)
}
/** Adapted version of [ClasspathChanges.Available] for readability in this test. */
private data class Changes(val lookupSymbols: Set<LookupSymbol>, val fqNames: Set<String>)
private fun ClasspathChanges.normalize(): Changes {
this as ClasspathChanges.Available
return Changes(lookupSymbols, fqNames.map { it.asString() }.toSet())
}
private fun Changes.assertEquals(actual: Changes) {
listOfNotNull(
compare(expected = this.lookupSymbols, actual = actual.lookupSymbols),
compare(expected = this.fqNames, actual = actual.fqNames)
).also {
if (it.isNotEmpty()) {
fail(it.joinToString("\n"))
}
}
}
private fun compare(expected: Set<*>, actual: Set<*>): String? {
return if (expected != actual) {
"Two sets differ:\n" +
"Elements in expected set but not in actual set: ${expected - actual}\n" +
"Elements in actual set but not in expected set: ${actual - expected}"
} else null
}
@@ -0,0 +1,44 @@
/*
* 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.incremental.classpathDiff
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.Util.readBytes
import org.junit.Assert.assertEquals
import org.junit.Test
abstract class ClasspathSnapshotSerializerTest : ClasspathSnapshotTestCommon() {
protected abstract val testSourceFile: ChangeableTestSourceFile
@Test
open fun `test ClassSnapshotDataSerializer`() {
val originalSnapshot = testSourceFile.compileAndSnapshot()
val serializedSnapshot = ClassSnapshotDataSerializer.toByteArray(originalSnapshot)
val deserializedSnapshot = ClassSnapshotDataSerializer.fromByteArray(serializedSnapshot)
assertEquals(originalSnapshot.toGson(), deserializedSnapshot.toGson())
}
}
class KotlinClassesClasspathSnapshotSerializerTest : ClasspathSnapshotSerializerTest() {
override val testSourceFile = SimpleKotlinClass(tmpDir)
}
class JavaClassesClasspathSnapshotSerializerTest : ClasspathSnapshotSerializerTest() {
override val testSourceFile = SimpleJavaClass(tmpDir)
@Test
override fun `test ClassSnapshotDataSerializer`() {
val originalSnapshot = testSourceFile.compile().let {
ClassSnapshotter.snapshot(listOf(ClassFileWithContents(it, it.readBytes())), includeDebugInfoInSnapshot = false)
}.single()
val serializedSnapshot = ClassSnapshotDataSerializer.toByteArray(originalSnapshot)
val deserializedSnapshot = ClassSnapshotDataSerializer.fromByteArray(serializedSnapshot)
assertEquals(originalSnapshot.toGson(), deserializedSnapshot.toGson())
}
}
@@ -0,0 +1,256 @@
/*
* 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.incremental.classpathDiff
import com.google.gson.GsonBuilder
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.SourceFile.JavaSourceFile
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.SourceFile.KotlinSourceFile
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.Util.compile
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.Util.compileAll
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.Util.snapshot
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.Util.snapshotAll
import org.jetbrains.kotlin.test.KotlinTestUtils
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import java.io.File
abstract class ClasspathSnapshotTestCommon {
companion object {
val testDataDir =
File("libraries/tools/kotlin-gradle-plugin/src/testData/org/jetbrains/kotlin/gradle/incremental/ClasspathSnapshotTestCommon")
}
@get:Rule
val tmpDir = TemporaryFolder()
// Use Gson to compare objects
private val gson by lazy { GsonBuilder().setPrettyPrinting().create() }
protected fun Any.toGson(): String = gson.toJson(this)
sealed class SourceFile(val baseDir: File, relativePath: String) {
val unixStyleRelativePath: String
init {
unixStyleRelativePath = relativePath.replace('\\', '/')
}
fun asFile() = File(baseDir, unixStyleRelativePath)
class KotlinSourceFile(baseDir: File, relativePath: String, val preCompiledClassFiles: List<ClassFile>) :
SourceFile(baseDir, relativePath) {
constructor(baseDir: File, relativePath: String, preCompiledClassFile: ClassFile) :
this(baseDir, relativePath, listOf(preCompiledClassFile))
}
class JavaSourceFile(baseDir: File, relativePath: String) : SourceFile(baseDir, relativePath)
}
/** Same as [SourceFile] but with a [TemporaryFolder] to store the results of operations on the [SourceFile]. */
open class TestSourceFile(val sourceFile: SourceFile, private val tmpDir: TemporaryFolder) {
fun replace(oldValue: String, newValue: String, preCompiledKotlinClassFile: ClassFile? = null): TestSourceFile {
val fileContents = sourceFile.asFile().readText()
check(fileContents.contains(oldValue)) { "String '$oldValue' not found in file '${sourceFile.asFile().path}'" }
val newSourceFile = when (sourceFile) {
is KotlinSourceFile -> KotlinSourceFile(tmpDir.newFolder(), sourceFile.unixStyleRelativePath, preCompiledKotlinClassFile!!)
is JavaSourceFile -> JavaSourceFile(tmpDir.newFolder(), sourceFile.unixStyleRelativePath)
}
newSourceFile.asFile().parentFile.mkdirs()
newSourceFile.asFile().writeText(fileContents.replace(oldValue, newValue))
return TestSourceFile(newSourceFile, tmpDir)
}
/**
* Compiles this source file and returns a single generated .class file, or fails if zero or more than one .class file was
* generated.
*
* Alternatively, the caller can call [compileAll] to get all generated .class files.
*/
fun compile(): ClassFile = compileAll().single()
/** Compiles this source file and returns all generated .class files. */
@Suppress("MemberVisibilityCanBePrivate")
fun compileAll(): List<ClassFile> = sourceFile.compile(tmpDir)
/**
* Compiles this source file and returns the snapshot of a single generated .class file, or fails if zero or more than one .class
* file was generated.
*
* Alternatively, the caller can call [compileAndSnapshotAll] to get the snapshots of all generated .class files.
*/
fun compileAndSnapshot() = compile().snapshot()
/** Compiles this source file and returns the snapshots of all generated .class files. */
fun compileAndSnapshotAll(): List<ClassSnapshot> = compileAll().snapshotAll()
}
object Util {
/** Compiles the source files in the given directory and returns all generated .class files. */
fun compileAll(srcDir: File, classpath: List<File>, tmpDir: TemporaryFolder): List<ClassFile> {
val kotlinClasses = compileKotlin(srcDir, classpath, tmpDir)
val javaClasspath = classpath + listOfNotNull(kotlinClasses.firstOrNull()?.classRoot)
val javaClasses = compileJava(srcDir, javaClasspath, tmpDir)
return kotlinClasses + javaClasses
}
private fun compileKotlin(srcDir: File, classpath: List<File>, tmpDir: TemporaryFolder): List<ClassFile> {
val preCompiledKotlinClassesDir = srcDir.path.let {
File(it.substringBeforeLast("src") + "classes" + it.substringAfterLast("src"))
}
preCompileKotlinFilesIfNecessary(srcDir, preCompiledKotlinClassesDir, classpath, tmpDir)
return getClassFilesInDir(preCompiledKotlinClassesDir)
}
private val preCompiledKotlinClassesDirs = mutableSetOf<File>()
/**
* If <kotlin-repo>/dist/kotlinc/lib/kotlin-compiler.jar is available (e.g., by running ./gradlew dist), we will be able to call the
* Kotlin compiler to generate classes. However, kotlin-compiler.jar is currently not available in CI builds, so we need to
* pre-compile the classes locally and put them in the test data to check in.
*/
@Synchronized // To safe-guard shared variable preCompiledKotlinClassesDirs
private fun preCompileKotlinFilesIfNecessary(
srcDir: File,
preCompiledKotlinClassesDir: File,
classpath: List<File>,
tmpDir: TemporaryFolder,
preCompile: Boolean = false // Set to `true` to pre-compile Kotlin class files locally (DO NOT check in with preCompile = true)
) {
if (preCompile) {
if (!preCompiledKotlinClassesDirs.contains(preCompiledKotlinClassesDir)) {
val classFiles = doCompileKotlin(srcDir, classpath, tmpDir)
preCompiledKotlinClassesDir.deleteRecursively()
for (classFile in classFiles) {
File(preCompiledKotlinClassesDir, classFile.unixStyleRelativePath).apply {
parentFile.mkdirs()
classFile.asFile().copyTo(this)
}
}
preCompiledKotlinClassesDirs.add(preCompiledKotlinClassesDir)
}
}
}
fun SourceFile.compile(tmpDir: TemporaryFolder): List<ClassFile> {
return if (this is KotlinSourceFile) {
preCompiledClassFiles.forEach {
preCompileKotlinFilesIfNecessary(baseDir, it.classRoot, classpath = emptyList(), tmpDir)
}
preCompiledClassFiles
} else {
val srcDir = tmpDir.newFolder()
asFile().copyTo(File(srcDir, unixStyleRelativePath))
compileAll(srcDir, classpath = emptyList(), tmpDir)
}
}
private fun doCompileKotlin(srcDir: File, classpath: List<File>, tmpDir: TemporaryFolder): List<ClassFile> {
if (srcDir.walk().none { it.path.endsWith(".kt") }) {
return emptyList()
}
val classesDir = tmpDir.newFolder()
org.jetbrains.kotlin.test.MockLibraryUtil.compileKotlin(
srcDir.path,
classesDir,
extraClasspath = classpath.map { it.path }.toTypedArray()
)
return getClassFilesInDir(classesDir)
}
private fun compileJava(srcDir: File, classpath: List<File>, tmpDir: TemporaryFolder): List<ClassFile> {
val javaFiles = srcDir.walk().toList().filter { it.path.endsWith(".java") }
if (javaFiles.isEmpty()) {
return emptyList()
}
val classesDir = tmpDir.newFolder()
val classpathOption =
if (classpath.isNotEmpty()) listOf("-classpath", classpath.joinToString(File.pathSeparator)) else emptyList()
KotlinTestUtils.compileJavaFiles(javaFiles, listOf("-d", classesDir.path) + classpathOption)
return getClassFilesInDir(classesDir)
}
private fun getClassFilesInDir(classesDir: File): List<ClassFile> {
return classesDir.walk().toList()
.filter { it.isFile && it.path.endsWith(".class") }
.map { ClassFile(classesDir, it.toRelativeString(classesDir)) }
.sortedBy { it.unixStyleRelativePath.substringBefore(".class") }
}
// `ClassFile`s in production code could be in a jar, but the `ClassFile`s in tests are currently in a directory, so converting it
// to a File is possible.
@Suppress("MemberVisibilityCanBePrivate")
fun ClassFile.asFile() = File(classRoot, unixStyleRelativePath)
@Suppress("MemberVisibilityCanBePrivate")
fun ClassFile.readBytes() = asFile().readBytes()
fun ClassFile.snapshot(protoBased: Boolean? = null): ClassSnapshot = listOf(this).snapshotAll(protoBased).single()
fun List<ClassFile>.snapshotAll(protoBased: Boolean? = null): List<ClassSnapshot> {
val classFilesWithContents = this.map { ClassFileWithContents(it, it.readBytes()) }
return ClassSnapshotter.snapshot(classFilesWithContents, protoBased, includeDebugInfoInSnapshot = true)
}
}
abstract class ChangeableTestSourceFile(sourceFile: SourceFile, tmpDir: TemporaryFolder) : TestSourceFile(sourceFile, tmpDir) {
abstract fun changePublicMethodSignature(): TestSourceFile
abstract fun changeMethodImplementation(): TestSourceFile
}
class SimpleKotlinClass(tmpDir: TemporaryFolder) : ChangeableTestSourceFile(
KotlinSourceFile(
baseDir = File(testDataDir, "src/kotlin"), relativePath = "com/example/SimpleKotlinClass.kt",
preCompiledClassFile = ClassFile(File(testDataDir, "classes/kotlin/original"), "com/example/SimpleKotlinClass.class")
), tmpDir
) {
override fun changePublicMethodSignature() = replace(
"publicFunction()", "publicFunction(newParam: Int)",
preCompiledKotlinClassFile = ClassFile(
File(testDataDir, "classes/kotlin/changedPublicMethodSignature"), "com/example/SimpleKotlinClass.class"
)
)
override fun changeMethodImplementation() = replace(
"I'm in a public function", "This function's implementation has changed!",
preCompiledKotlinClassFile = ClassFile(
File(testDataDir, "classes/kotlin/changedMethodImplementation"), "com/example/SimpleKotlinClass.class"
)
)
}
class SimpleJavaClass(tmpDir: TemporaryFolder) : ChangeableTestSourceFile(
JavaSourceFile(File(testDataDir, "src/java"), "com/example/SimpleJavaClass.java"), tmpDir
) {
override fun changePublicMethodSignature() = replace("publicMethod()", "publicMethod(int newParam)")
override fun changeMethodImplementation() = replace("I'm in a public method", "This method's implementation has changed!")
}
class JavaClassWithNestedClasses(tmpDir: TemporaryFolder) : ChangeableTestSourceFile(
JavaSourceFile(File(testDataDir, "src/java"), "com/example/JavaClassWithNestedClasses.java"), tmpDir
) {
/** The source file contains multiple classes, select the one that we want to test. */
val nestedClassToTest = "com/example/JavaClassWithNestedClasses\$InnerClass"
override fun changePublicMethodSignature() = replace("publicMethod()", "publicMethod(int newParam)")
override fun changeMethodImplementation() = replace("I'm in a public method", "This method's implementation has changed!")
}
}
@@ -0,0 +1,145 @@
/*
* 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.incremental.classpathDiff
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.Util.snapshot
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.io.File
abstract class ClasspathSnapshotterTest : ClasspathSnapshotTestCommon() {
protected abstract val testSourceFile: ChangeableTestSourceFile
private lateinit var testClassSnapshot: ClassSnapshot
@Before
fun setUp() {
testClassSnapshot = testSourceFile.compileAndSnapshot()
}
@Test
fun `test ClassSnapshotter's result against expected snapshot`() {
assertEquals(getExpectedSnapshotFile().readText(), testClassSnapshot.toGson())
}
private fun getExpectedSnapshotFile() = testSourceFile.sourceFile.asFile().path.let {
File(it.substringBeforeLast("src") + "expected-snapshot" + it.substringAfterLast("src").substringBeforeLast('.') + ".json")
}
@Test
fun `test ClassSnapshotter extracts ABI info from a class`() {
// Change public method signature
val updatedSnapshot = testSourceFile.changePublicMethodSignature().compileAndSnapshot()
// The snapshot must change
assertNotEquals(testClassSnapshot.toGson(), updatedSnapshot.toGson())
}
@Test
fun `test ClassSnapshotter does not extract non-ABI info from a class`() {
// Change method implementation
val updatedSnapshot = testSourceFile.changeMethodImplementation().compileAndSnapshot()
// The snapshot must not change
assertEquals(testClassSnapshot.toGson(), updatedSnapshot.toGson())
}
}
class KotlinClassesClasspathSnapshotterTest : ClasspathSnapshotterTest() {
override val testSourceFile = SimpleKotlinClass(tmpDir)
}
@RunWith(Parameterized::class)
class JavaClassesClasspathSnapshotterTest(private val protoBased: Boolean) : ClasspathSnapshotTestCommon() {
companion object {
@Parameterized.Parameters(name = "protoBased={0}")
@JvmStatic
fun parameters() = listOf(true, false)
}
private val testSourceFile = SimpleJavaClass(tmpDir)
private lateinit var testClassSnapshot: ClassSnapshot
@Before
fun setUp() {
testClassSnapshot = testSourceFile.compile().snapshot(protoBased)
}
@Test
fun `test ClassSnapshotter's result against expected snapshot`() {
assertEquals(getExpectedSnapshotFile().readText(), testClassSnapshot.toGson())
}
private fun getExpectedSnapshotFile() = testSourceFile.sourceFile.asFile().path.let {
File(
it.substringBeforeLast("src") + "expected-snapshot" + it.substringAfterLast("src")
.substringBeforeLast('.') + "-protoBased=$protoBased.json"
)
}
@Test
fun `test ClassSnapshotter extracts ABI info from a class`() {
// Change public method signature
val updatedSnapshot = testSourceFile.changePublicMethodSignature().compile().snapshot(protoBased)
// The snapshot must change
assertNotEquals(testClassSnapshot.toGson(), updatedSnapshot.toGson())
}
@Test
fun `test ClassSnapshotter does not extract non-ABI info from a class`() {
// Change method implementation
val updatedSnapshot = testSourceFile.changeMethodImplementation().compile().snapshot(protoBased)
// The snapshot must not change
assertEquals(testClassSnapshot.toGson(), updatedSnapshot.toGson())
}
}
class JavaClassWithNestedClassesClasspathSnapshotterTest : ClasspathSnapshotTestCommon() {
private val testSourceFile = JavaClassWithNestedClasses(tmpDir)
private lateinit var testClassSnapshot: ClassSnapshot
@Before
fun setUp() {
testClassSnapshot = testSourceFile.compileAndSnapshotNestedClass()
}
private fun TestSourceFile.compileAndSnapshotNestedClass(): ClassSnapshot {
return compileAndSnapshotAll().single {
if (it is RegularJavaClassSnapshot) {
it.classAbiExcludingMembers.name == testSourceFile.nestedClassToTest
} else false
}
}
@Test
fun `test ClassSnapshotter's result against expected snapshot`() {
val expectedSnapshotFile = File("${testDataDir.path}/expected-snapshot/java/${testSourceFile.nestedClassToTest}.json")
assertEquals(expectedSnapshotFile.readText(), testClassSnapshot.toGson())
}
@Test
fun `test ClassSnapshotter extracts ABI info from a class`() {
val updatedSnapshot = testSourceFile.changePublicMethodSignature().compileAndSnapshotNestedClass()
assertNotEquals(testClassSnapshot.toGson(), updatedSnapshot.toGson())
}
@Test
fun `test ClassSnapshotter does not extract non-ABI info from a class`() {
val updatedSnapshot = testSourceFile.changeMethodImplementation().compileAndSnapshotNestedClass()
assertEquals(testClassSnapshot.toGson(), updatedSnapshot.toGson())
}
}