KT-45777: Allow 2 levels of granularity when tracking changes

1. CLASS_LEVEL: allows tracking whether a .class file has changed
     without tracking what specific parts of the .class file (e.g.,
     fields or methods) have changed.

  2. CLASS_MEMBER_LEVEL: allows tracking not only whether a .class file
     has changed but also what specific parts of the .class file (e.g.,
     fields or methods) have changed.

 The idea is that for better performance we will use CLASS_LEVEL for
 classpath entries that are usually unchanged, and CLASS_MEMBER_LEVEL
 for classpath entries that are frequently changed. We'll work out the
 specifics in a following commit after some measurements.

Support running kotlinc on Windows in ClasspathSnapshotTestCommon
Also add tests for different Kotlin class kinds.
Add unit tests for CLASS_LEVEL snapshotting and diffing

Test: Updated ClasspathSnapshotterTest + ClasspathChangesComputerTest
Add ClasspathChangesComputerTest.testMixedClassSnapshotGranularities
This commit is contained in:
Hung Nguyen
2022-01-17 19:06:04 +00:00
committed by nataliya.valtman
parent 05457c291d
commit d2193f3873
76 changed files with 1170 additions and 566 deletions
@@ -35,33 +35,33 @@ fun File.isClassFile(): Boolean =
*/
fun File.cleanDirectoryContents() {
when {
isDirectory -> listFiles()!!.forEach { it.forceDeleteRecursively() }
isDirectory -> listFiles()!!.forEach { it.deleteRecursivelyOrThrow() }
isFile -> error("File.cleanDirectoryContents does not accept a regular file: $path")
else -> forceMkdirs()
else -> mkdirsOrThrow()
}
}
/** Deletes this file or directory recursively (if it exists). */
fun File.forceDeleteRecursively() {
/** Deletes this file or directory recursively (if it exists), throwing an exception if the deletion failed. */
fun File.deleteRecursivelyOrThrow() {
if (!deleteRecursively()) {
throw IOException("Could not delete '$path'")
}
}
/**
* Creates this directory (if it does not yet exist).
*
* If this is a regular file, this method will throw an exception.
* Creates this directory (if it does not yet exist), throwing an exception if the directiory creation failed or if a regular file already
* exists at this path.
*/
@Suppress("SpellCheckingInspection")
fun File.forceMkdirs() {
fun File.mkdirsOrThrow() {
when {
this.isDirectory -> { /* Do nothing */ }
this.isFile -> error("File.forceMkdirs does not accept a regular file: $path")
isDirectory -> Unit
isFile -> error("A regular file already exists at this path: $path")
else -> {
// Note that if the directory already exists, mkdirs() will return `false`, but here we ensure that the directory does not exist
// before calling mkdirs(), so it's safe to check the returned result of mkdirs() below.
if (!mkdirs()) {
// Note that `mkdirs()` returns `false` if the directory already exists (even though we have checked that the directory did not
// exist earlier, it might just have been created by some other thread). Therefore, we need to check if the directory exists
// again when `mkdirs()` returns `false`.
if (!mkdirs() && !isDirectory) {
throw IOException("Could not create directory '$path'")
}
}
@@ -346,7 +346,7 @@ object ByteArrayExternalizer : DataExternalizer<ByteArray> {
}
}
open class GenericCollectionExternalizer<T, C : Collection<T>>(
abstract class GenericCollectionExternalizer<T, C : Collection<T>>(
private val elementExternalizer: DataExternalizer<T>,
private val newCollection: (size: Int) -> MutableCollection<T>
) : DataExternalizer<C> {
@@ -364,6 +364,9 @@ open class GenericCollectionExternalizer<T, C : Collection<T>>(
repeat(size) {
collection.add(elementExternalizer.read(input))
}
// We want `collection` to be both a mutable collection (so we can add elements to it as done above) and a type that can be safely
// converted to type `C` (to be used as the returned value of this method). However, there is no type-safe way to express that, so
// we have to use this unsafe cast.
@Suppress("UNCHECKED_CAST")
return collection as C
}
@@ -375,12 +378,13 @@ class ListExternalizer<T>(elementExternalizer: DataExternalizer<T>) :
class SetExternalizer<T>(elementExternalizer: DataExternalizer<T>) :
GenericCollectionExternalizer<T, Set<T>>(elementExternalizer, { size -> LinkedHashSet(size) })
class LinkedHashMapExternalizer<K, V>(
open class MapExternalizer<K, V, M : Map<K, V>>(
private val keyExternalizer: DataExternalizer<K>,
private val valueExternalizer: DataExternalizer<V>
) : DataExternalizer<LinkedHashMap<K, V>> {
private val valueExternalizer: DataExternalizer<V>,
private val newMap: (size: Int) -> MutableMap<K, V> = { size -> LinkedHashMap(size) }
) : DataExternalizer<M> {
override fun save(output: DataOutput, map: LinkedHashMap<K, V>) {
override fun save(output: DataOutput, map: M) {
output.writeInt(map.size)
for ((key, value) in map) {
keyExternalizer.save(output, key)
@@ -388,14 +392,20 @@ class LinkedHashMapExternalizer<K, V>(
}
}
override fun read(input: DataInput): LinkedHashMap<K, V> {
override fun read(input: DataInput): M {
val size = input.readInt()
val map = LinkedHashMap<K, V>(size)
val map = newMap(size)
repeat(size) {
val key = keyExternalizer.read(input)
val value = valueExternalizer.read(input)
map[key] = value
}
return map
@Suppress("UNCHECKED_CAST")
return map as M
}
}
class LinkedHashMapExternalizer<K, V>(
keyExternalizer: DataExternalizer<K>,
valueExternalizer: DataExternalizer<V>
) : MapExternalizer<K, V, LinkedHashMap<K, V>>(keyExternalizer, valueExternalizer, { size -> LinkedHashMap(size) })