// Copyright 2023 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.buildtool;

import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.truth.Truth.assertThat;
import static com.google.devtools.build.lib.packages.Attribute.attr;
import static com.google.devtools.build.lib.packages.BuildType.LABEL_LIST;
import static com.google.devtools.build.lib.packages.Type.STRING;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
import com.google.devtools.build.lib.analysis.actions.FileWriteActionContext;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
import com.google.devtools.build.lib.analysis.config.BuildOptionsView;
import com.google.devtools.build.lib.analysis.config.Fragment;
import com.google.devtools.build.lib.analysis.config.FragmentOptions;
import com.google.devtools.build.lib.analysis.config.RequiresOptions;
import com.google.devtools.build.lib.analysis.config.TransitionFactories;
import com.google.devtools.build.lib.analysis.config.transitions.PatchTransition;
import com.google.devtools.build.lib.analysis.config.transitions.TransitionFactory;
import com.google.devtools.build.lib.analysis.util.MockRule;
import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase;
import com.google.devtools.build.lib.buildtool.util.TestRuleModule;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.exec.FileWriteStrategy;
import com.google.devtools.build.lib.exec.ModuleActionContextRegistry;
import com.google.devtools.build.lib.packages.ImplicitOutputsFunction;
import com.google.devtools.build.lib.packages.NonconfigurableAttributeMapper;
import com.google.devtools.build.lib.packages.RuleTransitionData;
import com.google.devtools.build.lib.runtime.BlazeModule;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.testutil.TestConstants;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.Symlinks;
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.OptionMetadataTag;
import com.google.testing.junit.testparameterinjector.TestParameter;
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
import java.io.IOException;
import java.util.Optional;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(TestParameterInjector.class)
public final class ConvenienceSymlinkTest extends BuildIntegrationTestCase {

  /** test options to cause the output directory to change */
  public static final class PathTestOptions extends FragmentOptions {
    @Option(
        name = "output_directory_name",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.AFFECTS_OUTPUTS},
        metadataTags = {OptionMetadataTag.EXPLICIT_IN_OUTPUT_PATH},
        defaultValue = "default")
    public String outputDirectoryName;

