// Copyright 2014 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.exec;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.ActionExecutionContext.ShowSubcommands;
import com.google.devtools.build.lib.actions.LocalHostCapacity;
import com.google.devtools.build.lib.actions.ResourceSet;
import com.google.devtools.build.lib.analysis.config.CoreOptionConverters.LabelConverter;
import com.google.devtools.build.lib.analysis.config.PerLabelOptions;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.util.OptionsUtils;
import com.google.devtools.build.lib.util.RegexFilter;
import com.google.devtools.build.lib.util.ResourceConverter;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.common.options.BoolOrEnumConverter;
import com.google.devtools.common.options.BooleanStyleOption;
import com.google.devtools.common.options.Converter;
import com.google.devtools.common.options.Converters;
import com.google.devtools.common.options.Converters.AssignmentToListOfValuesConverter;
import com.google.devtools.common.options.Converters.CommaSeparatedNonEmptyOptionListConverter;
import com.google.devtools.common.options.EnumConverter;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionDocumentationCategory;
import com.google.devtools.common.options.OptionEffectTag;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsClass;
import com.google.devtools.common.options.OptionsParsingException;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.annotation.Nullable;

/**
 * Options affecting the execution phase of a build.
 *
 * <p>These options are interpreted by the BuildTool to choose an Executor to be used for the build.
 *
 * <p>Note: from the user's point of view, the characteristic function of this set of options is
 * indistinguishable from that of the BuildRequestOptions: they are all per-request. The difference
 * is only apparent in the implementation: these options are used only by the lib.exec machinery,
 * which affects how C++ and Java compilation occur. (The BuildRequestOptions contain a mixture of
 * "semantic" options affecting the choice of targets to build, and "non-semantic" options affecting
 * the lib.actions machinery.) Ideally, the user would be unaware of the difference. For now, the
 * usage strings are identical modulo "part 1", "part 2".
 */
@OptionsClass
public abstract class ExecutionOptions extends OptionsBase {

  @Option(
      name = "spawn_strategy",
      defaultValue = "",
      converter = CommaSeparatedNonEmptyOptionListConverter.class,
      documentationCategory = OptionDocumentationCategory.EXECUTION_STRATEGY,
      effectTags = {OptionEffectTag.EXECUTION},
      help =
          "Specify how spawn actions are executed by default. Accepts a comma-separated list of"
              + " strategies from highest to lowest priority. For each action Bazel picks the"
              + " strategy with the highest priority that can execute the action. The default"
              + " value is \"remote,worker,sandboxed,local\". See"
              + " https://blog.bazel.build/2019/06/19/list-strategy.html for details.")
  public abstract List<String> getSpawnStrategy();

  @Option(
      name = "genrule_strategy",
      defaultValue = "",
      converter = CommaSeparatedNonEmptyOptionListConverter.class,
      documentationCategory = OptionDocumentationCategory.EXECUTION_STRATEGY,
      effectTags = {OptionEffectTag.EXECUTION},
      help =
          "Specify how to execute genrules. This flag will be phased out. Instead, use "
              + "--spawn_strategy=<value> to control all actions or --strategy=Genrule=<value> "
              + "to control genrules only.")
  public abstract List<String> getGenruleStrategy();

  @Option(
      name = "strategy",
      allowMultiple = true,
      converter = Converters.StringToStringListConverter.class,
      defaultValue = "null",
      documentationCategory = OptionDocumentationCategory.EXECUTION_STRATEGY,
      effectTags = {OptionEffectTag.EXECUTION},
      help =
          "Specify how to distribute compilation of other spawn actions. Accepts a comma-separated"
              + " list of strategies from highest to lowest priority. For each action Bazel picks"
              + " the strategy with the highest priority that can execute the action. The default"
              + " value is \"remote,worker,sandboxed,local\". This flag overrides the values set"
              + " by --spawn_strategy (and --genrule_strategy if used with mnemonic Genrule). See"
              + " https://blog.bazel.build/2019/06/19/list-strategy.html for details.")
  public abstract List<Map.Entry<String, List<String>>> getStrategy();

