// Copyright 2017 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.exec;

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

import com.google.common.collect.Maps;
import com.google.devtools.build.lib.actions.ActionInput;
import com.google.devtools.build.lib.actions.ActionInputFileCache;
import com.google.devtools.build.lib.actions.ActionInputHelper;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.EmptyRunfilesSupplier;
import com.google.devtools.build.lib.actions.Root;
import com.google.devtools.build.lib.actions.RunfilesSupplier;
import com.google.devtools.build.lib.analysis.Runfiles;
import com.google.devtools.build.lib.analysis.RunfilesSupplierImpl;
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.inmemoryfs.InMemoryFileSystem;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mockito;

/**
 * Tests for {@link SpawnInputExpander}.
 */
@RunWith(JUnit4.class)
public class SpawnInputExpanderTest {
  private FileSystem fs;
  private SpawnInputExpander expander;
  private Map<PathFragment, ActionInput> inputMappings;

  @Before
  public final void createSpawnInputExpander() throws Exception  {
    fs = new InMemoryFileSystem();
    expander = new SpawnInputExpander(/*strict=*/true);
    inputMappings = Maps.newHashMap();
  }

  private void scratchFile(String file, String... lines) throws Exception {
    Path path = fs.getPath(file);
    FileSystemUtils.createDirectoryAndParents(path.getParentDirectory());
    FileSystemUtils.writeLinesAs(path, StandardCharsets.UTF_8, lines);
  }

  @Test
  public void testEmptyRunfiles() throws Exception {
    RunfilesSupplier supplier = EmptyRunfilesSupplier.INSTANCE;
    expander.addRunfilesToInputs(inputMappings, supplier, null);
    assertThat(inputMappings).isEmpty();
  }

  @Test
  public void testRunfilesSingleFile() throws Exception {
    Artifact artifact =
        new Artifact(fs.getPath("/root/dir/file"), Root.asSourceRoot(fs.getPath("/root")));
    Runfiles runfiles = new Runfiles.Builder("workspace").addArtifact(artifact).build();
    RunfilesSupplier supplier = new RunfilesSupplierImpl(PathFragment.create("runfiles"), runfiles);
    ActionInputFileCache mockCache = Mockito.mock(ActionInputFileCache.class);
    Mockito.when(mockCache.isFile(artifact)).thenReturn(true);

    expander.addRunfilesToInputs(inputMappings, supplier, mockCache);
    assertThat(inputMappings).hasSize(1);
    assertThat(inputMappings)
        .containsEntry(PathFragment.create("runfiles/workspace/dir/file"), artifact);
  }

  @Test
  public void testRunfilesDirectoryStrict() throws Exception {
    Artifact artifact =
        new Artifact(fs.getPath("/root/dir/file"), Root.asSourceRoot(fs.getPath("/root")));
    Runfiles runfiles = new Runfiles.Builder("workspace").addArtifact(artifact).build();
    RunfilesSupplier supplier = new RunfilesSupplierImpl(PathFragment.create("runfiles"), runfiles);
    ActionInputFileCache mockCache = Mockito.mock(ActionInputFileCache.class);
    Mockito.when(mockCache.isFile(artifact)).thenReturn(false);

    try {
      expander.addRunfilesToInputs(inputMappings, supplier, mockCache);
      fail();
    } catch (IOException expected) {
      assertThat(expected.getMessage().contains("Not a file: /root/dir/file")).isTrue();
    }
  }

  @Test
  public void testRunfilesDirectoryNonStrict() throws Exception {
    Artifact artifact =
        new Artifact(fs.getPath("/root/dir/file"), Root.asSourceRoot(fs.getPath("/root")));
    Runfiles runfiles = new Runfiles.Builder("workspace").addArtifact(artifact).build();
    RunfilesSupplier supplier = new RunfilesSupplierImpl(PathFragment.create("runfiles"), runfiles);
    ActionInputFileCache mockCache = Mockito.mock(ActionInputFileCache.class);
    Mockito.when(mockCache.isFile(artifact)).thenReturn(false);

    expander = new SpawnInputExpander(/*strict=*/false);
    expander.addRunfilesToInputs(inputMappings, supplier, mockCache);
    assertThat(inputMappings).hasSize(1);
    assertThat(inputMappings)
        .containsEntry(PathFragment.create("runfiles/workspace/dir/file"), artifact);
  }

  @Test
  public void testRunfilesTwoFiles() throws Exception {
    Artifact artifact1 =
        new Artifact(fs.getPath("/root/dir/file"), Root.asSourceRoot(fs.getPath("/root")));
    Artifact artifact2 =
        new Artifact(fs.getPath("/root/dir/baz"), Root.asSourceRoot(fs.getPath("/root")));
    Runfiles runfiles = new Runfiles.Builder("workspace")
        .addArtifact(artifact1)
        .addArtifact(artifact2)
        .build();
    RunfilesSupplier supplier = new RunfilesSupplierImpl(PathFragment.create("runfiles"), runfiles);
    ActionInputFileCache mockCache = Mockito.mock(ActionInputFileCache.class);
    Mockito.when(mockCache.isFile(artifact1)).thenReturn(true);
    Mockito.when(mockCache.isFile(artifact2)).thenReturn(true);

    expander.addRunfilesToInputs(inputMappings, supplier, mockCache);
    assertThat(inputMappings).hasSize(2);
    assertThat(inputMappings)
        .containsEntry(PathFragment.create("runfiles/workspace/dir/file"), artifact1);
    assertThat(inputMappings)
        .containsEntry(PathFragment.create("runfiles/workspace/dir/baz"), artifact2);
  }

