KT-45777: Allow 2 levels of granularity when tracking changes
1. CLASS_LEVEL: allows tracking whether a .class file has changed
without tracking what specific parts of the .class file (e.g.,
fields or methods) have changed.
2. CLASS_MEMBER_LEVEL: allows tracking not only whether a .class file
has changed but also what specific parts of the .class file (e.g.,
fields or methods) have changed.
The idea is that for better performance we will use CLASS_LEVEL for
classpath entries that are usually unchanged, and CLASS_MEMBER_LEVEL
for classpath entries that are frequently changed. We'll work out the
specifics in a following commit after some measurements.
Support running kotlinc on Windows in ClasspathSnapshotTestCommon
Also add tests for different Kotlin class kinds.
Add unit tests for CLASS_LEVEL snapshotting and diffing
Test: Updated ClasspathSnapshotterTest + ClasspathChangesComputerTest
Add ClasspathChangesComputerTest.testMixedClassSnapshotGranularities
This commit is contained in:
committed by
nataliya.valtman
parent
05457c291d
commit
d2193f3873
+135
-22
@@ -8,20 +8,22 @@ package org.jetbrains.kotlin.incremental.classpathDiff
|
||||
import org.jetbrains.kotlin.build.report.metrics.DoNothingBuildMetricsReporter
|
||||
import org.jetbrains.kotlin.incremental.ChangesEither
|
||||
import org.jetbrains.kotlin.incremental.LookupSymbol
|
||||
import org.jetbrains.kotlin.incremental.classpathDiff.ClassSnapshotGranularity.CLASS_LEVEL
|
||||
import org.jetbrains.kotlin.incremental.classpathDiff.ClassSnapshotGranularity.CLASS_MEMBER_LEVEL
|
||||
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.ClassFileUtil.asFile
|
||||
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.ClassFileUtil.snapshot
|
||||
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.CompileUtil.compileAll
|
||||
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
|
||||
import org.jetbrains.kotlin.resolve.sam.SAM_LOOKUP_NAME
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.io.File
|
||||
import kotlin.test.fail
|
||||
|
||||
abstract class ClasspathChangesComputerTest : ClasspathSnapshotTestCommon() {
|
||||
private val testDataDir =
|
||||
File("compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest")
|
||||
|
||||
companion object {
|
||||
val testDataDir =
|
||||
File("compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest")
|
||||
}
|
||||
abstract class ClasspathChangesComputerTest : ClasspathSnapshotTestCommon() {
|
||||
|
||||
@Test
|
||||
abstract fun testAbiVersusNonAbiChanges()
|
||||
@@ -29,6 +31,12 @@ abstract class ClasspathChangesComputerTest : ClasspathSnapshotTestCommon() {
|
||||
@Test
|
||||
abstract fun testModifiedAddedRemovedElements()
|
||||
|
||||
@Test
|
||||
abstract fun testModifiedAddedRemovedElements_ClassLevelSnapshot()
|
||||
|
||||
@Test
|
||||
abstract fun testMixedClassSnapshotGranularities()
|
||||
|
||||
@Test
|
||||
abstract fun testImpactAnalysis()
|
||||
}
|
||||
@@ -80,6 +88,49 @@ class KotlinOnlyClasspathChangesComputerTest : ClasspathChangesComputerTest() {
|
||||
).assertEquals(changes)
|
||||
}
|
||||
|
||||
@Test
|
||||
override fun testModifiedAddedRemovedElements_ClassLevelSnapshot() {
|
||||
val changes = computeClasspathChanges(File(testDataDir, "testModifiedAddedRemovedElements/src/kotlin"), tmpDir, CLASS_LEVEL)
|
||||
Changes(
|
||||
lookupSymbols = setOf(
|
||||
LookupSymbol(name = "ModifiedClassUnchangedMembers", scope = "com.example"),
|
||||
LookupSymbol(name = "ModifiedClassChangedMembers", scope = "com.example"),
|
||||
LookupSymbol(name = "AddedClass", scope = "com.example"),
|
||||
LookupSymbol(name = "RemovedClass", scope = "com.example"),
|
||||
),
|
||||
fqNames = setOf(
|
||||
"com.example.ModifiedClassUnchangedMembers",
|
||||
"com.example.ModifiedClassChangedMembers",
|
||||
"com.example.AddedClass",
|
||||
"com.example.RemovedClass",
|
||||
)
|
||||
).assertEquals(changes)
|
||||
}
|
||||
|
||||
@Test
|
||||
override fun testMixedClassSnapshotGranularities() {
|
||||
val currentClasspathSnapshot = testMixedClassSnapshotGranularities_snapshotClasspath("kotlin", "current-classpath", tmpDir)
|
||||
val previousClasspathSnapshot = testMixedClassSnapshotGranularities_snapshotClasspath("kotlin", "previous-classpath", tmpDir)
|
||||
|
||||
val changes = computeClasspathChanges(currentClasspathSnapshot, previousClasspathSnapshot)
|
||||
Changes(
|
||||
lookupSymbols = setOf(
|
||||
LookupSymbol(name = "CoarseGrainedFirstBuild_CoarseGrainedSecondBuild_Class", scope = "com.example"),
|
||||
LookupSymbol(name = "CoarseGrainedFirstBuild_FineGrainedSecondBuild_Class", scope = "com.example"),
|
||||
LookupSymbol(name = "FineGrainedFirstBuild_CoarseGrainedSecondBuild_Class", scope = "com.example"),
|
||||
LookupSymbol(name = "modifiedProperty", scope = "com.example.FineGrainedFirstBuild_FineGrainedSecondBuild_Class"),
|
||||
LookupSymbol(name = "modifiedFunction", scope = "com.example.FineGrainedFirstBuild_FineGrainedSecondBuild_Class"),
|
||||
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.FineGrainedFirstBuild_FineGrainedSecondBuild_Class")
|
||||
),
|
||||
fqNames = setOf(
|
||||
"com.example.CoarseGrainedFirstBuild_CoarseGrainedSecondBuild_Class",
|
||||
"com.example.CoarseGrainedFirstBuild_FineGrainedSecondBuild_Class",
|
||||
"com.example.FineGrainedFirstBuild_CoarseGrainedSecondBuild_Class",
|
||||
"com.example.FineGrainedFirstBuild_FineGrainedSecondBuild_Class"
|
||||
)
|
||||
).assertEquals(changes)
|
||||
}
|
||||
|
||||
@Test
|
||||
override fun testImpactAnalysis() {
|
||||
val changes = computeClasspathChanges(File(testDataDir, "testImpactAnalysis_KotlinOnly/src"), tmpDir)
|
||||
@@ -230,6 +281,49 @@ class JavaOnlyClasspathChangesComputerTest : ClasspathChangesComputerTest() {
|
||||
).assertEquals(changes)
|
||||
}
|
||||
|
||||
@Test
|
||||
override fun testModifiedAddedRemovedElements_ClassLevelSnapshot() {
|
||||
val changes = computeClasspathChanges(File(testDataDir, "testModifiedAddedRemovedElements/src/java"), tmpDir, CLASS_LEVEL)
|
||||
Changes(
|
||||
lookupSymbols = setOf(
|
||||
LookupSymbol(name = "ModifiedClassUnchangedMembers", scope = "com.example"),
|
||||
LookupSymbol(name = "ModifiedClassChangedMembers", scope = "com.example"),
|
||||
LookupSymbol(name = "AddedClass", scope = "com.example"),
|
||||
LookupSymbol(name = "RemovedClass", scope = "com.example")
|
||||
),
|
||||
fqNames = setOf(
|
||||
"com.example.ModifiedClassUnchangedMembers",
|
||||
"com.example.ModifiedClassChangedMembers",
|
||||
"com.example.AddedClass",
|
||||
"com.example.RemovedClass"
|
||||
)
|
||||
).assertEquals(changes)
|
||||
}
|
||||
|
||||
@Test
|
||||
override fun testMixedClassSnapshotGranularities() {
|
||||
val currentClasspathSnapshot = testMixedClassSnapshotGranularities_snapshotClasspath("java", "current-classpath", tmpDir)
|
||||
val previousClasspathSnapshot = testMixedClassSnapshotGranularities_snapshotClasspath("java", "previous-classpath", tmpDir)
|
||||
|
||||
val changes = computeClasspathChanges(currentClasspathSnapshot, previousClasspathSnapshot)
|
||||
Changes(
|
||||
lookupSymbols = setOf(
|
||||
LookupSymbol(name = "CoarseGrainedFirstBuild_CoarseGrainedSecondBuild_Class", scope = "com.example"),
|
||||
LookupSymbol(name = "CoarseGrainedFirstBuild_FineGrainedSecondBuild_Class", scope = "com.example"),
|
||||
LookupSymbol(name = "FineGrainedFirstBuild_CoarseGrainedSecondBuild_Class", scope = "com.example"),
|
||||
LookupSymbol(name = "modifiedField", scope = "com.example.FineGrainedFirstBuild_FineGrainedSecondBuild_Class"),
|
||||
LookupSymbol(name = "modifiedMethod", scope = "com.example.FineGrainedFirstBuild_FineGrainedSecondBuild_Class"),
|
||||
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.FineGrainedFirstBuild_FineGrainedSecondBuild_Class")
|
||||
),
|
||||
fqNames = setOf(
|
||||
"com.example.CoarseGrainedFirstBuild_CoarseGrainedSecondBuild_Class",
|
||||
"com.example.CoarseGrainedFirstBuild_FineGrainedSecondBuild_Class",
|
||||
"com.example.FineGrainedFirstBuild_CoarseGrainedSecondBuild_Class",
|
||||
"com.example.FineGrainedFirstBuild_FineGrainedSecondBuild_Class"
|
||||
)
|
||||
).assertEquals(changes)
|
||||
}
|
||||
|
||||
@Test
|
||||
override fun testImpactAnalysis() {
|
||||
val changes = computeClasspathChanges(File(testDataDir, "testImpactAnalysis_JavaOnly/src"), tmpDir)
|
||||
@@ -262,7 +356,7 @@ class KotlinAndJavaClasspathChangesComputerTest : ClasspathSnapshotTestCommon()
|
||||
@Test
|
||||
fun testImpactAnalysis() {
|
||||
val changes =
|
||||
computeClasspathChanges(File(ClasspathChangesComputerTest.testDataDir, "testImpactAnalysis_KotlinAndJava/src"), tmpDir)
|
||||
computeClasspathChanges(File(testDataDir, "testImpactAnalysis_KotlinAndJava/src"), tmpDir)
|
||||
Changes(
|
||||
lookupSymbols = setOf(
|
||||
LookupSymbol(name = "changedProperty", scope = "com.example.ChangedKotlinSuperClass"),
|
||||
@@ -296,25 +390,44 @@ class KotlinAndJavaClasspathChangesComputerTest : ClasspathSnapshotTestCommon()
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeClasspathChanges(classpathSourceDir: File, tmpDir: TemporaryFolder): Changes {
|
||||
val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir)
|
||||
val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir)
|
||||
return computeClasspathChanges(currentSnapshot, previousSnapshot)
|
||||
private fun testMixedClassSnapshotGranularities_snapshotClasspath(
|
||||
language: String, classpathSourceDirName: String, tmpDir: TemporaryFolder
|
||||
): ClasspathSnapshot {
|
||||
val classes = compileAll(File("$testDataDir/testMixedClassSnapshotGranularities/src/$language/$classpathSourceDirName/0"), tmpDir)
|
||||
|
||||
fun getGranularity(classFile: ClassFile): ClassSnapshotGranularity {
|
||||
val granularity = when (val className = classFile.asFile().nameWithoutExtension) {
|
||||
"CoarseGrainedFirstBuild_CoarseGrainedSecondBuild_Class" -> CLASS_LEVEL to CLASS_LEVEL
|
||||
"CoarseGrainedFirstBuild_FineGrainedSecondBuild_Class" -> CLASS_LEVEL to CLASS_MEMBER_LEVEL
|
||||
"FineGrainedFirstBuild_CoarseGrainedSecondBuild_Class" -> CLASS_MEMBER_LEVEL to CLASS_LEVEL
|
||||
"FineGrainedFirstBuild_FineGrainedSecondBuild_Class" -> CLASS_MEMBER_LEVEL to CLASS_MEMBER_LEVEL
|
||||
else -> error("Unrecognized class: $className")
|
||||
}
|
||||
return when (classpathSourceDirName) {
|
||||
"previous-classpath" -> granularity.first
|
||||
"current-classpath" -> granularity.second
|
||||
else -> error("Unrecognized classpathSourceDirName: $classpathSourceDirName")
|
||||
}
|
||||
}
|
||||
|
||||
return classes.map { it.snapshot(getGranularity(it)) }.toClasspathSnapshot()
|
||||
}
|
||||
|
||||
private fun snapshotClasspath(classpathSourceDir: File, tmpDir: TemporaryFolder): 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))
|
||||
private fun List<ClassSnapshot>.toClasspathSnapshot(): ClasspathSnapshot {
|
||||
val classpathEntrySnapshot = ClasspathEntrySnapshot(associateByTo(LinkedHashMap()) {
|
||||
JvmClassName.byClassId((it as AccessibleClassSnapshot).classId).internalName + ".class"
|
||||
})
|
||||
return ClasspathSnapshot(listOf(classpathEntrySnapshot))
|
||||
}
|
||||
|
||||
val relativePaths = classFiles.map { it.unixStyleRelativePath }
|
||||
val classSnapshots = classFiles.snapshot().map { it.withHash }
|
||||
ClasspathEntrySnapshot(
|
||||
classSnapshots = relativePaths.zip(classSnapshots).toMap(LinkedHashMap())
|
||||
)
|
||||
}
|
||||
return ClasspathSnapshot(classpathEntrySnapshots)
|
||||
private fun computeClasspathChanges(
|
||||
classpathSourceDir: File,
|
||||
tmpDir: TemporaryFolder,
|
||||
granularity: ClassSnapshotGranularity? = null
|
||||
): Changes {
|
||||
val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir, granularity)
|
||||
val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir, granularity)
|
||||
return computeClasspathChanges(currentSnapshot, previousSnapshot)
|
||||
}
|
||||
|
||||
private fun computeClasspathChanges(
|
||||
|
||||
+3
-3
@@ -36,8 +36,8 @@ class KotlinClassesClasspathSnapshotSerializerTest : ClasspathSnapshotSerializer
|
||||
|
||||
override val sourceFile = TestSourceFile(
|
||||
KotlinSourceFile(
|
||||
baseDir = File(testDataDir, "src/kotlin"), relativePath = "com/example/SimpleClass.kt",
|
||||
preCompiledClassFile = ClassFile(File(testDataDir, "classes/kotlin"), "com/example/SimpleClass.class")
|
||||
baseDir = File(testDataDir, "kotlin/testSimpleClass/src"), relativePath = "com/example/SimpleClass.kt",
|
||||
preCompiledClassFile = ClassFile(File(testDataDir, "kotlin/testSimpleClass/classes"), "com/example/SimpleClass.class")
|
||||
), tmpDir
|
||||
)
|
||||
}
|
||||
@@ -46,7 +46,7 @@ class JavaClassesClasspathSnapshotSerializerTest : ClasspathSnapshotSerializerTe
|
||||
|
||||
override val sourceFile = TestSourceFile(
|
||||
JavaSourceFile(
|
||||
baseDir = File(testDataDir, "src/java"), relativePath = "com/example/SimpleClass.java",
|
||||
baseDir = File(testDataDir, "java/testSimpleClass/src"), relativePath = "com/example/SimpleClass.java",
|
||||
), tmpDir
|
||||
)
|
||||
}
|
||||
|
||||
+62
-9
@@ -6,14 +6,17 @@
|
||||
package org.jetbrains.kotlin.incremental.classpathDiff
|
||||
|
||||
import com.google.gson.GsonBuilder
|
||||
import org.jetbrains.kotlin.cli.common.isWindows
|
||||
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.ClassFileUtil.asFile
|
||||
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.ClassFileUtil.snapshot
|
||||
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.CompileUtil.compile
|
||||
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.CompileUtil.compileAll
|
||||
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.SourceFile.KotlinSourceFile
|
||||
import org.jetbrains.kotlin.test.KotlinTestUtils
|
||||
import org.junit.Rule
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.io.File
|
||||
import java.lang.ProcessBuilder.Redirect
|
||||
|
||||
abstract class ClasspathSnapshotTestCommon {
|
||||
|
||||
@@ -68,12 +71,12 @@ abstract class ClasspathSnapshotTestCommon {
|
||||
} else {
|
||||
val srcDir = tmpDir.newFolder()
|
||||
asFile().copyTo(File(srcDir, unixStyleRelativePath))
|
||||
compileAll(srcDir, classpath = emptyList(), tmpDir)
|
||||
compileAll(srcDir, tmpDir)
|
||||
}
|
||||
}
|
||||
|
||||
/** 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> {
|
||||
fun compileAll(srcDir: File, tmpDir: TemporaryFolder, classpath: List<File> = emptyList()): List<ClassFile> {
|
||||
val kotlinClasses = compileKotlin(srcDir, classpath, tmpDir)
|
||||
|
||||
val javaClasspath = classpath + listOfNotNull(kotlinClasses.firstOrNull()?.classRoot)
|
||||
@@ -126,11 +129,20 @@ abstract class ClasspathSnapshotTestCommon {
|
||||
}
|
||||
|
||||
val classesDir = tmpDir.newFolder()
|
||||
org.jetbrains.kotlin.test.MockLibraryUtil.compileKotlin(
|
||||
// Note: Calling the following is simpler:
|
||||
// org.jetbrains.kotlin.test.MockLibraryUtil.compileKotlin(
|
||||
// srcDir.path, classesDir, extraClasspath = classpath.map { it.path }.toTypedArray())
|
||||
// However, it currently fails with UnsupportedClassVersionError, so we have to launch a new kotlinc process instead.
|
||||
val kotlincBinary = if (isWindows) "dist/kotlinc/bin/kotlinc.bat" else "dist/kotlinc/bin/kotlinc"
|
||||
check(File(kotlincBinary).exists()) { "'${File(kotlincBinary).absolutePath}' not found. Run ./gradlew dist first." }
|
||||
val commandAndArgs = listOf(
|
||||
kotlincBinary,
|
||||
srcDir.path,
|
||||
classesDir,
|
||||
extraClasspath = classpath.map { it.path }.toTypedArray()
|
||||
"-d", classesDir.path,
|
||||
"-classpath", (listOf(srcDir) + classpath).joinToString(File.pathSeparator) { it.path }
|
||||
)
|
||||
runCommandInNewProcess(commandAndArgs)
|
||||
|
||||
return getClassFilesInDir(classesDir)
|
||||
}
|
||||
|
||||
@@ -164,11 +176,52 @@ abstract class ClasspathSnapshotTestCommon {
|
||||
|
||||
fun ClassFile.readBytes() = asFile().readBytes()
|
||||
|
||||
fun ClassFile.snapshot(): ClassSnapshot = listOf(this).snapshot().single()
|
||||
fun ClassFile.snapshot(granularity: ClassSnapshotGranularity? = null): ClassSnapshot = listOf(this).snapshot(granularity).single()
|
||||
|
||||
fun List<ClassFile>.snapshot(): List<ClassSnapshot> {
|
||||
val classFilesWithContents = this.map { ClassFileWithContents(it, it.readBytes()) }
|
||||
return ClassSnapshotter.snapshot(classFilesWithContents)
|
||||
fun List<ClassFile>.snapshot(granularity: ClassSnapshotGranularity? = null): List<ClassSnapshot> {
|
||||
val classes = map { ClassFileWithContents(it, it.readBytes()) }
|
||||
return if (granularity == null) {
|
||||
ClassSnapshotter.snapshot(classes)
|
||||
} else {
|
||||
ClassSnapshotter.snapshot(classes, granularity = granularity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun snapshotClasspath(
|
||||
classpathSourceDir: File,
|
||||
tmpDir: TemporaryFolder,
|
||||
granularity: ClassSnapshotGranularity? = null
|
||||
): ClasspathSnapshot {
|
||||
val classpath = mutableListOf<File>()
|
||||
val classpathEntrySnapshots = classpathSourceDir.listFiles()!!.sortedBy { it.name }.map { classpathEntrySourceDir ->
|
||||
val classFiles = compileAll(classpathEntrySourceDir, tmpDir, classpath)
|
||||
classpath.addAll(listOfNotNull(classFiles.firstOrNull()?.classRoot))
|
||||
|
||||
val relativePaths = classFiles.map { it.unixStyleRelativePath }
|
||||
val classSnapshots = classFiles.snapshot(granularity)
|
||||
ClasspathEntrySnapshot(
|
||||
classSnapshots = relativePaths.zip(classSnapshots).toMap(LinkedHashMap())
|
||||
)
|
||||
}
|
||||
return ClasspathSnapshot(classpathEntrySnapshots)
|
||||
}
|
||||
|
||||
private fun runCommandInNewProcess(commandAndArgs: List<String>) {
|
||||
val processBuilder = ProcessBuilder(commandAndArgs)
|
||||
processBuilder.redirectInput(Redirect.INHERIT)
|
||||
processBuilder.redirectOutput(Redirect.INHERIT)
|
||||
processBuilder.redirectErrorStream(true)
|
||||
val process = processBuilder.start()
|
||||
|
||||
val exitCode = try {
|
||||
process.waitFor()
|
||||
} finally {
|
||||
process.destroyForcibly()
|
||||
}
|
||||
check(exitCode == 0) {
|
||||
"Process returned exit code: $exitCode\n" +
|
||||
"commandAndArgs = ${commandAndArgs.joinToString(" ")}"
|
||||
}
|
||||
}
|
||||
|
||||
+129
-64
@@ -6,94 +6,159 @@
|
||||
package org.jetbrains.kotlin.incremental.classpathDiff
|
||||
|
||||
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.ClassFileUtil.readBytes
|
||||
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.ClassFileUtil.snapshot
|
||||
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.TestSourceFile
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import java.io.File
|
||||
|
||||
abstract class ClasspathSnapshotterTest : ClasspathSnapshotTestCommon() {
|
||||
private val testDataDir =
|
||||
File("compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotterTest")
|
||||
|
||||
companion object {
|
||||
val testDataDir =
|
||||
File("compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotterTest")
|
||||
}
|
||||
class KotlinOnlyClasspathSnapshotterTest : ClasspathSnapshotTestCommon() {
|
||||
|
||||
protected abstract val sourceFile: TestSourceFile
|
||||
protected abstract val sourceFileWithAbiChange: TestSourceFile
|
||||
protected abstract val sourceFileWithNonAbiChange: TestSourceFile
|
||||
|
||||
private val expectedSnapshotFile: File
|
||||
get() = sourceFile.asFile().let {
|
||||
val srcDir = File(it.path.substringBeforeLast("src") + "src")
|
||||
val relativePath = it.relativeTo(srcDir)
|
||||
testDataDir.resolve("expected-snapshot").resolve(relativePath.parent).resolve(relativePath.nameWithoutExtension + ".json")
|
||||
}
|
||||
@Suppress("SameParameterValue")
|
||||
private fun getSourceFile(testName: String, relativePath: String) = TestSourceFile(
|
||||
KotlinSourceFile(
|
||||
baseDir = File("$testDataDir/kotlin/$testName/src"), relativePath = relativePath,
|
||||
preCompiledClassFile = ClassFile(File("$testDataDir/kotlin/$testName/classes"), relativePath.replace(".kt", ".class"))
|
||||
), tmpDir
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `test ClassSnapshotter's result against expected snapshot`() {
|
||||
val classSnapshot = sourceFile.compileSingle().let {
|
||||
ClassSnapshotter.snapshot(listOf(ClassFileWithContents(it, it.readBytes())), includeDebugInfoInJavaSnapshot = true)
|
||||
}.single()
|
||||
fun testSimpleClass() {
|
||||
val sourceFile = getSourceFile("testSimpleClass", "com/example/SimpleClass.kt")
|
||||
val actualSnapshot = sourceFile.compileAndSnapshot().toGson()
|
||||
val expectedSnapshot = sourceFile.getExpectedSnapshotFile().readText()
|
||||
|
||||
assertEquals(expectedSnapshotFile.readText(), classSnapshot.toGson())
|
||||
assertEquals(expectedSnapshot, actualSnapshot)
|
||||
|
||||
// Check that the snapshot contains ABI info
|
||||
actualSnapshot.assertContains("publicProperty", "publicFunction")
|
||||
|
||||
// Private properties and functions' names/signatures are currently part of the snapshot. We will fix this later.
|
||||
actualSnapshot.assertContains("privateProperty", "privateFunction")
|
||||
|
||||
// Check that the snapshot does not contain non-ABI info
|
||||
actualSnapshot.assertDoesNotContain(
|
||||
"publicProperty's value",
|
||||
"privateProperty's value",
|
||||
"publicFunction's body",
|
||||
"privateFunction's body"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test ClassSnapshotter extracts ABI info from a class`() {
|
||||
// After an ABI change, the snapshot must change
|
||||
assertNotEquals(sourceFile.compileAndSnapshot().toGson(), sourceFileWithAbiChange.compileAndSnapshot().toGson())
|
||||
fun testSimpleClass_ClassLevelSnapshot() {
|
||||
val sourceFile = getSourceFile("testSimpleClass", "com/example/SimpleClass.kt")
|
||||
val classFile = sourceFile.compileSingle()
|
||||
val actualSnapshot = classFile.snapshot(ClassSnapshotGranularity.CLASS_LEVEL).toGson()
|
||||
val expectedSnapshot = sourceFile.getExpectedSnapshotFile(ClassSnapshotGranularity.CLASS_LEVEL).readText()
|
||||
|
||||
assertEquals(expectedSnapshot, actualSnapshot)
|
||||
|
||||
// Check that the snapshot does not contain class member details
|
||||
actualSnapshot.assertDoesNotContain("publicProperty", "privateProperty", "publicFunction", "privateFunction")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test ClassSnapshotter does not extract non-ABI info from a class`() {
|
||||
// After a non-ABI change, the snapshot must not change
|
||||
assertEquals(sourceFile.compileAndSnapshot().toGson(), sourceFileWithNonAbiChange.compileAndSnapshot().toGson())
|
||||
fun testPackageFacadeClasses() {
|
||||
val classpathSnapshot = snapshotClasspath(File("$testDataDir/kotlin/testPackageFacadeClasses/src"), tmpDir)
|
||||
val classSnapshots = classpathSnapshot.classpathEntrySnapshots.single().classSnapshots
|
||||
val fileFacadeSnapshot = classSnapshots["com/example/FileFacadeKt.class"]!!.toGson()
|
||||
val multifileClassSnapshot = classSnapshots["com/example/MultifileClass.class"]!!.toGson()
|
||||
val multifileClassPart1Snapshot = classSnapshots["com/example/MultifileClass__MultifileClass1Kt.class"]!!.toGson()
|
||||
val multifileClassPart2Snapshot = classSnapshots["com/example/MultifileClass__MultifileClass2Kt.class"]!!.toGson()
|
||||
|
||||
// Check that the snapshots contain ABI info
|
||||
fileFacadeSnapshot.assertContains("propertyInFileFacade", "functionInFileFacade")
|
||||
multifileClassPart1Snapshot.assertContains("propertyInMultifileClass1", "functionInMultifileClass1")
|
||||
multifileClassPart2Snapshot.assertContains("propertyInMultifileClass2", "functionInMultifileClass2")
|
||||
|
||||
// Check that the snapshots do not contain non-ABI info
|
||||
fileFacadeSnapshot.assertDoesNotContain("propertyInFileFacade's value", "functionInFileFacade's body")
|
||||
multifileClassPart1Snapshot.assertDoesNotContain("propertyInMultifileClass1's value", "functionInMultifileClass1's body")
|
||||
multifileClassPart2Snapshot.assertDoesNotContain("propertyInMultifileClass2's value", "functionInMultifileClass2's body")
|
||||
|
||||
// Classes with MULTIFILE_CLASS kind have no proto data
|
||||
multifileClassSnapshot.assertDoesNotContain(
|
||||
"propertyInMultifileClass1",
|
||||
"functionInMultifileClass1",
|
||||
"propertyInMultifileClass2",
|
||||
"functionInMultifileClass2"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class KotlinOnlyClasspathSnapshotterTest : ClasspathSnapshotterTest() {
|
||||
class JavaOnlyClasspathSnapshotterTest : ClasspathSnapshotTestCommon() {
|
||||
|
||||
override val sourceFile = TestSourceFile(
|
||||
KotlinSourceFile(
|
||||
baseDir = File(testDataDir, "src/kotlin"), relativePath = "com/example/SimpleClass.kt",
|
||||
preCompiledClassFile = ClassFile(File(testDataDir, "classes/kotlin"), "com/example/SimpleClass.class")
|
||||
), tmpDir
|
||||
@Suppress("SameParameterValue")
|
||||
private fun getSourceFile(testName: String, relativePath: String) = TestSourceFile(
|
||||
JavaSourceFile(baseDir = File("$testDataDir/java/$testName/src"), relativePath = relativePath), tmpDir
|
||||
)
|
||||
|
||||
override val sourceFileWithAbiChange = TestSourceFile(
|
||||
KotlinSourceFile(
|
||||
baseDir = File(testDataDir, "src-changed/kotlin/abi-change"), relativePath = "com/example/SimpleClass.kt",
|
||||
preCompiledClassFile = ClassFile(File(testDataDir, "classes-changed/kotllin/abi-change"), "com/example/SimpleClass.class")
|
||||
), tmpDir
|
||||
)
|
||||
private fun TestSourceFile.compileAndSnapshotWithDebugInfo(): ClassSnapshot {
|
||||
val classFile = compileSingle()
|
||||
return ClassSnapshotter.snapshot(
|
||||
listOf(ClassFileWithContents(classFile, classFile.readBytes())),
|
||||
includeDebugInfoInJavaSnapshot = true
|
||||
).single()
|
||||
}
|
||||
|
||||
override val sourceFileWithNonAbiChange = TestSourceFile(
|
||||
KotlinSourceFile(
|
||||
baseDir = File(testDataDir, "src-changed/kotlin/non-abi-change"), relativePath = "com/example/SimpleClass.kt",
|
||||
preCompiledClassFile = ClassFile(File(testDataDir, "classes-changed/kotlin/non-abi-change"), "com/example/SimpleClass.class")
|
||||
), tmpDir
|
||||
)
|
||||
@Test
|
||||
fun testSimpleClass() {
|
||||
val sourceFile = getSourceFile("testSimpleClass", "com/example/SimpleClass.java")
|
||||
val actualSnapshot = sourceFile.compileAndSnapshotWithDebugInfo().toGson()
|
||||
val expectedSnapshot = sourceFile.getExpectedSnapshotFile().readText()
|
||||
|
||||
assertEquals(expectedSnapshot, actualSnapshot)
|
||||
|
||||
// Check that the snapshot contains ABI info
|
||||
actualSnapshot.assertContains("publicField", "publicMethod")
|
||||
|
||||
// Check that the snapshot does not contain non-ABI info
|
||||
actualSnapshot.assertDoesNotContain(
|
||||
"privateField",
|
||||
"privateMethod",
|
||||
"publicField's value",
|
||||
"privateField's value",
|
||||
"publicMethod's body",
|
||||
"privateMethod's body"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSimpleClass_ClassLevelSnapshot() {
|
||||
val sourceFile = getSourceFile("testSimpleClass", "com/example/SimpleClass.java")
|
||||
val classFile = sourceFile.compileSingle()
|
||||
val actualSnapshot = classFile.snapshot(ClassSnapshotGranularity.CLASS_LEVEL).toGson()
|
||||
val expectedSnapshot = sourceFile.getExpectedSnapshotFile(ClassSnapshotGranularity.CLASS_LEVEL).readText()
|
||||
|
||||
assertEquals(expectedSnapshot, actualSnapshot)
|
||||
|
||||
// Check that the snapshot does not contain class member details
|
||||
actualSnapshot.assertDoesNotContain("publicField", "privateField", "publicMethod", "privateMethod")
|
||||
}
|
||||
}
|
||||
|
||||
class JavaOnlyClasspathSnapshotterTest : ClasspathSnapshotterTest() {
|
||||
|
||||
override val sourceFile = TestSourceFile(
|
||||
JavaSourceFile(
|
||||
baseDir = File(testDataDir, "src/java"), relativePath = "com/example/SimpleClass.java",
|
||||
), tmpDir
|
||||
)
|
||||
|
||||
override val sourceFileWithAbiChange = TestSourceFile(
|
||||
JavaSourceFile(
|
||||
baseDir = File(testDataDir, "src-changed/java/abi-change"), relativePath = "com/example/SimpleClass.java",
|
||||
), tmpDir
|
||||
)
|
||||
|
||||
override val sourceFileWithNonAbiChange = TestSourceFile(
|
||||
JavaSourceFile(
|
||||
baseDir = File(testDataDir, "src-changed/java/non-abi-change"), relativePath = "com/example/SimpleClass.java",
|
||||
), tmpDir
|
||||
)
|
||||
private fun TestSourceFile.getExpectedSnapshotFile(granularity: ClassSnapshotGranularity? = null): File {
|
||||
val relativePath = sourceFile.unixStyleRelativePath.substringBeforeLast(".") + ".json"
|
||||
val expectedSnapshotDirName = if (granularity == null) "expected-snapshot" else "expected-snapshot-${granularity.name}"
|
||||
return sourceFile.baseDir.resolve("../$expectedSnapshotDirName/$relativePath")
|
||||
}
|
||||
|
||||
private fun String.assertContains(vararg elements: String) {
|
||||
elements.forEach {
|
||||
assertTrue(contains(it))
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.assertDoesNotContain(vararg elements: String) {
|
||||
elements.forEach {
|
||||
assertFalse(contains(it))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user