From a3fe02adbdc7caf0a1b324c2ebaff2a738a295cc Mon Sep 17 00:00:00 2001 From: wyv Date: Mon, 28 Jun 2021 05:38:49 -0700 Subject: [PATCH] bzlmod: SelectionFunction (https://github.com/bazelbuild/bazel/issues/13316) This SkyFunction takes the result of DiscoveryFunction and runs MVS on it, resulting in a dep graph that contains only 1 version of each dependency (the highest requested one). PiperOrigin-RevId: 381844359 --- .../devtools/build/lib/bazel/bzlmod/BUILD | 3 + .../lib/bazel/bzlmod/DiscoveryFunction.java | 30 +- .../build/lib/bazel/bzlmod/Module.java | 16 +- .../build/lib/bazel/bzlmod/ParsedVersion.java | 182 +++++++++++ .../lib/bazel/bzlmod/SelectionFunction.java | 111 +++++++ .../lib/bazel/bzlmod/SelectionValue.java | 43 +++ .../build/lib/skyframe/SkyFunctions.java | 1 + .../lib/bazel/bzlmod/ParsedVersionTest.java | 80 +++++ .../bazel/bzlmod/SelectionFunctionTest.java | 290 ++++++++++++++++++ 9 files changed, 739 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ParsedVersion.java create mode 100644 src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SelectionFunction.java create mode 100644 src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SelectionValue.java create mode 100644 src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ParsedVersionTest.java create mode 100644 src/test/java/com/google/devtools/build/lib/bazel/bzlmod/SelectionFunctionTest.java diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD index ae23091cc5b860..3c6bc85938eef6 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD +++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD @@ -50,7 +50,10 @@ java_library( "ModuleFileValue.java", "ModuleOverride.java", "NonRegistryOverride.java", + "ParsedVersion.java", "RegistryOverride.java", + "SelectionFunction.java", + "SelectionValue.java", "SingleVersionOverride.java", ], deps = [ diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/DiscoveryFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/DiscoveryFunction.java index 206e9767e69945..e855d3b6dbfd51 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/DiscoveryFunction.java +++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/DiscoveryFunction.java @@ -77,24 +77,22 @@ public SkyValue compute(SkyKey skyKey, Environment env) private static Module rewriteDepKeys( Module module, ImmutableMap overrides, String rootModuleName) { - ImmutableMap.Builder newDeps = new ImmutableMap.Builder<>(); - for (Map.Entry entry : module.getDeps().entrySet()) { - ModuleKey depKey = entry.getValue(); - String newVersion = depKey.getVersion(); + return module.withDepKeysTransformed( + depKey -> { + String newVersion = depKey.getVersion(); - @Nullable ModuleOverride override = overrides.get(depKey.getName()); - if (override instanceof NonRegistryOverride || rootModuleName.equals(depKey.getName())) { - newVersion = ""; - } else if (override instanceof SingleVersionOverride) { - String overrideVersion = ((SingleVersionOverride) override).getVersion(); - if (!overrideVersion.isEmpty()) { - newVersion = overrideVersion; - } - } + @Nullable ModuleOverride override = overrides.get(depKey.getName()); + if (override instanceof NonRegistryOverride || rootModuleName.equals(depKey.getName())) { + newVersion = ""; + } else if (override instanceof SingleVersionOverride) { + String overrideVersion = ((SingleVersionOverride) override).getVersion(); + if (!overrideVersion.isEmpty()) { + newVersion = overrideVersion; + } + } - newDeps.put(entry.getKey(), ModuleKey.create(depKey.getName(), newVersion)); - } - return module.toBuilder().setDeps(newDeps.build()).build(); + return ModuleKey.create(depKey.getName(), newVersion); + }); } @Nullable diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/Module.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/Module.java index bba50676bd997b..843ad377f244a2 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/Module.java +++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/Module.java @@ -17,6 +17,8 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableMap; +import java.util.Map; +import java.util.function.UnaryOperator; import javax.annotation.Nullable; /** @@ -52,10 +54,22 @@ public abstract class Module { public abstract Builder toBuilder(); /** Returns a new, empty {@link Builder}. */ - static Builder builder() { + public static Builder builder() { return new AutoValue_Module.Builder(); } + /** + * Returns a new {@link Module} with all values in {@link #getDeps} transformed using the given + * function. + */ + public Module withDepKeysTransformed(UnaryOperator transform) { + ImmutableMap.Builder newDeps = new ImmutableMap.Builder<>(); + for (Map.Entry entry : getDeps().entrySet()) { + newDeps.put(entry.getKey(), transform.apply(entry.getValue())); + } + return toBuilder().setDeps(newDeps.build()).build(); + } + /** Builder type for {@link Module}. */ @AutoValue.Builder public abstract static class Builder { diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ParsedVersion.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ParsedVersion.java new file mode 100644 index 00000000000000..5237c7063accc2 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ParsedVersion.java @@ -0,0 +1,182 @@ +// Copyright 2021 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package com.google.devtools.build.lib.bazel.bzlmod; + +import static com.google.common.collect.Comparators.lexicographical; +import static com.google.common.primitives.Booleans.falseFirst; +import static com.google.common.primitives.Booleans.trueFirst; +import static java.util.Comparator.comparing; + +import com.google.auto.value.AutoValue; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import java.util.Comparator; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.annotation.Nullable; + +/** + * Represents a parsed version string, useful for comparison. The version format we support is + * {@code RELEASE[-PRERELEASE][+BUILD]}, where: + * + *
    + *
  • {@code RELEASE} is a sequence of decimal numbers separated by dots; + *
  • {@code PRERELEASE} is a sequence of "identifiers" (defined as a non-empty sequence of + * alphanumerical characters, hyphens, and underscores) separated by dots; + *
  • and {@code BUILD} is also a sequence of "identifiers" (see above) separated by dots. + *
+ * + * Otherwise, this format is identical to SemVer, especially in terms of the comparison algorithm + * (https://semver.org/#spec-item-11). In other words, this format is intentionally looser than + * SemVer; in particular, the "release" part isn't limited to exactly 3 numbers (major, minor, + * patch), but can be fewer or more. Underscores are also allowed in prerelease and build for regex + * brevity. + * + *

The special "empty string" version can also be used, and compares higher than everything else. + * It signifies that there is a {@link NonRegistryOverride} for a module. + */ +@AutoValue +abstract class ParsedVersion implements Comparable { + + // We don't care about the "build" part at all so don't capture it. + private static final Pattern PATTERN = + Pattern.compile("(?(?:\\d+\\.)*\\d+)(?:-(?[\\w.-]*))?(?:\\+[\\w.-]*)?"); + + private static final Splitter DOT_SPLITTER = Splitter.on('.'); + + /** + * Represents a segment in the prerelease part of the version string. This is separated from other + * "Identifier"s by a dot. An identifier is compared differently based on whether it's digits-only + * or not. + */ + @AutoValue + abstract static class Identifier { + + abstract boolean isDigitsOnly(); + + abstract int asNumber(); + + abstract String asString(); + + static Identifier from(String string) throws ParseException { + if (Strings.isNullOrEmpty(string)) { + throw new ParseException("identifier is empty"); + } + if (string.chars().allMatch(Character::isDigit)) { + return new AutoValue_ParsedVersion_Identifier(true, Integer.parseInt(string), string); + } else { + return new AutoValue_ParsedVersion_Identifier(false, 0, string); + } + } + } + + /** Returns the "release" part of the version string as a list of integers. */ + abstract ImmutableList getRelease(); + + /** Returns the "prerelease" part of the version string as a list of {@link Identifier}s. */ + abstract ImmutableList getPrerelease(); + + /** Returns the original version string. */ + public abstract String getOriginal(); + + /** + * Whether this is just the "empty string" version, which signifies a non-registry override for + * the module. + */ + boolean isOverride() { + return getRelease().isEmpty(); + } + + /** + * Whether this is a prerelease version (i.e. the prerelease part of the version string is + * non-empty). A prerelease version compares lower than the same version without the prerelease + * part. + */ + boolean isPrerelease() { + return !getPrerelease().isEmpty(); + } + + /** Parses a version string into a {@link ParsedVersion} object. */ + public static ParsedVersion parse(String version) throws ParseException { + if (version.isEmpty()) { + return new AutoValue_ParsedVersion(ImmutableList.of(), ImmutableList.of(), version); + } + Matcher matcher = PATTERN.matcher(version); + if (!matcher.matches()) { + throw new ParseException("bad version (does not match regex): " + version); + } + String release = matcher.group("release"); + @Nullable String prerelease = matcher.group("prerelease"); + + ImmutableList.Builder releaseSplit = new ImmutableList.Builder<>(); + for (String number : DOT_SPLITTER.split(release)) { + try { + releaseSplit.add(Integer.valueOf(number)); + } catch (NumberFormatException e) { + throw new ParseException("error parsing version: " + version, e); + } + } + + ImmutableList.Builder prereleaseSplit = new ImmutableList.Builder<>(); + if (!Strings.isNullOrEmpty(prerelease)) { + for (String ident : DOT_SPLITTER.split(prerelease)) { + try { + prereleaseSplit.add(Identifier.from(ident)); + } catch (ParseException e) { + throw new ParseException("error parsing version: " + version, e); + } + } + } + + return new AutoValue_ParsedVersion(releaseSplit.build(), prereleaseSplit.build(), version); + } + + private static final Comparator COMPARATOR = + Comparator.nullsFirst( + comparing(ParsedVersion::isOverride, falseFirst()) + .thenComparing( + ParsedVersion::getRelease, lexicographical(Comparator.naturalOrder())) + .thenComparing(ParsedVersion::isPrerelease, trueFirst()) + .thenComparing( + ParsedVersion::getPrerelease, + lexicographical( + comparing(Identifier::isDigitsOnly, trueFirst()) + .thenComparingInt(Identifier::asNumber) + .thenComparing(Identifier::asString)))); + + @Override + public int compareTo(ParsedVersion o) { + return Objects.compare(this, o, COMPARATOR); + } + + /** Returns the higher of two versions. */ + public static ParsedVersion max(@Nullable ParsedVersion a, @Nullable ParsedVersion b) { + return Objects.compare(a, b, COMPARATOR) >= 0 ? a : b; + } + + /** An exception encountered while trying to {@link ParsedVersion#parse parse} a version. */ + public static class ParseException extends Exception { + public ParseException(String message) { + super(message); + } + + public ParseException(String message, Throwable cause) { + super(message, cause); + } + } +} diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SelectionFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SelectionFunction.java new file mode 100644 index 00000000000000..752989492773da --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SelectionFunction.java @@ -0,0 +1,111 @@ +// Copyright 2021 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package com.google.devtools.build.lib.bazel.bzlmod; + +import com.google.common.collect.ImmutableMap; +import com.google.devtools.build.skyframe.SkyFunction; +import com.google.devtools.build.skyframe.SkyFunctionException; +import com.google.devtools.build.skyframe.SkyKey; +import com.google.devtools.build.skyframe.SkyValue; +import java.util.HashMap; +import java.util.Map; + +/** + * 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. + */ +public class SelectionFunction implements SkyFunction { + + @Override + public SkyValue compute(SkyKey skyKey, Environment env) + throws SkyFunctionException, InterruptedException { + DiscoveryValue discovery = (DiscoveryValue) env.getValue(DiscoveryValue.KEY); + if (discovery == null) { + return null; + } + + // TODO(wyv): compatibility_level, multiple_version_override + + // 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); + } + } + + // Now 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(); + if (!moduleKey.getVersion().equals(selectedVersion)) { + continue; + } + + // Rewrite deps to point to the selected version. + newDepGraphBuilder.put( + moduleKey, + module.withDepKeysTransformed( + depKey -> + ModuleKey.create( + depKey.getName(), + selectedVersionForEachModule.get(depKey.getName()).getOriginal()))); + } + 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); + return SelectionValue.create( + discovery.getRootModuleName(), + ImmutableMap.copyOf(finalDepGraph), + discovery.getOverrides()); + } + + 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); + } + } + + @Override + public String extractTag(SkyKey skyKey) { + return null; + } + + static final class SelectionFunctionException extends SkyFunctionException { + SelectionFunctionException(Exception cause) { + super(cause, Transience.PERSISTENT); + } + } +} diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SelectionValue.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SelectionValue.java new file mode 100644 index 00000000000000..dcccc30490255f --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SelectionValue.java @@ -0,0 +1,43 @@ +// Copyright 2021 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package com.google.devtools.build.lib.bazel.bzlmod; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableMap; +import com.google.devtools.build.lib.skyframe.SkyFunctions; +import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec; +import com.google.devtools.build.skyframe.SkyKey; +import com.google.devtools.build.skyframe.SkyValue; + +/** The result of running selection, containing the dependency graph post-version-resolution. */ +@AutoValue +public abstract class SelectionValue implements SkyValue { + + @AutoCodec public static final SkyKey KEY = () -> SkyFunctions.SELECTION; + + public static SelectionValue create( + String rootModuleName, + ImmutableMap depGraph, + ImmutableMap overrides) { + return new AutoValue_SelectionValue(rootModuleName, depGraph, overrides); + } + + public abstract String getRootModuleName(); + + public abstract ImmutableMap getDepGraph(); + + public abstract ImmutableMap getOverrides(); +} diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java index a6ccca63ede858..cb07796b366192 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java +++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java @@ -150,6 +150,7 @@ public final class SkyFunctions { public static final SkyFunctionName MODULE_FILE = SkyFunctionName.createNonHermetic("MODULE_FILE"); public static final SkyFunctionName DISCOVERY = SkyFunctionName.createHermetic("DISCOVERY"); + public static final SkyFunctionName SELECTION = SkyFunctionName.createHermetic("SELECTION"); public static Predicate isSkyFunction(final SkyFunctionName functionName) { return new Predicate() { diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ParsedVersionTest.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ParsedVersionTest.java new file mode 100644 index 00000000000000..3f90cbf41f5028 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ParsedVersionTest.java @@ -0,0 +1,80 @@ +// Copyright 2021 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package com.google.devtools.build.lib.bazel.bzlmod; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.devtools.build.lib.bazel.bzlmod.ParsedVersion.ParseException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link ParsedVersion}. */ +@RunWith(JUnit4.class) +public class ParsedVersionTest { + + @Test + public void testEmptyBeatsEverything() throws Exception { + assertThat(ParsedVersion.parse("")).isGreaterThan(null); + assertThat(ParsedVersion.parse("")).isGreaterThan(ParsedVersion.parse("1.0")); + assertThat(ParsedVersion.parse("")).isGreaterThan(ParsedVersion.parse("1.0+build")); + assertThat(ParsedVersion.parse("")).isGreaterThan(ParsedVersion.parse("1.0-pre")); + assertThat(ParsedVersion.parse("")).isGreaterThan(ParsedVersion.parse("1.0-pre+build-kek.lol")); + } + + @Test + public void testEverythingBeatsNull() throws Exception { + assertThat(ParsedVersion.parse("1.0")).isGreaterThan(null); + assertThat(ParsedVersion.parse("1.0+build")).isGreaterThan(null); + assertThat(ParsedVersion.parse("1.0-pre")).isGreaterThan(null); + assertThat(ParsedVersion.parse("1.0-pre+build-kek")).isGreaterThan(null); + } + + @Test + public void testReleaseVersion() throws Exception { + assertThat(ParsedVersion.parse("2.0")).isGreaterThan(ParsedVersion.parse("1.0")); + assertThat(ParsedVersion.parse("2.0")).isGreaterThan(ParsedVersion.parse("1.9")); + assertThat(ParsedVersion.parse("11.0")).isGreaterThan(ParsedVersion.parse("3.0")); + assertThat(ParsedVersion.parse("1.0.1")).isGreaterThan(ParsedVersion.parse("1.0")); + assertThat(ParsedVersion.parse("1.0.0")).isGreaterThan(ParsedVersion.parse("1.0")); + assertThat(ParsedVersion.parse("1.0+build2")) + .isEquivalentAccordingToCompareTo(ParsedVersion.parse("1.0+build3")); + assertThat(ParsedVersion.parse("1.0")).isGreaterThan(ParsedVersion.parse("1.0-pre")); + assertThat(ParsedVersion.parse("1.0")) + .isEquivalentAccordingToCompareTo(ParsedVersion.parse("1.0+build-notpre")); + } + + @Test + public void testPrereleaseVersion() throws Exception { + assertThat(ParsedVersion.parse("1.0-pre")).isGreaterThan(ParsedVersion.parse("1.0-are")); + assertThat(ParsedVersion.parse("1.0-3")).isGreaterThan(ParsedVersion.parse("1.0-2")); + assertThat(ParsedVersion.parse("1.0-pre")).isLessThan(ParsedVersion.parse("1.0-pre.foo")); + assertThat(ParsedVersion.parse("1.0-pre.3")).isGreaterThan(ParsedVersion.parse("1.0-pre.2")); + assertThat(ParsedVersion.parse("1.0-pre.10")).isGreaterThan(ParsedVersion.parse("1.0-pre.2")); + assertThat(ParsedVersion.parse("1.0-pre.10a")).isLessThan(ParsedVersion.parse("1.0-pre.2a")); + assertThat(ParsedVersion.parse("1.0-pre.99")).isLessThan(ParsedVersion.parse("1.0-pre.2a")); + } + + @Test + public void testParseException() throws Exception { + assertThrows(ParseException.class, () -> ParsedVersion.parse("abc")); + assertThrows(ParseException.class, () -> ParsedVersion.parse("1.0-pre?")); + assertThrows(ParseException.class, () -> ParsedVersion.parse("1.0-pre///")); + assertThrows(ParseException.class, () -> ParsedVersion.parse("1..0")); + assertThrows(ParseException.class, () -> ParsedVersion.parse("1.0-pre..erp")); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/SelectionFunctionTest.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/SelectionFunctionTest.java new file mode 100644 index 00000000000000..8d837e84506e35 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/SelectionFunctionTest.java @@ -0,0 +1,290 @@ +// Copyright 2021 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package com.google.devtools.build.lib.bazel.bzlmod; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.devtools.build.lib.skyframe.SkyFunctions; +import com.google.devtools.build.lib.testutil.FoundationTestCase; +import com.google.devtools.build.skyframe.EvaluationContext; +import com.google.devtools.build.skyframe.EvaluationResult; +import com.google.devtools.build.skyframe.InMemoryMemoizingEvaluator; +import com.google.devtools.build.skyframe.MemoizingEvaluator; +import com.google.devtools.build.skyframe.RecordingDifferencer; +import com.google.devtools.build.skyframe.SequencedRecordingDifferencer; +import com.google.devtools.build.skyframe.SequentialBuildDriver; +import com.google.devtools.build.skyframe.SkyFunction; +import com.google.devtools.build.skyframe.SkyFunctionName; +import com.google.devtools.build.skyframe.SkyKey; +import com.google.devtools.build.skyframe.SkyValue; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link SelectionFunction}. */ +@RunWith(JUnit4.class) +public class SelectionFunctionTest extends FoundationTestCase { + + private SequentialBuildDriver driver; + private RecordingDifferencer differencer; + private EvaluationContext evaluationContext; + + @Before + public void setup() throws Exception { + differencer = new SequencedRecordingDifferencer(); + evaluationContext = + EvaluationContext.newBuilder().setNumThreads(8).setEventHandler(reporter).build(); + } + + private void setUpDiscoveryResult(String rootModuleName, ImmutableMap depGraph) + throws Exception { + MemoizingEvaluator evaluator = + new InMemoryMemoizingEvaluator( + ImmutableMap.builder() + .put( + SkyFunctions.DISCOVERY, + new SkyFunction() { + @Override + public SkyValue compute(SkyKey skyKey, Environment env) { + return DiscoveryValue.create(rootModuleName, depGraph, ImmutableMap.of()); + } + + @Override + public String extractTag(SkyKey skyKey) { + return null; + } + }) + .put(SkyFunctions.SELECTION, new SelectionFunction()) + .build(), + differencer); + driver = new SequentialBuildDriver(evaluator); + } + + @Test + public void testSimpleDiamond() throws Exception { + setUpDiscoveryResult( + "A", + ImmutableMap.builder() + .put( + ModuleKey.create("A", ""), + Module.builder() + .setName("A") + .setVersion("") + .addDep("BfromA", ModuleKey.create("B", "1.0")) + .addDep("CfromA", ModuleKey.create("C", "2.0")) + .build()) + .put( + ModuleKey.create("B", "1.0"), + Module.builder() + .setName("B") + .setVersion("1.0") + .addDep("DfromB", ModuleKey.create("D", "1.0")) + .build()) + .put( + ModuleKey.create("C", "2.0"), + Module.builder() + .setName("C") + .setVersion("2.0") + .addDep("DfromC", ModuleKey.create("D", "2.0")) + .build()) + .put( + ModuleKey.create("D", "1.0"), + Module.builder().setName("D").setVersion("1.0").build()) + .put( + ModuleKey.create("D", "2.0"), + Module.builder().setName("D").setVersion("2.0").build()) + .build()); + + 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( + ModuleKey.create("A", ""), + Module.builder() + .setName("A") + .setVersion("") + .addDep("BfromA", ModuleKey.create("B", "1.0")) + .addDep("CfromA", ModuleKey.create("C", "2.0")) + .build(), + ModuleKey.create("B", "1.0"), + Module.builder() + .setName("B") + .setVersion("1.0") + .addDep("DfromB", ModuleKey.create("D", "2.0")) + .build(), + ModuleKey.create("C", "2.0"), + Module.builder() + .setName("C") + .setVersion("2.0") + .addDep("DfromC", ModuleKey.create("D", "2.0")) + .build(), + ModuleKey.create("D", "2.0"), + Module.builder().setName("D").setVersion("2.0").build()); + } + + @Test + public void testDiamondWithFurtherRemoval() throws Exception { + setUpDiscoveryResult( + "A", + ImmutableMap.builder() + .put( + ModuleKey.create("A", ""), + Module.builder() + .setName("A") + .setVersion("") + .addDep("B", ModuleKey.create("B", "1.0")) + .addDep("C", ModuleKey.create("C", "2.0")) + .build()) + .put( + ModuleKey.create("B", "1.0"), + Module.builder() + .setName("B") + .setVersion("1.0") + .addDep("D", ModuleKey.create("D", "1.0")) + .build()) + .put( + ModuleKey.create("C", "2.0"), + Module.builder() + .setName("C") + .setVersion("2.0") + .addDep("D", ModuleKey.create("D", "2.0")) + .build()) + .put( + ModuleKey.create("D", "1.0"), + Module.builder() + .setName("D") + .setVersion("1.0") + .addDep("E", ModuleKey.create("E", "1.0")) + .build()) + .put( + ModuleKey.create("D", "2.0"), + Module.builder().setName("D").setVersion("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()); + + 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( + ModuleKey.create("A", ""), + Module.builder() + .setName("A") + .setVersion("") + .addDep("B", ModuleKey.create("B", "1.0")) + .addDep("C", ModuleKey.create("C", "2.0")) + .build(), + ModuleKey.create("B", "1.0"), + Module.builder() + .setName("B") + .setVersion("1.0") + .addDep("D", ModuleKey.create("D", "2.0")) + .build(), + ModuleKey.create("C", "2.0"), + Module.builder() + .setName("C") + .setVersion("2.0") + .addDep("D", ModuleKey.create("D", "2.0")) + .build(), + ModuleKey.create("D", "2.0"), + Module.builder().setName("D").setVersion("2.0").build()); + } + + @Test + public void testCircularDependencyDueToSelection() throws Exception { + setUpDiscoveryResult( + "A", + ImmutableMap.builder() + .put( + ModuleKey.create("A", ""), + Module.builder() + .setName("A") + .setVersion("") + .addDep("B", ModuleKey.create("B", "1.0")) + .build()) + .put( + ModuleKey.create("B", "1.0"), + Module.builder() + .setName("B") + .setVersion("1.0") + .addDep("C", ModuleKey.create("C", "2.0")) + .build()) + .put( + ModuleKey.create("C", "2.0"), + Module.builder() + .setName("C") + .setVersion("2.0") + .addDep("B", ModuleKey.create("B", "1.0-pre")) + .build()) + .put( + ModuleKey.create("B", "1.0-pre"), + Module.builder() + .setName("B") + .setVersion("1.0-pre") + .addDep("D", ModuleKey.create("D", "1.0")) + .build()) + .put( + ModuleKey.create("D", "1.0"), + Module.builder().setName("D").setVersion("1.0").build()) + .build()); + + 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( + ModuleKey.create("A", ""), + Module.builder() + .setName("A") + .setVersion("") + .addDep("B", ModuleKey.create("B", "1.0")) + .build(), + ModuleKey.create("B", "1.0"), + Module.builder() + .setName("B") + .setVersion("1.0") + .addDep("C", ModuleKey.create("C", "2.0")) + .build(), + ModuleKey.create("C", "2.0"), + Module.builder() + .setName("C") + .setVersion("2.0") + .addDep("B", ModuleKey.create("B", "1.0")) + .build()); + // D is completely gone. + } +}