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>) {
|
override fun markDirty(removedAndCompiledSources: Collection<File>) {
|
||||||
for (sourceFile in removedAndCompiledSources) {
|
for (sourceFile in removedAndCompiledSources) {
|
||||||
val classes = sourceToClassesMap[sourceFile]
|
sourceToClassesMap[sourceFile].forEach { className ->
|
||||||
classes.forEach {
|
markDirty(className)
|
||||||
dirtyOutputClassesMap.markDirty(it)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceToClassesMap.clearOutputsForSource(sourceFile)
|
sourceToClassesMap.clearOutputsForSource(sourceFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun markDirty(className: ClassName) {
|
||||||
|
dirtyOutputClassesMap.markDirty(className)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates class storage based on the given class proto.
|
* 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 com.intellij.openapi.util.io.FileUtil
|
||||||
import org.jetbrains.kotlin.incremental.*
|
import org.jetbrains.kotlin.incremental.*
|
||||||
import org.jetbrains.kotlin.incremental.storage.FileToCanonicalPathConverter
|
import org.jetbrains.kotlin.incremental.storage.FileToCanonicalPathConverter
|
||||||
|
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
|
||||||
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.LinkedHashMap
|
import kotlin.collections.LinkedHashMap
|
||||||
|
|
||||||
/** Computes [ClasspathChanges] between two [ClasspathSnapshot]s .*/
|
/** Computes [ClasspathChanges] between two [ClasspathSnapshot]s .*/
|
||||||
object ClasspathChangesComputer {
|
object ClasspathChangesComputer {
|
||||||
|
|
||||||
fun compute(currentClasspathSnapshot: ClasspathSnapshot, previousClasspathSnapshot: ClasspathSnapshot): ClasspathChanges {
|
fun compute(
|
||||||
val currentClassSnapshots = currentClasspathSnapshot.getClassSnapshots()
|
currentClasspathEntrySnapshotFiles: List<File>,
|
||||||
val previousClassSnapshots = previousClasspathSnapshot.getClassSnapshots()
|
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 }
|
if (currentClassSnapshots.any { it is ContentHashJavaClassSnapshot }
|
||||||
|| previousClassSnapshots.any { it is ContentHashJavaClassSnapshot }) {
|
|| previousClassSnapshots.any { it is ContentHashJavaClassSnapshot }) {
|
||||||
return ClasspathChanges.NotAvailable.UnableToCompute
|
return ClasspathChanges.NotAvailable.UnableToCompute
|
||||||
@@ -27,56 +125,83 @@ object ClasspathChangesComputer {
|
|||||||
FileUtil.createTempDirectory(this::class.java.simpleName, "_WorkingDir_${UUID.randomUUID()}", /* deleteOnExit */ true)
|
FileUtil.createTempDirectory(this::class.java.simpleName, "_WorkingDir_${UUID.randomUUID()}", /* deleteOnExit */ true)
|
||||||
val incrementalJvmCache = IncrementalJvmCache(workingDir, /* targetOutputDir */ null, FileToCanonicalPathConverter)
|
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()
|
val unusedChangesCollector = ChangesCollector()
|
||||||
for (previousSnapshot in previousClassSnapshots) {
|
for (previousSnapshot in previousClassSnapshots) {
|
||||||
when (previousSnapshot) {
|
when (previousSnapshot) {
|
||||||
is KotlinClassSnapshot -> incrementalJvmCache.saveClassToCache(
|
is KotlinClassSnapshot -> {
|
||||||
kotlinClassInfo = previousSnapshot.classInfo,
|
incrementalJvmCache.saveClassToCache(
|
||||||
sourceFiles = null,
|
kotlinClassInfo = previousSnapshot.classInfo,
|
||||||
changesCollector = unusedChangesCollector
|
sourceFiles = null,
|
||||||
)
|
changesCollector = unusedChangesCollector
|
||||||
is RegularJavaClassSnapshot -> incrementalJvmCache.saveJavaClassProto(
|
)
|
||||||
source = null,
|
incrementalJvmCache.markDirty(previousSnapshot.classInfo.className)
|
||||||
serializedJavaClass = previousSnapshot.serializedJavaClass,
|
}
|
||||||
collector = unusedChangesCollector
|
is RegularJavaClassSnapshot -> {
|
||||||
)
|
incrementalJvmCache.saveJavaClassProto(
|
||||||
|
source = null,
|
||||||
|
serializedJavaClass = previousSnapshot.serializedJavaClass,
|
||||||
|
collector = unusedChangesCollector
|
||||||
|
)
|
||||||
|
incrementalJvmCache.markDirty(JvmClassName.byClassId(previousSnapshot.serializedJavaClass.classId))
|
||||||
|
}
|
||||||
is EmptyJavaClassSnapshot -> {
|
is EmptyJavaClassSnapshot -> {
|
||||||
// Nothing to process
|
// Nothing to process as these classes don't impact the result.
|
||||||
}
|
}
|
||||||
is ContentHashJavaClassSnapshot -> {
|
is ContentHashJavaClassSnapshot -> {
|
||||||
error("Unexpected type (it should have been handled earlier): ${previousSnapshot.javaClass.name}")
|
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()
|
val changesCollector = ChangesCollector()
|
||||||
for (currentSnapshot in currentClassSnapshots) {
|
for (currentSnapshot in currentClassSnapshots) {
|
||||||
when (currentSnapshot) {
|
when (currentSnapshot) {
|
||||||
is KotlinClassSnapshot -> incrementalJvmCache.saveClassToCache(
|
is KotlinClassSnapshot -> {
|
||||||
kotlinClassInfo = currentSnapshot.classInfo,
|
incrementalJvmCache.saveClassToCache(
|
||||||
sourceFiles = null,
|
kotlinClassInfo = currentSnapshot.classInfo,
|
||||||
changesCollector = changesCollector
|
sourceFiles = null,
|
||||||
)
|
changesCollector = changesCollector
|
||||||
is RegularJavaClassSnapshot -> incrementalJvmCache.saveJavaClassProto(
|
)
|
||||||
source = null,
|
}
|
||||||
serializedJavaClass = currentSnapshot.serializedJavaClass,
|
is RegularJavaClassSnapshot -> {
|
||||||
collector = changesCollector
|
incrementalJvmCache.saveJavaClassProto(
|
||||||
)
|
source = null,
|
||||||
|
serializedJavaClass = currentSnapshot.serializedJavaClass,
|
||||||
|
collector = changesCollector
|
||||||
|
)
|
||||||
|
}
|
||||||
is EmptyJavaClassSnapshot -> {
|
is EmptyJavaClassSnapshot -> {
|
||||||
// Nothing to process
|
// Nothing to process as these classes don't impact the result.
|
||||||
}
|
}
|
||||||
is ContentHashJavaClassSnapshot -> {
|
is ContentHashJavaClassSnapshot -> {
|
||||||
error("Unexpected type (it should have been handled earlier): ${currentSnapshot.javaClass.name}")
|
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)
|
incrementalJvmCache.clearCacheForRemovedClasses(changesCollector)
|
||||||
|
|
||||||
|
// Normalize the changes and clean up
|
||||||
val dirtyData = changesCollector.getDirtyData(listOf(incrementalJvmCache), EmptyICReporter)
|
val dirtyData = changesCollector.getDirtyData(listOf(incrementalJvmCache), EmptyICReporter)
|
||||||
workingDir.deleteRecursively()
|
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.
|
* Returns all [ClassSnapshot]s in this [ClasspathSnapshot].
|
||||||
// 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`
|
* If there are duplicate classes on the classpath, retain only the first one to match the compiler's behavior.
|
||||||
// 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
|
private fun ClasspathSnapshot.getNonDuplicateClassSnapshots(): List<ClassSnapshot> {
|
||||||
// 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`.
|
|
||||||
val classSnapshots = LinkedHashMap<String, ClassSnapshot>(classpathEntrySnapshots.sumOf { it.classSnapshots.size })
|
val classSnapshots = LinkedHashMap<String, ClassSnapshot>(classpathEntrySnapshots.sumOf { it.classSnapshots.size })
|
||||||
for (classpathEntrySnapshot in classpathEntrySnapshots) {
|
for (classpathEntrySnapshot in classpathEntrySnapshots) {
|
||||||
for ((unixStyleRelativePath, classSnapshot) in classpathEntrySnapshot.classSnapshots) {
|
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).
|
* 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
|
* Note: If a jar has duplicate entries after filtering, only the first one is retained.
|
||||||
* will be deterministic).
|
|
||||||
*/
|
*/
|
||||||
fun read(
|
fun read(
|
||||||
directoryOrJar: File,
|
directoryOrJar: File,
|
||||||
@@ -258,31 +257,31 @@ private object DirectoryOrJarContentsReader {
|
|||||||
directory: File,
|
directory: File,
|
||||||
entryFilter: ((unixStyleRelativePath: String, isDirectory: Boolean) -> Boolean)? = null
|
entryFilter: ((unixStyleRelativePath: String, isDirectory: Boolean) -> Boolean)? = null
|
||||||
): LinkedHashMap<String, ByteArray> {
|
): LinkedHashMap<String, ByteArray> {
|
||||||
val relativePathsToContents: MutableList<Pair<String, ByteArray>> = mutableListOf()
|
val relativePathsToContents = mutableMapOf<String, ByteArray>()
|
||||||
directory.walk().forEach { file ->
|
directory.walk().forEach { file ->
|
||||||
val unixStyleRelativePath = file.relativeTo(directory).invariantSeparatorsPath
|
val unixStyleRelativePath = file.relativeTo(directory).invariantSeparatorsPath
|
||||||
if (entryFilter == null || entryFilter(unixStyleRelativePath, file.isDirectory)) {
|
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(
|
private fun readJar(
|
||||||
jarFile: File,
|
jarFile: File,
|
||||||
entryFilter: ((unixStyleRelativePath: String, isDirectory: Boolean) -> Boolean)? = null
|
entryFilter: ((unixStyleRelativePath: String, isDirectory: Boolean) -> Boolean)? = null
|
||||||
): LinkedHashMap<String, ByteArray> {
|
): LinkedHashMap<String, ByteArray> {
|
||||||
val relativePathsToContents: MutableList<Pair<String, ByteArray>> = mutableListOf()
|
val relativePathsToContents = mutableMapOf<String, ByteArray>()
|
||||||
ZipInputStream(jarFile.inputStream().buffered()).use { zipInputStream ->
|
ZipInputStream(jarFile.inputStream().buffered()).use { zipInputStream ->
|
||||||
while (true) {
|
while (true) {
|
||||||
val entry = zipInputStream.nextEntry ?: break
|
val entry = zipInputStream.nextEntry ?: break
|
||||||
val unixStyleRelativePath = entry.name
|
val unixStyleRelativePath = entry.name
|
||||||
if (entryFilter == null || entryFilter(unixStyleRelativePath, entry.isDirectory)) {
|
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) {
|
override fun transform(outputs: TransformOutputs) {
|
||||||
val classpathEntry = inputArtifact.get().asFile
|
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)
|
val snapshot = ClasspathEntrySnapshotter.snapshot(classpathEntry)
|
||||||
ClasspathEntrySnapshotSerializer.save(snapshotFile, snapshot)
|
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.TaskConfigurator
|
||||||
import org.jetbrains.kotlin.gradle.internal.tasks.TaskWithLocalState
|
import org.jetbrains.kotlin.gradle.internal.tasks.TaskWithLocalState
|
||||||
import org.jetbrains.kotlin.gradle.internal.tasks.allOutputFiles
|
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.internal.transforms.ClasspathEntrySnapshotTransform
|
||||||
import org.jetbrains.kotlin.gradle.logging.GradleKotlinLogger
|
import org.jetbrains.kotlin.gradle.logging.GradleKotlinLogger
|
||||||
import org.jetbrains.kotlin.gradle.logging.GradlePrintingMessageCollector
|
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.JsLibraryUtils
|
||||||
import org.jetbrains.kotlin.utils.addToStdlib.cast
|
import org.jetbrains.kotlin.utils.addToStdlib.cast
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.LinkedHashSet
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -457,7 +457,8 @@ open class KotlinCompileArgumentsProvider<T : AbstractKotlinCompile<out CommonCo
|
|||||||
val isMultiplatform: Boolean = taskProvider.multiPlatformEnabled.get()
|
val isMultiplatform: Boolean = taskProvider.multiPlatformEnabled.get()
|
||||||
private val pluginData = taskProvider.kotlinPluginData?.orNull
|
private val pluginData = taskProvider.kotlinPluginData?.orNull
|
||||||
val pluginClasspath: FileCollection = listOfNotNull(taskProvider.pluginClasspath, pluginData?.classpath).reduce(FileCollection::plus)
|
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
|
class KotlinJvmCompilerArgumentsProvider
|
||||||
@@ -735,7 +736,7 @@ abstract class KotlinCompile @Inject constructor(
|
|||||||
|
|
||||||
with(classpathSnapshotProperties) {
|
with(classpathSnapshotProperties) {
|
||||||
if (isIncrementalCompilationEnabled() && useClasspathSnapshot.get()) {
|
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)
|
return super.source(*sources)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getClasspathChanges(@Suppress("UNUSED_PARAMETER") inputChanges: InputChanges): ClasspathChanges {
|
private fun getClasspathChanges(inputChanges: InputChanges): ClasspathChanges {
|
||||||
val currentSnapshotFiles = classpathSnapshotProperties.classpathSnapshot.files.toList()
|
val fileChanges = inputChanges.getFileChanges(classpathSnapshotProperties.classpathSnapshot).toList()
|
||||||
val previousSnapshotFiles = getClasspathSnapshotFilesInDir(classpathSnapshotProperties.classpathSnapshotDir.get().asFile)
|
return if (fileChanges.isEmpty()) {
|
||||||
|
ClasspathChanges.Available(LinkedHashSet(), LinkedHashSet())
|
||||||
// TODO: Compute changes for the changed snapshot files only, ignoring unchanged ones.
|
} else {
|
||||||
// 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
|
val currentClasspathEntrySnapshotFiles = classpathSnapshotProperties.classpathSnapshot.files.toList()
|
||||||
// detect them.
|
val changedCurrentFiles = fileChanges
|
||||||
val currentSnapshot = ClasspathSnapshotSerializer.load(currentSnapshotFiles)
|
.filter { it.changeType == ChangeType.ADDED || it.changeType == ChangeType.MODIFIED }
|
||||||
val previousSnapshot = ClasspathSnapshotSerializer.load(previousSnapshotFiles)
|
.map { it.file }.toSet()
|
||||||
|
ClasspathChangesComputer.compute(
|
||||||
return ClasspathChangesComputer.compute(currentSnapshot, previousSnapshot)
|
currentClasspathEntrySnapshotFiles = currentClasspathEntrySnapshotFiles,
|
||||||
}
|
previousClasspathEntrySnapshotFiles = getPreviousClasspathEntrySnapshotFiles(),
|
||||||
|
unchangedCurrentClasspathEntrySnapshotFiles = currentClasspathEntrySnapshotFiles.filter { it !in changedCurrentFiles }
|
||||||
/**
|
)
|
||||||
* 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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> {
|
private fun copyCurrentClasspathEntrySnapshotFiles() {
|
||||||
val subDirs = classpathSnapshotDir.listFiles() ?: return emptyList()
|
val snapshotFiles = classpathSnapshotProperties.classpathSnapshot.files.toList()
|
||||||
return subDirs.toList().sortedBy { it.name.toInt() }.map { File(it, CLASSPATH_ENTRY_SNAPSHOT_FILE_NAME) }
|
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
|
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.ClasspathChanges
|
||||||
import org.jetbrains.kotlin.incremental.LookupSymbol
|
import org.jetbrains.kotlin.incremental.LookupSymbol
|
||||||
import org.jetbrains.kotlin.name.ClassId
|
|
||||||
import org.jetbrains.kotlin.name.FqName
|
import org.jetbrains.kotlin.name.FqName
|
||||||
import org.jetbrains.kotlin.resolve.sam.SAM_LOOKUP_NAME
|
import org.jetbrains.kotlin.resolve.sam.SAM_LOOKUP_NAME
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TemporaryFolder
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
abstract class ClasspathChangesComputerTest : ClasspathSnapshotTestCommon() {
|
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:
|
// TODO Add more test cases:
|
||||||
// - private/non-private fields
|
// - private/non-private fields
|
||||||
// - inline functions
|
// - inline functions
|
||||||
@@ -71,37 +25,173 @@ abstract class ClasspathChangesComputerTest : ClasspathSnapshotTestCommon() {
|
|||||||
// - adding an annotation
|
// - adding an annotation
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testComputeClassChanges_changedPublicMethodSignature() {
|
abstract fun testSingleClass_changePublicMethodSignature()
|
||||||
val updatedSnapshot = testSourceFile.changePublicMethodSignature().compileAndSnapshot()
|
|
||||||
val classChanges = computeClassChanges(updatedSnapshot, originalSnapshot)
|
@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(
|
assertEquals(
|
||||||
Changes(
|
Changes(
|
||||||
lookupSymbols = setOf(
|
lookupSymbols = setOf(
|
||||||
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = testClassFqName.asString()),
|
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.SimpleKotlinClass"),
|
||||||
LookupSymbol(name = "changedPublicMethod", scope = testClassFqName.asString()),
|
LookupSymbol(name = "changedPublicMethod", scope = "com.example.SimpleKotlinClass"),
|
||||||
LookupSymbol(name = "publicMethod", scope = testClassFqName.asString())
|
LookupSymbol(name = "publicMethod", scope = "com.example.SimpleKotlinClass")
|
||||||
|
),
|
||||||
|
fqNames = setOf(
|
||||||
|
FqName("com.example.SimpleKotlinClass")
|
||||||
),
|
),
|
||||||
fqNames = setOf(testClassFqName),
|
|
||||||
),
|
),
|
||||||
classChanges
|
changes
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testComputeClassChanges_changedMethodImplementation() {
|
override fun testSingleClass_changeMethodImplementation() {
|
||||||
val updatedSnapshot = testSourceFile.changeMethodImplementation().compileAndSnapshot()
|
val sourceFile = SimpleKotlinClass(tmpDir)
|
||||||
val classChanges = computeClassChanges(updatedSnapshot, originalSnapshot)
|
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() {
|
class JavaClassesClasspathChangesComputerTest : ClasspathChangesComputerTest() {
|
||||||
override val testSourceFile = SimpleKotlinClass(tmpDir)
|
|
||||||
|
@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() {
|
private fun snapshotClasspath(classpathSourceDir: File, tmpDir: TemporaryFolder): ClasspathSnapshot {
|
||||||
override val testSourceFile = SimpleJavaClass(tmpDir)
|
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() }
|
private val gson by lazy { GsonBuilder().setPrettyPrinting().create() }
|
||||||
protected fun Any.toGson(): String = gson.toJson(this)
|
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
|
val unixStyleRelativePath: String
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -36,14 +36,18 @@ abstract class ClasspathSnapshotTestCommon {
|
|||||||
fun asFile() = File(baseDir, unixStyleRelativePath)
|
fun asFile() = File(baseDir, unixStyleRelativePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Same as [SourceFile] but with a [TemporaryFolder] to store the results of operations on the [SourceFile]. */
|
class KotlinSourceFile(baseDir: File, relativePath: String, val preCompiledClassFile: ClassFile) : SourceFile(baseDir, relativePath)
|
||||||
open class TestSourceFile(val sourceFile: SourceFile, protected val tmpDir: TemporaryFolder) {
|
|
||||||
|
|
||||||
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()
|
val fileContents = sourceFile.asFile().readText()
|
||||||
check(fileContents.contains(oldValue)) { "String '$oldValue' not found in file '${sourceFile.asFile().path}'" }
|
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().parentFile.mkdirs()
|
||||||
newSourceFile.asFile().writeText(fileContents.replace(oldValue, newValue))
|
newSourceFile.asFile().writeText(fileContents.replace(oldValue, newValue))
|
||||||
return TestSourceFile(newSourceFile, tmpDir)
|
return TestSourceFile(newSourceFile, tmpDir)
|
||||||
@@ -69,14 +73,12 @@ abstract class ClasspathSnapshotTestCommon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun compileKotlin(): List<ClassFile> {
|
private fun compileKotlin(): List<ClassFile> {
|
||||||
// TODO: Call Kotlin compiler to generate classes (see https://github.com/JetBrains/kotlin/pull/4512#discussion_r679432232)
|
// TODO: Call Kotlin compiler to generate classes, for example:
|
||||||
// Currently, we use the precompiled classes in the test data.
|
// org.jetbrains.kotlin.test.MockLibraryUtil.compileKotlin(sourceFile.asFile().path, sourceFile.preCompiledClassFile.classRoot)
|
||||||
return listOf(
|
// However, currently it is not possible (see https://github.com/JetBrains/kotlin/pull/4512#discussion_r679432232), so we use
|
||||||
ClassFile(
|
// the precompiled classes in the test data instead.
|
||||||
File(testDataDir.path + "/classes/" + sourceFile.baseDir.name),
|
sourceFile as KotlinSourceFile
|
||||||
sourceFile.unixStyleRelativePath.replace(".kt", ".class")
|
return listOf(sourceFile.preCompiledClassFile)
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun compileJava(): List<ClassFile> {
|
private fun compileJava(): List<ClassFile> {
|
||||||
@@ -125,17 +127,25 @@ abstract class ClasspathSnapshotTestCommon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SimpleKotlinClass(tmpDir: TemporaryFolder) : ChangeableTestSourceFile(
|
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(
|
override fun changePublicMethodSignature() = replace(
|
||||||
"publicMethod()", "changedPublicMethod()",
|
"publicMethod()", "changedPublicMethod()",
|
||||||
newBaseDir = File(tmpDir.newFolder(), "changedPublicMethodSignature")
|
preCompiledKotlinClassFile = ClassFile(
|
||||||
|
File(testDataDir, "classes/changedPublicMethodSignature"), "com/example/SimpleKotlinClass.class"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun changeMethodImplementation() = replace(
|
override fun changeMethodImplementation() = replace(
|
||||||
"I'm in a public method", "This method implementation has changed!",
|
"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