// Copyright 2022 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.sandbox;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.ArtifactRoot;
import com.google.devtools.build.lib.actions.ArtifactRoot.RootType;
import com.google.devtools.build.lib.actions.CommandLines.ParamFileActionInput;
import com.google.devtools.build.lib.actions.LocalHostCapacity;
import com.google.devtools.build.lib.actions.ParameterFile.ParameterFileType;
import com.google.devtools.build.lib.actions.Spawn;
import com.google.devtools.build.lib.actions.SpawnResult;
import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
import com.google.devtools.build.lib.exec.BinTools;
import com.google.devtools.build.lib.exec.SpawnRunner.SpawnExecutionContext;
import com.google.devtools.build.lib.exec.TreeDeleter;
import com.google.devtools.build.lib.exec.util.SpawnBuilder;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.sandbox.SpawnRunnerTestUtil.SpawnExecutionContextForTesting;
import com.google.devtools.build.lib.testutil.Scratch;
import com.google.devtools.build.lib.util.OS;
import com.google.devtools.build.lib.util.io.FileOutErr;
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.testing.junit.testparameterinjector.TestParameterInjector;
import java.io.IOException;
import java.time.Duration;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

/** Tests for {@link LinuxSandboxedSpawnRunner}. */
@RunWith(TestParameterInjector.class)
public final class LinuxSandboxedSpawnRunnerTest extends SandboxedSpawnRunnerTestCase {

  /** Tree deleter to use by default for all tests. */
  private static final TreeDeleter treeDeleter = new SynchronousTreeDeleter();

  @Before
  public void assumeRunningOnLinux() {
    assume().that(OS.getCurrent()).isEqualTo(OS.LINUX);
  }

  @Test
  public void exec_echoCommand_executesSuccessfully() throws Exception {
    LinuxSandboxedSpawnRunner runner = setupSandboxAndCreateRunner(createCommandEnvironment());
    Spawn spawn = new SpawnBuilder("echo", "echolalia").build();
    Path stdout = testRoot.getChild("stdout");
    SpawnExecutionContext policy = createSpawnExecutionContext(spawn, stdout);

    SpawnResult spawnResult = runner.exec(spawn, policy);

    assertThat(spawnResult.status()).isEqualTo(SpawnResult.Status.SUCCESS);
    assertThat(spawnResult.exitCode()).isEqualTo(0);
    assertThat(spawnResult.setupSuccess()).isTrue();
    assertThat(spawnResult.getWallTimeInMs()).isGreaterThan(0);
    assertThat(FileSystemUtils.readLines(stdout, UTF_8)).containsExactly("echolalia");
  }

  @Test
  public void exec_commandWithParamFiles_executesSuccessfully() throws Exception {
    CommandEnvironment commandEnvironment = createCommandEnvironment();
    LinuxSandboxedSpawnRunner runner = setupSandboxAndCreateRunner(commandEnvironment);
    Spawn spawn =
        new SpawnBuilder("cp", "params/param-file", "out")
            .withInput(
                new ParamFileActionInput(
                    PathFragment.create("params/param-file"),
                    ImmutableList.of("--foo", "--bar"),
                    ParameterFileType.UNQUOTED,
                    UTF_8))
            .withOutput("out")
            .build();
    SpawnExecutionContext policy = createSpawnExecutionContext(spawn);

    SpawnResult spawnResult = runner.exec(spawn, policy);

    assertThat(spawnResult.status()).isEqualTo(SpawnResult.Status.SUCCESS);
    Path paramFile = commandEnvironment.getExecRoot().getRelative("out");
    assertThat(paramFile.exists()).isTrue();
    assertThat(FileSystemUtils.readLines(paramFile, UTF_8))
        .containsExactly("--foo", "--bar")
        .inOrder();
  }

