// Copyright 2015 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.lib.pkgcache;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;

import com.google.common.base.Functions;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Maps;
import com.google.common.collect.MoreCollectors;
import com.google.devtools.build.lib.actions.ActionKeyContext;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.analysis.BuildView;
import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
import com.google.devtools.build.lib.analysis.ServerDirectories;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
import com.google.devtools.build.lib.analysis.util.AnalysisMock;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.cmdline.TargetParsingException;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventKind;
import com.google.devtools.build.lib.events.ExtendedEventHandler.Postable;
import com.google.devtools.build.lib.events.StoredEventHandler;
import com.google.devtools.build.lib.packages.BuildFileContainsErrorsException;
import com.google.devtools.build.lib.packages.ConstantRuleVisibility;
import com.google.devtools.build.lib.packages.PackageFactory;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.packages.semantics.BuildLanguageOptions;
import com.google.devtools.build.lib.packages.util.MockToolsConfig;
import com.google.devtools.build.lib.rules.repository.RepositoryDelegatorFunction;
import com.google.devtools.build.lib.server.FailureDetails.PackageLoading;
import com.google.devtools.build.lib.skyframe.BazelSkyframeExecutorConstants;
import com.google.devtools.build.lib.skyframe.PatternExpandingError;
import com.google.devtools.build.lib.skyframe.PrecomputedValue;
import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
import com.google.devtools.build.lib.skyframe.TargetPatternPhaseValue;
import com.google.devtools.build.lib.testutil.ManualClock;
import com.google.devtools.build.lib.testutil.MoreAsserts;
import com.google.devtools.build.lib.testutil.SkyframeExecutorTestHelper;
import com.google.devtools.build.lib.util.DetailedExitCode;
import com.google.devtools.build.lib.util.ExitCode;
import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
import com.google.devtools.build.lib.vfs.DigestHashFunction;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.ModifiedFileSet;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.Root;
import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
import com.google.devtools.common.options.Options;
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.OptionsParsingException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for {@link SkyframeExecutor#loadTargetPatternsWithFilters}. */
@RunWith(JUnit4.class)
public class LoadingPhaseRunnerTest {

  private static final ImmutableList<Logger> loggers = ImmutableList.of(
      Logger.getLogger(BuildView.class.getName()));
  static {
    for (Logger logger : loggers) {
      logger.setLevel(Level.OFF);
    }
  }

  private LoadingPhaseTester tester;

  @Before
  public final void createLoadingPhaseTester() throws Exception  {
    tester = new LoadingPhaseTester();
  }

  private List<Label> getLabels(String... labels) throws Exception {
    List<Label> result = new ArrayList<>();
    for (String label : labels) {
      result.add(Label.parseAbsoluteUnchecked(label));
    }
    return result;
  }

  private void assertCircularSymlinksDuringTargetParsing(String targetPattern) throws Exception {
    assertThrows(TargetParsingException.class, () -> tester.load(targetPattern));
    tester.assertContainsError("circular symlinks detected");
    TargetPatternPhaseValue result = tester.loadKeepGoing(targetPattern);
    assertThat(result.hasError()).isTrue();
  }

  private TargetPatternPhaseValue assertNoErrors(TargetPatternPhaseValue loadingResult) {
    assertThat(loadingResult.hasError()).isFalse();
    assertThat(loadingResult.hasPostExpansionError()).isFalse();
    tester.assertNoEvents();
    return loadingResult;
  }

  @Test
  public void testSmoke() throws Exception {
    tester.addFile("base/BUILD",
        "filegroup(name = 'hello', srcs = ['foo.txt'])");
    TargetPatternPhaseValue loadingResult = assertNoErrors(tester.load("//base:hello"));
    assertThat(loadingResult.getTargetLabels())
        .containsExactlyElementsIn(getLabels("//base:hello"));
    assertThat(loadingResult.getTestsToRunLabels()).isNull();
  }

  @Test
  public void testNonExistentPackage() throws Exception {
    TargetPatternPhaseValue loadingResult = tester.loadKeepGoing("//base:missing");
    assertThat(loadingResult.hasError()).isTrue();
    assertThat(loadingResult.hasPostExpansionError()).isFalse();
    assertThat(loadingResult.getTargetLabels()).isEmpty();
    assertThat(loadingResult.getTestsToRunLabels()).isNull();
    tester.assertContainsError("Skipping '//base:missing': no such package 'base'");
    tester.assertContainsWarning("Target pattern parsing failed.");
    PatternExpandingError err = tester.findPostOnce(PatternExpandingError.class);
    assertThat(err.getPattern()).containsExactly("//base:missing");
  }

  @Test
  public void testNonExistentPackageWithoutKeepGoing() throws Exception {
    assertThrows(TargetParsingException.class, () -> tester.load("//does/not/exist"));
    PatternExpandingError err = tester.findPostOnce(PatternExpandingError.class);
    assertThat(err.getPattern()).containsExactly("//does/not/exist");
  }

  @Test
  public void testNonExistentTarget() throws Exception {
    tester.addFile("base/BUILD");
    TargetPatternPhaseValue loadingResult = tester.loadKeepGoing("//base:missing");
    assertThat(loadingResult.hasError()).isTrue();
    assertThat(loadingResult.hasPostExpansionError()).isFalse();
    assertThat(loadingResult.getTargetLabels()).isEmpty();
    assertThat(loadingResult.getTestsToRunLabels()).isNull();
    tester.assertContainsError("Skipping '//base:missing': no such target '//base:missing'");
    tester.assertContainsWarning("Target pattern parsing failed.");
    PatternExpandingError err = tester.findPostOnce(PatternExpandingError.class);
    assertThat(err.getPattern()).containsExactly("//base:missing");
  }

  @Test
  public void testExistingAndNonExistentTargetsWithKeepGoing() throws Exception {
    tester.addFile("base/BUILD",
        "filegroup(name = 'hello', srcs = ['foo.txt'])");
    tester.loadKeepGoing("//base:hello", "//base:missing");
    PatternExpandingError err = tester.findPostOnce(PatternExpandingError.class);
    assertThat(err.getPattern()).containsExactly("//base:missing");
    TargetParsingCompleteEvent event = tester.findPostOnce(TargetParsingCompleteEvent.class);
    assertThat(event.getOriginalTargetPattern()).containsExactly("//base:hello", "//base:missing");
    assertThat(event.getFailedTargetPatterns()).containsExactly("//base:missing");
  }

  @Test
  public void testRecursiveAllRules() throws Exception {
    tester.addFile("base/BUILD",
        "filegroup(name = 'base', srcs = ['base.txt'])");
    tester.addFile("base/foo/BUILD",
        "filegroup(name = 'foo', srcs = ['foo.txt'])");
    tester.addFile("base/bar/BUILD",
        "filegroup(name = 'bar', srcs = ['bar.txt'])");
    TargetPatternPhaseValue loadingResult = tester.load("//base/...");
    assertThat(loadingResult.getTargetLabels())
        .containsExactlyElementsIn(getLabels("//base", "//base/foo", "//base/bar"));

    loadingResult = tester.load("//base/bar/...");
    assertThat(loadingResult.getTargetLabels())
        .containsExactlyElementsIn(getLabels("//base/bar"));
  }

  @Test
  public void testRecursiveAllTargets() throws Exception {
    tester.addFile("base/BUILD",
        "filegroup(name = 'base', srcs = ['base.txt'])");
    tester.addFile("base/foo/BUILD",
        "filegroup(name = 'foo', srcs = ['foo.txt'])");
    tester.addFile("base/bar/BUILD",
        "filegroup(name = 'bar', srcs = ['bar.txt'])");
    TargetPatternPhaseValue loadingResult = tester.load("//base/...:*");
    assertThat(loadingResult.getTargetLabels())
        .containsExactlyElementsIn(
            getLabels(
                "//base:BUILD",
                "//base:base",
                "//base:base.txt",
                "//base/foo:BUILD",
                "//base/foo:foo",
                "//base/foo:foo.txt",
                "//base/bar:BUILD",
                "//base/bar:bar",
                "//base/bar:bar.txt"));

    loadingResult = tester.load("//base/...:all-targets");
    assertThat(loadingResult.getTargetLabels())
        .containsExactlyElementsIn(
            getLabels(
                "//base:BUILD",
                "//base:base",
                "//base:base.txt",
                "//base/foo:BUILD",
                "//base/foo:foo",
                "//base/foo:foo.txt",
                "//base/bar:BUILD",
                "//base/bar:bar",
                "//base/bar:bar.txt"));
  }

  @Test
  public void testNonExistentRecursive() throws Exception {
    TargetPatternPhaseValue loadingResult = tester.loadKeepGoing("//base/...");
    assertThat(loadingResult.hasError()).isTrue();
    assertThat(loadingResult.hasPostExpansionError()).isFalse();
    assertThat(loadingResult.getTargetLabels()).isEmpty();
    assertThat(loadingResult.getTestsToRunLabels()).isNull();
    tester.assertContainsError("Skipping '//base/...': no targets found beneath 'base'");
    tester.assertContainsWarning("Target pattern parsing failed.");
    PatternExpandingError err = tester.findPostOnce(PatternExpandingError.class);
    assertThat(err.getPattern()).containsExactly("//base/...");
  }

  @Test
  public void testMistypedTarget() throws Exception {
    TargetParsingException e =
        assertThrows(TargetParsingException.class, () -> tester.load("foo//bar:missing"));
    assertThat(e)
        .hasMessageThat()
        .contains(
            "invalid target format 'foo//bar:missing': "
                + "invalid package name 'foo//bar': "
                + "package names may not contain '//' path separators");
    ParsingFailedEvent err = tester.findPostOnce(ParsingFailedEvent.class);
    assertThat(err.getPattern()).isEqualTo("foo//bar:missing");
  }

  @Test
  public void testEmptyTarget() throws Exception {
    TargetParsingException e = assertThrows(TargetParsingException.class, () -> tester.load(""));
    assertThat(e).hasMessageThat().contains("the empty string is not a valid target");
  }

  @Test
  public void testMistypedTargetKeepGoing() throws Exception {
    TargetPatternPhaseValue result = tester.loadKeepGoing("foo//bar:missing");
    assertThat(result.hasError()).isTrue();
    tester.assertContainsError(
          "invalid target format 'foo//bar:missing': "
          + "invalid package name 'foo//bar': "
          + "package names may not contain '//' path separators");
    ParsingFailedEvent err = tester.findPostOnce(ParsingFailedEvent.class);
    assertThat(err.getPattern()).isEqualTo("foo//bar:missing");
  }

  @Test
  public void testBadTargetPatternWithTest() throws Exception {
    tester.addFile("base/BUILD");
    TargetPatternPhaseValue loadingResult = tester.loadTestsKeepGoing("//base:missing");
    assertThat(loadingResult.hasError()).isTrue();
    assertThat(loadingResult.hasPostExpansionError()).isFalse();
    assertThat(loadingResult.getTargetLabels()).isEmpty();
    assertThat(loadingResult.getTestsToRunLabels()).isEmpty();
    tester.assertContainsError("Skipping '//base:missing': no such target '//base:missing'");
    tester.assertContainsWarning("Target pattern parsing failed.");
  }

  @Test
  public void testManualTarget() throws Exception {
    AnalysisMock.get().ccSupport().setup(tester.mockToolsConfig);
    tester.addFile("cc/BUILD", "cc_library(name = 'my_lib', srcs = ['lib.cc'], tags = ['manual'])");
    TargetPatternPhaseValue loadingResult = assertNoErrors(tester.load("//cc:all"));
    assertThat(loadingResult.getTargetLabels()).containsExactlyElementsIn(getLabels());

    // Explicitly specified on the command line.
    loadingResult = assertNoErrors(tester.load("//cc:my_lib"));
    assertThat(loadingResult.getTargetLabels()).containsExactlyElementsIn(getLabels("//cc:my_lib"));
  }

  @Test
  public void testConfigSettingTarget() throws Exception {
    AnalysisMock.get().ccSupport().setup(tester.mockToolsConfig);
    tester.addFile("config/BUILD",
        "cc_library(name = 'somelib', srcs = [ 'somelib.cc' ], hdrs = [ 'somelib.h' ])",
        "config_setting(name = 'configa', values = { 'define': 'foo=a' })",
        "config_setting(name = 'configb', values = { 'define': 'foo=b' })");
    TargetPatternPhaseValue result = assertNoErrors(tester.load("//config:all"));
    assertThat(result.getTargetLabels()).containsExactlyElementsIn(getLabels("//config:somelib"));

    // Explicitly specified on the command line.
    result = assertNoErrors(tester.load("//config:configa"));
    assertThat(result.getTargetLabels()).containsExactlyElementsIn(getLabels("//config:configa"));
  }

  @Test
  public void testNegativeTestDoesNotShowUpAtAll() throws Exception {
    tester.addFile("my_test/BUILD",
        "sh_test(name = 'my_test', srcs = ['test.cc'])");
    assertNoErrors(tester.loadTests("-//my_test"));
    assertThat(tester.getFilteredTargets()).isEmpty();
    assertThat(tester.getTestFilteredTargets()).isEmpty();
  }

  @Test
  public void testNegativeTargetDoesNotShowUpAtAll() throws Exception {
    tester.addFile("my_library/BUILD",
        "cc_library(name = 'my_library', srcs = ['test.cc'])");
    assertNoErrors(tester.loadTests("-//my_library"));
    assertThat(tester.getFilteredTargets()).isEmpty();
    assertThat(tester.getTestFilteredTargets()).isEmpty();
  }

  @Test
  public void testTestMinusAllTests() throws Exception {
    tester.addFile(
        "test/BUILD",
        "cc_library(name = 'bar1')",
        "cc_test(name = 'test', deps = [':bar1'], tags = ['manual'])");
    TargetPatternPhaseValue result = tester.loadTests("//test:test", "-//test:all");
    assertThat(result.hasError()).isFalse();
    assertThat(result.hasPostExpansionError()).isFalse();
    tester.assertContainsWarning("All specified test targets were excluded by filters");
    assertThat(tester.getFilteredTargets()).containsExactlyElementsIn(getLabels("//test:test"));
    assertThat(result.getTargetLabels()).isEmpty();
  }

  @Test
  public void testFindLongestPrefix() throws Exception {
    tester.addFile("base/BUILD", "exports_files(['bar', 'bar/bar', 'bar/baz'])");
    TargetPatternPhaseValue result = assertNoErrors(tester.load("base/bar/baz"));
    assertThat(result.getTargetLabels()).containsExactlyElementsIn(getLabels("//base:bar/baz"));
    result = assertNoErrors(tester.load("base/bar"));
    assertThat(result.getTargetLabels()).containsExactlyElementsIn(getLabels("//base:bar"));
  }

  @Test
  public void testMultiSegmentLabel() throws Exception {
    tester.addFile("base/foo/BUILD", "exports_files(['bar/baz'])");
    TargetPatternPhaseValue value = assertNoErrors(tester.load("base/foo:bar/baz"));
    assertThat(value.getTargetLabels()).containsExactlyElementsIn(getLabels("//base/foo:bar/baz"));
  }

  @Test
  public void testMultiSegmentLabelRelative() throws Exception {
    tester.addFile("base/foo/BUILD", "exports_files(['bar/baz'])");
    tester.setRelativeWorkingDirectory("base");
    TargetPatternPhaseValue value = assertNoErrors(tester.load("foo:bar/baz"));
    assertThat(value.getTargetLabels()).containsExactlyElementsIn(getLabels("//base/foo:bar/baz"));
  }

  @Test
  public void testDeletedPackage() throws Exception {
    tester.addFile("base/BUILD", "exports_files(['base'])");
    tester.setDeletedPackages(PackageIdentifier.createInMainRepo("base"));
    TargetPatternPhaseValue result = tester.loadKeepGoing("//base");
    assertThat(result.hasError()).isTrue();
    tester.assertContainsError(
        "no such package 'base': Package is considered deleted due to --deleted_packages");
    ParsingFailedEvent err = tester.findPostOnce(ParsingFailedEvent.class);
    assertThat(err.getPattern()).isEqualTo("//base");
  }

  private void writeBuildFilesForTestFiltering() throws Exception {
    tester.addFile("tests/BUILD",
        "sh_test(name = 't1', srcs = ['pass.sh'], size= 'small', local=1)",
        "sh_test(name = 't2', srcs = ['pass.sh'], size = 'medium')",
        "sh_test(name = 't3', srcs = ['pass.sh'], tags = ['manual', 'local'])");
  }

  @Test
  public void testTestFiltering() throws Exception {
    writeBuildFilesForTestFiltering();
    TargetPatternPhaseValue loadingResult = assertNoErrors(tester.loadTests("//tests:all"));
    assertThat(loadingResult.getTargetLabels())
        .containsExactlyElementsIn(getLabels("//tests:t1", "//tests:t2"));
    assertThat(loadingResult.getTestsToRunLabels())
        .containsExactlyElementsIn(getLabels("//tests:t1", "//tests:t2"));
    assertThat(tester.getFilteredTargets()).isEmpty();
    assertThat(tester.getTestFilteredTargets()).isEmpty();
  }

  @Test
  public void testTestFilteringIncludingManual() throws Exception {
    writeBuildFilesForTestFiltering();
    tester.useLoadingOptions("--build_manual_tests");
    TargetPatternPhaseValue loadingResult = assertNoErrors(tester.loadTests("//tests:all"));
    assertThat(loadingResult.getTargetLabels())
        .containsExactlyElementsIn(getLabels("//tests:t1", "//tests:t2", "//tests:t3"));
    assertThat(loadingResult.getTestsToRunLabels())
        .containsExactlyElementsIn(getLabels("//tests:t1", "//tests:t2"));
    assertThat(tester.getFilteredTargets()).isEmpty();
    assertThat(tester.getTestFilteredTargets()).isEmpty();
  }

  @Test
  public void testTestFilteringBuildTestsOnly() throws Exception {
    writeBuildFilesForTestFiltering();
    tester.useLoadingOptions("--build_tests_only");
    TargetPatternPhaseValue result = assertNoErrors(tester.loadTests("//tests:all"));
    assertThat(result.getTargetLabels())
        .containsExactlyElementsIn(getLabels("//tests:t1", "//tests:t2"));
    assertThat(result.getTestsToRunLabels())
        .containsExactlyElementsIn(getLabels("//tests:t1", "//tests:t2"));
    assertThat(tester.getFilteredTargets()).isEmpty();
    assertThat(tester.getTestFilteredTargets()).isEmpty();
  }

  @Test
  public void testTestFilteringSize() throws Exception {
    writeBuildFilesForTestFiltering();
    tester.useLoadingOptions("--test_size_filters=small");
    TargetPatternPhaseValue result = assertNoErrors(tester.loadTests("//tests:all"));
    assertThat(result.getTargetLabels())
        .containsExactlyElementsIn(getLabels("//tests:t1", "//tests:t2"));
    assertThat(result.getTestsToRunLabels()).containsExactlyElementsIn(getLabels("//tests:t1"));
    assertThat(tester.getFilteredTargets()).isEmpty();
    assertThat(tester.getTestFilteredTargets()).isEmpty();
  }

  @Test
  public void testTestFilteringSizeAndBuildTestsOnly() throws Exception {
    writeBuildFilesForTestFiltering();
    tester.useLoadingOptions("--test_size_filters=small", "--build_tests_only");
    TargetPatternPhaseValue result = assertNoErrors(tester.loadTests("//tests:all"));
    assertThat(result.getTargetLabels()).containsExactlyElementsIn(getLabels("//tests:t1"));
    assertThat(result.getTestsToRunLabels()).containsExactlyElementsIn(getLabels("//tests:t1"));
    assertThat(tester.getFilteredTargets()).isEmpty();
    assertThat(tester.getTestFilteredTargets()).containsExactlyElementsIn(getLabels("//tests:t2"));
  }

  @Test
  public void testTestFilteringLocalAndBuildTestsOnly() throws Exception {
    writeBuildFilesForTestFiltering();
    tester.useLoadingOptions("--test_tag_filters=local", "--build_tests_only");
    TargetPatternPhaseValue result = assertNoErrors(tester.loadTests("//tests:all", "//tests:t3"));
    assertThat(result.getTargetLabels())
        .containsExactlyElementsIn(getLabels("//tests:t1", "//tests:t3"));
    assertThat(result.getTestsToRunLabels())
        .containsExactlyElementsIn(getLabels("//tests:t1", "//tests:t3"));
    assertThat(tester.getFilteredTargets()).isEmpty();
    assertThat(tester.getTestFilteredTargets()).containsExactlyElementsIn(getLabels("//tests:t2"));
  }

  @Test
  public void testTestSuiteExpansion() throws Exception {
    AnalysisMock.get().ccSupport().setup(tester.mockToolsConfig);
    tester.addFile("cc/BUILD",
        "cc_test(name = 'my_test', srcs = ['test.cc'])",
        "test_suite(name = 'tests', tests = [':my_test'])");
    TargetPatternPhaseValue loadingResult = assertNoErrors(tester.loadTests("//cc:tests"));
    assertThat(loadingResult.getTargetLabels())
        .containsExactlyElementsIn(getLabels("//cc:my_test"));
    assertThat(loadingResult.getTestsToRunLabels())
        .containsExactlyElementsIn(getLabels("//cc:my_test"));
    assertThat(tester.getOriginalTargets())
        .containsExactlyElementsIn(getLabels("//cc:tests", "//cc:my_test"));
    assertThat(tester.getTestSuiteTargets())
        .containsExactly(Label.parseAbsoluteUnchecked("//cc:tests"));
  }

  @Test
  public void testTestSuiteExpansionFails() throws Exception {
    tester.addFile("ts/BUILD",
        "test_suite(name = 'tests', tests = ['//nonexistent:my_test'])");
    tester.useLoadingOptions("--build_tests_only");
    TargetPatternPhaseValue loadingResult = tester.loadTestsKeepGoing("//ts:tests");
    assertThat(loadingResult.hasError()).isTrue();
    assertThat(loadingResult.hasPostExpansionError()).isFalse();
    tester.assertContainsError("no such package 'nonexistent'");
  }

  @Test
  public void testTestSuiteExpansionFailsForBuild() throws Exception {
    tester.addFile("ts/BUILD",
        "test_suite(name = 'tests', tests = [':nonexistent_test'])");
    TargetPatternPhaseValue loadingResult = tester.loadKeepGoing("//ts:tests");
    assertThat(loadingResult.hasError()).isFalse();
    assertThat(loadingResult.hasPostExpansionError()).isTrue();
    tester.assertContainsError(
        "expecting a test or a test_suite rule but '//ts:nonexistent_test' is not one");
  }

  @Test
  public void failureWhileLoadingTestsForTestSuiteKeepGoing() throws Exception {
    tester.addFile("ts/BUILD", "test_suite(name = 'tests', tests = ['//pkg:tests'])");
    tester.addFile("pkg/BUILD", "test_suite(name = 'tests')", "test_suite()");
    TargetPatternPhaseValue loadingResult = tester.loadKeepGoing("//ts:tests");
    assertThat(loadingResult.hasError()).isFalse();
    assertThat(loadingResult.hasPostExpansionError()).isTrue();
    tester.assertContainsError("test_suite rule has no 'name' attribute");
  }

  @Test
  public void failureWhileLoadingTestsForTestSuiteNoKeepGoing() throws Exception {
    tester.addFile("ts/BUILD", "test_suite(name = 'tests', tests = ['//pkg:tests'])");
    tester.addFile("pkg/BUILD", "test_suite(name = 'tests')", "test_suite()");
    TargetParsingException e =
        assertThrows(TargetParsingException.class, () -> tester.load("//ts:tests"));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo("error loading package 'pkg': Package 'pkg' contains errors");
    tester.assertContainsError("test_suite rule has no 'name' attribute");
  }

  @Test
  public void testTestSuiteExpansionFailsMissingTarget() throws Exception {
    tester.addFile("other/BUILD", "");
    tester.addFile("ts/BUILD",
        "test_suite(name = 'tests', tests = ['//other:no_such_test'])");
    TargetPatternPhaseValue result = tester.loadTestsKeepGoing("//ts:tests");
    assertThat(result.hasError()).isTrue();
    assertThat(result.hasPostExpansionError()).isTrue();
    tester.assertContainsError("no such target '//other:no_such_test'");
  }

  @Test
  public void testTestSuiteExpansionFailsMultipleSuites() throws Exception {
    tester.addFile("other/BUILD", "");
    tester.addFile("ts/BUILD",
        "test_suite(name = 'a', tests = ['//other:no_such_test'])",
        "test_suite(name = 'b', tests = [])");
    TargetPatternPhaseValue result = tester.loadTestsKeepGoing("//ts:all");
    assertThat(result.hasError()).isTrue();
    assertThat(result.hasPostExpansionError()).isTrue();
    tester.assertContainsError("no such target '//other:no_such_test'");
  }

  @Test
  public void testTestSuiteOverridesManualWithBuildTestsOnly() throws Exception {
    tester.addFile("foo/BUILD",
        "sh_test(name = 'foo', srcs = ['foo.sh'], tags = ['manual'])",
        "sh_test(name = 'bar', srcs = ['bar.sh'], tags = ['manual'])",
        "sh_test(name = 'baz', srcs = ['baz.sh'])",
        "test_suite(name = 'foo_suite', tests = [':foo', ':baz'])");
    tester.useLoadingOptions("--build_tests_only");
    TargetPatternPhaseValue result = assertNoErrors(tester.loadTests("//foo:all"));
    assertThat(result.getTargetLabels())
        .containsExactlyElementsIn(getLabels("//foo:foo", "//foo:baz"));
    assertThat(result.getTestsToRunLabels())
        .containsExactlyElementsIn(getLabels("//foo:foo", "//foo:baz"));
    assertThat(tester.getFilteredTargets()).isEmpty();
    assertThat(tester.getTestFilteredTargets())
        .containsExactlyElementsIn(getLabels("//foo:foo_suite"));
  }

  /** Regression test for bug: "subtracting tests from test doesn't work" */
  @Test
  public void testFilterNegativeTestFromTestSuite() throws Exception {
    AnalysisMock.get().ccSupport().setup(tester.mockToolsConfig);
    tester.addFile("cc/BUILD",
        "cc_test(name = 'my_test', srcs = ['test.cc'])",
        "cc_test(name = 'my_other_test', srcs = ['other_test.cc'])",
        "test_suite(name = 'tests', tests = [':my_test', ':my_other_test'])");
    TargetPatternPhaseValue result =
        assertNoErrors(tester.loadTests("//cc:tests", "-//cc:my_test"));
    assertThat(result.getTargetLabels())
        .containsExactlyElementsIn(getLabels("//cc:my_other_test", "//cc:my_test"));
    assertThat(result.getTestsToRunLabels())
        .containsExactlyElementsIn(getLabels("//cc:my_other_test"));
  }

  /** Regression test for bug: "blaze doesn't seem to respect target subtractions" */
  @Test
  public void testNegativeTestSuiteExpanded() throws Exception {
    AnalysisMock.get().ccSupport().setup(tester.mockToolsConfig);
    tester.addFile("cc/BUILD",
        "cc_test(name = 'my_test', srcs = ['test.cc'])",
        "cc_test(name = 'my_other_test', srcs = ['other_test.cc'])",
        "test_suite(name = 'tests', tests = [':my_test'])",
        "test_suite(name = 'all_tests', tests = ['my_other_test'])");
    TargetPatternPhaseValue result =
        assertNoErrors(tester.loadTests("//cc:all_tests", "-//cc:tests"));
    assertThat(result.getTargetLabels())
        .containsExactlyElementsIn(getLabels("//cc:my_other_test"));
    assertThat(result.getTestsToRunLabels())
        .containsExactlyElementsIn(getLabels("//cc:my_other_test"));
  }

  @Test
  public void testTestSuiteIsSubtracted() throws Exception {
    // Test suites are expanded for each target pattern in sequence, not the whole set of target
    // patterns after all the inclusions and exclusions are processed.
    AnalysisMock.get().ccSupport().setup(tester.mockToolsConfig);
    tester.addFile("cc/BUILD",
        "cc_test(name = 'my_test', srcs = ['test.cc'])",
        "cc_test(name = 'my_other_test', srcs = ['other_test.cc'])",
        "test_suite(name = 'tests', tests = [':my_test'])");
    TargetPatternPhaseValue result =
        assertNoErrors(tester.loadTests("//cc:all", "-//cc:tests"));
    assertThat(result.getTargetLabels())
        .containsExactlyElementsIn(getLabels("//cc:my_test", "//cc:my_other_test"));
    assertThat(result.getTestsToRunLabels())
        .containsExactlyElementsIn(getLabels("//cc:my_other_test"));
  }

  /** Regression test for bug: "blaze test "no targets found" warning now fatal" */
  @Test
  public void testNoTestsInRecursivePattern() throws Exception {
    AnalysisMock.get().ccSupport().setup(tester.mockToolsConfig);
    tester.addFile("foo/BUILD", "cc_library(name = 'foo', srcs = ['foo.cc'])");
    TargetPatternPhaseValue result =
        assertNoErrors(tester.loadTests("//foo/..."));
    assertThat(result.getTargetLabels())
        .containsExactlyElementsIn(getLabels("//foo"));
    assertThat(result.getTestsToRunLabels()).isEmpty();
  }

  @Test
  public void testComplexTestSuite() throws Exception {
    AnalysisMock.get().ccSupport().setup(tester.mockToolsConfig);
    tester.addFile("cc/BUILD",
        "cc_test(name = 'test1', srcs = ['test.cc'])",
        "cc_test(name = 'test2', srcs = ['test.cc'])",
        "test_suite(name = 'empty', tags = ['impossible'], tests = [])",
        "test_suite(name = 'suite1', tests = ['empty', 'test1'])",
        "test_suite(name = 'suite2', tests = ['test2'])",
        "test_suite(name = 'all_tests', tests = ['suite1', 'suite2'])");
    TargetPatternPhaseValue result = assertNoErrors(tester.loadTests("//cc:all_tests"));
    assertThat(result.getTargetLabels())
        .containsExactlyElementsIn(getLabels("//cc:test1", "//cc:test2"));
  }

  @Test
  public void testAllExcludesManualTest() throws Exception {
    AnalysisMock.get().ccSupport().setup(tester.mockToolsConfig);
    tester.addFile("cc/BUILD",
        "cc_test(name = 'my_test', srcs = ['test.cc'])",
        "cc_test(name = 'my_other_test', srcs = ['other_test.cc'], tags = ['manual'])");
    TargetPatternPhaseValue result = assertNoErrors(tester.loadTests("//cc:all"));
    assertThat(result.getTargetLabels())
        .containsExactlyElementsIn(getLabels("//cc:my_test"));
    assertThat(result.getTestsToRunLabels())
        .containsExactlyElementsIn(getLabels("//cc:my_test"));
  }

  @Test
  public void testBuildFilterDoesNotApplyToTests() throws Exception {
    tester.addFile(
        "foo/BUILD",
        "sh_test(name = 'foo', srcs = ['foo.sh'])",
        "sh_library(name = 'lib', srcs = ['lib.sh'])",
        "sh_library(name = 'nofoo', srcs = ['nofoo.sh'], tags = ['nofoo'])");
    tester.useLoadingOptions("--build_tag_filters=nofoo");
    TargetPatternPhaseValue result = assertNoErrors(tester.loadTests("//foo:all"));
    assertThat(result.getTargetLabels())
        .containsExactlyElementsIn(getLabels("//foo:foo", "//foo:nofoo"));
    assertThat(result.getTestsToRunLabels()).containsExactlyElementsIn(getLabels("//foo:foo"));
  }

  /**
   * Regression test for bug: "blaze is lying to me about what tests exist (have been specified)"
   */
  @Test
  public void testTotalNegationEmitsWarning() throws Exception {
    AnalysisMock.get().ccSupport().setup(tester.mockToolsConfig);
    tester.addFile("cc/BUILD",
        "cc_test(name = 'my_test', srcs = ['test.cc'])",
        "test_suite(name = 'tests', tests = [':my_test'])");
    TargetPatternPhaseValue result = tester.loadTests("//cc:tests", "-//cc:my_test");
    tester.assertContainsWarning("All specified test targets were excluded by filters");
    assertThat(result.getTestsToRunLabels()).containsExactlyElementsIn(getLabels());
  }

  @Test
  public void testRepeatedSameLoad() throws Exception {
    tester.addFile("base/BUILD",
        "filegroup(name = 'hello', srcs = ['foo.txt'])");
    TargetPatternPhaseValue firstResult = assertNoErrors(tester.load("//base:hello"));
    TargetPatternPhaseValue secondResult = assertNoErrors(tester.load("//base:hello"));
    assertThat(secondResult.getTargetLabels()).isEqualTo(firstResult.getTargetLabels());
    assertThat(secondResult.getTestsToRunLabels()).isEqualTo(firstResult.getTestsToRunLabels());
  }

  /**
   * Tests whether globs can update correctly when a new file is added.
   *
   * <p>The usage of {@link LoadingPhaseTester#sync()} triggers this via
   * {@link SkyframeExecutor#invalidateFilesUnderPathForTesting}.
   */
  @Test
  public void testGlobPicksUpNewFile() throws Exception {
    tester.addFile("foo/BUILD", "filegroup(name='x', srcs=glob(['*.y']))");
    tester.addFile("foo/a.y");
    Label label =
        Iterables.getOnlyElement(assertNoErrors(tester.load("//foo:x")).getTargetLabels());
    Target result = tester.getTarget(label.toString());
    assertThat(
        Iterables.transform(result.getAssociatedRule().getLabels(), Functions.toStringFunction()))
        .containsExactly("//foo:a.y");

    tester.addFile("foo/b.y");
    tester.sync();
    label = Iterables.getOnlyElement(assertNoErrors(tester.load("//foo:x")).getTargetLabels());
    result = tester.getTarget(label.toString());
    assertThat(
        Iterables.transform(result.getAssociatedRule().getLabels(), Functions.toStringFunction()))
        .containsExactly("//foo:a.y", "//foo:b.y");
  }

  /** Regression test: handle symlink cycles gracefully. */
  @Test
  public void testCycleReporting_symlinkCycleDuringTargetParsing() throws Exception {
    tester.addFile("hello/BUILD", "cc_library(name = 'a', srcs = glob(['*.cc']))");
    Path buildFilePath = tester.getWorkspace().getRelative("hello/BUILD");
    Path dirPath = buildFilePath.getParentDirectory();
    Path fooFilePath = dirPath.getRelative("foo.cc");
    Path barFilePath = dirPath.getRelative("bar.cc");
    Path bazFilePath = dirPath.getRelative("baz.cc");
    fooFilePath.createSymbolicLink(barFilePath);
    barFilePath.createSymbolicLink(bazFilePath);
    bazFilePath.createSymbolicLink(fooFilePath);
    assertCircularSymlinksDuringTargetParsing("//hello:a");
  }

  @Test
  public void testRecursivePatternWithCircularSymlink() throws Exception {
    tester.getWorkspace().getChild("broken").createDirectory();

    // Create a circular symlink.
    tester.getWorkspace().getRelative(PathFragment.create("broken/BUILD"))
        .createSymbolicLink(PathFragment.create("BUILD"));

    assertCircularSymlinksDuringTargetParsing("//broken/...");
  }

  @Test
  public void testRecursivePatternWithTwoCircularSymlinks() throws Exception {
    tester.getWorkspace().getChild("broken").createDirectory();

    // Create a circular symlink.
    tester.getWorkspace().getRelative(PathFragment.create("broken/BUILD"))
        .createSymbolicLink(PathFragment.create("x"));
    tester.getWorkspace().getRelative(PathFragment.create("broken/x"))
        .createSymbolicLink(PathFragment.create("BUILD"));

    assertCircularSymlinksDuringTargetParsing("//broken/...");
  }

  @Test
  public void testSuiteInSuite() throws Exception {
    tester.addFile("suite/BUILD",
        "test_suite(name = 'a', tests = [':b'])",
        "test_suite(name = 'b', tests = [':c'])",
        "sh_test(name = 'c', srcs = ['test.cc'])");
    TargetPatternPhaseValue result = assertNoErrors(tester.load("//suite:a"));
    assertThat(result.getTargetLabels()).containsExactlyElementsIn(getLabels("//suite:c"));
  }

  @Test
  public void testTopLevelTargetErrorsPrintedExactlyOnce_noKeepGoing() throws Exception {
    tester.addFile("bad/BUILD", "sh_binary(name = 'bad', srcs = ['bad.sh'])", "fail('some error')");
    assertThrows(TargetParsingException.class, () -> tester.load("//bad"));
    tester.assertContainsEventWithFrequency("some error", 1);
    PatternExpandingError err = tester.findPostOnce(PatternExpandingError.class);
    assertThat(err.getPattern()).containsExactly("//bad");
  }

  @Test
  public void testTopLevelTargetErrorsPrintedExactlyOnce_keepGoing() throws Exception {
    tester.addFile("bad/BUILD", "sh_binary(name = 'bad', srcs = ['bad.sh'])", "fail('some error')");
    TargetPatternPhaseValue result = tester.loadKeepGoing("//bad");
    assertThat(result.hasError()).isTrue();
    tester.assertContainsEventWithFrequency("some error", 1);
  }

  @Test
  public void testCompileOneDependency() throws Exception {
    tester.addFile("base/BUILD",
        "cc_library(name = 'hello', srcs = ['hello.cc'])");
    tester.useLoadingOptions("--compile_one_dependency");
    TargetPatternPhaseValue result = assertNoErrors(tester.load("base/hello.cc"));
    assertThat(result.getTargetLabels()).containsExactlyElementsIn(getLabels("//base:hello"));
  }

  @Test
  public void testCompileOneDependencyNonExistentSource() throws Exception {
    tester.addFile("base/BUILD",
        "cc_library(name = 'hello', srcs = ['hello.cc', '//bad:bad.cc'])");
    tester.useLoadingOptions("--compile_one_dependency");
    try {
      TargetPatternPhaseValue loadingResult = tester.load("base/hello.cc");
      assertThat(loadingResult.hasPostExpansionError()).isFalse();
    } catch (LoadingFailedException expected) {
      tester.assertContainsError("no such package 'bad'");
    }
  }

  @Test
  public void testCompileOneDependencyNonExistentSourceKeepGoing() throws Exception {
    tester.addFile("base/BUILD",
        "cc_library(name = 'hello', srcs = ['hello.cc', '//bad:bad.cc'])");
    tester.useLoadingOptions("--compile_one_dependency");
    TargetPatternPhaseValue loadingResult = tester.loadKeepGoing("base/hello.cc");
    assertThat(loadingResult.hasPostExpansionError()).isFalse();
  }

  @Test
  public void testCompileOneDependencyReferencesFile() throws Exception {
    tester.addFile("base/BUILD",
        "cc_library(name = 'hello', srcs = ['hello.cc', '//bad:bad.cc'])");
    tester.useLoadingOptions("--compile_one_dependency");
    TargetParsingException e =
        assertThrows(TargetParsingException.class, () -> tester.load("//base:hello"));
    assertThat(e)
        .hasMessageThat()
        .contains("--compile_one_dependency target '//base:hello' must be a file");
  }

  @Test
  public void testParsingFailureReported() throws Exception {
    TargetPatternPhaseValue loadingResult = tester.loadKeepGoing("//does_not_exist");
    assertThat(loadingResult.hasError()).isTrue();
    ParsingFailedEvent event = tester.findPostOnce(ParsingFailedEvent.class);
    assertThat(event.getPattern()).isEqualTo("//does_not_exist");
    assertThat(event.getMessage()).contains("BUILD file not found");
  }

  @Test
  public void testCyclesKeepGoing() throws Exception {
    tester.addFile("test/BUILD", "load(':cycle1.bzl', 'make_cycle')");
    tester.addFile("test/cycle1.bzl", "load(':cycle2.bzl', 'make_cycle')");
    tester.addFile("test/cycle2.bzl", "load(':cycle1.bzl', 'make_cycle')");
    // The skyframe target pattern evaluator isn't able to provide partial results in the presence
    // of cycles, so it simply raises an exception rather than returning an empty result.
    TargetParsingException e =
        assertThrows(TargetParsingException.class, () -> tester.load("//test:cycle1"));
    assertThat(e).hasMessageThat().contains("cycles detected");
    tester.assertContainsEventWithFrequency("cycle detected in extension", 1);
    PatternExpandingError err = tester.findPostOnce(PatternExpandingError.class);
    assertThat(err.getPattern()).containsExactly("//test:cycle1");
  }

  @Test
  public void testCyclesNoKeepGoing() throws Exception {
    tester.addFile("test/BUILD", "load(':cycle1.bzl', 'make_cycle')");
    tester.addFile("test/cycle1.bzl", "load(':cycle2.bzl', 'make_cycle')");
    tester.addFile("test/cycle2.bzl", "load(':cycle1.bzl', 'make_cycle')");
    TargetParsingException e =
        assertThrows(TargetParsingException.class, () -> tester.load("//test:cycle1"));
    assertThat(e).hasMessageThat().contains("cycles detected");
    tester.assertContainsEventWithFrequency("cycle detected in extension", 1);
    PatternExpandingError err = tester.findPostOnce(PatternExpandingError.class);
    assertThat(err.getPattern()).containsExactly("//test:cycle1");
  }

  @Test
  public void mapsOriginalPatternsToLabels() throws Exception {
    tester.addFile("test/a/BUILD", "cc_library(name = 'a_lib', srcs = ['a.cc'])");
    tester.addFile("test/b/BUILD", "cc_library(name = 'b_lib', srcs = ['b.cc'])");

    tester.load("test/a:all", "test/b:all", "test/...");

    assertThat(tester.getOriginalPatternsToLabels())
        .containsExactly(
            "test/a:all", Label.parseAbsoluteUnchecked("//test/a:a_lib"),
            "test/b:all", Label.parseAbsoluteUnchecked("//test/b:b_lib"),
            "test/...", Label.parseAbsoluteUnchecked("//test/a:a_lib"),
            "test/...", Label.parseAbsoluteUnchecked("//test/b:b_lib"));
  }

  @Test
  public void testSuiteCycle() throws Exception {
    tester.addFile(
        "BUILD", "test_suite(name = 'a', tests = [':b']); test_suite(name = 'b', tests = [':a'])");
    assertThat(
            assertThrows(TargetParsingException.class, () -> tester.loadKeepGoing("//:a", "//:b")))
        .hasMessageThat()
        .contains("cycles detected");
    assertThat(tester.assertContainsError("cycle in dependency graph").toString())
        .containsMatch("in test_suite rule //:.: cycle in dependency graph");
    PatternExpandingError err = tester.findPostOnce(PatternExpandingError.class);
    assertThat(err.getPattern()).containsExactly("//:a", "//:b");
  }

  @Test
  public void mapsOriginalPatternsToLabels_omitsExcludedTargets() throws Exception {
    tester.addFile("test/a/BUILD", "cc_library(name = 'a_lib', srcs = ['a.cc'])");

    tester.load("test/...", "-test/a:a_lib");

    assertThat(tester.getOriginalPatternsToLabels()).isEmpty();
  }

  @Test
  public void testWildcard() throws Exception {
    tester.addFile("foo/lib/BUILD",
        "sh_library(name = 'lib2', srcs = ['foo.cc'])");
    TargetPatternPhaseValue value = assertNoErrors(tester.load("//foo/lib:all-targets"));
    assertThat(value.getTargetLabels())
        .containsExactlyElementsIn(
            getLabels("//foo/lib:BUILD", "//foo/lib:lib2", "//foo/lib:foo.cc"));

    value = assertNoErrors(tester.load("//foo/lib:*"));
    assertThat(value.getTargetLabels())
        .containsExactlyElementsIn(
            getLabels("//foo/lib:BUILD", "//foo/lib:lib2", "//foo/lib:foo.cc"));
  }

  @Test
  public void testWildcardConflict() throws Exception {
    tester.addFile("foo/lib/BUILD",
        "cc_library(name = 'lib1')",
        "cc_library(name = 'lib2')",
        "cc_library(name = 'all-targets')",
        "cc_library(name = 'all')");

    assertWildcardConflict("//foo/lib:all", ":all");
    assertWildcardConflict("//foo/lib:all-targets", ":all-targets");
  }

  private void assertWildcardConflict(String label, String suffix) throws Exception {
    TargetPatternPhaseValue value = tester.load(label);
    assertThat(value.getTargetLabels()).containsExactlyElementsIn(getLabels(label));
    tester.assertContainsWarning(String.format("The target pattern '%s' is ambiguous: '%s' is both "
        + "a wildcard, and the name of an existing cc_library rule; "
        + "using the latter interpretation", label, suffix));
  }

  @Test
  public void testAbsolutePatternEndsWithSlashAll() throws Exception {
    tester.addFile("foo/all/BUILD", "cc_library(name = 'all')");
    TargetPatternPhaseValue value = tester.load("//foo/all");
    assertThat(value.getTargetLabels()).containsExactlyElementsIn(getLabels("//foo/all:all"));
  }

  @Test
  public void testRelativeLabel() throws Exception {
    tester.addFile("base/BUILD",
        "filegroup(name = 'hello', srcs = ['foo.txt'])");
    TargetPatternPhaseValue value = assertNoErrors(tester.load("base:hello"));
    assertThat(value.getTargetLabels()).containsExactlyElementsIn(getLabels("//base:hello"));
  }

  @Test
  public void testAbsoluteLabelWithOffset() throws Exception {
    tester.addFile("base/BUILD",
        "filegroup(name = 'hello', srcs = ['foo.txt'])");
    tester.setRelativeWorkingDirectory("base");
    TargetPatternPhaseValue value = assertNoErrors(tester.load("//base:hello"));
    assertThat(value.getTargetLabels()).containsExactlyElementsIn(getLabels("//base:hello"));
  }

  @Test
  public void testRelativeLabelWithOffset() throws Exception {
    tester.addFile("base/BUILD",
        "filegroup(name = 'hello', srcs = ['foo.txt'])");
    tester.setRelativeWorkingDirectory("base");
    TargetPatternPhaseValue value = assertNoErrors(tester.load(":hello"));
    assertThat(value.getTargetLabels()).containsExactlyElementsIn(getLabels("//base:hello"));
  }

  private void expectError(String pattern, String message) throws Exception {
    TargetParsingException e =
        assertThrows(TargetParsingException.class, () -> tester.load(pattern));
    assertThat(e).hasMessageThat().contains(message);
  }

  @Test
  public void testPatternWithSingleSlashIsError() throws Exception {
    expectError(
        "/single/slash",
        "not a valid absolute pattern (absolute target patterns must start with exactly "
            + "two slashes): '/single/slash'");
  }

  @Test
  public void testPatternWithSingleSlashIsErrorAndOffset() throws Exception {
    tester.setRelativeWorkingDirectory("base");
    expectError(
        "/single/slash",
        "not a valid absolute pattern (absolute target patterns must start with exactly "
            + "two slashes): '/single/slash'");
  }

  @Test
  public void testPatternWithTripleSlashIsError() throws Exception {
    expectError(
        "///triple/slash",
        "not a valid absolute pattern (absolute target patterns must start with exactly "
            + "two slashes): '///triple/slash'");
  }

  @Test
  public void testPatternEndingWithSingleSlashIsError() throws Exception {
    expectError(
        "foo/",
        "The package part of 'foo/' should not end in a slash");
  }

  @Test
  public void testPatternStartingWithDotDotSlash() throws Exception {
    expectError(
        "../foo",
        "Bad target pattern '../foo': package name component contains only '.' characters");
  }

  private void runTestPackageLoadingError(boolean keepGoing, String... patterns) throws Exception {
    tester.addFile("bad/BUILD", "nope");
    if (keepGoing) {
      TargetPatternPhaseValue value = tester.loadKeepGoing(patterns);
      assertThat(value.hasError()).isTrue();
      tester.assertContainsWarning("Target pattern parsing failed");
    } else {
      TargetParsingException exn =
          assertThrows(TargetParsingException.class, () -> tester.load(patterns));
      assertThat(exn).hasCauseThat().isInstanceOf(BuildFileContainsErrorsException.class);
      assertThat(exn).hasCauseThat().hasMessageThat().contains("Package 'bad' contains errors");
    }
    tester.assertContainsError("/workspace/bad/BUILD:1:1: name 'nope' is not defined");
  }

  @Test
  public void testPackageLoadingError_keepGoing_explicitTarget() throws Exception {
    runTestPackageLoadingError(/*keepGoing=*/ true, "//bad:BUILD");
  }

  @Test
  public void testPackageLoadingError_noKeepGoing_explicitTarget() throws Exception {
    runTestPackageLoadingError(/*keepGoing=*/ false, "//bad:BUILD");
  }

  @Test
  public void testPackageLoadingError_keepGoing_targetsInPackage() throws Exception {
    runTestPackageLoadingError(/*keepGoing=*/ true, "//bad:all");
  }

  @Test
  public void testPackageLoadingError_noKeepGoing_targetsInPackage() throws Exception {
    runTestPackageLoadingError(/*keepGoing=*/ false, "//bad:all");
  }

  @Test
  public void testPackageLoadingError_keepGoing_targetsBeneathDirectory() throws Exception {
    runTestPackageLoadingError(/*keepGoing=*/ true, "//bad/...");
  }

  @Test
  public void testPackageLoadingError_noKeepGoing_targetsBeneathDirectory() throws Exception {
    runTestPackageLoadingError(/*keepGoing=*/ false, "//bad/...");
  }

  @Test
  public void testPackageLoadingError_keepGoing_someGoodTargetsBeneathDirectory() throws Exception {
    tester.addFile("good/BUILD", "sh_library(name = 't')\n");
    runTestPackageLoadingError(/*keepGoing=*/ true, "//...");
  }

  @Test
  public void testPackageLoadingError_noKeepGoing_someGoodTargetsBeneathDirectory()
      throws Exception {
    tester.addFile("good/BUILD", "sh_library(name = 't')\n");
    runTestPackageLoadingError(/*keepGoing=*/ false, "//...");
  }

  private void runTestPackageFileInconsistencyError(boolean keepGoing, String... patterns)
      throws Exception {
    tester.addFile("bad/BUILD", "sh_library(name = 't')\n");
    IOException ioExn = new IOException("nope");
    tester.throwExceptionOnGetInputStream(tester.getWorkspace().getRelative("bad/BUILD"), ioExn);
    if (keepGoing) {
      TargetPatternPhaseValue value = tester.loadKeepGoing(patterns);
      assertThat(value.hasError()).isTrue();
      tester.assertContainsWarning("Target pattern parsing failed");
      tester.assertContainsError("error loading package 'bad': nope");
    } else {
      TargetParsingException exn =
          assertThrows(TargetParsingException.class, () -> tester.load(patterns));
      assertThat(exn).hasCauseThat().isInstanceOf(BuildFileContainsErrorsException.class);
      assertThat(exn).hasCauseThat().hasMessageThat().contains("error loading package 'bad': nope");
    }
  }

  @Test
  public void testPackageFileInconsistencyError_keepGoing_explicitTarget() throws Exception {
    runTestPackageFileInconsistencyError(true, "//bad:BUILD");
  }

  @Test
  public void testPackageFileInconsistencyError_noKeepGoing_explicitTarget() throws Exception {
    runTestPackageFileInconsistencyError(false, "//bad:BUILD");
  }

  @Test
  public void testPackageFileInconsistencyError_keepGoing_targetsInPackage() throws Exception {
    runTestPackageFileInconsistencyError(true, "//bad:all");
  }

  @Test
  public void testPackageFileInconsistencyError_noKeepGoing_targetsInPackage() throws Exception {
    runTestPackageFileInconsistencyError(false, "//bad:all");
  }

  @Test
  public void testPackageFileInconsistencyError_keepGoing_argetsBeneathDirectory()
      throws Exception {
    runTestPackageFileInconsistencyError(true, "//bad/...");
  }

  @Test
  public void testPackageFileInconsistencyError_noKeepGoing_targetsBeneathDirectory()
      throws Exception {
    runTestPackageFileInconsistencyError(false, "//bad/...");
  }

  @Test
  public void testPackageFileInconsistencyError_keepGoing_someGoodTargetsBeneathDirectory()
      throws Exception {
    tester.addFile("good/BUILD", "sh_library(name = 't')\n");
    runTestPackageFileInconsistencyError(true, "//...");
  }

  @Test
  public void testPackageFileInconsistencyError_noKeepGoing_someGoodTargetsBeneathDirectory()
      throws Exception {
    tester.addFile("good/BUILD", "sh_library(name = 't')\n");
    runTestPackageFileInconsistencyError(false, "//...");
  }

  private void runTestExtensionLoadingError(boolean keepGoing, String... patterns)
      throws Exception {
    tester.addFile("bad/f1.bzl", "nope");
    tester.addFile("bad/BUILD", "load(\":f1.bzl\", \"not_a_symbol\")");
    if (keepGoing) {
      TargetPatternPhaseValue value = tester.loadKeepGoing(patterns);
      assertThat(value.hasError()).isTrue();
      tester.assertContainsWarning("Target pattern parsing failed");
    } else {
      TargetParsingException exn =
          assertThrows(TargetParsingException.class, () -> tester.load(patterns));
      assertThat(exn).hasCauseThat().isInstanceOf(BuildFileContainsErrorsException.class);
      assertThat(exn).hasCauseThat().hasMessageThat().contains("Extension 'bad/f1.bzl' has errors");
      DetailedExitCode detailedExitCode = exn.getDetailedExitCode();
      assertThat(detailedExitCode.getExitCode()).isEqualTo(ExitCode.BUILD_FAILURE);
      assertThat(detailedExitCode.getFailureDetail().getPackageLoading().getCode())
          .isEqualTo(PackageLoading.Code.IMPORT_STARLARK_FILE_ERROR);
    }
    tester.assertContainsError("/workspace/bad/f1.bzl:1:1: name 'nope' is not defined");
  }

  @Test
  public void testExtensionLoadingError_keepGoing_explicitTarget() throws Exception {
    runTestExtensionLoadingError(/*keepGoing=*/ true, "//bad:BUILD");
  }

  @Test
  public void testExtensionLoadingError_noKeepGoing_explicitTarget() throws Exception {
    runTestExtensionLoadingError(/*keepGoing=*/ false, "//bad:BUILD");
  }

  @Test
  public void testExtensionLoadingError_keepGoing_targetsInPackage() throws Exception {
    runTestExtensionLoadingError(/*keepGoing=*/ true, "//bad:all");
  }

  @Test
  public void testExtensionLoadingError_noKeepGoing_targetsInPackage() throws Exception {
    runTestExtensionLoadingError(/*keepGoing=*/ false, "//bad:all");
  }

  @Test
  public void testExtensionLoadingError_keepGoing_targetsBeneathDirectory() throws Exception {
    runTestExtensionLoadingError(/*keepGoing=*/ true, "//bad/...");
  }

  @Test
  public void testExtensionLoadingError_noKeepGoing_targetsBeneathDirectory() throws Exception {
    runTestExtensionLoadingError(/*keepGoing=*/ false, "//bad/...");
  }

  @Test
  public void testExtensionLoadingError_keepGoing_someGoodTargetsBeneathDirectory()
      throws Exception {
    tester.addFile("good/BUILD", "sh_library(name = 't')\n");
    runTestExtensionLoadingError(/*keepGoing=*/ true, "//...");
  }

  @Test
  public void testExtensionLoadingError_noKeepGoing_someGoodTargetsBeneathDirectory()
      throws Exception {
    tester.addFile("good/BUILD", "sh_library(name = 't')\n");
    runTestExtensionLoadingError(/*keepGoing=*/ false, "//...");
  }

  private static class LoadingPhaseTester {
    private final ManualClock clock = new ManualClock();
    private final CustomInMemoryFs fs = new CustomInMemoryFs(clock);
    private final Path workspace;

    private final AnalysisMock analysisMock;
    private final SkyframeExecutor skyframeExecutor;

    private final List<Path> changes = new ArrayList<>();
    private final BlazeDirectories directories;
    private final ActionKeyContext actionKeyContext = new ActionKeyContext();

    private LoadingOptions options;
    private final StoredEventHandler storedErrors;

    private PathFragment relativeWorkingDirectory = PathFragment.EMPTY_FRAGMENT;
    private TargetParsingCompleteEvent targetParsingCompleteEvent;
    private LoadingPhaseCompleteEvent loadingPhaseCompleteEvent;

    private MockToolsConfig mockToolsConfig;

    public LoadingPhaseTester() throws IOException {
      this.workspace = fs.getPath("/workspace");
      workspace.createDirectory();
      mockToolsConfig = new MockToolsConfig(workspace);
      analysisMock = AnalysisMock.get();
      analysisMock.setupMockClient(mockToolsConfig);
      directories =
          new BlazeDirectories(
              new ServerDirectories(
                  fs.getPath("/install"), fs.getPath("/output"), fs.getPath("/userRoot")),
              workspace,
              /* defaultSystemJavabase= */ null,
              analysisMock.getProductName());
      workspace.getRelative("base").deleteTree();

      ConfiguredRuleClassProvider ruleClassProvider = analysisMock.createRuleClassProvider();
      PackageFactory pkgFactory =
          analysisMock.getPackageFactoryBuilderForTesting(directories).build(ruleClassProvider, fs);
      PackageOptions options = Options.getDefaults(PackageOptions.class);
      storedErrors = new StoredEventHandler();
      BuildOptions defaultBuildOptions;
      try {
        defaultBuildOptions = BuildOptions.of(ImmutableList.of());
      } catch (OptionsParsingException e) {
        throw new RuntimeException(e);
      }
      skyframeExecutor =
          BazelSkyframeExecutorConstants.newBazelSkyframeExecutorBuilder()
              .setPkgFactory(pkgFactory)
              .setFileSystem(fs)
              .setDirectories(directories)
              .setActionKeyContext(actionKeyContext)
              .setDefaultBuildOptions(defaultBuildOptions)
              .setExtraSkyFunctions(analysisMock.getSkyFunctions(directories))
              .build();
      SkyframeExecutorTestHelper.process(skyframeExecutor);
      PathPackageLocator pkgLocator =
          PathPackageLocator.create(
              null,
              options.packagePath,
              storedErrors,
              workspace,
              workspace,
              BazelSkyframeExecutorConstants.BUILD_FILES_BY_PRIORITY);
      PackageOptions packageOptions = Options.getDefaults(PackageOptions.class);
      packageOptions.defaultVisibility = ConstantRuleVisibility.PRIVATE;
      packageOptions.showLoadingProgress = true;
      packageOptions.globbingThreads = 7;
      skyframeExecutor.injectExtraPrecomputedValues(
          ImmutableList.of(
              PrecomputedValue.injected(
                  RepositoryDelegatorFunction.RESOLVED_FILE_INSTEAD_OF_WORKSPACE,
                  Optional.empty())));
      skyframeExecutor.preparePackageLoading(
          pkgLocator,
          packageOptions,
          Options.getDefaults(BuildLanguageOptions.class),
          UUID.randomUUID(),
          ImmutableMap.<String, String>of(),
          new TimestampGranularityMonitor(clock));
      skyframeExecutor.setActionEnv(ImmutableMap.<String, String>of());
      this.options = Options.getDefaults(LoadingOptions.class);
    }

    public void useLoadingOptions(String... options) throws OptionsParsingException {
      OptionsParser parser = OptionsParser.builder().optionsClasses(LoadingOptions.class).build();
      parser.parse(ImmutableList.copyOf(options));
      this.options = parser.getOptions(LoadingOptions.class);
    }

    public void setRelativeWorkingDirectory(String relativeWorkingDirectory) {
      this.relativeWorkingDirectory = PathFragment.create(relativeWorkingDirectory);
    }

    public void setDeletedPackages(PackageIdentifier... packages) {
      skyframeExecutor.setDeletedPackages(ImmutableList.copyOf(packages));
    }

    public TargetPatternPhaseValue load(String... patterns) throws Exception {
      return loadWithFlags(/*keepGoing=*/false, /*determineTests=*/false, patterns);
    }

    public TargetPatternPhaseValue loadKeepGoing(String... patterns) throws Exception {
      return loadWithFlags(/*keepGoing=*/true, /*determineTests=*/false, patterns);
    }

    public TargetPatternPhaseValue loadTests(String... patterns) throws Exception {
      return loadWithFlags(/*keepGoing=*/false, /*determineTests=*/true, patterns);
    }

    public TargetPatternPhaseValue loadTestsKeepGoing(String... patterns) throws Exception {
      return loadWithFlags(/*keepGoing=*/true, /*determineTests=*/true, patterns);
    }

    public TargetPatternPhaseValue loadWithFlags(
        boolean keepGoing, boolean determineTests, String... patterns) throws Exception {
      sync();
      storedErrors.clear();
      TargetPatternPhaseValue result =
          skyframeExecutor.loadTargetPatternsWithFilters(
              storedErrors,
              ImmutableList.copyOf(patterns),
              relativeWorkingDirectory,
              options,
              // We load very few packages, and everything is in memory; two should be plenty.
              /* threadCount= */ 2,
              keepGoing,
              determineTests);
      this.targetParsingCompleteEvent = findPost(TargetParsingCompleteEvent.class);
      this.loadingPhaseCompleteEvent = findPost(LoadingPhaseCompleteEvent.class);
      if (!keepGoing) {
        assertThat(storedErrors.hasErrors()).isFalse();
      }
      return result;
    }

    public Path getWorkspace() {
      return workspace;
    }

    public void addFile(String fileName, String... content) throws IOException {
      Path buildFile = workspace.getRelative(fileName);
      Preconditions.checkState(!buildFile.exists());
      Path currentPath = buildFile;

      // Add the new file and all the directories that will be created by
      // createDirectoryAndParents()
      while (!currentPath.exists()) {
        changes.add(currentPath);
        currentPath = currentPath.getParentDirectory();
      }

      buildFile.getParentDirectory().createDirectoryAndParents();
      FileSystemUtils.writeContentAsLatin1(buildFile, Joiner.on('\n').join(content));
    }

    private void sync() throws InterruptedException {
      clock.advanceMillis(1);
      ModifiedFileSet.Builder builder = ModifiedFileSet.builder();
      for (Path path : changes) {
        if (!path.startsWith(workspace)) {
          continue;
        }

        PathFragment workspacePath = path.relativeTo(workspace);
        builder.modify(workspacePath);
      }
      ModifiedFileSet modified = builder.build();
      skyframeExecutor.invalidateFilesUnderPathForTesting(
          storedErrors, modified, Root.fromPath(workspace));

      changes.clear();
    }

    public Target getTarget(String targetName) throws Exception {
      StoredEventHandler eventHandler = new StoredEventHandler();
      Target target = getPkgManager().getTarget(
          eventHandler, Label.parseAbsoluteUnchecked(targetName));
      assertThat(eventHandler.hasErrors()).isFalse();
      return target;
    }

    private PackageManager getPkgManager() {
      return skyframeExecutor.getPackageManager();
    }

    public ImmutableSet<Label> getFilteredTargets() {
      return ImmutableSet.copyOf(targetParsingCompleteEvent.getFilteredLabels());
    }

    public ImmutableSet<Label> getTestFilteredTargets() {
      return ImmutableSet.copyOf(targetParsingCompleteEvent.getTestFilteredLabels());
    }

    public ImmutableSet<Label> getOriginalTargets() {
      return ImmutableSet.copyOf(targetParsingCompleteEvent.getLabels());
    }

    public ImmutableSetMultimap<String, Label> getOriginalPatternsToLabels() {
      return targetParsingCompleteEvent.getOriginalPatternsToLabels();
    }

    public ImmutableSet<Label> getTestSuiteTargets() {
      return loadingPhaseCompleteEvent.getFilteredLabels();
    }

    void throwExceptionOnGetInputStream(Path path, IOException exn) {
      fs.throwExceptionOnGetInputStream(path, exn);
    }

    private Iterable<Event> filteredEvents() {
      return Iterables.filter(storedErrors.getEvents(), new Predicate<Event>() {
        @Override
        public boolean apply(Event event) {
          return event.getKind() != EventKind.PROGRESS;
        }
      });
    }

    public void assertNoEvents() {
      MoreAsserts.assertNoEvents(filteredEvents());
    }

    public Event assertContainsWarning(String expectedMessage) {
      return MoreAsserts.assertContainsEvent(filteredEvents(), expectedMessage, EventKind.WARNING);
    }

    public Event assertContainsError(String expectedMessage) {
      return MoreAsserts.assertContainsEvent(filteredEvents(), expectedMessage, EventKind.ERRORS);
    }

    public void assertContainsEventWithFrequency(String expectedMessage, int expectedFrequency) {
      MoreAsserts.assertContainsEventWithFrequency(
          filteredEvents(), expectedMessage, expectedFrequency);
    }

    public <T extends Postable> T findPost(Class<T> clazz) {
      return Iterators.getNext(
          storedErrors.getPosts().stream().filter(clazz::isInstance).map(clazz::cast).iterator(),
          null);
    }

    public <T extends Postable> T findPostOnce(Class<T> clazz) {
      return storedErrors
          .getPosts()
          .stream()
          .filter(clazz::isInstance)
          .map(clazz::cast)
          .collect(MoreCollectors.onlyElement());
    }
  }

  /**
   * Custom {@link InMemoryFileSystem} that can be pre-configured per-file to throw a supplied
   * IOException instead of the usual behavior.
   */
  private static class CustomInMemoryFs extends InMemoryFileSystem {

    private final Map<Path, IOException> pathsToErrorOnGetInputStream = Maps.newHashMap();

    CustomInMemoryFs(ManualClock manualClock) {
      super(manualClock, DigestHashFunction.SHA256);
    }

    synchronized void throwExceptionOnGetInputStream(Path path, IOException exn) {
      pathsToErrorOnGetInputStream.put(path, exn);
    }

    @Override
    protected synchronized InputStream getInputStream(Path path) throws IOException {
      IOException exnToThrow = pathsToErrorOnGetInputStream.get(path);
      if (exnToThrow != null) {
        throw exnToThrow;
      }
      return super.getInputStream(path);
    }
  }
}
