diff --git a/dataset/GitHub_Java/bazelbuild.bazel/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SelectionFunction.java b/dataset/GitHub_Java/bazelbuild.bazel/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SelectionFunction.java index 75298949277..f0077a4392e 100644 --- a/dataset/GitHub_Java/bazelbuild.bazel/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SelectionFunction.java +++ b/dataset/GitHub_Java/bazelbuild.bazel/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SelectionFunction.java @@ -15,21 +15,161 @@ package com.google.devtools.build.lib.bazel.bzlmod; +import static com.google.common.collect.ImmutableMap.toImmutableMap; + +import com.google.auto.value.AutoValue; +import com.google.common.base.Joiner; +import com.google.common.base.Preconditions; +import com.google.common.collect.Comparators; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSortedSet; +import com.google.common.collect.Maps; +import com.google.devtools.build.lib.bazel.bzlmod.ModuleFileValue.RootModuleFileValue; +import com.google.devtools.build.lib.server.FailureDetails.ExternalDeps.Code; 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.ArrayDeque; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Queue; +import java.util.Set; +import javax.annotation.Nullable; /** * Runs module selection. This step of module resolution reads the output of {@link * DiscoveryFunction} and applies the Minimal Version Selection algorithm to it, removing unselected * modules from the dependency graph and rewriting dependencies to point to the selected versions. + * + *

Essentially, what needs to happen is: + * + *

*/ public class SelectionFunction implements SkyFunction { + /** During selection, a version is selected for each distinct "selection group". */ + @AutoValue + abstract static class SelectionGroup { + static SelectionGroup create( + String moduleName, int compatibilityLevel, Version targetAllowedVersion) { + return new AutoValue_SelectionFunction_SelectionGroup( + moduleName, compatibilityLevel, targetAllowedVersion); + } + + abstract String getModuleName(); + + abstract int getCompatibilityLevel(); + + /** This is only used for modules with multiple-version overrides. */ + abstract Version getTargetAllowedVersion(); + } + + @AutoValue + abstract static class ModuleNameAndCompatibilityLevel { + static ModuleNameAndCompatibilityLevel create(String moduleName, int compatibilityLevel) { + return new AutoValue_SelectionFunction_ModuleNameAndCompatibilityLevel( + moduleName, compatibilityLevel); + } + + abstract String getModuleName(); + + abstract int getCompatibilityLevel(); + } + + /** + * Computes a mapping from (moduleName, compatibilityLevel) to the set of allowed versions. This + * is only performed for modules with multiple-version overrides. + */ + private static ImmutableMap> + computeAllowedVersionSets( + ImmutableMap overrides, ImmutableMap depGraph) + throws ExternalDepsException { + Map> allowedVersionSets = + new HashMap<>(); + for (Map.Entry overrideEntry : overrides.entrySet()) { + String moduleName = overrideEntry.getKey(); + ModuleOverride override = overrideEntry.getValue(); + if (!(override instanceof MultipleVersionOverride)) { + continue; + } + ImmutableList allowedVersions = ((MultipleVersionOverride) override).getVersions(); + for (Version allowedVersion : allowedVersions) { + Module allowedVersionModule = depGraph.get(ModuleKey.create(moduleName, allowedVersion)); + if (allowedVersionModule == null) { + throw ExternalDepsException.withMessage( + Code.VERSION_RESOLUTION_ERROR, + "multiple_version_override for module %s contains version %s, but it doesn't" + + " exist in the dependency graph", + moduleName, + allowedVersion); + } + ImmutableSortedSet.Builder allowedVersionSet = + allowedVersionSets.computeIfAbsent( + ModuleNameAndCompatibilityLevel.create( + moduleName, allowedVersionModule.getCompatibilityLevel()), + // Remember that the empty version compares greater than any other version, so we + // can use it as a sentinel value. + k -> ImmutableSortedSet.naturalOrder().add(Version.EMPTY)); + allowedVersionSet.add(allowedVersion); + } + } + return ImmutableMap.copyOf( + Maps.transformValues(allowedVersionSets, ImmutableSortedSet.Builder::build)); + } + + /** + * Computes the {@link SelectionGroup} for the given module. If the module has a multiple-version + * override (which would be reflected in the allowedVersionSets), information in there will be + * used to compute its targetAllowedVersion. + */ + private static SelectionGroup computeSelectionGroup( + Module module, + ImmutableMap> + allowedVersionSets) { + ImmutableSortedSet allowedVersionSet = + allowedVersionSets.get( + ModuleNameAndCompatibilityLevel.create( + module.getName(), module.getCompatibilityLevel())); + if (allowedVersionSet == null) { + // This means that this module has no multiple-version override. + return SelectionGroup.create(module.getName(), module.getCompatibilityLevel(), Version.EMPTY); + } + return SelectionGroup.create( + module.getName(), + module.getCompatibilityLevel(), + // We use the `ceiling` method here to quickly locate the lowest allowed version that's + // still no lower than this module's version. + // If this module's version is higher than any allowed version (in which case EMPTY is + // returned), it should result in an error. We don't immediately throw here because it might + // still become unreferenced later. + allowedVersionSet.ceiling(module.getVersion())); + } + @Override public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException, InterruptedException { @@ -37,28 +177,48 @@ public SkyValue compute(SkyKey skyKey, Environment env) if (discovery == null) { return null; } + ImmutableMap depGraph = discovery.getDepGraph(); + RootModuleFileValue rootModule = + (RootModuleFileValue) env.getValue(ModuleFileValue.keyForRootModule()); + if (rootModule == null) { + return null; + } + ImmutableMap overrides = rootModule.getOverrides(); - // TODO(wyv): compatibility_level, multiple_version_override + // For any multiple-version overrides, build a mapping from (moduleName, compatibilityLevel) to + // the set of allowed versions. + ImmutableMap> allowedVersionSets; + try { + allowedVersionSets = computeAllowedVersionSets(overrides, depGraph); + } catch (ExternalDepsException e) { + throw new SelectionFunctionException(e); + } - // First figure out the version to select for every module. - ImmutableMap depGraph = discovery.getDepGraph(); - Map selectedVersionForEachModule = new HashMap<>(); - for (ModuleKey key : depGraph.keySet()) { - try { - ParsedVersion parsedVersion = ParsedVersion.parse(key.getVersion()); - selectedVersionForEachModule.merge(key.getName(), parsedVersion, ParsedVersion::max); - } catch (ParsedVersion.ParseException e) { - throw new SelectionFunctionException(e); - } + // For each module in the dep graph, pre-compute its selection group. For most modules this is + // simply its (moduleName, compatibilityLevel) tuple; for modules with multiple-version + // overrides, it additionally includes the targetAllowedVersion, which denotes the version to + // "snap" to during selection. + ImmutableMap selectionGroups = + ImmutableMap.copyOf( + Maps.transformValues( + depGraph, module -> computeSelectionGroup(module, allowedVersionSets))); + + // Figure out the version to select for every selection group. + Map selectedVersions = new HashMap<>(); + for (Map.Entry entry : selectionGroups.entrySet()) { + ModuleKey key = entry.getKey(); + SelectionGroup selectionGroup = entry.getValue(); + selectedVersions.merge(selectionGroup, key.getVersion(), Comparators::max); } - // Now build a new dep graph where deps with unselected versions are removed. + // Build a new dep graph where deps with unselected versions are removed. ImmutableMap.Builder newDepGraphBuilder = new ImmutableMap.Builder<>(); for (Map.Entry entry : depGraph.entrySet()) { ModuleKey moduleKey = entry.getKey(); Module module = entry.getValue(); + // Remove any dep whose version isn't selected. - String selectedVersion = selectedVersionForEachModule.get(moduleKey.getName()).getOriginal(); + Version selectedVersion = selectedVersions.get(selectionGroups.get(moduleKey)); if (!moduleKey.getVersion().equals(selectedVersion)) { continue; } @@ -69,32 +229,173 @@ public SkyValue compute(SkyKey skyKey, Environment env) module.withDepKeysTransformed( depKey -> ModuleKey.create( - depKey.getName(), - selectedVersionForEachModule.get(depKey.getName()).getOriginal()))); + depKey.getName(), selectedVersions.get(selectionGroups.get(depKey))))); } ImmutableMap newDepGraph = newDepGraphBuilder.build(); // Further remove unreferenced modules from the graph. We can find out which modules are // referenced by collecting deps transitively from the root. - HashMap finalDepGraph = new HashMap<>(); - collectDeps(ModuleKey.create(discovery.getRootModuleName(), ""), newDepGraph, finalDepGraph); + // We can also take this opportunity to check that none of the remaining modules conflict with + // each other (e.g. same module name but different compatibility levels, or not satisfying + // multiple_version_override). + DepGraphWalker walker = + new DepGraphWalker(newDepGraph, discovery.getRootModuleName(), overrides, selectionGroups); + try { + newDepGraph = walker.walk(); + } catch (ExternalDepsException e) { + throw new SelectionFunctionException(e); + } + + ImmutableMap canonicalRepoNameLookup = + newDepGraph.keySet().stream() + .collect(toImmutableMap(ModuleKey::getCanonicalRepoName, key -> key)); + ImmutableMap moduleNameLookup = + newDepGraph.keySet().stream() + .filter(key -> !(overrides.get(key.getName()) instanceof MultipleVersionOverride)) + .collect(toImmutableMap(ModuleKey::getName, key -> key)); return SelectionValue.create( - discovery.getRootModuleName(), - ImmutableMap.copyOf(finalDepGraph), - discovery.getOverrides()); + discovery.getRootModuleName(), newDepGraph, canonicalRepoNameLookup, moduleNameLookup); } - private void collectDeps( - ModuleKey key, - ImmutableMap oldDepGraph, - HashMap newDepGraph) { - if (newDepGraph.containsKey(key)) { - return; - } - Module module = oldDepGraph.get(key); - newDepGraph.put(key, module); - for (ModuleKey depKey : module.getDeps().values()) { - collectDeps(depKey, oldDepGraph, newDepGraph); + /** + * Walks the dependency graph from the root node, collecting any reachable nodes through deps into + * a new dep graph and checking that nothing conflicts. + */ + static class DepGraphWalker { + private static final Joiner JOINER = Joiner.on(", "); + private final ImmutableMap oldDepGraph; + private final ModuleKey rootModuleKey; + private final ImmutableMap overrides; + private final ImmutableMap selectionGroups; + private final HashMap moduleByName; + + DepGraphWalker( + ImmutableMap oldDepGraph, + String rootModuleName, + ImmutableMap overrides, + ImmutableMap selectionGroups) { + this.oldDepGraph = oldDepGraph; + this.rootModuleKey = ModuleKey.create(rootModuleName, Version.EMPTY); + this.overrides = overrides; + this.selectionGroups = selectionGroups; + this.moduleByName = new HashMap<>(); + } + + /** + * Walks the old dep graph and builds a new dep graph containing only deps reachable from the + * root module. The returned map has a guaranteed breadth-first iteration order. + */ + ImmutableMap walk() throws ExternalDepsException { + ImmutableMap.Builder newDepGraph = ImmutableMap.builder(); + Set known = new HashSet<>(); + Queue toVisit = new ArrayDeque<>(); + toVisit.add(ModuleKeyAndDependent.create(rootModuleKey, null)); + known.add(rootModuleKey); + while (!toVisit.isEmpty()) { + ModuleKeyAndDependent moduleKeyAndDependent = toVisit.remove(); + ModuleKey key = moduleKeyAndDependent.getModuleKey(); + Module module = oldDepGraph.get(key); + visit(key, module, moduleKeyAndDependent.getDependent()); + + for (ModuleKey depKey : module.getDeps().values()) { + if (known.add(depKey)) { + toVisit.add(ModuleKeyAndDependent.create(depKey, key)); + } + } + newDepGraph.put(key, module); + } + return newDepGraph.build(); + } + + void visit(ModuleKey key, Module module, @Nullable ModuleKey from) + throws ExternalDepsException { + ModuleOverride override = overrides.get(key.getName()); + if (override instanceof MultipleVersionOverride) { + if (selectionGroups.get(key).getTargetAllowedVersion().isEmpty()) { + // This module has no target allowed version, which means that there's no allowed version + // higher than its version at the same compatibility level. + Preconditions.checkState( + from != null, "the root module cannot have a multiple version override"); + throw ExternalDepsException.withMessage( + Code.VERSION_RESOLUTION_ERROR, + "%s depends on %s which is not allowed by the multiple_version_override on %s," + + " which allows only [%s]", + from, + key, + key.getName(), + JOINER.join(((MultipleVersionOverride) override).getVersions())); + } + } else { + ExistingModule existingModuleWithSameName = + moduleByName.put( + module.getName(), ExistingModule.create(key, module.getCompatibilityLevel(), from)); + if (existingModuleWithSameName != null) { + // This has to mean that a module with the same name but a different compatibility level + // was also selected. + Preconditions.checkState( + from != null && existingModuleWithSameName.getDependent() != null, + "the root module cannot possibly exist more than once in the dep graph"); + throw ExternalDepsException.withMessage( + Code.VERSION_RESOLUTION_ERROR, + "%s depends on %s with compatibility level %d, but %s depends on %s with" + + " compatibility level %d which is different", + from, + key, + module.getCompatibilityLevel(), + existingModuleWithSameName.getDependent(), + existingModuleWithSameName.getModuleKey(), + existingModuleWithSameName.getCompatibilityLevel()); + } + } + + // Make sure that we don't have `module` depending on the same dependency version twice. + HashMap depKeyToRepoName = new HashMap<>(); + for (Map.Entry depEntry : module.getDeps().entrySet()) { + String repoName = depEntry.getKey(); + ModuleKey depKey = depEntry.getValue(); + String previousRepoName = depKeyToRepoName.put(depKey, repoName); + if (previousRepoName != null) { + throw ExternalDepsException.withMessage( + Code.VERSION_RESOLUTION_ERROR, + "%s depends on %s at least twice (with repo names %s and %s). Consider adding a" + + " multiple_version_override if you want to depend on multiple versions of" + + " %s simultaneously", + key, + depKey, + repoName, + previousRepoName, + key.getName()); + } + } + } + + @AutoValue + abstract static class ModuleKeyAndDependent { + abstract ModuleKey getModuleKey(); + + @Nullable + abstract ModuleKey getDependent(); + + static ModuleKeyAndDependent create(ModuleKey moduleKey, @Nullable ModuleKey dependent) { + return new AutoValue_SelectionFunction_DepGraphWalker_ModuleKeyAndDependent( + moduleKey, dependent); + } + } + + @AutoValue + abstract static class ExistingModule { + abstract ModuleKey getModuleKey(); + + abstract int getCompatibilityLevel(); + + @Nullable + abstract ModuleKey getDependent(); + + static ExistingModule create( + ModuleKey moduleKey, int compatibilityLevel, ModuleKey dependent) { + return new AutoValue_SelectionFunction_DepGraphWalker_ExistingModule( + moduleKey, compatibilityLevel, dependent); + } } } diff --git a/dataset/GitHub_Java/bazelbuild.bazel/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SelectionValue.java b/dataset/GitHub_Java/bazelbuild.bazel/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SelectionValue.java index dcccc304902..91aafcbee49 100644 --- a/dataset/GitHub_Java/bazelbuild.bazel/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SelectionValue.java +++ b/dataset/GitHub_Java/bazelbuild.bazel/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SelectionValue.java @@ -31,13 +31,23 @@ public abstract class SelectionValue implements SkyValue { public static SelectionValue create( String rootModuleName, ImmutableMap depGraph, - ImmutableMap overrides) { - return new AutoValue_SelectionValue(rootModuleName, depGraph, overrides); + ImmutableMap canonicalRepoNameLookup, + ImmutableMap moduleNameLookup) { + return new AutoValue_SelectionValue( + rootModuleName, depGraph, canonicalRepoNameLookup, moduleNameLookup); } public abstract String getRootModuleName(); + /** The post-selection dep graph. Must have BFS iteration order, starting from the root module. */ public abstract ImmutableMap getDepGraph(); - public abstract ImmutableMap getOverrides(); + /** A mapping from a canonical repo name to the key of the module backing it. */ + public abstract ImmutableMap getCanonicalRepoNameLookup(); + + /** + * A mapping from a plain module name to the key of the module (only works for modules without + * multiple-version overrides). + */ + public abstract ImmutableMap getModuleNameLookup(); } diff --git a/dataset/GitHub_Java/bazelbuild.bazel/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/SelectionFunctionTest.java b/dataset/GitHub_Java/bazelbuild.bazel/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/SelectionFunctionTest.java index 8d837e84506..72da54b526b 100644 --- a/dataset/GitHub_Java/bazelbuild.bazel/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/SelectionFunctionTest.java +++ b/dataset/GitHub_Java/bazelbuild.bazel/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/SelectionFunctionTest.java @@ -16,10 +16,13 @@ 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 static org.junit.Assert.fail; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.devtools.build.lib.bazel.bzlmod.ModuleFileValue.RootModuleFileValue; import com.google.devtools.build.lib.skyframe.SkyFunctions; import com.google.devtools.build.lib.testutil.FoundationTestCase; import com.google.devtools.build.skyframe.EvaluationContext; @@ -53,17 +56,42 @@ public void setup() throws Exception { EvaluationContext.newBuilder().setNumThreads(8).setEventHandler(reporter).build(); } - private void setUpDiscoveryResult(String rootModuleName, ImmutableMap depGraph) + private void setUpSkyFunctions( + String rootModuleName, + ImmutableMap depGraph, + ImmutableMap overrides) throws Exception { MemoizingEvaluator evaluator = new InMemoryMemoizingEvaluator( ImmutableMap.builder() + .put( + SkyFunctions.MODULE_FILE, + new SkyFunction() { + @Override + public SkyValue compute(SkyKey skyKey, Environment env) { + Preconditions.checkArgument( + skyKey.equals(ModuleFileValue.keyForRootModule())); + return RootModuleFileValue.create( + Module.builder() + .setName(rootModuleName) + .setVersion(Version.EMPTY) + .build(), + overrides, + // This lookup is not used in this test + ImmutableMap.of()); + } + + @Override + public String extractTag(SkyKey skyKey) { + return null; + } + }) .put( SkyFunctions.DISCOVERY, new SkyFunction() { @Override public SkyValue compute(SkyKey skyKey, Environment env) { - return DiscoveryValue.create(rootModuleName, depGraph, ImmutableMap.of()); + return DiscoveryValue.create(rootModuleName, depGraph); } @Override @@ -79,38 +107,47 @@ public String extractTag(SkyKey skyKey) { @Test public void testSimpleDiamond() throws Exception { - setUpDiscoveryResult( + setUpSkyFunctions( "A", ImmutableMap.builder() .put( - ModuleKey.create("A", ""), + createModuleKey("A", ""), Module.builder() .setName("A") - .setVersion("") - .addDep("BfromA", ModuleKey.create("B", "1.0")) - .addDep("CfromA", ModuleKey.create("C", "2.0")) + .setVersion(Version.EMPTY) + .addDep("BfromA", createModuleKey("B", "1.0")) + .addDep("CfromA", createModuleKey("C", "2.0")) .build()) .put( - ModuleKey.create("B", "1.0"), + createModuleKey("B", "1.0"), Module.builder() .setName("B") - .setVersion("1.0") - .addDep("DfromB", ModuleKey.create("D", "1.0")) + .setVersion(Version.parse("1.0")) + .addDep("DfromB", createModuleKey("D", "1.0")) .build()) .put( - ModuleKey.create("C", "2.0"), + createModuleKey("C", "2.0"), Module.builder() .setName("C") - .setVersion("2.0") - .addDep("DfromC", ModuleKey.create("D", "2.0")) + .setVersion(Version.parse("2.0")) + .addDep("DfromC", createModuleKey("D", "2.0")) .build()) .put( - ModuleKey.create("D", "1.0"), - Module.builder().setName("D").setVersion("1.0").build()) + createModuleKey("D", "1.0"), + Module.builder() + .setName("D") + .setVersion(Version.parse("1.0")) + .setCompatibilityLevel(1) + .build()) .put( - ModuleKey.create("D", "2.0"), - Module.builder().setName("D").setVersion("2.0").build()) - .build()); + createModuleKey("D", "2.0"), + Module.builder() + .setName("D") + .setVersion(Version.parse("2.0")) + .setCompatibilityLevel(1) + .build()) + .build(), + ImmutableMap.of()); EvaluationResult result = driver.evaluate(ImmutableList.of(SelectionValue.KEY), evaluationContext); @@ -121,72 +158,98 @@ public void testSimpleDiamond() throws Exception { assertThat(selectionValue.getRootModuleName()).isEqualTo("A"); assertThat(selectionValue.getDepGraph()) .containsExactly( - ModuleKey.create("A", ""), + createModuleKey("A", ""), Module.builder() .setName("A") - .setVersion("") - .addDep("BfromA", ModuleKey.create("B", "1.0")) - .addDep("CfromA", ModuleKey.create("C", "2.0")) + .setVersion(Version.EMPTY) + .addDep("BfromA", createModuleKey("B", "1.0")) + .addDep("CfromA", createModuleKey("C", "2.0")) .build(), - ModuleKey.create("B", "1.0"), + createModuleKey("B", "1.0"), Module.builder() .setName("B") - .setVersion("1.0") - .addDep("DfromB", ModuleKey.create("D", "2.0")) + .setVersion(Version.parse("1.0")) + .addDep("DfromB", createModuleKey("D", "2.0")) .build(), - ModuleKey.create("C", "2.0"), + createModuleKey("C", "2.0"), Module.builder() .setName("C") - .setVersion("2.0") - .addDep("DfromC", ModuleKey.create("D", "2.0")) + .setVersion(Version.parse("2.0")) + .addDep("DfromC", createModuleKey("D", "2.0")) .build(), - ModuleKey.create("D", "2.0"), - Module.builder().setName("D").setVersion("2.0").build()); + createModuleKey("D", "2.0"), + Module.builder() + .setName("D") + .setVersion(Version.parse("2.0")) + .setCompatibilityLevel(1) + .build()) + .inOrder(); + assertThat(selectionValue.getCanonicalRepoNameLookup()) + .containsExactly( + "A.", + createModuleKey("A", ""), + "B.1.0", + createModuleKey("B", "1.0"), + "C.2.0", + createModuleKey("C", "2.0"), + "D.2.0", + createModuleKey("D", "2.0")); + assertThat(selectionValue.getModuleNameLookup()) + .containsExactly( + "A", + createModuleKey("A", ""), + "B", + createModuleKey("B", "1.0"), + "C", + createModuleKey("C", "2.0"), + "D", + createModuleKey("D", "2.0")); } @Test public void testDiamondWithFurtherRemoval() throws Exception { - setUpDiscoveryResult( + setUpSkyFunctions( "A", ImmutableMap.builder() .put( - ModuleKey.create("A", ""), + createModuleKey("A", ""), Module.builder() .setName("A") - .setVersion("") - .addDep("B", ModuleKey.create("B", "1.0")) - .addDep("C", ModuleKey.create("C", "2.0")) + .setVersion(Version.EMPTY) + .addDep("B", createModuleKey("B", "1.0")) + .addDep("C", createModuleKey("C", "2.0")) .build()) .put( - ModuleKey.create("B", "1.0"), + createModuleKey("B", "1.0"), Module.builder() .setName("B") - .setVersion("1.0") - .addDep("D", ModuleKey.create("D", "1.0")) + .setVersion(Version.parse("1.0")) + .addDep("D", createModuleKey("D", "1.0")) .build()) .put( - ModuleKey.create("C", "2.0"), + createModuleKey("C", "2.0"), Module.builder() .setName("C") - .setVersion("2.0") - .addDep("D", ModuleKey.create("D", "2.0")) + .setVersion(Version.parse("2.0")) + .addDep("D", createModuleKey("D", "2.0")) .build()) .put( - ModuleKey.create("D", "1.0"), + createModuleKey("D", "1.0"), Module.builder() .setName("D") - .setVersion("1.0") - .addDep("E", ModuleKey.create("E", "1.0")) + .setVersion(Version.parse("1.0")) + .addDep("E", createModuleKey("E", "1.0")) .build()) .put( - ModuleKey.create("D", "2.0"), - Module.builder().setName("D").setVersion("2.0").build()) + createModuleKey("D", "2.0"), + Module.builder().setName("D").setVersion(Version.parse("2.0")).build()) // Only D@1.0 needs E. When D@1.0 is removed, E should be gone as well (even though // E@1.0 is selected for E). .put( - ModuleKey.create("E", "1.0"), - Module.builder().setName("E").setVersion("1.0").build()) - .build()); + createModuleKey("E", "1.0"), + Module.builder().setName("E").setVersion(Version.parse("1.0")).build()) + .build(), + ImmutableMap.of()); EvaluationResult result = driver.evaluate(ImmutableList.of(SelectionValue.KEY), evaluationContext); @@ -197,66 +260,88 @@ public void testDiamondWithFurtherRemoval() throws Exception { assertThat(selectionValue.getRootModuleName()).isEqualTo("A"); assertThat(selectionValue.getDepGraph()) .containsExactly( - ModuleKey.create("A", ""), + createModuleKey("A", ""), Module.builder() .setName("A") - .setVersion("") - .addDep("B", ModuleKey.create("B", "1.0")) - .addDep("C", ModuleKey.create("C", "2.0")) + .setVersion(Version.EMPTY) + .addDep("B", createModuleKey("B", "1.0")) + .addDep("C", createModuleKey("C", "2.0")) .build(), - ModuleKey.create("B", "1.0"), + createModuleKey("B", "1.0"), Module.builder() .setName("B") - .setVersion("1.0") - .addDep("D", ModuleKey.create("D", "2.0")) + .setVersion(Version.parse("1.0")) + .addDep("D", createModuleKey("D", "2.0")) .build(), - ModuleKey.create("C", "2.0"), + createModuleKey("C", "2.0"), Module.builder() .setName("C") - .setVersion("2.0") - .addDep("D", ModuleKey.create("D", "2.0")) + .setVersion(Version.parse("2.0")) + .addDep("D", createModuleKey("D", "2.0")) .build(), - ModuleKey.create("D", "2.0"), - Module.builder().setName("D").setVersion("2.0").build()); + createModuleKey("D", "2.0"), + Module.builder().setName("D").setVersion(Version.parse("2.0")).build()) + .inOrder(); + assertThat(selectionValue.getCanonicalRepoNameLookup()) + .containsExactly( + "A.", + createModuleKey("A", ""), + "B.1.0", + createModuleKey("B", "1.0"), + "C.2.0", + createModuleKey("C", "2.0"), + "D.2.0", + createModuleKey("D", "2.0")); + assertThat(selectionValue.getModuleNameLookup()) + .containsExactly( + "A", + createModuleKey("A", ""), + "B", + createModuleKey("B", "1.0"), + "C", + createModuleKey("C", "2.0"), + "D", + createModuleKey("D", "2.0")); } @Test public void testCircularDependencyDueToSelection() throws Exception { - setUpDiscoveryResult( + setUpSkyFunctions( "A", ImmutableMap.builder() .put( - ModuleKey.create("A", ""), + createModuleKey("A", ""), Module.builder() .setName("A") - .setVersion("") - .addDep("B", ModuleKey.create("B", "1.0")) + .setVersion(Version.EMPTY) + .addDep("B", createModuleKey("B", "1.0")) .build()) .put( - ModuleKey.create("B", "1.0"), + createModuleKey("B", "1.0"), Module.builder() .setName("B") - .setVersion("1.0") - .addDep("C", ModuleKey.create("C", "2.0")) + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "2.0")) .build()) .put( - ModuleKey.create("C", "2.0"), + createModuleKey("C", "2.0"), Module.builder() .setName("C") - .setVersion("2.0") - .addDep("B", ModuleKey.create("B", "1.0-pre")) + .setVersion(Version.parse("2.0")) + .addDep("B", createModuleKey("B", "1.0-pre")) .build()) .put( - ModuleKey.create("B", "1.0-pre"), + createModuleKey("B", "1.0-pre"), Module.builder() .setName("B") - .setVersion("1.0-pre") - .addDep("D", ModuleKey.create("D", "1.0")) + .setVersion(Version.parse("1.0-pre")) + .addDep("D", createModuleKey("D", "1.0")) .build()) .put( - ModuleKey.create("D", "1.0"), - Module.builder().setName("D").setVersion("1.0").build()) - .build()); + createModuleKey("D", "1.0"), + Module.builder().setName("D").setVersion(Version.parse("1.0")).build()) + .build(), + ImmutableMap.of()); EvaluationResult result = driver.evaluate(ImmutableList.of(SelectionValue.KEY), evaluationContext); @@ -267,24 +352,1102 @@ public void testCircularDependencyDueToSelection() throws Exception { assertThat(selectionValue.getRootModuleName()).isEqualTo("A"); assertThat(selectionValue.getDepGraph()) .containsExactly( - ModuleKey.create("A", ""), + createModuleKey("A", ""), Module.builder() .setName("A") - .setVersion("") - .addDep("B", ModuleKey.create("B", "1.0")) + .setVersion(Version.EMPTY) + .addDep("B", createModuleKey("B", "1.0")) .build(), - ModuleKey.create("B", "1.0"), + createModuleKey("B", "1.0"), Module.builder() .setName("B") - .setVersion("1.0") - .addDep("C", ModuleKey.create("C", "2.0")) + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "2.0")) .build(), - ModuleKey.create("C", "2.0"), + createModuleKey("C", "2.0"), Module.builder() .setName("C") - .setVersion("2.0") - .addDep("B", ModuleKey.create("B", "1.0")) - .build()); + .setVersion(Version.parse("2.0")) + .addDep("B", createModuleKey("B", "1.0")) + .build()) + .inOrder(); // D is completely gone. + assertThat(selectionValue.getCanonicalRepoNameLookup()) + .containsExactly( + "A.", + createModuleKey("A", ""), + "B.1.0", + createModuleKey("B", "1.0"), + "C.2.0", + createModuleKey("C", "2.0")); + assertThat(selectionValue.getModuleNameLookup()) + .containsExactly( + "A", + createModuleKey("A", ""), + "B", + createModuleKey("B", "1.0"), + "C", + createModuleKey("C", "2.0")); + } + + @Test + public void differentCompatibilityLevelIsRejected() throws Exception { + setUpSkyFunctions( + "A", + ImmutableMap.builder() + .put( + createModuleKey("A", ""), + Module.builder() + .setName("A") + .setVersion(Version.EMPTY) + .addDep("BfromA", createModuleKey("B", "1.0")) + .addDep("CfromA", createModuleKey("C", "2.0")) + .build()) + .put( + createModuleKey("B", "1.0"), + Module.builder() + .setName("B") + .setVersion(Version.parse("1.0")) + .addDep("DfromB", createModuleKey("D", "1.0")) + .build()) + .put( + createModuleKey("C", "2.0"), + Module.builder() + .setName("C") + .setVersion(Version.parse("2.0")) + .addDep("DfromC", createModuleKey("D", "2.0")) + .build()) + .put( + createModuleKey("D", "1.0"), + Module.builder() + .setName("D") + .setVersion(Version.parse("1.0")) + .setCompatibilityLevel(1) + .build()) + .put( + createModuleKey("D", "2.0"), + Module.builder() + .setName("D") + .setVersion(Version.parse("2.0")) + .setCompatibilityLevel(2) + .build()) + .build(), + ImmutableMap.of()); + + EvaluationResult result = + driver.evaluate(ImmutableList.of(SelectionValue.KEY), evaluationContext); + assertThat(result.hasError()).isTrue(); + String error = result.getError().toString(); + assertThat(error).contains("B@1.0 depends on D@1.0 with compatibility level 1"); + assertThat(error).contains("C@2.0 depends on D@2.0 with compatibility level 2"); + assertThat(error).contains("which is different"); + } + + @Test + public void differentCompatibilityLevelIsOkIfUnreferenced() throws Exception { + // A 1.0 -> B 1.0 -> C 2.0 + // \-> C 1.0 + // \-> D 1.0 -> B 1.1 + // \-> E 1.0 -> C 1.1 + setUpSkyFunctions( + "A", + ImmutableMap.builder() + .put( + createModuleKey("A", ""), + Module.builder() + .setName("A") + .setVersion(Version.parse("1.0")) + .addDep("B", createModuleKey("B", "1.0")) + .addDep("C", createModuleKey("C", "1.0")) + .addDep("D", createModuleKey("D", "1.0")) + .addDep("E", createModuleKey("E", "1.0")) + .build()) + .put( + createModuleKey("B", "1.0"), + Module.builder() + .setName("B") + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "2.0")) + .build()) + .put( + createModuleKey("C", "2.0"), + Module.builder() + .setName("C") + .setVersion(Version.parse("2.0")) + .setCompatibilityLevel(2) + .build()) + .put( + createModuleKey("C", "1.0"), + Module.builder() + .setName("C") + .setVersion(Version.parse("1.0")) + .setCompatibilityLevel(1) + .build()) + .put( + createModuleKey("D", "1.0"), + Module.builder() + .setName("D") + .setVersion(Version.parse("1.0")) + .addDep("B", createModuleKey("B", "1.1")) + .build()) + .put( + createModuleKey("B", "1.1"), + Module.builder().setName("B").setVersion(Version.parse("1.1")).build()) + .put( + createModuleKey("E", "1.0"), + Module.builder() + .setName("E") + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "1.1")) + .build()) + .put( + createModuleKey("C", "1.1"), + Module.builder() + .setName("C") + .setVersion(Version.parse("1.1")) + .setCompatibilityLevel(1) + .build()) + .build(), + ImmutableMap.of()); + + EvaluationResult result = + driver.evaluate(ImmutableList.of(SelectionValue.KEY), evaluationContext); + if (result.hasError()) { + fail(result.getError().toString()); + } + // After selection, C 2.0 is gone, so we're okay. + // A 1.0 -> B 1.1 + // \-> C 1.1 + // \-> D 1.0 -> B 1.1 + // \-> E 1.0 -> C 1.1 + SelectionValue selectionValue = result.get(SelectionValue.KEY); + assertThat(selectionValue.getRootModuleName()).isEqualTo("A"); + assertThat(selectionValue.getDepGraph()) + .containsExactly( + createModuleKey("A", ""), + Module.builder() + .setName("A") + .setVersion(Version.parse("1.0")) + .addDep("B", createModuleKey("B", "1.1")) + .addDep("C", createModuleKey("C", "1.1")) + .addDep("D", createModuleKey("D", "1.0")) + .addDep("E", createModuleKey("E", "1.0")) + .build(), + createModuleKey("B", "1.1"), + Module.builder().setName("B").setVersion(Version.parse("1.1")).build(), + createModuleKey("C", "1.1"), + Module.builder() + .setName("C") + .setVersion(Version.parse("1.1")) + .setCompatibilityLevel(1) + .build(), + createModuleKey("D", "1.0"), + Module.builder() + .setName("D") + .setVersion(Version.parse("1.0")) + .addDep("B", createModuleKey("B", "1.1")) + .build(), + createModuleKey("E", "1.0"), + Module.builder() + .setName("E") + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "1.1")) + .build()) + .inOrder(); + assertThat(selectionValue.getCanonicalRepoNameLookup()) + .containsExactly( + "A.", + createModuleKey("A", ""), + "B.1.1", + createModuleKey("B", "1.1"), + "C.1.1", + createModuleKey("C", "1.1"), + "D.1.0", + createModuleKey("D", "1.0"), + "E.1.0", + createModuleKey("E", "1.0")); + assertThat(selectionValue.getModuleNameLookup()) + .containsExactly( + "A", + createModuleKey("A", ""), + "B", + createModuleKey("B", "1.1"), + "C", + createModuleKey("C", "1.1"), + "D", + createModuleKey("D", "1.0"), + "E", + createModuleKey("E", "1.0")); + } + + @Test + public void multipleVersionOverride_fork_allowedVersionMissingInDepGraph() throws Exception { + setUpSkyFunctions( + "A", + ImmutableMap.builder() + .put( + createModuleKey("A", ""), + Module.builder() + .setName("A") + .setVersion(Version.EMPTY) + .addDep("B1", createModuleKey("B", "1.0")) + .addDep("B2", createModuleKey("B", "2.0")) + .build()) + .put( + createModuleKey("B", "1.0"), + Module.builder().setName("B").setVersion(Version.parse("1.0")).build()) + .put( + createModuleKey("B", "2.0"), + Module.builder().setName("B").setVersion(Version.parse("2.0")).build()) + .build(), + ImmutableMap.of( + "B", + MultipleVersionOverride.create( + ImmutableList.of(Version.parse("1.0"), Version.parse("2.0"), Version.parse("3.0")), + ""))); + + EvaluationResult result = + driver.evaluate(ImmutableList.of(SelectionValue.KEY), evaluationContext); + assertThat(result.hasError()).isTrue(); + String error = result.getError().toString(); + assertThat(error) + .contains( + "multiple_version_override for module B contains version 3.0, but it doesn't exist in" + + " the dependency graph"); + } + + @Test + public void multipleVersionOverride_fork_goodCase() throws Exception { + // For more complex good cases, see the "diamond" test cases below. + setUpSkyFunctions( + "A", + ImmutableMap.builder() + .put( + createModuleKey("A", ""), + Module.builder() + .setName("A") + .setVersion(Version.EMPTY) + .addDep("B1", createModuleKey("B", "1.0")) + .addDep("B2", createModuleKey("B", "2.0")) + .build()) + .put( + createModuleKey("B", "1.0"), + Module.builder().setName("B").setVersion(Version.parse("1.0")).build()) + .put( + createModuleKey("B", "2.0"), + Module.builder().setName("B").setVersion(Version.parse("2.0")).build()) + .build(), + ImmutableMap.of( + "B", + MultipleVersionOverride.create( + ImmutableList.of(Version.parse("1.0"), Version.parse("2.0")), ""))); + + EvaluationResult result = + driver.evaluate(ImmutableList.of(SelectionValue.KEY), evaluationContext); + if (result.hasError()) { + fail(result.getError().toString()); + } + SelectionValue selectionValue = result.get(SelectionValue.KEY); + assertThat(selectionValue.getRootModuleName()).isEqualTo("A"); + assertThat(selectionValue.getDepGraph()) + .containsExactly( + createModuleKey("A", ""), + Module.builder() + .setName("A") + .setVersion(Version.EMPTY) + .addDep("B1", createModuleKey("B", "1.0")) + .addDep("B2", createModuleKey("B", "2.0")) + .build(), + createModuleKey("B", "1.0"), + Module.builder().setName("B").setVersion(Version.parse("1.0")).build(), + createModuleKey("B", "2.0"), + Module.builder().setName("B").setVersion(Version.parse("2.0")).build()) + .inOrder(); + assertThat(selectionValue.getCanonicalRepoNameLookup()) + .containsExactly( + "A.", + createModuleKey("A", ""), + "B.1.0", + createModuleKey("B", "1.0"), + "B.2.0", + createModuleKey("B", "2.0")); + // No B in the module name lookup because there's a multiple-version override. + assertThat(selectionValue.getModuleNameLookup()).containsExactly("A", createModuleKey("A", "")); + } + + @Test + public void multipleVersionOverride_fork_sameVersionUsedTwice() throws Exception { + setUpSkyFunctions( + "A", + ImmutableMap.builder() + .put( + createModuleKey("A", ""), + Module.builder() + .setName("A") + .setVersion(Version.EMPTY) + .addDep("B1", createModuleKey("B", "1.0")) + .addDep("B2", createModuleKey("B", "1.3")) + .addDep("B3", createModuleKey("B", "1.5")) + .build()) + .put( + createModuleKey("B", "1.0"), + Module.builder().setName("B").setVersion(Version.parse("1.0")).build()) + .put( + createModuleKey("B", "1.3"), + Module.builder().setName("B").setVersion(Version.parse("1.3")).build()) + .put( + createModuleKey("B", "1.5"), + Module.builder().setName("B").setVersion(Version.parse("1.5")).build()) + .build(), + ImmutableMap.of( + "B", + MultipleVersionOverride.create( + ImmutableList.of(Version.parse("1.0"), Version.parse("1.5")), ""))); + + EvaluationResult result = + driver.evaluate(ImmutableList.of(SelectionValue.KEY), evaluationContext); + assertThat(result.hasError()).isTrue(); + String error = result.getError().toString(); + assertThat(error) + .containsMatch( + "A@_ depends on B@1.5 at least twice \\(with repo names (B2 and B3)|(B3 and B2)\\)"); + } + + @Test + public void multipleVersionOverride_diamond_differentCompatibilityLevels() throws Exception { + setUpSkyFunctions( + "A", + ImmutableMap.builder() + .put( + createModuleKey("A", ""), + Module.builder() + .setName("A") + .setVersion(Version.EMPTY) + .addDep("BfromA", createModuleKey("B", "1.0")) + .addDep("CfromA", createModuleKey("C", "2.0")) + .build()) + .put( + createModuleKey("B", "1.0"), + Module.builder() + .setName("B") + .setVersion(Version.parse("1.0")) + .addDep("DfromB", createModuleKey("D", "1.0")) + .build()) + .put( + createModuleKey("C", "2.0"), + Module.builder() + .setName("C") + .setVersion(Version.parse("2.0")) + .addDep("DfromC", createModuleKey("D", "2.0")) + .build()) + .put( + createModuleKey("D", "1.0"), + Module.builder() + .setName("D") + .setVersion(Version.parse("1.0")) + .setCompatibilityLevel(1) + .build()) + .put( + createModuleKey("D", "2.0"), + Module.builder() + .setName("D") + .setVersion(Version.parse("2.0")) + .setCompatibilityLevel(2) + .build()) + .build(), + ImmutableMap.of( + "D", + MultipleVersionOverride.create( + ImmutableList.of(Version.parse("1.0"), Version.parse("2.0")), ""))); + + EvaluationResult result = + driver.evaluate(ImmutableList.of(SelectionValue.KEY), evaluationContext); + if (result.hasError()) { + fail(result.getError().toString()); + } + SelectionValue selectionValue = result.get(SelectionValue.KEY); + assertThat(selectionValue.getRootModuleName()).isEqualTo("A"); + assertThat(selectionValue.getDepGraph()) + .containsExactly( + createModuleKey("A", ""), + Module.builder() + .setName("A") + .setVersion(Version.EMPTY) + .addDep("BfromA", createModuleKey("B", "1.0")) + .addDep("CfromA", createModuleKey("C", "2.0")) + .build(), + createModuleKey("B", "1.0"), + Module.builder() + .setName("B") + .setVersion(Version.parse("1.0")) + .addDep("DfromB", createModuleKey("D", "1.0")) + .build(), + createModuleKey("C", "2.0"), + Module.builder() + .setName("C") + .setVersion(Version.parse("2.0")) + .addDep("DfromC", createModuleKey("D", "2.0")) + .build(), + createModuleKey("D", "1.0"), + Module.builder() + .setName("D") + .setVersion(Version.parse("1.0")) + .setCompatibilityLevel(1) + .build(), + createModuleKey("D", "2.0"), + Module.builder() + .setName("D") + .setVersion(Version.parse("2.0")) + .setCompatibilityLevel(2) + .build()) + .inOrder(); + assertThat(selectionValue.getCanonicalRepoNameLookup()) + .containsExactly( + "A.", + createModuleKey("A", ""), + "B.1.0", + createModuleKey("B", "1.0"), + "C.2.0", + createModuleKey("C", "2.0"), + "D.1.0", + createModuleKey("D", "1.0"), + "D.2.0", + createModuleKey("D", "2.0")); + assertThat(selectionValue.getModuleNameLookup()) + .containsExactly( + "A", + createModuleKey("A", ""), + "B", + createModuleKey("B", "1.0"), + "C", + createModuleKey("C", "2.0")); + } + + @Test + public void multipleVersionOverride_diamond_sameCompatibilityLevel() throws Exception { + setUpSkyFunctions( + "A", + ImmutableMap.builder() + .put( + createModuleKey("A", ""), + Module.builder() + .setName("A") + .setVersion(Version.EMPTY) + .addDep("BfromA", createModuleKey("B", "1.0")) + .addDep("CfromA", createModuleKey("C", "2.0")) + .build()) + .put( + createModuleKey("B", "1.0"), + Module.builder() + .setName("B") + .setVersion(Version.parse("1.0")) + .addDep("DfromB", createModuleKey("D", "1.0")) + .build()) + .put( + createModuleKey("C", "2.0"), + Module.builder() + .setName("C") + .setVersion(Version.parse("2.0")) + .addDep("DfromC", createModuleKey("D", "2.0")) + .build()) + .put( + createModuleKey("D", "1.0"), + Module.builder().setName("D").setVersion(Version.parse("1.0")).build()) + .put( + createModuleKey("D", "2.0"), + Module.builder().setName("D").setVersion(Version.parse("2.0")).build()) + .build(), + ImmutableMap.of( + "D", + MultipleVersionOverride.create( + ImmutableList.of(Version.parse("1.0"), Version.parse("2.0")), ""))); + + EvaluationResult result = + driver.evaluate(ImmutableList.of(SelectionValue.KEY), evaluationContext); + if (result.hasError()) { + fail(result.getError().toString()); + } + SelectionValue selectionValue = result.get(SelectionValue.KEY); + assertThat(selectionValue.getRootModuleName()).isEqualTo("A"); + assertThat(selectionValue.getDepGraph()) + .containsExactly( + createModuleKey("A", ""), + Module.builder() + .setName("A") + .setVersion(Version.EMPTY) + .addDep("BfromA", createModuleKey("B", "1.0")) + .addDep("CfromA", createModuleKey("C", "2.0")) + .build(), + createModuleKey("B", "1.0"), + Module.builder() + .setName("B") + .setVersion(Version.parse("1.0")) + .addDep("DfromB", createModuleKey("D", "1.0")) + .build(), + createModuleKey("C", "2.0"), + Module.builder() + .setName("C") + .setVersion(Version.parse("2.0")) + .addDep("DfromC", createModuleKey("D", "2.0")) + .build(), + createModuleKey("D", "1.0"), + Module.builder().setName("D").setVersion(Version.parse("1.0")).build(), + createModuleKey("D", "2.0"), + Module.builder().setName("D").setVersion(Version.parse("2.0")).build()) + .inOrder(); + assertThat(selectionValue.getCanonicalRepoNameLookup()) + .containsExactly( + "A.", + createModuleKey("A", ""), + "B.1.0", + createModuleKey("B", "1.0"), + "C.2.0", + createModuleKey("C", "2.0"), + "D.1.0", + createModuleKey("D", "1.0"), + "D.2.0", + createModuleKey("D", "2.0")); + assertThat(selectionValue.getModuleNameLookup()) + .containsExactly( + "A", + createModuleKey("A", ""), + "B", + createModuleKey("B", "1.0"), + "C", + createModuleKey("C", "2.0")); + } + + @Test + public void multipleVersionOverride_diamond_snappingToNextHighestVersion() throws Exception { + // A --> B1@1.0 -> C@1.0 + // \-> B2@1.0 -> C@1.3 [allowed] + // \-> B3@1.0 -> C@1.5 + // \-> B4@1.0 -> C@1.7 [allowed] + // \-> B5@1.0 -> C@2.0 [allowed] + setUpSkyFunctions( + "A", + ImmutableMap.builder() + .put( + createModuleKey("A", ""), + Module.builder() + .setName("A") + .setVersion(Version.EMPTY) + .addDep("B1", createModuleKey("B1", "1.0")) + .addDep("B2", createModuleKey("B2", "1.0")) + .addDep("B3", createModuleKey("B3", "1.0")) + .addDep("B4", createModuleKey("B4", "1.0")) + .addDep("B5", createModuleKey("B5", "1.0")) + .build()) + .put( + createModuleKey("B1", "1.0"), + Module.builder() + .setName("B1") + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "1.0")) + .build()) + .put( + createModuleKey("B2", "1.0"), + Module.builder() + .setName("B2") + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "1.3")) + .build()) + .put( + createModuleKey("B3", "1.0"), + Module.builder() + .setName("B3") + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "1.5")) + .build()) + .put( + createModuleKey("B4", "1.0"), + Module.builder() + .setName("B4") + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "1.7")) + .build()) + .put( + createModuleKey("B5", "1.0"), + Module.builder() + .setName("B5") + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "2.0")) + .build()) + .put( + createModuleKey("C", "1.0"), + Module.builder() + .setName("C") + .setVersion(Version.parse("1.0")) + .setCompatibilityLevel(1) + .build()) + .put( + createModuleKey("C", "1.3"), + Module.builder() + .setName("C") + .setVersion(Version.parse("1.3")) + .setCompatibilityLevel(1) + .build()) + .put( + createModuleKey("C", "1.5"), + Module.builder() + .setName("C") + .setVersion(Version.parse("1.5")) + .setCompatibilityLevel(1) + .build()) + .put( + createModuleKey("C", "1.7"), + Module.builder() + .setName("C") + .setVersion(Version.parse("1.7")) + .setCompatibilityLevel(1) + .build()) + .put( + createModuleKey("C", "2.0"), + Module.builder() + .setName("C") + .setVersion(Version.parse("2.0")) + .setCompatibilityLevel(2) + .build()) + .build(), + ImmutableMap.of( + "C", + MultipleVersionOverride.create( + ImmutableList.of(Version.parse("1.3"), Version.parse("1.7"), Version.parse("2.0")), + ""))); + + EvaluationResult result = + driver.evaluate(ImmutableList.of(SelectionValue.KEY), evaluationContext); + if (result.hasError()) { + fail(result.getError().toString()); + } + SelectionValue selectionValue = result.get(SelectionValue.KEY); + assertThat(selectionValue.getRootModuleName()).isEqualTo("A"); + // A --> B1@1.0 -> C@1.3 [originally C@1.0] + // \-> B2@1.0 -> C@1.3 [allowed] + // \-> B3@1.0 -> C@1.7 [originally C@1.5] + // \-> B4@1.0 -> C@1.7 [allowed] + // \-> B5@1.0 -> C@2.0 [allowed] + assertThat(selectionValue.getDepGraph()) + .containsExactly( + createModuleKey("A", ""), + Module.builder() + .setName("A") + .setVersion(Version.EMPTY) + .addDep("B1", createModuleKey("B1", "1.0")) + .addDep("B2", createModuleKey("B2", "1.0")) + .addDep("B3", createModuleKey("B3", "1.0")) + .addDep("B4", createModuleKey("B4", "1.0")) + .addDep("B5", createModuleKey("B5", "1.0")) + .build(), + createModuleKey("B1", "1.0"), + Module.builder() + .setName("B1") + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "1.3")) + .build(), + createModuleKey("B2", "1.0"), + Module.builder() + .setName("B2") + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "1.3")) + .build(), + createModuleKey("B3", "1.0"), + Module.builder() + .setName("B3") + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "1.7")) + .build(), + createModuleKey("B4", "1.0"), + Module.builder() + .setName("B4") + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "1.7")) + .build(), + createModuleKey("B5", "1.0"), + Module.builder() + .setName("B5") + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "2.0")) + .build(), + createModuleKey("C", "1.3"), + Module.builder() + .setName("C") + .setVersion(Version.parse("1.3")) + .setCompatibilityLevel(1) + .build(), + createModuleKey("C", "1.7"), + Module.builder() + .setName("C") + .setVersion(Version.parse("1.7")) + .setCompatibilityLevel(1) + .build(), + createModuleKey("C", "2.0"), + Module.builder() + .setName("C") + .setVersion(Version.parse("2.0")) + .setCompatibilityLevel(2) + .build()) + .inOrder(); + assertThat(selectionValue.getCanonicalRepoNameLookup()) + .containsExactly( + "A.", + createModuleKey("A", ""), + "B1.1.0", + createModuleKey("B1", "1.0"), + "B2.1.0", + createModuleKey("B2", "1.0"), + "B3.1.0", + createModuleKey("B3", "1.0"), + "B4.1.0", + createModuleKey("B4", "1.0"), + "B5.1.0", + createModuleKey("B5", "1.0"), + "C.1.3", + createModuleKey("C", "1.3"), + "C.1.7", + createModuleKey("C", "1.7"), + "C.2.0", + createModuleKey("C", "2.0")); + assertThat(selectionValue.getModuleNameLookup()) + .containsExactly( + "A", + createModuleKey("A", ""), + "B1", + createModuleKey("B1", "1.0"), + "B2", + createModuleKey("B2", "1.0"), + "B3", + createModuleKey("B3", "1.0"), + "B4", + createModuleKey("B4", "1.0"), + "B5", + createModuleKey("B5", "1.0")); + } + + @Test + public void multipleVersionOverride_diamond_dontSnapToDifferentCompatibility() throws Exception { + // A --> B1@1.0 -> C@1.0 [allowed] + // \-> B2@1.0 -> C@1.7 + // \-> B3@1.0 -> C@2.0 [allowed] + setUpSkyFunctions( + "A", + ImmutableMap.builder() + .put( + createModuleKey("A", ""), + Module.builder() + .setName("A") + .setVersion(Version.EMPTY) + .addDep("B1", createModuleKey("B1", "1.0")) + .addDep("B2", createModuleKey("B2", "1.0")) + .addDep("B3", createModuleKey("B3", "1.0")) + .build()) + .put( + createModuleKey("B1", "1.0"), + Module.builder() + .setName("B1") + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "1.0")) + .build()) + .put( + createModuleKey("B2", "1.0"), + Module.builder() + .setName("B2") + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "1.7")) + .build()) + .put( + createModuleKey("B3", "1.0"), + Module.builder() + .setName("B3") + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "2.0")) + .build()) + .put( + createModuleKey("C", "1.0"), + Module.builder() + .setName("C") + .setVersion(Version.parse("1.0")) + .setCompatibilityLevel(1) + .build()) + .put( + createModuleKey("C", "1.7"), + Module.builder() + .setName("C") + .setVersion(Version.parse("1.7")) + .setCompatibilityLevel(1) + .build()) + .put( + createModuleKey("C", "2.0"), + Module.builder() + .setName("C") + .setVersion(Version.parse("2.0")) + .setCompatibilityLevel(2) + .build()) + .build(), + ImmutableMap.of( + "C", + MultipleVersionOverride.create( + ImmutableList.of(Version.parse("1.0"), Version.parse("2.0")), ""))); + + EvaluationResult result = + driver.evaluate(ImmutableList.of(SelectionValue.KEY), evaluationContext); + assertThat(result.hasError()).isTrue(); + String error = result.getError().toString(); + assertThat(error) + .contains( + "B2@1.0 depends on C@1.7 which is not allowed by the multiple_version_override on C," + + " which allows only [1.0, 2.0]"); + } + + @Test + public void multipleVersionOverride_diamond_unknownCompatibility() throws Exception { + // A --> B1@1.0 -> C@1.0 [allowed] + // \-> B2@1.0 -> C@2.0 [allowed] + // \-> B3@1.0 -> C@3.0 + setUpSkyFunctions( + "A", + ImmutableMap.builder() + .put( + createModuleKey("A", ""), + Module.builder() + .setName("A") + .setVersion(Version.EMPTY) + .addDep("B1", createModuleKey("B1", "1.0")) + .addDep("B2", createModuleKey("B2", "1.0")) + .addDep("B3", createModuleKey("B3", "1.0")) + .build()) + .put( + createModuleKey("B1", "1.0"), + Module.builder() + .setName("B1") + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "1.0")) + .build()) + .put( + createModuleKey("B2", "1.0"), + Module.builder() + .setName("B2") + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "2.0")) + .build()) + .put( + createModuleKey("B3", "1.0"), + Module.builder() + .setName("B3") + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "3.0")) + .build()) + .put( + createModuleKey("C", "1.0"), + Module.builder() + .setName("C") + .setVersion(Version.parse("1.0")) + .setCompatibilityLevel(1) + .build()) + .put( + createModuleKey("C", "2.0"), + Module.builder() + .setName("C") + .setVersion(Version.parse("2.0")) + .setCompatibilityLevel(2) + .build()) + .put( + createModuleKey("C", "3.0"), + Module.builder() + .setName("C") + .setVersion(Version.parse("3.0")) + .setCompatibilityLevel(3) + .build()) + .build(), + ImmutableMap.of( + "C", + MultipleVersionOverride.create( + ImmutableList.of(Version.parse("1.0"), Version.parse("2.0")), ""))); + + EvaluationResult result = + driver.evaluate(ImmutableList.of(SelectionValue.KEY), evaluationContext); + assertThat(result.hasError()).isTrue(); + String error = result.getError().toString(); + assertThat(error) + .contains( + "B3@1.0 depends on C@3.0 which is not allowed by the multiple_version_override on C," + + " which allows only [1.0, 2.0]"); + } + + @Test + public void multipleVersionOverride_diamond_badVersionsAreOkayIfUnreferenced() throws Exception { + // 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 + setUpSkyFunctions( + "A", + ImmutableMap.builder() + .put( + createModuleKey("A", ""), + Module.builder() + .setName("A") + .setVersion(Version.EMPTY) + .addDep("B1", createModuleKey("B1", "1.0")) + .addDep("B2", createModuleKey("B2", "1.0")) + .addDep("B3", createModuleKey("B3", "1.0")) + .addDep("B4", createModuleKey("B4", "1.0")) + .build()) + .put( + createModuleKey("B1", "1.0"), + Module.builder() + .setName("B1") + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "1.0")) + .addDep("B2", createModuleKey("B2", "1.1")) + .build()) + .put( + createModuleKey("B2", "1.0"), + Module.builder() + .setName("B2") + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "1.5")) + .build()) + .put( + createModuleKey("B2", "1.1"), + Module.builder().setName("B2").setVersion(Version.parse("1.1")).build()) + .put( + createModuleKey("B3", "1.0"), + Module.builder() + .setName("B3") + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "2.0")) + .addDep("B4", createModuleKey("B4", "1.1")) + .build()) + .put( + createModuleKey("B4", "1.0"), + Module.builder() + .setName("B4") + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "3.0")) + .build()) + .put( + createModuleKey("B4", "1.1"), + Module.builder().setName("B4").setVersion(Version.parse("1.1")).build()) + .put( + createModuleKey("C", "1.0"), + Module.builder() + .setName("C") + .setVersion(Version.parse("1.0")) + .setCompatibilityLevel(1) + .build()) + .put( + createModuleKey("C", "1.5"), + Module.builder() + .setName("C") + .setVersion(Version.parse("1.5")) + .setCompatibilityLevel(1) + .build()) + .put( + createModuleKey("C", "2.0"), + Module.builder() + .setName("C") + .setVersion(Version.parse("2.0")) + .setCompatibilityLevel(2) + .build()) + .put( + createModuleKey("C", "3.0"), + Module.builder() + .setName("C") + .setVersion(Version.parse("3.0")) + .setCompatibilityLevel(3) + .build()) + .build(), + ImmutableMap.of( + "C", + MultipleVersionOverride.create( + ImmutableList.of(Version.parse("1.0"), Version.parse("2.0")), ""))); + + EvaluationResult result = + driver.evaluate(ImmutableList.of(SelectionValue.KEY), evaluationContext); + if (result.hasError()) { + fail(result.getError().toString()); + } + SelectionValue selectionValue = result.get(SelectionValue.KEY); + assertThat(selectionValue.getRootModuleName()).isEqualTo("A"); + // 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. + assertThat(selectionValue.getDepGraph()) + .containsExactly( + createModuleKey("A", ""), + Module.builder() + .setName("A") + .setVersion(Version.EMPTY) + .addDep("B1", createModuleKey("B1", "1.0")) + .addDep("B2", createModuleKey("B2", "1.1")) + .addDep("B3", createModuleKey("B3", "1.0")) + .addDep("B4", createModuleKey("B4", "1.1")) + .build(), + createModuleKey("B1", "1.0"), + Module.builder() + .setName("B1") + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "1.0")) + .addDep("B2", createModuleKey("B2", "1.1")) + .build(), + createModuleKey("B2", "1.1"), + Module.builder().setName("B2").setVersion(Version.parse("1.1")).build(), + createModuleKey("B3", "1.0"), + Module.builder() + .setName("B3") + .setVersion(Version.parse("1.0")) + .addDep("C", createModuleKey("C", "2.0")) + .addDep("B4", createModuleKey("B4", "1.1")) + .build(), + createModuleKey("B4", "1.1"), + Module.builder().setName("B4").setVersion(Version.parse("1.1")).build(), + createModuleKey("C", "1.0"), + Module.builder() + .setName("C") + .setVersion(Version.parse("1.0")) + .setCompatibilityLevel(1) + .build(), + createModuleKey("C", "2.0"), + Module.builder() + .setName("C") + .setVersion(Version.parse("2.0")) + .setCompatibilityLevel(2) + .build()) + .inOrder(); + assertThat(selectionValue.getCanonicalRepoNameLookup()) + .containsExactly( + "A.", + createModuleKey("A", ""), + "B1.1.0", + createModuleKey("B1", "1.0"), + "B2.1.1", + createModuleKey("B2", "1.1"), + "B3.1.0", + createModuleKey("B3", "1.0"), + "B4.1.1", + createModuleKey("B4", "1.1"), + "C.1.0", + createModuleKey("C", "1.0"), + "C.2.0", + createModuleKey("C", "2.0")); + assertThat(selectionValue.getModuleNameLookup()) + .containsExactly( + "A", + createModuleKey("A", ""), + "B1", + createModuleKey("B1", "1.0"), + "B2", + createModuleKey("B2", "1.1"), + "B3", + createModuleKey("B3", "1.0"), + "B4", + createModuleKey("B4", "1.1")); } }