  @Test
  public void exec_spawnRunningBinTool_executesSuccessfully() throws Exception {
    CommandEnvironment commandEnvironment = createCommandEnvironment();
    LinuxSandboxedSpawnRunner runner = setupSandboxAndCreateRunner(commandEnvironment);
    BinTools.PathActionInput pathActionInput =
        new BinTools.PathActionInput(
            new Scratch().file("/execRoot/tool", "#!/bin/bash", "echo hello > $1"),
            PathFragment.create("_bin/tool"));
    Artifact output =
        ActionsTestUtil.createArtifact(
            ArtifactRoot.asDerivedRoot(
                commandEnvironment.getExecRoot(), RootType.Output, "blaze-out"),
            commandEnvironment.getExecRoot().getRelative("blaze-out/output"));
    Spawn spawn =
        new SpawnBuilder("_bin/tool", output.getExecPathString())
            .withInput(pathActionInput)
            .withOutput(output)
            .build();
    SpawnExecutionContext policy = createSpawnExecutionContext(spawn);

    SpawnResult spawnResult = runner.exec(spawn, policy);

    assertThat(spawnResult.status()).isEqualTo(SpawnResult.Status.SUCCESS);
    assertThat(FileSystemUtils.readLines(output.getPath(), UTF_8)).containsExactly("hello");
  }

  @Test
  public void exec_collectsExecutionStatistics() throws Exception {
    CommandEnvironment commandEnvironment = createCommandEnvironment();
    LinuxSandboxedSpawnRunner runner = setupSandboxAndCreateRunner(commandEnvironment);
    Path cpuTimeSpenderPath =
        SpawnRunnerTestUtil.copyCpuTimeSpenderIntoPath(commandEnvironment.getExecRoot());
    Duration minimumWallTimeToSpend = Duration.ofSeconds(10);
    // Because of e.g. interference, wall time taken may be much larger than CPU time used.
    Duration maximumWallTimeToSpend = Duration.ofSeconds(40);
    Duration minimumUserTimeToSpend = minimumWallTimeToSpend;
    Duration maximumUserTimeToSpend = minimumUserTimeToSpend.plus(Duration.ofSeconds(2));
    Duration minimumSystemTimeToSpend = Duration.ZERO;
    Duration maximumSystemTimeToSpend = minimumSystemTimeToSpend.plus(Duration.ofSeconds(2));
    Spawn spawn =
        new SpawnBuilder(
                cpuTimeSpenderPath.getPathString(),
                String.valueOf(minimumUserTimeToSpend.getSeconds()),
                String.valueOf(minimumSystemTimeToSpend.getSeconds()))
            .build();
    SpawnExecutionContextForTesting policy = createSpawnExecutionContext(spawn);

    SpawnResult spawnResult = runner.exec(spawn, policy);

    assertThat(spawnResult.status()).isEqualTo(SpawnResult.Status.SUCCESS);
    assertThat(spawnResult.exitCode()).isEqualTo(0);
    assertThat(spawnResult.setupSuccess()).isTrue();
    assertThat(spawnResult.getWallTimeInMs()).isAtLeast((int) minimumWallTimeToSpend.toMillis());
    assertThat(spawnResult.getWallTimeInMs()).isAtMost((int) maximumWallTimeToSpend.toMillis());
    assertThat(spawnResult.getUserTimeInMs()).isAtLeast((int) minimumUserTimeToSpend.toMillis());
    assertThat(spawnResult.getUserTimeInMs()).isAtMost((int) maximumUserTimeToSpend.toMillis());
    assertThat(spawnResult.getSystemTimeInMs())
        .isAtLeast((int) minimumSystemTimeToSpend.toMillis());
    assertThat(spawnResult.getSystemTimeInMs()).isAtMost((int) maximumSystemTimeToSpend.toMillis());
    assertThat(spawnResult.getNumBlockOutputOperations()).isAtLeast(0L);
    assertThat(spawnResult.getNumBlockInputOperations()).isAtLeast(0L);
    assertThat(spawnResult.getNumInvoluntaryContextSwitches()).isAtLeast(0L);
  }

  @Test
  public void hermeticTmp_tmpCreatedAndMounted() throws Exception {
    runtimeWrapper.addOptions("--incompatible_sandbox_hermetic_tmp");
    CommandEnvironment commandEnvironment = createCommandEnvironment();
    LinuxSandboxedSpawnRunner runner = setupSandboxAndCreateRunner(commandEnvironment);
    Spawn spawn = new SpawnBuilder().build();
    SandboxedSpawn sandboxedSpawn = runner.prepareSpawn(spawn, createSpawnExecutionContext(spawn));

    Path sandboxPath =
        sandboxedSpawn.getSandboxExecRoot().getParentDirectory().getParentDirectory();
    Path hermeticTmpPath = sandboxPath.getRelative("_hermetic_tmp");
    assertThat(hermeticTmpPath.isDirectory()).isTrue();

    assertThat(sandboxedSpawn).isInstanceOf(SymlinkedSandboxedSpawn.class);
    String args = String.join(" ", sandboxedSpawn.getArguments());
    assertThat(args).contains("-w /tmp");
    assertThat(args).contains("-M " + hermeticTmpPath + " -m /tmp");
  }

