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

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

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.events.EventKind;
import com.google.devtools.build.lib.packages.ConstantRuleVisibility;
import com.google.devtools.build.lib.packages.util.SubincludePreprocessor;
import com.google.devtools.build.lib.pkgcache.PackageCacheOptions;
import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
import com.google.devtools.build.lib.util.BlazeClock;
import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
import com.google.devtools.build.lib.vfs.FileStatus;
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.common.options.Options;
import java.io.IOException;
import java.util.Collection;
import java.util.UUID;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class SkyframeLabelVisitorTest extends SkyframeLabelVisitorTestCase {
  @Test
  public void testLabelVisitorDetectsMissingPackages() throws Exception {
    reporter.removeHandler(failFastHandler); // expect errors

    scratch.file(
        "pkg/BUILD", "sh_library(name = 'x', deps = ['//nopkg:y', 'z'])", "sh_library(name = 'z')");

    assertLabelsVisitedWithErrors(
        ImmutableSet.of("//pkg:x", "//pkg:z"), ImmutableSet.of("//pkg:x"));
    assertContainsEvent("no such package 'nopkg'");
  }

  /**
   * Tests that Blaze is resilient to changing symlinks between builds. This test is a more
   * "integrated" version of FilesystemValueCheckerTest#testDirtySymlink.
   */
  @Test
  public void testChangingSymlink() throws Exception {
    Path path = scratch.file("foo/BUILD", "sh_library(name = 'foo')");
    Path sym1 = scratch.resolve(rootDirectory + "/sym1/BUILD");
    Path sym2 = scratch.resolve(rootDirectory + "/sym2/BUILD");
    Path symlink = scratch.resolve(rootDirectory + "/bar/BUILD");
    FileSystemUtils.ensureSymbolicLink(symlink, sym1);
    FileSystemUtils.ensureSymbolicLink(sym1, path);
    FileSystemUtils.ensureSymbolicLink(sym2, path);
    scratch.file("unrelated/BUILD", "sh_library(name = 'unrelated')");
    assertLabelsVisited(
        ImmutableSet.of("//bar:foo"), ImmutableSet.of("//bar:foo"), !EXPECT_ERROR, !KEEP_GOING);
    assertTrue(sym1.delete());
    FileSystemUtils.ensureSymbolicLink(sym1, sym2);
    syncPackages();
    assertLabelsVisited(
        ImmutableSet.of("//unrelated:unrelated"),
        ImmutableSet.of("//unrelated:unrelated"),
        !EXPECT_ERROR,
        !KEEP_GOING);
    assertTrue(sym1.delete());
    FileSystemUtils.ensureSymbolicLink(sym1, path);
    assertTrue(symlink.delete());
    symlink = scratch.file("bar/BUILD", "sh_library(name = 'bar')");
    syncPackages();
    assertLabelsVisited(
        ImmutableSet.of("//bar:bar"), ImmutableSet.of("//bar:bar"), !EXPECT_ERROR, !KEEP_GOING);
  }

  @Test
  public void testFailFastLoading() throws Exception {
    reporter.removeHandler(failFastHandler); // expect errors

    Path buildFile =
        scratch.file(
            "pkg/BUILD", "sh_library(name = 'x', deps = ['z', 'z'])", "sh_library(name = 'z')");

    // In the first case below, we will hit see an error on "//pkg:x", and therefore
    // not traverse into "//pkg:z" due to fail-fast.
    assertLabelsVisited(
        ImmutableSet.of("//pkg:x"), ImmutableSet.of("//pkg:x"), EXPECT_ERROR, !KEEP_GOING);
    assertContainsEvent("Label '//pkg:z' is duplicated in the 'deps' attribute of rule 'x'");
    assertLabelsVisitedWithErrors(
        ImmutableSet.of("//pkg:x", "//pkg:z"), ImmutableSet.of("//pkg:x"));

    // Also make sure reloading works if the package has changed, but the names
    // of the targets have not.
    scratch.overwriteFile(
        "pkg/BUILD", "sh_library(name = 'x', deps = ['z'])", "sh_library(name = 'z')");
    buildFile.setLastModifiedTime(buildFile.getLastModifiedTime() + 1);
    syncPackages();
    assertLabelsVisited(
        ImmutableSet.of("//pkg:x", "//pkg:z"),
        ImmutableSet.of("//pkg:x"),
        !EXPECT_ERROR,
        !KEEP_GOING);
    // Check stability (not redundant).
    assertLabelsVisited(
        ImmutableSet.of("//pkg:x", "//pkg:z"),
        ImmutableSet.of("//pkg:x"),
        !EXPECT_ERROR,
        !KEEP_GOING);
  }

  @Test
  public void testNewFailure() throws Exception {
    reporter.removeHandler(failFastHandler); // expect errors

    Path buildFile =
        scratch.file("pkg/BUILD", "sh_library(name = 'x', deps = ['z'])", "sh_library(name = 'z')");
    assertLabelsVisited(
        ImmutableSet.of("//pkg:x", "//pkg:z"),
        ImmutableSet.of("//pkg:x"),
        !EXPECT_ERROR,
        !KEEP_GOING);

    scratch.overwriteFile(
        "pkg/BUILD", "sh_library(name = 'x', deps = ['z', 'z'])", "sh_library(name = 'z')");
    buildFile.setLastModifiedTime(buildFile.getLastModifiedTime() + 1);
    syncPackages();
    assertLabelsVisited(
        ImmutableSet.of("//pkg:x"), ImmutableSet.of("//pkg:x"), EXPECT_ERROR, !KEEP_GOING);
    // Check stability (not redundant).
    assertLabelsVisited(
        ImmutableSet.of("//pkg:x"), ImmutableSet.of("//pkg:x"), EXPECT_ERROR, !KEEP_GOING);
  }

  @Test
  public void testNewTransitiveFailure() throws Exception {
    reporter.removeHandler(failFastHandler); // expect errors

    Path buildFile =
        scratch.file("pkg/BUILD", "sh_library(name = 'x', deps = ['z'])", "sh_library(name = 'z')");
    scratch.file("pkg2/BUILD", "sh_library(name = 'q', deps=['F','F'])", "sh_library(name = 'F')");
    assertLabelsVisited(
        ImmutableSet.of("//pkg:x", "//pkg:z"),
        ImmutableSet.of("//pkg:x"),
        !EXPECT_ERROR,
        !KEEP_GOING);

    scratch.overwriteFile(
        "pkg/BUILD",
        "sh_library(name = 'x', deps = ['z'])",
        "sh_library(name = 'z', deps = [ '//pkg2:q'])");
    buildFile.setLastModifiedTime(buildFile.getLastModifiedTime() + 1);
    syncPackages();

    assertLabelsVisited(
        ImmutableSet.of("//pkg:x", "//pkg:z", "//pkg2:q", "//pkg2:F"),
        ImmutableSet.of("//pkg:x"),
        EXPECT_ERROR,
        KEEP_GOING);
    // Check stability (not redundant).
    assertLabelsVisited(
        ImmutableSet.of("//pkg:x", "//pkg:z", "//pkg2:q", "//pkg2:F"),
        ImmutableSet.of("//pkg:x"),
        EXPECT_ERROR,
        KEEP_GOING);
  }

  @Test
  public void testAddDepInNewPkg() throws Exception {
    Path buildFile =
        scratch.file("pkg/BUILD", "sh_library(name = 'x', deps = ['z'])", "sh_library(name = 'z')");
    scratch.file("pkg2/BUILD", "sh_library(name = 'q')");

    assertLabelsVisited(
        ImmutableSet.of("//pkg:x", "//pkg:z"),
        ImmutableSet.of("//pkg:x"),
        !EXPECT_ERROR,
        !KEEP_GOING);

    scratch.overwriteFile(
        "pkg/BUILD", "sh_library(name = 'x', deps = ['z', '//pkg2:q'])", "sh_library(name = 'z')");
    buildFile.setLastModifiedTime(buildFile.getLastModifiedTime() + 1);
    syncPackages();

    assertLabelsVisited(
        ImmutableSet.of("//pkg:x", "//pkg:z", "//pkg2:q"),
        ImmutableSet.of("//pkg:x"),
        !EXPECT_ERROR,
        !KEEP_GOING);
    // Check stability (not redundant).
    assertLabelsVisited(
        ImmutableSet.of("//pkg:x", "//pkg:z", "//pkg2:q"),
        ImmutableSet.of("//pkg:x"),
        !EXPECT_ERROR,
        !KEEP_GOING);
  }

  // Regression test for: "IllegalArgumentException thrown during build."  This happened if "."
  // occurred in a label name segment.
  @Test
  public void testDotLabelName() throws Exception {
    scratch.file("pkg/BUILD", "exports_files(srcs = ['.', 'x/.'])");

    assertLabelsVisited(
        ImmutableSet.of("//pkg:.", "//pkg:x/."),
        ImmutableSet.of("//pkg:.", "//pkg:x/."),
        !EXPECT_ERROR,
        !KEEP_GOING);

    syncPackages();

    assertLabelsVisited(
        ImmutableSet.of("//pkg:.", "//pkg:x/."),
        ImmutableSet.of("//pkg:.", "//pkg:x/."),
        !EXPECT_ERROR,
        !KEEP_GOING);
  }

  @Test
  public void testLabelVisitorPlural() throws Exception {
    reporter.removeHandler(failFastHandler); // expect errors

    scratch.file(
        "pkg/BUILD",
        "sh_library(name = 'x', deps = ['//nopkg:y', 'z'])",
        "sh_library(name = 'z')",
        "sh_library(name = 'o', deps = ['//nopkg2:o'])");

    assertLabelsVisitedWithErrors(
        ImmutableSet.of("//pkg:x", "//pkg:z", "//pkg:o"), ImmutableSet.of("//pkg:x", "//pkg:o"));
    assertContainsEvent("no such package 'nopkg'");
    assertContainsEvent("no such package 'nopkg2'");
  }

  // Indirectly tests that there are dependencies between packages and their subpackages.
  @Test
  public void testSubpackageBoundaryAdd() throws Exception {
    scratch.file(
        "x/BUILD", "sh_library(name = 'x', deps = ['//x:y/z'])", "sh_library(name = 'y/z')");

    assertLabelsVisited(
        ImmutableSet.of("//x:x", "//x:y/z"), ImmutableSet.of("//x:x"), !EXPECT_ERROR, !KEEP_GOING);

    scratch.file("x/y/BUILD", "sh_library(name = 'z')");
    syncPackages(
        ModifiedFileSet.builder()
            .modify(new PathFragment("x/y"))
            .modify(new PathFragment("x/y/BUILD"))
            .build());

    reporter.removeHandler(failFastHandler); // expect errors
    assertLabelsVisitedWithErrors(ImmutableSet.of("//x:x"), ImmutableSet.of("//x:x"));
    assertContainsEvent("Label '//x:y/z' crosses boundary of subpackage 'x/y'");
  }

  // Indirectly tests that there are dependencies between packages and their subpackages.
  @Test
  public void testSubpackageBoundaryDelete() throws Exception {
    reporter.removeHandler(failFastHandler); // expect errors
    scratch.file(
        "x/BUILD", "sh_library(name = 'x', deps = ['//x:y/z'])", "sh_library(name = 'y/z')");
    scratch.file("x/y/BUILD", "sh_library(name = 'z')");
    assertLabelsVisitedWithErrors(ImmutableSet.of("//x:x"), ImmutableSet.of("//x:x"));
    assertContainsEvent("Label '//x:y/z' crosses boundary of subpackage 'x/y'");

    scratch.deleteFile("x/y/BUILD");
    syncPackages(ModifiedFileSet.builder().modify(new PathFragment("x/y/BUILD")).build());

    reporter.addHandler(failFastHandler); // don't expect errors
    assertLabelsVisited(
        ImmutableSet.of("//x:x", "//x:y/z"), ImmutableSet.of("//x:x"), !EXPECT_ERROR, !KEEP_GOING);
  }

  @Test
  public void testInterruptPending() throws Exception {
    scratch.file("x/BUILD");
    Thread.currentThread().interrupt();

    try {
      assertLabelsVisitedWithErrors(ImmutableSet.of("//x:x"), ImmutableSet.of("//x:BUILD"));
      fail();
    } catch (InterruptedException e) {
      // Expected
    }
  }

  // Regression test for "crash when // encountered in package name".
  @Test
  public void testDoubleSlashInPackageName() throws Exception {
    reporter.removeHandler(failFastHandler); // expect errors
    scratch.file("x/BUILD", "sh_library(name='x', deps=['//x//y'])");
    assertLabelsVisitedWithErrors(ImmutableSet.of("//x:x"), ImmutableSet.of("//x"));
    assertContainsEvent(
        "//x:x: invalid label '//x//y' in element 0 of attribute "
            + "'deps' in 'sh_library' rule: invalid package name 'x//y': "
            + "package names may not contain '//' path separators");
  }

  // Regression test for "Bazel hangs on input of illegal rule".
  @Test
  public void testCrashInLoadPackageIsReportedEffectively() throws Exception {
    reporter.removeHandler(failFastHandler);
    // Inject a NullPointerException into loadPackage().  This is triggered by
    // any ERROR event.
    reporter.addHandler(
        new EventHandler() {
          @Override
          public void handle(Event event) {
            if (EventKind.ERRORS.contains(event.getKind())) {
              throw new NullPointerException("oops");
            }
          }
        });

    // Visitation of //x reaches package "bad" by many paths.  The first time,
    // loadPackage() crashes (because of the injected NPE).  Previously,
    // on a subsequent visitation, the visitor would get livelocked due the
    // stale PendingEntry stuck in the PackageCache.  With the fix, the NPE is
    // thrown.
    scratch.file("bad/BUILD", "this is a bad build file");
    scratch.file(
        "x/BUILD",
        "sh_library(name='x', ",
        "           deps=['//bad:a', '//bad:b', '//bad:c',",
        "                 '//bad:d', '//bad:e', '//bad:f'])");

    try {
      // Used to get stuck.
      assertLabelsVisitedWithErrors(ImmutableSet.of("//x:x"), ImmutableSet.of("//x"));
      fail(); // unreachable
    } catch (NullPointerException npe) {
      // This is expected for legacy blaze.
    } catch (RuntimeException re) {
      // This is expected for Skyframe blaze.
      assertThat(re.getCause()).isInstanceOf(NullPointerException.class);
    }
  }

  // Regression test for: "Need better context for missing build file error due to
  // use in visibility rule".
  @Test
  public void testErrorMessageContainsTarget() throws Exception {
    reporter.removeHandler(failFastHandler); // expect errors

    scratch.file(
        "a/BUILD",
        "package_group(name = 'pkgs', includes = ['//not/a/package:pkgs'])",
        "sh_library(name = 'foo', visibility = [':pkgs'])");

    assertLabelsVisitedWithErrors(
        ImmutableSet.of("//a:foo", "//a:pkgs"), ImmutableSet.of("//a:foo"));
    assertContainsEvent(
        "in target '//a:pkgs', no such label '//not/a/package:pkgs': no "
            + "such package 'not/a/package'");
  }

  @Test
  public void testKeepGoing() throws Exception {
    reporter.removeHandler(failFastHandler);
    scratch.file(
        "parent/BUILD",
        "sh_library(name = 'parent', deps = ['//child:child'])",
        "invalidbuildsyntax");
    scratch.file("child/BUILD", "sh_library(name = 'child')", "invalidbuildsyntax");
    assertLabelsVisited(
        ImmutableSet.of("//parent:parent", "//child:child"),
        ImmutableSet.of("//parent:parent"),
        EXPECT_ERROR,
        KEEP_GOING);
  }

  /**
   * In the case of Skyframe we print a warning inside SkyframeLabelVisitor because the existing
   * interfaces forces us to do the keep_going + show warning logic there.
   */
  @Test
  public void testNewBuildFileConflict() throws Exception {
    Collection<Event> warnings = assertNewBuildFileConflict();
    assertThat(warnings).hasSize(1);
    assertThat(warnings.iterator().next().toString())
        .contains("errors encountered while loading target '//pkg:x'");
  }

  @Test
  public void testWithNoSubincludes() throws Exception {
    // This test uses the preprocessor.
    preprocessorFactorySupplier.inject(
        new SubincludePreprocessor(
            scratch.getFileSystem(), getSkyframeExecutor().getPackageManager()));
    PackageCacheOptions packageCacheOptions = Options.getDefaults(PackageCacheOptions.class);
    packageCacheOptions.defaultVisibility = ConstantRuleVisibility.PRIVATE;
    packageCacheOptions.showLoadingProgress = true;
    packageCacheOptions.globbingThreads = 7;
    getSkyframeExecutor()
        .preparePackageLoading(
            new PathPackageLocator(outputBase, ImmutableList.of(rootDirectory)),
            packageCacheOptions,
            loadingMock.getDefaultsPackageContent(),
            UUID.randomUUID(),
            ImmutableMap.<String, String>of(),
            new TimestampGranularityMonitor(BlazeClock.instance()));
    this.visitor = getSkyframeExecutor().pkgLoader();
    scratch.file("pkg/BUILD", "sh_library(name = 'x', deps = ['z'])", "sh_library(name = 'z')");
    assertLabelsVisited(
        ImmutableSet.of("//pkg:x", "//pkg:z"),
        ImmutableSet.of("//pkg:x"),
        !EXPECT_ERROR,
        !KEEP_GOING);
    assertLabelsVisited(
        ImmutableSet.of("//pkg:x", "//pkg:z"),
        ImmutableSet.of("//pkg:x"),
        !EXPECT_ERROR,
        !KEEP_GOING);

    scratch.file("hassub/BUILD", "subinclude('//sub:sub')");
    scratch.file("sub/BUILD", "exports_files(['sub'])");
    scratch.file("sub/sub", "sh_library(name='zzz')");

    assertLabelsVisited(
        ImmutableSet.of("//hassub:zzz"),
        ImmutableSet.of("//hassub:zzz"),
        !EXPECT_ERROR,
        !KEEP_GOING);
  }

  // Regression test for: "package loading ignores subincludes for purposes of checking for
  // subpackages cutting of labels"
  //
  // Indirectly tests that there are dependencies between a package and other packages that could
  // potentially cutoff its subincludes.
  @Test
  public void testSubpackageBoundarySubincludes() throws Exception {
    // This test uses the python preprocessor.
    preprocessorFactorySupplier.inject(
        new SubincludePreprocessor(
            scratch.getFileSystem(), getSkyframeExecutor().getPackageManager()));
    PackageCacheOptions packageCacheOptions = Options.getDefaults(PackageCacheOptions.class);
    packageCacheOptions.defaultVisibility = ConstantRuleVisibility.PRIVATE;
    packageCacheOptions.showLoadingProgress = true;
    packageCacheOptions.globbingThreads = 7;
    getSkyframeExecutor()
        .preparePackageLoading(
            new PathPackageLocator(outputBase, ImmutableList.of(rootDirectory)),
            packageCacheOptions,
            loadingMock.getDefaultsPackageContent(),
            UUID.randomUUID(),
            ImmutableMap.<String, String>of(),
            new TimestampGranularityMonitor(BlazeClock.instance()));
    this.visitor = getSkyframeExecutor().pkgLoader();
    scratch.file("a/BUILD", "subinclude('//b:c/d/foo')");
    scratch.file("b/BUILD", "exports_files(['c/d/foo'])");
    scratch.file("b/c/d/foo", "sh_library(name = 'a')");

    assertLabelsVisited(
        ImmutableSet.of("//a:a"), ImmutableSet.of("//a:a"), !EXPECT_ERROR, !KEEP_GOING);

    Path subpackageBuildFile = scratch.file("b/c/BUILD", "exports_files(['foo'])");
    syncPackages(ModifiedFileSet.builder().modify(new PathFragment("b/c/BUILD")).build());

    reporter.removeHandler(failFastHandler); // expect errors
    assertLabelsVisitedWithErrors(ImmutableSet.of("//a:a"), ImmutableSet.of("//a:a"));
    assertContainsEvent("Label '//b:c/d/foo' crosses boundary of subpackage 'b/c'");

    subpackageBuildFile.delete();
    syncPackages(ModifiedFileSet.builder().modify(new PathFragment("b/c/BUILD")).build());

    assertLabelsVisited(
        ImmutableSet.of("//a:a"), ImmutableSet.of("//a:a"), !EXPECT_ERROR, !KEEP_GOING);
  }

  // Regression test for: "ClassCastException in SkyframeLabelVisitor.sync()"
  @Test
  public void testRootCauseOnInconsistentFilesystem() throws Exception {
    reporter.removeHandler(failFastHandler);
    scratch.file("foo/BUILD", "sh_library(name = 'foo', deps = ['//bar:baz/fizz'])");
    Path barBuildFile = scratch.file("bar/BUILD", "sh_library(name = 'bar/baz')");
    Path bazDir = barBuildFile.getParentDirectory().getRelative("baz");
    scratch.file("bar/baz/BUILD");
    FileStatus inconsistentParentFileStatus =
        new FileStatus() {
          @Override
          public boolean isFile() {
            return true;
          }

          @Override
          public boolean isSpecialFile() {
            return false;
          }

          @Override
          public boolean isDirectory() {
            return false;
          }

          @Override
          public boolean isSymbolicLink() {
            return false;
          }

          @Override
          public long getSize() throws IOException {
            return 0;
          }

          @Override
          public long getLastModifiedTime() throws IOException {
            return 0;
          }

          @Override
          public long getLastChangeTime() throws IOException {
            return 0;
          }

          @Override
          public long getNodeId() throws IOException {
            return 0;
          }
        };
    fs.stubStat(bazDir, inconsistentParentFileStatus);
    Label fooLabel = Label.parseAbsolute("//foo:foo");
    getSkyframeExecutor()
        .getPackageManager()
        .newTransitiveLoader()
        .sync(
            reporter, /*targetsToVisit=*/
            ImmutableSet.of(getTarget(fooLabel)),
            ImmutableSet.<Label>of(), /*keepGoing=*/
            true,
            /*parallelThreads=*/ 100, /*maxDepth=*/
            -1);
    assertContainsEvent("Inconsistent filesystem operations");
  }
}
