KT-45777: Compute classpath changes based on changed snapshots only
to avoid unnecessarily loading unchanged ones. Duplicate classes will make this a bit tricky. This commit outlines the algorithm to handle them, the full implementation will follow later. Also handle removed classes when computing classpath changes. Test: New tests in ClasspathChangesComputerTest
This commit is contained in:
committed by
nataliya.valtman
parent
c2af8b2a29
commit
e26dc4d574
@@ -109,15 +109,17 @@ abstract class AbstractIncrementalCache<ClassName>(
|
||||
|
||||
override fun markDirty(removedAndCompiledSources: Collection<File>) {
|
||||
for (sourceFile in removedAndCompiledSources) {
|
||||
val classes = sourceToClassesMap[sourceFile]
|
||||
classes.forEach {
|
||||
dirtyOutputClassesMap.markDirty(it)
|
||||
sourceToClassesMap[sourceFile].forEach { className ->
|
||||
markDirty(className)
|
||||
}
|
||||
|
||||
sourceToClassesMap.clearOutputsForSource(sourceFile)
|
||||
}
|
||||
}
|
||||
|
||||
fun markDirty(className: ClassName) {
|
||||
dirtyOutputClassesMap.markDirty(className)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates class storage based on the given class proto.
|
||||
*
|
||||
|
||||
+161
-42
@@ -8,16 +8,114 @@ package org.jetbrains.kotlin.gradle.incremental
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import org.jetbrains.kotlin.incremental.*
|
||||
import org.jetbrains.kotlin.incremental.storage.FileToCanonicalPathConverter
|
||||
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import kotlin.collections.LinkedHashMap
|
||||
|
||||
/** Computes [ClasspathChanges] between two [ClasspathSnapshot]s .*/
|
||||
object ClasspathChangesComputer {
|
||||
|
||||
fun compute(currentClasspathSnapshot: ClasspathSnapshot, previousClasspathSnapshot: ClasspathSnapshot): ClasspathChanges {
|
||||
val currentClassSnapshots = currentClasspathSnapshot.getClassSnapshots()
|
||||
val previousClassSnapshots = previousClasspathSnapshot.getClassSnapshots()
|
||||
fun compute(
|
||||
currentClasspathEntrySnapshotFiles: List<File>,
|
||||
previousClasspathEntrySnapshotFiles: List<File>,
|
||||
unchangedCurrentClasspathEntrySnapshotFiles: List<File>
|
||||
): ClasspathChanges {
|
||||
// To improve performance, we will compute changes for the changed snapshot files only, ignoring unchanged ones. (Duplicate classes
|
||||
// will make this a bit tricky, but it will be dealt with below.)
|
||||
// First, align unchanged snapshot files in the current classpath with unchanged snapshot files in the previous classpath. Gradle
|
||||
// has this information, but doesn't expose it, so we have to reconstruct it here.
|
||||
val unchangedCurrentToPreviousAlignment: Map<File, File> =
|
||||
alignUnchangedSnapshotFiles(unchangedCurrentClasspathEntrySnapshotFiles, previousClasspathEntrySnapshotFiles)
|
||||
|
||||
// Use sets to make presence checks faster
|
||||
val unchangedCurrentFiles: Set<File> = unchangedCurrentToPreviousAlignment.keys
|
||||
val unchangedPreviousFiles: Set<File> = unchangedCurrentToPreviousAlignment.values.toSet()
|
||||
|
||||
// We will split the current files into 2 groups:
|
||||
// 1a) Unchanged current files
|
||||
// 1b) Added files
|
||||
// We will split the previous files into 2 groups:
|
||||
// 2a) Unchanged previous files
|
||||
// 2b) Removed files
|
||||
// If the classpath doesn't contain duplicate classes, comparing (1b) with (2b) would be enough.
|
||||
// However, if the classpath contains duplicate classes, comparing (1b) with (2b) would not be enough.
|
||||
// Therefore, to deal with duplicate classes while still being able to compare (1b) with (2b), we will find snapshot files in groups
|
||||
// (1a) and (2a) that have duplicate classes with groups (1b) or (2b) and add them to groups (1b) and (2b). Duplicate classes in
|
||||
// groups (1b) and (2b) will then be handled in a separate step (see ClasspathChangesComputer.getNonDuplicateClassSnapshots).
|
||||
val addedFiles: List<File> = currentClasspathEntrySnapshotFiles.filter { it !in unchangedCurrentFiles }
|
||||
val removedFiles: List<File> = previousClasspathEntrySnapshotFiles.filter { it !in unchangedPreviousFiles }
|
||||
|
||||
val adjustedAddedFiles = addedFiles.toMutableSet()
|
||||
val adjustedRemovedFiles = removedFiles.toMutableSet()
|
||||
unchangedCurrentToPreviousAlignment.forEach { (unchangedCurrentFile, unchangedPreviousFile) ->
|
||||
if (unchangedCurrentFile.containsDuplicatesWith(addedFiles) || unchangedPreviousFile.containsDuplicatesWith(removedFiles)) {
|
||||
adjustedAddedFiles.add(unchangedCurrentFile)
|
||||
adjustedRemovedFiles.add(unchangedPreviousFile)
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the original order of added/removed files as it is important for the handling of duplicate classes.
|
||||
val finalAddedFiles: List<File> = currentClasspathEntrySnapshotFiles.filter { it in adjustedAddedFiles }
|
||||
val finalRemovedFiles: List<File> = previousClasspathEntrySnapshotFiles.filter { it in adjustedRemovedFiles }
|
||||
|
||||
val changedCurrentSnapshot = ClasspathSnapshotSerializer.load(finalAddedFiles)
|
||||
val changedPreviousSnapshot = ClasspathSnapshotSerializer.load(finalRemovedFiles)
|
||||
|
||||
return compute(changedCurrentSnapshot, changedPreviousSnapshot)
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the unchanged snapshot files of the current build to the unchanged snapshot files of the previous build (selected from all the
|
||||
* snapshot files of the previous build).
|
||||
*/
|
||||
private fun alignUnchangedSnapshotFiles(unchangedCurrentSnapshotFiles: List<File>, previousSnapshotFiles: List<File>): Map<File, File> {
|
||||
var index = 0
|
||||
return unchangedCurrentSnapshotFiles.associateWith { unchangedCurrentFile ->
|
||||
var candidates = previousSnapshotFiles.subList(index, previousSnapshotFiles.size).filter { previousFile ->
|
||||
unchangedCurrentFile.lastModified() == previousFile.lastModified() && unchangedCurrentFile.length() == previousFile.length()
|
||||
}
|
||||
if (candidates.size > 1) {
|
||||
candidates = candidates.filter { candidate ->
|
||||
unchangedCurrentFile.readBytes().contentEquals(candidate.readBytes())
|
||||
}
|
||||
}
|
||||
check(candidates.isNotEmpty()) {
|
||||
"Can't find previous snapshot file of unchanged current snapshot file '${unchangedCurrentFile.path}'"
|
||||
}
|
||||
// If there are multiple candidates, select the first one. (It doesn't have to match Gradle's alignment as long as it's still a
|
||||
// correct alignment.)
|
||||
val unchangedPreviousFile = candidates.first()
|
||||
while (previousSnapshotFiles[index] != unchangedPreviousFile) {
|
||||
index++
|
||||
}
|
||||
index++
|
||||
unchangedPreviousFile
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns `true` if this snapshot file contains a duplicate class with another snapshot file in the given set. */
|
||||
@Suppress("unused", "UNUSED_PARAMETER")
|
||||
private fun File.containsDuplicatesWith(otherSnapshotFiles: List<File>): Boolean {
|
||||
// TODO: Implement and optimize this method
|
||||
// Existing approach (with `kotlin.incremental.useClasspathSnapshot=false`) doesn't seem to handle duplicate classes, so it is
|
||||
// probably not a regression that we are not handling duplicate classes here yet.
|
||||
return false
|
||||
}
|
||||
|
||||
fun compute(currentClasspathSnapshot: ClasspathSnapshot, previousClasspathSnapshot: ClasspathSnapshot): ClasspathChanges {
|
||||
val currentClassSnapshots = currentClasspathSnapshot.getNonDuplicateClassSnapshots()
|
||||
val previousClassSnapshots = previousClasspathSnapshot.getNonDuplicateClassSnapshots()
|
||||
|
||||
return compute(currentClassSnapshots, previousClassSnapshots)
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes changes between two lists of [ClassSnapshot]s.
|
||||
*
|
||||
* Each list must not contain duplicate classes.
|
||||
*/
|
||||
fun compute(currentClassSnapshots: List<ClassSnapshot>, previousClassSnapshots: List<ClassSnapshot>): ClasspathChanges {
|
||||
if (currentClassSnapshots.any { it is ContentHashJavaClassSnapshot }
|
||||
|| previousClassSnapshots.any { it is ContentHashJavaClassSnapshot }) {
|
||||
return ClasspathChanges.NotAvailable.UnableToCompute
|
||||
@@ -27,56 +125,83 @@ object ClasspathChangesComputer {
|
||||
FileUtil.createTempDirectory(this::class.java.simpleName, "_WorkingDir_${UUID.randomUUID()}", /* deleteOnExit */ true)
|
||||
val incrementalJvmCache = IncrementalJvmCache(workingDir, /* targetOutputDir */ null, FileToCanonicalPathConverter)
|
||||
|
||||
// Store previous class snapshots in incrementalJvmCache, the returned ChangesCollector result is not used.
|
||||
// Step 1:
|
||||
// - Add previous class snapshots to incrementalJvmCache.
|
||||
// - Internally, incrementalJvmCache maintains a set of dirty classes to detect removed classes. Add previous classes to this set
|
||||
// to detect removed classes later (see step 2).
|
||||
// - The final ChangesCollector result will contain all symbols in the previous classes (we actually don't need them, but it's
|
||||
// part of the API's effects).
|
||||
val unusedChangesCollector = ChangesCollector()
|
||||
for (previousSnapshot in previousClassSnapshots) {
|
||||
when (previousSnapshot) {
|
||||
is KotlinClassSnapshot -> incrementalJvmCache.saveClassToCache(
|
||||
kotlinClassInfo = previousSnapshot.classInfo,
|
||||
sourceFiles = null,
|
||||
changesCollector = unusedChangesCollector
|
||||
)
|
||||
is RegularJavaClassSnapshot -> incrementalJvmCache.saveJavaClassProto(
|
||||
source = null,
|
||||
serializedJavaClass = previousSnapshot.serializedJavaClass,
|
||||
collector = unusedChangesCollector
|
||||
)
|
||||
is KotlinClassSnapshot -> {
|
||||
incrementalJvmCache.saveClassToCache(
|
||||
kotlinClassInfo = previousSnapshot.classInfo,
|
||||
sourceFiles = null,
|
||||
changesCollector = unusedChangesCollector
|
||||
)
|
||||
incrementalJvmCache.markDirty(previousSnapshot.classInfo.className)
|
||||
}
|
||||
is RegularJavaClassSnapshot -> {
|
||||
incrementalJvmCache.saveJavaClassProto(
|
||||
source = null,
|
||||
serializedJavaClass = previousSnapshot.serializedJavaClass,
|
||||
collector = unusedChangesCollector
|
||||
)
|
||||
incrementalJvmCache.markDirty(JvmClassName.byClassId(previousSnapshot.serializedJavaClass.classId))
|
||||
}
|
||||
is EmptyJavaClassSnapshot -> {
|
||||
// Nothing to process
|
||||
// Nothing to process as these classes don't impact the result.
|
||||
}
|
||||
is ContentHashJavaClassSnapshot -> {
|
||||
error("Unexpected type (it should have been handled earlier): ${previousSnapshot.javaClass.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
// Call the following method even though there are no removed classes, just in case the method updates the state of
|
||||
// incrementalJvmCache.
|
||||
incrementalJvmCache.clearCacheForRemovedClasses(unusedChangesCollector)
|
||||
|
||||
// Compute changes between the current class snapshots and the previously stored snapshots, and save the result in changesCollector.
|
||||
// Step 2:
|
||||
// - Add current class snapshots to incrementalJvmCache. This will overwrite any previous class snapshots that have the same
|
||||
// `JvmClassName`. The previous class snapshots that are not overwritten will be detected as removed class snapshots and will be
|
||||
// removed from incrementalJvmCache in step 3.
|
||||
// - Internally, incrementalJvmCache will mark these classes as non-dirty. That means unchanged/modified classes that were marked
|
||||
// as dirty in step 1 will now be non-dirty (added classes will also be marked as non-dirty even though they were not marked as
|
||||
// dirty before). After this, the remaining dirty classes will be removed classes.
|
||||
// - The intermediate ChangesCollector result will contain all symbols in added classes and changed (added/modified/removed)
|
||||
// symbols in modified classes. We will collect all symbols in removed classes in step 3.
|
||||
val changesCollector = ChangesCollector()
|
||||
for (currentSnapshot in currentClassSnapshots) {
|
||||
when (currentSnapshot) {
|
||||
is KotlinClassSnapshot -> incrementalJvmCache.saveClassToCache(
|
||||
kotlinClassInfo = currentSnapshot.classInfo,
|
||||
sourceFiles = null,
|
||||
changesCollector = changesCollector
|
||||
)
|
||||
is RegularJavaClassSnapshot -> incrementalJvmCache.saveJavaClassProto(
|
||||
source = null,
|
||||
serializedJavaClass = currentSnapshot.serializedJavaClass,
|
||||
collector = changesCollector
|
||||
)
|
||||
is KotlinClassSnapshot -> {
|
||||
incrementalJvmCache.saveClassToCache(
|
||||
kotlinClassInfo = currentSnapshot.classInfo,
|
||||
sourceFiles = null,
|
||||
changesCollector = changesCollector
|
||||
)
|
||||
}
|
||||
is RegularJavaClassSnapshot -> {
|
||||
incrementalJvmCache.saveJavaClassProto(
|
||||
source = null,
|
||||
serializedJavaClass = currentSnapshot.serializedJavaClass,
|
||||
collector = changesCollector
|
||||
)
|
||||
}
|
||||
is EmptyJavaClassSnapshot -> {
|
||||
// Nothing to process
|
||||
// Nothing to process as these classes don't impact the result.
|
||||
}
|
||||
is ContentHashJavaClassSnapshot -> {
|
||||
error("Unexpected type (it should have been handled earlier): ${currentSnapshot.javaClass.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3:
|
||||
// - Detect removed classes: they are the remaining dirty classes.
|
||||
// - Remove previous class snapshots of removed classes from incrementalJvmCache.
|
||||
// - The final ChangesCollector result will contain all symbols in added classes, changed (added/modified/removed) symbols in
|
||||
// modified classes, and all symbols in removed classes.
|
||||
incrementalJvmCache.clearCacheForRemovedClasses(changesCollector)
|
||||
|
||||
// Normalize the changes and clean up
|
||||
val dirtyData = changesCollector.getDirtyData(listOf(incrementalJvmCache), EmptyICReporter)
|
||||
workingDir.deleteRecursively()
|
||||
|
||||
@@ -86,18 +211,12 @@ object ClasspathChangesComputer {
|
||||
)
|
||||
}
|
||||
|
||||
private fun ClasspathSnapshot.getClassSnapshots(): List<ClassSnapshot> {
|
||||
// If there are duplicate classes on the classpath, retain only the first one to match the compiler's behavior.
|
||||
// We still need to consider whether to remove duplicate classes based on the class file name or the `ClassId`, as two different
|
||||
// `ClassId`s can have the same class file name (e.g., nested class `B$C` of top-level class `A` in `first.jar` and nested class `C`
|
||||
// of top-level class `A$B` in `second.jar` both have the same class file name `A$B$C`).
|
||||
// - If we use class file name, only `A$B$C` in `first.jar` will be retained. This matches the compiler's behavior because when
|
||||
// resolving either of those classes, the compiler will only look for `A$B$C` in the first jar, even when the actual class is
|
||||
// located in the second jar.
|
||||
// - If we use `ClassId`, both `A$B$C` in `first.jar` and `A$B$C` in `second.jar` will be retained. That means that the snapshot
|
||||
// of the class in `second.jar` will be considered when computing classpath changes, which is not expected as it doesn't match
|
||||
// the compiler's behavior.
|
||||
// Therefore, we will remove duplicate classes based on the class file name, not the `ClassId`.
|
||||
/**
|
||||
* Returns all [ClassSnapshot]s in this [ClasspathSnapshot].
|
||||
*
|
||||
* If there are duplicate classes on the classpath, retain only the first one to match the compiler's behavior.
|
||||
*/
|
||||
private fun ClasspathSnapshot.getNonDuplicateClassSnapshots(): List<ClassSnapshot> {
|
||||
val classSnapshots = LinkedHashMap<String, ClassSnapshot>(classpathEntrySnapshots.sumOf { it.classSnapshots.size })
|
||||
for (classpathEntrySnapshot in classpathEntrySnapshots) {
|
||||
for ((unixStyleRelativePath, classSnapshot) in classpathEntrySnapshot.classSnapshots) {
|
||||
|
||||
+7
-8
@@ -239,8 +239,7 @@ private object DirectoryOrJarContentsReader {
|
||||
*
|
||||
* The map entries are sorted based on their Unix-style relative paths (to ensure deterministic results across filesystems).
|
||||
*
|
||||
* Note: If a jar has duplicate entries, only one of them will be used (there is no guarantee which one will be used, but the selection
|
||||
* will be deterministic).
|
||||
* Note: If a jar has duplicate entries after filtering, only the first one is retained.
|
||||
*/
|
||||
fun read(
|
||||
directoryOrJar: File,
|
||||
@@ -258,31 +257,31 @@ private object DirectoryOrJarContentsReader {
|
||||
directory: File,
|
||||
entryFilter: ((unixStyleRelativePath: String, isDirectory: Boolean) -> Boolean)? = null
|
||||
): LinkedHashMap<String, ByteArray> {
|
||||
val relativePathsToContents: MutableList<Pair<String, ByteArray>> = mutableListOf()
|
||||
val relativePathsToContents = mutableMapOf<String, ByteArray>()
|
||||
directory.walk().forEach { file ->
|
||||
val unixStyleRelativePath = file.relativeTo(directory).invariantSeparatorsPath
|
||||
if (entryFilter == null || entryFilter(unixStyleRelativePath, file.isDirectory)) {
|
||||
relativePathsToContents.add(unixStyleRelativePath to file.readBytes())
|
||||
relativePathsToContents[unixStyleRelativePath] = file.readBytes()
|
||||
}
|
||||
}
|
||||
return relativePathsToContents.sortedBy { it.first }.toMap(LinkedHashMap())
|
||||
return relativePathsToContents.toSortedMap().toMap(LinkedHashMap())
|
||||
}
|
||||
|
||||
private fun readJar(
|
||||
jarFile: File,
|
||||
entryFilter: ((unixStyleRelativePath: String, isDirectory: Boolean) -> Boolean)? = null
|
||||
): LinkedHashMap<String, ByteArray> {
|
||||
val relativePathsToContents: MutableList<Pair<String, ByteArray>> = mutableListOf()
|
||||
val relativePathsToContents = mutableMapOf<String, ByteArray>()
|
||||
ZipInputStream(jarFile.inputStream().buffered()).use { zipInputStream ->
|
||||
while (true) {
|
||||
val entry = zipInputStream.nextEntry ?: break
|
||||
val unixStyleRelativePath = entry.name
|
||||
if (entryFilter == null || entryFilter(unixStyleRelativePath, entry.isDirectory)) {
|
||||
relativePathsToContents.add(unixStyleRelativePath to zipInputStream.readBytes())
|
||||
relativePathsToContents.computeIfAbsent(unixStyleRelativePath) { zipInputStream.readBytes() }
|
||||
}
|
||||
}
|
||||
}
|
||||
return relativePathsToContents.sortedBy { it.first }.toMap(LinkedHashMap())
|
||||
return relativePathsToContents.toSortedMap().toMap(LinkedHashMap())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-3
@@ -22,11 +22,9 @@ abstract class ClasspathEntrySnapshotTransform : TransformAction<TransformParame
|
||||
|
||||
override fun transform(outputs: TransformOutputs) {
|
||||
val classpathEntry = inputArtifact.get().asFile
|
||||
val snapshotFile = outputs.file(CLASSPATH_ENTRY_SNAPSHOT_FILE_NAME)
|
||||
val snapshotFile = outputs.file(classpathEntry.name.replace('.', '_') + "-snapshot.bin")
|
||||
|
||||
val snapshot = ClasspathEntrySnapshotter.snapshot(classpathEntry)
|
||||
ClasspathEntrySnapshotSerializer.save(snapshotFile, snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
const val CLASSPATH_ENTRY_SNAPSHOT_FILE_NAME = "classpath-entry-snapshot.bin"
|
||||
|
||||
+46
-35
@@ -42,7 +42,6 @@ import org.jetbrains.kotlin.gradle.internal.*
|
||||
import org.jetbrains.kotlin.gradle.internal.tasks.TaskConfigurator
|
||||
import org.jetbrains.kotlin.gradle.internal.tasks.TaskWithLocalState
|
||||
import org.jetbrains.kotlin.gradle.internal.tasks.allOutputFiles
|
||||
import org.jetbrains.kotlin.gradle.internal.transforms.CLASSPATH_ENTRY_SNAPSHOT_FILE_NAME
|
||||
import org.jetbrains.kotlin.gradle.internal.transforms.ClasspathEntrySnapshotTransform
|
||||
import org.jetbrains.kotlin.gradle.logging.GradleKotlinLogger
|
||||
import org.jetbrains.kotlin.gradle.logging.GradlePrintingMessageCollector
|
||||
@@ -62,6 +61,7 @@ import org.jetbrains.kotlin.statistics.metrics.BooleanMetrics
|
||||
import org.jetbrains.kotlin.utils.JsLibraryUtils
|
||||
import org.jetbrains.kotlin.utils.addToStdlib.cast
|
||||
import java.io.File
|
||||
import java.util.LinkedHashSet
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -457,7 +457,8 @@ open class KotlinCompileArgumentsProvider<T : AbstractKotlinCompile<out CommonCo
|
||||
val isMultiplatform: Boolean = taskProvider.multiPlatformEnabled.get()
|
||||
private val pluginData = taskProvider.kotlinPluginData?.orNull
|
||||
val pluginClasspath: FileCollection = listOfNotNull(taskProvider.pluginClasspath, pluginData?.classpath).reduce(FileCollection::plus)
|
||||
val pluginOptions: CompilerPluginOptions = listOfNotNull(taskProvider.pluginOptions, pluginData?.options).reduce(CompilerPluginOptions::plus)
|
||||
val pluginOptions: CompilerPluginOptions =
|
||||
listOfNotNull(taskProvider.pluginOptions, pluginData?.options).reduce(CompilerPluginOptions::plus)
|
||||
}
|
||||
|
||||
class KotlinJvmCompilerArgumentsProvider
|
||||
@@ -735,7 +736,7 @@ abstract class KotlinCompile @Inject constructor(
|
||||
|
||||
with(classpathSnapshotProperties) {
|
||||
if (isIncrementalCompilationEnabled() && useClasspathSnapshot.get()) {
|
||||
copyClasspathSnapshotFilesToDir(classpathSnapshot.files.toList(), classpathSnapshotDir.get().asFile)
|
||||
copyCurrentClasspathEntrySnapshotFiles()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -825,44 +826,54 @@ abstract class KotlinCompile @Inject constructor(
|
||||
return super.source(*sources)
|
||||
}
|
||||
|
||||
private fun getClasspathChanges(@Suppress("UNUSED_PARAMETER") inputChanges: InputChanges): ClasspathChanges {
|
||||
val currentSnapshotFiles = classpathSnapshotProperties.classpathSnapshot.files.toList()
|
||||
val previousSnapshotFiles = getClasspathSnapshotFilesInDir(classpathSnapshotProperties.classpathSnapshotDir.get().asFile)
|
||||
|
||||
// TODO: Compute changes for the changed snapshot files only, ignoring unchanged ones.
|
||||
// We'll need to be careful when the classpath contains duplicate classes, as we'll need to look at the entire classpath in order to
|
||||
// detect them.
|
||||
val currentSnapshot = ClasspathSnapshotSerializer.load(currentSnapshotFiles)
|
||||
val previousSnapshot = ClasspathSnapshotSerializer.load(previousSnapshotFiles)
|
||||
|
||||
return ClasspathChangesComputer.compute(currentSnapshot, previousSnapshot)
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies classpath snapshot files to the given directory.
|
||||
*
|
||||
* To preserve their order, we put them in subdirectories with names being their indices in the original list, as shown below:
|
||||
* classpathSnapshotDir/0/snapshotFileName
|
||||
* classpathSnapshotDir/1/snapshotFileName
|
||||
* ...
|
||||
* classpathSnapshotDir/N-1/snapshotFileName
|
||||
*/
|
||||
private fun copyClasspathSnapshotFilesToDir(classpathSnapshotFiles: List<File>, classpathSnapshotDir: File) {
|
||||
classpathSnapshotDir.deleteRecursively()
|
||||
classpathSnapshotDir.mkdirs()
|
||||
for ((index, file) in classpathSnapshotFiles.withIndex()) {
|
||||
file.copyTo(File("$classpathSnapshotDir/$index/$CLASSPATH_ENTRY_SNAPSHOT_FILE_NAME"), overwrite = true)
|
||||
private fun getClasspathChanges(inputChanges: InputChanges): ClasspathChanges {
|
||||
val fileChanges = inputChanges.getFileChanges(classpathSnapshotProperties.classpathSnapshot).toList()
|
||||
return if (fileChanges.isEmpty()) {
|
||||
ClasspathChanges.Available(LinkedHashSet(), LinkedHashSet())
|
||||
} else {
|
||||
val currentClasspathEntrySnapshotFiles = classpathSnapshotProperties.classpathSnapshot.files.toList()
|
||||
val changedCurrentFiles = fileChanges
|
||||
.filter { it.changeType == ChangeType.ADDED || it.changeType == ChangeType.MODIFIED }
|
||||
.map { it.file }.toSet()
|
||||
ClasspathChangesComputer.compute(
|
||||
currentClasspathEntrySnapshotFiles = currentClasspathEntrySnapshotFiles,
|
||||
previousClasspathEntrySnapshotFiles = getPreviousClasspathEntrySnapshotFiles(),
|
||||
unchangedCurrentClasspathEntrySnapshotFiles = currentClasspathEntrySnapshotFiles.filter { it !in changedCurrentFiles }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all classpath snapshot files in the given directory, sorted by their original indices (the subdirectories' names).
|
||||
* Copies classpath entry snapshot files of the current build to `classpathSnapshotDir`. They will be used in the next build (see
|
||||
* [getPreviousClasspathEntrySnapshotFiles]).
|
||||
*
|
||||
* See [copyClasspathSnapshotFilesToDir] for the structure of the classpath snapshot directory.
|
||||
* To preserve their order, we put them in subdirectories with names being their indices, as shown below:
|
||||
* classpathSnapshotDir/0/a-snapshot.bin
|
||||
* classpathSnapshotDir/1/b-snapshot.bin
|
||||
* ...
|
||||
* classpathSnapshotDir/N-1/z-snapshot.bin
|
||||
*/
|
||||
private fun getClasspathSnapshotFilesInDir(classpathSnapshotDir: File): List<File> {
|
||||
val subDirs = classpathSnapshotDir.listFiles() ?: return emptyList()
|
||||
return subDirs.toList().sortedBy { it.name.toInt() }.map { File(it, CLASSPATH_ENTRY_SNAPSHOT_FILE_NAME) }
|
||||
private fun copyCurrentClasspathEntrySnapshotFiles() {
|
||||
val snapshotFiles = classpathSnapshotProperties.classpathSnapshot.files.toList()
|
||||
val classpathSnapshotDir = classpathSnapshotProperties.classpathSnapshotDir.get().asFile
|
||||
classpathSnapshotDir.deleteRecursively()
|
||||
classpathSnapshotDir.mkdirs()
|
||||
snapshotFiles.forEachIndexed { index, snapshotFile ->
|
||||
val targetFile = File("$classpathSnapshotDir/$index/${snapshotFile.name}")
|
||||
snapshotFile.copyTo(targetFile, overwrite = true)
|
||||
// Preserve timestamp to find out unchanged files later (see `ClasspathChangesComputer.alignUnchangedSnapshotFiles`)
|
||||
targetFile.setLastModified(snapshotFile.lastModified())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns classpath entry snapshot files of the previous build, stored in `classpathSnapshotDir` (see
|
||||
* [copyCurrentClasspathEntrySnapshotFiles]).
|
||||
*/
|
||||
private fun getPreviousClasspathEntrySnapshotFiles(): List<File> {
|
||||
val classpathSnapshotDir = classpathSnapshotProperties.classpathSnapshotDir.get().asFile
|
||||
val subDirs = classpathSnapshotDir.listFiles()!!.sortedBy { it.name.toInt() }
|
||||
return subDirs.map { it.listFiles()!!.single() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+157
-67
@@ -5,65 +5,19 @@
|
||||
|
||||
package org.jetbrains.kotlin.gradle.incremental
|
||||
|
||||
import org.jetbrains.kotlin.gradle.incremental.ClasspathSnapshotTestCommon.*
|
||||
import org.jetbrains.kotlin.gradle.incremental.ClasspathSnapshotTestCommon.Util.snapshot
|
||||
import org.jetbrains.kotlin.incremental.ClasspathChanges
|
||||
import org.jetbrains.kotlin.incremental.LookupSymbol
|
||||
import org.jetbrains.kotlin.name.ClassId
|
||||
import org.jetbrains.kotlin.name.FqName
|
||||
import org.jetbrains.kotlin.resolve.sam.SAM_LOOKUP_NAME
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.io.File
|
||||
|
||||
abstract class ClasspathChangesComputerTest : ClasspathSnapshotTestCommon() {
|
||||
|
||||
protected abstract val testSourceFile: ChangeableTestSourceFile
|
||||
|
||||
private lateinit var originalSnapshot: ClassSnapshot
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
originalSnapshot = testSourceFile.compileAndSnapshot()
|
||||
}
|
||||
|
||||
/** Adapted version of [ClasspathChanges.Available] for readability in this test. */
|
||||
private data class Changes(private val lookupSymbols: Set<LookupSymbol>, private val fqNames: Set<FqName>)
|
||||
|
||||
private fun computeClassChanges(current: ClassSnapshot, previous: ClassSnapshot): Changes {
|
||||
val classChanges =
|
||||
ClasspathChangesComputer.compute(current.toClasspathSnapshot(), previous.toClasspathSnapshot()) as ClasspathChanges.Available
|
||||
return Changes(HashSet(classChanges.lookupSymbols), HashSet(classChanges.fqNames))
|
||||
}
|
||||
|
||||
private fun ClassSnapshot.toClasspathSnapshot(): ClasspathSnapshot {
|
||||
return ClasspathSnapshot(
|
||||
classpathEntrySnapshots = listOf(
|
||||
ClasspathEntrySnapshot(
|
||||
LinkedHashMap<String, ClassSnapshot>(1).also {
|
||||
it[getClassId()!!.getUnixStyleRelativePath()] = this
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun ClassSnapshot.getClassId(): ClassId? {
|
||||
return when (this) {
|
||||
is KotlinClassSnapshot -> classInfo.classId
|
||||
is RegularJavaClassSnapshot -> serializedJavaClass.classId
|
||||
is EmptyJavaClassSnapshot, is ContentHashJavaClassSnapshot -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun ClassId.getUnixStyleRelativePath() = asString().replace('.', '$') + ".class"
|
||||
|
||||
/**
|
||||
* Returns the [FqName] of the class in this source file (e.g., "com/example/Foo$Bar.kt" or
|
||||
* "com/example/Foo$Bar.java" has [FqName] "com.example.Foo.Bar").
|
||||
*
|
||||
* This source file must contain only 1 class.
|
||||
*/
|
||||
private fun SourceFile.getClassFqName() =
|
||||
FqName(unixStyleRelativePath.substringBeforeLast('.').replace('/', '.').replace('$', '.'))
|
||||
|
||||
// TODO Add more test cases:
|
||||
// - private/non-private fields
|
||||
// - inline functions
|
||||
@@ -71,37 +25,173 @@ abstract class ClasspathChangesComputerTest : ClasspathSnapshotTestCommon() {
|
||||
// - adding an annotation
|
||||
|
||||
@Test
|
||||
fun testComputeClassChanges_changedPublicMethodSignature() {
|
||||
val updatedSnapshot = testSourceFile.changePublicMethodSignature().compileAndSnapshot()
|
||||
val classChanges = computeClassChanges(updatedSnapshot, originalSnapshot)
|
||||
abstract fun testSingleClass_changePublicMethodSignature()
|
||||
|
||||
@Test
|
||||
abstract fun testSingleClass_changeMethodImplementation()
|
||||
|
||||
@Test
|
||||
abstract fun testMultipleClasses()
|
||||
}
|
||||
|
||||
class KotlinClassesClasspathChangesComputerTest : ClasspathChangesComputerTest() {
|
||||
|
||||
@Test
|
||||
override fun testSingleClass_changePublicMethodSignature() {
|
||||
val sourceFile = SimpleKotlinClass(tmpDir)
|
||||
val previousSnapshot = sourceFile.compileAndSnapshot()
|
||||
val currentSnapshot = sourceFile.changePublicMethodSignature().compileAndSnapshot()
|
||||
val changes = ClasspathChangesComputer.compute(listOf(currentSnapshot), listOf(previousSnapshot)).normalize()
|
||||
|
||||
val testClassFqName = testSourceFile.sourceFile.getClassFqName()
|
||||
assertEquals(
|
||||
Changes(
|
||||
lookupSymbols = setOf(
|
||||
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = testClassFqName.asString()),
|
||||
LookupSymbol(name = "changedPublicMethod", scope = testClassFqName.asString()),
|
||||
LookupSymbol(name = "publicMethod", scope = testClassFqName.asString())
|
||||
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.SimpleKotlinClass"),
|
||||
LookupSymbol(name = "changedPublicMethod", scope = "com.example.SimpleKotlinClass"),
|
||||
LookupSymbol(name = "publicMethod", scope = "com.example.SimpleKotlinClass")
|
||||
),
|
||||
fqNames = setOf(
|
||||
FqName("com.example.SimpleKotlinClass")
|
||||
),
|
||||
fqNames = setOf(testClassFqName),
|
||||
),
|
||||
classChanges
|
||||
changes
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testComputeClassChanges_changedMethodImplementation() {
|
||||
val updatedSnapshot = testSourceFile.changeMethodImplementation().compileAndSnapshot()
|
||||
val classChanges = computeClassChanges(updatedSnapshot, originalSnapshot)
|
||||
override fun testSingleClass_changeMethodImplementation() {
|
||||
val sourceFile = SimpleKotlinClass(tmpDir)
|
||||
val previousSnapshot = sourceFile.compileAndSnapshot()
|
||||
val currentSnapshot = sourceFile.changeMethodImplementation().compileAndSnapshot()
|
||||
val changes = ClasspathChangesComputer.compute(listOf(currentSnapshot), listOf(previousSnapshot)).normalize()
|
||||
|
||||
assertEquals(Changes(emptySet(), emptySet()), classChanges)
|
||||
assertEquals(Changes(emptySet(), emptySet()), changes)
|
||||
}
|
||||
|
||||
@Test
|
||||
override fun testMultipleClasses() {
|
||||
val classpathSourceDir = File(testDataDir, "../ClasspathChangesComputerTest/testMultipleClasses/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()
|
||||
|
||||
assertEquals(
|
||||
Changes(
|
||||
lookupSymbols = setOf(
|
||||
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.B"),
|
||||
LookupSymbol(name = "b2", scope = "com.example.B"),
|
||||
LookupSymbol(name = "b3", scope = "com.example.B"),
|
||||
LookupSymbol(name = "b4", scope = "com.example.B"),
|
||||
LookupSymbol(name = "C", scope = "com.example"),
|
||||
LookupSymbol(name = "D", scope = "com.example")
|
||||
),
|
||||
fqNames = setOf(
|
||||
FqName("com.example.B"),
|
||||
FqName("com.example.C"),
|
||||
FqName("com.example.D")
|
||||
)
|
||||
),
|
||||
changes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class KotlinClassesClasspathChangesComputerTest : ClasspathChangesComputerTest() {
|
||||
override val testSourceFile = SimpleKotlinClass(tmpDir)
|
||||
class JavaClassesClasspathChangesComputerTest : ClasspathChangesComputerTest() {
|
||||
|
||||
@Test
|
||||
override fun testSingleClass_changePublicMethodSignature() {
|
||||
val sourceFile = SimpleJavaClass(tmpDir)
|
||||
val previousSnapshot = sourceFile.compileAndSnapshot()
|
||||
val currentSnapshot = sourceFile.changePublicMethodSignature().compileAndSnapshot()
|
||||
val changes = ClasspathChangesComputer.compute(listOf(currentSnapshot), listOf(previousSnapshot)).normalize()
|
||||
|
||||
assertEquals(
|
||||
Changes(
|
||||
lookupSymbols = setOf(
|
||||
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.SimpleJavaClass"),
|
||||
LookupSymbol(name = "changedPublicMethod", scope = "com.example.SimpleJavaClass"),
|
||||
LookupSymbol(name = "publicMethod", scope = "com.example.SimpleJavaClass")
|
||||
),
|
||||
fqNames = setOf(
|
||||
FqName("com.example.SimpleJavaClass")
|
||||
),
|
||||
),
|
||||
changes
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
override fun testSingleClass_changeMethodImplementation() {
|
||||
val sourceFile = SimpleJavaClass(tmpDir)
|
||||
val previousSnapshot = sourceFile.compileAndSnapshot()
|
||||
val currentSnapshot = sourceFile.changeMethodImplementation().compileAndSnapshot()
|
||||
val changes = ClasspathChangesComputer.compute(listOf(currentSnapshot), listOf(previousSnapshot)).normalize()
|
||||
|
||||
assertEquals(Changes(emptySet(), emptySet()), changes)
|
||||
}
|
||||
|
||||
@Test
|
||||
override fun testMultipleClasses() {
|
||||
val classpathSourceDir = File(testDataDir, "../ClasspathChangesComputerTest/testMultipleClasses/src/java").canonicalFile
|
||||
val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir)
|
||||
val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir)
|
||||
val changes = ClasspathChangesComputer.compute(currentSnapshot, previousSnapshot).normalize()
|
||||
|
||||
assertEquals(
|
||||
Changes(
|
||||
lookupSymbols = setOf(
|
||||
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.B"),
|
||||
LookupSymbol(name = "b2", scope = "com.example.B"),
|
||||
LookupSymbol(name = "b3", scope = "com.example.B"),
|
||||
LookupSymbol(name = "b4", scope = "com.example.B"),
|
||||
LookupSymbol(name = "C", scope = "com.example"),
|
||||
LookupSymbol(name = "D", scope = "com.example"),
|
||||
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.D"),
|
||||
LookupSymbol(name = "<init>", scope = "com.example.D"),
|
||||
LookupSymbol(name = "d", scope = "com.example.D")
|
||||
),
|
||||
fqNames = setOf(
|
||||
FqName("com.example.B"),
|
||||
FqName("com.example.C"),
|
||||
FqName("com.example.D")
|
||||
)
|
||||
),
|
||||
changes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class JavaClassesClasspathChangesComputerTest : ClasspathChangesComputerTest() {
|
||||
override val testSourceFile = SimpleJavaClass(tmpDir)
|
||||
private fun snapshotClasspath(classpathSourceDir: File, tmpDir: TemporaryFolder): ClasspathSnapshot {
|
||||
val classpathEntrySnapshots = classpathSourceDir.listFiles()!!.map { classpathEntrySourceDir ->
|
||||
val relativePathsInDir = classpathEntrySourceDir.walk()
|
||||
.filter { it.extension == "kt" || it.extension == "java" }
|
||||
.map { file -> file.toRelativeString(classpathEntrySourceDir) }
|
||||
.sortedBy { it }
|
||||
val sourceFiles = relativePathsInDir.map { relativePath ->
|
||||
if (relativePath.endsWith(".kt")) {
|
||||
KotlinSourceFile(
|
||||
classpathEntrySourceDir, relativePath,
|
||||
preCompiledClassFile = ClassFile(
|
||||
File(classpathEntrySourceDir.path.replace("src/kotlin", "classes/kotlin")),
|
||||
relativePath.replace(".kt", ".class")
|
||||
)
|
||||
)
|
||||
} else {
|
||||
SourceFile(classpathEntrySourceDir, relativePath)
|
||||
}
|
||||
}
|
||||
val classFiles = sourceFiles.map { TestSourceFile(it, tmpDir).compile() }
|
||||
ClasspathEntrySnapshot(
|
||||
classSnapshots = classFiles.map { it.unixStyleRelativePath to it.snapshot() }.toMap(LinkedHashMap())
|
||||
)
|
||||
}
|
||||
return ClasspathSnapshot(classpathEntrySnapshots)
|
||||
}
|
||||
|
||||
/** Adapted version of [ClasspathChanges.Available] for readability in this test. */
|
||||
private data class Changes(private val lookupSymbols: Set<LookupSymbol>, private val fqNames: Set<FqName>)
|
||||
|
||||
private fun ClasspathChanges.normalize(): Changes {
|
||||
this as ClasspathChanges.Available
|
||||
return Changes(lookupSymbols, fqNames)
|
||||
}
|
||||
|
||||
+26
-16
@@ -26,7 +26,7 @@ abstract class ClasspathSnapshotTestCommon {
|
||||
private val gson by lazy { GsonBuilder().setPrettyPrinting().create() }
|
||||
protected fun Any.toGson(): String = gson.toJson(this)
|
||||
|
||||
class SourceFile(val baseDir: File, relativePath: String) {
|
||||
open class SourceFile(val baseDir: File, relativePath: String) {
|
||||
val unixStyleRelativePath: String
|
||||
|
||||
init {
|
||||
@@ -36,14 +36,18 @@ abstract class ClasspathSnapshotTestCommon {
|
||||
fun asFile() = File(baseDir, unixStyleRelativePath)
|
||||
}
|
||||
|
||||
/** Same as [SourceFile] but with a [TemporaryFolder] to store the results of operations on the [SourceFile]. */
|
||||
open class TestSourceFile(val sourceFile: SourceFile, protected val tmpDir: TemporaryFolder) {
|
||||
class KotlinSourceFile(baseDir: File, relativePath: String, val preCompiledClassFile: ClassFile) : SourceFile(baseDir, relativePath)
|
||||
|
||||
fun replace(oldValue: String, newValue: String, newBaseDir: File? = null): TestSourceFile {
|
||||
/** 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 = SourceFile(newBaseDir ?: tmpDir.newFolder(), sourceFile.unixStyleRelativePath)
|
||||
val newSourceFile =
|
||||
preCompiledKotlinClassFile?.let { KotlinSourceFile(tmpDir.newFolder(), sourceFile.unixStyleRelativePath, it) }
|
||||
?: SourceFile(tmpDir.newFolder(), sourceFile.unixStyleRelativePath)
|
||||
newSourceFile.asFile().parentFile.mkdirs()
|
||||
newSourceFile.asFile().writeText(fileContents.replace(oldValue, newValue))
|
||||
return TestSourceFile(newSourceFile, tmpDir)
|
||||
@@ -69,14 +73,12 @@ abstract class ClasspathSnapshotTestCommon {
|
||||
}
|
||||
|
||||
private fun compileKotlin(): List<ClassFile> {
|
||||
// TODO: Call Kotlin compiler to generate classes (see https://github.com/JetBrains/kotlin/pull/4512#discussion_r679432232)
|
||||
// Currently, we use the precompiled classes in the test data.
|
||||
return listOf(
|
||||
ClassFile(
|
||||
File(testDataDir.path + "/classes/" + sourceFile.baseDir.name),
|
||||
sourceFile.unixStyleRelativePath.replace(".kt", ".class")
|
||||
)
|
||||
)
|
||||
// TODO: Call Kotlin compiler to generate classes, for example:
|
||||
// org.jetbrains.kotlin.test.MockLibraryUtil.compileKotlin(sourceFile.asFile().path, sourceFile.preCompiledClassFile.classRoot)
|
||||
// However, currently it is not possible (see https://github.com/JetBrains/kotlin/pull/4512#discussion_r679432232), so we use
|
||||
// the precompiled classes in the test data instead.
|
||||
sourceFile as KotlinSourceFile
|
||||
return listOf(sourceFile.preCompiledClassFile)
|
||||
}
|
||||
|
||||
private fun compileJava(): List<ClassFile> {
|
||||
@@ -125,17 +127,25 @@ abstract class ClasspathSnapshotTestCommon {
|
||||
}
|
||||
|
||||
class SimpleKotlinClass(tmpDir: TemporaryFolder) : ChangeableTestSourceFile(
|
||||
SourceFile(File(testDataDir, "src/original"), "com/example/SimpleKotlinClass.kt"), tmpDir
|
||||
KotlinSourceFile(
|
||||
baseDir = File(testDataDir, "src/original"),
|
||||
relativePath = "com/example/SimpleKotlinClass.kt",
|
||||
preCompiledClassFile = ClassFile(File(testDataDir, "classes/original"), "com/example/SimpleKotlinClass.class")
|
||||
), tmpDir
|
||||
) {
|
||||
|
||||
override fun changePublicMethodSignature() = replace(
|
||||
"publicMethod()", "changedPublicMethod()",
|
||||
newBaseDir = File(tmpDir.newFolder(), "changedPublicMethodSignature")
|
||||
preCompiledKotlinClassFile = ClassFile(
|
||||
File(testDataDir, "classes/changedPublicMethodSignature"), "com/example/SimpleKotlinClass.class"
|
||||
)
|
||||
)
|
||||
|
||||
override fun changeMethodImplementation() = replace(
|
||||
"I'm in a public method", "This method implementation has changed!",
|
||||
newBaseDir = File(tmpDir.newFolder(), "changedMethodImplementation")
|
||||
preCompiledKotlinClassFile = ClassFile(
|
||||
File(testDataDir, "classes/changedMethodImplementation"), "com/example/SimpleKotlinClass.class"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+6
@@ -0,0 +1,6 @@
|
||||
package com.example;
|
||||
|
||||
// Unchanged class
|
||||
public class A {
|
||||
public int a = 0;
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.example;
|
||||
|
||||
// Modified class
|
||||
public class B {
|
||||
public int b1 = 0; // Unchanged field
|
||||
public String b2 = ""; // Modified field
|
||||
//public int b3 = 0; // Removed field
|
||||
public int b4 = 0; // Added field
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.example;
|
||||
|
||||
// Added class
|
||||
public class D {
|
||||
public int d = 0;
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package com.example;
|
||||
|
||||
public class A {
|
||||
public int a = 0;
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.example;
|
||||
|
||||
public class B {
|
||||
public int b1 = 0;
|
||||
public int b2 = 0;
|
||||
public int b3 = 0;
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.example;
|
||||
|
||||
// Will be removed
|
||||
public class C {
|
||||
public int c = 0;
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.example
|
||||
|
||||
// Unchanged class
|
||||
class A {
|
||||
val a: Int = 0
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.example
|
||||
|
||||
// Modified class
|
||||
public class B {
|
||||
val b1: Int = 0 // Unchanged field
|
||||
val b2: String = "" // Modified field
|
||||
//val b3: Int = 0 // Removed field
|
||||
val b4: Int = 0 // Added field
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.example
|
||||
|
||||
// Added class
|
||||
public class D {
|
||||
val d: Int = 0
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package com.example
|
||||
|
||||
class A {
|
||||
val a: Int = 0
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.example
|
||||
|
||||
public class B {
|
||||
val b1: Int = 0
|
||||
val b2: Int = 0
|
||||
val b3: Int = 0
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.example
|
||||
|
||||
// Will be removed
|
||||
public class C {
|
||||
val c: Int = 0
|
||||
}
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Reference in New Issue
Block a user