// Copyright 2015 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.actions;

import static com.google.common.truth.Truth.assertThat;
import static com.google.devtools.build.lib.analysis.actions.CustomCommandLine.builder;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact;
import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact;
import com.google.devtools.build.lib.actions.ArtifactRoot.RootType;
import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
import com.google.devtools.build.lib.analysis.actions.CustomCommandLine;
import com.google.devtools.build.lib.analysis.actions.CustomCommandLine.VectorArg;
import com.google.devtools.build.lib.analysis.actions.CustomCommandLine.VectorArg.SimpleVectorArg;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.collect.nestedset.Order;
import com.google.devtools.build.lib.testutil.Scratch;
import com.google.devtools.build.lib.util.Fingerprint;
import com.google.devtools.build.lib.util.OnDemandString;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.testing.junit.testparameterinjector.TestParameter;
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import net.starlark.java.eval.EvalException;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

/** Tests for {@link CustomCommandLine}. */
@RunWith(TestParameterInjector.class)
public final class CustomCommandLineTest {
  private ArtifactRoot rootDir;
  private Artifact artifact1;
  private Artifact artifact2;

  @Before
  public void createArtifacts() throws Exception {
    Scratch scratch = new Scratch();
    rootDir = ArtifactRoot.asDerivedRoot(scratch.dir("/exec/root"), RootType.Output, "dir");
    artifact1 = ActionsTestUtil.createArtifact(rootDir, scratch.file("/exec/root/dir/file1.txt"));
    artifact2 = ActionsTestUtil.createArtifact(rootDir, scratch.file("/exec/root/dir/file2.txt"));
  }

  @Test
  public void addScalar_addsSingleArgument() throws Exception {
    assertThat(builder().add("--arg").build().arguments()).containsExactly("--arg");
    assertThat(builder().addDynamicString("--arg").build().arguments()).containsExactly("--arg");
    assertThat(builder().addLabel(Label.parseCanonical("//a:b")).build().arguments())
        .containsExactly("//a:b");
    assertThat(builder().addPath(PathFragment.create("path")).build().arguments())
        .containsExactly("path");
    assertThat(builder().addExecPath(artifact1).build().arguments())
        .containsExactly("dir/file1.txt");
    assertThat(
            builder()
                .addLazyString(
                    new OnDemandString() {
                      @Override
                      public String toString() {
                        return "foo";
                      }
                    })
                .build()
                .arguments())
        .containsExactly("foo");
  }

  @Test
  public void addScalar_withConstantArg_addsStringPrependedByArg() throws Exception {
    assertThat(builder().add("--arg", "val").build().arguments())
        .containsExactly("--arg", "val")
        .inOrder();
    assertThat(builder().addLabel("--arg", Label.parseCanonical("//a:b")).build().arguments())
        .containsExactly("--arg", "//a:b")
        .inOrder();
    assertThat(builder().addPath("--arg", PathFragment.create("path")).build().arguments())
        .containsExactly("--arg", "path")
        .inOrder();
    assertThat(builder().addExecPath("--arg", artifact1).build().arguments())
        .containsExactly("--arg", "dir/file1.txt")
        .inOrder();
    assertThat(
            builder()
                .addLazyString(
                    "--arg",
                    new OnDemandString() {
                      @Override
                      public String toString() {
                        return "foo";
                      }
                    })
                .build()
                .arguments())
        .containsExactly("--arg", "foo")
        .inOrder();
  }

  @Test
  public void addFormatted_addsCorrectlyFormattedArgument() throws Exception {
    assertThat(builder().addFormatted("%s%s", "hello", "world").build().arguments())
        .containsExactly("helloworld");
  }

  @Test
  public void addPrefixed_addsPrefixForArguments() throws Exception {
    assertThat(builder().addPrefixed("prefix-", "foo").build().arguments())
        .containsExactly("prefix-foo");
    assertThat(
            builder()
                .addPrefixedLabel("prefix-", Label.parseCanonical("//a:b"))
                .build()
                .arguments())
        .containsExactly("prefix-//a:b");
    assertThat(
            builder().addPrefixedPath("prefix-", PathFragment.create("path")).build().arguments())
        .containsExactly("prefix-path");
    assertThat(builder().addPrefixedExecPath("prefix-", artifact1).build().arguments())
        .containsExactly("prefix-dir/file1.txt");
  }

