diff --git a/src/test/java/com/google/devtools/build/lib/buildtool/BUILD b/src/test/java/com/google/devtools/build/lib/buildtool/BUILD index c604a800700a67..3c86a20180776f 100644 --- a/src/test/java/com/google/devtools/build/lib/buildtool/BUILD +++ b/src/test/java/com/google/devtools/build/lib/buildtool/BUILD @@ -675,3 +675,37 @@ java_test( "@com_google_testparameterinjector//:testparameterinjector", ], ) + +java_test( + name = "ConvenienceSymlinkTest", + srcs = [ + "ConvenienceSymlinkTest.java", + ], + tags = ["no_windows"], # Creates symbolic links. + deps = [ + "//src/main/java/com/google/devtools/build/lib:runtime", + "//src/main/java/com/google/devtools/build/lib/analysis:actions/file_write_action_context", + "//src/main/java/com/google/devtools/build/lib/analysis:analysis_cluster", + "//src/main/java/com/google/devtools/build/lib/analysis:config/build_options", + "//src/main/java/com/google/devtools/build/lib/analysis:config/fragment", + "//src/main/java/com/google/devtools/build/lib/analysis:config/fragment_options", + "//src/main/java/com/google/devtools/build/lib/analysis:config/transition_factories", + "//src/main/java/com/google/devtools/build/lib/analysis:config/transitions/patch_transition", + "//src/main/java/com/google/devtools/build/lib/analysis:config/transitions/transition_factory", + "//src/main/java/com/google/devtools/build/lib/cmdline", + "//src/main/java/com/google/devtools/build/lib/events", + "//src/main/java/com/google/devtools/build/lib/exec:file_write_strategy", + "//src/main/java/com/google/devtools/build/lib/exec:module_action_context_registry", + "//src/main/java/com/google/devtools/build/lib/packages", + "//src/main/java/com/google/devtools/build/lib/vfs", + "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment", + "//src/main/java/com/google/devtools/common/options", + "//src/test/java/com/google/devtools/build/lib/analysis/util", + "//src/test/java/com/google/devtools/build/lib/buildtool/util", + "//src/test/java/com/google/devtools/build/lib/testutil:TestConstants", + "//third_party:guava", + "//third_party:junit4", + "//third_party:truth", + "@com_google_testparameterinjector//:testparameterinjector", + ], +) diff --git a/src/test/java/com/google/devtools/build/lib/buildtool/ConvenienceSymlinkTest.java b/src/test/java/com/google/devtools/build/lib/buildtool/ConvenienceSymlinkTest.java new file mode 100644 index 00000000000000..0f73d29ab80f9a --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/buildtool/ConvenienceSymlinkTest.java @@ -0,0 +1,1121 @@ +// 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> 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 { + @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> 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 { + @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( + // Turn on the new symlink behavior + "--use_top_level_targets_for_symlinks", + "--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 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_unsetsSymlinks() 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", + "basic_rule(name='default')", + "incoming_transition_rule(name='config1', path='set_from_config1')", + "incoming_transition_rule(name='config2', path='set_from_config2')"); + buildTarget("//targets:default", "//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 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 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 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 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 symlinks = getConvenienceSymlinks(); + assertThat(symlinks).doesNotContainKey("test-" + TestConstants.WORKSPACE_NAME); + assertThat(symlinks).doesNotContainKey("test-out"); + } +}