  @Option(
      name = "strategy_regexp",
      allowMultiple = true,
      converter = RegexFilterAssignmentConverter.class,
      documentationCategory = OptionDocumentationCategory.EXECUTION_STRATEGY,
      effectTags = {OptionEffectTag.EXECUTION},
      defaultValue = "null",
      help =
          "Override which spawn strategy should be used to execute spawn actions that have "
              + "descriptions matching a certain regex_filter. See --per_file_copt for details on "
              + "regex_filter matching. "
              + "The last regex_filter that matches the description is used. "
              + "This option overrides other flags for specifying strategy. "
              + "Example: --strategy_regexp=//foo.*\\.cc,-//foo/bar=local means to run actions "
              + "using local strategy if their descriptions match //foo.*.cc but not //foo/bar. "
              + "Example: --strategy_regexp='Compiling.*/bar=local "
              + " --strategy_regexp=Compiling=sandboxed will run 'Compiling //foo/bar/baz' with "
              + "the 'local' strategy, but reversing the order would run it with 'sandboxed'. ")
  public abstract List<Map.Entry<RegexFilter, List<String>>> getStrategyByRegexp();

  @Option(
      name = "allowed_strategies_by_exec_platform",
      allowMultiple = true,
      converter = LabelToStringListConverter.class,
      defaultValue = "null",
      documentationCategory = OptionDocumentationCategory.EXECUTION_STRATEGY,
      effectTags = {OptionEffectTag.EXECUTION},
      help =
          """
          Filters spawn strategies by the execution platform without affecting order.
          For example:
          ```
          common --spawn_strategy=remote,sandboxed,worker,local
          common --strategy=Genrule=local
          common --allowed_strategies_by_exec_platform=@platforms//host:host=local,sandboxed,worker
          common --allowed_strategies_by_exec_platform=//:linux_amd64=remote
          ```
          With the above options;
          - Actions configured for the host platform will be given `remote,sandboxed,worker`.
          - Actions configured for the `//:linux_amd64` platform will be given `remote`.
          - Actions configured for the `//:linux_amd64` platform with mnemonic `Genrule` will be
            given no strategies and fail to spawn.
          """)
  public abstract List<Map.Entry<Label, List<String>>> getAllowedStrategiesByExecPlatform();

  @Option(
      name = "materialize_param_files",
      defaultValue = "false",
      documentationCategory = OptionDocumentationCategory.LOGGING,
      effectTags = {OptionEffectTag.EXECUTION},
      help =
          "Writes intermediate parameter files to output tree even when using remote action "
              + "execution or caching. Useful when debugging actions. This is implied by "
              + "--subcommands and --verbose_failures.")
  public abstract boolean getMaterializeParamFiles();

  @Option(
      name = "experimental_materialize_param_files_directly",
      defaultValue = "false",
      documentationCategory = OptionDocumentationCategory.LOGGING,
      effectTags = {OptionEffectTag.EXECUTION},
      help = "If materializing param files, do so with direct writes to disk.")
  public abstract boolean getMaterializeParamFilesDirectly();

  public final boolean shouldMaterializeParamFiles() {
    // Implied by --subcommands and --verbose_failures
    return getMaterializeParamFiles()
        || getShowSubcommands() != ShowSubcommands.FALSE
        || getVerboseFailures();
  }

  @Option(
      name = "verbose_failures",
      defaultValue = "false",
      documentationCategory = OptionDocumentationCategory.LOGGING,
      effectTags = {OptionEffectTag.TERMINAL_OUTPUT},
      help = "If a command fails, print out the full command line.")
  public abstract boolean getVerboseFailures();

  @Option(
      name = "subcommands",
      abbrev = 's',
      defaultValue = "false",
      converter = ShowSubcommandsConverter.class,
      documentationCategory = OptionDocumentationCategory.LOGGING,
      effectTags = {OptionEffectTag.TERMINAL_OUTPUT},
      help =
          "Display the subcommands executed during a build. Related flags:"
              + " --execution_log_json_file, --execution_log_binary_file (for logging subcommands"
              + " to a file in a tool-friendly format).")
  public abstract ShowSubcommands getShowSubcommands();

  @Option(
      name = "check_up_to_date",
      defaultValue = "false",
      documentationCategory = OptionDocumentationCategory.EXECUTION_STRATEGY,
      effectTags = {OptionEffectTag.EXECUTION},
      help =
          "Don't perform the build, just check if it is up-to-date.  If all targets are "
              + "up-to-date, the build completes successfully.  If any step needs to be executed "
              + "an error is reported and the build fails.")
  public abstract boolean getCheckUpToDate();