  @Test
  public void addAll_addsAllArguments() throws Exception {
    assertThat(builder().addAll(list("val1", "val2")).build().arguments())
        .containsExactly("val1", "val2")
        .inOrder();
    assertThat(builder().addAll(nestedSet("val1", "val2")).build().arguments())
        .containsExactly("val1", "val2")
        .inOrder();
    assertThat(
            builder()
                .addPaths(list(PathFragment.create("path1"), PathFragment.create("path2")))
                .build()
                .arguments())
        .containsExactly("path1", "path2")
        .inOrder();
    assertThat(
            builder()
                .addPaths(nestedSet(PathFragment.create("path1"), PathFragment.create("path2")))
                .build()
                .arguments())
        .containsExactly("path1", "path2")
        .inOrder();
    assertThat(builder().addExecPaths(list(artifact1, artifact2)).build().arguments())
        .containsExactly("dir/file1.txt", "dir/file2.txt")
        .inOrder();
    assertThat(builder().addExecPaths(nestedSet(artifact1, artifact2)).build().arguments())
        .containsExactly("dir/file1.txt", "dir/file2.txt")
        .inOrder();
  }

  @Test
  public void vectorAdds_withCompileTimeArg_addsAllValuesPrependedByArg() throws Exception {
    assertThat(builder().addAll("--arg", list("val1", "val2")).build().arguments())
        .containsExactly("--arg", "val1", "val2")
        .inOrder();
    assertThat(builder().addAll("--arg", nestedSet("val1", "val2")).build().arguments())
        .containsExactly("--arg", "val1", "val2")
        .inOrder();
    assertThat(
            builder()
                .addPaths("--arg", list(PathFragment.create("path1"), PathFragment.create("path2")))
                .build()
                .arguments())
        .containsExactly("--arg", "path1", "path2")
        .inOrder();
    assertThat(
            builder()
                .addPaths(
                    "--arg", nestedSet(PathFragment.create("path1"), PathFragment.create("path2")))
                .build()
                .arguments())
        .containsExactly("--arg", "path1", "path2")
        .inOrder();
    assertThat(builder().addExecPaths("--arg", list(artifact1, artifact2)).build().arguments())
        .containsExactly("--arg", "dir/file1.txt", "dir/file2.txt")
        .inOrder();
    assertThat(builder().addExecPaths("--arg", nestedSet(artifact1, artifact2)).build().arguments())
        .containsExactly("--arg", "dir/file1.txt", "dir/file2.txt")
        .inOrder();
  }

  private enum CustomCommandLineMode {
    REGULAR {
      @Override
      CustomCommandLine addAll(VectorArg<String> vectorArg) {
        return builder().addAll(vectorArg).build();
      }

      @Override
      CustomCommandLine addPaths(VectorArg<PathFragment> vectorArg) {
        return builder().addPaths(vectorArg).build();
      }

      @Override
      CustomCommandLine addExecPaths(VectorArg<Artifact> vectorArg) {
        return builder().addExecPaths(vectorArg).build();
      }

      @Override
      ImmutableList<String> expected(String... values) {
        return ImmutableList.copyOf(values);
      }
    },
    WITH_CONSTANT_ARG {
      @Override
      CustomCommandLine addAll(VectorArg<String> vectorArg) {
        return builder().addAll("--arg", vectorArg).build();
      }

      @Override
      CustomCommandLine addPaths(VectorArg<PathFragment> vectorArg) {
        return builder().addPaths("--arg", vectorArg).build();
      }

      @Override
      CustomCommandLine addExecPaths(VectorArg<Artifact> vectorArg) {
        return builder().addExecPaths("--arg", vectorArg).build();
      }

      @Override
      ImmutableList<String> expected(String... values) {
        return ImmutableList.<String>builderWithExpectedSize(values.length + 1)
            .add("--arg")
            .add(values)
            .build();
      }
    };

    abstract CustomCommandLine addAll(VectorArg<String> vectorArg);

    abstract CustomCommandLine addPaths(VectorArg<PathFragment> vectorArg);

    abstract CustomCommandLine addExecPaths(VectorArg<Artifact> vectorArg);

    abstract ImmutableList<String> expected(String... values);
  }