  @Test
  public void testRunfilesSymlink() throws Exception {
    Artifact artifact =
        new Artifact(fs.getPath("/root/dir/file"), Root.asSourceRoot(fs.getPath("/root")));
    Runfiles runfiles = new Runfiles.Builder("workspace")
        .addSymlink(PathFragment.create("symlink"), artifact).build();
    RunfilesSupplier supplier = new RunfilesSupplierImpl(PathFragment.create("runfiles"), runfiles);
    ActionInputFileCache mockCache = Mockito.mock(ActionInputFileCache.class);
    Mockito.when(mockCache.isFile(artifact)).thenReturn(true);

    expander.addRunfilesToInputs(inputMappings, supplier, mockCache);
    assertThat(inputMappings).hasSize(1);
    assertThat(inputMappings)
        .containsEntry(PathFragment.create("runfiles/workspace/symlink"), artifact);
  }

  @Test
  public void testRunfilesRootSymlink() throws Exception {
    Artifact artifact =
        new Artifact(fs.getPath("/root/dir/file"), Root.asSourceRoot(fs.getPath("/root")));
    Runfiles runfiles = new Runfiles.Builder("workspace")
        .addRootSymlink(PathFragment.create("symlink"), artifact).build();
    RunfilesSupplier supplier = new RunfilesSupplierImpl(PathFragment.create("runfiles"), runfiles);
    ActionInputFileCache mockCache = Mockito.mock(ActionInputFileCache.class);
    Mockito.when(mockCache.isFile(artifact)).thenReturn(true);

    expander.addRunfilesToInputs(inputMappings, supplier, mockCache);
    assertThat(inputMappings).hasSize(2);
    assertThat(inputMappings).containsEntry(PathFragment.create("runfiles/symlink"), artifact);
    // If there's no other entry, Runfiles adds an empty file in the workspace to make sure the
    // directory gets created.
    assertThat(inputMappings)
        .containsEntry(
            PathFragment.create("runfiles/workspace/.runfile"), SpawnInputExpander.EMPTY_FILE);
  }

  @Test
  public void testEmptyManifest() throws Exception {
    // See AnalysisUtils for the mapping from "foo" to "_foo/MANIFEST".
    scratchFile("/root/_foo/MANIFEST");

    Artifact artifact =
        new Artifact(fs.getPath("/root/foo"), Root.asSourceRoot(fs.getPath("/root")));
    expander.parseFilesetManifest(inputMappings, artifact, "workspace");
    assertThat(inputMappings).isEmpty();
  }

  @Test
  public void testManifestWithSingleFile() throws Exception {
    // See AnalysisUtils for the mapping from "foo" to "_foo/MANIFEST".
    scratchFile(
        "/root/out/_foo/MANIFEST",
        "workspace/bar /dir/file",
        "<some digest>");

    Root outputRoot = Root.asDerivedRoot(fs.getPath("/root"), fs.getPath("/root/out"), true);
    Artifact artifact = new Artifact(fs.getPath("/root/out/foo"), outputRoot);
    expander.parseFilesetManifest(inputMappings, artifact, "workspace");
    assertThat(inputMappings).hasSize(1);
    assertThat(inputMappings)
        .containsEntry(PathFragment.create("out/foo/bar"), ActionInputHelper.fromPath("/dir/file"));
  }

  @Test
  public void testManifestWithTwoFiles() throws Exception {
    // See AnalysisUtils for the mapping from "foo" to "_foo/MANIFEST".
    scratchFile(
        "/root/out/_foo/MANIFEST",
        "workspace/bar /dir/file",
        "<some digest>",
        "workspace/baz /dir/file",
        "<some digest>");

    Root outputRoot = Root.asDerivedRoot(fs.getPath("/root"), fs.getPath("/root/out"), true);
    Artifact artifact = new Artifact(fs.getPath("/root/out/foo"), outputRoot);
    expander.parseFilesetManifest(inputMappings, artifact, "workspace");
    assertThat(inputMappings).hasSize(2);
    assertThat(inputMappings)
        .containsEntry(PathFragment.create("out/foo/bar"), ActionInputHelper.fromPath("/dir/file"));
    assertThat(inputMappings)
        .containsEntry(PathFragment.create("out/foo/baz"), ActionInputHelper.fromPath("/dir/file"));
  }

  @Test
  public void testManifestWithDirectory() throws Exception {
    // See AnalysisUtils for the mapping from "foo" to "_foo/MANIFEST".
    scratchFile(
        "/root/out/_foo/MANIFEST",
        "workspace/bar /some",
        "<some digest>");

    Root outputRoot = Root.asDerivedRoot(fs.getPath("/root"), fs.getPath("/root/out"), true);
    Artifact artifact = new Artifact(fs.getPath("/root/out/foo"), outputRoot);
    expander.parseFilesetManifest(inputMappings, artifact, "workspace");
    assertThat(inputMappings).hasSize(1);
    assertThat(inputMappings)
        .containsEntry(
            PathFragment.create("out/foo/bar"), ActionInputHelper.fromPath("/some"));
  }
}
