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:
Hung Nguyen
2022-01-17 19:06:04 +00:00
committed by nataliya.valtman
parent 05457c291d
commit d2193f3873
76 changed files with 1170 additions and 566 deletions
@@ -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(
@@ -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
)
}
@@ -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(" ")}"
}
}
@@ -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))
}
}