  private enum VectorArgMode {
    LIST {
      @Override
      <T> SimpleVectorArg<T> of(T... objects) {
        return VectorArg.of(list(objects));
      }

      @Override
      <T> SimpleVectorArg<T> each(VectorArg.Builder vectorArg, T... objects) {
        return vectorArg.each(list(objects));
      }
    },
    NESTED_SET {
      @Override
      <T> SimpleVectorArg<T> of(T... objects) {
        return VectorArg.of(nestedSet(objects));
      }

      @Override
      <T> SimpleVectorArg<T> each(VectorArg.Builder vectorArg, T... objects) {
        return vectorArg.each(nestedSet(objects));
      }
    };

    abstract <T> SimpleVectorArg<T> of(T... objects);

    abstract <T> SimpleVectorArg<T> each(VectorArg.Builder vectorArg, T... objects);
  }

  @Test
  public void addAllVector_addsAllArguments(
      @TestParameter CustomCommandLineMode customCommandLine,
      @TestParameter VectorArgMode vectorArg)
      throws Exception {
    assertThat(customCommandLine.addAll(vectorArg.of("1", "2")).arguments())
        .containsExactlyElementsIn(customCommandLine.expected("1", "2"))
        .inOrder();
    assertThat(
            customCommandLine
                .addAll(vectorArg.of(foo("1"), foo("2")).mapped(Foo::expandToStr))
                .arguments())
        .containsExactlyElementsIn(customCommandLine.expected("1", "2"))
        .inOrder();
  }

  @Test
  public void addJoinedVector_addsJoinedArguments(
      @TestParameter CustomCommandLineMode customCommandLine,
      @TestParameter VectorArgMode vectorArg)
      throws Exception {
    assertThat(
            customCommandLine
                .addAll(vectorArg.each(VectorArg.join(":"), "val1", "val2"))
                .arguments())
        .containsExactlyElementsIn(customCommandLine.expected("val1:val2"))
        .inOrder();
    assertThat(
            customCommandLine
                .addPaths(
                    vectorArg.each(
                        VectorArg.join(":"),
                        PathFragment.create("path1"),
                        PathFragment.create("path2")))
                .arguments())
        .containsExactlyElementsIn(customCommandLine.expected("path1:path2"))
        .inOrder();
    assertThat(
            customCommandLine
                .addExecPaths(vectorArg.each(VectorArg.join(":"), artifact1, artifact2))
                .arguments())
        .containsExactlyElementsIn(customCommandLine.expected("dir/file1.txt:dir/file2.txt"))
        .inOrder();
    assertThat(
            customCommandLine
                .addAll(
                    vectorArg
                        .each(VectorArg.join(":"), foo("1"), foo("2"))
                        .mapped(Foo::expandToStr))
                .arguments())
        .containsExactlyElementsIn(customCommandLine.expected("1:2"))
        .inOrder();
  }

  @Test
  public void addFormatEachVector_addsFormattedStrings(
      @TestParameter CustomCommandLineMode customCommandLine,
      @TestParameter VectorArgMode vectorArg)
      throws Exception {
    assertThat(
            customCommandLine
                .addAll(vectorArg.each(VectorArg.format("-D%s"), "val1", "val2"))
                .arguments())
        .containsExactlyElementsIn(customCommandLine.expected("-Dval1", "-Dval2"))
        .inOrder();
    assertThat(
            customCommandLine
                .addPaths(
                    vectorArg.each(
                        VectorArg.format("-D%s"),
                        PathFragment.create("path1"),
                        PathFragment.create("path2")))
                .arguments())
        .containsExactlyElementsIn(customCommandLine.expected("-Dpath1", "-Dpath2"))
        .inOrder();
    assertThat(
            customCommandLine
                .addExecPaths(vectorArg.each(VectorArg.format("-D%s"), artifact1, artifact2))
                .arguments())
        .containsExactlyElementsIn(customCommandLine.expected("-Ddir/file1.txt", "-Ddir/file2.txt"))
        .inOrder();
    assertThat(
            customCommandLine
                .addAll(
                    vectorArg
                        .each(VectorArg.format("-D%s"), foo("1"), foo("2"))
                        .mapped(Foo::expandToStr))
                .arguments())
        .containsExactlyElementsIn(customCommandLine.expected("-D1", "-D2"))
        .inOrder();
  }