  @Option(
      name = "check_tests_up_to_date",
      defaultValue = "false",
      implicitRequirements = {"--check_up_to_date"},
      documentationCategory = OptionDocumentationCategory.TESTING,
      effectTags = {OptionEffectTag.EXECUTION},
      help =
          "Don't run tests, just check if they are up-to-date.  If all tests results are "
              + "up-to-date, the testing completes successfully.  If any test needs to be built or "
              + "executed, an error is reported and the testing fails.  This option implies "
              + "--check_up_to_date behavior.")
  public abstract boolean getTestCheckUpToDate();

  @Option(
      name = "test_strategy",
      defaultValue = "",
      documentationCategory = OptionDocumentationCategory.TESTING,
      effectTags = {OptionEffectTag.EXECUTION},
      help = "Specifies which strategy to use when running tests.")
  public abstract String getTestStrategy();

  @Option(
      name = "test_keep_going",
      defaultValue = "true",
      documentationCategory = OptionDocumentationCategory.TESTING,
      effectTags = {OptionEffectTag.EXECUTION},
      help =
          "When disabled, any non-passing test will cause the entire build to stop. By default "
              + "all tests are run, even if some do not pass.")
  public abstract boolean getTestKeepGoing();

  @Option(
      name = "flaky_test_attempts",
      allowMultiple = true,
      defaultValue = "default",
      converter = TestAttemptsConverter.class,
      documentationCategory = OptionDocumentationCategory.TESTING,
      effectTags = {OptionEffectTag.EXECUTION},
      help =
          "Each test will be retried up to the specified number of times in case of any test"
              + " failure. Tests that required more than one attempt to pass are marked as 'FLAKY'"
              + " in the test summary. Normally the value specified is just an integer or the"
              + " string 'default'. If an integer, then all tests will be run up to N times. If"
              + " 'default', then only a single test attempt will be made for regular tests and"
              + " three for tests marked explicitly as flaky by their rule (flaky=1 attribute)."
              + " Alternate syntax: regex_filter@flaky_test_attempts. Where flaky_test_attempts is"
              + " as above and regex_filter stands for a list of include and exclude regular"
              + " expression patterns (Also see --runs_per_test). Example:"
              + " --flaky_test_attempts=//foo/.*,-//foo/bar/.*@3 deflakes all tests in //foo/"
              + " except those under foo/bar three times. This option can be passed multiple"
              + " times. The most recently passed argument that matches takes precedence. If"
              + " nothing matches, behavior is as if 'default' above.")
  public abstract List<PerLabelOptions> getTestAttempts();

  @Option(
      name = "test_tmpdir",
      defaultValue = "null",
      converter = OptionsUtils.PathFragmentConverter.class,
      documentationCategory = OptionDocumentationCategory.TESTING,
      effectTags = {OptionEffectTag.UNKNOWN},
      help = "Specifies the base temporary directory for 'bazel test' to use.")
  public abstract PathFragment getTestTmpDir();

  @Option(
      name = "test_output",
      defaultValue = "summary",
      converter = TestOutputFormat.Converter.class,
      documentationCategory = OptionDocumentationCategory.LOGGING,
      effectTags = {
        OptionEffectTag.TEST_RUNNER,
        OptionEffectTag.TERMINAL_OUTPUT,
        OptionEffectTag.EXECUTION
      },
      help =
          """
          Specifies desired output mode. Not to be confused with `--test_summary` which controls
          the test summary printed on command completion.

          Valid values are;
          - `summary` (default) to print summaries for failed tests,
          - `errors` to also print test logs for failed tests,
          - `all` to print summaries and logs for all tests and
          - `streamed` to output logs for all tests in real time (this will force tests to be
            executed locally one at a time regardless of `--test_strategy` value).
          """)
  public abstract TestOutputFormat getTestOutput();

  public abstract void setTestOutput(TestOutputFormat value);

