// 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.packages;

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

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.actions.ThreadStateReceiver;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.packages.PackageValidator.InvalidPackageException;
import com.google.devtools.build.lib.packages.util.PackageLoadingTestCase;
import com.google.devtools.build.lib.vfs.DigestHashFunction;
import com.google.devtools.build.lib.vfs.Dirent;
import com.google.devtools.build.lib.vfs.FileSystem;
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.SyscallCache;
import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import net.starlark.java.eval.Starlark;
import net.starlark.java.syntax.ParserInput;
import net.starlark.java.syntax.StarlarkFile;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
 * Unit tests for {@code PackageFactory}. Note: PackageLoadingTestCase doesn't support REPOSITORY
 * skyframe function, thus these tests cannot load external packages, {@code @repo://pkg}.
 */
@RunWith(JUnit4.class)
public final class PackageFactoryTest extends PackageLoadingTestCase {

  private Path throwOnReaddir = null;

  // Overrides FileSystem.readdir for the benefit of one test method
  // (testTransientErrorsInGlobbing) that injects a failure.
  @Override
  protected FileSystem createFileSystem() {
    return new InMemoryFileSystem(DigestHashFunction.SHA256) {
      @Override
      public Collection<Dirent> readdir(PathFragment path, boolean followSymlinks)
          throws IOException {
        if (throwOnReaddir != null && throwOnReaddir.asFragment().equals(path)) {
          throw new FileNotFoundException(path.getPathString());
        }
        return super.readdir(path, followSymlinks);
      }
    };
  }

  @Test
  public void testCreatePackage() throws Exception {
    scratch.file("pkgname/BUILD", "# empty build file ");
    Package pkg = loadPackage("pkgname");
    assertThat(pkg.getName()).isEqualTo("pkgname");
    assertThat(Sets.newHashSet(pkg.getTargets(Rule.class))).isEmpty();
  }

  @Test
  public void testBadRuleName() throws Exception {
    reporter.removeHandler(failFastHandler);
    scratch.file("badrulename/BUILD", "cc_library(name = 3)");
    Package pkg = loadPackage("badrulename");
    assertContainsEvent("cc_library 'name' attribute must be a string");
    assertThat(pkg.containsErrors()).isTrue();
  }

  @Test
  public void testNoRuleName() throws Exception {
    reporter.removeHandler(failFastHandler);
    scratch.file("badrulename/BUILD", "cc_library()");
    Package pkg = loadPackage("badrulename");
    assertContainsEvent("cc_library rule has no 'name' attribute");
    assertThat(pkg.containsErrors()).isTrue();
  }

  @Test
  public void testBadPackageName() throws Exception {
    // This is a "shallow" syntactic error: failure to form the
    // PackageIdentifier that is the real argument to loadPackage.
    LabelSyntaxException e =
        assertThrows(LabelSyntaxException.class, () -> loadPackage("not even a legal/.../label"));
    assertThat(e).hasMessageThat().contains("invalid package name 'not even a legal/.../label'");
  }

  @Test
  public void testColonInExportsFilesTargetName() throws Exception {
    reporter.removeHandler(failFastHandler);
    scratch.file(
        "googledata/cafe/BUILD", "exports_files(['houseads/house_ads:ca-aol_parenting_html'])");
    Package pkg = loadPackage("googledata/cafe");
    assertContainsEvent("target names may not contain ':'");
    assertThat(pkg.getTargets(FileTarget.class).toString())
        .doesNotContain("houseads/house_ads:ca-aol_parenting_html");
    assertThat(pkg.containsErrors()).isTrue();
  }

  @Test
  public void testExportsFilesVisibilityMustBeSequence() throws Exception {
    expectEvalError(
        "in call to exports_files(), parameter 'visibility' got value of type 'depset', want"
            + " 'sequence or NoneType'",
        "exports_files(srcs=[], visibility=depset(['notice']))");
  }

  @Test
  public void testExportsFilesLicensesMustBeSequence() throws Exception {
    expectEvalError(
        "in call to exports_files(), parameter 'licenses' got value of type 'depset', want"
            + " 'sequence or NoneType'",
        "exports_files(srcs=[], licenses=depset(['notice']))");
  }

  @Test
  public void testPackageNameWithPROTECTEDIsOk() throws Exception {
    reporter.removeHandler(failFastHandler);
    // One "PROTECTED":
    assertThat(isValidPackageName("foo/PROTECTED/bar")).isTrue();
    // Multiple "PROTECTED"s:
    assertThat(isValidPackageName("foo/PROTECTED/bar/PROTECTED/wiz")).isTrue();
  }

  private boolean isValidPackageName(String packageName) throws Exception {
    // Write a license decl just in case it's a third_party package:
    scratch.file(packageName + "/BUILD", "licenses(['notice'])");
    Package pkg = loadPackage(packageName);
    return !pkg.containsErrors();
  }

  @Test
  public void testDuplicatedDependencies() throws Exception {
    reporter.removeHandler(failFastHandler);
    scratch.file(
        "has_dupe/BUILD",
        "cc_library(name='dep')",
        "cc_library(name='has_dupe', deps=[':dep', ':dep'])");

    Package pkg = loadPackage("has_dupe");
    assertContainsEvent(
        "Label '//has_dupe:dep' is duplicated in the 'deps' attribute of rule 'has_dupe'");
    assertThat(pkg.containsErrors()).isTrue();
    assertThat(pkg.getRule("has_dupe")).isNotNull();
    assertThat(pkg.getRule("dep")).isNotNull();
    assertThat(pkg.getRule("has_dupe").containsErrors()).isTrue();
    // All rules in an errant package are themselves errant.
    assertThat(pkg.getRule("dep").containsErrors()).isTrue();
  }

  @Test
  public void testPrefixWithinSameRule1() throws Exception {
    reporter.removeHandler(failFastHandler);
    scratch.file(
        "fruit/orange/BUILD", "genrule(name='orange', srcs=[], outs=['a', 'a/b'], cmd='')");
    loadPackage("fruit/orange");
    assertContainsEvent("rule 'orange' has conflicting output files 'a/b' and 'a");
  }

  @Test
  public void testPrefixWithinSameRule2() throws Exception {
    reporter.removeHandler(failFastHandler);
    scratch.file(
        "fruit/orange/BUILD", "genrule(name='orange', srcs=[], outs=['a/b', 'a'], cmd='')");
    loadPackage("fruit/orange");
    assertContainsEvent("rule 'orange' has conflicting output files 'a' and 'a/b");
  }

  @Test
  public void testPrefixBetweenRules1() throws Exception {
    reporter.removeHandler(failFastHandler);
    scratch.file(
        "fruit/kiwi/BUILD",
        "genrule(name='kiwi1', srcs=[], outs=['a'], cmd='')",
        "genrule(name='kiwi2', srcs=[], outs=['a/b'], cmd='')");
    loadPackage("fruit/kiwi");
    assertContainsEvent(
        "output file 'a/b' of rule 'kiwi2' conflicts with output file 'a' of rule 'kiwi1'");
  }

  @Test
  public void testPrefixBetweenRules2() throws Exception {
    reporter.removeHandler(failFastHandler);
    scratch.file(
        "fruit/kiwi/BUILD",
        "genrule(name='kiwi1', srcs=[], outs=['a/b'], cmd='')",
        "genrule(name='kiwi2', srcs=[], outs=['a'], cmd='')");
    loadPackage("fruit/kiwi");
    assertContainsEvent(
        "output file 'a' of rule 'kiwi2' conflicts with output file 'a/b' of rule 'kiwi1'");
  }

  @Test
  public void testPackageNameFunction() throws Exception {
    scratch.file("pina/BUILD", "cc_library(name=package_name() + '-colada')");
    Package pkg = loadPackage("pina");
    assertNoEvents();
    assertThat(pkg.containsErrors()).isFalse();
    assertThat(pkg.getRule("pina-colada")).isNotNull();
    assertThat(pkg.getRule("pina-colada").containsErrors()).isFalse();
    assertThat(Sets.newHashSet(pkg.getTargets(Rule.class)).size()).isSameInstanceAs(1);
  }

  @Test
  public void testDuplicateRuleName() throws Exception {
    reporter.removeHandler(failFastHandler);
    scratch.file(
        "duplicaterulename/BUILD",
        "proto_library(name = 'spellcheck_proto',",
        "         srcs = ['spellcheck.proto'],",
        "         cc_api_version = 2)",
        "cc_library(name = 'spellcheck_proto')", // conflict error stops execution
        "x = 1//0"); // not reached
    Package pkg = loadPackage("duplicaterulename");
    assertContainsEvent(
        "cc_library rule 'spellcheck_proto' in package 'duplicaterulename' conflicts with"
            + " existing proto_library rule");
    assertDoesNotContainEvent("division by zero");
    assertThat(pkg.containsErrors()).isTrue();
  }

  @Test
  public void testBuildFileTargetExists() throws Exception {
    scratch.file("foo/BUILD");
    Target target = getTarget("//foo:BUILD");
    assertThat(target.getName()).isEqualTo("BUILD");
    // Test that it's memoized:
    assertThat(target.getPackage().getTarget("BUILD")).isSameInstanceAs(target);
  }

  @Test
  public void testCreationOfInputFiles() throws Exception {
    scratch.file(
        "foo/BUILD",
        "exports_files(['Z'])",
        "cc_library(name='W', deps=['X', 'Y'])",
        "cc_library(name='X', srcs=['X'])",
        "cc_library(name='Y')");
    Package pkg = loadPackage("foo");
    assertThat(pkg.containsErrors()).isFalse();

    // X is a rule with a circular self-dependency.
    assertThat(pkg.getTarget("X").getClass()).isSameInstanceAs(Rule.class);

    // Y is a rule
    assertThat(pkg.getTarget("Y").getClass()).isSameInstanceAs(Rule.class);

    // Z is a file
    assertThat(pkg.getTarget("Z").getClass()).isSameInstanceAs(InputFile.class);

    // A is nothing
    NoSuchTargetException e = assertThrows(NoSuchTargetException.class, () -> pkg.getTarget("A"));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "no such target '//foo:A': target 'A' not declared in package 'foo' defined by"
                + " /workspace/foo/BUILD (Tip: use `query \"//foo:*\"` to see all the targets in"
                + " that package)");

    // These are the only input files: BUILD, Z
    Set<String> inputFiles = Sets.newTreeSet();
    for (InputFile inputFile : pkg.getTargets(InputFile.class)) {
      inputFiles.add(inputFile.getName());
    }
    assertThat(Lists.newArrayList(inputFiles)).containsExactly("BUILD", "Z").inOrder();
  }

  @Test
  public void testDuplicateRuleIsNotAddedToPackage() throws Exception {
    reporter.removeHandler(failFastHandler);
    scratch.file(
        "dup/BUILD",
        "proto_library(name = 'dup_proto',",
        "              srcs  = ['dup.proto'],",
        "              cc_api_version = 2)",
        "",
        "cc_library(name = 'dup_proto',",
        "           srcs = ['dup.pb.cc', 'dup.pb.h'])");
    Package pkg = loadPackage("dup");
    assertContainsEvent(
        "cc_library rule 'dup_proto' in package 'dup' conflicts with existing proto_library rule");
    assertThat(pkg.containsErrors()).isTrue();

    Rule dupProto = pkg.getRule("dup_proto");
    // Check that the first rule of the given name "wins", and that each of the
    // "winning" rule's outputs is a member of the package.
    assertThat(dupProto.getRuleClass()).isEqualTo("proto_library");
    for (OutputFile out : dupProto.getOutputFiles()) {
      assertThat(pkg.getTargets(FileTarget.class)).contains(out);
    }
  }

  @Test
  public void testConflictingRuleDoesNotUpdatePackage() throws Exception {
    reporter.removeHandler(failFastHandler);

    // In this test, rule2's outputs conflict with rule1, so rule2 is rejected.
    // However, we must check that neither rule2, nor any of its inputs or
    // outputs is a member of the package, and that the conflicting output file
    // "out2" still has rule1 as its getGeneratingRule().
    scratch.file(
        "conflict/BUILD",
        "genrule(name = 'rule1',",
        "        cmd = '',",
        "        srcs = ['in1', 'in2'],",
        "        outs = ['out1', 'out2'])",
        "genrule(name = 'rule2',",
        "        cmd = '',",
        "        srcs = ['in3', 'in4'],",
        "        outs = ['out3', 'out2'])");
    Package pkg = loadPackage("conflict");
    assertContainsEvent(
        "generated file 'out2' in rule 'rule2' "
            + "conflicts with existing generated file from rule 'rule1'");
    assertThat(pkg.containsErrors()).isTrue();

    assertThat(pkg.getRule("rule2")).isNull();

    // Ensure that rule2's "out2" didn't overwrite rule1's:
    assertThat(((OutputFile) pkg.getTarget("out2")).getGeneratingRule())
        .isSameInstanceAs(pkg.getRule("rule1"));

    // None of rule2, its inputs, or its outputs should belong to pkg:
    List<Target> found = new ArrayList<>();
    for (String targetName : ImmutableList.of("rule2", "in3", "in4", "out3")) {
      try {
        found.add(pkg.getTarget(targetName));
        // No fail() here: if there's no exception, we add the name to a list
        // and we check below that it's empty.
      } catch (NoSuchTargetException e) {
        /* good! */
      }
    }
    assertThat(found).isEmpty();
  }

  // Was: Regression test for bug "Rules declared after an error in
  // a package should be considered 'in error'".
  // Then: Regression test for bug "Why aren't ERRORS considered
  // fatal?*"
  // Now: Regression test for: execution should stop at the first EvalException;
  // all rules created prior to the exception error are marked in error.
  @Test
  public void testAllRulesInErrantPackageAreInError() throws Exception {
    reporter.removeHandler(failFastHandler);
    scratch.file(
        "error/BUILD",
        "genrule(name = 'rule1',",
        "        cmd = ':',",
        "        outs = ['out.1'])",
        "list = ['bad']",
        "x = 1//0", // dynamic error
        "genrule(name = 'rule2',",
        "        cmd = ':',",
        "        outs = list)");
    Package pkg = loadPackage("error");
    assertContainsEvent("division by zero");

    assertThat(pkg.containsErrors()).isTrue();

    // rule1 would be fine but is still marked as in error:
    assertThat(pkg.getRule("rule1").containsErrors()).isTrue();

    // rule2's genrule is never executed.
    Rule rule2 = pkg.getRule("rule2");
    assertThat(rule2).isNull();
  }

  @Test
  public void testHelpfulErrorForMissingExportsFiles() throws Exception {
    scratch.file("x/BUILD", "cc_library(name='x', srcs=['x.cc'])");
    scratch.file("x/x.cc");
    scratch.file("x/y.cc");
    scratch.file("x/dir/dummy");

    Package pkg = loadPackage("x");

    assertThat(pkg.getTarget("x.cc")).isNotNull(); // existing and mentioned.

    NoSuchTargetException e =
        assertThrows(NoSuchTargetException.class, () -> pkg.getTarget("y.cc"));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "no such target '//x:y.cc': "
                + "target 'y.cc' not declared in package 'x' "
                + "defined by /workspace/x/BUILD; "
                + "however, a source file of this name exists.  "
                + "(Perhaps add 'exports_files([\"y.cc\"])' to x/BUILD?)");

    e = assertThrows(NoSuchTargetException.class, () -> pkg.getTarget("z.cc"));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "no such target '//x:z.cc': target 'z.cc' not declared in package 'x' defined by"
                + " /workspace/x/BUILD (did you mean 'x.cc'? Tip: use `query \"//x:*\"` to see all"
                + " the targets in that package)");

    e = assertThrows(NoSuchTargetException.class, () -> pkg.getTarget("dir"));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "no such target '//x:dir': target 'dir' not declared in package 'x' defined by"
                + " /workspace/x/BUILD; however, a source directory of this name exists.  (Perhaps"
                + " add 'exports_files([\"dir\"])' to x/BUILD, or define a filegroup?)");
  }

  @Test
  public void testTestSuitesImplicitlyDependOnAllRulesInPackage() throws Exception {
    scratch.file(
        "x/BUILD",
        "java_test(name='j')",
        "test_suite(name='t1')",
        "test_suite(name='t2', tests=[])",
        "test_suite(name='t3', tests=['//foo'])",
        "test_suite(name='t4', tests=['//foo'])",
        "cc_test(name='c')");
    Package pkg = loadPackage("x");

    // Things to note:
    // - The '$implicit_tests' attribute is unset unless the 'tests' attribute is unset or empty.
    // - The '$implicit_tests' attribute's value for t1 and t2 is magically able to contain both j
    //    and c, even though c is instantiated after t1 and t2 are.

    assertThat(attributes(pkg.getRule("t1")).get("$implicit_tests", BuildType.LABEL_LIST))
        .containsExactlyElementsIn(
            Sets.newHashSet(Label.parseCanonical("//x:c"), Label.parseCanonical("//x:j")));
    assertThat(attributes(pkg.getRule("t2")).get("$implicit_tests", BuildType.LABEL_LIST))
        .containsExactlyElementsIn(
            Sets.newHashSet(Label.parseCanonical("//x:c"), Label.parseCanonical("//x:j")));
    assertThat(attributes(pkg.getRule("t3")).get("$implicit_tests", BuildType.LABEL_LIST))
        .isEmpty();
    assertThat(attributes(pkg.getRule("t4")).get("$implicit_tests", BuildType.LABEL_LIST))
        .isEmpty();
  }

  @Test
  public void testPackageValidationFailureRegisteredAfterLoading() throws Exception {
    scratch.file("x/BUILD", "# old");
    Package pkg = loadPackage("x");
    assertThat(pkg.containsErrors()).isFalse();

    // Install a validator.
    this.validator =
        (pkg2, packageOverhead, eventHandler) -> {
          if (pkg2.getName().equals("x")) {
            eventHandler.handle(Event.warn("warning event"));
            throw new InvalidPackageException(pkg2.getPackageIdentifier(), "nope");
          }
        };

    scratch.overwriteFile("x/BUILD", "# new"); // change file to cause reloading
    invalidatePackages();

    InvalidPackageException ex =
        assertThrows(InvalidPackageException.class, () -> loadPackage("x"));
    assertThat(ex).hasMessageThat().contains("no such package 'x': nope");
    assertContainsEvent("warning event");
  }

  @Test
  public void testGlobDirectoryExclusion() throws Exception {
    emptyFile("fruit/data/apple");
    emptyFile("fruit/data/pear");
    emptyFile("fruit/data/berry/black");
    emptyFile("fruit/data/berry/blue");
    scratch.file(
        "fruit/BUILD",
        "cc_library(name = 'yes', srcs = glob(['data/*']))",
        "cc_library(name = 'no',  srcs = glob(['data/*'], exclude_directories=0))");
    Package pkg = loadPackage("fruit");
    assertNoEvents();
    List<Label> yesFiles = attributes(pkg.getRule("yes")).get("srcs", BuildType.LABEL_LIST);
    List<Label> noFiles = attributes(pkg.getRule("no")).get("srcs", BuildType.LABEL_LIST);

    assertThat(yesFiles)
        .containsExactly(
            Label.parseCanonical("@//fruit:data/apple"),
            Label.parseCanonical("@//fruit:data/pear"));

    assertThat(noFiles)
        .containsExactly(
            Label.parseCanonical("@//fruit:data/apple"),
            Label.parseCanonical("@//fruit:data/pear"),
            Label.parseCanonical("@//fruit:data/berry"));
  }

  // TODO(bazel-team): This is really a test for GlobCache.
  @Test
  public void testRecursiveGlob() throws Exception {
    emptyFile("rg/a.cc");
    emptyFile("rg/foo/bar.cc");
    emptyFile("rg/foo/foo.cc");
    emptyFile("rg/foo/wiz/bam.cc");
    emptyFile("rg/foo/wiz/bum.cc");
    emptyFile("rg/foo/wiz/quid/gav.cc");
    scratch.file(
        "rg/BUILD",
        "cc_library(name = 'ri', srcs = glob(['**/*.cc']))",
        "cc_library(name = 're', srcs = glob(['*.cc'], exclude=['**/*.c']))");
    Package pkg = loadPackage("rg");
    assertNoEvents();

    assertGlob(
        pkg,
        ImmutableList.of(
            "BUILD",
            "a.cc",
            "foo",
            "foo/bar.cc",
            "foo/foo.cc",
            "foo/wiz",
            "foo/wiz/bam.cc",
            "foo/wiz/bum.cc",
            "foo/wiz/quid",
            "foo/wiz/quid/gav.cc"),
        "**");

    assertGlob(
        pkg,
        ImmutableList.of(
            "a.cc",
            "foo/bar.cc",
            "foo/foo.cc",
            "foo/wiz/bam.cc",
            "foo/wiz/bum.cc",
            "foo/wiz/quid/gav.cc"),
        "**/*.cc");
    assertGlob(pkg, ImmutableList.of("foo/bar.cc", "foo/wiz/bam.cc", "foo/wiz/bum.cc"), "**/b*.cc");
    assertGlob(
        pkg,
        ImmutableList.of(
            "foo/bar.cc", "foo/foo.cc", "foo/wiz/bam.cc", "foo/wiz/bum.cc", "foo/wiz/quid/gav.cc"),
        "**/*/*.cc");
    assertGlob(pkg, ImmutableList.of("foo/wiz/quid/gav.cc"), "foo/**/quid/*.cc");

    assertGlob(
        pkg,
        Collections.<String>emptyList(),
        ImmutableList.of("*.cc", "*/*.cc", "*/*/*.cc"),
        ImmutableList.of("**/*.cc"));
    assertGlob(
        pkg,
        Collections.<String>emptyList(),
        ImmutableList.of("**/*.cc"),
        ImmutableList.of("**/*.cc"));
    assertGlob(
        pkg,
        Collections.<String>emptyList(),
        ImmutableList.of("**/*.cc"),
        ImmutableList.of("*.cc", "*/*.cc", "*/*/*.cc", "*/*/*/*.cc"));
    assertGlob(
        pkg,
        Collections.<String>emptyList(),
        ImmutableList.of("**"),
        ImmutableList.of("*", "*/*", "*/*/*", "*/*/*/*"));
    assertGlob(
        pkg,
        ImmutableList.of(
            "foo/bar.cc", "foo/foo.cc", "foo/wiz/bam.cc", "foo/wiz/bum.cc", "foo/wiz/quid/gav.cc"),
        ImmutableList.of("**/*.cc"),
        ImmutableList.of("*.cc"));
    assertGlob(
        pkg,
        ImmutableList.of("a.cc", "foo/wiz/bam.cc", "foo/wiz/bum.cc", "foo/wiz/quid/gav.cc"),
        ImmutableList.of("**/*.cc"),
        ImmutableList.of("*/*.cc"));
    assertGlob(
        pkg,
        ImmutableList.of("a.cc", "foo/bar.cc", "foo/foo.cc", "foo/wiz/quid/gav.cc"),
        ImmutableList.of("**/*.cc"),
        ImmutableList.of("**/wiz/*.cc"));
  }

  @Test
  public void testTooManyArgumentsGlobErrors() throws Exception {
    reporter.removeHandler(failFastHandler);
    assertGlobFails(
        "glob(['incl'],['excl'],3,True,'extraarg')",
        "glob() accepts no more than 4 positional arguments but got 5");
  }

  @Test
  public void testGlobEnforcesListArgument() throws Exception {
    reporter.removeHandler(failFastHandler);
    assertGlobFails(
        "glob(1, exclude=2)",
        "in call to glob(), parameter 'include' got value of type 'int', want 'sequence'");
  }

  @Test
  public void testGlobEnforcesListOfStringsArguments() throws Exception {
    reporter.removeHandler(failFastHandler);
    assertGlobFails(
        "glob(['a', 'b'], exclude=['c', 42])",
        "expected value of type 'string' for element 1 of 'glob' argument, but got 42 (int)");
  }

  @Test
  public void testGlobNegativeTest() throws Exception {
    // Negative test that assertGlob does throw an error when asserting against the wrong values.
    // The AssertionError comes from FoundationTestCase.failFastHandler.
    AssertionError e =
        assertThrows(
            AssertionError.class,
            () ->
                assertGlobMatches(
                    /*result=*/ ImmutableList.of("Wombat1.java", "This_file_doesn_t_exist.java"),
                    /*includes=*/ ImmutableList.of("W*", "subdir"),
                    /*excludes=*/ ImmutableList.<String>of(),
                    /* excludeDirs= */ true));
    assertThat(e).hasMessageThat().contains("incorrect glob result");
  }

  @Test
  public void testGlobExcludeDirectories() throws Exception {
    assertGlobMatches(
        /*result=*/ ImmutableList.of("Wombat1.java", "Wombat2.java"),
        /*includes=*/ ImmutableList.of("W*", "subdir"),
        /*excludes=*/ ImmutableList.<String>of(),
        /* excludeDirs= */ true);
  }

  @Test
  public void testGlobDoesNotExcludeDirectories() throws Exception {
    assertGlobMatches(
        /*result=*/ ImmutableList.of("Wombat1.java", "Wombat2.java", "subdir"),
        /*includes=*/ ImmutableList.of("W*", "subdir"),
        /*excludes=*/ ImmutableList.<String>of(),
        /* excludeDirs= */ false);
  }

  @Test
  public void testGlobWithEmptyExcludedList() throws Exception {
    assertGlobMatches(
        /*result=*/ ImmutableList.of("Wombat1.java", "Wombat2.java"),
        /*includes=*/ ImmutableList.of("W*"),
        /*excludes=*/ Collections.<String>emptyList(),
        /* excludeDirs= */ false);
  }

  @Test
  public void testGlobWithQuestionMarkProducesError() throws Exception {
    assertGlobProducesError("Wombat?.java", true);
  }

  @Test
  public void testGlobWithoutQuestionMarkDoesntProduceError() throws Exception {
    assertGlobProducesError("Wombat*.java", false);
  }

  @Test
  public void testGlobWithNonMatchingExcludedList() throws Exception {
    assertGlobMatches(
        /*result=*/ ImmutableList.of("Wombat1.java"),
        /*includes=*/ ImmutableList.of("W*"),
        /*excludes=*/ ImmutableList.of("*2*"),
        /* excludeDirs= */ false);
  }

  @Test
  public void testGlobWithTwoMatchingGlobExpressionsAndNonmatchingExclusion() throws Exception {
    assertGlobMatches(
        /*result=*/ ImmutableList.of("Wombat1.java", "subdir/Wombat3.java"),
        /*includes=*/ ImmutableList.of("W*", "subdir/W*"),
        /*excludes=*/ ImmutableList.of("*2*"),
        /* excludeDirs= */ false);
  }

  @Test
  public void testGlobWithSubdirMatchAndExclusion() throws Exception {
    assertGlobMatches(
        /*result=*/ ImmutableList.of("subdir/Wombat3.java"),
        /*includes=*/ ImmutableList.of("W*", "subdir/W*"),
        /*excludes=*/ ImmutableList.of("Wombat*.java"),
        /* excludeDirs= */ false);
  }

  @Test
  public void testBadCharacterInGlob() throws Exception {
    reporter.removeHandler(failFastHandler);
    assertGlobFails("glob(['?'])", "Error in glob: wildcard ? forbidden");
  }

  @Test
  public void testBadExcludePattern() throws Exception {
    reporter.removeHandler(failFastHandler);
    // The 'exclude' check is currently only reached if the pattern is "complex".
    // This seems like a bug:
    //   assertGlobFails("glob(['BUILD'], ['/'])", "pattern cannot be absolute");
    assertGlobFails("glob(['BUILD'], ['/*/*'])", "pattern cannot be absolute");
  }

  @Test
  public void testGlobEscapesAt() throws Exception {
    // See lib.skyframe.PackageFunctionTest.globEscapesAt and
    // https://github.com/bazelbuild/bazel/issues/10606.
    scratch.file("p/@f.txt");
    scratch.file(
        "p/BUILD",
        "name = glob(['*.txt'])[0]",
        "name == ':@f.txt' or fail('got %s' % name)"); // observe prepended colon
    loadPackage("p"); // no error
  }

  /**
   * Tests that a glob evaluation that encounters an I/O error throws instead of constructing a
   * package.
   */
  @Test
  public void testGlobWithIOErrors() throws Exception {
    reporter.removeHandler(failFastHandler);
    scratch.file("pkg/BUILD", "glob(['globs/**'])");
    Path dir = scratch.dir("pkg/globs/unreadable");
    dir.setReadable(false);

    NoSuchPackageException ex =
        assertThrows(NoSuchPackageException.class, () -> loadPackage("pkg"));
    assertThat(ex)
        .hasMessageThat()
        .contains("error globbing [globs/**] op=FILES: " + dir + " (Permission denied)");
  }

  @Test
  public void testNativeModuleIsDisabled() throws Exception {
    reporter.removeHandler(failFastHandler);
    scratch.file("pkg/BUILD", "native.cc_library(name='bar')");
    Package pkg = loadPackage("pkg");
    assertThat(pkg.containsErrors()).isTrue();
  }

  @Test
  public void testPackageGroupSpecMinimal() throws Exception {
    expectEvalSuccess("package_group(name='skin', packages=[])");
  }

  @Test
  public void testPackageGroupSpecSimple() throws Exception {
    expectEvalSuccess("package_group(name='skin', packages=['//group/abelian'])");
  }

  @Test
  public void testPackageGroupSpecEmpty() throws Exception {
    expectEvalSuccess("package_group(name='seed')");
  }

  @Test
  public void testPackageGroupSpecIncludes() throws Exception {
    expectEvalSuccess(
        "package_group(name='wine',",
        "              includes=['//wine:cabernet_sauvignon',",
        "                        '//wine:pinot_noir'])");
  }

  @Test
  public void testPackageGroupSpecBad() throws Exception {
    expectEvalError("invalid package name", "package_group(name='skin', packages=['--25:17--'])");
  }

  @Test
  public void testPackageGroupsWithSameName() throws Exception {
    expectEvalError(
        "conflicts with existing package group",
        "package_group(name='skin', packages=[])",
        "package_group(name='skin', packages=[])");
  }

  @Test
  public void testPackageGroupNamedArguments() throws Exception {
    expectEvalError(
        "package_group() got unexpected positional argument", "package_group('skin', name = 'x')");
  }

  @Test
  public void testPackageSpecMinimal() throws Exception {
    Package pkg = expectEvalSuccess("package(default_visibility=[])");
    assertThat(pkg.getDefaultVisibility()).isNotNull();
  }

  @Test
  public void testPackageSpecSimple() throws Exception {
    expectEvalSuccess("package(default_visibility=['//group:lie'])");
  }

  @Test
  public void testPackageSpecBad() throws Exception {
    expectEvalError("invalid target name", "package(default_visibility=[':::'])");
  }

  @Test
  public void testDoublePackageSpecification() throws Exception {
    expectEvalError(
        "can only be used once",
        "package(default_visibility=[])",
        "package(default_visibility=[])");
  }

  @Test
  public void testEmptyPackageSpecification() throws Exception {
    expectEvalError("at least one argument must be given to the 'package' function", "package()");
  }

  @Test
  public void testDefaultTestonly() throws Exception {
    Package pkg = expectEvalSuccess("package(default_testonly = 1)");
    assertThat(pkg.getDefaultTestOnly()).isTrue();
  }

  @Test
  public void testDefaultDeprecation() throws Exception {
    String testMessage = "OMG PONIES!";
    Package pkg = expectEvalSuccess("package(default_deprecation = \"" + testMessage + "\")");
    assertThat(pkg.getDefaultDeprecation()).isEqualTo(testMessage);
  }

  @Test
  public void testExportsBuildFile() throws Exception {
    Package pkg =
        expectEvalSuccess("exports_files(['BUILD'], visibility=['//visibility:private'])");
    assertThat(pkg.getTarget("BUILD")).isEqualTo(pkg.getBuildFile());
  }

  @Test
  public void testDefaultDeprecationPropagation() throws Exception {
    String msg = "I am completely operational, and all my circuits are functioning perfectly.";
    scratch.file(
        "foo/BUILD",
        "package(default_deprecation = \"" + msg + "\")",
        "sh_library(name = 'bar', srcs=['b'])");
    Rule fooRule = (Rule) getTarget("//foo:bar");
    String deprAttr =
        attributes(fooRule).get("deprecation", com.google.devtools.build.lib.packages.Type.STRING);
    assertThat(deprAttr).isEqualTo(msg);
  }

  @Test
  public void testDefaultTestonlyPropagation() throws Exception {
    scratch.file(
        "foo/BUILD",
        "package(default_testonly = 1)",
        "sh_library(name = 'foo', srcs=['b'])",
        "sh_library(name = 'bar', srcs=['b'], testonly = 0)");
    Package pkg = loadPackage("foo");

    Rule fooRule = (Rule) pkg.getTarget("foo");
    assertThat(
            attributes(fooRule)
                .get("testonly", com.google.devtools.build.lib.packages.Type.BOOLEAN))
        .isTrue();

    Rule barRule = (Rule) pkg.getTarget("bar");
    assertThat(
            attributes(barRule)
                .get("testonly", com.google.devtools.build.lib.packages.Type.BOOLEAN))
        .isFalse();
  }

  @Test
  public void testDefaultDeprecationOverriding() throws Exception {
    String msg = "I am completely operational, and all my circuits are functioning perfectly.";
    String deceive = "OMG PONIES!";
    scratch.file(
        "foo/BUILD",
        "package(default_deprecation = \"" + deceive + "\")",
        "sh_library(name = 'bar', srcs=['b'], deprecation = \"" + msg + "\")");
    Package pkg = loadPackage("foo");

    Rule fooRule = (Rule) pkg.getTarget("bar");
    String deprAttr =
        attributes(fooRule).get("deprecation", com.google.devtools.build.lib.packages.Type.STRING);
    assertThat(deprAttr).isEqualTo(msg);
  }

  @Test
  public void testPackageFeatures() throws Exception {
    scratch.file(
        "a/BUILD",
        "sh_library(name='before')",
        "package(features=['b', 'c'])",
        "sh_library(name='after')");
    Package pkg = loadPackage("a");
    assertThat(pkg.getFeatures()).containsExactly("b", "c");
  }

  @Test
  public void testTransientErrorsInGlobbing() throws Exception {
    Path buildFile = scratch.file("e/BUILD", "sh_library(name = 'e', data = glob(['*']))");
    throwOnReaddir = buildFile.getParentDirectory();
    invalidatePackages();
    reporter.removeHandler(failFastHandler);
    assertThrows(NoSuchPackageException.class, () -> loadPackage("e")); // symlink cycle

    throwOnReaddir = null;
    invalidatePackages();

    reporter.addHandler(failFastHandler);
    Package pkg = loadPackage("e"); // no error
    assertThat(pkg.containsErrors()).isFalse();
    assertThat(pkg.getRule("e")).isNotNull();
    List<?> globList = (List) pkg.getRule("e").getAttr("data");
    assertThat(globList).containsExactly(Label.parseCanonical("//e:BUILD"));
  }

  @Test
  public void testExportTwicePublicOK() throws Exception {
    // In theory, this could be an error, but too many existing files rely on it
    // and it is okay.
    expectEvalSuccess(
        "exports_files([\"a.cc\"],",
        "    visibility = [ \"//visibility:public\" ])",
        "exports_files([\"a.cc\"],",
        "    visibility = [ \"//visibility:public\" ])");
  }

  @Test
  public void testExportTwicePublicOK2() throws Exception {
    expectEvalSuccess(
        "exports_files([\"a.cc\"],",
        "    visibility = [ \"//visibility:private\" ])",
        "exports_files([\"a.cc\"],",
        "    visibility = [ \"//visibility:private\" ])");
  }

  @Test
  public void testExportTwiceFail() throws Exception {
    expectEvalError(
        "visibility for exported file 'a.cc' declared twice",
        "exports_files([\"a.cc\"],",
        "    visibility = [ \"//visibility:private\" ])",
        "exports_files([\"a.cc\"],",
        "    visibility = [ \"//visibility:public\" ])");
  }

  @Test
  public void testExportTwiceFail2() throws Exception {
    expectEvalError(
        "visibility for exported file 'a.cc' declared twice",
        "exports_files([\"a.cc\"],",
        "    visibility = [ \"//visibility:public\" ])",
        "exports_files([\"a.cc\"],",
        "    visibility = [ \"//visibility:private\" ])");
  }

  @Test
  public void testExportLicenseTwice() throws Exception {
    expectEvalError(
        "licenses for exported file 'a.cc' declared twice",
        "exports_files([\"a.cc\"], licenses = [\"notice\"])",
        "exports_files([\"a.cc\"], licenses = [\"notice\"])");
  }

  @Test
  public void testExportGenruleConflict() throws Exception {
    expectEvalError(
        "generated file 'a.cc' in rule 'foo' conflicts with existing source file",
        "exports_files([\"a.cc\"],",
        "    visibility = [ \"//visibility:public\" ])",
        "genrule(name = 'foo',",
        "    outs = ['a.cc'],",
        "    cmd = '')");
  }

  @Test
  public void testGenruleExportConflict() throws Exception {
    expectEvalError(
        "generated label '//pkg:a.cc' conflicts with existing generated file",
        "genrule(name = 'foo',",
        "    outs = ['a.cc'],",
        "    cmd = '')",
        "exports_files([\"a.cc\"],",
        "    visibility = [ \"//visibility:public\" ])");
  }

  @Test
  public void testValidEnvironmentGroup() throws Exception {
    expectEvalSuccess(
        "environment(name = 'foo')",
        "environment_group(name='group', environments = [':foo'], defaults = [':foo'])");
  }

  @Test
  public void testIncompleteEnvironmentGroup() throws Exception {
    expectEvalError(
        "environment_group() missing 1 required named argument: defaults",
        "environment(name = 'foo')",
        "environment_group(name='group', environments = [':foo'])");
  }

  @Test
  public void testEnvironmentGroupMissingTarget() throws Exception {
    expectEvalError(
        "environment //pkg:foo does not exist",
        "environment_group(name='group', environments = [':foo'], defaults = [':foo'])");
  }

  @Test
  public void testEnvironmentGroupWrongTargetType() throws Exception {
    expectEvalError(
        "//pkg:foo is not a valid environment",
        "cc_library(name = 'foo')",
        "environment_group(name='group', environments = [':foo'], defaults = [':foo'])");
  }

  @Test
  public void testEnvironmentGroupWrongPackage() throws Exception {
    expectEvalError(
        "//foo:foo is not in the same package as group //pkg:group",
        "environment_group(name='group', environments = ['//foo'], defaults = ['//foo'])");
  }

  @Test
  public void testEnvironmentGroupInvalidDefault() throws Exception {
    expectEvalError(
        "default //pkg:bar is not a declared environment for group //pkg:group",
        "environment(name = 'foo')",
        "environment(name = 'bar')",
        "environment_group(name='group', environments = [':foo'], defaults = [':bar'])");
  }

  @Test
  public void testEnvironmentGroupDuplicateEnvironments() throws Exception {
    expectEvalError(
        "label '//pkg:foo' is duplicated in the 'environments' list of 'group'",
        "environment(name = 'foo')",
        "environment_group(name='group', environments = [':foo', ':foo'], defaults = [':foo'])");
  }

  @Test
  public void testEnvironmentGroupDuplicateDefaults() throws Exception {
    expectEvalError(
        "label '//pkg:foo' is duplicated in the 'defaults' list of 'group'",
        "environment(name = 'foo')",
        "environment_group(name='group', environments = [':foo'], defaults = [':foo', ':foo'])");
  }

  @Test
  public void testMultipleEnvironmentGroupsValidMembership() throws Exception {
    expectEvalSuccess(
        "environment(name = 'foo')",
        "environment(name = 'bar')",
        "environment_group(name='foo_group', environments = [':foo'], defaults = [':foo'])",
        "environment_group(name='bar_group', environments = [':bar'], defaults = [':bar'])");
  }

  @Test
  public void testMultipleEnvironmentGroupsConflictingMembership() throws Exception {
    expectEvalError(
        "environment //pkg:foo belongs to both //pkg:bar_group and //pkg:foo_group",
        "environment(name = 'foo')",
        "environment(name = 'bar')",
        "environment_group(name='foo_group', environments = [':foo'], defaults = [':foo'])",
        "environment_group(name='bar_group', environments = [':foo'], defaults = [':foo'])");
  }

  @Test
  public void testFulfillsReferencesWrongTargetType() throws Exception {
    expectEvalError(
        "in \"fulfills\" attribute of //pkg:foo: //pkg:bar is not a valid environment",
        "environment(name = 'foo', fulfills = [':bar'])",
        "cc_library(name = 'bar')",
        "environment_group(name='foo_group', environments = [':foo'], defaults = [])");
  }

  @Test
  public void testFulfillsNotInEnvironmentGroup() throws Exception {
    expectEvalError(
        "in \"fulfills\" attribute of //pkg:foo: //pkg:bar is not a member of this group",
        "environment(name = 'foo', fulfills = [':bar'])",
        "environment(name = 'bar')",
        "environment_group(name='foo_group', environments = [':foo'], defaults = [])");
  }

  @Test
  public void testPackageDefaultEnvironments() throws Exception {
    Package pkg =
        expectEvalSuccess(
            "package(",
            "    default_compatible_with=['//foo'],",
            "    default_restricted_to=['//bar'],",
            ")");
    assertThat(pkg.getDefaultCompatibleWith()).containsExactly(Label.parseCanonical("//foo"));
    assertThat(pkg.getDefaultRestrictedTo()).containsExactly(Label.parseCanonical("//bar"));
  }

  @Test
  public void testPackageDefaultCompatibilityDuplicates() throws Exception {
    expectEvalError(
        "'//foo:foo' is duplicated in the 'default_compatible_with' list",
        "package(default_compatible_with=['//foo', '//bar', '//foo'])");
  }

  @Test
  public void testPackageDefaultRestrictionDuplicates() throws Exception {
    expectEvalError(
        "'//foo:foo' is duplicated in the 'default_restricted_to' list",
        "package(default_restricted_to=['//foo', '//bar', '//foo'])");
  }

  @Test
  public void testGlobPatternExtractor() {
    StarlarkFile file =
        StarlarkFile.parse(
            ParserInput.fromLines(
                "pattern = '*'",
                "some_variable = glob([",
                "  '**/*',",
                "  'a' + 'b',",
                "  pattern,",
                "])",
                "other_variable = glob(include = ['a'], exclude = ['b'])",
                "third_variable = glob(['c'], exclude_directories = 0)"));
    List<String> globs = new ArrayList<>();
    List<String> globsWithDirs = new ArrayList<>();
    List<String> subpackages = new ArrayList<>();
    PackageFactory.checkBuildSyntax(
        file, globs, globsWithDirs, subpackages, new HashMap<>(), /* errors= */ null);
    assertThat(globs).containsExactly("ab", "a", "**/*");
    assertThat(globsWithDirs).containsExactly("c");
    assertThat(subpackages).isEmpty();
  }

  // Tests of BUILD file dialect checks:

  @Test
  public void testDefInBuild() throws Exception {
    checkBuildDialectError(
        "def func(): pass", //
        "functions may not be defined in BUILD files");
  }

  @Test
  public void testLambdaInBuild() throws Exception {
    checkBuildDialectError(
        "lambda: None", //
        "functions may not be defined in BUILD files");
  }

  @Test
  public void testForStatementForbiddenInBuild() throws Exception {
    checkBuildDialectError(
        "for _ in []: pass", //
        "for statements are not allowed in BUILD files");
  }

  @Test
  public void testIfStatementForbiddenInBuild() throws Exception {
    checkBuildDialectError(
        "if False: pass", //
        "if statements are not allowed in BUILD files");
  }

  @Test
  public void testKwargsForbiddenInBuild() throws Exception {
    checkBuildDialectError(
        "print(**dict)", //
        "**kwargs arguments are not allowed in BUILD files");
    checkBuildDialectError(
        "len(dict(**{'a': 1}))", //
        "**kwargs arguments are not allowed in BUILD files");
  }

  @Test
  public void testArgsForbiddenInBuild() throws Exception {
    checkBuildDialectError(
        "print(*['a'])", //
        "*args arguments are not allowed in BUILD files");
  }

  // Asserts that evaluation of the specified BUILD file produces the expected error.
  // Modifies: scratch, events, packages; be careful when calling more than once per @Test!
  private void checkBuildDialectError(String content, String expectedError) throws Exception {
    eventCollector.clear();
    reporter.removeHandler(failFastHandler);
    scratch.overwriteFile("p/BUILD", content);
    invalidatePackages();
    Package pkg = loadPackage("p");
    assertContainsEvent(expectedError);
    assertThat(pkg.containsErrors()).isTrue();
  }

  private Package expectEvalSuccess(String... content) throws Exception {
    scratch.file("pkg/BUILD", content);
    Package pkg = loadPackage("pkg");
    assertThat(pkg.containsErrors()).isFalse();
    return pkg;
  }

  private void expectEvalError(String expectedError, String... content) throws Exception {
    reporter.removeHandler(failFastHandler);
    scratch.file("pkg/BUILD", content);
    Package pkg = loadPackage("pkg");
    assertWithMessage("Expected evaluation error, but none was not reported")
        .that(pkg.containsErrors())
        .isTrue();
    assertContainsEvent(expectedError);
  }

  private static AttributeMap attributes(Rule rule) {
    return RawAttributeMapper.of(rule);
  }

  private static void assertGlob(Package pkg, List<String> expected, String... include)
      throws Exception {
    assertGlob(pkg, expected, ImmutableList.copyOf(include), ImmutableList.of());
  }

  private static void assertGlob(
      Package pkg, List<String> expected, List<String> include, List<String> exclude)
      throws Exception {
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    try {
      GlobCache globCache =
          new GlobCache(
              pkg.getFilename().asPath().getParentDirectory(),
              pkg.getPackageIdentifier(),
              ImmutableSet.of(),
              // a package locator that finds no packages
              new CachingPackageLocator() {
                @Override
                public Path getBuildFileForPackage(PackageIdentifier packageName) {
                  return null;
                }

                @Override
                public String getBaseNameForLoadedPackage(PackageIdentifier packageName) {
                  return null;
                }
              },
              SyscallCache.NO_CACHE,
              executorService,
              -1,
              ThreadStateReceiver.NULL_INSTANCE);
      assertThat(globCache.globUnsorted(include, exclude, Globber.Operation.FILES_AND_DIRS, true))
          .containsExactlyElementsIn(expected);
    } finally {
      executorService.shutdownNow();
    }
  }

  private Path emptyFile(String path) {
    try {
      return scratch.file(path);
    } catch (IOException e) {
      throw new IllegalStateException(e);
    }
  }

  /********************************************************************
   *                                                                  *
   *              Test "glob" function in build language              *
   *                                                                  *
   ********************************************************************/
  private void assertGlobFails(String globCallExpression, String expectedError) throws Exception {
    Package pkg = buildPackageWithGlob(globCallExpression);

    assertContainsEvent(expectedError);
    assertThat(pkg.containsErrors()).isTrue();
  }

  private Package buildPackageWithGlob(String globCallExpression) throws Exception {
    scratch.deleteFile("dummypackage/BUILD");
    scratch.file("dummypackage/BUILD", "x = " + globCallExpression);
    return loadPackage("dummypackage");
  }

  /**
   * Test globbing in the context of a package, using the build language. We use the specially setup
   * "globs" test package and the files beneath it.
   *
   * @param result the expected list of filenames that match the glob
   * @param includes an include pattern for the glob
   * @param excludes an exclude pattern for the glob
   * @param excludeDirs an exclude_directories flag for the glob
   * @throws Exception if the glob doesn't match the expected result.
   */
  private void assertGlobMatches(
      List<String> result, List<String> includes, List<String> excludes, boolean excludeDirs)
      throws Exception {
    // If the glob doesn't match the expected result, BUILD execution calls fail() which
    // posts an ERROR to the fail-fast handler, throwing AssertionError.
    Package pkg =
        evaluateGlob(
            includes,
            excludes,
            excludeDirs,
            String.format(
                "(result == sorted(%s)) or fail('incorrect glob result: got %%s, want %%s' %%"
                    + " (result, sorted(%s)))",
                Starlark.repr(result), Starlark.repr(result)));
    // Execution succeeded. Assert that there were no other errors in the package.
    assertThat(pkg.containsErrors()).isFalse();
  }

  /**
   * Evaluate a glob() call against a test directory and BUILD code to process the results.
   *
   * @param includes a list of glob patterns; glob will include these files.
   * @param excludes a list of glob patterns to exclude even if previously included.
   * @param excludeDirs true if directories should be excluded from the match.
   * @param resultAssertion code in the BUILD language that can access the variable result, to which
   *     the result of the glob will be bound, and that may contain an assertion on it.
   * @throws AssertionError if any ERROR events are reported to the fail-fast handler during
   *     execution.
   */
  // TODO(adonovan): these tests would be cleaner if they did print(glob(...)) as a side effect
  // of package loading so that the caller of loadPackage can extract and return the value,
  // for @Test methods to make assertions in the usual way.
  private Package evaluateGlob(
      List<String> includes, List<String> excludes, boolean excludeDirs, String resultAssertion)
      throws Exception {
    Path globsDir = scratch.dir("globs");
    globsDir.getChild("subdir").createDirectory();
    for (String file : ImmutableList.of("Wombat1.java", "Wombat2.java", "subdir/Wombat3.java")) {
      FileSystemUtils.createEmptyFile(globsDir.getRelative(file));
    }
    scratch.file(
        "globs/BUILD",
        String.format(
            "result = glob(%s, exclude=%s, exclude_directories=%d, allow_empty = True)",
            Starlark.repr(includes), Starlark.repr(excludes), excludeDirs ? 1 : 0),
        resultAssertion);
    return loadPackage("globs");
  }

  private void assertGlobProducesError(String pattern, boolean errorExpected) throws Exception {
    reporter.removeHandler(failFastHandler);
    Package pkg = evaluateGlob(ImmutableList.of(pattern), ImmutableList.of(), false, "");
    assertThat(pkg.containsErrors()).isEqualTo(errorExpected);
    boolean foundError = false;
    for (Event event : eventCollector) {
      if (event.getMessage().contains("glob")) {
        if (!errorExpected) {
          fail("error not expected for glob pattern " + pattern + ", but got: " + event);
          return;
        }
        foundError = errorExpected;
        break;
      }
    }
    assertThat(foundError).isEqualTo(errorExpected);
  }

  private Package loadPackage(String pkgid) throws Exception {
    return getTarget("//" + pkgid + ":BUILD").getPackage();
  }
}
