aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar ccalvarin <ccalvarin@google.com>2017-11-09 23:03:39 +0100
committerGravatar Damien Martin-Guillerez <dmarting@google.com>2017-11-10 23:27:38 +0100
commit688ff592223b756f3749f4d6e46a649ec673ebdb (patch)
tree02da24962c611385f890c06d7c5662a820d056ee
parent10b0d8aa6b73a024cc007c5e075cb329add878ef (diff)
Make the config expansion behavior modular.
Reorganize the BlazeOptionHandler to make it easy to exchange out how the config options are expanded using the definitions in the blazerc. Also change the getOptionsMap call to actually structure the rc options in the order that we parse them: we expand them in command order (for the test command, first add all "common" options, then "build," then "test") and then within each command, we expand the options in the rc order. This somewhat simplifies the rc expansion code, and avoids the two-phase regrouping that used to happen. This change should not change the semantics of rc option ordering. A followup change will add an alternative implementation for --config's expansion. RELNOTES: None. PiperOrigin-RevId: 175208971
-rw-r--r--src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java6
-rw-r--r--src/main/java/com/google/devtools/build/lib/runtime/BlazeOptionHandler.java363
-rw-r--r--src/test/java/com/google/devtools/build/lib/runtime/BlazeOptionHandlerTest.java639
3 files changed, 863 insertions, 145 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java
index 8ba597cf41..2ed29f3613 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java
@@ -260,12 +260,11 @@ public class BlazeCommandDispatcher {
StoredEventHandler storedEventHandler = new StoredEventHandler();
BlazeOptionHandler optionHandler =
- new BlazeOptionHandler(
+ BlazeOptionHandler.getHandler(
runtime,
workspace,
command,
commandAnnotation,
- commandName,
// Provide the options parser so that we can cache OptionsData here.
createOptionsParser(command),
invocationPolicy);
@@ -588,8 +587,7 @@ public class BlazeCommandDispatcher {
* classes.
*
* <p>An overriding method should first call this method and can then override default values
- * directly or by calling {@link BlazeOptionHandler#parseOptionsForCommand} for command-specific
- * options.
+ * directly or by calling {@link BlazeOptionHandler#parseOptions} for command-specific options.
*/
private OptionsParser createOptionsParser(BlazeCommand command)
throws OptionsParser.ConstructionException {
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeOptionHandler.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeOptionHandler.java
index 25afbf2cd7..cb7548ba81 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeOptionHandler.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeOptionHandler.java
@@ -26,7 +26,6 @@ import com.google.devtools.build.lib.events.ExtendedEventHandler;
import com.google.devtools.build.lib.runtime.commands.ProjectFileSupport;
import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy;
import com.google.devtools.build.lib.util.ExitCode;
-import com.google.devtools.build.lib.util.Pair;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.common.options.InvocationPolicyEnforcer;
@@ -44,14 +43,13 @@ import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.logging.Level;
-import javax.annotation.Nullable;
/**
* Handles parsing the blaze command arguments.
*
* <p>This class manages rc options, default options, and invocation policy.
*/
-public class BlazeOptionHandler {
+public abstract class BlazeOptionHandler {
// Keep in sync with options added in OptionProcessor::AddRcfileArgsAndOptions()
private static final ImmutableSet<String> INTERNAL_COMMAND_OPTIONS =
ImmutableSet.of(
@@ -63,32 +61,40 @@ public class BlazeOptionHandler {
"client_env",
"client_cwd");
- private final BlazeRuntime runtime;
- private final OptionsParser optionsParser;
- private final BlazeWorkspace workspace;
- private final BlazeCommand command;
- private final Command commandAnnotation;
- private final String commandName;
- private final InvocationPolicy invocationPolicy;
- private final List<String> rcfileNotes = new ArrayList<>();
+ protected final BlazeRuntime runtime;
+ protected final OptionsParser optionsParser;
+ protected final BlazeWorkspace workspace;
+ protected final BlazeCommand command;
+ protected final Command commandAnnotation;
+ protected final InvocationPolicy invocationPolicy;
+ protected final List<String> rcfileNotes = new ArrayList<>();
- public BlazeOptionHandler(
+ private BlazeOptionHandler(
BlazeRuntime runtime,
BlazeWorkspace workspace,
BlazeCommand command,
Command commandAnnotation,
- String commandName,
OptionsParser optionsParser,
InvocationPolicy invocationPolicy) {
this.runtime = runtime;
this.workspace = workspace;
this.command = command;
this.commandAnnotation = commandAnnotation;
- this.commandName = commandName;
this.optionsParser = optionsParser;
this.invocationPolicy = invocationPolicy;
}
+ public static BlazeOptionHandler getHandler(
+ BlazeRuntime runtime,
+ BlazeWorkspace workspace,
+ BlazeCommand command,
+ Command commandAnnotation,
+ OptionsParser optionsParser,
+ InvocationPolicy invocationPolicy) {
+ return new FixPointConfigExpansionRcHandler(
+ runtime, workspace, command, commandAnnotation, optionsParser, invocationPolicy);
+ }
+
// Return options as OptionsProvider so the options can't be easily modified after we've
// applied the invocation policy.
OptionsProvider getOptionsResult() {
@@ -111,7 +117,9 @@ public class BlazeOptionHandler {
if (!workspace.getDirectories().inWorkspace()) {
eventHandler.handle(
Event.error(
- "The '" + commandName + "' command is only supported from within a workspace."));
+ "The '"
+ + commandAnnotation.name()
+ + "' command is only supported from within a workspace."));
return ExitCode.COMMAND_LINE_ERROR;
}
@@ -144,6 +152,50 @@ public class BlazeOptionHandler {
return ExitCode.SUCCESS;
}
+ /**
+ * Parses the unconditional options from .rc files for the current command.
+ *
+ * <p>This is not as trivial as simply taking the list of options for the specified command
+ * because commands can inherit arguments from each other, and we have to respect that (e.g. if an
+ * option is specified for 'build', it needs to take effect for the 'test' command, too). More
+ * specific commands should have priority over the broader commands (say a "build" option that
+ * conflicts with a "common" option should override the common one regardless of order.)
+ *
+ * <p>For each command, the options are parsed in rc order. This uses the master rc file first,
+ * and follows import statements. This is the order in which they were passed by the client.
+ */
+ void parseRcOptions(
+ EventHandler eventHandler, ListMultimap<String, RcChunkOfArgs> commandToRcArgs)
+ throws OptionsParsingException {
+ for (String commandToParse : getCommandNamesToParse(commandAnnotation)) {
+ // Get all args defined for this command (or "common"), grouped by rc chunk.
+ for (RcChunkOfArgs rcArgs : commandToRcArgs.get(commandToParse)) {
+ if (!rcArgs.args.isEmpty()) {
+ String inherited = commandToParse.equals(commandAnnotation.name()) ? "" : "Inherited ";
+ String source =
+ rcArgs.rcFile.equals("client")
+ ? "Options provided by the client"
+ : String.format(
+ "Reading rc options for '%s' from %s",
+ commandAnnotation.name(), rcArgs.rcFile);
+ rcfileNotes.add(
+ String.format(
+ "%s:\n %s'%s' options: %s",
+ source, inherited, commandToParse, Joiner.on(' ').join(rcArgs.args)));
+ }
+ optionsParser.parse(PriorityCategory.RC_FILE, rcArgs.rcFile, rcArgs.args);
+ }
+ }
+ }
+
+ /**
+ * Expand the values of --config according to the definitions provided in the rc files and the
+ * applicable command.
+ */
+ abstract void expandConfigOptions(
+ EventHandler eventHandler, ListMultimap<String, RcChunkOfArgs> commandToRcArgs)
+ throws OptionsParsingException;
+
private void parseArgsAndConfigs(List<String> args, ExtendedEventHandler eventHandler)
throws OptionsParsingException {
Path workspaceDirectory = workspace.getWorkspace();
@@ -165,17 +217,16 @@ public class BlazeOptionHandler {
optionsParser.parseWithSourceFunction(
PriorityCategory.COMMAND_LINE, commandOptionSourceFunction, cmdLineAfterCommand);
- // Command-specific options from .blazerc passed in via --default_override
- // and --rc_source. A no-op if none are provided.
+ // Command-specific options from .blazerc passed in via --default_override and --rc_source.
ClientOptions rcFileOptions = optionsParser.getOptions(ClientOptions.class);
- List<Pair<String, ListMultimap<String, String>>> optionsMap =
- getOptionsMap(
+ ListMultimap<String, RcChunkOfArgs> commandToRcArgs =
+ structureRcOptionsAndConfigs(
eventHandler,
rcFileOptions.rcSource,
rcFileOptions.optionsOverrides,
runtime.getCommandMap().keySet());
+ parseRcOptions(eventHandler, commandToRcArgs);
- parseOptionsForCommand(rcfileNotes, commandAnnotation, optionsParser, optionsMap, null, null);
if (commandAnnotation.builds()) {
// splits project files from targets in the traditional sense
ProjectFileSupport.handleProjectFiles(
@@ -187,35 +238,7 @@ public class BlazeOptionHandler {
commandAnnotation.name());
}
- // Fix-point iteration until all configs are loaded.
- List<String> configsLoaded = ImmutableList.of();
- Set<String> unknownConfigs = new LinkedHashSet<>();
- CommonCommandOptions commonOptions = optionsParser.getOptions(CommonCommandOptions.class);
- while (!commonOptions.configs.equals(configsLoaded)) {
- Set<String> missingConfigs = new LinkedHashSet<>(commonOptions.configs);
- missingConfigs.removeAll(configsLoaded);
- parseOptionsForCommand(
- rcfileNotes,
- commandAnnotation,
- optionsParser,
- optionsMap,
- missingConfigs,
- unknownConfigs);
- configsLoaded = commonOptions.configs;
- commonOptions = optionsParser.getOptions(CommonCommandOptions.class);
- }
- if (!unknownConfigs.isEmpty()) {
- if (commonOptions.allowUndefinedConfigs) {
- eventHandler.handle(
- Event.warn(
- "Config values are not defined in any .rc file: "
- + Joiner.on(", ").join(unknownConfigs)));
- } else {
- throw new OptionsParsingException(
- "Config values are not defined in any .rc file: "
- + Joiner.on(", ").join(unknownConfigs));
- }
- }
+ expandConfigOptions(eventHandler, commandToRcArgs);
}
/**
@@ -264,7 +287,7 @@ public class BlazeOptionHandler {
// BlazeCommand.editOptions, so the code needs to be safe regardless of the actual flag
// values. At the time of this writing, editOptions was only used as a convenience feature or
// to improve the user experience, but not required for safety or correctness.
- optionsPolicyEnforcer.enforce(optionsParser, commandName);
+ optionsPolicyEnforcer.enforce(optionsParser, commandAnnotation.name());
// Print warnings for odd options usage
for (String warning : optionsParser.getWarnings()) {
eventHandler.handle(Event.warn(warning));
@@ -277,92 +300,100 @@ public class BlazeOptionHandler {
}
/**
- * Parses the options from .rc files for a command invocation. It works in one of two modes;
- * either it loads the non-config options, or the config options that are specified in the {@code
- * configs} parameter.
- *
- * <p>This method adds every option pertaining to the specified command to the options parser. To
- * do that, it needs the command -> option mapping that is generated from the .rc files.
+ * Implements the legacy way of expanding blazerc and config expansions.
*
- * <p>It is not as trivial as simply taking the list of options for the specified command because
- * commands can inherit arguments from each other, and we have to respect that (e.g. if an option
- * is specified for 'build', it needs to take effect for the 'test' command, too).
+ * <p>--config options are expanded in a fixed point expansion at the end of the rc option block.
+ * Their expansion is defined in the rc file, and is triggered by the presence of a {@code
+ * --config=someConfigName} option somewhere in the user's options. The resulting option order is
+ * {@code <rc options> <config expansions> <command line options>}, where all of the config values
+ * mentioned are in the mid segment, regardless of whether they were defined in an rc file, on the
+ * command line, or in the expansion of other config values that were triggered earlier. If the
+ * same config value is provided twice (say, once on the command line and once in another config
+ * expansion) it will only be expanded once.
*
- * <p>Note that the order in which the options are parsed is well-defined: all options from the
- * same rc file are parsed at the same time, and the rc files are handled in the order in which
- * they were passed in from the client.
- *
- * @param rcfileNotes note message that would be printed during parsing
- * @param commandAnnotation the command for which options should be parsed.
- * @param optionsParser parser to receive parsed options.
- * @param optionsMap .rc files in structured format: a list of pairs, where the first part is the
- * name of the rc file, and the second part is a multimap of command name (plus config, if
- * present) to the list of options for that command
- * @param configs the configs for which to parse options; if {@code null}, non-config options are
- * parsed
- * @param unknownConfigs optional; a collection that the method will populate with the config
- * values in {@code configs} that none of the .rc files had entries for
- * @throws OptionsParsingException
+ * <p>This behavior is relatively well-defined, but the final order of options is not intuitive.
+ * As a simple example, consider a user specified command line with --config=foo last. Most users
+ * expect the expansion of --config=foo to override earlier flags in their command line if
+ * necessary, but this is not the case. This is why we are phasing out this behavior. We will
+ * maintain the old behavior to avoid breaking users during a transition period.
*/
- protected static void parseOptionsForCommand(
- List<String> rcfileNotes,
- Command commandAnnotation,
- OptionsParser optionsParser,
- List<Pair<String, ListMultimap<String, String>>> optionsMap,
- @Nullable Collection<String> configs,
- @Nullable Collection<String> unknownConfigs)
- throws OptionsParsingException {
- Set<String> knownConfigs = new HashSet<>();
- for (String commandToParse : getCommandNamesToParse(commandAnnotation)) {
- for (Pair<String, ListMultimap<String, String>> entry : optionsMap) {
- String rcFile = entry.first;
- List<String> allOptions = new ArrayList<>();
- if (configs == null) {
- Collection<String> values = entry.second.get(commandToParse);
- if (!values.isEmpty()) {
- allOptions.addAll(entry.second.get(commandToParse));
- String inherited = commandToParse.equals(commandAnnotation.name()) ? "" : "Inherited ";
- String source =
- rcFile.equals("client")
- ? "Options provided by the client"
- : String.format(
- "Reading rc options for '%s' from %s", commandAnnotation.name(), rcFile);
- rcfileNotes.add(
- String.format(
- "%s:\n %s'%s' options: %s",
- source, inherited, commandToParse, Joiner.on(' ').join(values)));
- }
+ public static final class FixPointConfigExpansionRcHandler extends BlazeOptionHandler {
+
+ private FixPointConfigExpansionRcHandler(
+ BlazeRuntime runtime,
+ BlazeWorkspace workspace,
+ BlazeCommand command,
+ Command commandAnnotation,
+ OptionsParser optionsParser,
+ InvocationPolicy invocationPolicy) {
+ super(runtime, workspace, command, commandAnnotation, optionsParser, invocationPolicy);
+ }
+
+ @Override
+ void expandConfigOptions(
+ EventHandler eventHandler, ListMultimap<String, RcChunkOfArgs> commandToRcArgs)
+ throws OptionsParsingException {
+ // Fix-point iteration until all configs are loaded.
+ List<String> configsLoaded = ImmutableList.of();
+ Set<String> unknownConfigs = new LinkedHashSet<>();
+ CommonCommandOptions commonOptions = optionsParser.getOptions(CommonCommandOptions.class);
+ while (!commonOptions.configs.equals(configsLoaded)) {
+ // Parse the configs we have not seen yet.
+ Set<String> missingConfigs = new LinkedHashSet<>(commonOptions.configs);
+ missingConfigs.removeAll(configsLoaded);
+ parseConfigOptionsForCommand(commandToRcArgs, missingConfigs, unknownConfigs);
+ // Refresh the list of config values we've processed to be the list of config values we had
+ // before the call to parseConfigOptionsForCommand.
+ configsLoaded = commonOptions.configs;
+ commonOptions = optionsParser.getOptions(CommonCommandOptions.class);
+ }
+ if (!unknownConfigs.isEmpty()) {
+ if (commonOptions.allowUndefinedConfigs) {
+ eventHandler.handle(
+ Event.warn(
+ "Config values are not defined in any .rc file: "
+ + Joiner.on(", ").join(unknownConfigs)));
} else {
- for (String config : configs) {
- String configDef = commandToParse + ":" + config;
- Collection<String> values = entry.second.get(configDef);
- if (!values.isEmpty()) {
- allOptions.addAll(values);
- knownConfigs.add(config);
- rcfileNotes.add(
- String.format(
- "Found applicable config definition %s in file %s: %s",
- configDef, rcFile, String.join(" ", values)));
- }
- }
+ throw new OptionsParsingException(
+ "Config values are not defined in any .rc file: "
+ + Joiner.on(", ").join(unknownConfigs));
}
- processOptionList(optionsParser, rcFile, allOptions);
}
}
- if (unknownConfigs != null && configs != null && configs.size() > knownConfigs.size()) {
- configs
- .stream()
- .filter(Predicates.not(Predicates.in(knownConfigs)))
- .forEachOrdered(unknownConfigs::add);
- }
- }
- // Processes the option list for an .rc file - command pair.
- private static void processOptionList(
- OptionsParser optionsParser, String rcfile, List<String> rcfileOptions)
- throws OptionsParsingException {
- if (!rcfileOptions.isEmpty()) {
- optionsParser.parse(PriorityCategory.RC_FILE, rcfile, rcfileOptions);
+ /**
+ * Go through the configs given and parse their expansion if a definition was found.
+ *
+ * @param configs the configs for which to parse options.
+ * @param unknownConfigs a collection that the method will populate with the config values in
+ * {@code configs} that none of the .rc files had entries for.
+ */
+ private void parseConfigOptionsForCommand(
+ ListMultimap<String, RcChunkOfArgs> commandToRcArgs,
+ Collection<String> configs,
+ Collection<String> unknownConfigs)
+ throws OptionsParsingException {
+ Set<String> knownConfigs = new HashSet<>();
+ for (String commandToParse : getCommandNamesToParse(commandAnnotation)) {
+ for (String config : configs) {
+ String configDef = commandToParse + ":" + config;
+ for (RcChunkOfArgs rcArgs : commandToRcArgs.get(configDef)) {
+ // Track that we've found at least 1 definition for this config value.
+ knownConfigs.add(config);
+ rcfileNotes.add(
+ String.format(
+ "Found applicable config definition %s in file %s: %s",
+ configDef, rcArgs.rcFile, String.join(" ", rcArgs.args)));
+ optionsParser.parse(PriorityCategory.RC_FILE, rcArgs.rcFile, rcArgs.args);
+ }
+ }
+ }
+ if (configs.size() > knownConfigs.size()) {
+ configs
+ .stream()
+ .filter(Predicates.not(Predicates.in(knownConfigs)))
+ .forEachOrdered(unknownConfigs::add);
+ }
}
}
@@ -396,28 +427,66 @@ public class BlazeOptionHandler {
}
/**
- * Convert a list of option override specifications to a more easily digestible form.
+ * We receive the rc file arguments from the client in an order that maintains the location of
+ * "import" statements, expanding the imported rc file in place so that its args override previous
+ * args in the file and are overridden by later arguments. We cannot group the args by rc file for
+ * parsing, as we would lose this ordering, so we store them in these "chunks."
*
- * @param overrides list of option override specifications
+ * <p>Each chunk comes from a single rc file, but the args stored here may not contain the entire
+ * file if its contents were interrupted by an import statement.
+ */
+ static class RcChunkOfArgs {
+ public RcChunkOfArgs(String rcFile, List<String> args) {
+ this.rcFile = rcFile;
+ this.args = args;
+ }
+
+ // The name of the rc file, usually a path.
+ String rcFile;
+ // The list of arguments specified in this rc "chunk". This is all for a single command (or
+ // command:config definition), as different commands will be grouped together, so this list of
+ // arguments can all be parsed as a continuous group.
+ List<String> args;
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof RcChunkOfArgs) {
+ RcChunkOfArgs other = (RcChunkOfArgs) o;
+ return rcFile.equals(other.rcFile) && args.equals(other.args);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return rcFile.hashCode() + args.hashCode();
+ }
+ }
+
+ /**
+ * The rc options are passed via {@link ClientOptions#optionsOverrides} and {@link
+ * ClientOptions#rcSource}, which is basically a line-by-line transfer of the rc files read by the
+ * client. This is not a particularly useful format for expanding the options, so this method
+ * structures the list so that it is easier to find the arguments that apply to a command, or to
+ * find the definitions of a config value.
*/
@VisibleForTesting
- static List<Pair<String, ListMultimap<String, String>>> getOptionsMap(
+ static ListMultimap<String, RcChunkOfArgs> structureRcOptionsAndConfigs(
EventHandler eventHandler,
List<String> rcFiles,
- List<ClientOptions.OptionOverride> overrides,
+ List<ClientOptions.OptionOverride> rawOverrides,
Set<String> validCommands) {
- List<Pair<String, ListMultimap<String, String>>> result = new ArrayList<>();
+ ListMultimap<String, RcChunkOfArgs> commandToRcArgs = ArrayListMultimap.create();
String lastRcFile = null;
- ListMultimap<String, String> lastMap = null;
- for (ClientOptions.OptionOverride override : overrides) {
+ ListMultimap<String, String> commandToArgMapForLastRc = null;
+ for (ClientOptions.OptionOverride override : rawOverrides) {
if (override.blazeRc < 0 || override.blazeRc >= rcFiles.size()) {
eventHandler.handle(
Event.warn("inconsistency in generated command line args. Ignoring bogus argument\n"));
continue;
}
String rcFile = rcFiles.get(override.blazeRc);
-
String command = override.command;
int index = command.indexOf(':');
if (index > 0) {
@@ -435,19 +504,31 @@ public class BlazeOptionHandler {
continue;
}
+ // We've moved on to another rc file "chunk," store the accumulated args from the last one.
if (!rcFile.equals(lastRcFile)) {
if (lastRcFile != null) {
- result.add(Pair.of(lastRcFile, lastMap));
+ // Go through the various commands identified in this rc file (or chunk of file) and
+ // store them grouped first by command, then by rc chunk.
+ for (String commandKey : commandToArgMapForLastRc.keySet()) {
+ commandToRcArgs.put(
+ commandKey,
+ new RcChunkOfArgs(lastRcFile, commandToArgMapForLastRc.get(commandKey)));
+ }
}
lastRcFile = rcFile;
- lastMap = ArrayListMultimap.create();
+ commandToArgMapForLastRc = ArrayListMultimap.create();
}
- lastMap.put(override.command, override.option);
+
+ commandToArgMapForLastRc.put(override.command, override.option);
}
if (lastRcFile != null) {
- result.add(Pair.of(lastRcFile, lastMap));
+ // Once again, for this last rc file chunk, store them grouped by command.
+ for (String commandKey : commandToArgMapForLastRc.keySet()) {
+ commandToRcArgs.put(
+ commandKey, new RcChunkOfArgs(lastRcFile, commandToArgMapForLastRc.get(commandKey)));
+ }
}
- return result;
+ return commandToRcArgs;
}
}
diff --git a/src/test/java/com/google/devtools/build/lib/runtime/BlazeOptionHandlerTest.java b/src/test/java/com/google/devtools/build/lib/runtime/BlazeOptionHandlerTest.java
new file mode 100644
index 0000000000..4c434d171d
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/runtime/BlazeOptionHandlerTest.java
@@ -0,0 +1,639 @@
+// Copyright 2017 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.runtime;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.ServerDirectories;
+import com.google.devtools.build.lib.bazel.rules.BazelRulesModule;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.runtime.BlazeOptionHandler.RcChunkOfArgs;
+import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy;
+import com.google.devtools.build.lib.testutil.Scratch;
+import com.google.devtools.build.lib.testutil.TestConstants;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsParsingException;
+import com.google.devtools.common.options.OptionsProvider;
+import com.google.devtools.common.options.TestOptions;
+import java.util.Arrays;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests {@link BlazeOptionHandler}.
+ *
+ * <p>Avoids testing anything that is controlled by the {@link BlazeCommandDispatcher}, for
+ * isolation. As a part of this, this test intentionally avoids testing how errors and informational
+ * messages are logged, to minimize dependence on the ui, and only checks for the existence of these
+ * messages.
+ */
+@RunWith(JUnit4.class)
+public class BlazeOptionHandlerTest {
+
+ private final Scratch scratch = new Scratch();
+ private final StoredEventHandler eventHandler = new StoredEventHandler();
+ private OptionsParser parser;
+ private BlazeRuntime runtime;
+ private BlazeOptionHandler optionHandler;
+
+ @Before
+ public void initStuff() throws Exception {
+ parser =
+ OptionsParser.newOptionsParser(
+ ImmutableList.of(TestOptions.class, CommonCommandOptions.class, ClientOptions.class));
+ parser.setAllowResidue(true);
+ String productName = TestConstants.PRODUCT_NAME;
+ ServerDirectories serverDirectories =
+ new ServerDirectories(scratch.dir("install_base"), scratch.dir("output_base"));
+ this.runtime =
+ new BlazeRuntime.Builder()
+ .setFileSystem(scratch.getFileSystem())
+ .setServerDirectories(serverDirectories)
+ .setProductName(productName)
+ .setStartupOptionsProvider(
+ OptionsParser.newOptionsParser(BlazeServerStartupOptions.class))
+ .addBlazeModule(new BazelRulesModule())
+ .build();
+ this.runtime.overrideCommands(ImmutableList.of(new C0Command()));
+
+ BlazeDirectories directories =
+ new BlazeDirectories(serverDirectories, scratch.dir("workspace"), productName);
+ runtime.initWorkspace(directories, /*binTools=*/ null);
+ optionHandler =
+ BlazeOptionHandler.getHandler(
+ runtime,
+ runtime.getWorkspace(),
+ new C0Command(),
+ C0Command.class.getAnnotation(Command.class),
+ parser,
+ InvocationPolicy.getDefaultInstance());
+ }
+
+ @Command(
+ name = "c0",
+ shortDescription = "c0 desc",
+ help = "c0 help",
+ options = {TestOptions.class}
+ )
+ private static class C0Command implements BlazeCommand {
+ @Override
+ public ExitCode exec(CommandEnvironment env, OptionsProvider options) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void editOptions(OptionsParser optionsParser) {}
+ }
+
+ private ListMultimap<String, RcChunkOfArgs> structuredArgsFrom2SimpleRcsWithOnlyResidue() {
+ ListMultimap<String, RcChunkOfArgs> structuredArgs = ArrayListMultimap.create();
+ // first add all lines of rc1, then rc2, to simulate a simple, import free, 2 rc file setup.
+ structuredArgs.put("c0", new RcChunkOfArgs("rc1", ImmutableList.of("a")));
+ structuredArgs.put("c0:config", new RcChunkOfArgs("rc1", ImmutableList.of("b")));
+
+ structuredArgs.put("common", new RcChunkOfArgs("rc2", ImmutableList.of("c")));
+ structuredArgs.put("c0", new RcChunkOfArgs("rc2", ImmutableList.of("d", "e")));
+ structuredArgs.put("c1:other", new RcChunkOfArgs("rc2", ImmutableList.of("f", "g")));
+ return structuredArgs;
+ }
+
+ private ListMultimap<String, RcChunkOfArgs> structuredArgsFrom2SimpleRcsWithFlags() {
+ ListMultimap<String, RcChunkOfArgs> structuredArgs = ArrayListMultimap.create();
+ structuredArgs.put(
+ "c0", new RcChunkOfArgs("rc1", ImmutableList.of("--test_multiple_string=foo")));
+ structuredArgs.put(
+ "c0:config", new RcChunkOfArgs("rc1", ImmutableList.of("--test_multiple_string=config")));
+
+ structuredArgs.put(
+ "common", new RcChunkOfArgs("rc2", ImmutableList.of("--test_multiple_string=common")));
+ structuredArgs.put(
+ "c0", new RcChunkOfArgs("rc2", ImmutableList.of("--test_multiple_string=bar")));
+ structuredArgs.put(
+ "c1:other", new RcChunkOfArgs("rc2", ImmutableList.of("--test_multiple_string=other")));
+ return structuredArgs;
+ }
+
+ private ListMultimap<String, RcChunkOfArgs> structuredArgsFromImportedRcsWithOnlyResidue() {
+ ListMultimap<String, RcChunkOfArgs> structuredArgs = ArrayListMultimap.create();
+ // first add all lines of rc1, then rc2, but then jump back to 1 as if rc2 was loaded in an
+ // import statement halfway through rc1.
+ structuredArgs.put("c0", new RcChunkOfArgs("rc1", ImmutableList.of("a")));
+ structuredArgs.put("c0:config", new RcChunkOfArgs("rc1", ImmutableList.of("b")));
+
+ structuredArgs.put("common", new RcChunkOfArgs("rc2", ImmutableList.of("c")));
+ structuredArgs.put("c0", new RcChunkOfArgs("rc2", ImmutableList.of("d", "e")));
+ structuredArgs.put("c1:other", new RcChunkOfArgs("rc2", ImmutableList.of("f", "g")));
+
+ structuredArgs.put("c0", new RcChunkOfArgs("rc1", ImmutableList.of("h")));
+ return structuredArgs;
+ }
+
+ @Test
+ public void testStructureRcOptionsAndConfigs_argumentless() throws Exception {
+ ListMultimap<String, RcChunkOfArgs> structuredRc =
+ BlazeOptionHandler.structureRcOptionsAndConfigs(
+ eventHandler,
+ Arrays.asList("rc1", "rc2"),
+ Arrays.asList(),
+ ImmutableSet.of("c0", "c1"));
+ assertThat(structuredRc).isEmpty();
+ assertThat(eventHandler.isEmpty()).isTrue();
+ }
+
+ @Test
+ public void testStructureRcOptionsAndConfigs_configOnly() throws Exception {
+ BlazeOptionHandler.structureRcOptionsAndConfigs(
+ eventHandler,
+ Arrays.asList("rc1", "rc2"),
+ Arrays.asList(new ClientOptions.OptionOverride(0, "c0:none", "a")),
+ ImmutableSet.of("c0"));
+ assertThat(eventHandler.isEmpty()).isTrue();
+ }
+
+ @Test
+ public void testStructureRcOptionsAndConfigs_invalidCommand() throws Exception {
+ BlazeOptionHandler.structureRcOptionsAndConfigs(
+ eventHandler,
+ Arrays.asList("rc1", "rc2"),
+ Arrays.asList(new ClientOptions.OptionOverride(0, "c1", "a")),
+ ImmutableSet.of("c0"));
+ assertThat(eventHandler.getEvents())
+ .contains(
+ Event.warn("while reading option defaults file 'rc1':\n invalid command name 'c1'."));
+ }
+
+ @Test
+ public void testStructureRcOptionsAndConfigs_twoRcs() throws Exception {
+ ListMultimap<String, RcChunkOfArgs> structuredRc =
+ BlazeOptionHandler.structureRcOptionsAndConfigs(
+ eventHandler,
+ Arrays.asList("rc1", "rc2"),
+ Arrays.asList(
+ new ClientOptions.OptionOverride(0, "c0", "a"),
+ new ClientOptions.OptionOverride(0, "c0:config", "b"),
+ new ClientOptions.OptionOverride(1, "common", "c"),
+ new ClientOptions.OptionOverride(1, "c0", "d"),
+ new ClientOptions.OptionOverride(1, "c0", "e"),
+ new ClientOptions.OptionOverride(1, "c1:other", "f"),
+ new ClientOptions.OptionOverride(1, "c1:other", "g")),
+ ImmutableSet.of("c0", "c1"));
+ assertThat(structuredRc).isEqualTo(structuredArgsFrom2SimpleRcsWithOnlyResidue());
+ assertThat(eventHandler.isEmpty()).isTrue();
+ }
+
+ @Test
+ public void testStructureRcOptionsAndConfigs_importedRcs() throws Exception {
+ ListMultimap<String, RcChunkOfArgs> structuredRc =
+ BlazeOptionHandler.structureRcOptionsAndConfigs(
+ eventHandler,
+ Arrays.asList("rc1", "rc2"),
+ Arrays.asList(
+ new ClientOptions.OptionOverride(0, "c0", "a"),
+ new ClientOptions.OptionOverride(0, "c0:config", "b"),
+ new ClientOptions.OptionOverride(1, "common", "c"),
+ new ClientOptions.OptionOverride(1, "c0", "d"),
+ new ClientOptions.OptionOverride(1, "c0", "e"),
+ new ClientOptions.OptionOverride(1, "c1:other", "f"),
+ new ClientOptions.OptionOverride(1, "c1:other", "g"),
+ new ClientOptions.OptionOverride(0, "c0", "h")),
+ ImmutableSet.of("c0", "c1"));
+ assertThat(structuredRc).isEqualTo(structuredArgsFromImportedRcsWithOnlyResidue());
+ assertThat(eventHandler.isEmpty()).isTrue();
+ }
+
+ @Test
+ public void testStructureRcOptionsAndConfigs_badOverrideIndex() throws Exception {
+ ListMultimap<String, RcChunkOfArgs> structuredRc =
+ BlazeOptionHandler.structureRcOptionsAndConfigs(
+ eventHandler,
+ Arrays.asList("rc1", "rc2"),
+ Arrays.asList(
+ new ClientOptions.OptionOverride(0, "c0", "a"),
+ new ClientOptions.OptionOverride(0, "c0:config", "b"),
+ new ClientOptions.OptionOverride(2, "c4:other", "z"),
+ new ClientOptions.OptionOverride(-1, "c3:other", "q"),
+ new ClientOptions.OptionOverride(1, "common", "c"),
+ new ClientOptions.OptionOverride(1, "c0", "d"),
+ new ClientOptions.OptionOverride(1, "c0", "e"),
+ new ClientOptions.OptionOverride(1, "c1:other", "f"),
+ new ClientOptions.OptionOverride(1, "c1:other", "g")),
+ ImmutableSet.of("c0", "c1"));
+ assertThat(structuredRc).isEqualTo(structuredArgsFrom2SimpleRcsWithOnlyResidue());
+ assertThat(eventHandler.getEvents())
+ .containsAllOf(
+ Event.warn("inconsistency in generated command line args. Ignoring bogus argument\n"),
+ Event.warn("inconsistency in generated command line args. Ignoring bogus argument\n"));
+ }
+
+ @Test
+ public void testParseRcOptions_empty() throws Exception {
+ optionHandler.parseRcOptions(eventHandler, ArrayListMultimap.create());
+ assertThat(eventHandler.getEvents()).isEmpty();
+ assertThat(parser.getResidue()).isEmpty();
+ }
+
+ @Test
+ public void testParseRcOptions_flatRcs_residue() throws Exception {
+ optionHandler.parseRcOptions(eventHandler, structuredArgsFrom2SimpleRcsWithOnlyResidue());
+ assertThat(eventHandler.getEvents()).isEmpty();
+ assertThat(parser.getResidue()).containsExactly("c", "a", "d", "e").inOrder();
+ }
+
+ @Test
+ public void testParseRcOptions_flatRcs_flags() throws Exception {
+ optionHandler.parseRcOptions(eventHandler, structuredArgsFrom2SimpleRcsWithFlags());
+ assertThat(eventHandler.getEvents()).isEmpty();
+ TestOptions options = parser.getOptions(TestOptions.class);
+ assertThat(options).isNotNull();
+ assertThat(options.testMultipleString).containsExactly("common", "foo", "bar").inOrder();
+ }
+
+ @Test
+ public void testParseRcOptions_importedRcs_residue() throws Exception {
+ optionHandler.parseRcOptions(eventHandler, structuredArgsFromImportedRcsWithOnlyResidue());
+ assertThat(eventHandler.getEvents()).isEmpty();
+ assertThat(parser.getResidue()).containsExactly("c", "a", "d", "e", "h").inOrder();
+ }
+
+ @Test
+ public void testExpandConfigOptions_configless() throws Exception {
+ optionHandler.expandConfigOptions(eventHandler, structuredArgsFrom2SimpleRcsWithOnlyResidue());
+ assertThat(parser.getResidue()).isEmpty();
+ }
+
+ @Test
+ public void testExpandConfigOptions_withConfig() throws Exception {
+ parser.parse("--config=config");
+ optionHandler.expandConfigOptions(eventHandler, structuredArgsFrom2SimpleRcsWithOnlyResidue());
+ assertThat(parser.getResidue()).containsExactly("b");
+ assertThat(optionHandler.getRcfileNotes())
+ .containsExactly("Found applicable config definition c0:config in file rc1: b");
+ }
+
+ @Test
+ public void testExpandConfigOptions_withConfigForUnapplicableCommand() throws Exception {
+ parser.parse("--config=other");
+ optionHandler.expandConfigOptions(eventHandler, structuredArgsFrom2SimpleRcsWithOnlyResidue());
+ assertThat(parser.getResidue()).isEmpty();
+ assertThat(optionHandler.getRcfileNotes()).isEmpty();
+ assertThat(eventHandler.getEvents())
+ .contains(Event.warn("Config values are not defined in any .rc file: other"));
+ }
+
+ @Test
+ public void testAllowUndefinedConfig() throws Exception {
+ parser.parse("--config=invalid", "--allow_undefined_configs");
+ optionHandler.expandConfigOptions(eventHandler, ArrayListMultimap.create());
+ assertThat(parser.getResidue()).isEmpty();
+ assertThat(optionHandler.getRcfileNotes()).isEmpty();
+ assertThat(eventHandler.getEvents())
+ .contains(Event.warn("Config values are not defined in any .rc file: invalid"));
+ }
+
+ @Test
+ public void testNoAllowUndefinedConfig() {
+ try {
+ parser.parse("--config=invalid", "--noallow_undefined_configs");
+ optionHandler.expandConfigOptions(eventHandler, ArrayListMultimap.create());
+ fail();
+ } catch (OptionsParsingException e) {
+ assertThat(e)
+ .hasMessageThat()
+ .contains("Config values are not defined in any .rc file: invalid");
+ }
+ }
+
+ @Test
+ public void testParseOptions_argless() {
+ optionHandler.parseOptions(ImmutableList.of("c0"), eventHandler);
+ assertThat(parser.getResidue()).isEmpty();
+ assertThat(optionHandler.getRcfileNotes()).isEmpty();
+ assertThat(eventHandler.isEmpty()).isTrue();
+ }
+
+ @Test
+ public void testParseOptions_residue() {
+ optionHandler.parseOptions(ImmutableList.of("c0", "res"), eventHandler);
+ assertThat(parser.getResidue()).contains("res");
+ assertThat(optionHandler.getRcfileNotes()).isEmpty();
+ assertThat(eventHandler.isEmpty()).isTrue();
+ }
+
+ @Test
+ public void testParseOptions_explicitOption() {
+ optionHandler.parseOptions(
+ ImmutableList.of("c0", "--test_multiple_string=explicit"), eventHandler);
+ assertThat(parser.getResidue()).isEmpty();
+ assertThat(optionHandler.getRcfileNotes()).isEmpty();
+ assertThat(eventHandler.isEmpty()).isTrue();
+ TestOptions options = parser.getOptions(TestOptions.class);
+ assertThat(options).isNotNull();
+ assertThat(options.testMultipleString).containsExactly("explicit");
+ }
+
+ @Test
+ public void testParseOptions_rcOption() {
+ optionHandler.parseOptions(
+ ImmutableList.of(
+ "c0",
+ "--default_override=0:c0=--test_multiple_string=rc_a",
+ "--default_override=0:c0=--test_multiple_string=rc_b",
+ "--rc_source=/somewhere/.blazerc"),
+ eventHandler);
+ assertThat(parser.getResidue()).isEmpty();
+ // Check that multiple options in the same rc chunk are collapsed into 1 announce_rc entry.
+ assertThat(optionHandler.getRcfileNotes())
+ .containsExactly(
+ "Reading rc options for 'c0' from /somewhere/.blazerc:\n"
+ + " 'c0' options: --test_multiple_string=rc_a --test_multiple_string=rc_b");
+ assertThat(eventHandler.getEvents()).isEmpty();
+ TestOptions options = parser.getOptions(TestOptions.class);
+ assertThat(options).isNotNull();
+ assertThat(options.testMultipleString).containsExactly("rc_a", "rc_b");
+ }
+
+ @Test
+ public void testParseOptions_multipleRcs() {
+ optionHandler.parseOptions(
+ ImmutableList.of(
+ "c0",
+ "--default_override=0:c0=--test_multiple_string=rc1_a",
+ "--default_override=1:c0=--test_multiple_string=rc2",
+ "--default_override=0:c0=--test_multiple_string=rc1_b",
+ "--rc_source=/somewhere/.blazerc",
+ "--rc_source=/some/other/.blazerc"),
+ eventHandler);
+ assertThat(parser.getResidue()).isEmpty();
+ assertThat(optionHandler.getRcfileNotes())
+ .containsExactly(
+ "Reading rc options for 'c0' from /somewhere/.blazerc:\n"
+ + " 'c0' options: --test_multiple_string=rc1_a",
+ "Reading rc options for 'c0' from /some/other/.blazerc:\n"
+ + " 'c0' options: --test_multiple_string=rc2",
+ "Reading rc options for 'c0' from /somewhere/.blazerc:\n"
+ + " 'c0' options: --test_multiple_string=rc1_b");
+ assertThat(eventHandler.getEvents()).isEmpty();
+ TestOptions options = parser.getOptions(TestOptions.class);
+ assertThat(options).isNotNull();
+ assertThat(options.testMultipleString).containsExactly("rc1_a", "rc2", "rc1_b").inOrder();
+ }
+
+ @Test
+ public void testParseOptions_multipleRcsWithMultipleCommands() {
+ optionHandler.parseOptions(
+ ImmutableList.of(
+ "c0",
+ "--default_override=0:c0=--test_multiple_string=rc1_a",
+ "--default_override=1:c0=--test_multiple_string=rc2",
+ "--default_override=1:common=--test_multiple_string=rc2_common",
+ "--default_override=0:c0=--test_multiple_string=rc1_b",
+ "--default_override=0:common=--test_multiple_string=rc1_common",
+ "--rc_source=/somewhere/.blazerc",
+ "--rc_source=/some/other/.blazerc"),
+ eventHandler);
+ assertThat(parser.getResidue()).isEmpty();
+ assertThat(optionHandler.getRcfileNotes())
+ .containsExactly(
+ "Reading rc options for 'c0' from /some/other/.blazerc:\n"
+ + " Inherited 'common' options: --test_multiple_string=rc2_common",
+ "Reading rc options for 'c0' from /somewhere/.blazerc:\n"
+ + " Inherited 'common' options: --test_multiple_string=rc1_common",
+ "Reading rc options for 'c0' from /somewhere/.blazerc:\n"
+ + " 'c0' options: --test_multiple_string=rc1_a",
+ "Reading rc options for 'c0' from /some/other/.blazerc:\n"
+ + " 'c0' options: --test_multiple_string=rc2",
+ "Reading rc options for 'c0' from /somewhere/.blazerc:\n"
+ + " 'c0' options: --test_multiple_string=rc1_b");
+ assertThat(eventHandler.getEvents()).isEmpty();
+ TestOptions options = parser.getOptions(TestOptions.class);
+ assertThat(options).isNotNull();
+ assertThat(options.testMultipleString)
+ .containsExactly("rc2_common", "rc1_common", "rc1_a", "rc2", "rc1_b")
+ .inOrder();
+ }
+
+ @Test
+ public void testParseOptions_rcOptionAndExplicit() {
+ optionHandler.parseOptions(
+ ImmutableList.of(
+ "c0",
+ "--default_override=0:c0=--test_multiple_string=rc",
+ "--rc_source=/somewhere/.blazerc",
+ "--test_multiple_string=explicit"),
+ eventHandler);
+ assertThat(parser.getResidue()).isEmpty();
+ assertThat(optionHandler.getRcfileNotes())
+ .containsExactly(
+ "Reading rc options for 'c0' from /somewhere/.blazerc:\n"
+ + " 'c0' options: --test_multiple_string=rc");
+ assertThat(eventHandler.getEvents()).isEmpty();
+ TestOptions options = parser.getOptions(TestOptions.class);
+ assertThat(options).isNotNull();
+ assertThat(options.testMultipleString).containsExactly("rc", "explicit").inOrder();
+ }
+
+ @Test
+ public void testParseOptions_multiCommandRcOptionAndExplicit() {
+ optionHandler.parseOptions(
+ ImmutableList.of(
+ "c0",
+ "--default_override=0:c0=--test_multiple_string=rc_c0_1",
+ "--default_override=0:common=--test_multiple_string=rc_common",
+ "--default_override=0:c0=--test_multiple_string=rc_c0_2",
+ "--rc_source=/somewhere/.blazerc",
+ "--test_multiple_string=explicit"),
+ eventHandler);
+ assertThat(parser.getResidue()).isEmpty();
+ assertThat(optionHandler.getRcfileNotes())
+ .containsExactly(
+ "Reading rc options for 'c0' from /somewhere/.blazerc:\n"
+ + " Inherited 'common' options: --test_multiple_string=rc_common",
+ "Reading rc options for 'c0' from /somewhere/.blazerc:\n"
+ + " 'c0' options: --test_multiple_string=rc_c0_1 --test_multiple_string=rc_c0_2");
+ assertThat(eventHandler.getEvents()).isEmpty();
+ TestOptions options = parser.getOptions(TestOptions.class);
+ assertThat(options).isNotNull();
+ assertThat(options.testMultipleString)
+ .containsExactly("rc_common", "rc_c0_1", "rc_c0_2", "explicit")
+ .inOrder();
+ }
+
+ @Test
+ public void testParseOptions_multipleRcsWithMultipleCommandsPlusExplicitOption() {
+ optionHandler.parseOptions(
+ ImmutableList.of(
+ "c0",
+ "--default_override=0:c0=--test_multiple_string=rc1_a",
+ "--default_override=1:c0=--test_multiple_string=rc2",
+ "--test_multiple_string=explicit",
+ "--default_override=1:common=--test_multiple_string=rc2_common",
+ "--default_override=0:c0=--test_multiple_string=rc1_b",
+ "--default_override=0:common=--test_multiple_string=rc1_common",
+ "--rc_source=/somewhere/.blazerc",
+ "--rc_source=/some/other/.blazerc"),
+ eventHandler);
+ assertThat(parser.getResidue()).isEmpty();
+ assertThat(optionHandler.getRcfileNotes())
+ .containsExactly(
+ "Reading rc options for 'c0' from /some/other/.blazerc:\n"
+ + " Inherited 'common' options: --test_multiple_string=rc2_common",
+ "Reading rc options for 'c0' from /somewhere/.blazerc:\n"
+ + " Inherited 'common' options: --test_multiple_string=rc1_common",
+ "Reading rc options for 'c0' from /somewhere/.blazerc:\n"
+ + " 'c0' options: --test_multiple_string=rc1_a",
+ "Reading rc options for 'c0' from /some/other/.blazerc:\n"
+ + " 'c0' options: --test_multiple_string=rc2",
+ "Reading rc options for 'c0' from /somewhere/.blazerc:\n"
+ + " 'c0' options: --test_multiple_string=rc1_b");
+ assertThat(eventHandler.getEvents()).isEmpty();
+ TestOptions options = parser.getOptions(TestOptions.class);
+ assertThat(options).isNotNull();
+ assertThat(options.testMultipleString)
+ .containsExactly("rc2_common", "rc1_common", "rc1_a", "rc2", "rc1_b", "explicit")
+ .inOrder();
+ }
+
+ @Test
+ public void testParseOptions_explicitConfig() {
+ optionHandler.parseOptions(
+ ImmutableList.of(
+ "c0",
+ "--default_override=0:c0=--test_multiple_string=rc",
+ "--default_override=0:c0:conf=--test_multiple_string=config",
+ "--rc_source=/somewhere/.blazerc",
+ "--test_multiple_string=explicit",
+ "--config=conf"),
+ eventHandler);
+ assertThat(parser.getResidue()).isEmpty();
+ assertThat(optionHandler.getRcfileNotes())
+ .containsExactly(
+ "Reading rc options for 'c0' from /somewhere/.blazerc:\n"
+ + " 'c0' options: --test_multiple_string=rc",
+ "Found applicable config definition c0:conf in file /somewhere/.blazerc: "
+ + "--test_multiple_string=config");
+ assertThat(eventHandler.getEvents()).isEmpty();
+ TestOptions options = parser.getOptions(TestOptions.class);
+ assertThat(options).isNotNull();
+ // "config" is lower priority (occurs earlier in the list) than "explicit" in the fix-point
+ // expansion, despite --config=conf occurring later.
+ assertThat(options.testMultipleString).containsExactly("rc", "config", "explicit").inOrder();
+ }
+
+ @Test
+ public void testParseOptions_rcSpecifiedConfig() {
+ optionHandler.parseOptions(
+ ImmutableList.of(
+ "c0",
+ "--default_override=0:c0=--config=conf",
+ "--default_override=0:c0=--test_multiple_string=rc",
+ "--default_override=0:c0:conf=--test_multiple_string=config",
+ "--rc_source=/somewhere/.blazerc",
+ "--test_multiple_string=explicit"),
+ eventHandler);
+ assertThat(parser.getResidue()).isEmpty();
+ assertThat(optionHandler.getRcfileNotes())
+ .containsExactly(
+ "Reading rc options for 'c0' from /somewhere/.blazerc:\n"
+ + " 'c0' options: --config=conf --test_multiple_string=rc",
+ "Found applicable config definition c0:conf in file /somewhere/.blazerc: "
+ + "--test_multiple_string=config");
+ assertThat(eventHandler.getEvents()).isEmpty();
+ TestOptions options = parser.getOptions(TestOptions.class);
+ assertThat(options).isNotNull();
+ // "config" is higher priority (occurs later in the list) than "rc" in the fix-point
+ // expansion, despite --config=conf occurring before the explicit mention of "rc".
+ assertThat(options.testMultipleString).containsExactly("rc", "config", "explicit").inOrder();
+ }
+
+ @Test
+ public void testParseOptions_recursiveConfig() {
+ optionHandler.parseOptions(
+ ImmutableList.of(
+ "c0",
+ "--default_override=0:c0=--config=conf",
+ "--default_override=0:c0=--test_multiple_string=rc",
+ "--default_override=0:c0:other=--test_multiple_string=other",
+ "--default_override=0:c0:conf=--test_multiple_string=config1",
+ "--default_override=0:c0:conf=--config=other",
+ "--default_override=0:common:other=--test_multiple_string=othercommon",
+ "--rc_source=/somewhere/.blazerc",
+ "--test_multiple_string=explicit"),
+ eventHandler);
+ assertThat(parser.getResidue()).isEmpty();
+ assertThat(optionHandler.getRcfileNotes())
+ .containsExactly(
+ "Reading rc options for 'c0' from /somewhere/.blazerc:\n"
+ + " 'c0' options: --config=conf --test_multiple_string=rc",
+ "Found applicable config definition c0:conf in file /somewhere/.blazerc: "
+ + "--test_multiple_string=config1 --config=other",
+ "Found applicable config definition common:other in file /somewhere/.blazerc: "
+ + "--test_multiple_string=othercommon",
+ "Found applicable config definition c0:other in file /somewhere/.blazerc: "
+ + "--test_multiple_string=other");
+ assertThat(eventHandler.getEvents()).isEmpty();
+ TestOptions options = parser.getOptions(TestOptions.class);
+ assertThat(options).isNotNull();
+ // The 2nd config, --config=other, is expanded after the config that added it.
+ assertThat(options.testMultipleString)
+ .containsExactly("rc", "config1", "othercommon", "other", "explicit")
+ .inOrder();
+ }
+
+ @Test
+ public void testParseOptions_recursiveConfigWasAlreadyPresent() {
+ optionHandler.parseOptions(
+ ImmutableList.of(
+ "c0",
+ "--default_override=0:c0=--config=conf",
+ "--default_override=0:c0=--config=other",
+ "--default_override=0:c0=--test_multiple_string=rc",
+ "--default_override=0:c0:other=--test_multiple_string=other",
+ "--default_override=0:c0:conf=--test_multiple_string=config1",
+ "--default_override=0:c0:conf=--config=other",
+ "--default_override=0:common:other=--test_multiple_string=othercommon",
+ "--rc_source=/somewhere/.blazerc",
+ "--test_multiple_string=explicit"),
+ eventHandler);
+ assertThat(parser.getResidue()).isEmpty();
+ assertThat(optionHandler.getRcfileNotes())
+ .containsExactly(
+ "Reading rc options for 'c0' from /somewhere/.blazerc:\n"
+ + " 'c0' options: --config=conf --config=other --test_multiple_string=rc",
+ "Found applicable config definition common:other in file /somewhere/.blazerc: "
+ + "--test_multiple_string=othercommon",
+ "Found applicable config definition c0:conf in file /somewhere/.blazerc: "
+ + "--test_multiple_string=config1 --config=other",
+ "Found applicable config definition c0:other in file /somewhere/.blazerc: "
+ + "--test_multiple_string=other");
+ assertThat(eventHandler.getEvents()).isEmpty();
+ TestOptions options = parser.getOptions(TestOptions.class);
+ assertThat(options).isNotNull();
+ // The 2nd config, --config=other, is expanded at the same time as --config=conf, since they are
+ // both initially present. The "common" definition is therefore first. other is not reexpanded
+ // when it is added by --config=conf, since it was already included.
+ assertThat(options.testMultipleString)
+ .containsExactly("rc", "othercommon", "config1", "other", "explicit")
+ .inOrder();
+ }
+}