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:
+ *
+ *
+ * - In the most basic case, only one version of each module is selected (ie. remains in the dep
+ * graph). The selected version is simply the highest among all existing versions in the dep
+ * graph. In other words, each module name forms a "selection group". If foo@1.5 is selected,
+ * then any other foo@X is removed from the dep graph, and any module depending on foo@X will
+ * depend on foo@1.5 instead.
+ *
- As an extension of the above, we also remove any module that becomes unreachable from the
+ * root module because of the removal of some other module.
+ *
- If, however, versions of the same module but with different compatibility levels exist in
+ * the dep graph, then one version is selected for each compatibility level (ie. we split the
+ * selection groups by compatibility level). In the end, though, still only one version can
+ * remain in the dep graph after the removal of unselected and unreachable modules.
+ *
- Things get more complicated with multiple-version overrides. If module foo has a
+ * multiple-version override which allows version [1.3, 1.5, 2.0] (using the major version as
+ * the compatibility level), then we further split the selection groups by the target allowed
+ * version (keep in mind that versions are upgraded to the nearest higher-or-equal allowed
+ * version at the same compatibility level). If, for example, some module depends on foo@1.0,
+ * then it'll depend on foo@1.3 post-selection instead (and foo@1.0 will be removed). If any
+ * of foo@1.7, foo@2.2, or foo@3.0 exist in the dependency graph before selection, they must
+ * be removed before the end of selection (by becoming unreachable, for example), otherwise
+ * it'll be an error since they're not allowed by the override (these versions are in
+ * selection groups that have no valid target allowed version).
+ *
*/
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"));
}
}