  @Test
  public void addFormatEachJoinedVector_addsJoinedFormattedStrings(
      @TestParameter CustomCommandLineMode customCommandLine,
      @TestParameter VectorArgMode vectorArg)
      throws Exception {
    assertThat(
            customCommandLine
                .addAll(vectorArg.each(VectorArg.format("-D%s").join(":"), "val1", "val2"))
                .arguments())
        .containsExactlyElementsIn(customCommandLine.expected("-Dval1:-Dval2"))
        .inOrder();
    assertThat(
            customCommandLine
                .addPaths(
                    vectorArg.each(
                        VectorArg.format("-D%s").join(":"),
                        PathFragment.create("path1"),
                        PathFragment.create("path2")))
                .arguments())
        .containsExactlyElementsIn(customCommandLine.expected("-Dpath1:-Dpath2"))
        .inOrder();
    assertThat(
            customCommandLine
                .addExecPaths(
                    vectorArg.each(VectorArg.format("-D%s").join(":"), artifact1, artifact2))
                .arguments())
        .containsExactlyElementsIn(customCommandLine.expected("-Ddir/file1.txt:-Ddir/file2.txt"))
        .inOrder();
    assertThat(
            customCommandLine
                .addAll(
                    vectorArg
                        .each(VectorArg.format("-D%s").join(":"), foo("1"), foo("2"))
                        .mapped(Foo::expandToStr))
                .arguments())
        .containsExactlyElementsIn(customCommandLine.expected("-D1:-D2"))
        .inOrder();
  }

  @Test
  public void addBeforeEachVector_addsArgumentsEachPrependedWithArg(
      @TestParameter CustomCommandLineMode customCommandLine,
      @TestParameter VectorArgMode vectorArg)
      throws Exception {
    assertThat(
            customCommandLine
                .addAll(vectorArg.each(VectorArg.addBefore("-D"), "val1", "val2"))
                .arguments())
        .containsExactlyElementsIn(customCommandLine.expected("-D", "val1", "-D", "val2"))
        .inOrder();
    assertThat(
            customCommandLine
                .addPaths(
                    vectorArg.each(
                        VectorArg.addBefore("-D"),
                        PathFragment.create("path1"),
                        PathFragment.create("path2")))
                .arguments())
        .containsExactlyElementsIn(customCommandLine.expected("-D", "path1", "-D", "path2"))
        .inOrder();
    assertThat(
            customCommandLine
                .addExecPaths(vectorArg.each(VectorArg.addBefore("-D"), artifact1, artifact2))
                .arguments())
        .containsExactlyElementsIn(
            customCommandLine.expected("-D", "dir/file1.txt", "-D", "dir/file2.txt"))
        .inOrder();
    assertThat(
            customCommandLine
                .addAll(
                    vectorArg
                        .each(VectorArg.addBefore("-D"), foo("1"), foo("2"))
                        .mapped(Foo::expandToStr))
                .arguments())
        .containsExactlyElementsIn(customCommandLine.expected("-D", "1", "-D", "2"))
        .inOrder();
  }

  @Test
  public void addBeforeEachFormattedVector_addsFormattedStringsPrependedWithArg(
      @TestParameter VectorArgMode vectorArg) throws Exception {
    assertThat(
            builder()
                .addAll(vectorArg.each(VectorArg.addBefore("-D").format("D%s"), "val1", "val2"))
                .build()
                .arguments())
        .containsExactly("-D", "Dval1", "-D", "Dval2")
        .inOrder();
    assertThat(
            builder()
                .addPaths(
                    vectorArg.each(
                        VectorArg.addBefore("-D").format("D%s"),
                        PathFragment.create("path1"),
                        PathFragment.create("path2")))
                .build()
                .arguments())
        .containsExactly("-D", "Dpath1", "-D", "Dpath2")
        .inOrder();
    assertThat(
            builder()
                .addExecPaths(
                    vectorArg.each(VectorArg.addBefore("-D").format("D%s"), artifact1, artifact2))
                .build()
                .arguments())
        .containsExactly("-D", "Ddir/file1.txt", "-D", "Ddir/file2.txt")
        .inOrder();
    assertThat(
            builder()
                .addAll(
                    vectorArg
                        .each(VectorArg.addBefore("-D").format("D%s"), foo("1"), foo("2"))
                        .mapped(Foo::expandToStr))
                .build()
                .arguments())
        .containsExactly("-D", "D1", "-D", "D2")
        .inOrder();
  }

  @Test
  public void addCombinedArgs_addsAllArguments() throws Exception {
    CustomCommandLine cl =
        builder()
            .add("--arg")
            .addAll("--args", ImmutableList.of("abc"))
            .addExecPaths("--path1", ImmutableList.of(artifact1))
            .addExecPath("--path2", artifact2)
            .build();
    assertThat(cl.arguments())
        .containsExactly(
            "--arg", "--args", "abc", "--path1", "dir/file1.txt", "--path2", "dir/file2.txt")
        .inOrder();
  }

