From e32711ae708036e20f9c5240ed16a436963a0186 Mon Sep 17 00:00:00 2001 From: Goooler Date: Fri, 20 Mar 2026 09:20:12 +0800 Subject: [PATCH 1/6] Add BytecodeVersionDiff --- .../diffuse/diff/BytecodeVersionDiff.kt | 103 ++++++++++++++++++ .../com/jakewharton/diffuse/diff/JarsDiff.kt | 7 +- 2 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 reports/src/main/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiff.kt diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiff.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiff.kt new file mode 100644 index 00000000..6c86b6a1 --- /dev/null +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiff.kt @@ -0,0 +1,103 @@ +package com.jakewharton.diffuse.diff + +import com.jakewharton.diffuse.diffuseTable +import com.jakewharton.diffuse.format.Jar +import com.jakewharton.diffuse.format.TypeDescriptor +import com.jakewharton.diffuse.report.toDiffString +import com.jakewharton.picnic.TextAlignment.MiddleRight +import com.jakewharton.picnic.renderText +import kotlin.collections.iterator + +/** + * Diff for bytecode versions across two sets of JARs. + * + * @param versionCounts per-version summary: maps each bytecode version integer to a pair of + * (`oldCount`, `newCount`) where the counts differ, sorted by `version`. + * @param changedClasses classes that exist in both old and new but changed bytecode version, as + * triples of (`descriptor`, `oldVersion`, `newVersion`), sorted by `descriptor`. + */ +internal class BytecodeVersionDiff( + val versionCounts: Map>, + val changedClasses: List>, +) { + val changed + get() = versionCounts.isNotEmpty() +} + +internal fun bytecodeVersionDiff(oldJars: List, newJars: List): BytecodeVersionDiff { + val oldVersionMap: Map = + oldJars.flatMap { it.classes }.associate { it.descriptor to it.bytecodeVersion } + val newVersionMap: Map = + newJars.flatMap { it.classes }.associate { it.descriptor to it.bytecodeVersion } + + // Classes present in both with different versions. + val changedClasses = + oldVersionMap + .mapNotNull { (descriptor, oldVersion) -> + val newVersion = newVersionMap[descriptor] + if (newVersion != null && newVersion != oldVersion) { + Triple(descriptor, oldVersion, newVersion) + } else { + null + } + } + .sortedBy { it.first } + + // Tally per-version counts across all classes in old and new. + val allVersions = (oldVersionMap.values + newVersionMap.values).toSortedSet() + val versionCounts = + allVersions + .associateWith { version -> + val oldCount = oldVersionMap.values.count { it == version } + val newCount = newVersionMap.values.count { it == version } + oldCount to newCount + } + .filter { (_, counts) -> counts.first != counts.second } + + return BytecodeVersionDiff(versionCounts, changedClasses) +} + +internal fun StringBuilder.appendBytecodeVersionDiff(name: String, diff: BytecodeVersionDiff) { + if (diff.changed) { + appendLine() + appendLine("$name:") + appendLine() + appendLine( + buildString { + appendLine( + 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 net = (newCount - oldCount).toDiffString() + val added = (newCount - oldCount).coerceAtLeast(0).toDiffString(zeroSign = '+') + val removed = + (-(oldCount - newCount).coerceAtLeast(0)).toDiffString(zeroSign = '-') + row(version, oldCount, newCount, "$net ($added $removed)") + } + } + } + .renderText() + ) + if (diff.changedClasses.isNotEmpty()) { + appendLine() + diff.changedClasses.forEach { (descriptor, oldVersion, newVersion) -> + appendLine("$descriptor: $oldVersion → $newVersion") + } + } + } + .prependIndent(" ") + ) + } +} 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 b402e235..96e1e3f7 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/JarsDiff.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/JarsDiff.kt @@ -18,10 +18,7 @@ internal class JarsDiff( val newMapping: ApiMapping, ) { val classes = componentDiff(oldJars, newJars) { it.classes.map(Class::descriptor) } - val bytecodeVersions = - componentDiff(oldJars, newJars) { jar -> - jar.classes.map { "${it.descriptor}: ${it.bytecodeVersion}" } - } + val bytecodeVersions = bytecodeVersionDiff(oldJars, newJars) val methods = componentDiff(oldJars, newJars) { it.members.filterIsInstance() } val declaredMethods = componentDiff(oldJars, newJars) { it.declaredMembers.filterIsInstance() } @@ -76,7 +73,7 @@ internal fun JarsDiff.toSummaryTable(name: String) = internal fun JarsDiff.toDetailReport() = buildString { // TODO appendComponentDiff("STRINGS", strings)? appendComponentDiff("CLASSES", classes) - appendComponentDiff("BYTECODE VERSIONS", bytecodeVersions) + appendBytecodeVersionDiff("BYTECODE VERSIONS", bytecodeVersions) appendComponentDiff("METHODS", methods) appendComponentDiff("FIELDS", fields) } From dac2f05ed93629e1cbe6526eb3687af60b4c1694 Mon Sep 17 00:00:00 2001 From: Goooler Date: Fri, 20 Mar 2026 09:30:02 +0800 Subject: [PATCH 2/6] Add tests --- .../diffuse/diff/BytecodeVersionDiff.kt | 8 +- .../diffuse/diff/BytecodeVersionDiffTest.kt | 158 ++++++++++++++++++ 2 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 reports/src/test/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiffTest.kt diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiff.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiff.kt index 6c86b6a1..9f7f6878 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiff.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiff.kt @@ -20,8 +20,7 @@ internal class BytecodeVersionDiff( val versionCounts: Map>, val changedClasses: List>, ) { - val changed - get() = versionCounts.isNotEmpty() + val changed = versionCounts.isNotEmpty() } internal fun bytecodeVersionDiff(oldJars: List, newJars: List): BytecodeVersionDiff { @@ -89,6 +88,8 @@ internal fun StringBuilder.appendBytecodeVersionDiff(name: String, diff: Bytecod } } .renderText() + .lines() + .joinToString("\n") { it.trimEnd() } ) if (diff.changedClasses.isNotEmpty()) { appendLine() @@ -97,7 +98,8 @@ internal fun StringBuilder.appendBytecodeVersionDiff(name: String, diff: Bytecod } } } - .prependIndent(" ") + .lines() + .joinToString("\n") { if (it.isBlank()) "" else " $it" } ) } } 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..e699f81e --- /dev/null +++ b/reports/src/test/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiffTest.kt @@ -0,0 +1,158 @@ +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 = BytecodeVersionDiff(versionCounts = emptyMap(), changedClasses = emptyList()) + assertThat(diff.changed).isFalse() + assertThat(buildString { appendBytecodeVersionDiff("BYTECODE VERSIONS", diff) }).isEqualTo("") + } + + @Test + fun singleVersionUpgrade() { + // One class moved from version 65 to 69. + val diff = + BytecodeVersionDiff( + versionCounts = mapOf(65 to (1 to 0), 69 to (0 to 1)), + changedClasses = listOf(Triple(TypeDescriptor("Lorg/example/MainKt;"), 65, 69)), + ) + + assertThat(diff.changed).isTrue() + assertThat(buildString { appendBytecodeVersionDiff("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 = + BytecodeVersionDiff( + versionCounts = mapOf(61 to (3 to 1), 65 to (0 to 2)), + changedClasses = + listOf( + Triple(TypeDescriptor("Lcom/example/Bar;"), 61, 65), + Triple(TypeDescriptor("Lcom/example/Foo;"), 61, 65), + ), + ) + + assertThat(buildString { appendBytecodeVersionDiff("BYTECODE VERSIONS", diff) }) + .isEqualTo( + """ + | + |BYTECODE VERSIONS: + | + | version │ old │ new │ diff + | ─────────┼─────┼─────┼──────────── + | 61 │ 3 │ 1 │ -2 (+0 -2) + | 65 │ 0 │ 2 │ +2 (+2 -0) + | + | com.example.Bar: 61 → 65 + | com.example.Foo: 61 → 65 + | + |""" + .trimMargin() + ) + } + + @Test + fun noChangedClassesButVersionCountsDiffer() { + // New classes added at version 65, no pre-existing classes changed version. + val diff = + BytecodeVersionDiff(versionCounts = mapOf(65 to (0 to 2)), changedClasses = emptyList()) + + assertThat(buildString { appendBytecodeVersionDiff("BYTECODE VERSIONS", diff) }) + .isEqualTo( + """ + | + |BYTECODE VERSIONS: + | + | version │ old │ new │ diff + | ─────────┼─────┼─────┼──────────── + | 65 │ 0 │ 2 │ +2 (+2 -0) + | + |""" + .trimMargin() + ) + } + + @Test + fun singleVersionDowngrade() { + // One class moved from version 69 back down to 65. + val diff = + BytecodeVersionDiff( + versionCounts = mapOf(65 to (0 to 1), 69 to (1 to 0)), + changedClasses = listOf(Triple(TypeDescriptor("Lorg/example/MainKt;"), 69, 65)), + ) + + assertThat(diff.changed).isTrue() + assertThat(buildString { appendBytecodeVersionDiff("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 = + BytecodeVersionDiff( + versionCounts = mapOf(61 to (0 to 2), 65 to (2 to 0)), + changedClasses = + listOf( + Triple(TypeDescriptor("Lcom/example/Bar;"), 65, 61), + Triple(TypeDescriptor("Lcom/example/Foo;"), 65, 61), + ), + ) + + assertThat(buildString { appendBytecodeVersionDiff("BYTECODE VERSIONS", diff) }) + .isEqualTo( + """ + | + |BYTECODE VERSIONS: + | + | version │ old │ new │ diff + | ─────────┼─────┼─────┼──────────── + | 61 │ 0 │ 2 │ +2 (+2 -0) + | 65 │ 2 │ 0 │ -2 (+0 -2) + | + | com.example.Bar: 65 → 61 + | com.example.Foo: 65 → 61 + | + |""" + .trimMargin() + ) + } +} From 9fa45980dea146275aad5f0e202fe4a79806d438 Mon Sep 17 00:00:00 2001 From: Goooler Date: Fri, 20 Mar 2026 10:17:58 +0800 Subject: [PATCH 3/6] Use inline classes --- .../diffuse/diff/BytecodeVersionDiff.kt | 42 +++++++++++++------ .../diffuse/diff/BytecodeVersionDiffTest.kt | 33 ++++++++++----- 2 files changed, 51 insertions(+), 24 deletions(-) diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiff.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiff.kt index 9f7f6878..e6cb08fc 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiff.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiff.kt @@ -17,17 +17,28 @@ import kotlin.collections.iterator * triples of (`descriptor`, `oldVersion`, `newVersion`), sorted by `descriptor`. */ internal class BytecodeVersionDiff( - val versionCounts: Map>, - val changedClasses: List>, + val versionCounts: Map>, + val changedClasses: List>, ) { val changed = versionCounts.isNotEmpty() + + @JvmInline value class Count(val count: Int) + + @JvmInline + value class Version(val version: Int) : Comparable { + override fun compareTo(other: Version): Int = version.compareTo(other.version) + } } internal fun bytecodeVersionDiff(oldJars: List, newJars: List): BytecodeVersionDiff { - val oldVersionMap: Map = - oldJars.flatMap { it.classes }.associate { it.descriptor to it.bytecodeVersion } - val newVersionMap: Map = - newJars.flatMap { it.classes }.associate { it.descriptor to it.bytecodeVersion } + val oldVersionMap: Map = + oldJars + .flatMap { it.classes } + .associate { it.descriptor to BytecodeVersionDiff.Version(it.bytecodeVersion) } + val newVersionMap: Map = + newJars + .flatMap { it.classes } + .associate { it.descriptor to BytecodeVersionDiff.Version(it.bytecodeVersion) } // Classes present in both with different versions. val changedClasses = @@ -49,7 +60,7 @@ internal fun bytecodeVersionDiff(oldJars: List, newJars: List): Byteco .associateWith { version -> val oldCount = oldVersionMap.values.count { it == version } val newCount = newVersionMap.values.count { it == version } - oldCount to newCount + (BytecodeVersionDiff.Count(oldCount) to BytecodeVersionDiff.Count(newCount)) } .filter { (_, counts) -> counts.first != counts.second } @@ -79,11 +90,16 @@ internal fun StringBuilder.appendBytecodeVersionDiff(name: String, diff: Bytecod for ((version, counts) in diff.versionCounts) { val (oldCount, newCount) = counts - val net = (newCount - oldCount).toDiffString() - val added = (newCount - oldCount).coerceAtLeast(0).toDiffString(zeroSign = '+') + val net = (newCount.count - oldCount.count).toDiffString() + val added = + (newCount.count - oldCount.count) + .coerceAtLeast(0) + .toDiffString(zeroSign = '+') val removed = - (-(oldCount - newCount).coerceAtLeast(0)).toDiffString(zeroSign = '-') - row(version, oldCount, newCount, "$net ($added $removed)") + (-(oldCount.count - newCount.count).coerceAtLeast(0)).toDiffString( + zeroSign = '-' + ) + row(version.version, oldCount.count, newCount.count, "$net ($added $removed)") } } } @@ -94,11 +110,11 @@ internal fun StringBuilder.appendBytecodeVersionDiff(name: String, diff: Bytecod if (diff.changedClasses.isNotEmpty()) { appendLine() diff.changedClasses.forEach { (descriptor, oldVersion, newVersion) -> - appendLine("$descriptor: $oldVersion → $newVersion") + appendLine("$descriptor: ${oldVersion.version} → ${newVersion.version}") } } } - .lines() + .lineSequence() .joinToString("\n") { if (it.isBlank()) "" else " $it" } ) } diff --git a/reports/src/test/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiffTest.kt b/reports/src/test/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiffTest.kt index e699f81e..cc744712 100644 --- a/reports/src/test/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiffTest.kt +++ b/reports/src/test/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiffTest.kt @@ -4,6 +4,8 @@ import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isTrue +import com.jakewharton.diffuse.diff.BytecodeVersionDiff.Count +import com.jakewharton.diffuse.diff.BytecodeVersionDiff.Version import com.jakewharton.diffuse.format.TypeDescriptor import org.junit.Test @@ -20,8 +22,10 @@ class BytecodeVersionDiffTest { // One class moved from version 65 to 69. val diff = BytecodeVersionDiff( - versionCounts = mapOf(65 to (1 to 0), 69 to (0 to 1)), - changedClasses = listOf(Triple(TypeDescriptor("Lorg/example/MainKt;"), 65, 69)), + versionCounts = + mapOf(Version(65) to (Count(1) to Count(0)), Version(69) to (Count(0) to Count(1))), + changedClasses = + listOf(Triple(TypeDescriptor("Lorg/example/MainKt;"), Version(65), Version(69))), ) assertThat(diff.changed).isTrue() @@ -48,11 +52,12 @@ class BytecodeVersionDiffTest { // Two classes moved from 61 to 65; the unchanged-count version is filtered out. val diff = BytecodeVersionDiff( - versionCounts = mapOf(61 to (3 to 1), 65 to (0 to 2)), + versionCounts = + mapOf(Version(61) to (Count(3) to Count(1)), Version(65) to (Count(0) to Count(2))), changedClasses = listOf( - Triple(TypeDescriptor("Lcom/example/Bar;"), 61, 65), - Triple(TypeDescriptor("Lcom/example/Foo;"), 61, 65), + Triple(TypeDescriptor("Lcom/example/Bar;"), Version(61), Version(65)), + Triple(TypeDescriptor("Lcom/example/Foo;"), Version(61), Version(65)), ), ) @@ -79,7 +84,10 @@ class BytecodeVersionDiffTest { fun noChangedClassesButVersionCountsDiffer() { // New classes added at version 65, no pre-existing classes changed version. val diff = - BytecodeVersionDiff(versionCounts = mapOf(65 to (0 to 2)), changedClasses = emptyList()) + BytecodeVersionDiff( + versionCounts = mapOf(Version(65) to (Count(0) to Count(2))), + changedClasses = emptyList(), + ) assertThat(buildString { appendBytecodeVersionDiff("BYTECODE VERSIONS", diff) }) .isEqualTo( @@ -101,8 +109,10 @@ class BytecodeVersionDiffTest { // One class moved from version 69 back down to 65. val diff = BytecodeVersionDiff( - versionCounts = mapOf(65 to (0 to 1), 69 to (1 to 0)), - changedClasses = listOf(Triple(TypeDescriptor("Lorg/example/MainKt;"), 69, 65)), + versionCounts = + mapOf(Version(65) to (Count(0) to Count(1)), Version(69) to (Count(1) to Count(0))), + changedClasses = + listOf(Triple(TypeDescriptor("Lorg/example/MainKt;"), Version(69), Version(65))), ) assertThat(diff.changed).isTrue() @@ -129,11 +139,12 @@ class BytecodeVersionDiffTest { // Two classes moved from 65 down to 61. val diff = BytecodeVersionDiff( - versionCounts = mapOf(61 to (0 to 2), 65 to (2 to 0)), + versionCounts = + mapOf(Version(61) to (Count(0) to Count(2)), Version(65) to (Count(2) to Count(0))), changedClasses = listOf( - Triple(TypeDescriptor("Lcom/example/Bar;"), 65, 61), - Triple(TypeDescriptor("Lcom/example/Foo;"), 65, 61), + Triple(TypeDescriptor("Lcom/example/Bar;"), Version(65), Version(61)), + Triple(TypeDescriptor("Lcom/example/Foo;"), Version(65), Version(61)), ), ) From 5699a82cf5461eef0fdcb25d15910eb4bd65d99d Mon Sep 17 00:00:00 2001 From: Goooler Date: Fri, 20 Mar 2026 10:31:45 +0800 Subject: [PATCH 4/6] Add more cases --- .../diffuse/diff/BytecodeVersionDiff.kt | 66 +++---- .../diffuse/diff/BytecodeVersionDiffTest.kt | 171 +++++++++++++++--- 2 files changed, 177 insertions(+), 60 deletions(-) diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiff.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiff.kt index e6cb08fc..b0f7b99f 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiff.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiff.kt @@ -20,7 +20,7 @@ internal class BytecodeVersionDiff( val versionCounts: Map>, val changedClasses: List>, ) { - val changed = versionCounts.isNotEmpty() + val changed = versionCounts.isNotEmpty() || changedClasses.isNotEmpty() @JvmInline value class Count(val count: Int) @@ -60,7 +60,7 @@ internal fun bytecodeVersionDiff(oldJars: List, newJars: List): Byteco .associateWith { version -> val oldCount = oldVersionMap.values.count { it == version } val newCount = newVersionMap.values.count { it == version } - (BytecodeVersionDiff.Count(oldCount) to BytecodeVersionDiff.Count(newCount)) + BytecodeVersionDiff.Count(oldCount) to BytecodeVersionDiff.Count(newCount) } .filter { (_, counts) -> counts.first != counts.second } @@ -74,41 +74,45 @@ internal fun StringBuilder.appendBytecodeVersionDiff(name: String, diff: Bytecod appendLine() appendLine( buildString { - appendLine( - diffuseTable { - header { - row { - cell("version") - cell("old") - cell("new") - cell("diff") + if (diff.versionCounts.isNotEmpty()) { + appendLine( + diffuseTable { + header { + row { + cell("version") + cell("old") + cell("new") + cell("diff") + } } - } - body { - cellStyle { alignment = MiddleRight } + body { + cellStyle { alignment = MiddleRight } - for ((version, counts) in diff.versionCounts) { - val (oldCount, newCount) = counts - val net = (newCount.count - oldCount.count).toDiffString() - val added = - (newCount.count - oldCount.count) - .coerceAtLeast(0) - .toDiffString(zeroSign = '+') - val removed = - (-(oldCount.count - newCount.count).coerceAtLeast(0)).toDiffString( - zeroSign = '-' - ) - row(version.version, oldCount.count, newCount.count, "$net ($added $removed)") + for ((version, counts) in diff.versionCounts) { + val (oldCount, newCount) = counts + val net = (newCount.count - oldCount.count).toDiffString() + val added = + (newCount.count - oldCount.count) + .coerceAtLeast(0) + .toDiffString(zeroSign = '+') + val removed = + (-(oldCount.count - newCount.count).coerceAtLeast(0)).toDiffString( + zeroSign = '-' + ) + row(version.version, oldCount.count, newCount.count, "$net ($added $removed)") + } } } - } - .renderText() - .lines() - .joinToString("\n") { it.trimEnd() } - ) + .renderText() + .lines() + .joinToString("\n") { it.trimEnd() } + ) + } if (diff.changedClasses.isNotEmpty()) { - appendLine() + if (diff.versionCounts.isNotEmpty()) { + appendLine() + } diff.changedClasses.forEach { (descriptor, oldVersion, newVersion) -> appendLine("$descriptor: ${oldVersion.version} → ${newVersion.version}") } diff --git a/reports/src/test/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiffTest.kt b/reports/src/test/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiffTest.kt index cc744712..c255af1d 100644 --- a/reports/src/test/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiffTest.kt +++ b/reports/src/test/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiffTest.kt @@ -23,9 +23,14 @@ class BytecodeVersionDiffTest { val diff = BytecodeVersionDiff( versionCounts = - mapOf(Version(65) to (Count(1) to Count(0)), Version(69) to (Count(0) to Count(1))), + mapOf( + Version(65) to (Count(1) to Count(0)), // -1 net + Version(69) to (Count(0) to Count(1)), // +1 net + ), changedClasses = - listOf(Triple(TypeDescriptor("Lorg/example/MainKt;"), Version(65), Version(69))), + listOf( + Triple(TypeDescriptor("Lorg/example/MainKt;"), Version(65), Version(69)) // Up + ), ) assertThat(diff.changed).isTrue() @@ -53,11 +58,14 @@ class BytecodeVersionDiffTest { val diff = BytecodeVersionDiff( versionCounts = - mapOf(Version(61) to (Count(3) to Count(1)), Version(65) to (Count(0) to Count(2))), + mapOf( + Version(61) to (Count(3) to Count(1)), // -2 net + Version(65) to (Count(0) to Count(2)), // +2 net + ), changedClasses = listOf( - Triple(TypeDescriptor("Lcom/example/Bar;"), Version(61), Version(65)), - Triple(TypeDescriptor("Lcom/example/Foo;"), Version(61), Version(65)), + Triple(TypeDescriptor("Lcom/example/ClassB;"), Version(61), Version(65)), // Up + Triple(TypeDescriptor("Lcom/example/ClassA;"), Version(61), Version(65)), // Up ), ) @@ -72,8 +80,8 @@ class BytecodeVersionDiffTest { | 61 │ 3 │ 1 │ -2 (+0 -2) | 65 │ 0 │ 2 │ +2 (+2 -0) | - | com.example.Bar: 61 → 65 - | com.example.Foo: 61 → 65 + | com.example.ClassB: 61 → 65 + | com.example.ClassA: 61 → 65 | |""" .trimMargin() @@ -81,14 +89,22 @@ class BytecodeVersionDiffTest { } @Test - fun noChangedClassesButVersionCountsDiffer() { - // New classes added at version 65, no pre-existing classes changed version. + fun singleVersionDowngrade() { + // One class moved from version 69 back down to 65. val diff = BytecodeVersionDiff( - versionCounts = mapOf(Version(65) to (Count(0) to Count(2))), - changedClasses = emptyList(), + versionCounts = + mapOf( + Version(65) to (Count(0) to Count(1)), // +1 net + Version(69) to (Count(1) to Count(0)), // -1 net + ), + changedClasses = + listOf( + Triple(TypeDescriptor("Lorg/example/MainKt;"), Version(69), Version(65)) // Down + ), ) + assertThat(diff.changed).isTrue() assertThat(buildString { appendBytecodeVersionDiff("BYTECODE VERSIONS", diff) }) .isEqualTo( """ @@ -97,7 +113,10 @@ class BytecodeVersionDiffTest { | | version │ old │ new │ diff | ─────────┼─────┼─────┼──────────── - | 65 │ 0 │ 2 │ +2 (+2 -0) + | 65 │ 0 │ 1 │ +1 (+1 -0) + | 69 │ 1 │ 0 │ -1 (+0 -1) + | + | org.example.MainKt: 69 → 65 | |""" .trimMargin() @@ -105,17 +124,22 @@ class BytecodeVersionDiffTest { } @Test - fun singleVersionDowngrade() { - // One class moved from version 69 back down to 65. + fun multipleClassesDowngrade() { + // Two classes moved from 65 down to 61. val diff = BytecodeVersionDiff( versionCounts = - mapOf(Version(65) to (Count(0) to Count(1)), Version(69) to (Count(1) to Count(0))), + mapOf( + Version(61) to (Count(0) to Count(2)), // +2 net + Version(65) to (Count(2) to Count(0)), // -2 net + ), changedClasses = - listOf(Triple(TypeDescriptor("Lorg/example/MainKt;"), Version(69), Version(65))), + listOf( + Triple(TypeDescriptor("Lcom/example/ClassB;"), Version(65), Version(61)), // Down + Triple(TypeDescriptor("Lcom/example/ClassA;"), Version(65), Version(61)), // Down + ), ) - assertThat(diff.changed).isTrue() assertThat(buildString { appendBytecodeVersionDiff("BYTECODE VERSIONS", diff) }) .isEqualTo( """ @@ -124,10 +148,11 @@ class BytecodeVersionDiffTest { | | version │ old │ new │ diff | ─────────┼─────┼─────┼──────────── - | 65 │ 0 │ 1 │ +1 (+1 -0) - | 69 │ 1 │ 0 │ -1 (+0 -1) + | 61 │ 0 │ 2 │ +2 (+2 -0) + | 65 │ 2 │ 0 │ -2 (+0 -2) | - | org.example.MainKt: 69 → 65 + | com.example.ClassB: 65 → 61 + | com.example.ClassA: 65 → 61 | |""" .trimMargin() @@ -135,16 +160,20 @@ class BytecodeVersionDiffTest { } @Test - fun multipleClassesDowngrade() { - // Two classes moved from 65 down to 61. + fun mixedUpgradeAndDowngrade() { val diff = BytecodeVersionDiff( versionCounts = - mapOf(Version(61) to (Count(0) to Count(2)), Version(65) to (Count(2) to Count(0))), + mapOf( + Version(65) to (Count(2) to Count(1)), // -1 net + Version(69) to (Count(0) to Count(1)), // +1 net + // Version 61 is old 1, new 1 (Filtered) + ), changedClasses = listOf( - Triple(TypeDescriptor("Lcom/example/Bar;"), Version(65), Version(61)), - Triple(TypeDescriptor("Lcom/example/Foo;"), Version(65), Version(61)), + Triple(TypeDescriptor("Lcom/example/ClassA;"), Version(61), Version(65)), // Up + Triple(TypeDescriptor("Lcom/example/ClassB;"), Version(65), Version(61)), // Down + Triple(TypeDescriptor("Lcom/example/ClassC;"), Version(65), Version(69)), // Up ), ) @@ -156,11 +185,95 @@ class BytecodeVersionDiffTest { | | version │ old │ new │ diff | ─────────┼─────┼─────┼──────────── - | 61 │ 0 │ 2 │ +2 (+2 -0) - | 65 │ 2 │ 0 │ -2 (+0 -2) + | 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 = + BytecodeVersionDiff( + versionCounts = + mapOf( + Version(65) to (Count(0) to Count(2)) // +2 net + ), + changedClasses = emptyList(), + ) + + assertThat(buildString { appendBytecodeVersionDiff("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 = + BytecodeVersionDiff( + versionCounts = + mapOf( + Version(61) to (Count(2) to Count(0)) // -2 net + ), + changedClasses = emptyList(), + ) + + assertThat(buildString { appendBytecodeVersionDiff("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 = + BytecodeVersionDiff( + versionCounts = emptyMap(), + changedClasses = + listOf( + Triple(TypeDescriptor("Lcom/example/ClassA;"), Version(61), Version(65)), // Up + Triple(TypeDescriptor("Lcom/example/ClassB;"), Version(65), Version(61)), // Down + ), + ) + + assertThat(diff.changed).isTrue() + assertThat(buildString { appendBytecodeVersionDiff("BYTECODE VERSIONS", diff) }) + .isEqualTo( + """ + | + |BYTECODE VERSIONS: | - | com.example.Bar: 65 → 61 - | com.example.Foo: 65 → 61 + | com.example.ClassA: 61 → 65 + | com.example.ClassB: 65 → 61 | |""" .trimMargin() From 4a00983f3f8ed18486d3d334d44cda94d2d479a5 Mon Sep 17 00:00:00 2001 From: Goooler Date: Fri, 20 Mar 2026 11:06:45 +0800 Subject: [PATCH 5/6] Cleanups --- .../diffuse/diff/BytecodeVersionDiff.kt | 88 ++++++++----------- .../diffuse/diff/BytecodeVersionDiffTest.kt | 46 +++++----- 2 files changed, 62 insertions(+), 72 deletions(-) diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiff.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiff.kt index b0f7b99f..608b212a 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiff.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiff.kt @@ -68,58 +68,48 @@ internal fun bytecodeVersionDiff(oldJars: List, newJars: List): Byteco } internal fun StringBuilder.appendBytecodeVersionDiff(name: String, diff: BytecodeVersionDiff) { - if (diff.changed) { - appendLine() - appendLine("$name:") - appendLine() - appendLine( - buildString { - if (diff.versionCounts.isNotEmpty()) { - appendLine( - diffuseTable { - header { - row { - cell("version") - cell("old") - cell("new") - cell("diff") - } - } + if (!diff.changed) return + appendLine() + appendLine("$name:") + appendLine() - body { - cellStyle { alignment = MiddleRight } - - for ((version, counts) in diff.versionCounts) { - val (oldCount, newCount) = counts - val net = (newCount.count - oldCount.count).toDiffString() - val added = - (newCount.count - oldCount.count) - .coerceAtLeast(0) - .toDiffString(zeroSign = '+') - val removed = - (-(oldCount.count - newCount.count).coerceAtLeast(0)).toDiffString( - zeroSign = '-' - ) - row(version.version, oldCount.count, newCount.count, "$net ($added $removed)") - } - } - } - .renderText() - .lines() - .joinToString("\n") { it.trimEnd() } - ) + if (diff.versionCounts.isNotEmpty()) { + diffuseTable { + header { + row { + cell("version") + cell("old") + cell("new") + cell("diff") } - if (diff.changedClasses.isNotEmpty()) { - if (diff.versionCounts.isNotEmpty()) { - appendLine() - } - diff.changedClasses.forEach { (descriptor, oldVersion, newVersion) -> - appendLine("$descriptor: ${oldVersion.version} → ${newVersion.version}") - } + } + + body { + cellStyle { alignment = MiddleRight } + + for ((version, counts) in diff.versionCounts) { + val (oldCount, newCount) = counts + val net = (newCount.count - oldCount.count).toDiffString() + val added = + (newCount.count - oldCount.count).coerceAtLeast(0).toDiffString(zeroSign = '+') + val removed = + (-(oldCount.count - newCount.count).coerceAtLeast(0)).toDiffString(zeroSign = '-') + row(version.version, oldCount.count, newCount.count, "$net ($added $removed)") } } - .lineSequence() - .joinToString("\n") { if (it.isBlank()) "" else " $it" } - ) + } + .renderText() + .prependIndent(" ") + .let(::appendLine) + } + + if (diff.changedClasses.isNotEmpty()) { + if (diff.versionCounts.isNotEmpty()) { + appendLine() + } + diff.changedClasses.forEach { (descriptor, oldVersion, newVersion) -> + appendLine(" $descriptor: ${oldVersion.version} → ${newVersion.version}") + } } + appendLine() } diff --git a/reports/src/test/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiffTest.kt b/reports/src/test/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiffTest.kt index c255af1d..7efcef97 100644 --- a/reports/src/test/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiffTest.kt +++ b/reports/src/test/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiffTest.kt @@ -40,10 +40,10 @@ class BytecodeVersionDiffTest { | |BYTECODE VERSIONS: | - | version │ old │ new │ diff + | version │ old │ new │ diff | ─────────┼─────┼─────┼──────────── - | 65 │ 1 │ 0 │ -1 (+0 -1) - | 69 │ 0 │ 1 │ +1 (+1 -0) + | 65 │ 1 │ 0 │ -1 (+0 -1) + | 69 │ 0 │ 1 │ +1 (+1 -0) | | org.example.MainKt: 65 → 69 | @@ -64,8 +64,8 @@ class BytecodeVersionDiffTest { ), changedClasses = listOf( - Triple(TypeDescriptor("Lcom/example/ClassB;"), Version(61), Version(65)), // Up Triple(TypeDescriptor("Lcom/example/ClassA;"), Version(61), Version(65)), // Up + Triple(TypeDescriptor("Lcom/example/ClassB;"), Version(61), Version(65)), // Up ), ) @@ -75,13 +75,13 @@ class BytecodeVersionDiffTest { | |BYTECODE VERSIONS: | - | version │ old │ new │ diff + | version │ old │ new │ diff | ─────────┼─────┼─────┼──────────── - | 61 │ 3 │ 1 │ -2 (+0 -2) - | 65 │ 0 │ 2 │ +2 (+2 -0) + | 61 │ 3 │ 1 │ -2 (+0 -2) + | 65 │ 0 │ 2 │ +2 (+2 -0) | - | com.example.ClassB: 61 → 65 | com.example.ClassA: 61 → 65 + | com.example.ClassB: 61 → 65 | |""" .trimMargin() @@ -111,10 +111,10 @@ class BytecodeVersionDiffTest { | |BYTECODE VERSIONS: | - | version │ old │ new │ diff + | version │ old │ new │ diff | ─────────┼─────┼─────┼──────────── - | 65 │ 0 │ 1 │ +1 (+1 -0) - | 69 │ 1 │ 0 │ -1 (+0 -1) + | 65 │ 0 │ 1 │ +1 (+1 -0) + | 69 │ 1 │ 0 │ -1 (+0 -1) | | org.example.MainKt: 69 → 65 | @@ -135,8 +135,8 @@ class BytecodeVersionDiffTest { ), changedClasses = listOf( - Triple(TypeDescriptor("Lcom/example/ClassB;"), Version(65), Version(61)), // Down Triple(TypeDescriptor("Lcom/example/ClassA;"), Version(65), Version(61)), // Down + Triple(TypeDescriptor("Lcom/example/ClassB;"), Version(65), Version(61)), // Down ), ) @@ -146,13 +146,13 @@ class BytecodeVersionDiffTest { | |BYTECODE VERSIONS: | - | version │ old │ new │ diff + | version │ old │ new │ diff | ─────────┼─────┼─────┼──────────── - | 61 │ 0 │ 2 │ +2 (+2 -0) - | 65 │ 2 │ 0 │ -2 (+0 -2) + | 61 │ 0 │ 2 │ +2 (+2 -0) + | 65 │ 2 │ 0 │ -2 (+0 -2) | - | com.example.ClassB: 65 → 61 | com.example.ClassA: 65 → 61 + | com.example.ClassB: 65 → 61 | |""" .trimMargin() @@ -183,10 +183,10 @@ class BytecodeVersionDiffTest { | |BYTECODE VERSIONS: | - | version │ old │ new │ diff + | version │ old │ new │ diff | ─────────┼─────┼─────┼──────────── - | 65 │ 2 │ 1 │ -1 (+0 -1) - | 69 │ 0 │ 1 │ +1 (+1 -0) + | 65 │ 2 │ 1 │ -1 (+0 -1) + | 69 │ 0 │ 1 │ +1 (+1 -0) | | com.example.ClassA: 61 → 65 | com.example.ClassB: 65 → 61 @@ -215,9 +215,9 @@ class BytecodeVersionDiffTest { | |BYTECODE VERSIONS: | - | version │ old │ new │ diff + | version │ old │ new │ diff | ─────────┼─────┼─────┼──────────── - | 65 │ 0 │ 2 │ +2 (+2 -0) + | 65 │ 0 │ 2 │ +2 (+2 -0) | |""" .trimMargin() @@ -242,9 +242,9 @@ class BytecodeVersionDiffTest { | |BYTECODE VERSIONS: | - | version │ old │ new │ diff + | version │ old │ new │ diff | ─────────┼─────┼─────┼──────────── - | 61 │ 2 │ 0 │ -2 (+0 -2) + | 61 │ 2 │ 0 │ -2 (+0 -2) | |""" .trimMargin() From 8a36e339fdc99df3bcbc2c6cf6dad8253b784e1f Mon Sep 17 00:00:00 2001 From: Goooler Date: Fri, 27 Mar 2026 10:18:34 +0800 Subject: [PATCH 6/6] Remove filter --- .../jakewharton/diffuse/diff/BytecodeVersionDiff.kt | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiff.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiff.kt index 608b212a..922e96fb 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiff.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/BytecodeVersionDiff.kt @@ -55,14 +55,11 @@ internal fun bytecodeVersionDiff(oldJars: List, newJars: List): Byteco // Tally per-version counts across all classes in old and new. val allVersions = (oldVersionMap.values + newVersionMap.values).toSortedSet() - val versionCounts = - allVersions - .associateWith { version -> - val oldCount = oldVersionMap.values.count { it == version } - val newCount = newVersionMap.values.count { it == version } - BytecodeVersionDiff.Count(oldCount) to BytecodeVersionDiff.Count(newCount) - } - .filter { (_, counts) -> counts.first != counts.second } + val versionCounts = allVersions.associateWith { version -> + val oldCount = oldVersionMap.values.count { it == version } + val newCount = newVersionMap.values.count { it == version } + BytecodeVersionDiff.Count(oldCount) to BytecodeVersionDiff.Count(newCount) + } return BytecodeVersionDiff(versionCounts, changedClasses) }