  @Option(
      name = "max_test_output_bytes",
      defaultValue = "-1",
      documentationCategory = OptionDocumentationCategory.LOGGING,
      effectTags = {
        OptionEffectTag.TEST_RUNNER,
        OptionEffectTag.TERMINAL_OUTPUT,
        OptionEffectTag.EXECUTION
      },
      help =
          "Specifies maximum per-test-log size that can be emitted when --test_output is 'errors' "
              + "or 'all'. Useful for avoiding overwhelming the output with excessively noisy test "
              + "output. The test header is included in the log size. Negative values imply no "
              + "limit. Output is all or nothing.")
  public abstract int getMaxTestOutputBytes();

  @Option(
      name = "test_summary",
      defaultValue = "short",
      converter = TestSummaryFormat.Converter.class,
      documentationCategory = OptionDocumentationCategory.LOGGING,
      effectTags = {OptionEffectTag.TERMINAL_OUTPUT},
      help =
          """
          Specifies the desired format of the test summary. Valid values are;
          - `short` to list all tests that ran to completion.
          - `short_uncached` to list tests that ran to completion, omitting cached tests.
          - `terse` to list only failed and flaky tests.
          - `detailed` to list tests that ran to completion and their test cases.
          - `detailed_uncached` to list tests that ran to completion and their test cases,
            omitting cached tests.
          - `testcase` to print summary in test case resolution without detailed information about
            failed test cases.
          - `none` to omit the summary.
          """)
  public abstract TestSummaryFormat getTestSummary();

  public abstract void setTestSummary(TestSummaryFormat value);

  @Option(
      name = "local_resources",
      defaultValue = "null",
      documentationCategory = OptionDocumentationCategory.BUILD_TIME_OPTIMIZATION,
      effectTags = {OptionEffectTag.HOST_MACHINE_RESOURCE_OPTIMIZATIONS},
      allowMultiple = true,
      help =
          "Set the number of resources available to Bazel. "
              + "Takes in an assignment to a float or "
              + ResourceConverter.HOST_RAM_KEYWORD
              + "/"
              + ResourceConverter.HOST_CPUS_KEYWORD
              + ", optionally "
              + "followed by [-|*]<float> (eg. memory="
              + ResourceConverter.HOST_RAM_KEYWORD
              + "*.5 to use half the available RAM). "
              + "Can be used multiple times to specify multiple "
              + "types of resources. Bazel will limit concurrently running actions "
              + "based on the available resources and the resources required. "
              + "Tests can declare the amount of resources they need "
              + "by using a tag of the \"resources:<resource name>:<amount>\" format. ",
      converter = ResourceConverter.AssignmentConverter.class)
  public abstract List<Map.Entry<String, Double>> getLocalResourcesFields();

  public final ImmutableMap<String, Double> getLocalResources() {
    ImmutableMap.Builder<String, Double> resources = ImmutableMap.builder();
    return resources
        .put(ResourceSet.CPU, LocalHostCapacity.getLocalHostCapacity().getCpuUsage())
        .put(ResourceSet.MEMORY, .67 * LocalHostCapacity.getLocalHostCapacity().getMemoryMb())
        .putAll(getLocalResourcesFields())
        .buildKeepingLast();
  }

  @Option(
      name = "experimental_cpu_load_scheduling",
      defaultValue = "false",
      documentationCategory = OptionDocumentationCategory.BUILD_TIME_OPTIMIZATION,
      effectTags = {OptionEffectTag.EXECUTION},
      help =
          "Enables the experimental local execution scheduling based on CPU load, not estimation of"
              + " actions one by one.  Experimental scheduling have showed the large benefit on a"
              + " large local builds on a powerful machines with the large number of cores."
              + " Recommended to use with --local_resources=cpu=HOST_CPUS")
  public abstract boolean getExperimentalCpuLoadScheduling();

  @Option(
      name = "experimental_cpu_load_scheduling_window_size",
      defaultValue = "5000ms",
      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
      effectTags = {OptionEffectTag.EXECUTION},
      help =
          "The size of window during experimental scheduling of action based on CPU load. Make"
              + " sense to define only when flag --experimental_cpu_load_scheduling is enabled.")
  public abstract Duration getExperimentalCpuLoadSchedulingWindowSize();