    @Option(
        name = "useless_option",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.NO_OP},
        defaultValue = "default")
    public String uselessOption;
  }

  /** Test fragment. */
  @RequiresOptions(options = {PathTestOptions.class})
  public static final class PathTestConfiguration extends Fragment {
    private final String outputDirectoryName;

    public PathTestConfiguration(BuildOptions buildOptions) {
      this.outputDirectoryName = buildOptions.get(PathTestOptions.class).outputDirectoryName;
    }

    @Override
    public String getOutputDirectoryName() {
      return outputDirectoryName;
    }
  }

  private static final class PathTransition implements PatchTransition {
    private final String newPath;

    PathTransition(String newPath) {
      this.newPath = newPath;
    }

    @Override
    public ImmutableSet<Class<? extends FragmentOptions>> requiresOptionFragments() {
      return ImmutableSet.of(PathTestOptions.class);
    }

    @Override
    public BuildOptions patch(BuildOptionsView options, EventHandler eventHandler) {
      BuildOptionsView clone = options.clone();
      clone.get(PathTestOptions.class).outputDirectoryName = newPath;
      return clone.underlying();
    }
  }

  private static final class PathTransitionFactory
      implements TransitionFactory<RuleTransitionData> {
    @Override
    public PatchTransition create(RuleTransitionData ruleData) {
      return new PathTransition(
          NonconfigurableAttributeMapper.of(ruleData.rule()).get("path", STRING));
    }
  }

  private static final class UselessOptionTransition implements PatchTransition {
    private final String newValue;

    UselessOptionTransition(String newValue) {
      this.newValue = newValue;
    }

    @Override
    public ImmutableSet<Class<? extends FragmentOptions>> requiresOptionFragments() {
      return ImmutableSet.of(PathTestOptions.class);
    }

    @Override
    public BuildOptions patch(BuildOptionsView options, EventHandler eventHandler) {
      BuildOptionsView clone = options.clone();
      clone.get(PathTestOptions.class).uselessOption = newValue;
      return clone.underlying();
    }
  }

  private static final class UselessOptionTransitionFactory
      implements TransitionFactory<RuleTransitionData> {
    @Override
    public PatchTransition create(RuleTransitionData ruleData) {
      return new UselessOptionTransition(
          NonconfigurableAttributeMapper.of(ruleData.rule()).get("value", STRING));
    }
  }

  private static final class PathTestRulesModule extends BlazeModule {
    @Override
    public void registerActionContexts(
        ModuleActionContextRegistry.Builder registryBuilder,
        CommandEnvironment env,
        BuildRequest buildRequest) {
      // we need an implementation of FileWriteActionContext to get our file writes to succeed
      registryBuilder.register(FileWriteActionContext.class, new FileWriteStrategy());
      // we need something to consume FileWriteActionContext or the registration will have no effect
      registryBuilder.restrictTo(FileWriteActionContext.class, "local");
    }

    @Override
    public void initializeRuleClasses(ConfiguredRuleClassProvider.Builder builder) {
      TestRuleModule.getModule().initializeRuleClasses(builder);

      MockRule basicRule =
          () ->
              MockRule.define(
                  "basic_rule",
                  (ruleBuilder, env) ->
                      ruleBuilder
                          .add(attr("deps", LABEL_LIST).allowedFileTypes())
                          .setImplicitOutputsFunction(
                              ImplicitOutputsFunction.fromTemplates("%{name}.bin")));
      MockRule incomingTransitionRule =
          () ->
              MockRule.define(
                  "incoming_transition_rule",
                  (ruleBuilder, env) ->
                      ruleBuilder
                          .add(
                              attr("path", STRING)
                                  .mandatory()
                                  .nonconfigurable("used in transition"))
                          .add(attr("deps", LABEL_LIST).allowedFileTypes())
                          .setImplicitOutputsFunction(
                              ImplicitOutputsFunction.fromTemplates("%{name}.bin"))
                          .cfg(new PathTransitionFactory()));
      MockRule incomingUnrelatedTransitionRule =
          () ->
              MockRule.define(
                  "incoming_unrelated_transition_rule",
                  (ruleBuilder, env) ->
                      ruleBuilder
                          .add(
                              attr("value", STRING)
                                  .mandatory()
                                  .nonconfigurable("used in transition"))
                          .add(attr("deps", LABEL_LIST).allowedFileTypes())
                          .setImplicitOutputsFunction(
                              ImplicitOutputsFunction.fromTemplates("%{name}.bin"))
                          .cfg(new UselessOptionTransitionFactory()));

      MockRule outgoingTransitionRule =
          () ->
              MockRule.define(
                  "outgoing_transition_rule",
                  (ruleBuilder, env) ->
                      ruleBuilder
                          .add(
                              attr("deps", LABEL_LIST)
                                  .allowedFileTypes()
                                  .cfg(
                                      TransitionFactories.of(
                                          new PathTransition("set_by_outgoing_transition_rule"))))
                          .setImplicitOutputsFunction(
                              ImplicitOutputsFunction.fromTemplates("%{name}.bin")));

      builder
          .addConfigurationFragment(PathTestConfiguration.class)
          .addRuleDefinition(basicRule)
          .addRuleDefinition(incomingTransitionRule)
          .addRuleDefinition(incomingUnrelatedTransitionRule)
          .addRuleDefinition(outgoingTransitionRule);
    }
  }

  @TestParameter boolean mergedAnalysisExecution;

  @Override
  protected void setupOptions() throws Exception {
    super.setupOptions();

    addOptions("--experimental_merged_skyframe_analysis_execution=" + mergedAnalysisExecution);
  }

  @Override
  protected BlazeModule getRulesModule() {
    return new PathTestRulesModule();
  }

  private Path getExecRoot() {
    return getBlazeWorkspace().getDirectories().getExecRoot(TestConstants.WORKSPACE_NAME);
  }

  private Path getOutputPath() {
    return getBlazeWorkspace().getDirectories().getOutputPath(TestConstants.WORKSPACE_NAME);
  }

  /** Gets a mapping from the workspace-relative paths of symlinks to the paths they point to. */
  private ImmutableMap<String, Path> getConvenienceSymlinks() throws IOException {
    return getWorkspace().getDirectoryEntries().stream()
        .filter(Path::isSymbolicLink)
        .collect(
            toImmutableMap(
                (path) -> path.relativeTo(getWorkspace()).toString(),
                (path) -> {
                  try {
                    return getWorkspace().getRelative(path.readSymbolicLinkUnchecked());
                  } catch (IOException ex) {
                    throw new RuntimeException(ex);
                  }
                }));
  }

  @Test
  public void sanityCheckFilesHaveNullConfigurations() throws Exception {
    // Other tests in this file expect that files will have a null configuration.
    write("files/BUILD", "exports_files(['foo.txt', 'bar.txt'])");
    write("files/foo.txt", "This is just a test file to pretend to build.");
    write("files/bar.txt", "This is just a test file to pretend to build.");
    BuildResult result = buildTarget("//files:foo.txt", "//files:bar.txt");

    assertThat(
            result.getActualTargets().stream()
                .collect(
                    toImmutableMap(
                        target -> target.getLabel().toString(),
                        target -> Optional.ofNullable(target.getConfigurationKey()))))
        .containsExactly(
            "//files:foo.txt", Optional.empty(),
            "//files:bar.txt", Optional.empty());
  }

  @Test
  public void sanityCheckOutputDirectory() throws Exception {
    // Other tests in this file expect that changing the output_directory_name flag changes the
    // output directory of the configuration to the same value.

    // This test relies on hard-coded paths for intermediate artifacts so
    // must force output directory naming into legacy behaviors for now.
    addOptions(
        "--output_directory_name=set_by_flag",
        "--compilation_mode=fastbuild",
        "--experimental_output_directory_naming_scheme=legacy",
        "--experimental_exec_configuration_distinguisher=legacy");

    write(
        "path/BUILD",
        "basic_rule(name='from_flag')",
        "incoming_transition_rule(name='from_transition', path='set_by_transition')",
        "incoming_unrelated_transition_rule(name='unrelated_transition', value='whatever')",
        "outgoing_transition_rule(name='outgoing_transition')");
    BuildResult result =
        buildTarget(
            "//path:from_flag",
            "//path:from_transition",
            "//path:unrelated_transition",
            "//path:outgoing_transition");

    assertThat(
            result.getActualTargets().stream()
                .collect(
                    toImmutableMap(
                        (target) -> target.getLabel().toString(),
                        (target) ->
                            getConfiguration(target)
                                .getOutputDirectory(RepositoryName.MAIN)
                                .getRoot()
                                .asPath()
                                .relativeTo(getOutputPath())
                                .toString())))
        .containsExactly(
            "//path:from_flag", "set_by_flag-" + getTargetConfiguration().getCpu() + "-fastbuild",
            "//path:from_transition",
                "set_by_transition-" + getTargetConfiguration().getCpu() + "-fastbuild",
            "//path:unrelated_transition",
                "set_by_flag-" + getTargetConfiguration().getCpu() + "-fastbuild",
            "//path:outgoing_transition",
                "set_by_flag-" + getTargetConfiguration().getCpu() + "-fastbuild");
  }

  @Test
  public void buildingNothing_unsetsSymlinks() throws Exception {
    addOptions("--symlink_prefix=nothing-", "--incompatible_skip_genfiles_symlink=false");

    Path config = getOutputBase().getRelative("some-imaginary-config");
    // put symlinks at the convenience symlinks spots to simulate a prior build
    Path binLink = getWorkspace().getChild("nothing-bin");
    binLink.createSymbolicLink(config.getChild("bin"));
    Path genfilesLink = getWorkspace().getChild("nothing-genfiles");
    genfilesLink.createSymbolicLink(config.getChild("genfiles"));
    Path testlogsLink = getWorkspace().getChild("nothing-testlogs");
    testlogsLink.createSymbolicLink(config.getChild("testlogs"));

    buildTarget();

    // there should be nothing at any of the convenience symlinks which depend on configuration -
    // the symlinks put there during the simulated prior build should have been deleted
    assertThat(binLink.exists(Symlinks.NOFOLLOW)).isTrue();
    assertThat(genfilesLink.exists(Symlinks.NOFOLLOW)).isTrue();
    assertThat(testlogsLink.exists(Symlinks.NOFOLLOW)).isTrue();

    // the execroot and output path symlinks should have been created because they don't depend on
    // configuration, but no other symlinks should have been created
    assertThat(getConvenienceSymlinks())
        .containsExactly(
            // notably absent: nothing-bin, nothing-genfiles, nothing-testlogs
            // these were also not created under other names
            "nothing-bin",
            getOutputPath()
                .getRelative("default-" + getTargetConfiguration().getCpu() + "-fastbuild/bin"),
            "nothing-genfiles",
            getOutputPath()
                .getRelative("default-" + getTargetConfiguration().getCpu() + "-fastbuild/bin"),
            "nothing-testlogs",
            getOutputPath()
                .getRelative(
                    "default-" + getTargetConfiguration().getCpu() + "-fastbuild/testlogs"),
            "nothing-" + TestConstants.WORKSPACE_NAME,
            getExecRoot(),
            "nothing-out",
            getOutputPath());
  }

  @Test
  public void buildingOnlyTargetsWithNullConfigurations_unsetsSymlinks() throws Exception {
    addOptions("--symlink_prefix=nulled-", "--incompatible_skip_genfiles_symlink=false");

    Path config = getOutputBase().getRelative("some-imaginary-config");
    // put symlinks at the convenience symlinks spots to simulate a prior build
    Path binLink = getWorkspace().getChild("nulled-bin");
    binLink.createSymbolicLink(config.getChild("bin"));
    Path genfilesLink = getWorkspace().getChild("nulled-genfiles");
    genfilesLink.createSymbolicLink(config.getChild("genfiles"));
    Path testlogsLink = getWorkspace().getChild("nulled-testlogs");
    testlogsLink.createSymbolicLink(config.getChild("testlogs"));

    write("files/BUILD", "exports_files(['foo.txt', 'bar.txt'])");
    write("files/foo.txt", "This is just a test file to pretend to build.");
    write("files/bar.txt", "This is just a test file to pretend to build.");
    buildTarget("//files:foo.txt", "//files:bar.txt");

    // there should be nothing at any of the convenience symlinks which depend on configuration -
    // the symlinks put there during the simulated prior build should have been deleted
    assertThat(binLink.exists(Symlinks.NOFOLLOW)).isFalse();
    assertThat(genfilesLink.exists(Symlinks.NOFOLLOW)).isFalse();
    assertThat(testlogsLink.exists(Symlinks.NOFOLLOW)).isFalse();

    // the execroot and output path symlinks should have been created because they don't depend on
    // configuration, but no other symlinks should have been created
    assertThat(getConvenienceSymlinks())
        .containsExactly(
            // notably absent: nulled-bin, nulled-genfiles, nulled-testlogs
            // these were also not created under other names
            "nulled-" + TestConstants.WORKSPACE_NAME, getExecRoot(), "nulled-out", getOutputPath());
  }

  @Test
  public void buildingTargetsWithDifferentOutputDirectories_unsetsSymlinksIfNoneAreTopLevel()
      throws Exception {
    addOptions("--symlink_prefix=ambiguous-", "--incompatible_skip_genfiles_symlink=false");

    Path config = getOutputPath().getRelative("some-imaginary-config");
    // put symlinks at the convenience symlinks spots to simulate a prior build
    Path binLink = getWorkspace().getChild("ambiguous-bin");
    binLink.createSymbolicLink(config.getChild("bin"));
    Path genfilesLink = getWorkspace().getChild("ambiguous-genfiles");
    genfilesLink.createSymbolicLink(config.getChild("genfiles"));
    Path testlogsLink = getWorkspace().getChild("ambiguous-testlogs");
    testlogsLink.createSymbolicLink(config.getChild("testlogs"));

    write(
        "targets/BUILD",
        "incoming_transition_rule(name='config1', path='set_from_config1')",
        "incoming_transition_rule(name='config2', path='set_from_config2')");
    buildTarget("//targets:config1", "//targets:config2");

    // there should be nothing at any of the convenience symlinks which depend on configuration -
    // the symlinks put there during the simulated prior build should have been deleted
    assertThat(binLink.exists(Symlinks.NOFOLLOW)).isFalse();
    assertThat(genfilesLink.exists(Symlinks.NOFOLLOW)).isFalse();
    assertThat(testlogsLink.exists(Symlinks.NOFOLLOW)).isFalse();

    // the execroot and output path symlinks should have been created because they don't depend on
    // configuration, but no other symlinks should have been created
    assertThat(getConvenienceSymlinks())
        .containsExactly(
            // notably absent: ambiguous-bin, ambiguous-genfiles, ambiguous-testlogs
            // these were also not created under other names
            "ambiguous-" + TestConstants.WORKSPACE_NAME,
            getExecRoot(),
            "ambiguous-out",
            getOutputPath());
  }

  @Test
  public void buildingTargetsWithDifferentOutputDirectories_setsSymlinksIfAnyAreTopLevel()
      throws Exception {
    addOptions(
        "--symlink_prefix=ambiguous-",
        "--incompatible_skip_genfiles_symlink=false",
        "--incompatible_merge_genfiles_directory=false",
        "--incompatible_skip_genfiles_symlink=false");

    Path config = getOutputPath().getRelative("some-imaginary-config");
    // put symlinks at the convenience symlinks spots to simulate a prior build
    Path binLink = getWorkspace().getChild("ambiguous-bin");
    binLink.createSymbolicLink(config.getChild("bin"));
    Path genfilesLink = getWorkspace().getChild("ambiguous-genfiles");
    genfilesLink.createSymbolicLink(config.getChild("genfiles"));
    Path testlogsLink = getWorkspace().getChild("ambiguous-testlogs");
    testlogsLink.createSymbolicLink(config.getChild("testlogs"));

    write(
        "targets/BUILD",
        "basic_rule(name='default')",
        "incoming_transition_rule(name='config1', path='set_from_config1')");
    buildTarget("//targets:default", "//targets:config1");

    assertThat(getConvenienceSymlinks())
        .containsExactly(
            "ambiguous-bin",
            getOutputPath()
                .getRelative("default-" + getTargetConfiguration().getCpu() + "-fastbuild/bin"),
            "ambiguous-genfiles",
            getOutputPath()
                .getRelative(
                    "default-" + getTargetConfiguration().getCpu() + "-fastbuild/genfiles"),
            "ambiguous-testlogs",
            getOutputPath()
                .getRelative(
                    "default-" + getTargetConfiguration().getCpu() + "-fastbuild/testlogs"),
            "ambiguous-" + TestConstants.WORKSPACE_NAME,
            getExecRoot(),
            "ambiguous-out",
            getOutputPath());
  }

  @Test
  public void buildingTargetsWithSameConfiguration_setsSymlinks() throws Exception {
    addOptions(
        "--symlink_prefix=same-",
        "--compilation_mode=fastbuild",
        "--incompatible_merge_genfiles_directory=false",
        "--incompatible_skip_genfiles_symlink=false");

    write(
        "targets/BUILD",
        "incoming_transition_rule(name='configged1', path='configured')",
        "incoming_transition_rule(name='configged2', path='configured')");
    buildTarget("//targets:configged1", "//targets:configged2");

    assertThat(getConvenienceSymlinks())
        .containsExactly(
            "same-bin",
            getOutputPath()
                .getRelative("configured-" + getTargetConfiguration().getCpu() + "-fastbuild/bin"),
            "same-genfiles",
            getOutputPath()
                .getRelative(
                    "configured-" + getTargetConfiguration().getCpu() + "-fastbuild/genfiles"),
            "same-testlogs",
            getOutputPath()
                .getRelative(
                    "configured-" + getTargetConfiguration().getCpu() + "-fastbuild/testlogs"),
            "same-" + TestConstants.WORKSPACE_NAME,
            getExecRoot(),
            "same-out",
            getOutputPath());
  }

  @Test
  public void buildingSameConfigurationTargetsWithDifferentConfigurationDeps_setsSymlinks()
      throws Exception {
    addOptions(
        "--output_directory_name=from_flag",
        "--symlink_prefix=united-",
        "--compilation_mode=fastbuild",
        "--incompatible_merge_genfiles_directory=false",
        "--incompatible_skip_genfiles_symlink=false");

    write(
        "targets/BUILD",
        "outgoing_transition_rule(name='configged1', deps=[':alternate1'])",
        "outgoing_transition_rule(name='configged2', deps=[':alternate2'])",
        "basic_rule(name='alternate1')",
        "incoming_transition_rule(name='alternate2', path='alternate_transition')");
    buildTarget("//targets:configged1", "//targets:configged2");

    assertThat(getConvenienceSymlinks())
        .containsExactly(
            "united-bin",
            getOutputPath()
                .getRelative("from_flag-" + getTargetConfiguration().getCpu() + "-fastbuild/bin"),
            "united-genfiles",
            getOutputPath()
                .getRelative(
                    "from_flag-" + getTargetConfiguration().getCpu() + "-fastbuild/genfiles"),
            "united-testlogs",
            getOutputPath()
                .getRelative(
                    "from_flag-" + getTargetConfiguration().getCpu() + "-fastbuild/testlogs"),
            "united-" + TestConstants.WORKSPACE_NAME,
            getExecRoot(),
            "united-out",
            getOutputPath());
  }

  @Test
  public void differentConfigurationSameOutputDirectory_setsSymlinks() throws Exception {
    // TODO(blaze-configurability-team): Remove when `--experimental_output_directory_naming_scheme`
    //    is universally set to `diff_from_baseline`
    addOptions(
        "--output_directory_name=from_flag",
        "--symlink_prefix=unchanged-",
        "--compilation_mode=fastbuild",
        "--incompatible_merge_genfiles_directory=false",
        "--incompatible_skip_genfiles_symlink=false",
        "--experimental_output_directory_naming_scheme=legacy");

    write(
        "targets/BUILD",
        "basic_rule(name='from_flag')",
        "incoming_unrelated_transition_rule(name='configged1', value='one_transition')",
        "incoming_unrelated_transition_rule(name='configged2', value='alternate_transition')");
    buildTarget("//targets:from_flag", "//targets:configged1", "//targets:configged2");

    assertThat(getConvenienceSymlinks())
        .containsExactly(
            "unchanged-bin",
            getOutputPath()
                .getRelative("from_flag-" + getTargetConfiguration().getCpu() + "-fastbuild/bin"),
            "unchanged-genfiles",
            getOutputPath()
                .getRelative(
                    "from_flag-" + getTargetConfiguration().getCpu() + "-fastbuild/genfiles"),
            "unchanged-testlogs",
            getOutputPath()
                .getRelative(
                    "from_flag-" + getTargetConfiguration().getCpu() + "-fastbuild/testlogs"),
            "unchanged-" + TestConstants.WORKSPACE_NAME,
            getExecRoot(),
            "unchanged-out",
            getOutputPath());
  }

  @Test
  public void nullConfigurationWithOtherMatchingOutputDir_setsSymlinks() throws Exception {
    addOptions(
        "--output_directory_name=from_flag",
        "--symlink_prefix=mixed-",
        "--compilation_mode=fastbuild",
        "--incompatible_merge_genfiles_directory=false",
        "--incompatible_skip_genfiles_symlink=false");

    write(
        "targets/BUILD",
        "exports_files(['null'])",
        "basic_rule(name='configured1')",
        "basic_rule(name='configured2')");
    write("targets/null", "This is just a test file to pretend to build.");
    buildTarget("//targets:null", "//targets:configured1", "//targets:configured2");

    assertThat(getConvenienceSymlinks())
        .containsExactly(
            "mixed-bin",
            getOutputPath()
                .getRelative("from_flag-" + getTargetConfiguration().getCpu() + "-fastbuild/bin"),
            "mixed-genfiles",
            getOutputPath()
                .getRelative(
                    "from_flag-" + getTargetConfiguration().getCpu() + "-fastbuild/genfiles"),
            "mixed-testlogs",
            getOutputPath()
                .getRelative(
                    "from_flag-" + getTargetConfiguration().getCpu() + "-fastbuild/testlogs"),
            "mixed-" + TestConstants.WORKSPACE_NAME,
            getExecRoot(),
            "mixed-out",
            getOutputPath());
  }

  @Test
  public void settingSymlinksReplacesSymlinksAlreadyPresent() throws Exception {
    addOptions(
        "--output_directory_name=from_flag",
        "--symlink_prefix=replaced-",
        "--compilation_mode=fastbuild",
        "--incompatible_skip_genfiles_symlink=false");

    Path binLink = getWorkspace().getChild("replaced-bin");
    Path genfilesLink = getWorkspace().getChild("replaced-genfiles");
    Path testlogsLink = getWorkspace().getChild("replaced-testlogs");
    Path workspaceLink = getWorkspace().getChild("replaced-" + TestConstants.WORKSPACE_NAME);
    Path outLink = getWorkspace().getChild("replaced-out");

    PathFragment original = getOutputPath().getRelative("original/destination").asFragment();
    binLink.createSymbolicLink(original);
    genfilesLink.createSymbolicLink(original);
    testlogsLink.createSymbolicLink(original);
    workspaceLink.createSymbolicLink(original);
    outLink.createSymbolicLink(original);

    write("target/BUILD", "basic_rule(name='target')");
    buildTarget("//target:target");

    // Implicitly test for symlink-ness; readSymbolicLink would throw if they are not symlinks.
    assertThat(binLink.readSymbolicLink()).isNotEqualTo(original);
    assertThat(genfilesLink.readSymbolicLink()).isNotEqualTo(original);
    assertThat(testlogsLink.readSymbolicLink()).isNotEqualTo(original);
    assertThat(workspaceLink.readSymbolicLink()).isNotEqualTo(original);
    assertThat(outLink.readSymbolicLink()).isNotEqualTo(original);
  }

  @Test
  public void settingSymlinksCreatesSymlinksIfNotAlreadyPresent() throws Exception {
    addOptions(
        "--output_directory_name=from_flag",
        "--symlink_prefix=created-",
        "--compilation_mode=fastbuild",
        "--incompatible_skip_genfiles_symlink=false");

    write("target/BUILD", "basic_rule(name='target')");
    buildTarget("//target:target");

    assertThat(getWorkspace().getChild("created-bin").isSymbolicLink()).isTrue();
    assertThat(getWorkspace().getChild("created-genfiles").isSymbolicLink()).isTrue();
    assertThat(getWorkspace().getChild("created-testlogs").isSymbolicLink()).isTrue();
    assertThat(getWorkspace().getChild("created-" + TestConstants.WORKSPACE_NAME).isSymbolicLink())
        .isTrue();
    assertThat(getWorkspace().getChild("created-out").isSymbolicLink()).isTrue();
  }

  @Test
  public void genfilesLink_omittedWithIncompatibleFlag() throws Exception {
    addOptions(
        "--output_directory_name=from_flag",
        "--symlink_prefix=prefix-",
        "--compilation_mode=fastbuild",
        "--incompatible_skip_genfiles_symlink=true");

    // Simulate leftover symlink from prior build.
    Path config = getOutputPath().getRelative("some-imaginary-config");
    Path genfilesLink = getWorkspace().getChild("prefix-genfiles");
    genfilesLink.createSymbolicLink(config.getChild("genfiles"));

    write("target/BUILD", "basic_rule(name='target')");
    buildTarget("//target:target");

    assertThat(getWorkspace().getChild("prefix-genfiles").isSymbolicLink()).isFalse();
  }

  @Test
  public void genfilesLink_presentWithoutIncompatibleFlag() throws Exception {
    addOptions(
        "--output_directory_name=from_flag",
        "--symlink_prefix=prefix-",
        "--compilation_mode=fastbuild",
        "--incompatible_skip_genfiles_symlink=false");

    write("target/BUILD", "basic_rule(name='target')");
    buildTarget("//target:target");

    assertThat(getWorkspace().getChild("prefix-genfiles").isSymbolicLink()).isTrue();
  }

  @Test
  public void settingSymlinksDoesNotReplaceNormalFilesAlreadyPresent() throws Exception {
    addOptions(
        "--output_directory_name=from_flag",
        "--symlink_prefix=blocked-",
        "--compilation_mode=fastbuild");

    Path binLink = getWorkspace().getChild("blocked-bin");
    Path genfilesLink = getWorkspace().getChild("blocked-genfiles");
    Path testlogsLink = getWorkspace().getChild("blocked-testlogs");
    Path workspaceLink = getWorkspace().getChild("blocked-" + TestConstants.WORKSPACE_NAME);
    Path outLink = getWorkspace().getChild("blocked-out");

    FileSystemUtils.writeIsoLatin1(binLink, "this file is not a symlink");
    FileSystemUtils.writeIsoLatin1(genfilesLink, "this file is not a symlink");
    FileSystemUtils.writeIsoLatin1(testlogsLink, "this file is not a symlink");
    FileSystemUtils.writeIsoLatin1(workspaceLink, "this file is not a symlink");
    FileSystemUtils.writeIsoLatin1(outLink, "this file is not a symlink");

    write("target/BUILD", "basic_rule(name='target')");
    buildTarget("//target:target");

    assertThat(binLink.isFile(Symlinks.NOFOLLOW)).isTrue();
    assertThat(genfilesLink.isFile(Symlinks.NOFOLLOW)).isTrue();
    assertThat(testlogsLink.isFile(Symlinks.NOFOLLOW)).isTrue();
    assertThat(workspaceLink.isFile(Symlinks.NOFOLLOW)).isTrue();
    assertThat(outLink.isFile(Symlinks.NOFOLLOW)).isTrue();
  }

  @Test
  public void settingSymlinksDoesNotReplaceDirectoriesAlreadyPresent() throws Exception {
    addOptions(
        "--output_directory_name=from_flag",
        "--symlink_prefix=blocked-",
        "--compilation_mode=fastbuild");

    Path binLink = getWorkspace().getChild("blocked-bin");
    Path genfilesLink = getWorkspace().getChild("blocked-genfiles");
    Path testlogsLink = getWorkspace().getChild("blocked-testlogs");
    Path workspaceLink = getWorkspace().getChild("blocked-" + TestConstants.WORKSPACE_NAME);
    Path outLink = getWorkspace().getChild("blocked-out");

    binLink.createDirectory();
    genfilesLink.createDirectory();
    testlogsLink.createDirectory();
    workspaceLink.createDirectory();
    outLink.createDirectory();

    write("target/BUILD", "basic_rule(name='target')");
    buildTarget("//target:target");

    assertThat(binLink.isDirectory(Symlinks.NOFOLLOW)).isTrue();
    assertThat(genfilesLink.isDirectory(Symlinks.NOFOLLOW)).isTrue();
    assertThat(testlogsLink.isDirectory(Symlinks.NOFOLLOW)).isTrue();
    assertThat(workspaceLink.isDirectory(Symlinks.NOFOLLOW)).isTrue();
    assertThat(outLink.isDirectory(Symlinks.NOFOLLOW)).isTrue();
  }

  @Test
  public void settingSymlinksReplacesSymlinksEvenIfNotPointingInsideExecRoot() throws Exception {
    addOptions(
        "--output_directory_name=from_flag",
        "--symlink_prefix=replaced-",
        "--compilation_mode=fastbuild",
        "--incompatible_merge_genfiles_directory=false",
        "--incompatible_skip_genfiles_symlink=false");

    Path binLink = getWorkspace().getChild("replaced-bin");
    Path genfilesLink = getWorkspace().getChild("replaced-genfiles");
    Path testlogsLink = getWorkspace().getChild("replaced-testlogs");
    Path workspaceLink = getWorkspace().getChild("replaced-" + TestConstants.WORKSPACE_NAME);
    Path outLink = getWorkspace().getChild("replaced-out");

    Path original = getWorkspace().getRelative("/arbitrary/somewhere/else/in/the/filesystem");
    binLink.createSymbolicLink(original);
    genfilesLink.createSymbolicLink(original);
    testlogsLink.createSymbolicLink(original);
    workspaceLink.createSymbolicLink(original);
    outLink.createSymbolicLink(original);

    write("target/BUILD", "basic_rule(name='target')");
    buildTarget("//target:target");

    // Implicitly test for symlink-ness; readSymbolicLink would throw if they are not symlinks.
    assertThat(binLink.readSymbolicLink())
        .isEqualTo(
            getOutputPath()
                .getRelative("from_flag-" + getTargetConfiguration().getCpu() + "-fastbuild/bin")
                .asFragment());
    assertThat(genfilesLink.readSymbolicLink())
        .isEqualTo(
            getOutputPath()
                .getRelative(
                    "from_flag-" + getTargetConfiguration().getCpu() + "-fastbuild/genfiles")
                .asFragment());
    assertThat(testlogsLink.readSymbolicLink())
        .isEqualTo(
            getOutputPath()
                .getRelative(
                    "from_flag-" + getTargetConfiguration().getCpu() + "-fastbuild/testlogs")
                .asFragment());
    assertThat(workspaceLink.readSymbolicLink()).isEqualTo(getExecRoot().asFragment());
    assertThat(outLink.readSymbolicLink()).isEqualTo(getOutputPath().asFragment());
  }

  @Test
  public void settingSymlinksCreatesDirectoriesIfNeeded() throws Exception {
    addOptions(
        "--output_directory_name=from_flag",
        "--symlink_prefix=created/",
        "--compilation_mode=fastbuild",
        "--incompatible_skip_genfiles_symlink=false");

    write("target/BUILD", "basic_rule(name='target')");
    buildTarget("//target:target");

    assertThat(getWorkspace().getChild("created").isDirectory()).isTrue();
    assertThat(getWorkspace().getRelative("created/bin").isSymbolicLink()).isTrue();
    assertThat(getWorkspace().getRelative("created/genfiles").isSymbolicLink()).isTrue();
    assertThat(getWorkspace().getRelative("created/testlogs").isSymbolicLink()).isTrue();
    assertThat(
            getWorkspace().getRelative("created/" + TestConstants.WORKSPACE_NAME).isSymbolicLink())
        .isTrue();
    assertThat(getWorkspace().getRelative("created/out").isSymbolicLink()).isTrue();
  }

  @Test
  public void settingSymlinksDoesNothingWhenParentExistsAndIsNotADirectory() throws Exception {
    addOptions(
        "--output_directory_name=from_flag",
        "--symlink_prefix=blocked/",
        "--compilation_mode=fastbuild");

    Path parentDir = getWorkspace().getChild("blocked");
    FileSystemUtils.writeIsoLatin1(parentDir, "this file is not a directory");

    write("target/BUILD", "basic_rule(name='target')");
    buildTarget("//target:target");

    assertThat(getWorkspace().getChild("blocked").isFile(Symlinks.NOFOLLOW)).isTrue();
  }

  @Test
  public void settingSymlinksUsesExistingOrPopulatedParentDirectoryAsNormal() throws Exception {
    addOptions(
        "--output_directory_name=from_flag",
        "--symlink_prefix=cooperating/",
        "--compilation_mode=fastbuild",
        "--incompatible_skip_genfiles_symlink=false");
    write("target/BUILD", "basic_rule(name='target')");
    write("cooperating/original", "this file makes the directory come to life");
    buildTarget("//target:target");

    assertThat(getWorkspace().getChild("cooperating").isDirectory()).isTrue();
    assertThat(getWorkspace().getRelative("cooperating/bin").isSymbolicLink()).isTrue();
    assertThat(getWorkspace().getRelative("cooperating/genfiles").isSymbolicLink()).isTrue();
    assertThat(getWorkspace().getRelative("cooperating/testlogs").isSymbolicLink()).isTrue();
    assertThat(
            getWorkspace()
                .getRelative("cooperating/" + TestConstants.WORKSPACE_NAME)
                .isSymbolicLink())
        .isTrue();
    assertThat(getWorkspace().getRelative("cooperating/out").isSymbolicLink()).isTrue();
    assertThat(getWorkspace().getRelative("cooperating/original").isFile()).isTrue();
  }

  @Test
  public void settingSymlinksIgnoresSymlinksWithDifferentPrefix() throws Exception {
    addOptions(
        "--output_directory_name=from_flag",
        "--symlink_prefix=new-prefix-",
        "--compilation_mode=fastbuild");
    Path binLink = getWorkspace().getChild("other-prefix-bin");
    Path genfilesLink = getWorkspace().getChild("other-prefix-genfiles");
    Path testlogsLink = getWorkspace().getChild("other-prefix-testlogs");
    Path workspaceLink = getWorkspace().getChild("other-prefix-" + TestConstants.WORKSPACE_NAME);
    Path outLink = getWorkspace().getChild("other-prefix-out");

    PathFragment original = getOutputPath().getRelative("original/destination").asFragment();
    binLink.createSymbolicLink(original);
    genfilesLink.createSymbolicLink(original);
    testlogsLink.createSymbolicLink(original);
    workspaceLink.createSymbolicLink(original);
    outLink.createSymbolicLink(original);

    write("target/BUILD", "basic_rule(name='target')");
    buildTarget("//target:target");

    // Implicitly test for symlink-ness; readSymbolicLink would throw if they are not symlinks.
    assertThat(binLink.readSymbolicLink()).isEqualTo(original);
    assertThat(genfilesLink.readSymbolicLink()).isEqualTo(original);
    assertThat(testlogsLink.readSymbolicLink()).isEqualTo(original);
    assertThat(workspaceLink.readSymbolicLink()).isEqualTo(original);
    assertThat(outLink.readSymbolicLink()).isEqualTo(original);
  }

  @Test
  public void unsettingSymlinksRemovesConfigurationSymlinksIfAlreadyPresent() throws Exception {
    addOptions(
        "--output_directory_name=from_flag",
        "--symlink_prefix=deleted-",
        "--compilation_mode=fastbuild",
        "--incompatible_skip_genfiles_symlink=false");
    Path binLink = getWorkspace().getChild("deleted-bin");
    Path genfilesLink = getWorkspace().getChild("deleted-genfiles");
    Path testlogsLink = getWorkspace().getChild("deleted-testlogs");
    Path workspaceLink = getWorkspace().getChild("deleted-" + TestConstants.WORKSPACE_NAME);
    Path outLink = getWorkspace().getChild("deleted-out");

    Path config = getOutputPath().getRelative("some-imaginary-config");
    // put symlinks at the convenience symlinks spots to simulate a prior build
    binLink.createSymbolicLink(config.getChild("bin"));
    genfilesLink.createSymbolicLink(config.getChild("genfiles"));
    testlogsLink.createSymbolicLink(config.getChild("testlogs"));

    write("file/BUILD", "exports_files(['file'])");
    write("file/file", "this is just a file to pretend to build");
    buildTarget("//file:file");

    assertThat(binLink.exists(Symlinks.NOFOLLOW)).isFalse();
    assertThat(genfilesLink.exists(Symlinks.NOFOLLOW)).isFalse();
    assertThat(testlogsLink.exists(Symlinks.NOFOLLOW)).isFalse();

    assertThat(workspaceLink.isSymbolicLink()).isTrue();
    assertThat(outLink.isSymbolicLink()).isTrue();
  }

  @Test
  public void unsettingSymlinksSucceedsIfNotAlreadyPresent() throws Exception {
    addOptions(
        "--output_directory_name=from_flag",
        "--symlink_prefix=already-absent-",
        "--compilation_mode=fastbuild");
    Path binLink = getWorkspace().getChild("already-absent-bin");
    Path genfilesLink = getWorkspace().getChild("already-absent-genfiles");
    Path testlogsLink = getWorkspace().getChild("already-absent-testlogs");
    Path workspaceLink = getWorkspace().getChild("already-absent-" + TestConstants.WORKSPACE_NAME);
    Path outLink = getWorkspace().getChild("already-absent-out");

    write("file/BUILD", "exports_files(['file'])");
    write("file/file", "this is just a file to pretend to build");
    buildTarget("//file:file");

    assertThat(binLink.exists(Symlinks.NOFOLLOW)).isFalse();
    assertThat(genfilesLink.exists(Symlinks.NOFOLLOW)).isFalse();
    assertThat(testlogsLink.exists(Symlinks.NOFOLLOW)).isFalse();

    assertThat(workspaceLink.isSymbolicLink()).isTrue();
    assertThat(outLink.isSymbolicLink()).isTrue();
  }

  @Test
  public void unsettingSymlinksDoesNotRemoveNormalFilesAlreadyPresent() throws Exception {
    addOptions(
        "--output_directory_name=from_flag",
        "--symlink_prefix=blocked-",
        "--compilation_mode=fastbuild");
    Path binLink = getWorkspace().getChild("blocked-bin");
    Path genfilesLink = getWorkspace().getChild("blocked-genfiles");
    Path testlogsLink = getWorkspace().getChild("blocked-testlogs");
    Path workspaceLink = getWorkspace().getChild("blocked-" + TestConstants.WORKSPACE_NAME);
    Path outLink = getWorkspace().getChild("blocked-out");

    FileSystemUtils.writeIsoLatin1(binLink, "this file is not a symlink");
    FileSystemUtils.writeIsoLatin1(genfilesLink, "this file is not a symlink");
    FileSystemUtils.writeIsoLatin1(testlogsLink, "this file is not a symlink");
    FileSystemUtils.writeIsoLatin1(workspaceLink, "this file is not a symlink");
    FileSystemUtils.writeIsoLatin1(outLink, "this file is not a symlink");

    write("file/BUILD", "exports_files(['file'])");
    write("file/file", "this is just a file to pretend to build");
    buildTarget("//file:file");

    assertThat(binLink.isFile(Symlinks.NOFOLLOW)).isTrue();
    assertThat(genfilesLink.isFile(Symlinks.NOFOLLOW)).isTrue();
    assertThat(testlogsLink.isFile(Symlinks.NOFOLLOW)).isTrue();
    assertThat(workspaceLink.isFile(Symlinks.NOFOLLOW)).isTrue();
    assertThat(outLink.isFile(Symlinks.NOFOLLOW)).isTrue();
  }

  @Test
  public void unsettingSymlinksDoesNotRemoveDirectoriesAlreadyPresent() throws Exception {
    addOptions(
        "--output_directory_name=from_flag",
        "--symlink_prefix=blocked-",
        "--compilation_mode=fastbuild");
    Path binLink = getWorkspace().getChild("blocked-bin");
    Path genfilesLink = getWorkspace().getChild("blocked-genfiles");
    Path testlogsLink = getWorkspace().getChild("blocked-testlogs");
    Path workspaceLink = getWorkspace().getChild("blocked-" + TestConstants.WORKSPACE_NAME);
    Path outLink = getWorkspace().getChild("blocked-out");

    binLink.createDirectory();
    genfilesLink.createDirectory();
    testlogsLink.createDirectory();
    workspaceLink.createDirectory();
    outLink.createDirectory();

    write("file/BUILD", "exports_files(['file'])");
    write("file/file", "this is just a file to pretend to build");
    buildTarget("//file:file");

    assertThat(binLink.isDirectory(Symlinks.NOFOLLOW)).isTrue();
    assertThat(genfilesLink.isDirectory(Symlinks.NOFOLLOW)).isTrue();
    assertThat(testlogsLink.isDirectory(Symlinks.NOFOLLOW)).isTrue();
    assertThat(workspaceLink.isDirectory(Symlinks.NOFOLLOW)).isTrue();
    assertThat(outLink.isDirectory(Symlinks.NOFOLLOW)).isTrue();
  }

  @Test
  public void unsettingSymlinksRemovesSymlinksEvenIfNotPointingInsideExecRoot() throws Exception {
    addOptions(
        "--output_directory_name=from_flag",
        "--symlink_prefix=deleted-",
        "--compilation_mode=fastbuild",
        "--incompatible_skip_genfiles_symlink=false");
    Path binLink = getWorkspace().getChild("deleted-bin");
    Path genfilesLink = getWorkspace().getChild("deleted-genfiles");
    Path testlogsLink = getWorkspace().getChild("deleted-testlogs");
    Path workspaceLink = getWorkspace().getChild("deleted-" + TestConstants.WORKSPACE_NAME);
    Path outLink = getWorkspace().getChild("deleted-out");

    Path original = getWorkspace().getRelative("/arbitrary/somewhere/else/in/the/filesystem");
    binLink.createSymbolicLink(original);
    genfilesLink.createSymbolicLink(original);
    testlogsLink.createSymbolicLink(original);
    workspaceLink.createSymbolicLink(original);
    outLink.createSymbolicLink(original);

    write("file/BUILD", "exports_files(['file'])");
    write("file/file", "this is just a file to pretend to build");
    buildTarget("//file:file");

    assertThat(binLink.exists(Symlinks.NOFOLLOW)).isFalse();
    assertThat(genfilesLink.exists(Symlinks.NOFOLLOW)).isFalse();
    assertThat(testlogsLink.exists(Symlinks.NOFOLLOW)).isFalse();

    assertThat(workspaceLink.isSymbolicLink()).isTrue();
    assertThat(outLink.isSymbolicLink()).isTrue();
  }

  @Test
  public void unsettingSymlinksIgnoresSymlinksWithDifferentPrefix() throws Exception {
    addOptions(
        "--output_directory_name=from_flag",
        "--symlink_prefix=new-prefix-",
        "--compilation_mode=fastbuild");
    Path binLink = getWorkspace().getChild("other-prefix-bin");
    Path genfilesLink = getWorkspace().getChild("other-prefix-genfiles");
    Path testlogsLink = getWorkspace().getChild("other-prefix-testlogs");
    Path workspaceLink = getWorkspace().getChild("other-prefix-" + TestConstants.WORKSPACE_NAME);
    Path outLink = getWorkspace().getChild("other-prefix-out");

    PathFragment original = getOutputPath().getRelative("original/destination").asFragment();
    binLink.createSymbolicLink(original);
    genfilesLink.createSymbolicLink(original);
    testlogsLink.createSymbolicLink(original);
    workspaceLink.createSymbolicLink(original);
    outLink.createSymbolicLink(original);

    write("file/BUILD", "exports_files(['file'])");
    write("file/file", "this is just a file to pretend to build");
    buildTarget("//file:file");

    // Implicitly test for symlink-ness; readSymbolicLink would throw if they are not symlinks.
    assertThat(binLink.readSymbolicLink()).isEqualTo(original);
    assertThat(genfilesLink.readSymbolicLink()).isEqualTo(original);
    assertThat(testlogsLink.readSymbolicLink()).isEqualTo(original);
    assertThat(workspaceLink.readSymbolicLink()).isEqualTo(original);
    assertThat(outLink.readSymbolicLink()).isEqualTo(original);
  }

  @Test
  public void symlinkPrefix_specialNoCreateValue_doesNotCreateOrDeleteSymlinks() throws Exception {
    addOptions("--symlink_prefix=/");

    write("foo/BUILD", "exports_files(['bar.txt'])");
    write("foo/bar.txt", "This is just a test file to pretend to build.");

    // This will be a preexisting symlink and when --symlink_prefix=/ is used, assert that this
    // preexisting symlink still exists.
    Path binLink = getWorkspace().getChild("blaze-" + TestConstants.WORKSPACE_NAME);
    binLink.createSymbolicLink(PathFragment.create("foo/"));

    buildTarget("//foo:bar.txt");

    ImmutableMap<String, Path> symlinks = getConvenienceSymlinks();
    assertThat(symlinks).containsKey("blaze-" + TestConstants.WORKSPACE_NAME);
  }

  @Test
  public void convenienceSymlinks_ignore_leaveSymlinksUntouched() throws Exception {
    addOptions("--experimental_convenience_symlinks=ignore");

    write("foo/BUILD", "exports_files(['bar.txt'])");
    write("foo/bar.txt", "This is just a test file to pretend to build.");
    buildTarget("//foo:bar.txt");

    // This will be a preexisting symlink that will remain after the build
    Path binLink = getWorkspace().getChild("blaze-" + TestConstants.WORKSPACE_NAME);
    binLink.createSymbolicLink(PathFragment.create("foo/"));

    ImmutableMap<String, Path> symlinks = getConvenienceSymlinks();
    assertThat(symlinks).containsKey("blaze-" + TestConstants.WORKSPACE_NAME);
  }

  @Test
  public void convenienceSymlinks_normal_createSymlinks() throws Exception {
    addOptions("--symlink_prefix=test-", "--experimental_convenience_symlinks=normal");

    write("foo/BUILD", "exports_files(['bar.txt'])");
    write("foo/bar.txt", "This is just a test file to pretend to build.");
    buildTarget("//foo:bar.txt");

    ImmutableMap<String, Path> symlinks = getConvenienceSymlinks();
    assertThat(symlinks).containsKey("test-" + TestConstants.WORKSPACE_NAME);
    assertThat(symlinks).containsKey("test-out");
  }

  @Test
  public void convenienceSymlinks_clean_deletesAndDoesNotCreateSymlinks() throws Exception {
    addOptions("--symlink_prefix=test-", "--experimental_convenience_symlinks=clean");

    write("foo/BUILD", "exports_files(['bar.txt'])");
    write("foo/bar.txt", "This is just a test file to pretend to build.");

    // This will be a preexisting symlink that will be deleted after the build
    Path binLink = getWorkspace().getChild("test-" + TestConstants.WORKSPACE_NAME);
    binLink.createSymbolicLink(PathFragment.create("foo"));

    buildTarget("//foo:bar.txt");

    ImmutableMap<String, Path> symlinks = getConvenienceSymlinks();
    assertThat(symlinks).doesNotContainKey("test-" + TestConstants.WORKSPACE_NAME);
    assertThat(symlinks).doesNotContainKey("test-out");
  }
}