  @Test
  public void addNulls_addsNothing() throws Exception {
    Artifact treeArtifact = createTreeArtifact("myTreeArtifact");
    assertThat(treeArtifact).isNotNull();

    CustomCommandLine cl =
        builder()
            .addDynamicString(null)
            .addLabel(null)
            .addPath(null)
            .addExecPath(null)
            .addLazyString(null)
            .add("foo", null)
            .addLabel("foo", null)
            .addPath("foo", null)
            .addExecPath("foo", null)
            .addLazyString("foo", null)
            .addPrefixed("prefix", null)
            .addPrefixedLabel("prefix", null)
            .addPrefixedPath("prefix", null)
            .addPrefixedExecPath("prefix", null)
            .addAll((ImmutableList<String>) null)
            .addAll(ImmutableList.of())
            .addPaths((ImmutableList<PathFragment>) null)
            .addPaths(ImmutableList.of())
            .addExecPaths((ImmutableList<Artifact>) null)
            .addExecPaths(ImmutableList.of())
            .addAll((NestedSet<String>) null)
            .addAll(NestedSetBuilder.emptySet(Order.STABLE_ORDER))
            .addPaths((NestedSet<PathFragment>) null)
            .addPaths(NestedSetBuilder.emptySet(Order.STABLE_ORDER))
            .addExecPaths((NestedSet<Artifact>) null)
            .addExecPaths(NestedSetBuilder.emptySet(Order.STABLE_ORDER))
            .addAll(VectorArg.of((NestedSet<String>) null))
            .addAll(VectorArg.of(NestedSetBuilder.<String>emptySet(Order.STABLE_ORDER)))
            .addAll("foo", (ImmutableList<String>) null)
            .addAll("foo", ImmutableList.of())
            .addPaths("foo", (ImmutableList<PathFragment>) null)
            .addPaths("foo", ImmutableList.of())
            .addExecPaths("foo", (ImmutableList<Artifact>) null)
            .addExecPaths("foo", ImmutableList.of())
            .addAll("foo", (NestedSet<String>) null)
            .addAll("foo", NestedSetBuilder.emptySet(Order.STABLE_ORDER))
            .addPaths("foo", (NestedSet<PathFragment>) null)
            .addPaths("foo", NestedSetBuilder.emptySet(Order.STABLE_ORDER))
            .addExecPaths("foo", (NestedSet<Artifact>) null)
            .addExecPaths("foo", NestedSetBuilder.emptySet(Order.STABLE_ORDER))
            .addAll("foo", VectorArg.of((NestedSet<String>) null))
            .addAll("foo", VectorArg.of(NestedSetBuilder.<String>emptySet(Order.STABLE_ORDER)))
            .addPlaceholderTreeArtifactExecPath("foo", null)
            .build();

    assertThat(cl.arguments()).isEmpty();
  }

  @Test
  public void evaluateTreeFileArtifacts_replacesTreeArtifactsWithChildrenExecPaths()
      throws Exception {
    SpecialArtifact treeArtifactOne = createTreeArtifact("myArtifact/treeArtifact1");
    SpecialArtifact treeArtifactTwo = createTreeArtifact("myArtifact/treeArtifact2");

    CustomCommandLine commandLineTemplate =
        builder()
            .addPlaceholderTreeArtifactExecPath("--argOne", treeArtifactOne)
            .addPlaceholderTreeArtifactExecPath("--argTwo", treeArtifactTwo)
            .build();

    TreeFileArtifact treeFileArtifactOne =
        TreeFileArtifact.createTreeOutput(treeArtifactOne, "children/child1");
    TreeFileArtifact treeFileArtifactTwo =
        TreeFileArtifact.createTreeOutput(treeArtifactTwo, "children/child2");

    CustomCommandLine commandLine =
        commandLineTemplate.evaluateTreeFileArtifacts(
            ImmutableList.of(treeFileArtifactOne, treeFileArtifactTwo));

    assertThat(commandLine.arguments())
        .containsExactly(
            "--argOne",
            "dir/myArtifact/treeArtifact1/children/child1",
            "--argTwo",
            "dir/myArtifact/treeArtifact2/children/child2")
        .inOrder();
  }