  @Option(
      name = "local_test_jobs",
      defaultValue = "auto",
      documentationCategory = OptionDocumentationCategory.TESTING,
      effectTags = {OptionEffectTag.EXECUTION},
      help =
          "The max number of local test jobs to run concurrently. "
              + "Takes "
              + ResourceConverter.FLAG_SYNTAX
              + ". 0 means local resources will limit the number of local test jobs to run "
              + "concurrently instead. Setting this greater than the value for --jobs "
              + "is ineffectual.",
      converter = LocalTestJobsConverter.class)
  public abstract int getLocalTestJobs();

  public final boolean usingLocalTestJobs() {
    return getLocalTestJobs() != 0;
  }

  @Option(
      name = "cache_computed_file_digests",
      defaultValue = "50000",
      documentationCategory = OptionDocumentationCategory.BUILD_TIME_OPTIMIZATION,
      effectTags = {OptionEffectTag.UNKNOWN},
      help =
          "If greater than 0, configures Bazel to cache file digests in memory based on their "
              + "metadata instead of recomputing the digests from disk every time they are needed. "
              + "Setting this to 0 ensures correctness because not all file changes can be noted "
              + "from file metadata. When not 0, the number indicates the size of the cache as the "
              + "number of file digests to be cached.")
  public abstract long getCacheSizeForComputedFileDigests();

  @Option(
      name = "experimental_enable_critical_path_profiling",
      defaultValue = "true",
      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
      effectTags = {OptionEffectTag.UNKNOWN},
      help =
          "If set (the default), critical path profiling is enabled for the execution phase. This"
              + " has a slight overhead in RAM and CPU, and may prevent Bazel from making certain"
              + " aggressive RAM optimizations in some cases.")
  public abstract boolean getEnableCriticalPathProfiling();

  @Option(
      name = "experimental_stats_summary",
      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
      effectTags = {OptionEffectTag.TERMINAL_OUTPUT},
      defaultValue = "false",
      help = "Enable a modernized summary of the build stats.")
  public abstract boolean getStatsSummary();

  @Option(
      name = "execution_log_binary_file",
      defaultValue = "null",
      category = "verbosity",
      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
      effectTags = {OptionEffectTag.UNKNOWN},
      converter = ExecutionLogFileConverter.class,
      help =
          "Log the executed spawns into this file as length-delimited SpawnExec protos, according"
              + " to src/main/protobuf/spawn.proto. Prefer --execution_log_compact_file, which is"
              + " significantly smaller and cheaper to produce. The flag accepts boolean and string"
              + " values. If string, it represents a local path. If true, then"
              + " --experimental_stream_log_file_uploads must be set, whereby it will stream the"
              + " execution log to remote storage. If false, then logging to the execution log is"
              + " disabled. Related flags: --execution_log_compact_file (compact format; mutually"
              + " exclusive), --execution_log_json_file (text JSON format; mutually exclusive),"
              + " --execution_log_sort (whether to sort the execution log), --subcommands (for"
              + " displaying subcommands in terminal output).")
  public abstract PathFragment getExecutionLogBinaryFile();

  @Option(
      name = "execution_log_json_file",
      defaultValue = "null",
      category = "verbosity",
      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
      effectTags = {OptionEffectTag.UNKNOWN},
      converter = ExecutionLogFileConverter.class,
      help =
          "Log the executed spawns into this file as newline-delimited JSON representations of"
              + " SpawnExec protos, according to src/main/protobuf/spawn.proto. Prefer"
              + " --execution_log_compact_file, which is significantly smaller and cheaper to"
              + " produce. The flag accepts boolean and string values. If string, it represents a"
              + " local path. If true, then --experimental_stream_log_file_uploads must be set,"
              + " whereby it will stream the execution log to remote storage. If false, then"
              + " logging to the execution log is disabled. Related flags:"
              + " --execution_log_compact_file (compact format; mutually exclusive),"
              + " --execution_log_binary_file (binary protobuf format; mutually exclusive),"
              + " --execution_log_sort (whether to sort the execution log),"
              + " --subcommands (for displaying subcommands in terminal output).")
  public abstract PathFragment getExecutionLogJsonFile();

