aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-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();
+ }
+}