  @Test
  public void addAllMappedTreeFileArtifacts_mapToRelativePath_addsTreeFileRelativePaths()
      throws Exception {
    SpecialArtifact treeArtifact = createTreeArtifact("myArtifact/treeArtifact");

    TreeFileArtifact treeFileArtifactOne =
        TreeFileArtifact.createTreeOutput(treeArtifact, "children/child1");
    TreeFileArtifact treeFileArtifactTwo =
        TreeFileArtifact.createTreeOutput(treeArtifact, "children/child2");

    CommandLineItem.MapFn<Artifact> expandParentRelativePath =
        (src, args) -> {
          try {
            args.accept(src.getTreeRelativePathString());
          } catch (EvalException e) {
            throw new IllegalStateException("Unexpected EvalException thown.", e);
          }
        };

    CustomCommandLine commandLineTemplate =
        builder()
            .addAll(
                VectorArg.SimpleVectorArg.of(
                        ImmutableList.of(treeFileArtifactOne, treeFileArtifactTwo))
                    .mapped(expandParentRelativePath))
            .build();

    assertThat(commandLineTemplate.arguments())
        .containsExactly("children/child1", "children/child2")
        .inOrder();
  }

  @Test
  public void arguments_unsubstitutedTreeArtifactPlaceholder_fails() {
    Artifact treeArtifactOne = createTreeArtifact("myArtifact/treeArtifact1");
    Artifact treeArtifactTwo = createTreeArtifact("myArtifact/treeArtifact2");

    CustomCommandLine commandLineTemplate =
        builder()
            .addPlaceholderTreeArtifactExecPath("--argOne", treeArtifactOne)
            .addPlaceholderTreeArtifactExecPath("--argTwo", treeArtifactTwo)
            .build();

    assertThrows(RuntimeException.class, commandLineTemplate::arguments);
  }

  @Test
  public void addToFingerPrint_computesUniqueKeyForDifferentCommandLines() throws Exception {
    NestedSet<String> values = NestedSetBuilder.<String>stableOrder().add("a").add("b").build();
    ImmutableList<CustomCommandLine> commandLines =
        ImmutableList.<CustomCommandLine>builder()
            .add(builder().add("arg").build())
            .add(builder().addFormatted("--foo=%s", "arg").build())
            .add(builder().addPrefixed("--foo=%s", "arg").build())
            .add(builder().addAll(values).build())
            .add(builder().addAll(VectorArg.addBefore("--foo=%s").each(values)).build())
            .add(builder().addAll(VectorArg.join("--foo=%s").each(values)).build())
            .add(builder().addAll(VectorArg.format("--foo=%s").each(values)).build())
            .add(
                builder()
                    .addAll(VectorArg.of(values).mapped((s, args) -> args.accept(s + "_mapped")))
                    .build())
            .build();

    // Ensure all these command lines have distinct keys
    ActionKeyContext actionKeyContext = new ActionKeyContext();
    Map<String, CustomCommandLine> digests = new HashMap<>();
    for (CustomCommandLine commandLine : commandLines) {
      Fingerprint fingerprint = new Fingerprint();
      commandLine.addToFingerprint(
          actionKeyContext,
          /* artifactExpander= */ null,
          fingerprint,
          PathStripper.PathMapper.NOOP);
      String digest = fingerprint.hexDigestAndReset();
      CustomCommandLine previous = digests.putIfAbsent(digest, commandLine);
      if (previous != null) {
        fail(
            String.format(
                "Found two command lines with identical digest %s: '%s' and '%s'",
                digest,
                Joiner.on(' ').join(previous.arguments()),
                Joiner.on(' ').join(commandLine.arguments())));
      }
    }
  }

  private SpecialArtifact createTreeArtifact(String rootRelativePath) {
    return ActionsTestUtil.createTreeArtifactWithGeneratingAction(
        rootDir, rootDir.getExecPath().getRelative(rootRelativePath));
  }

  private static <T> ImmutableList<T> list(T... objects) {
    return ImmutableList.copyOf(objects);
  }

  private static <T> NestedSet<T> nestedSet(T... objects) {
    return NestedSetBuilder.create(Order.STABLE_ORDER, objects);
  }

  private static Foo foo(String str) {
    return new Foo(str);
  }

  private static class Foo {
    private final String str;

    Foo(String str) {
      this.str = str;
    }

    static void expandToStr(Foo foo, Consumer<String> args) {
      args.accept(foo.str);
    }
  }
}