  @Option(
      name = "execution_log_compact_file",
      oldName = "experimental_execution_log_compact_file",
      defaultValue = "null",
      category = "verbosity",
      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
      effectTags = {OptionEffectTag.UNKNOWN},
      converter = ExecutionLogFileConverter.class,
      help =
          "Log the executed spawns into this file as length-delimited ExecLogEntry protos,"
              + " according to src/main/protobuf/spawn.proto. The entire file is zstd compressed."
              + " The flag accepts boolean and string values. If string, it represents a local"
              + " path. If true, then --experimental_stream_log_file_uploads must be set, whereby"
              + " it will stream the execution log to remote storage. If false, then logging to the"
              + " execution log is disabled. Related flags: --execution_log_binary_file (binary"
              + " protobuf format; mutually exclusive), --execution_log_json_file (text JSON"
              + " format; mutually exclusive), --subcommands (for displaying subcommands in"
              + " terminal output).")
  public abstract PathFragment getExecutionLogCompactFile();

  @Option(
      name = "execution_log_sort",
      defaultValue = "true",
      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
      effectTags = {OptionEffectTag.UNKNOWN},
      help =
          "Whether to sort the execution log, making it easier to compare logs across invocations."
              + " Set to false to avoid potentially significant CPU and memory usage at the end of"
              + " the invocation, at the cost of producing the log in nondeterministic execution"
              + " order. Only applies to the binary and JSON formats; the compact format is never"
              + " sorted.")
  public abstract boolean getExecutionLogSort();

  @Option(
      name = "execution_log_mnemonic_filter",
      defaultValue = ".*",
      converter = RegexFilter.RegexFilterConverter.class,
      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
      effectTags = {OptionEffectTag.UNKNOWN},
      help =
          "Filter the execution log by mnemonic. Only spawns with a matching mnemonic will be"
              + " logged. Supports a comma-separated list of regexes, with optional '-' prefix"
              + " for exclusions. The default is to log every spawn.")
  public abstract RegexFilter getExecutionLogMnemonicFilter();

  @Option(
      // TODO: when this flag is moved to non-experimental, rename it to a more general name
      // to reflect the new logic - it's not only about cache evictions.
      name = "experimental_remote_cache_eviction_retries",
      defaultValue = "5",
      documentationCategory = OptionDocumentationCategory.REMOTE,
      effectTags = {OptionEffectTag.EXECUTION},
      help =
          "The maximum number of attempts to retry if the build encountered a transient remote"
              + " cache error that would otherwise fail the build. Applies for example when"
              + " artifacts are evicted from the remote cache, or in certain cache failure"
              + " conditions. A new invocation id will be generated for each attempt.")
  public abstract int getRemoteRetryOnTransientCacheError();

  @Option(
      name = "allow_one_action_on_resource_unavailable",
      defaultValue = "true", // TODO: b/405364605 - Flip internally and change the default to false.
      documentationCategory = OptionDocumentationCategory.EXECUTION_STRATEGY,
      effectTags = {OptionEffectTag.EXECUTION},
      help =
          "If set, allow at least one action to run even if the resource is not enough or"
              + " unavailable.")
  public abstract boolean getAllowOneActionOnResourceUnavailable();

  /**
   * Accepts a filesystem path, or boolean-like values selecting a default location or disabling the
   * log.
   */
  public static final class ExecutionLogFileConverter extends Converter.Contextless<PathFragment>
      implements BooleanStyleOption {

    private static final Converters.BooleanConverter BOOLEAN_CONVERTER =
        new Converters.BooleanConverter();

    @Override
    @Nullable
    public PathFragment convert(String input) {
      if (input.isEmpty()) {
        return PathFragment.EMPTY_FRAGMENT;
      }
      try {
        return BOOLEAN_CONVERTER.convert(input) ? PathFragment.EMPTY_FRAGMENT : null;
      } catch (OptionsParsingException e) {
        return new OptionsUtils.PathFragmentConverter().convert(input);
      }
    }

    @Override
    public String getTypeDescription() {
      return "a path, or a boolean to use the default execution log location";
    }
  }

  /** An enum for specifying different formats of test output. */
  public enum TestOutputFormat {
    /**
     * Provide summary output only. NOTE: Functionally this is `NONE`, as `--test_summary` controls
     * the summary output.
     */
    SUMMARY,
    /** Print output from failed tests to the stderr after the test failure. */
    ERRORS,
    /** Print output from all tests to the stderr after the test completion. */
    ALL,
    /**
     * Stream output from tests as they run. Forces tests to be executed sequentially and locally.
     */
    STREAMED;

