Skip to content

Commit

Permalink
Address review feedback
Browse files Browse the repository at this point in the history
1. Output detailed hash data as json
2. Use a different serialization format for targethashes to avoid
   ambiguity
  • Loading branch information
alex-torok committed Sep 30, 2024
1 parent 27b0b68 commit f121539
Show file tree
Hide file tree
Showing 10 changed files with 119 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ class GenerateHashesCommand : Callable<Int> {
var ignoredRuleHashingAttributes: Set<String> = emptySet()

@CommandLine.Option(
names = ["-d", "--depsFile"],
description = ["Path to the file where dependency edges are written to. If not specified, the dependency edges will not be written to a file."],
names = ["-d", "--depEdgesFile"],
description = ["Path to the file where dependency edges are written to. If not specified, the dependency edges will not be written to a file. Needed for computing build graph distance metrics. See bazel-diff docs for more details about build graph distance metrics."],
scope = CommandLine.ScopeType.INHERIT,
defaultValue = CommandLine.Parameters.NULL_VALUE
)
Expand Down
41 changes: 17 additions & 24 deletions cli/src/main/kotlin/com/bazel_diff/cli/GetImpactedTargetsCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class GetImpactedTargetsCommand : Callable<Int> {
lateinit var finalHashesJSONPath: File

@CommandLine.Option(
names = ["-d", "--depsFile"],
names = ["-d", "--depEdgesFile"],
description = ["Path to the file where dependency edges are. If specified, build graph distance metrics will be computed from the given hash data."],
scope = CommandLine.ScopeType.INHERIT,
defaultValue = CommandLine.Parameters.NULL_VALUE
Expand All @@ -58,7 +58,7 @@ class GetImpactedTargetsCommand : Callable<Int> {
@CommandLine.Option(
names = ["-o", "--output"],
scope = CommandLine.ScopeType.LOCAL,
description = ["Filepath to write the impacted Bazel targets to, newline separated. If not specified, the targets will be written to STDOUT."],
description = ["Filepath to write the impacted Bazel targets to. If using depEdgesFile: formatted in json, otherwise: newline separated. If not specified, the output will be written to STDOUT."],
)
var outputPath: File? = null

Expand All @@ -78,30 +78,17 @@ class GetImpactedTargetsCommand : Callable<Int> {
val from = deserialiser.executeTargetHash(startingHashesJSONPath)
val to = deserialiser.executeTargetHash(finalHashesJSONPath)

val typeFilter = TargetTypeFilter(targetType, to)

val impactedTargetsStream = if (depsMappingJSONPath != null) {
val depsMapping = deserialiser.deserializeDeps(depsMappingJSONPath!!)
CalculateImpactedTargetsInteractor().executeWithDistances(from, to, depsMapping)
.filterKeys { typeFilter.accepts(it) }
.map { (label, metrics) ->
"${label}~${metrics.targetDistance}~${metrics.packageDistance}"
}.stream()
} else {
CalculateImpactedTargetsInteractor().execute(from, to)
.filter { typeFilter.accepts(it) }
.stream()
}

return try {
BufferedWriter(when (val path = outputPath) {
val outputWriter = BufferedWriter(when (val path = outputPath) {
null -> FileWriter(FileDescriptor.out)
else -> FileWriter(path)
}).use { writer ->
impactedTargetsStream.forEach { line ->
writer.write(line)
writer.write("\n")
}
})

return try {
if (depsMappingJSONPath != null) {
val depsMapping = deserialiser.deserializeDeps(depsMappingJSONPath!!)
CalculateImpactedTargetsInteractor().executeWithDistances(from, to, depsMapping, outputWriter, targetType)
} else {
CalculateImpactedTargetsInteractor().execute(from, to, outputWriter, targetType)
}
CommandLine.ExitCode.OK
} catch (e: IOException) {
Expand All @@ -122,5 +109,11 @@ class GetImpactedTargetsCommand : Callable<Int> {
"Incorrect final hashes: file doesn't exist or can't be read."
)
}
if (depsFile != null && !depsFile.canRead()) {
throw CommandLine.ParameterException(
spec.commandLine(),
"Incorrect dep edges file: file doesn't exist or can't be read."
)
}
}
}
6 changes: 3 additions & 3 deletions cli/src/main/kotlin/com/bazel_diff/hash/TargetDigest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ data class TargetDigest(
val deps: List<String>? = null,
) {
fun clone(newDeps: List<String>? = null): TargetDigest {
var newDeps = newDeps
var toUse = newDeps
if (newDeps == null) {
newDeps = deps
toUse = deps
}
return TargetDigest(overallDigest.clone(), directDigest.clone(), newDeps)
return TargetDigest(overallDigest.clone(), directDigest.clone(), toUse)
}
}

Expand Down
16 changes: 11 additions & 5 deletions cli/src/main/kotlin/com/bazel_diff/hash/TargetHash.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ data class TargetHash(
val deps: List<String>? = null
) {
val hashWithType by lazy {
"${type}#${hash}#${directHash}"
"${type}#${hash}~${directHash}"
}

val totalHash by lazy {
"${hash}#${directHash}"
"${hash}~${directHash}"
}

fun toJson(includeTargetType: Boolean): String {
Expand All @@ -30,9 +30,15 @@ data class TargetHash(
fun fromJson(json: String): TargetHash {
val parts = json.split("#")
return when (parts.size) {
2 -> TargetHash("", parts[0], parts[1])
3 -> TargetHash(parts[0], parts[1], parts[2])
else -> throw IllegalArgumentException("Invalid JSON format")
1 -> Pair("", parts[0])
2 -> Pair(parts[0], parts[1])
else -> throw IllegalArgumentException("Invalid targetHash format: $json")
}.let { (type, hash) ->
val hashes = hash.split("~")
when (hashes.size) {
2 -> TargetHash(type, hashes[0], hashes[1])
else -> throw IllegalArgumentException("Invalid targetHash format: $json")
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package com.bazel_diff.interactor

import com.bazel_diff.hash.TargetHash
import com.google.common.collect.Maps
import com.google.gson.Gson
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File
import java.io.Writer
import java.util.concurrent.ConcurrentMap
import java.util.concurrent.ConcurrentHashMap
import java.util.stream.Stream
Expand All @@ -16,6 +19,7 @@ data class TargetDistanceMetrics(
) {}

class CalculateImpactedTargetsInteractor : KoinComponent {
private val gson: Gson by inject()

@VisibleForTesting
class InvalidDependencyEdgesException(message: String) : Exception(message)
Expand All @@ -25,10 +29,24 @@ class CalculateImpactedTargetsInteractor : KoinComponent {
INDIRECT
}

fun execute(from: Map<String, TargetHash>, to: Map<String, TargetHash>): Set<String> {
fun execute(from: Map<String, TargetHash>, to: Map<String, TargetHash>, outputWriter: Writer, targetTypes: Set<String>?) {
/**
* This call might be faster if end hashes is a sorted map
*/
val typeFilter = TargetTypeFilter(targetTypes, to)

computeSimpleImpactedTargets(from, to)
.filter { typeFilter.accepts(it) }
.let { impactedTargets ->
outputWriter.use { writer ->
impactedTargets.forEach {
writer.write("$it\n")
}
}
}
}

fun computeSimpleImpactedTargets(from: Map<String, TargetHash>, to: Map<String, TargetHash>): Set<String> {
val difference = Maps.difference(to, from)
val onlyInEnd: Set<String> = difference.entriesOnlyOnLeft().keys
val changed: Set<String> = difference.entriesDiffering().keys
Expand All @@ -39,7 +57,27 @@ class CalculateImpactedTargetsInteractor : KoinComponent {
return impactedTargets
}

fun executeWithDistances(from: Map<String, TargetHash>, to: Map<String, TargetHash>, depEdges: Map<String, List<String>>): Map<String, TargetDistanceMetrics> {
fun executeWithDistances(from: Map<String, TargetHash>, to: Map<String, TargetHash>, depEdges: Map<String, List<String>>, outputWriter: Writer, targetTypes: Set<String>?) {
val typeFilter = TargetTypeFilter(targetTypes, to)

computeAllDistances(from, to, depEdges)
.filterKeys { typeFilter.accepts(it) }
.let { impactedTargets ->
outputWriter.use { writer ->
writer.write(gson.toJson(
impactedTargets.map{
mapOf(
"label" to it.key,
"targetDistance" to it.value.targetDistance,
"packageDistance" to it.value.packageDistance
)
}
))
}
}
}

fun computeAllDistances(from: Map<String, TargetHash>, to: Map<String, TargetHash>, depEdges: Map<String, List<String>>): Map<String, TargetDistanceMetrics> {
val difference = Maps.difference(to, from)

val newLabels = difference.entriesOnlyOnLeft().keys
Expand Down
22 changes: 18 additions & 4 deletions cli/src/test/kotlin/com/bazel_diff/e2e/E2ETest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.bazel_diff.e2e
import assertk.assertThat
import assertk.assertions.isEqualTo
import com.bazel_diff.cli.BazelDiff
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
Expand Down Expand Up @@ -46,11 +48,23 @@ class E2ETest {
listOf("get-impacted-targets", "-sh", from.absolutePath, "-fh", to.absolutePath, "-o", impactedTargetsOutput.absolutePath) + extraGetImpactedTargetsArgs + if (computeDistances) listOf("-d", depsFile.absolutePath) else emptyList()
)

val actual: Set<String> = impactedTargetsOutput.readLines().filter { it.isNotBlank() }.toSet()
val expected: Set<String> =
javaClass.getResourceAsStream(expectedResultFile).use { it.bufferedReader().readLines().filter { it.isNotBlank() }.toSet() }
if (!computeDistances) {
val actual: Set<String> = impactedTargetsOutput.readLines().filter { it.isNotBlank() }.toSet()
val expected: Set<String> =
javaClass.getResourceAsStream(expectedResultFile).use { it.bufferedReader().readLines().filter { it.isNotBlank() }.toSet() }

assertThat(actual).isEqualTo(expected)
} else {
// When computing target distances, the output format is json. Read the files and assert the sorted contents.
val gson = Gson()
val shape = object : TypeToken<List<Map<String, Any>>>() {}.type
val actual = gson.fromJson<List<Map<String, Any>>>(impactedTargetsOutput.readText(), shape).sortedBy { it["label"] as String }
val expected = javaClass.getResourceAsStream(expectedResultFile).use {
gson.fromJson<List<Map<String, Any>>>(it.bufferedReader().readText(), shape).sortedBy { it["label"] as String }
}

assertThat(actual).isEqualTo(expected)
assertThat(actual).isEqualTo(expected)
}
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class CalculateImpactedTargetsInteractorTest : KoinTest {
end["3"] = "d"
val endHashes = end.mapValues { TargetHash("", it.value, it.value) }

val impacted = interactor.execute(startHashes, endHashes)
val impacted = interactor.computeSimpleImpactedTargets(startHashes, endHashes)
assertThat(impacted).containsExactlyInAnyOrder(
"1", "3"
)
Expand All @@ -53,7 +53,7 @@ class CalculateImpactedTargetsInteractorTest : KoinTest {
makeIndirectlyChanged(endHashes, "//:3")

val interactor = CalculateImpactedTargetsInteractor()
val impacted = interactor.executeWithDistances(startHashes, endHashes, depEdges)
val impacted = interactor.computeAllDistances(startHashes, endHashes, depEdges)

assertThat(impacted).containsOnly(
"//:1" to TargetDistanceMetrics(0, 0),
Expand All @@ -72,7 +72,7 @@ class CalculateImpactedTargetsInteractorTest : KoinTest {
)

val interactor = CalculateImpactedTargetsInteractor()
val impacted = interactor.executeWithDistances(startHashes, endHashes, mapOf())
val impacted = interactor.computeAllDistances(startHashes, endHashes, mapOf())

assertThat(impacted).containsOnly(
"//:1" to TargetDistanceMetrics(0, 0),
Expand All @@ -91,7 +91,7 @@ class CalculateImpactedTargetsInteractorTest : KoinTest {
makeIndirectlyChanged(endHashes, "//:2", "//:3")

val interactor = CalculateImpactedTargetsInteractor()
val impacted = interactor.executeWithDistances(startHashes, endHashes, depEdges)
val impacted = interactor.computeAllDistances(startHashes, endHashes, depEdges)

assertThat(impacted).containsOnly(
"//:1" to TargetDistanceMetrics(0,0),
Expand All @@ -111,7 +111,7 @@ class CalculateImpactedTargetsInteractorTest : KoinTest {
makeIndirectlyChanged(endHashes, "//A:2", "//B:3", "//B:4", "//C:5")

val interactor = CalculateImpactedTargetsInteractor()
val impacted = interactor.executeWithDistances(startHashes, endHashes, depEdges)
val impacted = interactor.computeAllDistances(startHashes, endHashes, depEdges)

assertThat(impacted).containsOnly(
"//A:1" to TargetDistanceMetrics(0,0),
Expand Down Expand Up @@ -139,7 +139,7 @@ class CalculateImpactedTargetsInteractorTest : KoinTest {
makeIndirectlyChanged(endHashes, "//A:target_1", "//A:target_2", "//A:target_3", "//A:target_4", "//B:target", "//C:target", "//D:target", "//final:final")

val interactor = CalculateImpactedTargetsInteractor()
val impacted = interactor.executeWithDistances(startHashes, endHashes, depEdges)
val impacted = interactor.computeAllDistances(startHashes, endHashes, depEdges)

assertThat(impacted["//final:final"]).isEqualTo(TargetDistanceMetrics(4,2))
}
Expand All @@ -161,7 +161,7 @@ class CalculateImpactedTargetsInteractorTest : KoinTest {
makeIndirectlyChanged(endHashes, "//:2", "//:3")

val interactor = CalculateImpactedTargetsInteractor()
val impacted = interactor.executeWithDistances(startHashes, endHashes, depEdges)
val impacted = interactor.computeAllDistances(startHashes, endHashes, depEdges)

assertThat(impacted).containsOnly(
"//:1" to TargetDistanceMetrics(0,0),
Expand All @@ -181,12 +181,12 @@ class CalculateImpactedTargetsInteractorTest : KoinTest {

val interactor = CalculateImpactedTargetsInteractor()
assertThat {
interactor.executeWithDistances(startHashes, endHashes, depEdges)
interactor.computeAllDistances(startHashes, endHashes, depEdges)
}.isFailure().message().isEqualTo("//:2 was indirectly impacted, but has no impacted dependencies.")

assertThat {
// empty dep edges
interactor.executeWithDistances(startHashes, endHashes, mapOf())
interactor.computeAllDistances(startHashes, endHashes, mapOf())
}.isFailure().message().isEqualTo("//:2 was indirectly impacted, but has no dependencies.")

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class DeserialiseHashesInteractorTest : KoinTest {
@Test
fun testDeserialisation() {
val file = temp.newFile().apply {
writeText("""{"target-name":"hash#direct"}""")
writeText("""{"target-name":"hash~direct"}""")
}

val actual = interactor.executeTargetHash(file)
Expand All @@ -44,9 +44,9 @@ class DeserialiseHashesInteractorTest : KoinTest {
fun testDeserialisationWithType() {
val file = temp.newFile().apply {
writeText("""{
| "target-1":"GeneratedFile#hash1#direct1",
| "target-2":"Rule#hash2#direct2",
| "target-3":"SourceFile#hash3#direct3"
| "target-1":"GeneratedFile#hash1~direct1",
| "target-2":"Rule#hash2~direct2",
| "target-3":"SourceFile#hash3~direct3"
|}""".trimMargin())
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//test/java/com/integration:bazel-diff-integration-test-lib~0~0
//test/java/com/integration:bazel-diff-integration-tests~1~0
//test/java/com/integration:TestStringGenerator.java~0~0
[
{"label": "//test/java/com/integration:bazel-diff-integration-test-lib", "targetDistance": 0, "packageDistance": 0},
{"label": "//test/java/com/integration:bazel-diff-integration-tests", "targetDistance": 1, "packageDistance": 0},
{"label": "//test/java/com/integration:TestStringGenerator.java", "targetDistance": 0, "packageDistance": 0}
]
18 changes: 10 additions & 8 deletions cli/src/test/resources/fixture/impacted_targets_distances-1-2.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
//test/java/com/integration:libbazel-diff-integration-test-lib-src.jar~0~0
//test/java/com/integration:TestStringGenerator.java~0~0
//test/java/com/integration:bazel-diff-integration-test-lib~0~0
//test/java/com/integration:bazel-diff-integration-tests-src.jar~2~0
//test/java/com/integration:libbazel-diff-integration-test-lib.jar~0~0
//test/java/com/integration:bazel-diff-integration-tests~1~0
//test/java/com/integration:bazel-diff-integration-tests_deploy-src.jar~2~0
//test/java/com/integration:bazel-diff-integration-tests.jar~2~0
[
{"label": "//test/java/com/integration:libbazel-diff-integration-test-lib-src.jar", "targetDistance": 0, "packageDistance": 0},
{"label": "//test/java/com/integration:TestStringGenerator.java", "targetDistance": 0, "packageDistance": 0},
{"label": "//test/java/com/integration:bazel-diff-integration-test-lib", "targetDistance": 0, "packageDistance": 0},
{"label": "//test/java/com/integration:bazel-diff-integration-tests-src.jar", "targetDistance": 2, "packageDistance": 0},
{"label": "//test/java/com/integration:libbazel-diff-integration-test-lib.jar", "targetDistance": 0, "packageDistance": 0},
{"label": "//test/java/com/integration:bazel-diff-integration-tests", "targetDistance": 1, "packageDistance": 0},
{"label": "//test/java/com/integration:bazel-diff-integration-tests_deploy-src.jar", "targetDistance": 2, "packageDistance": 0},
{"label": "//test/java/com/integration:bazel-diff-integration-tests.jar", "targetDistance": 2, "packageDistance": 0}
]

0 comments on commit f121539

Please sign in to comment.