diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD index 8857dca62b3cf2..6b5ed065a5f87f 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD +++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD @@ -215,3 +215,21 @@ java_library( "//third_party:guava", ], ) + +java_library( + name = "inspection", + srcs = [ + "BazelModuleInspectorFunction.java", + "BazelModuleInspectorValue.java", + ], + deps = [ + ":common", + ":resolution", + "//src/main/java/com/google/devtools/build/lib/skyframe:sky_functions", + "//src/main/java/com/google/devtools/build/lib/skyframe/serialization/autocodec:serialization-constant", + "//src/main/java/com/google/devtools/build/skyframe", + "//src/main/java/com/google/devtools/build/skyframe:skyframe-objects", + "//third_party:auto_value", + "//third_party:guava", + ], +) diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleInspectorFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleInspectorFunction.java new file mode 100644 index 00000000000000..3e5d1a6bd1947c --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleInspectorFunction.java @@ -0,0 +1,154 @@ +// Copyright 2022 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package com.google.devtools.build.lib.bazel.bzlmod; + +import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static com.google.common.collect.ImmutableSet.toImmutableSet; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.devtools.build.lib.bazel.bzlmod.BazelModuleInspectorValue.AugmentedModule; +import com.google.devtools.build.lib.bazel.bzlmod.BazelModuleInspectorValue.AugmentedModule.ResolutionReason; +import com.google.devtools.build.lib.bazel.bzlmod.ModuleFileValue.RootModuleFileValue; +import com.google.devtools.build.skyframe.SkyFunction; +import com.google.devtools.build.skyframe.SkyFunctionException; +import com.google.devtools.build.skyframe.SkyKey; +import com.google.devtools.build.skyframe.SkyValue; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +/** + * Precomputes an augmented version of the un-pruned dep graph that is used for dep graph + * inspection. By this stage, the Bazel module resolution should have been completed. + */ +public class BazelModuleInspectorFunction implements SkyFunction { + + @Override + public SkyValue compute(SkyKey skyKey, Environment env) + throws SkyFunctionException, InterruptedException { + RootModuleFileValue root = + (RootModuleFileValue) env.getValue(ModuleFileValue.KEY_FOR_ROOT_MODULE); + if (root == null) { + return null; + } + BazelModuleResolutionValue resolutionValue = + (BazelModuleResolutionValue) env.getValue(BazelModuleResolutionValue.KEY); + if (resolutionValue == null) { + return null; + } + ImmutableMap overrides = root.getOverrides(); + ImmutableMap unprunedDepGraph = resolutionValue.getUnprunedDepGraph(); + ImmutableMap resolvedDepGraph = resolutionValue.getDepGraph(); + + ImmutableMap depGraph = + computeAugmentedGraph(unprunedDepGraph, resolvedDepGraph.keySet(), overrides); + + // Group all ModuleKeys seen by their module name for easy lookup + ImmutableMap> modulesIndex = + ImmutableMap.copyOf( + depGraph.values().stream() + .collect( + Collectors.groupingBy( + AugmentedModule::getName, + Collectors.mapping(AugmentedModule::getKey, toImmutableSet())))); + + return BazelModuleInspectorValue.create(depGraph, modulesIndex); + } + + public static ImmutableMap computeAugmentedGraph( + ImmutableMap unprunedDepGraph, + ImmutableSet usedModules, + ImmutableMap overrides) { + Map depGraphAugmentBuilder = new HashMap<>(); + + // For all Modules in the un-pruned dep graph, inspect their dependencies and add themselves + // to their children AugmentedModule as dependant. Also fill in their own AugmentedModule + // with a map from their dependencies to the resolution reason that was applied to each. + // The newly created graph will also contain ModuleAugments for non-loaded modules. + for (Entry e : unprunedDepGraph.entrySet()) { + ModuleKey parentKey = e.getKey(); + Module parentModule = e.getValue(); + + AugmentedModule.Builder parentBuilder = + depGraphAugmentBuilder + .computeIfAbsent( + parentKey, k -> AugmentedModule.builder(k).setName(parentModule.getName())) + .setVersion(parentModule.getVersion()) + .setLoaded(true); + + for (String childDep : parentModule.getDeps().keySet()) { + ModuleKey originalKey = parentModule.getOriginalDeps().get(childDep); + Module originalModule = unprunedDepGraph.get(originalKey); + ModuleKey key = parentModule.getDeps().get(childDep); + Module module = unprunedDepGraph.get(key); + + AugmentedModule.Builder originalChildBuilder = + depGraphAugmentBuilder.computeIfAbsent(originalKey, AugmentedModule::builder); + if (originalModule != null) { + originalChildBuilder + .setName(originalModule.getName()) + .setVersion(originalModule.getVersion()) + .setLoaded(true); + } + + AugmentedModule.Builder newChildBuilder = + depGraphAugmentBuilder.computeIfAbsent( + key, + k -> + AugmentedModule.builder(k) + .setName(module.getName()) + .setVersion(module.getVersion()) + .setLoaded(true)); + + // originalDependants and dependants can differ because + // parentModule could have had originalChild in the unresolved graph, but in the resolved + // graph the originalChild could have become orphan due to an override or selection + originalChildBuilder.addOriginalDependant(parentKey); + // also, even if the dep has not changed, the parentModule may not be referenced + // anymore in the resolved graph, so parentModule will only be added above + if (usedModules.contains(parentKey)) { + newChildBuilder.addDependant(parentKey); + } + + ResolutionReason reason = ResolutionReason.ORIGINAL; + if (!key.getVersion().equals(originalKey.getVersion())) { + ModuleOverride override = overrides.get(key.getName()); + if (override != null) { + if (override instanceof SingleVersionOverride) { + reason = ResolutionReason.SINGLE_VERSION_OVERRIDE; + } else if (override instanceof MultipleVersionOverride) { + reason = ResolutionReason.MULTIPLE_VERSION_OVERRIDE; + } else { + // There is no other possible override + Preconditions.checkArgument(override instanceof NonRegistryOverride); + reason = ResolutionReason.NON_REGISTRY_OVERRIDE; + } + } else { + reason = ResolutionReason.MINIMAL_VERSION_SELECTION; + } + } + + parentBuilder.addDep(key, reason); + } + } + + return depGraphAugmentBuilder.entrySet().stream() + .collect(toImmutableMap(Entry::getKey, e -> e.getValue().build())); + } +} diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleInspectorValue.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleInspectorValue.java new file mode 100644 index 00000000000000..76b56ac07de9ee --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleInspectorValue.java @@ -0,0 +1,168 @@ +// Copyright 2022 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package com.google.devtools.build.lib.bazel.bzlmod; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.devtools.build.lib.skyframe.SkyFunctions; +import com.google.devtools.build.lib.skyframe.serialization.autocodec.SerializationConstant; +import com.google.devtools.build.skyframe.SkyKey; +import com.google.devtools.build.skyframe.SkyValue; + +/** + * The result of running Bazel module inspection pre-processing, containing the un-pruned and + * augmented wrappers of the Bazel module dependency graph (post-version-resolution). + */ +@AutoValue +public abstract class BazelModuleInspectorValue implements SkyValue { + + @SerializationConstant + public static final SkyKey KEY = () -> SkyFunctions.BAZEL_MODULE_INSPECTION; + + public static BazelModuleInspectorValue create( + ImmutableMap depGraph, + ImmutableMap> modulesIndex) { + return new AutoValue_BazelModuleInspectorValue(depGraph, modulesIndex); + } + + /** + * The (bidirectional) inspection dep graph, containing wrappers of the {@link Module}, augmented + * with references to dependants. The order is non-deterministic, inherited from the {@code + * completeDepGraph} of {@link BazelModuleResolutionValue}. For any KEY in the returned map, it's + * guaranteed that {@code depGraph[KEY].getKey() == KEY}. + */ + public abstract ImmutableMap getDepGraph(); + + /** + * Index of all module keys mentioned in the un-pruned dep graph (loaded or not) for easy lookup. + * It is a map from module name to the set of {@link ModuleKey}s that point to a version of + * that module. + */ + public abstract ImmutableMap> getModulesIndex(); + + /** + * A wrapper for {@link Module}, augmented with references to dependants (and also those who are + * not used in the final dep graph). + */ + @AutoValue + abstract static class AugmentedModule { + /** Name of the module. Same as in {@link Module}. */ + abstract String getName(); + + /** Version of the module. Same as in {@link Module}. */ + abstract Version getVersion(); + + /** {@link ModuleKey} of this module. Same as in {@link Module} */ + abstract ModuleKey getKey(); + + /** + * The set of modules in the resolved dep graph that depend on this module + * after the module resolution. + */ + abstract ImmutableSet getDependants(); + + /** + * The set of modules in the complete dep graph that originally depended on this module *before* + * the module resolution (can contain unused nodes). + */ + abstract ImmutableSet getOriginalDependants(); + + /** + * A map from the resolved dependencies of this module to the rules that were used for their + * resolution (can be either the original dependency, changed by the Minimal-Version Selection + * algorithm or by an override rule + */ + abstract ImmutableMap getDeps(); + + /** + * Flag that tell whether the module was loaded and added to the dependency graph. Modules + * overridden by {@code single_version_override} and {@link NonRegistryOverride} are not loaded + * so their {@code originalDeps} are (yet) unknown. + */ + abstract boolean isLoaded(); + + /** Flag for checking whether the module is present in the resolved dep graph. */ + boolean isUsed() { + return !getDependants().isEmpty(); + } + + /** Returns a new {@link AugmentedModule.Builder} with {@code key} set. */ + public static AugmentedModule.Builder builder(ModuleKey key) { + return new AutoValue_BazelModuleInspectorValue_AugmentedModule.Builder() + .setName(key.getName()) + .setVersion(key.getVersion()) + .setKey(key) + .setLoaded(false); + } + + /** Builder type for {@link AugmentedModule}. */ + @AutoValue.Builder + public abstract static class Builder { + public abstract AugmentedModule.Builder setName(String value); + + public abstract AugmentedModule.Builder setVersion(Version value); + + public abstract AugmentedModule.Builder setKey(ModuleKey value); + + public abstract AugmentedModule.Builder setLoaded(boolean value); + + public abstract AugmentedModule.Builder setOriginalDependants(ImmutableSet value); + + public abstract AugmentedModule.Builder setDependants(ImmutableSet value); + + public abstract AugmentedModule.Builder setDeps( + ImmutableMap value); + + abstract ImmutableSet.Builder originalDependantsBuilder(); + + public AugmentedModule.Builder addOriginalDependant(ModuleKey depKey) { + originalDependantsBuilder().add(depKey); + return this; + } + + abstract ImmutableSet.Builder dependantsBuilder(); + + public AugmentedModule.Builder addDependant(ModuleKey depKey) { + dependantsBuilder().add(depKey); + return this; + } + + abstract ImmutableMap.Builder depsBuilder(); + + public AugmentedModule.Builder addDep(ModuleKey depKey, ResolutionReason reason) { + depsBuilder().put(depKey, reason); + return this; + } + + abstract AugmentedModule build(); + } + + /** The reason why a final dependency of a module was resolved the way it was. */ + enum ResolutionReason { + /** The dependency is the original dependency defined in the MODULE.bazel file. */ + ORIGINAL, + /** The dependency was replaced by the Minimal-Version Selection algorithm. */ + MINIMAL_VERSION_SELECTION, + /** The dependency was replaced by a {@code single_version_override} rule. */ + SINGLE_VERSION_OVERRIDE, + /** The dependency was replaced by a {@code multiple_version_override} rule. */ + MULTIPLE_VERSION_OVERRIDE, + /** The dependency was replaced by a {@link NonRegistryOverride} rule. */ + NON_REGISTRY_OVERRIDE + } + } +} diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java index 169647327f563c..469e8761892b66 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java +++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java @@ -141,6 +141,8 @@ public final class SkyFunctions { SkyFunctionName.createNonHermetic("BUILD_DRIVER"); public static final SkyFunctionName BAZEL_MODULE_RESOLUTION = SkyFunctionName.createHermetic("BAZEL_MODULE_RESOLUTION"); + public static final SkyFunctionName BAZEL_MODULE_INSPECTION = + SkyFunctionName.createHermetic("BAZEL_MODULE_INSPECTION"); public static final SkyFunctionName MODULE_EXTENSION_RESOLUTION = SkyFunctionName.createHermetic("MODULE_EXTENSION_RESOLUTION"); public static final SkyFunctionName SINGLE_EXTENSION_USAGES = diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD index 5dfb4d38f5f184..7f3fd0fe74689e 100644 --- a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD +++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD @@ -32,6 +32,7 @@ java_library( "//src/main/java/com/google/devtools/build/lib/analysis:server_directories", "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:common", "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:exception", + "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:inspection", "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:module_extension", "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:registry", "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:repo_rule_helper", diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleInspectorFunctionTest.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleInspectorFunctionTest.java new file mode 100644 index 00000000000000..97bba755d586e1 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleInspectorFunctionTest.java @@ -0,0 +1,581 @@ +// Copyright 2022 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package com.google.devtools.build.lib.bazel.bzlmod; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.devtools.build.lib.bazel.bzlmod.BzlmodTestUtil.createModuleKey; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.devtools.build.lib.bazel.bzlmod.BazelModuleInspectorValue.AugmentedModule; +import com.google.devtools.build.lib.bazel.bzlmod.BazelModuleInspectorValue.AugmentedModule.ResolutionReason; +import com.google.devtools.build.lib.bazel.bzlmod.BzlmodTestUtil.ModuleBuilder; +import com.google.devtools.build.lib.bazel.bzlmod.Version.ParseException; +import java.util.AbstractMap.SimpleEntry; +import java.util.Map.Entry; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link BazelModuleInspectorFunction}. */ +@RunWith(JUnit4.class) +public class BazelModuleInspectorFunctionTest { + + @Test + public void testDiamond_simple() throws Exception { + ImmutableMap unprunedDepGraph = + ImmutableMap.builder() + .put( + ModuleBuilder.create("A", Version.EMPTY) + .setKey(ModuleKey.ROOT) + .addDep("BfromA", createModuleKey("B", "1.0")) + .addDep("CfromA", createModuleKey("C", "2.0")) + .buildEntry()) + .put( + ModuleBuilder.create("B", "1.0") + .addDep("DfromB", createModuleKey("D", "2.0")) + .addOriginalDep("DfromB", createModuleKey("D", "1.0")) + .buildEntry()) + .put( + ModuleBuilder.create("C", "2.0") + .addDep("DfromC", createModuleKey("D", "2.0")) + .buildEntry()) + .put(ModuleBuilder.create("D", "1.0", 1).buildEntry()) + .put(ModuleBuilder.create("D", "2.0", 1).buildEntry()) + .buildOrThrow(); + + ImmutableSet usedModules = + ImmutableSet.of( + ModuleKey.ROOT, + createModuleKey("B", "1.0"), + createModuleKey("C", "2.0"), + createModuleKey("D", "2.0")); + + ImmutableMap depGraph = + BazelModuleInspectorFunction.computeAugmentedGraph( + unprunedDepGraph, usedModules, /*overrides*/ ImmutableMap.of()); + + assertThat(depGraph.entrySet()) + .containsExactly( + buildAugmentedModule(ModuleKey.ROOT, "A") + .addDep("B", "1.0") + .addDep("C", "2.0") + .buildEntry(), + buildAugmentedModule("B", "1.0") + .addDep("D", "2.0", ResolutionReason.MINIMAL_VERSION_SELECTION) + .addStillDependant(ModuleKey.ROOT) + .buildEntry(), + buildAugmentedModule("C", "2.0") + .addDep("D", "2.0") + .addStillDependant(ModuleKey.ROOT) + .buildEntry(), + buildAugmentedModule("D", "2.0") + .addDependant("B", "1.0") + .addStillDependant("C", "2.0") + .buildEntry(), + buildAugmentedModule("D", "1.0").addOriginalDependant("B", "1.0").buildEntry()); + } + + @Test + public void testDiamond_withFurtherRemoval() throws Exception { + ImmutableMap unprunedDepGraph = + ImmutableMap.builder() + .put( + ModuleBuilder.create("A", Version.EMPTY) + .setKey(ModuleKey.ROOT) + .addDep("B", createModuleKey("B", "1.0")) + .addDep("C", createModuleKey("C", "2.0")) + .buildEntry()) + .put( + ModuleBuilder.create("B", "1.0") + .addDep("D", createModuleKey("D", "2.0")) + .addOriginalDep("D", createModuleKey("D", "1.0")) + .buildEntry()) + .put( + ModuleBuilder.create("C", "2.0") + .addDep("D", createModuleKey("D", "2.0")) + .buildEntry()) + .put(ModuleBuilder.create("D", "2.0").buildEntry()) + .put( + ModuleBuilder.create("D", "1.0") + .addDep("E", createModuleKey("E", "1.0")) + .buildEntry()) + .put(ModuleBuilder.create("E", "1.0").buildEntry()) + .buildOrThrow(); + + ImmutableSet usedModules = + ImmutableSet.of( + ModuleKey.ROOT, + createModuleKey("B", "1.0"), + createModuleKey("C", "2.0"), + createModuleKey("D", "2.0")); + + ImmutableMap depGraph = + BazelModuleInspectorFunction.computeAugmentedGraph( + unprunedDepGraph, usedModules, /*overrides*/ ImmutableMap.of()); + + assertThat(depGraph.entrySet()) + .containsExactly( + buildAugmentedModule(ModuleKey.ROOT, "A") + .addDep("B", "1.0") + .addDep("C", "2.0") + .buildEntry(), + buildAugmentedModule("B", "1.0") + .addDep("D", "2.0", ResolutionReason.MINIMAL_VERSION_SELECTION) + .addStillDependant(ModuleKey.ROOT) + .buildEntry(), + buildAugmentedModule("C", "2.0") + .addDep("D", "2.0") + .addStillDependant(ModuleKey.ROOT) + .buildEntry(), + buildAugmentedModule("D", "2.0") + .addDependant("B", "1.0") + .addStillDependant("C", "2.0") + .buildEntry(), + buildAugmentedModule("D", "1.0") + .addDep("E", "1.0") + .addOriginalDependant("B", "1.0") + .buildEntry(), + buildAugmentedModule("E", "1.0").addOriginalDependant("D", "1.0").buildEntry()); + } + + @Test + public void testCircularDependencyDueToSelection() throws Exception { + ImmutableMap unprunedDepGraph = + ImmutableMap.builder() + .put( + ModuleBuilder.create("A", Version.EMPTY) + .setKey(ModuleKey.ROOT) + .addDep("B", createModuleKey("B", "1.0")) + .buildEntry()) + .put( + ModuleBuilder.create("B", "1.0") + .addDep("C", createModuleKey("C", "2.0")) + .buildEntry()) + .put( + ModuleBuilder.create("C", "2.0") + .addDep("B", createModuleKey("B", "1.0")) + .addOriginalDep("B", createModuleKey("B", "1.0-pre")) + .buildEntry()) + .put( + ModuleBuilder.create("B", "1.0-pre") + .addDep("D", createModuleKey("D", "1.0")) + .buildEntry()) + .put(ModuleBuilder.create("D", "1.0").buildEntry()) + .buildOrThrow(); + + ImmutableSet usedModules = + ImmutableSet.of(ModuleKey.ROOT, createModuleKey("B", "1.0"), createModuleKey("C", "2.0")); + + ImmutableMap depGraph = + BazelModuleInspectorFunction.computeAugmentedGraph( + unprunedDepGraph, usedModules, /*overrides*/ ImmutableMap.of()); + + assertThat(depGraph.entrySet()) + .containsExactly( + buildAugmentedModule(ModuleKey.ROOT, "A").addDep("B", "1.0").buildEntry(), + buildAugmentedModule("B", "1.0") + .addDep("C", "2.0") + .addStillDependant(ModuleKey.ROOT) + .addDependant("C", "2.0") + .buildEntry(), + buildAugmentedModule("C", "2.0") + .addDep("B", "1.0", ResolutionReason.MINIMAL_VERSION_SELECTION) + .addStillDependant("B", "1.0") + .buildEntry(), + buildAugmentedModule("B", "1.0-pre") + .addDep("D", "1.0") + .addOriginalDependant("C", "2.0") + .buildEntry(), + buildAugmentedModule("D", "1.0").addOriginalDependant("B", "1.0-pre").buildEntry()); + } + + @Test + public void testSingleVersionOverride_withRemoval() throws Exception { + // Original (non-resolved) dep graph + // single_version_override (C, 2.0) + // A -> B 1.0 -> C 1.0 -> D -> 1.0 + // C 2.0 -> D -> 2.0 + ImmutableMap unprunedDepGraph = + ImmutableMap.builder() + .put( + ModuleBuilder.create("A", Version.EMPTY) + .setKey(ModuleKey.ROOT) + .addDep("B", createModuleKey("B", "1.0")) + .buildEntry()) + .put( + ModuleBuilder.create("B", "1.0") + .addDep("C", createModuleKey("C", "2.0")) + .addOriginalDep("C", createModuleKey("C", "1.0")) + .buildEntry()) + .put( + ModuleBuilder.create("C", "2.0") + .addDep("D", createModuleKey("D", "2.0")) + .buildEntry()) + .put(ModuleBuilder.create("D", "2.0").buildEntry()) + .buildOrThrow(); + + ImmutableMap overrides = + ImmutableMap.of( + "C", SingleVersionOverride.create(Version.parse("2.0"), "", ImmutableList.of(), 0)); + + ImmutableSet usedModules = + ImmutableSet.of( + ModuleKey.ROOT, + createModuleKey("B", "1.0"), + createModuleKey("C", "1.0"), + createModuleKey("C", "2.0"), + createModuleKey("D", "1.0"), + createModuleKey("D", "2.0")); + + ImmutableMap depGraph = + BazelModuleInspectorFunction.computeAugmentedGraph( + unprunedDepGraph, usedModules, overrides); + + assertThat(depGraph.entrySet()) + .containsExactly( + buildAugmentedModule(ModuleKey.ROOT, "A").addDep("B", "1.0").buildEntry(), + buildAugmentedModule("B", "1.0") + .addDep("C", "2.0", ResolutionReason.SINGLE_VERSION_OVERRIDE) + .addStillDependant(ModuleKey.ROOT) + .buildEntry(), + buildAugmentedModule("C", "1.0", false).addOriginalDependant("B", "1.0").buildEntry(), + buildAugmentedModule("C", "2.0") + .addDependant("B", "1.0") + .addDep("D", "2.0") + .buildEntry(), + buildAugmentedModule("D", "2.0").addStillDependant("C", "2.0").buildEntry()); + } + + @Test + public void testNonRegistryOverride_withRemoval() throws Exception { + // Original (non-resolved) dep graph + // archive_override "file://users/user/B.zip" + // A -> B 1.0 -> C 1.0 (not loaded) + // (local) B 1.0-hotfix -> C 1.1 + ImmutableMap unprunedDepGraph = + ImmutableMap.builder() + .put( + ModuleBuilder.create("A", Version.EMPTY) + .setKey(ModuleKey.ROOT) + .addDep("B", createModuleKey("B", "")) + .addOriginalDep("B", createModuleKey("B", "1.0")) + .buildEntry()) + .put( + ModuleBuilder.create("B", "1.0") + .setKey(createModuleKey("B", "")) + .addDep("C", createModuleKey("C", "1.1")) + .buildEntry()) + .put(ModuleBuilder.create("C", "1.1").buildEntry()) + .buildOrThrow(); + + ImmutableMap overrides = + ImmutableMap.of( + "B", + ArchiveOverride.create( + ImmutableList.of("file://users/user/B.zip"), ImmutableList.of(), "", "", 0)); + + ImmutableSet usedModules = + ImmutableSet.of(ModuleKey.ROOT, createModuleKey("B", ""), createModuleKey("C", "1.1")); + + ImmutableMap depGraph = + BazelModuleInspectorFunction.computeAugmentedGraph( + unprunedDepGraph, usedModules, overrides); + + assertThat(depGraph.entrySet()) + .containsExactly( + buildAugmentedModule(ModuleKey.ROOT, "A") + .addDep("B", "", ResolutionReason.NON_REGISTRY_OVERRIDE) + .buildEntry(), + buildAugmentedModule("B", "1.0", false) + .addOriginalDependant(ModuleKey.ROOT) + .buildEntry(), + buildAugmentedModule(createModuleKey("B", ""), "B", Version.parse("1.0"), true) + .addDep("C", "1.1") + .addDependant(ModuleKey.ROOT) + .buildEntry(), + buildAugmentedModule("C", "1.1").addStillDependant("B", "").buildEntry()); + } + + @Test + public void testMultipleVersionOverride_simpleSnapToHigher() throws Exception { + // Initial dep graph + // A -> (B1)B 1.0 -> C 1.0 + // \-> (B2)B 2.0 -> C 1.5 + // \-> C 2.0 + // multiple_version_override C: [1.5, 2.0] + // multiple_version_override B: [1.0, 2.0] + ImmutableMap unprunedDepGraph = + ImmutableMap.builder() + .put( + ModuleBuilder.create("A", Version.EMPTY) + .setKey(ModuleKey.ROOT) + .addDep("B1", createModuleKey("B", "1.0")) + .addDep("B2", createModuleKey("B", "2.0")) + .addDep("C", createModuleKey("C", "2.0")) + .buildEntry()) + .put( + ModuleBuilder.create("B", "1.0") + .addDep("C", createModuleKey("C", "1.5")) + .addOriginalDep("C", createModuleKey("C", "1.0")) + .buildEntry()) + .put( + ModuleBuilder.create("B", "2.0") + .addDep("C", createModuleKey("C", "1.5")) + .buildEntry()) + .put(ModuleBuilder.create("C", "1.0").buildEntry()) + .put(ModuleBuilder.create("C", "1.5").buildEntry()) + .put(ModuleBuilder.create("C", "2.0").buildEntry()) + .buildOrThrow(); + + ImmutableMap overrides = + ImmutableMap.of( + "B", + MultipleVersionOverride.create( + ImmutableList.of(Version.parse("1.0"), Version.parse("2.0")), ""), + "C", + MultipleVersionOverride.create( + ImmutableList.of(Version.parse("1.5"), Version.parse("2.0")), "")); + + ImmutableSet usedModules = + ImmutableSet.of( + ModuleKey.ROOT, + createModuleKey("B", "1.0"), + createModuleKey("B", "2.0"), + createModuleKey("C", "1.5"), + createModuleKey("C", "2.0")); + ImmutableMap depGraph = + BazelModuleInspectorFunction.computeAugmentedGraph( + unprunedDepGraph, usedModules, overrides); + + assertThat(depGraph.entrySet()) + .containsExactly( + buildAugmentedModule(ModuleKey.ROOT, "A") + .addDep("B", "1.0") + .addDep("B", "2.0") + .addDep("C", "2.0") + .buildEntry(), + buildAugmentedModule("B", "1.0") + .addStillDependant(ModuleKey.ROOT) + .addDep("C", "1.5", ResolutionReason.MULTIPLE_VERSION_OVERRIDE) + .buildEntry(), + buildAugmentedModule("B", "2.0") + .addStillDependant(ModuleKey.ROOT) + .addDep("C", "1.5") + .buildEntry(), + buildAugmentedModule("C", "1.0").addOriginalDependant("B", "1.0").buildEntry(), + buildAugmentedModule("C", "1.5") + .addDependant("B", "1.0") + .addStillDependant("B", "2.0") + .buildEntry(), + buildAugmentedModule("C", "2.0").addStillDependant(ModuleKey.ROOT).buildEntry()); + } + + @Test + public void testMultipleVersionOverride_badDepsUnreferenced() throws Exception { + // Initial dep graph + // A --> B1@1.0 --> C@1.0 [allowed] + // \ \-> B2@1.1 + // \-> B2@1.0 --> C@1.5 + // \-> B3@1.0 --> C@2.0 [allowed] + // \ \-> B4@1.1 + // \-> B4@1.0 --> C@3.0 + // + // Resolved dep graph + // A --> B1@1.0 --> C@1.0 [allowed] + // \ \-> B2@1.1 + // \-> B2@1.1 + // \-> B3@1.0 --> C@2.0 [allowed] + // \ \-> B4@1.1 + // \-> B4@1.1 + // C@1.5 and C@3.0, the versions violating the allowlist, are gone. + ImmutableMap unprunedDepGraph = + ImmutableMap.builder() + .put( + ModuleBuilder.create("A", Version.EMPTY) + .setKey(ModuleKey.ROOT) + .addDep("B1", createModuleKey("B1", "1.0")) + .addDep("B2", createModuleKey("B2", "1.1")) + .addOriginalDep("B2", createModuleKey("B2", "1.0")) + .addDep("B3", createModuleKey("B3", "1.0")) + .addDep("B4", createModuleKey("B4", "1.1")) + .addOriginalDep("B4", createModuleKey("B4", "1.0")) + .buildEntry()) + .put( + ModuleBuilder.create("B1", "1.0") + .addDep("C", createModuleKey("C", "1.0")) + .addDep("B2", createModuleKey("B2", "1.1")) + .buildEntry()) + .put( + ModuleBuilder.create("B2", "1.0") + .addDep("C", createModuleKey("C", "1.5")) + .buildEntry()) + .put(ModuleBuilder.create("B2", "1.1").buildEntry()) + .put( + ModuleBuilder.create("B3", "1.0") + .addDep("C", createModuleKey("C", "2.0")) + .addDep("B4", createModuleKey("B4", "1.1")) + .buildEntry()) + .put( + ModuleBuilder.create("B4", "1.0") + .addDep("C", createModuleKey("C", "3.0")) + .buildEntry()) + .put(ModuleBuilder.create("B4", "1.1").buildEntry()) + .put(ModuleBuilder.create("C", "1.0", 1).buildEntry()) + .put(ModuleBuilder.create("C", "1.5", 1).buildEntry()) + .put(ModuleBuilder.create("C", "2.0", 2).buildEntry()) + .put(ModuleBuilder.create("C", "3.0", 3).buildEntry()) + .buildOrThrow(); + + ImmutableMap overrides = + ImmutableMap.of( + "C", + MultipleVersionOverride.create( + ImmutableList.of(Version.parse("1.0"), Version.parse("2.0")), "")); + + ImmutableSet usedModules = + ImmutableSet.of( + ModuleKey.ROOT, + createModuleKey("B1", "1.0"), + createModuleKey("B2", "1.1"), + createModuleKey("B3", "1.0"), + createModuleKey("B4", "1.1"), + createModuleKey("C", "1.0"), + createModuleKey("C", "2.0")); + ImmutableMap depGraph = + BazelModuleInspectorFunction.computeAugmentedGraph( + unprunedDepGraph, usedModules, overrides); + + assertThat(depGraph.entrySet()) + .containsExactly( + buildAugmentedModule(ModuleKey.ROOT, "A") + .addDep("B1", "1.0") + .addDep("B2", "1.1", ResolutionReason.MINIMAL_VERSION_SELECTION) + .addDep("B3", "1.0") + .addDep("B4", "1.1", ResolutionReason.MINIMAL_VERSION_SELECTION) + .buildEntry(), + buildAugmentedModule("B1", "1.0") + .addDep("C", "1.0") + .addDep("B2", "1.1") + .addStillDependant(ModuleKey.ROOT) + .buildEntry(), + buildAugmentedModule("B2", "1.0") + .addDep("C", "1.5") + .addOriginalDependant(ModuleKey.ROOT) + .buildEntry(), + buildAugmentedModule("B2", "1.1") + .addDependant(ModuleKey.ROOT) + .addStillDependant("B1", "1.0") + .buildEntry(), + buildAugmentedModule("B3", "1.0") + .addDep("C", "2.0") + .addDep("B4", "1.1") + .addStillDependant(ModuleKey.ROOT) + .buildEntry(), + buildAugmentedModule("B4", "1.0") + .addDep("C", "3.0") + .addOriginalDependant(ModuleKey.ROOT) + .buildEntry(), + buildAugmentedModule("B4", "1.1") + .addDependant(ModuleKey.ROOT) + .addStillDependant("B3", "1.0") + .buildEntry(), + buildAugmentedModule("C", "1.0").addStillDependant("B1", "1.0").buildEntry(), + buildAugmentedModule("C", "1.5").addOriginalDependant("B2", "1.0").buildEntry(), + buildAugmentedModule("C", "2.0").addStillDependant("B3", "1.0").buildEntry(), + buildAugmentedModule("C", "3.0").addOriginalDependant("B4", "1.0").buildEntry()); + } + + static ModuleAugmentBuilder buildAugmentedModule( + ModuleKey key, String name, Version version, boolean loaded) { + ModuleAugmentBuilder myBuilder = new ModuleAugmentBuilder(); + myBuilder.key = key; + myBuilder.builder = + AugmentedModule.builder(key).setName(name).setVersion(version).setLoaded(loaded); + return myBuilder; + } + + static ModuleAugmentBuilder buildAugmentedModule(String name, String version, boolean loaded) + throws ParseException { + ModuleKey key = createModuleKey(name, version); + return buildAugmentedModule(key, name, Version.parse(version), loaded); + } + + static ModuleAugmentBuilder buildAugmentedModule(String name, String version) + throws ParseException { + ModuleKey key = createModuleKey(name, version); + return buildAugmentedModule(key, name, Version.parse(version), true); + } + + static ModuleAugmentBuilder buildAugmentedModule(ModuleKey key, String name) { + return buildAugmentedModule(key, name, key.getVersion(), true); + } + + private static final class ModuleAugmentBuilder { + + private AugmentedModule.Builder builder; + private ModuleKey key; + + private ModuleAugmentBuilder() {} + + ModuleAugmentBuilder addDep(String name, String version, ResolutionReason reason) { + this.builder.addDep(createModuleKey(name, version), reason); + return this; + } + + ModuleAugmentBuilder addDep(String name, String version) { + this.builder.addDep(createModuleKey(name, version), ResolutionReason.ORIGINAL); + return this; + } + + ModuleAugmentBuilder addDependant(String name, String version) { + this.builder.addDependant(createModuleKey(name, version)); + return this; + } + + ModuleAugmentBuilder addDependant(ModuleKey key) { + this.builder.addDependant(key); + return this; + } + + ModuleAugmentBuilder addOriginalDependant(String name, String version) { + this.builder.addOriginalDependant(createModuleKey(name, version)); + return this; + } + + ModuleAugmentBuilder addOriginalDependant(ModuleKey key) { + this.builder.addOriginalDependant(key); + return this; + } + + ModuleAugmentBuilder addStillDependant(String name, String version) { + this.builder.addOriginalDependant(createModuleKey(name, version)); + this.builder.addDependant(createModuleKey(name, version)); + return this; + } + + ModuleAugmentBuilder addStillDependant(ModuleKey key) { + this.builder.addOriginalDependant(key); + this.builder.addDependant(key); + return this; + } + + Entry buildEntry() { + return new SimpleEntry<>(this.key, this.builder.build()); + } + } +}