// 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.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.cache.CompactPersistentActionCache;
import com.google.devtools.build.lib.testutil.BlazeTestUtils;
import com.google.devtools.build.lib.testutil.Suite;
import com.google.devtools.build.lib.testutil.TestSpec;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.UnixGlob;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.io.IOException;

/**
 * These tests belong to {@link TimestampBuilderTest}, but they're in a
 * separate class for now because they are a little slower.
 */
@TestSpec(size = Suite.MEDIUM_TESTS)
@RunWith(JUnit4.class)
public class TimestampBuilderMediumTest extends TimestampBuilderTestCase {
  private Path cacheRoot;
  private CompactPersistentActionCache cache;

  @Before
  public final void setCache() throws Exception  {
    // BlazeRuntime.setupLogging(Level.FINEST);  // Uncomment this for debugging.

    cacheRoot = scratch.dir("cacheRoot");
    cache = createCache();
  }

  private CompactPersistentActionCache createCache() throws IOException {
    return new CompactPersistentActionCache(cacheRoot, clock);
  }

  /**
   * Creates and returns a new caching builder based on a given {@code cache}.
   */
  private Builder persistentBuilder(CompactPersistentActionCache cache) throws Exception {
    return createBuilder(cache);
  }

  // TODO(blaze-team): (2009) :
  // - test timestamp monotonicity is not required (i.e. set mtime backwards)
  // - test change of key causes rebuild

