diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/DexDiff.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/DexDiff.kt index 678d68a1..c63b1638 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/DexDiff.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/DexDiff.kt @@ -17,8 +17,8 @@ internal class DexDiff(val oldDexes: List, val newDexes: List) : Binar val isMultidex = oldDexes.size > 1 || newDexes.size > 1 val formatVersions = - componentDiff(oldDexes, newDexes) { dex -> - listOf("${dex.filename}: ${dex.formatVersion}") + versionDiff(oldDexes, newDexes) { dex -> + mapOf(dex.filename to dex.formatVersion) } val strings = componentDiff(oldDexes, newDexes) { it.strings } val types = componentDiff(oldDexes, newDexes) { it.types } @@ -122,7 +122,7 @@ internal fun DexDiff.toSummaryTable() = diffuseTable { .renderText() internal fun DexDiff.toDetailReport() = buildString { - appendComponentDiff("FORMAT VERSIONS", formatVersions) + appendVersionDiff("FORMAT VERSIONS", formatVersions) appendComponentDiff("STRINGS", strings) appendComponentDiff("TYPES", types) appendComponentDiff("METHODS", methods) diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/JarsDiff.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/JarsDiff.kt index 026a2b28..b1a8f4e2 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/JarsDiff.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/JarsDiff.kt @@ -19,12 +19,16 @@ internal class JarsDiff( ) { val classes = componentDiff(oldJars, newJars) { it.classes.map(Class::descriptor) } val bytecodeVersions = - componentDiff(oldJars, newJars) { jar -> - jar.classes.map { "${it.descriptor}: ${it.bytecodeVersion}" } + versionDiff(oldJars, newJars) { jar -> + jar.classes.associate { it.descriptor to it.bytecodeVersion } } val kotlinMetadataVersions = - componentDiff(oldJars, newJars) { jar -> - jar.classes.map { "${it.descriptor}: ${it.kotlinMetadataVersion.joinToString(".")}" } + versionDiff(oldJars, newJars) { jar -> + jar.classes + .filter { it.kotlinMetadataVersion.isNotEmpty() } + .associate { + it.descriptor to KotlinMetadataVersion(it.kotlinMetadataVersion.toList()) + } } val methods = componentDiff(oldJars, newJars) { it.members.filterIsInstance() } val declaredMethods = @@ -80,8 +84,8 @@ internal fun JarsDiff.toSummaryTable(name: String) = diffuseTable { internal fun JarsDiff.toDetailReport() = buildString { // TODO appendComponentDiff("STRINGS", strings)? appendComponentDiff("CLASSES", classes) - appendComponentDiff("BYTECODE VERSIONS", bytecodeVersions) - appendComponentDiff("KOTLIN METADATA VERSIONS", kotlinMetadataVersions) + appendVersionDiff("BYTECODE VERSIONS", bytecodeVersions) + appendVersionDiff("KOTLIN METADATA VERSIONS", kotlinMetadataVersions) appendComponentDiff("METHODS", methods) appendComponentDiff("FIELDS", fields) } diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/KotlinMetadataVersion.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/KotlinMetadataVersion.kt new file mode 100644 index 00000000..d4ba2530 --- /dev/null +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/KotlinMetadataVersion.kt @@ -0,0 +1,21 @@ +package com.jakewharton.diffuse.diff + +internal data class KotlinMetadataVersion(private val numbers: List) : + Comparable { + private val major = numbers[0] + private val minor = numbers[1] + private val patch = numbers[2] + + override fun compareTo(other: KotlinMetadataVersion): Int = comparator.compare(this, other) + + override fun toString(): String = numbers.joinToString(".") + + private companion object { + val comparator = + compareBy( + KotlinMetadataVersion::major, + KotlinMetadataVersion::minor, + KotlinMetadataVersion::patch, + ) + } +} diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/VersionDiff.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/VersionDiff.kt new file mode 100644 index 00000000..7e4bad1e --- /dev/null +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/VersionDiff.kt @@ -0,0 +1,110 @@ +package com.jakewharton.diffuse.diff + +import com.jakewharton.diffuse.diffuseTable +import com.jakewharton.diffuse.report.toDiffString +import com.jakewharton.picnic.TextAlignment.MiddleRight +import com.jakewharton.picnic.renderText + +/** + * Diff for versioned items across two sets of containers. + * + * @param versionCounts per-version summary: maps each version to a pair of (`oldCount`, `newCount`) + * where the counts differ, sorted by `version`. + * @param changedItems items that exist in both old and new but changed version, as triples of + * (`item`, `oldVersion`, `newVersion`), sorted by `item`. + */ +internal class VersionDiff, V : Comparable>( + val versionCounts: Map>, + val changedItems: List>, +) { + val changed = versionCounts.isNotEmpty() || changedItems.isNotEmpty() +} + +internal fun , V : Comparable> versionDiff( + oldItems: List, + newItems: List, + selector: (R) -> Map, +): VersionDiff { + val oldVersionMap = buildMap { + for (item in oldItems) { + putAll(selector(item)) + } + } + val newVersionMap = buildMap { + for (item in newItems) { + putAll(selector(item)) + } + } + + // Items present in both with different versions. + val changedItems = buildList { + for ((item, oldVersion) in oldVersionMap) { + val newVersion = newVersionMap[item] + if (newVersion != null && newVersion != oldVersion) { + add(Triple(item, oldVersion, newVersion)) + } + } + } + .sortedBy { it.first } + + // Tally per-version counts across all items in old and new. + val oldCountMap = oldVersionMap.values.groupingBy { it }.eachCount() + val newCountMap = newVersionMap.values.groupingBy { it }.eachCount() + val allVersions = (oldCountMap.keys + newCountMap.keys).toSortedSet() + val versionCounts = buildMap { + for (version in allVersions) { + val oldCount = oldCountMap.getOrDefault(version, 0) + val newCount = newCountMap.getOrDefault(version, 0) + if (oldCount != newCount) { + put(version, oldCount to newCount) + } + } + } + + return VersionDiff(versionCounts, changedItems) +} + +internal fun StringBuilder.appendVersionDiff(name: String, diff: VersionDiff<*, *>) { + if (!diff.changed) return + appendLine() + appendLine("$name:") + appendLine() + + if (diff.versionCounts.isNotEmpty()) { + diffuseTable { + header { + row { + cell("version") + cell("old") + cell("new") + cell("diff") + } + } + + body { + cellStyle { alignment = MiddleRight } + + for ((version, counts) in diff.versionCounts) { + val (oldCount, newCount) = counts + val delta = newCount - oldCount + val net = delta.toDiffString() + val added = delta.coerceAtLeast(0).toDiffString(zeroSign = '+') + val removed = delta.coerceAtMost(0).toDiffString(zeroSign = '-') + row(version, oldCount, newCount, "$net ($added $removed)") + } + } + } + .renderText() + .prependIndent(" ") + .let(::appendLine) + } + + if (diff.changedItems.isNotEmpty()) { + if (diff.versionCounts.isNotEmpty()) { + appendLine() + } + for ((item, oldVersion, newVersion) in diff.changedItems) { + appendLine(" $item: $oldVersion → $newVersion") + } + } +} diff --git a/reports/src/test/kotlin/com/jakewharton/diffuse/DexDiffTest.kt b/reports/src/test/kotlin/com/jakewharton/diffuse/DexDiffTest.kt index e5921d6e..ce00f742 100644 --- a/reports/src/test/kotlin/com/jakewharton/diffuse/DexDiffTest.kt +++ b/reports/src/test/kotlin/com/jakewharton/diffuse/DexDiffTest.kt @@ -2,6 +2,8 @@ package com.jakewharton.diffuse import assertk.assertThat import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isTrue import com.jakewharton.diffuse.diff.DexDiff import com.jakewharton.diffuse.diff.toDetailReport import com.jakewharton.diffuse.format.Dex @@ -20,7 +22,7 @@ class DexDiffTest { newDexes = listOf(Dex("classes.dex", 35)), ) - assertThat(diff.changed).isEqualTo(false) + assertThat(diff.changed).isFalse() assertThat(diff.toDetailReport()).isEqualTo("") } @@ -32,21 +34,19 @@ class DexDiffTest { newDexes = listOf(Dex("classes.dex", 38)), ) - assertThat(diff.changed).isEqualTo(true) + assertThat(diff.changed).isTrue() assertThat(diff.toDetailReport()) .isEqualTo( """ | |FORMAT VERSIONS: | - | old │ new │ diff - | ─────┼─────┼─────────── - | 1 │ 1 │ 0 (+1 -1) - | - | + classes.dex: 38 - | - | - classes.dex: 35 - | + | version │ old │ new │ diff + | ─────────┼─────┼─────┼──────────── + | 35 │ 1 │ 0 │ -1 (+0 -1) + | 38 │ 0 │ 1 │ +1 (+1 -0) + | + | classes.dex: 35 → 38 |""" .trimMargin() ) @@ -60,21 +60,65 @@ class DexDiffTest { newDexes = listOf(Dex("classes.dex", 35), Dex("classes2.dex", 38)), ) - assertThat(diff.changed).isEqualTo(true) + assertThat(diff.changed).isTrue() + assertThat(diff.toDetailReport()) + .isEqualTo( + """ + | + |FORMAT VERSIONS: + | + | version │ old │ new │ diff + | ─────────┼─────┼─────┼──────────── + | 35 │ 2 │ 1 │ -1 (+0 -1) + | 38 │ 0 │ 1 │ +1 (+1 -0) + | + | classes2.dex: 35 → 38 + |""" + .trimMargin() + ) + } + + @Test + fun formatVersionsMultidexAdded() { + val diff = + DexDiff( + oldDexes = listOf(Dex("classes.dex", 35)), + newDexes = listOf(Dex("classes.dex", 35), Dex("classes2.dex", 38)), + ) + + assertThat(diff.changed).isTrue() + assertThat(diff.toDetailReport()) + .isEqualTo( + """ + | + |FORMAT VERSIONS: + | + | version │ old │ new │ diff + | ─────────┼─────┼─────┼──────────── + | 38 │ 0 │ 1 │ +1 (+1 -0) + |""" + .trimMargin() + ) + } + + @Test + fun formatVersionsMultidexRemoved() { + val diff = + DexDiff( + oldDexes = listOf(Dex("classes.dex", 35), Dex("classes2.dex", 38)), + newDexes = listOf(Dex("classes.dex", 35)), + ) + + assertThat(diff.changed).isTrue() assertThat(diff.toDetailReport()) .isEqualTo( """ | |FORMAT VERSIONS: | - | old │ new │ diff - | ─────┼─────┼─────────── - | 2 │ 2 │ 0 (+1 -1) - | - | + classes2.dex: 38 - | - | - classes2.dex: 35 - | + | version │ old │ new │ diff + | ─────────┼─────┼─────┼──────────── + | 38 │ 1 │ 0 │ -1 (+0 -1) |""" .trimMargin() ) diff --git a/reports/src/test/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiffTest.kt b/reports/src/test/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiffTest.kt new file mode 100644 index 00000000..1559af21 --- /dev/null +++ b/reports/src/test/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiffTest.kt @@ -0,0 +1,317 @@ +package com.jakewharton.diffuse.diff + +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isTrue +import com.jakewharton.diffuse.format.TypeDescriptor +import org.junit.Test + +class BytecodeVersionDiffTest { + @Test + fun nothingChangedProducesNoOutput() { + val diff = + VersionDiff(versionCounts = emptyMap(), changedItems = emptyList()) + assertThat(diff.changed).isFalse() + assertThat(buildString { appendVersionDiff("BYTECODE VERSIONS", diff) }).isEqualTo("") + } + + @Test + fun singleVersionUpgrade() { + // One class moved from version 65 to 69. + val diff = + VersionDiff( + versionCounts = + mapOf( + 65 to (1 to 0), // -1 net + 69 to (0 to 1), // +1 net + ), + changedItems = + listOf( + Triple( + TypeDescriptor("Lorg/example/MainKt;"), + 65, + 69, + ) // Up + ), + ) + + assertThat(diff.changed).isTrue() + assertThat(buildString { appendVersionDiff("BYTECODE VERSIONS", diff) }) + .isEqualTo( + """ + | + |BYTECODE VERSIONS: + | + | version │ old │ new │ diff + | ─────────┼─────┼─────┼──────────── + | 65 │ 1 │ 0 │ -1 (+0 -1) + | 69 │ 0 │ 1 │ +1 (+1 -0) + | + | org.example.MainKt: 65 → 69 + |""" + .trimMargin() + ) + } + + @Test + fun multipleClassesUpgrade() { + // Two classes moved from 61 to 65; the unchanged-count version is filtered out. + val diff = + VersionDiff( + versionCounts = + mapOf( + 61 to (3 to 1), // -2 net + 65 to (0 to 2), // +2 net + ), + changedItems = + listOf( + Triple( + TypeDescriptor("Lcom/example/ClassA;"), + 61, + 65, + ), // Up + Triple( + TypeDescriptor("Lcom/example/ClassB;"), + 61, + 65, + ), // Up + ), + ) + + assertThat(buildString { appendVersionDiff("BYTECODE VERSIONS", diff) }) + .isEqualTo( + """ + | + |BYTECODE VERSIONS: + | + | version │ old │ new │ diff + | ─────────┼─────┼─────┼──────────── + | 61 │ 3 │ 1 │ -2 (+0 -2) + | 65 │ 0 │ 2 │ +2 (+2 -0) + | + | com.example.ClassA: 61 → 65 + | com.example.ClassB: 61 → 65 + |""" + .trimMargin() + ) + } + + @Test + fun singleVersionDowngrade() { + // One class moved from version 69 back down to 65. + val diff = + VersionDiff( + versionCounts = + mapOf( + 65 to (0 to 1), // +1 net + 69 to (1 to 0), // -1 net + ), + changedItems = + listOf( + Triple( + TypeDescriptor("Lorg/example/MainKt;"), + 69, + 65, + ) // Down + ), + ) + + assertThat(diff.changed).isTrue() + assertThat(buildString { appendVersionDiff("BYTECODE VERSIONS", diff) }) + .isEqualTo( + """ + | + |BYTECODE VERSIONS: + | + | version │ old │ new │ diff + | ─────────┼─────┼─────┼──────────── + | 65 │ 0 │ 1 │ +1 (+1 -0) + | 69 │ 1 │ 0 │ -1 (+0 -1) + | + | org.example.MainKt: 69 → 65 + |""" + .trimMargin() + ) + } + + @Test + fun multipleClassesDowngrade() { + // Two classes moved from 65 down to 61. + val diff = + VersionDiff( + versionCounts = + mapOf( + 61 to (0 to 2), // +2 net + 65 to (2 to 0), // -2 net + ), + changedItems = + listOf( + Triple( + TypeDescriptor("Lcom/example/ClassA;"), + 65, + 61, + ), // Down + Triple( + TypeDescriptor("Lcom/example/ClassB;"), + 65, + 61, + ), // Down + ), + ) + + assertThat(buildString { appendVersionDiff("BYTECODE VERSIONS", diff) }) + .isEqualTo( + """ + | + |BYTECODE VERSIONS: + | + | version │ old │ new │ diff + | ─────────┼─────┼─────┼──────────── + | 61 │ 0 │ 2 │ +2 (+2 -0) + | 65 │ 2 │ 0 │ -2 (+0 -2) + | + | com.example.ClassA: 65 → 61 + | com.example.ClassB: 65 → 61 + |""" + .trimMargin() + ) + } + + @Test + fun mixedUpgradeAndDowngrade() { + val diff = + VersionDiff( + versionCounts = + mapOf( + 65 to (2 to 1), // -1 net + 69 to (0 to 1), // +1 net + // Version 61 is old 1, new 1 (Filtered) + ), + changedItems = + listOf( + Triple( + TypeDescriptor("Lcom/example/ClassA;"), + 61, + 65, + ), // Up + Triple( + TypeDescriptor("Lcom/example/ClassB;"), + 65, + 61, + ), // Down + Triple( + TypeDescriptor("Lcom/example/ClassC;"), + 65, + 69, + ), // Up + ), + ) + + assertThat(buildString { appendVersionDiff("BYTECODE VERSIONS", diff) }) + .isEqualTo( + """ + | + |BYTECODE VERSIONS: + | + | version │ old │ new │ diff + | ─────────┼─────┼─────┼──────────── + | 65 │ 2 │ 1 │ -1 (+0 -1) + | 69 │ 0 │ 1 │ +1 (+1 -0) + | + | com.example.ClassA: 61 → 65 + | com.example.ClassB: 65 → 61 + | com.example.ClassC: 65 → 69 + |""" + .trimMargin() + ) + } + + @Test + fun noChangedClassesButVersionCountsDiffer() { + // New classes added at version 65, no pre-existing classes changed version. + val diff = + VersionDiff( + versionCounts = + mapOf( + 65 to (0 to 2) // +2 net + ), + changedItems = emptyList(), + ) + + assertThat(buildString { appendVersionDiff("BYTECODE VERSIONS", diff) }) + .isEqualTo( + """ + | + |BYTECODE VERSIONS: + | + | version │ old │ new │ diff + | ─────────┼─────┼─────┼──────────── + | 65 │ 0 │ 2 │ +2 (+2 -0) + |""" + .trimMargin() + ) + } + + @Test + fun onlyRemovals() { + // Classes removed at version 61. + val diff = + VersionDiff( + versionCounts = + mapOf( + 61 to (2 to 0) // -2 net + ), + changedItems = emptyList(), + ) + + assertThat(buildString { appendVersionDiff("BYTECODE VERSIONS", diff) }) + .isEqualTo( + """ + | + |BYTECODE VERSIONS: + | + | version │ old │ new │ diff + | ─────────┼─────┼─────┼──────────── + | 61 │ 2 │ 0 │ -2 (+0 -2) + |""" + .trimMargin() + ) + } + + @Test + fun netZeroVersionChangesStillProducesOutput() { + // Classes shifted versions but net counts per version stayed same. + // Table is hidden, but class list is shown. + val diff = + VersionDiff( + versionCounts = emptyMap(), + changedItems = + listOf( + Triple( + TypeDescriptor("Lcom/example/ClassA;"), + 61, + 65, + ), // Up + Triple( + TypeDescriptor("Lcom/example/ClassB;"), + 65, + 61, + ), // Down + ), + ) + + assertThat(diff.changed).isTrue() + assertThat(buildString { appendVersionDiff("BYTECODE VERSIONS", diff) }) + .isEqualTo( + """ + | + |BYTECODE VERSIONS: + | + | com.example.ClassA: 61 → 65 + | com.example.ClassB: 65 → 61 + |""" + .trimMargin() + ) + } +} diff --git a/reports/src/test/kotlin/com/jakewharton/diffuse/diff/KotlinMetadataVersionDiffTest.kt b/reports/src/test/kotlin/com/jakewharton/diffuse/diff/KotlinMetadataVersionDiffTest.kt new file mode 100644 index 00000000..ff067f0d --- /dev/null +++ b/reports/src/test/kotlin/com/jakewharton/diffuse/diff/KotlinMetadataVersionDiffTest.kt @@ -0,0 +1,347 @@ +package com.jakewharton.diffuse.diff + +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isTrue +import com.jakewharton.diffuse.format.ApiMapping +import com.jakewharton.diffuse.format.Jar.Companion.toJar +import com.jakewharton.diffuse.format.TypeDescriptor +import com.jakewharton.diffuse.io.Input.Companion.asInput +import com.jakewharton.diffuse.testing.requireResource +import java.io.ByteArrayOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream +import okio.ByteString.Companion.toByteString +import org.junit.Test + +class KotlinMetadataVersionDiffTest { + @Test + fun nonKotlinClassesAreIgnored() { + val classBytes = Test::class.java.requireResource("Test.class").readBytes() + val jarBytes = ByteArrayOutputStream() + ZipOutputStream(jarBytes).use { zip -> + zip.putNextEntry(ZipEntry("org/junit/Test.class")) + zip.write(classBytes) + zip.closeEntry() + } + val inputJar = jarBytes.toByteArray().toByteString().asInput("input.jar").toJar() + + val diff = JarsDiff(listOf(inputJar), ApiMapping.EMPTY, listOf(inputJar), ApiMapping.EMPTY) + + assertThat(diff.kotlinMetadataVersions.versionCounts).isEqualTo(emptyMap()) + assertThat(diff.kotlinMetadataVersions.changedItems).isEqualTo(emptyList()) + } + + @Test + fun nothingChangedProducesNoOutput() { + val diff = + VersionDiff( + versionCounts = emptyMap(), + changedItems = emptyList(), + ) + assertThat(diff.changed).isFalse() + assertThat(buildString { appendVersionDiff("KOTLIN METADATA VERSIONS", diff) }).isEqualTo("") + } + + @Test + fun singleVersionUpgrade() { + // One class moved from version 2.3.0 to 2.4.0. + val diff = + VersionDiff( + versionCounts = + mapOf( + KotlinMetadataVersion(2, 3, 0) to (1 to 0), // -1 net + KotlinMetadataVersion(2, 4, 0) to (0 to 1), // +1 net + ), + changedItems = + listOf( + Triple( + TypeDescriptor("Lorg/example/MainKt;"), + KotlinMetadataVersion(2, 3, 0), + KotlinMetadataVersion(2, 4, 0), + ) // Up + ), + ) + + assertThat(diff.changed).isTrue() + assertThat(buildString { appendVersionDiff("KOTLIN METADATA VERSIONS", diff) }) + .isEqualTo( + """ + | + |KOTLIN METADATA VERSIONS: + | + | version │ old │ new │ diff + | ─────────┼─────┼─────┼──────────── + | 2.3.0 │ 1 │ 0 │ -1 (+0 -1) + | 2.4.0 │ 0 │ 1 │ +1 (+1 -0) + | + | org.example.MainKt: 2.3.0 → 2.4.0 + |""" + .trimMargin() + ) + } + + @Test + fun multipleClassesUpgrade() { + // Two classes moved from 1.9.0 to 2.0.0; the unchanged-count version is filtered out. + val diff = + VersionDiff( + versionCounts = + mapOf( + KotlinMetadataVersion(1, 9, 0) to (3 to 1), // -2 net + KotlinMetadataVersion(2, 0, 0) to (0 to 2), // +2 net + ), + changedItems = + listOf( + Triple( + TypeDescriptor("Lcom/example/ClassA;"), + KotlinMetadataVersion(1, 9, 0), + KotlinMetadataVersion(2, 0, 0), + ), // Up + Triple( + TypeDescriptor("Lcom/example/ClassB;"), + KotlinMetadataVersion(1, 9, 0), + KotlinMetadataVersion(2, 0, 0), + ), // Up + ), + ) + + assertThat(buildString { appendVersionDiff("KOTLIN METADATA VERSIONS", diff) }) + .isEqualTo( + """ + | + |KOTLIN METADATA VERSIONS: + | + | version │ old │ new │ diff + | ─────────┼─────┼─────┼──────────── + | 1.9.0 │ 3 │ 1 │ -2 (+0 -2) + | 2.0.0 │ 0 │ 2 │ +2 (+2 -0) + | + | com.example.ClassA: 1.9.0 → 2.0.0 + | com.example.ClassB: 1.9.0 → 2.0.0 + |""" + .trimMargin() + ) + } + + @Test + fun singleVersionDowngrade() { + // One class moved from version 2.4.0 back down to 2.3.0. + val diff = + VersionDiff( + versionCounts = + mapOf( + KotlinMetadataVersion(2, 3, 0) to (0 to 1), // +1 net + KotlinMetadataVersion(2, 4, 0) to (1 to 0), // -1 net + ), + changedItems = + listOf( + Triple( + TypeDescriptor("Lorg/example/MainKt;"), + KotlinMetadataVersion(2, 4, 0), + KotlinMetadataVersion(2, 3, 0), + ) // Down + ), + ) + + assertThat(diff.changed).isTrue() + assertThat(buildString { appendVersionDiff("KOTLIN METADATA VERSIONS", diff) }) + .isEqualTo( + """ + | + |KOTLIN METADATA VERSIONS: + | + | version │ old │ new │ diff + | ─────────┼─────┼─────┼──────────── + | 2.3.0 │ 0 │ 1 │ +1 (+1 -0) + | 2.4.0 │ 1 │ 0 │ -1 (+0 -1) + | + | org.example.MainKt: 2.4.0 → 2.3.0 + |""" + .trimMargin() + ) + } + + @Test + fun multipleClassesDowngrade() { + // Two classes moved from 2.0.0 down to 1.9.0. + val diff = + VersionDiff( + versionCounts = + mapOf( + KotlinMetadataVersion(1, 9, 0) to (0 to 2), // +2 net + KotlinMetadataVersion(2, 0, 0) to (2 to 0), // -2 net + ), + changedItems = + listOf( + Triple( + TypeDescriptor("Lcom/example/ClassA;"), + KotlinMetadataVersion(2, 0, 0), + KotlinMetadataVersion(1, 9, 0), + ), // Down + Triple( + TypeDescriptor("Lcom/example/ClassB;"), + KotlinMetadataVersion(2, 0, 0), + KotlinMetadataVersion(1, 9, 0), + ), // Down + ), + ) + + assertThat(buildString { appendVersionDiff("KOTLIN METADATA VERSIONS", diff) }) + .isEqualTo( + """ + | + |KOTLIN METADATA VERSIONS: + | + | version │ old │ new │ diff + | ─────────┼─────┼─────┼──────────── + | 1.9.0 │ 0 │ 2 │ +2 (+2 -0) + | 2.0.0 │ 2 │ 0 │ -2 (+0 -2) + | + | com.example.ClassA: 2.0.0 → 1.9.0 + | com.example.ClassB: 2.0.0 → 1.9.0 + |""" + .trimMargin() + ) + } + + @Test + fun mixedUpgradeAndDowngrade() { + val diff = + VersionDiff( + versionCounts = + mapOf( + KotlinMetadataVersion(2, 0, 0) to (2 to 1), // -1 net + KotlinMetadataVersion(2, 4, 0) to (0 to 1), // +1 net + // Version 1.9.0 is old 1, new 1 (Filtered) + ), + changedItems = + listOf( + Triple( + TypeDescriptor("Lcom/example/ClassA;"), + KotlinMetadataVersion(1, 9, 0), + KotlinMetadataVersion(2, 0, 0), + ), // Up + Triple( + TypeDescriptor("Lcom/example/ClassB;"), + KotlinMetadataVersion(2, 0, 0), + KotlinMetadataVersion(1, 9, 0), + ), // Down + Triple( + TypeDescriptor("Lcom/example/ClassC;"), + KotlinMetadataVersion(2, 0, 0), + KotlinMetadataVersion(2, 4, 0), + ), // Up + ), + ) + + assertThat(buildString { appendVersionDiff("KOTLIN METADATA VERSIONS", diff) }) + .isEqualTo( + """ + | + |KOTLIN METADATA VERSIONS: + | + | version │ old │ new │ diff + | ─────────┼─────┼─────┼──────────── + | 2.0.0 │ 2 │ 1 │ -1 (+0 -1) + | 2.4.0 │ 0 │ 1 │ +1 (+1 -0) + | + | com.example.ClassA: 1.9.0 → 2.0.0 + | com.example.ClassB: 2.0.0 → 1.9.0 + | com.example.ClassC: 2.0.0 → 2.4.0 + |""" + .trimMargin() + ) + } + + @Test + fun noChangedClassesButVersionCountsDiffer() { + // New classes added at version 2.0.0, no pre-existing classes changed version. + val diff = + VersionDiff( + versionCounts = + mapOf( + KotlinMetadataVersion(2, 0, 0) to (0 to 2) // +2 net + ), + changedItems = emptyList(), + ) + + assertThat(buildString { appendVersionDiff("KOTLIN METADATA VERSIONS", diff) }) + .isEqualTo( + """ + | + |KOTLIN METADATA VERSIONS: + | + | version │ old │ new │ diff + | ─────────┼─────┼─────┼──────────── + | 2.0.0 │ 0 │ 2 │ +2 (+2 -0) + |""" + .trimMargin() + ) + } + + @Test + fun onlyRemovals() { + // Classes removed at version 1.9.0. + val diff = + VersionDiff( + versionCounts = + mapOf( + KotlinMetadataVersion(1, 9, 0) to (2 to 0) // -2 net + ), + changedItems = emptyList(), + ) + + assertThat(buildString { appendVersionDiff("KOTLIN METADATA VERSIONS", diff) }) + .isEqualTo( + """ + | + |KOTLIN METADATA VERSIONS: + | + | version │ old │ new │ diff + | ─────────┼─────┼─────┼──────────── + | 1.9.0 │ 2 │ 0 │ -2 (+0 -2) + |""" + .trimMargin() + ) + } + + @Test + fun netZeroVersionChangesStillProducesOutput() { + // Classes shifted versions but net counts per version stayed same. + // Table is hidden, but class list is shown. + val diff = + VersionDiff( + versionCounts = emptyMap(), + changedItems = + listOf( + Triple( + TypeDescriptor("Lcom/example/ClassA;"), + KotlinMetadataVersion(1, 9, 0), + KotlinMetadataVersion(2, 0, 0), + ), // Up + Triple( + TypeDescriptor("Lcom/example/ClassB;"), + KotlinMetadataVersion(2, 0, 0), + KotlinMetadataVersion(1, 9, 0), + ), // Down + ), + ) + + assertThat(diff.changed).isTrue() + assertThat(buildString { appendVersionDiff("KOTLIN METADATA VERSIONS", diff) }) + .isEqualTo( + """ + | + |KOTLIN METADATA VERSIONS: + | + | com.example.ClassA: 1.9.0 → 2.0.0 + | com.example.ClassB: 2.0.0 → 1.9.0 + |""" + .trimMargin() + ) + } +} + +private fun KotlinMetadataVersion(vararg numbers: Int) = KotlinMetadataVersion(numbers.toList()) diff --git a/reports/src/test/kotlin/com/jakewharton/diffuse/diff/VersionDiffTest.kt b/reports/src/test/kotlin/com/jakewharton/diffuse/diff/VersionDiffTest.kt new file mode 100644 index 00000000..c5047bf0 --- /dev/null +++ b/reports/src/test/kotlin/com/jakewharton/diffuse/diff/VersionDiffTest.kt @@ -0,0 +1,136 @@ +package com.jakewharton.diffuse.diff + +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.containsOnly +import assertk.assertions.isEmpty +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isTrue +import com.jakewharton.diffuse.format.TypeDescriptor +import org.junit.Test + +class VersionDiffTest { + @Test + fun nothingChanged() { + val items = + listOf( + mapOf( + TypeDescriptor("Lcom/example/ClassA;") to 61, + TypeDescriptor("Lcom/example/ClassB;") to 65, + ) + ) + + val diff = versionDiff(items, items) { it } + + assertThat(diff.changed).isFalse() + assertThat(diff.versionCounts).isEmpty() + assertThat(diff.changedItems).isEmpty() + } + + @Test + fun unchangedVersionsAreFilteredOutFromVersionCounts() { + val oldItems = + listOf( + mapOf( + TypeDescriptor("Lcom/example/ClassA;") to 61, + TypeDescriptor("Lcom/example/ClassB;") to 65, + TypeDescriptor("Lcom/example/ClassUnchanged;") to 61, + ) + ) + val newItems = + listOf( + mapOf( + TypeDescriptor("Lcom/example/ClassA;") to 65, + TypeDescriptor("Lcom/example/ClassB;") to 61, + TypeDescriptor("Lcom/example/ClassUnchanged;") to 61, + ) + ) + + val diff = versionDiff(oldItems, newItems) { it } + + assertThat(diff.changed).isTrue() + // 61: old=2, new=2 (net=0 -> filtered) + // 65: old=1, new=1 (net=0 -> filtered) + assertThat(diff.versionCounts).isEmpty() + assertThat(diff.changedItems) + .containsExactly( + Triple(TypeDescriptor("Lcom/example/ClassA;"), 61, 65), + Triple(TypeDescriptor("Lcom/example/ClassB;"), 65, 61), + ) + } + + @Test + fun partialVersionCountChangeFiltersOnlyUnchanged() { + val oldItems = + listOf( + mapOf( + TypeDescriptor("Lcom/example/ClassA;") to 61, + TypeDescriptor("Lcom/example/ClassB;") to 65, + TypeDescriptor("Lcom/example/ClassC;") to 65, + ) + ) + val newItems = + listOf( + mapOf( + TypeDescriptor("Lcom/example/ClassA;") to 65, + TypeDescriptor("Lcom/example/ClassB;") to 61, + TypeDescriptor("Lcom/example/ClassC;") to 69, + ) + ) + + val diff = versionDiff(oldItems, newItems) { it } + + assertThat(diff.changed).isTrue() + // 61: old=1, new=1 (net=0 -> filtered) + // 65: old=2, new=1 (net=-1 -> kept) + // 69: old=0, new=1 (net=+1 -> kept) + assertThat(diff.versionCounts) + .isEqualTo( + mapOf( + 65 to (2 to 1), + 69 to (0 to 1), + ) + ) + assertThat(diff.changedItems) + .containsExactly( + Triple(TypeDescriptor("Lcom/example/ClassA;"), 61, 65), + Triple(TypeDescriptor("Lcom/example/ClassB;"), 65, 61), + Triple(TypeDescriptor("Lcom/example/ClassC;"), 65, 69), + ) + } + + @Test + fun multipleDexesAggregation() { + val oldItems = + listOf( + mapOf( + "classes.dex" to 35, + "classes2.dex" to 35, + ), + mapOf("classes3.dex" to 35), + ) + val newItems = + listOf( + mapOf("classes.dex" to 35), + mapOf( + "classes2.dex" to 38, + "classes3.dex" to 38, + ), + ) + + val diff = versionDiff(oldItems, newItems) { it } + + assertThat(diff.changed).isTrue() + assertThat(diff.versionCounts) + .containsOnly( + 35 to (3 to 1), + 38 to (0 to 2), + ) + assertThat(diff.changedItems) + .containsExactly( + Triple("classes2.dex", 35, 38), + Triple("classes3.dex", 35, 38), + ) + } +}