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:
committed by
teamcityserver
parent
c46e9943cc
commit
dfaf195e1d
+337
@@ -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
|
||||
}
|
||||
+44
@@ -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())
|
||||
}
|
||||
}
|
||||
+256
@@ -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!")
|
||||
}
|
||||
}
|
||||
+145
@@ -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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user