  @Test
  public void testUnneededInputs() throws Exception {
    Artifact hello = createSourceArtifact("hello");
    BlazeTestUtils.makeEmptyFile(hello.getPath());
    Artifact optional = createSourceArtifact("hello.optional");
    Artifact goodbye = createDerivedArtifact("goodbye");
    Button button = createActionButton(Sets.newHashSet(hello, optional), Sets.newHashSet(goodbye));

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertTrue(button.pressed); // built

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertFalse(button.pressed); // not rebuilt

    // Creating a new persistent cache does not cause a rebuild
    cache.save();
    cache = createCache();
    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), hello);
    assertFalse(button.pressed); // not rebuilt

    BlazeTestUtils.makeEmptyFile(optional.getPath());

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertTrue(button.pressed); // built

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertFalse(button.pressed); // not rebuilt

    optional.getPath().delete();

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertTrue(button.pressed); // built

    // Creating a new persistent cache does not cause a rebuild
    cache.save();
    cache = createCache();
    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), hello);
    assertFalse(button.pressed); // not rebuilt
  }

  @Test
  public void testPersistentCache_ModifyingInputCausesActionReexecution() throws Exception {
    // /hello -> [action] -> /goodbye
    Artifact hello = createSourceArtifact("hello");
    BlazeTestUtils.makeEmptyFile(hello.getPath());
    Artifact goodbye = createDerivedArtifact("goodbye");
    Button button = createActionButton(Sets.newHashSet(hello), Sets.newHashSet(goodbye));

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertTrue(button.pressed); // built

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertFalse(button.pressed); // not rebuilt

    hello.getPath().setWritable(true);
    FileSystemUtils.writeContentAsLatin1(hello.getPath(), "new content");

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertTrue(button.pressed); // rebuilt

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertFalse(button.pressed); // not rebuilt

    // Creating a new persistent cache does not cause a rebuild
    cache.save();
    buildArtifacts(persistentBuilder(createCache()), goodbye);
    assertFalse(button.pressed); // not rebuilt
  }

  @Test
  public void testModifyingInputCausesActionReexecution() throws Exception {
    // /hello -> [action] -> /goodbye
    Artifact hello = createSourceArtifact("hello");
    FileSystemUtils.createDirectoryAndParents(hello.getPath().getParentDirectory());
    FileSystemUtils.writeContentAsLatin1(hello.getPath(), "content1");
    Artifact goodbye = createDerivedArtifact("goodbye");
    Button button = createActionButton(Sets.newHashSet(hello), Sets.newHashSet(goodbye));

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertTrue(button.pressed); // built

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertFalse(button.pressed); // not rebuilt

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertFalse(button.pressed); // still not rebuilt

    FileSystemUtils.writeContentAsLatin1(hello.getPath(), "content2");

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertTrue(button.pressed); // rebuilt

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertFalse(button.pressed); // not rebuilt

    // Creating a new persistent cache does not cause a rebuild
    cache.save();
    buildArtifacts(persistentBuilder(createCache()), goodbye);
    assertFalse(button.pressed); // not rebuilt
  }

  @Test
  public void testArtifactOrderingDoesNotMatter() throws Exception {
    // (/hello,/there) -> [action] -> /goodbye

    Artifact hello = createSourceArtifact("hello");
    Artifact there = createSourceArtifact("there");
    FileSystemUtils.createDirectoryAndParents(hello.getPath().getParentDirectory());
    FileSystemUtils.writeContentAsLatin1(hello.getPath(), "hello");
    FileSystemUtils.writeContentAsLatin1(there.getPath(), "there");
    Artifact goodbye = createDerivedArtifact("goodbye");
    Button button =
        createActionButton(
            Sets.newLinkedHashSet(ImmutableList.of(hello, there)), Sets.newHashSet(goodbye));

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertTrue(button.pressed); // built

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertFalse(button.pressed); // not rebuilt

    // Now create duplicate graph, with swapped order.
    clearActions();
    Artifact goodbye2 = createDerivedArtifact("goodbye");
    Button button2 =
        createActionButton(
            Sets.newLinkedHashSet(ImmutableList.of(there, hello)), Sets.newHashSet(goodbye2));

    button2.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertFalse(button2.pressed); // still not rebuilt
  }

  @Test
  public void testOldCacheKeysAreCleanedUp() throws Exception {
    // [action1] -> (/goodbye), cache key will be /goodbye
    Artifact goodbye = createDerivedArtifact("goodbye");
    FileSystemUtils.createDirectoryAndParents(goodbye.getPath().getParentDirectory());
    FileSystemUtils.writeContentAsLatin1(goodbye.getPath(), "test");
    Button button = createActionButton(emptySet, Sets.newLinkedHashSet(ImmutableList.of(goodbye)));

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertTrue(button.pressed); // built

    // action1 is cached using the cache key /goodbye.
    assertThat(cache.get(goodbye.getExecPathString())).isNotNull();

    // [action2] -> (/hello,/goodbye), cache key will be /hello
    clearActions();
    Artifact hello = createDerivedArtifact("hello");
    Artifact goodbye2 = createDerivedArtifact("goodbye");
    Button button2 =
        createActionButton(emptySet, Sets.newLinkedHashSet(ImmutableList.of(hello, goodbye2)));

    button2.pressed = false;
    buildArtifacts(persistentBuilder(cache), hello, goodbye2);
    assertTrue(button2.pressed); // rebuilt

    // action2 is cached using the cache key /hello.
    assertThat(cache.get(hello.getExecPathString())).isNotNull();

    // Now, action1 should no longer be in the cache.
    assertThat(cache.get(goodbye.getExecPathString())).isNull();
  }

  @Test
  public void testArtifactNamesMatter() throws Exception {
    // /hello -> [action] -> /goodbye

    Artifact hello = createSourceArtifact("hello");
    FileSystemUtils.createDirectoryAndParents(hello.getPath().getParentDirectory());
    FileSystemUtils.writeContentAsLatin1(hello.getPath(), "hello");
    Artifact goodbye = createDerivedArtifact("goodbye");
    Button button = createActionButton(Sets.newHashSet(hello), Sets.newHashSet(goodbye));

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertTrue(button.pressed); // built

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertFalse(button.pressed); // not rebuilt

    // Now create duplicate graph, replacing "hello" with "hi".
    clearActions();
    Artifact hi = createSourceArtifact("hi");
    FileSystemUtils.createDirectoryAndParents(hi.getPath().getParentDirectory());
    FileSystemUtils.writeContentAsLatin1(hi.getPath(), "hello");
    Artifact goodbye2 = createDerivedArtifact("goodbye");
    Button button2 = createActionButton(Sets.newHashSet(hi), Sets.newHashSet(goodbye2));

    button2.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye2);
    assertTrue(button2.pressed); // name changed. must rebuild.
  }

  @Test
  public void testDuplicateInputs() throws Exception {
    // (/hello,/hello) -> [action] -> /goodbye

    Artifact hello = createSourceArtifact("hello");
    FileSystemUtils.createDirectoryAndParents(hello.getPath().getParentDirectory());
    FileSystemUtils.writeContentAsLatin1(hello.getPath(), "hello");
    Artifact goodbye = createDerivedArtifact("goodbye");
    Button button =
        createActionButton(Lists.<Artifact>newArrayList(hello, hello), Sets.newHashSet(goodbye));

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertTrue(button.pressed); // built

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertFalse(button.pressed); // not rebuilt

    FileSystemUtils.writeContentAsLatin1(hello.getPath(), "hello2");

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertTrue(button.pressed); // rebuilt

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertFalse(button.pressed); // not rebuilt

    // Creating a new persistent cache does not cause a rebuild
    cache.save();
    buildArtifacts(persistentBuilder(createCache()), goodbye);
    assertFalse(button.pressed); // not rebuilt
  }

  /**
   * Tests that changing timestamp of the input file without changing it content
   * does not cause action reexecution when metadata cache uses file digests in
   * addition to the timestamp.
   */
  @Test
  public void testModifyingTimestampOnlyDoesNotCauseActionReexecution() throws Exception {
    // /hello -> [action] -> /goodbye
    Artifact hello = createSourceArtifact("hello");
    FileSystemUtils.createDirectoryAndParents(hello.getPath().getParentDirectory());
    FileSystemUtils.writeContentAsLatin1(hello.getPath(), "content1");
    Artifact goodbye = createDerivedArtifact("goodbye");
    Button button = createActionButton(Sets.newHashSet(hello), Sets.newHashSet(goodbye));

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertTrue(button.pressed); // built

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), goodbye);
    assertFalse(button.pressed); // not rebuilt

    // Creating a new persistent caches, including metadata cache does not cause
    // a rebuild
    cache.save();
    Builder builder = persistentBuilder(createCache());
    buildArtifacts(builder, goodbye);
    assertFalse(button.pressed); // not rebuilt
  }

  @Test
  public void testPersistentCache_ModifyingOutputCausesActionReexecution() throws Exception {
    // [action] -> /hello
    Artifact hello = createDerivedArtifact("hello");
    Button button = createActionButton(emptySet, Sets.newHashSet(hello));

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), hello);
    assertTrue(button.pressed); // built

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), hello);
    assertFalse(button.pressed); // not rebuilt

    hello.getPath().setWritable(true);
    FileSystemUtils.writeContentAsLatin1(hello.getPath(), "new content");

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), hello);
    assertTrue(button.pressed); // rebuilt

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), hello);
    assertFalse(button.pressed); // not rebuilt

    // Creating a new persistent cache does not cause a rebuild
    cache.save();
    buildArtifacts(persistentBuilder(createCache()), hello);
    assertFalse(button.pressed); // not rebuilt
  }

  @Test
  public void testPersistentCache_missingFilenameIndexCausesActionReexecution() throws Exception {
    // [action] -> /hello
    Artifact hello = createDerivedArtifact("hello");
    Button button = createActionButton(emptySet, Sets.newHashSet(hello));

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), hello);
    assertTrue(button.pressed); // built

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), hello);
    assertFalse(button.pressed); // not rebuilt

    hello.getPath().setWritable(true);
    FileSystemUtils.writeContentAsLatin1(hello.getPath(), "new content");

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), hello);
    assertTrue(button.pressed); // rebuilt

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), hello);
    assertFalse(button.pressed); // not rebuilt

    // Creating a new persistent cache does not cause a rebuild
    cache.save();

    // Remove filename index file.
    assertTrue(
        Iterables.getOnlyElement(
                UnixGlob.forPath(cacheRoot).addPattern("filename_index*").globInterruptible())
            .delete());

    // Now first cache creation attempt should cause IOException while renaming corrupted files.
    // Second attempt will initialize empty cache, causing rebuild.
    try {
      createCache();
      fail("Expected IOException");
    } catch (IOException e) {
      assertThat(e).hasMessage("Failed action cache referential integrity check: empty index");
    }

    buildArtifacts(persistentBuilder(createCache()), hello);
    assertTrue(button.pressed); // rebuilt due to the missing filename index
  }

  @Test
  public void testPersistentCache_failedIntegrityCheckCausesActionReexecution() throws Exception {
    // [action] -> /hello
    Artifact hello = createDerivedArtifact("hello");
    Button button = createActionButton(emptySet, Sets.newHashSet(hello));

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), hello);
    assertTrue(button.pressed); // built

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), hello);
    assertFalse(button.pressed); // not rebuilt

    hello.getPath().setWritable(true);
    FileSystemUtils.writeContentAsLatin1(hello.getPath(), "new content");

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), hello);
    assertTrue(button.pressed); // rebuilt

    button.pressed = false;
    buildArtifacts(persistentBuilder(cache), hello);
    assertFalse(button.pressed); // not rebuilt

    cache.save();

    // Get filename index path and store a copy of it.
    Path indexPath =
        Iterables.getOnlyElement(
            UnixGlob.forPath(cacheRoot).addPattern("filename_index*").globInterruptible());
    Path indexCopy = scratch.resolve("index_copy");
    FileSystemUtils.copyFile(indexPath, indexCopy);

    // Add extra records to the action cache and indexer.
    Artifact helloExtra = createDerivedArtifact("hello_extra");
    Button buttonExtra = createActionButton(emptySet, Sets.newHashSet(helloExtra));
    buildArtifacts(persistentBuilder(cache), helloExtra);
    assertTrue(buttonExtra.pressed); // built

    cache.save();
    assertTrue(indexPath.getFileSize() > indexCopy.getFileSize());

    // Validate current cache.
    buildArtifacts(persistentBuilder(createCache()), hello);
    assertFalse(button.pressed); // not rebuilt

    // Restore outdated file index.
    FileSystemUtils.copyFile(indexCopy, indexPath);

    // Now first cache creation attempt should cause IOException while renaming corrupted files.
    // Second attempt will initialize empty cache, causing rebuild.
    try {
      createCache();
      fail("Expected IOException");
    } catch (IOException e) {
      assertThat(e.getMessage()).contains("Failed action cache referential integrity check");
    }

    // Validate cache with incorrect (out-of-date) filename index.
    buildArtifacts(persistentBuilder(createCache()), hello);
    assertTrue(button.pressed); // rebuilt due to the out-of-date index
  }
}