  @Test
  public void hermeticTmp_sandboxTmpfsOnTmp_tmpNotCreatedOrMounted() throws Exception {
    runtimeWrapper.addOptions("--incompatible_sandbox_hermetic_tmp", "--sandbox_tmpfs_path=/tmp");
    CommandEnvironment commandEnvironment = createCommandEnvironment();
    LinuxSandboxedSpawnRunner runner = setupSandboxAndCreateRunner(commandEnvironment);
    Spawn spawn = new SpawnBuilder().build();
    SandboxedSpawn sandboxedSpawn = runner.prepareSpawn(spawn, createSpawnExecutionContext(spawn));

    Path sandboxPath =
        sandboxedSpawn.getSandboxExecRoot().getParentDirectory().getParentDirectory();
    Path hermeticTmpPath = sandboxPath.getRelative("_hermetic_tmp");
    assertThat(hermeticTmpPath.isDirectory()).isFalse();

    assertThat(sandboxedSpawn).isInstanceOf(SymlinkedSandboxedSpawn.class);
    String args = String.join(" ", sandboxedSpawn.getArguments());
    assertThat(args).contains("-w /tmp");
    assertThat(args).contains("-e /tmp");
    assertThat(args).doesNotContain("-m /tmp");
  }

  @Test
  public void hermeticTmp_sandboxTmpfsUnderTmp_tmpNotCreatedOrMounted() throws Exception {
    runtimeWrapper.addOptions(
        "--incompatible_sandbox_hermetic_tmp", "--sandbox_tmpfs_path=/tmp/subdir");
    CommandEnvironment commandEnvironment = createCommandEnvironment();
    LinuxSandboxedSpawnRunner runner = setupSandboxAndCreateRunner(commandEnvironment);
    Spawn spawn = new SpawnBuilder().build();
    SandboxedSpawn sandboxedSpawn = runner.prepareSpawn(spawn, createSpawnExecutionContext(spawn));

    Path sandboxPath =
        sandboxedSpawn.getSandboxExecRoot().getParentDirectory().getParentDirectory();
    Path hermeticTmpPath = sandboxPath.getRelative("_hermetic_tmp");
    assertThat(hermeticTmpPath.isDirectory()).isFalse();

    assertThat(sandboxedSpawn).isInstanceOf(SymlinkedSandboxedSpawn.class);
    String args = String.join(" ", sandboxedSpawn.getArguments());
    assertThat(args).contains("-w /tmp");
    assertThat(args).contains("-e /tmp");
    assertThat(args).doesNotContain("-m /tmp");
  }

  private static LinuxSandboxedSpawnRunner setupSandboxAndCreateRunner(
      CommandEnvironment commandEnvironment) throws IOException {
    Path execRoot = commandEnvironment.getExecRoot();
    execRoot.createDirectory();

    SpawnRunnerTestUtil.copyLinuxSandboxIntoPath(execRoot);

    Path sandboxBase = execRoot.getRelative("sandbox");
    sandboxBase.createDirectory();

    return LinuxSandboxedStrategy.create(
        new SandboxHelpers(),
        commandEnvironment,
        sandboxBase,
        /*timeoutKillDelay=*/ Duration.ofSeconds(2),
        /*sandboxfsProcess=*/ null,
        /*sandboxfsMapSymlinkTargets=*/ false,
        treeDeleter);
  }

  private SpawnExecutionContextForTesting createSpawnExecutionContext(Spawn spawn) {
    return createSpawnExecutionContext(spawn, testRoot.getChild("stdout"));
  }

  private SpawnExecutionContextForTesting createSpawnExecutionContext(Spawn spawn, Path stdout) {
    FileOutErr fileOutErr = new FileOutErr(stdout, testRoot.getChild("stderr"));
    return new SpawnExecutionContextForTesting(spawn, fileOutErr, Duration.ofMinutes(1));
  }

  private CommandEnvironment createCommandEnvironment() throws Exception {
    CommandEnvironment commandEnvironment = runtimeWrapper.newCommand();
    commandEnvironment.setWorkspaceName("workspace");
    commandEnvironment
        .getLocalResourceManager()
        .setAvailableResources(LocalHostCapacity.getLocalHostCapacity());
    return commandEnvironment;
  }
}