    /** Converts to {@link TestOutputFormat}. */
    public static class Converter extends EnumConverter<TestOutputFormat> {
      public Converter() {
        super(TestOutputFormat.class, "test output");
      }
    }
  }

  /** An enum for specifying different formatting styles of test summaries. */
  public enum TestSummaryFormat {
    /** Show all tests that can to completion, but not individual test cases. */
    SHORT,
    /** Like "SHORT", but do not show tests that were cached. */
    SHORT_UNCACHED,
    /** Like "SHORT", but even shorter: Only failed and flaky tests. */
    TERSE,
    /**
     * Show all tests (including tests that failed to build), their test cases, and a summary of all
     * test cases (passed, skipped, failing).
     */
    DETAILED,
    /** Like "DETAILED", but only for tests that were not cached. */
    DETAILED_UNCACHED,
    /** Do not print summary. */
    NONE,
    /** Summarize all test cases (passed, skipped, failing). */
    TESTCASE;

    /** Converts to {@link TestSummaryFormat}. */
    public static class Converter extends EnumConverter<TestSummaryFormat> {
      public Converter() {
        super(TestSummaryFormat.class, "test summary");
      }
    }
  }

  /** Converter for the --flaky_test_attempts option. */
  public static class TestAttemptsConverter extends PerLabelOptions.PerLabelOptionsConverter {
    private static final int MIN_VALUE = 1;
    private static final int MAX_VALUE = 10;

    private void validateInput(String input) throws OptionsParsingException {
      if (!Objects.equals(input, "default")) {
        int value = Integer.parseInt(input);
        if (value < MIN_VALUE) {
          throw new OptionsParsingException("'" + input + "' should be >= " + MIN_VALUE);
        } else if (value > MAX_VALUE) {
          throw new OptionsParsingException("'" + input + "' should be <= " + MAX_VALUE);
        }
      }
    }

    @Override
    public PerLabelOptions convert(String input) throws OptionsParsingException {
      try {
        return parseAsInteger(input);
      } catch (NumberFormatException ignored) {
        return parseAsRegex(input);
      }
    }

    private PerLabelOptions parseAsInteger(String input)
        throws NumberFormatException, OptionsParsingException {
      validateInput(input);
      RegexFilter catchAll =
          new RegexFilter(Collections.singletonList(".*"), Collections.<String>emptyList());
      return new PerLabelOptions(catchAll, Collections.singletonList(input));
    }

    private PerLabelOptions parseAsRegex(String input) throws OptionsParsingException {
      PerLabelOptions testRegexps = super.convert(input);
      if (testRegexps.getOptions().size() != 1) {
        throw new OptionsParsingException("'" + input + "' has multiple runs for a single pattern");
      }
      String runsPerTest = Iterables.getOnlyElement(testRegexps.getOptions());
      try {
        // Run this in order to catch errors.
        validateInput(runsPerTest);
      } catch (NumberFormatException e) {
        throw new OptionsParsingException("'" + input + "' has a non-numeric value", e);
      }
      return testRegexps;
    }

    @Override
    public String getTypeDescription() {
      return "a positive integer, the string \"default\", or test_regex@attempts. "
          + "This flag may be passed more than once";
    }
  }

  /** Converter for --local_test_jobs, which takes {@value FLAG_SYNTAX} */
  public static class LocalTestJobsConverter extends ResourceConverter.IntegerConverter {
    public LocalTestJobsConverter() throws OptionsParsingException {
      super(/* auto= */ () -> 0, /* minValue= */ 0, /* maxValue= */ Integer.MAX_VALUE);
    }
  }

  /** Converter for --subcommands */
  public static class ShowSubcommandsConverter extends BoolOrEnumConverter<ShowSubcommands> {
    public ShowSubcommandsConverter() {
      super(
          ShowSubcommands.class, "subcommand option", ShowSubcommands.TRUE, ShowSubcommands.FALSE);
    }
  }

  /** Converter for options that take a label-to-string-list assignment. */
  protected static class LabelToStringListConverter
      extends AssignmentToListOfValuesConverter<Label, String> {

    LabelToStringListConverter() {
      super(new LabelConverter(), new Converters.StringConverter(), AllowEmptyKeys.NO);
    }

    @Override
    public String getTypeDescription() {
      return "a '<Label>=value[,value]' assignment";
    }
  }
}
