diff --git a/src/main/java/com/google/devtools/build/lib/analysis/PlatformOptions.java b/src/main/java/com/google/devtools/build/lib/analysis/PlatformOptions.java index 788dc3a85956e0..391aae39f8f00a 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/PlatformOptions.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/PlatformOptions.java @@ -20,6 +20,8 @@ import com.google.devtools.build.lib.analysis.config.BuildConfiguration.LabelListConverter; import com.google.devtools.build.lib.analysis.config.FragmentOptions; import com.google.devtools.build.lib.cmdline.Label; +import com.google.devtools.build.lib.util.OptionsUtils; +import com.google.devtools.build.lib.vfs.PathFragment; import com.google.devtools.common.options.Converters.CommaSeparatedOptionListConverter; import com.google.devtools.common.options.Option; import com.google.devtools.common.options.OptionDocumentationCategory; @@ -39,6 +41,13 @@ public class PlatformOptions extends FragmentOptions { public static final Label LEGACY_DEFAULT_TARGET_PLATFORM = Label.parseAbsoluteUnchecked("@bazel_tools//platforms:target_platform"); + /** + * Main workspace-relative location to use when the user does not explicitly set {@code + * --platform_mappings}. + */ + public static final PathFragment DEFAULT_PLATFORM_MAPPINGS = + PathFragment.create("platform_mappings"); + @Option( name = "host_platform", oldName = "experimental_host_platform", @@ -169,6 +178,23 @@ public class PlatformOptions extends FragmentOptions { + " java_runtime.") public boolean useToolchainResolutionForJavaRules; + @Option( + name = "platform_mappings", + converter = OptionsUtils.EmptyToNullRelativePathFragmentConverter.class, + defaultValue = "", + documentationCategory = OptionDocumentationCategory.TOOLCHAIN, + effectTags = { + OptionEffectTag.AFFECTS_OUTPUTS, + OptionEffectTag.CHANGES_INPUTS, + OptionEffectTag.LOADING_AND_ANALYSIS + }, + help = + "The location of a mapping file that describes which platform to use if none is set or " + + "which flags to set when a platform already exists. Must be relative to the main " + + "workspace root. Defaults to 'platform_mappings' (a file directly under the " + + "workspace root).") + public PathFragment platformMappings; + @Override public PlatformOptions getHost() { PlatformOptions host = (PlatformOptions) getDefault(); diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PlatformMappingFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/PlatformMappingFunction.java new file mode 100644 index 00000000000000..33694f43b976ce --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/skyframe/PlatformMappingFunction.java @@ -0,0 +1,309 @@ +// Copyright 2019 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.skyframe; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.devtools.build.lib.actions.FileValue; +import com.google.devtools.build.lib.actions.MissingInputFileException; +import com.google.devtools.build.lib.analysis.BlazeDirectories; +import com.google.devtools.build.lib.analysis.config.BuildOptions; +import com.google.devtools.build.lib.cmdline.Label; +import com.google.devtools.build.lib.cmdline.LabelSyntaxException; +import com.google.devtools.build.lib.cmdline.RepositoryName; +import com.google.devtools.build.lib.events.Location; +import com.google.devtools.build.lib.vfs.FileSystemUtils; +import com.google.devtools.build.lib.vfs.PathFragment; +import com.google.devtools.build.lib.vfs.Root; +import com.google.devtools.build.lib.vfs.RootedPath; +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.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Optional; +import javax.annotation.Nullable; + +/** + * Function that reads the contents of a mapping file specified in {@code --platform_mappings} and + * parses them for use in a {@link PlatformMappingValue}. + * + *

Note that this class only parses the mapping-file specific format, parsing (and validation) of + * flags contained therein is left to the invocation of {@link + * PlatformMappingValue#map(BuildConfigurationValue.Key, BuildOptions)}. + */ +public class PlatformMappingFunction implements SkyFunction { + + private final BlazeDirectories blazeDirectories; + + public PlatformMappingFunction(BlazeDirectories blazeDirectories) { + this.blazeDirectories = blazeDirectories; + } + + @Nullable + @Override + public SkyValue compute(SkyKey skyKey, Environment env) + throws PlatformMappingException, InterruptedException { + PlatformMappingValue.Key platformMappingKey = (PlatformMappingValue.Key) skyKey.argument(); + PathFragment workspaceRelativeMappingPath = + platformMappingKey.getWorkspaceRelativeMappingPath(); + + Root workspaceRoot = Root.fromPath(blazeDirectories.getWorkspace()); + RootedPath rootedMappingPath = + RootedPath.toRootedPath(workspaceRoot, workspaceRelativeMappingPath); + FileValue fileValue = (FileValue) env.getValue(FileValue.key(rootedMappingPath)); + if (fileValue == null) { + return null; + } + + if (!fileValue.exists()) { + if (!platformMappingKey.wasExplicitlySetByUser()) { + // If no flag was passed and the default mapping file does not exist treat this as if the + // mapping file was empty rather than an error. + return PlatformMappingValue.EMPTY; + } + throw new PlatformMappingException( + new MissingInputFileException( + String.format( + "--platform_mappings was set to '%s' but no such file exists relative to the " + + "top-level workspace, '%s'", + workspaceRelativeMappingPath, workspaceRoot), + Location.BUILTIN), + SkyFunctionException.Transience.PERSISTENT); + } + if (fileValue.isDirectory()) { + throw new PlatformMappingException( + new MissingInputFileException( + String.format( + "--platform_mappings was set to '%s' relative to the top-level workspace '%s' but" + + "that path refers to a directory, not a file", + workspaceRelativeMappingPath, workspaceRoot), + Location.BUILTIN), + SkyFunctionException.Transience.PERSISTENT); + } + + Iterable lines; + try { + lines = + FileSystemUtils.readLines(fileValue.realRootedPath().asPath(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new PlatformMappingException(e, SkyFunctionException.Transience.PERSISTENT); + } + + return new Parser(lines.iterator()).parse().toPlatformMappingValue(); + } + + @Nullable + @Override + public String extractTag(SkyKey skyKey) { + return null; + } + + @VisibleForTesting + static class PlatformMappingException extends SkyFunctionException { + + public PlatformMappingException(Exception cause, Transience transience) { + super(cause, transience); + } + } + + @VisibleForTesting + static class Parser { + + private final Iterator lines; + + /** + * Using an optional to represent the next line with contents, {@link Optional#empty()} if we + * reached end of file. + * + *

Stores the current non-comment, non-empty, non-whitespace line. Don't access the field + * directly, it can either be "used up" by calling {@link #consume()} or retrieved without + * moving on by calling {@link #peek()}. + */ + private Optional line; + + Parser(Iterator lines) { + this.lines = lines; + } + + Mappings parse() throws PlatformMappingException { + goToNextContentLine(); + + if (!line.isPresent()) { + return new Mappings(ImmutableMap.of(), ImmutableMap.of()); + } + + Map> platformsToFlags = ImmutableMap.of(); + Map, Label> flagsToPlatforms = ImmutableMap.of(); + + if (!peek().equalsIgnoreCase("platforms:") && !peek().equalsIgnoreCase("flags:")) { + throwParsingException("Expected 'platforms:' or 'flags:' but got " + peek()); + } + + if (peek().equalsIgnoreCase("platforms:")) { + consume(); + platformsToFlags = platformsToFlags(); + } + + if (line.isPresent()) { + if (!peek().equalsIgnoreCase("flags:")) { + throwParsingException("Expected 'flags:' but got " + peek()); + } + consume(); + flagsToPlatforms = flagsToPlatforms(); + } + + if (line.isPresent()) { + throwParsingException("Expected end of file but got " + peek()); + } + return new Mappings(platformsToFlags, flagsToPlatforms); + } + + private Map> platformsToFlags() throws PlatformMappingException { + ImmutableMap.Builder> platformsToFlags = ImmutableMap.builder(); + while (line.isPresent() && !peek().equalsIgnoreCase("flags:")) { + Label platform = platform(); + Collection flags = flags(); + platformsToFlags.put(platform, flags); + } + + return platformsToFlags.build(); + } + + private Label platform() throws PlatformMappingException { + if (!line.isPresent()) { + throwParsingException("Expected platform label but got end of file"); + } + String label = consume(); + + Label platform; + try { + ImmutableMap emptyRepositoryMapping = ImmutableMap.of(); + // It is ok for us to use an empty repository mapping in this instance because all platform + // labels used in the mapping file should be relative to the root repository. Repository + // mappings however only apply within a repository imported by the root repository. + platform = Label.parseAbsolute(label, emptyRepositoryMapping); + } catch (LabelSyntaxException e) { + throw new PlatformMappingException( + new PlatformMappingParsingException("Expected platform label but got " + label, e), + SkyFunctionException.Transience.PERSISTENT); + } + return platform; + } + + private Collection flags() throws PlatformMappingException { + ImmutableSet.Builder flags = ImmutableSet.builder(); + // Note: Short form flags are not supported. + while (lineIsFlag()) { + flags.add(consume()); + } + ImmutableSet parsedFlags = flags.build(); + if (parsedFlags.isEmpty()) { + if (!line.isPresent()) { + throwParsingException("Expected a flag but got end of file"); + } + throwParsingException( + "Expected a standard format flag (starting with --) but got " + peek()); + } + + return parsedFlags; + } + + private Map, Label> flagsToPlatforms() throws PlatformMappingException { + ImmutableMap.Builder, Label> flagsToPlatforms = ImmutableMap.builder(); + while (lineIsFlag()) { + Collection flags = flags(); + Label platform = platform(); + flagsToPlatforms.put(flags, platform); + } + return flagsToPlatforms.build(); + } + + private String consume() { + Preconditions.checkState( + line.isPresent(), "Must make sure that a line exists before consuming."); + String value = line.get(); + goToNextContentLine(); + return value; + } + + private String peek() { + Preconditions.checkState( + line.isPresent(), "Must make sure that a line exists before peeking."); + return line.get(); + } + + private void throwParsingException(String message) throws PlatformMappingException { + throw new PlatformMappingException( + new PlatformMappingParsingException(message), SkyFunctionException.Transience.PERSISTENT); + } + + private boolean lineIsFlag() { + return line.isPresent() && peek().startsWith("--"); + } + + private void goToNextContentLine() { + while (lines.hasNext()) { + String line = lines.next().trim(); + if (line.isEmpty() || line.startsWith("#")) { + continue; + } + this.line = Optional.of(line); + return; + } + line = Optional.empty(); + } + } + + /** + * Simple data holder to make testing easier. Only for use internal to this file/tests thereof. + */ + @VisibleForTesting + static class Mappings { + final Map> platformsToFlags; + final Map, Label> flagsToPlatforms; + + Mappings( + Map> platformsToFlags, + Map, Label> flagsToPlatforms) { + this.platformsToFlags = platformsToFlags; + this.flagsToPlatforms = flagsToPlatforms; + } + + PlatformMappingValue toPlatformMappingValue() { + return new PlatformMappingValue(platformsToFlags, flagsToPlatforms); + } + } + + /** + * Inner wrapper exception to work around the fact that {@link SkyFunctionException} cannot carry + * a message of its own. + */ + private static class PlatformMappingParsingException extends Exception { + public PlatformMappingParsingException(String message) { + super(message); + } + + public PlatformMappingParsingException(String message, Throwable cause) { + super(message, cause); + } + } +} diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PlatformMappingValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/PlatformMappingValue.java index 4ad16cb904a48c..1c90fa6a5f89fa 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/PlatformMappingValue.java +++ b/src/main/java/com/google/devtools/build/lib/skyframe/PlatformMappingValue.java @@ -16,6 +16,7 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Interner; import com.google.common.collect.Iterables; import com.google.devtools.build.lib.analysis.PlatformOptions; @@ -24,7 +25,7 @@ import com.google.devtools.build.lib.concurrent.BlazeInterners; import com.google.devtools.build.lib.concurrent.ThreadSafety; import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec; -import com.google.devtools.build.lib.vfs.RootedPath; +import com.google.devtools.build.lib.vfs.PathFragment; import com.google.devtools.build.skyframe.SkyFunctionName; import com.google.devtools.build.skyframe.SkyKey; import com.google.devtools.build.skyframe.SkyValue; @@ -35,6 +36,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import javax.annotation.Nullable; /** * Stores contents of a platforms/flags mapping file for transforming one {@link @@ -46,22 +48,54 @@ */ public final class PlatformMappingValue implements SkyValue { + public static final PlatformMappingValue EMPTY = + new PlatformMappingValue(ImmutableMap.of(), ImmutableMap.of()); + /** Key for {@link PlatformMappingValue} based on the location of the mapping file. */ @ThreadSafety.Immutable @AutoCodec public static final class Key implements SkyKey { private static final Interner interner = BlazeInterners.newWeakInterner(); - private final RootedPath path; + /** + * Creates a new platform mappings key with the given, main workspace-relative path to the + * mappings file, typically derived from the {@code --platform_mappings} flag. + * + *

If the path is {@code null} the {@link PlatformOptions#DEFAULT_PLATFORM_MAPPINGS default + * path} will be used and the key marked as not having been set by a user. + * + * @param workspaceRelativeMappingPath main workspace relative path to the mappings file or + * {@code null} if the default location should be used + */ + public static Key create(@Nullable PathFragment workspaceRelativeMappingPath) { + if (workspaceRelativeMappingPath == null) { + return create(PlatformOptions.DEFAULT_PLATFORM_MAPPINGS, false); + } else { + return create(workspaceRelativeMappingPath, true); + } + } + + @AutoCodec.Instantiator + @AutoCodec.VisibleForSerialization + static Key create(PathFragment workspaceRelativeMappingPath, boolean wasExplicitlySetByUser) { + return interner.intern(new Key(workspaceRelativeMappingPath, wasExplicitlySetByUser)); + } + + private final PathFragment path; + private final boolean wasExplicitlySetByUser; - private Key(RootedPath path) { + private Key(PathFragment path, boolean wasExplicitlySetByUser) { this.path = path; + this.wasExplicitlySetByUser = wasExplicitlySetByUser; } - @AutoCodec.VisibleForSerialization - @AutoCodec.Instantiator - static Key create(RootedPath path) { - return interner.intern(new Key(path)); + /** Returns the main-workspace relative path this mapping's mapping file can be found at. */ + public PathFragment getWorkspaceRelativeMappingPath() { + return path; + } + + public boolean wasExplicitlySetByUser() { + return wasExplicitlySetByUser; } @Override @@ -78,17 +112,21 @@ public boolean equals(Object o) { return false; } Key key = (Key) o; - return Objects.equals(path, key.path); + return Objects.equals(path, key.path) && wasExplicitlySetByUser == key.wasExplicitlySetByUser; } @Override public int hashCode() { - return Objects.hash(path); + return Objects.hash(path, wasExplicitlySetByUser); } @Override public String toString() { - return "PlatformMappingValue.Key{" + "path=" + path + '}'; + return "PlatformMappingValue.Key{path=" + + path + + ", wasExplicitlySetByUser=" + + wasExplicitlySetByUser + + "}"; } } 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 8bfa5dfd312b51..21f5ad10d6b238 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 @@ -118,7 +118,7 @@ public final class SkyFunctions { public static final SkyFunctionName BUILD_INFO = SkyFunctionName.createHermetic("BUILD_INFO"); public static final SkyFunctionName WORKSPACE_NAME = SkyFunctionName.createHermetic("WORKSPACE_NAME"); - static final SkyFunctionName PLATFORM_MAPPING = + public static final SkyFunctionName PLATFORM_MAPPING = SkyFunctionName.createHermetic("PLATFORM_MAPPING"); static final SkyFunctionName COVERAGE_REPORT = SkyFunctionName.createHermetic("COVERAGE_REPORT"); public static final SkyFunctionName REPOSITORY = SkyFunctionName.createHermetic("REPOSITORY"); diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java index 2e84d5e4ce7e85..eb44df0887411c 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java +++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java @@ -587,6 +587,7 @@ private ImmutableMap skyFunctions( map.put(SkyFunctions.REPOSITORY_MAPPING, new RepositoryMappingFunction()); map.put(SkyFunctions.RESOLVED_HASH_VALUES, new ResolvedHashesFunction()); map.put(SkyFunctions.RESOLVED_FILE, new ResolvedFileFunction()); + map.put(SkyFunctions.PLATFORM_MAPPING, new PlatformMappingFunction(directories)); map.putAll(extraSkyFunctions); return map.build(); } diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/PlatformMappingFunctionParserTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/PlatformMappingFunctionParserTest.java new file mode 100644 index 00000000000000..6d7a9fa5225710 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/skyframe/PlatformMappingFunctionParserTest.java @@ -0,0 +1,354 @@ +// Copyright 2019 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.skyframe; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.devtools.build.lib.cmdline.Label; +import com.google.devtools.build.lib.cmdline.LabelSyntaxException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link PlatformMappingFunction}. */ +@RunWith(JUnit4.class) +public class PlatformMappingFunctionParserTest { + + private static final Label PLATFORM1 = Label.parseAbsoluteUnchecked("//platforms:one"); + private static final Label PLATFORM2 = Label.parseAbsoluteUnchecked("//platforms:two"); + + @Test + public void testParse() throws Exception { + PlatformMappingFunction.Mappings mappings = + parse( + "platforms:", + " //platforms:one", + " --cpu=one", + " //platforms:two", + " --cpu=two", + "flags:", + " --cpu=one", + " //platforms:one", + " --cpu=two", + " //platforms:two"); + + assertThat(mappings.platformsToFlags.keySet()).containsExactly(PLATFORM1, PLATFORM2); + assertThat(mappings.platformsToFlags.get(PLATFORM1)).containsExactly("--cpu=one"); + assertThat(mappings.platformsToFlags.get(PLATFORM2)).containsExactly("--cpu=two"); + + assertThat(mappings.flagsToPlatforms.keySet()) + .containsExactly(ImmutableSet.of("--cpu=one"), ImmutableSet.of("--cpu=two")); + assertThat(mappings.flagsToPlatforms.get(ImmutableSet.of("--cpu=one"))).isEqualTo(PLATFORM1); + assertThat(mappings.flagsToPlatforms.get(ImmutableSet.of("--cpu=two"))).isEqualTo(PLATFORM2); + } + + @Test + public void testParseComment() throws Exception { + PlatformMappingFunction.Mappings mappings = + parse( + "# A mapping file!", + "platforms:", + " # comment1", + " //platforms:one", + "# comment2", + " --cpu=one", + " //platforms:two", + " --cpu=two", + "flags:", + "# another comment", + " --cpu=one", + " //platforms:one", + " --cpu=two", + " //platforms:two"); + + assertThat(mappings.platformsToFlags.keySet()).containsExactly(PLATFORM1, PLATFORM2); + assertThat(mappings.platformsToFlags.get(PLATFORM1)).containsExactly("--cpu=one"); + assertThat(mappings.platformsToFlags.get(PLATFORM2)).containsExactly("--cpu=two"); + + assertThat(mappings.flagsToPlatforms.keySet()) + .containsExactly(ImmutableSet.of("--cpu=one"), ImmutableSet.of("--cpu=two")); + assertThat(mappings.flagsToPlatforms.get(ImmutableSet.of("--cpu=one"))).isEqualTo(PLATFORM1); + assertThat(mappings.flagsToPlatforms.get(ImmutableSet.of("--cpu=two"))).isEqualTo(PLATFORM2); + } + + @Test + public void testParseWhitespace() throws Exception { + PlatformMappingFunction.Mappings mappings = + parse( + "", + "platforms:", + " ", + " //platforms:one", + "", + " --cpu=one", + " //platforms:two ", + " --cpu=two ", + "flags:", + " ", + "", + "--cpu=one", + " //platforms:one", + " --cpu=two", + " //platforms:two"); + + assertThat(mappings.platformsToFlags.keySet()).containsExactly(PLATFORM1, PLATFORM2); + assertThat(mappings.platformsToFlags.get(PLATFORM1)).containsExactly("--cpu=one"); + assertThat(mappings.platformsToFlags.get(PLATFORM2)).containsExactly("--cpu=two"); + + assertThat(mappings.flagsToPlatforms.keySet()) + .containsExactly(ImmutableSet.of("--cpu=one"), ImmutableSet.of("--cpu=two")); + assertThat(mappings.flagsToPlatforms.get(ImmutableSet.of("--cpu=one"))).isEqualTo(PLATFORM1); + assertThat(mappings.flagsToPlatforms.get(ImmutableSet.of("--cpu=two"))).isEqualTo(PLATFORM2); + } + + @Test + public void testParseMultipleFlagsInPlatform() throws Exception { + PlatformMappingFunction.Mappings mappings = + parse( + "platforms:", + " //platforms:one", + " --cpu=one", + " --compilation_mode=dbg", + " //platforms:two", + " --cpu=two"); + + assertThat(mappings.platformsToFlags.keySet()).containsExactly(PLATFORM1, PLATFORM2); + assertThat(mappings.platformsToFlags.get(PLATFORM1)) + .containsExactly("--cpu=one", "--compilation_mode=dbg"); + } + + @Test + public void testParseMultipleFlagsInFlags() throws Exception { + PlatformMappingFunction.Mappings mappings = + parse( + "flags:", + " --compilation_mode=dbg", + " --cpu=one", + " //platforms:one", + " --cpu=two", + " //platforms:two"); + + assertThat(mappings.flagsToPlatforms.keySet()) + .containsExactly( + ImmutableSet.of("--cpu=one", "--compilation_mode=dbg"), ImmutableSet.of("--cpu=two")); + assertThat( + mappings.flagsToPlatforms.get(ImmutableSet.of("--cpu=one", "--compilation_mode=dbg"))) + .isEqualTo(PLATFORM1); + } + + @Test + public void testParseOnlyPlatforms() throws Exception { + PlatformMappingFunction.Mappings mappings = + parse( + "platforms:", // Force line break + " //platforms:one", // Force line break + " --cpu=one" // Force line break + ); + + assertThat(mappings.platformsToFlags.keySet()).containsExactly(PLATFORM1); + assertThat(mappings.platformsToFlags.get(PLATFORM1)).containsExactly("--cpu=one"); + assertThat(mappings.flagsToPlatforms).isEmpty(); + } + + @Test + public void testParseOnlyFlags() throws Exception { + PlatformMappingFunction.Mappings mappings = + parse( + "flags:", // Force line break + " --cpu=one", // Force line break + " //platforms:one" // Force line break + ); + + assertThat(mappings.flagsToPlatforms.keySet()).containsExactly(ImmutableSet.of("--cpu=one")); + assertThat(mappings.flagsToPlatforms.get(ImmutableSet.of("--cpu=one"))).isEqualTo(PLATFORM1); + assertThat(mappings.platformsToFlags).isEmpty(); + } + + @Test + public void testParseEmpty() throws Exception { + PlatformMappingFunction.Mappings mappings = parse(); + + assertThat(mappings.flagsToPlatforms).isEmpty(); + assertThat(mappings.platformsToFlags).isEmpty(); + } + + @Test + public void testParseEmptySections() throws Exception { + PlatformMappingFunction.Mappings mappings = parse("platforms:", "flags:"); + + assertThat(mappings.flagsToPlatforms).isEmpty(); + assertThat(mappings.platformsToFlags).isEmpty(); + } + + @Test + public void testParseCommentOnly() throws Exception { + PlatformMappingFunction.Mappings mappings = parse("#No mappings"); + + assertThat(mappings.flagsToPlatforms).isEmpty(); + assertThat(mappings.platformsToFlags).isEmpty(); + } + + @Test + public void testParseExtraPlatformInFlags() throws Exception { + PlatformMappingFunction.PlatformMappingException exception = + assertThrows( + PlatformMappingFunction.PlatformMappingException.class, + () -> + parse( + "flags:", // Force line break + " --cpu=one", // Force line break + " //platforms:one", // Force line break + " //platforms:two" // Force line break + )); + + assertThat(exception).hasMessageThat().contains("//platforms:two"); + } + + @Test + public void testParsePlatformWithoutFlags() throws Exception { + PlatformMappingFunction.PlatformMappingException exception = + assertThrows( + PlatformMappingFunction.PlatformMappingException.class, + () -> + parse( + "platforms:", // Force line break + " //platforms:one" // Force line break + )); + + assertThat(exception).hasMessageThat().contains("end of file"); + } + + @Test + public void testParseFlagsWithoutPlatform() throws Exception { + PlatformMappingFunction.PlatformMappingException exception = + assertThrows( + PlatformMappingFunction.PlatformMappingException.class, + () -> + parse( + "flags:", // Force line break + " --cpu=one" // Force line break + )); + + assertThat(exception).hasMessageThat().contains("end of file"); + } + + @Test + public void testParseCommentEndOfFile() throws Exception { + PlatformMappingFunction.Mappings mappings = + parse( + "platforms:", // Force line break + " //platforms:one", // Force line break + " --cpu=one", // Force line break + "# No more mappings" // Force line break + ); + + assertThat(mappings.platformsToFlags).isNotEmpty(); + } + + @Test + public void testParseUnknownSection() throws Exception { + PlatformMappingFunction.PlatformMappingException exception = + assertThrows( + PlatformMappingFunction.PlatformMappingException.class, + () -> + parse( + "platform:", // Force line break + " //platforms:one", // Force line break + " --cpu=one" // Force line break + )); + + assertThat(exception).hasMessageThat().contains("platform:"); + + PlatformMappingFunction.PlatformMappingException exception2 = + assertThrows( + PlatformMappingFunction.PlatformMappingException.class, + () -> + parse( + "platforms:", + " //platforms:one", + " --cpu=one", + "flag:", + " --cpu=one", + " //platforms:one")); + + assertThat(exception).hasMessageThat().contains("platform"); + } + + @Test + public void testParsePlatformsInvalidPlatformLabel() throws Exception { + PlatformMappingFunction.PlatformMappingException exception = + assertThrows( + PlatformMappingFunction.PlatformMappingException.class, + () -> + parse( + "platforms:", // Force line break + " @@@", // Force line break + " --cpu=one")); + + assertThat(exception).hasMessageThat().contains("@@@"); + assertThat(exception).hasCauseThat().hasCauseThat().isInstanceOf(LabelSyntaxException.class); + } + + @Test + public void testParseFlagsInvalidPlatformLabel() throws Exception { + PlatformMappingFunction.PlatformMappingException exception = + assertThrows( + PlatformMappingFunction.PlatformMappingException.class, + () -> + parse( + "flags:", // Force line break + " --cpu=one", // Force line break + " @@@")); + + assertThat(exception).hasMessageThat().contains("@@@"); + assertThat(exception).hasCauseThat().hasCauseThat().isInstanceOf(LabelSyntaxException.class); + } + + @Test + public void testParsePlatformsInvalidFlag() throws Exception { + PlatformMappingFunction.PlatformMappingException exception = + assertThrows( + PlatformMappingFunction.PlatformMappingException.class, + () -> + parse( + "platforms:", // Force line break + " //platforms:one", // Force line break + " -cpu=one")); + + assertThat(exception).hasMessageThat().contains("-cpu"); + } + + @Test + public void testParseFlagsInvalidFlag() throws Exception { + PlatformMappingFunction.PlatformMappingException exception = + assertThrows( + PlatformMappingFunction.PlatformMappingException.class, + () -> + parse( + "flags:", // Force line break + " -cpu=one", // Force line break + " //platforms:one")); + + assertThat(exception).hasMessageThat().contains("-cpu"); + } + + private PlatformMappingFunction.Mappings parse(String... lines) + throws PlatformMappingFunction.PlatformMappingException { + return new PlatformMappingFunction.Parser(ImmutableList.copyOf(lines).iterator()).parse(); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/PlatformMappingFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/PlatformMappingFunctionTest.java new file mode 100644 index 00000000000000..194fa03a2a0345 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/skyframe/PlatformMappingFunctionTest.java @@ -0,0 +1,159 @@ +// Copyright 2019 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.skyframe; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.devtools.build.lib.analysis.PlatformOptions.LEGACY_DEFAULT_TARGET_PLATFORM; +import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.devtools.build.lib.actions.MissingInputFileException; +import com.google.devtools.build.lib.analysis.PlatformConfiguration; +import com.google.devtools.build.lib.analysis.PlatformOptions; +import com.google.devtools.build.lib.analysis.config.BuildConfiguration; +import com.google.devtools.build.lib.analysis.config.BuildOptions; +import com.google.devtools.build.lib.analysis.config.FragmentOptions; +import com.google.devtools.build.lib.analysis.util.BuildViewTestCase; +import com.google.devtools.build.lib.cmdline.Label; +import com.google.devtools.build.lib.rules.repository.RepositoryDelegatorFunction; +import com.google.devtools.build.lib.skyframe.util.SkyframeExecutorTestUtils; +import com.google.devtools.build.lib.vfs.PathFragment; +import com.google.devtools.build.skyframe.EvaluationResult; +import com.google.devtools.common.options.OptionsParsingException; +import java.util.Set; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Unit tests for {@link PlatformMappingFunction}. + * + *

Note that all parsing tests are located in {@link PlatformMappingFunctionParserTest}. + */ +@RunWith(JUnit4.class) +public class PlatformMappingFunctionTest extends BuildViewTestCase { + + // We don't actually care about the contents of this set other than that it is passed intact + // through the mapping logic. The platform fragment in it is purely an example, it could be any + // set of fragments. + private static final Set> PLATFORM_FRAGMENT_CLASS = + ImmutableSet.of(PlatformConfiguration.class); + + private static final ImmutableList> + BUILD_CONFIG_PLATFORM_OPTIONS = + ImmutableList.of(BuildConfiguration.Options.class, PlatformOptions.class); + + private static final Label PLATFORM1 = Label.parseAbsoluteUnchecked("//platforms:one"); + + private static final BuildOptions DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS = + getDefaultBuildConfigPlatformOptions(); + private static final BuildOptions.OptionsDiffForReconstruction EMPTY_DIFF = + BuildOptions.diffForReconstruction( + DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS, DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS); + + @Test + public void testMappingFileDoesNotExist() throws Exception { + MissingInputFileException exception = + assertThrows( + MissingInputFileException.class, + () -> + executeFunction( + PlatformMappingValue.Key.create(PathFragment.create("random_location")))); + assertThat(exception).hasMessageThat().contains("random_location"); + } + + @Test + public void testMappingFileDoesNotExistDefaultLocation() throws Exception { + PlatformMappingValue platformMappingValue = + executeFunction(PlatformMappingValue.Key.create(null)); + + BuildConfigurationValue.Key key = + BuildConfigurationValue.key(PLATFORM_FRAGMENT_CLASS, EMPTY_DIFF); + + BuildConfigurationValue.Key mapped = + platformMappingValue.map(key, DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS); + + assertThat(toMappedOptions(mapped).get(PlatformOptions.class).platforms) + .containsExactly(LEGACY_DEFAULT_TARGET_PLATFORM); + } + + @Test + public void testMappingFileIsDirectory() throws Exception { + scratch.dir("somedir"); + + MissingInputFileException exception = + assertThrows( + MissingInputFileException.class, + () -> executeFunction(PlatformMappingValue.Key.create(PathFragment.create("somedir")))); + assertThat(exception).hasMessageThat().contains("somedir"); + } + + @Test + public void testMappingFileIsRead() throws Exception { + scratch.file( + "my_mapping_file", + "platforms:", // Force line break + " //platforms:one", // Force line break + " --cpu=one"); + + PlatformMappingValue platformMappingValue = + executeFunction(PlatformMappingValue.Key.create(PathFragment.create("my_mapping_file"))); + + BuildOptions modifiedOptions = DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS.clone(); + modifiedOptions.get(PlatformOptions.class).platforms = ImmutableList.of(PLATFORM1); + + BuildConfigurationValue.Key mapped = + platformMappingValue.map( + keyForOptions(modifiedOptions), DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS); + + assertThat(toMappedOptions(mapped).get(BuildConfiguration.Options.class).cpu).isEqualTo("one"); + } + + private PlatformMappingValue executeFunction(PlatformMappingValue.Key key) throws Exception { + SkyframeExecutor skyframeExecutor = getSkyframeExecutor(); + skyframeExecutor.injectExtraPrecomputedValues( + ImmutableList.of( + PrecomputedValue.injected( + RepositoryDelegatorFunction.RESOLVED_FILE_INSTEAD_OF_WORKSPACE, + Optional.absent()))); + EvaluationResult result = + SkyframeExecutorTestUtils.evaluate(skyframeExecutor, key, /*keepGoing=*/ false, reporter); + if (result.hasError()) { + throw result.getError(key).getException(); + } + return result.get(key); + } + + private BuildOptions toMappedOptions(BuildConfigurationValue.Key mapped) { + return DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS.applyDiff(mapped.getOptionsDiff()); + } + + private static BuildOptions getDefaultBuildConfigPlatformOptions() { + try { + return BuildOptions.of(BUILD_CONFIG_PLATFORM_OPTIONS); + } catch (OptionsParsingException e) { + throw new RuntimeException(e); + } + } + + private BuildConfigurationValue.Key keyForOptions(BuildOptions modifiedOptions) { + BuildOptions.OptionsDiffForReconstruction diff = + BuildOptions.diffForReconstruction(DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS, modifiedOptions); + + return BuildConfigurationValue.key(PLATFORM_FRAGMENT_CLASS, diff); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/PlatformMappingValueTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/PlatformMappingValueTest.java index af504702078190..88cdf7be4093e1 100644 --- a/src/test/java/com/google/devtools/build/lib/skyframe/PlatformMappingValueTest.java +++ b/src/test/java/com/google/devtools/build/lib/skyframe/PlatformMappingValueTest.java @@ -27,6 +27,7 @@ import com.google.devtools.build.lib.analysis.config.CompilationMode; import com.google.devtools.build.lib.analysis.config.FragmentOptions; import com.google.devtools.build.lib.cmdline.Label; +import com.google.devtools.build.lib.vfs.PathFragment; import com.google.devtools.common.options.OptionsParsingException; import java.util.Collection; import java.util.Set; @@ -203,6 +204,23 @@ public void testMapNoMappingIfPlatformIsSetAndNoPlatformMapping() throws Excepti assertThat(keyForOptions(modifiedOptions)).isEqualTo(mapped); } + @Test + public void testDefaultKey() { + PlatformMappingValue.Key key = PlatformMappingValue.Key.create(null); + + assertThat(key.getWorkspaceRelativeMappingPath()) + .isEqualTo(PlatformOptions.DEFAULT_PLATFORM_MAPPINGS); + assertThat(key.wasExplicitlySetByUser()).isFalse(); + } + + @Test + public void testCustomKey() { + PlatformMappingValue.Key key = PlatformMappingValue.Key.create(PathFragment.create("my/path")); + + assertThat(key.getWorkspaceRelativeMappingPath()).isEqualTo(PathFragment.create("my/path")); + assertThat(key.wasExplicitlySetByUser()).isTrue(); + } + private BuildOptions toMappedOptions(BuildConfigurationValue.Key mapped) { return DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS.applyDiff(mapped.getOptionsDiff()); }