diff options
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(); + } +} |