#!/bin/bash
# Copyright 2018 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.

CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${CURRENT_DIR}/../integration_test_setup.sh" \
  || { echo "integration_test_setup.sh not found!" >&2; exit 1; }

# Bazel build arguments to disable the use of the sandbox.  We have tests below
# that configure a fake sandboxfs and they would fail if they were to use it.
DISABLE_SANDBOX_ARGS=(
  --genrule_strategy=local
  --spawn_strategy=local
)

# Creates a fake sandboxfs process in "path" that logs interactions with it in
# the given "log" file and reports the given "version".
function create_fake_sandboxfs() {
  local path="${1}"; shift
  local log="${1}"; shift
  local version="${1}"; shift

  cat >"${path}" <<EOF
#!/bin/sh

rm -f "${log}"
trap 'echo "Terminated" >>"${log}"' EXIT TERM

echo "PID: \${$}" >>"${log}"
echo "ARGS: \${*}" >>"${log}"

if [ "\${1}" = --version ]; then
  echo "${version}"
  exit 0
fi

while read line; do
  echo "Received: \${line}" >>"${log}"
  if [ -z "\${line}" ]; then
    echo "Done"
  fi
done
EOF
  chmod +x "${path}"
}

function create_hello_package() {
  mkdir -p hello

  cat >hello/BUILD <<EOF
cc_binary(name = "hello", srcs = ["hello.cc"])
EOF

  cat >hello/hello.cc <<EOF
#include <stdio.h>
int main(void) { printf("Hello, world!\n"); return 0; }
EOF
}

function test_default_sandboxfs_from_path() {
  mkdir -p fake-tools
  create_fake_sandboxfs fake-tools/sandboxfs "$(pwd)/log" "sandboxfs 0.0.0"
  PATH="$(pwd)/fake-tools:${PATH}"; export PATH

  create_hello_package

  # This test relies on a PATH change that is only recognized when the server
  # first starts up, so ensure there are no Bazel servers left behind.
  #
  # TODO(philwo): This is awful.  The testing infrastructure should ensure
  # that tests cannot pollute each other's state, but at the moment that's not
  # the case.
  bazel shutdown

  bazel build \
    "${DISABLE_SANDBOX_ARGS[@]}" \
    --experimental_use_sandboxfs \
    //hello >"${TEST_log}" 2>&1 || fail "Build should have succeeded"

  # Dump fake sandboxfs' log for debugging.
  sed -e 's,^,SANDBOXFS: ,' log >>"${TEST_log}"

  grep -q "Terminated" log \
    || fail "sandboxfs process was not terminated (not executed?)"
}

function test_explicit_sandboxfs_not_found() {
  create_hello_package

  bazel build \
    --experimental_use_sandboxfs \
    --experimental_sandboxfs_path="/non-existent/sandboxfs" \
    //hello >"${TEST_log}" 2>&1 && fail "Build succeeded but should have failed"

  expect_log "Failed to get sandboxfs version.*/non-existent/sandboxfs"
}

function test_explicit_sandboxfs_is_invalid() {
  mkdir -p fake-tools
  create_hello_package
  do_build() {
    bazel build \
      "${DISABLE_SANDBOX_ARGS[@]}" \
      --experimental_use_sandboxfs=yes \
      --experimental_sandboxfs_path="$(pwd)/fake-tools/sandboxfs" \
      //hello
  }

  # Try with a binary that prints an invalid version string.
  create_fake_sandboxfs fake-tools/sandboxfs "$(pwd)/log" "not-sandboxfs 0.0.0"
  do_build >"${TEST_log}" 2>&1 && fail "Build should have failed"

  # Now try with a valid binary just to ensure our test scenario works.
  create_fake_sandboxfs fake-tools/sandboxfs "$(pwd)/log" "sandboxfs 0.0.0"
  do_build >"${TEST_log}" 2>&1 || fail "Build should have succeeded"
  sed -e 's,^,SANDBOXFS: ,' log >>"${TEST_log}"

  grep -q "Terminated" log \
    || fail "sandboxfs process was not terminated (not executed?)"
}

function test_use_sandboxfs_if_present() {
  # This test relies on a PATH change that is only recognized when the server
  # first starts up, so ensure there are no Bazel servers left behind.
  #
  # TODO(philwo): This is awful.  The testing infrastructure should ensure
  # that tests cannot pollute each other's state, but at the moment that's not
  # the case.
  bazel shutdown

  mkdir -p fake-tools
  PATH="$(pwd)/fake-tools:${PATH}"; export PATH
  create_hello_package
  do_build() {
    bazel build \
      "${DISABLE_SANDBOX_ARGS[@]}" \
      --experimental_use_sandboxfs=auto \
      //hello
  }

  # Try with sandboxfs not in the PATH.
  do_build >"${TEST_log}" 2>&1 || fail "Build should have succeeded"
  [[ ! -f log ]] || echo "sandboxfs was used but should not have"

  # Now try with sandboxfs in the PATH to ensure our test scenario works.
  create_fake_sandboxfs fake-tools/sandboxfs "$(pwd)/log" "sandboxfs 0.0.0"
  do_build >"${TEST_log}" 2>&1 || fail "Build should have succeeded"
  sed -e 's,^,SANDBOXFS: ,' log >>"${TEST_log}"
  grep -q "Terminated" log \
    || fail "sandboxfs process was not terminated (not executed?)"
}

# Runs a build of the given target using a fake sandboxfs that captures its
# activity and dumps it to the given log file.
function build_with_fake_sandboxfs() {
  local log="${1}"; shift

  create_fake_sandboxfs fake-sandboxfs.sh "${log}" "sandboxfs 0.0.0"

  local ret=0
  bazel build \
    "${DISABLE_SANDBOX_ARGS[@]}" \
    --experimental_use_sandboxfs \
    --experimental_sandboxfs_path="$(pwd)/fake-sandboxfs.sh" \
    "${@}" >"${TEST_log}" 2>&1 || ret="${?}"

  # Dump fake sandboxfs' log for debugging.
  sed -e 's,^,SANDBOXFS: ,' log >>"${TEST_log}"

  return "${ret}"
}

function test_mount_unmount() {
  create_hello_package

  local output_base="$(bazel info output_base)"
  local sandbox_base="${output_base}/sandbox"

  build_with_fake_sandboxfs "$(pwd)/log" //hello \
    || fail "Build should have succeeded"

  grep -q "ARGS: .*${sandbox_base}/sandboxfs" log \
    || fail "Cannot find expected mount point in sandboxfs mount call"
  grep -q "Terminated" log \
    || fail "sandboxfs process was not terminated (not unmounted?)"
}

function test_debug_lifecycle() {
  create_hello_package

  function sandboxfs_pid() {
    case "$(uname)" in
      Darwin)
        # We cannot use ps to look for the sandbox process because this is
        # not allowed when running with macOS's App Sandboxing.
        grep -q "Terminated" log && return
        grep "^PID:" log | awk '{print $2}'
        ;;

      *)
        # We could use the same approach we follow on Darwin to look for the
        # PID of the subprocess, but it's better if we look at the real
        # process table if we are able to.
        ps ax | grep [f]ake-sandboxfs | awk '{print $1}'
        ;;
    esac
  }

  # Want sandboxfs to be left mounted after a build with debugging on.
  build_with_fake_sandboxfs "$(pwd)/log" --sandbox_debug //hello
  grep -q "ARGS:" log || fail "sandboxfs was not run"
  grep -q "Terminated" log \
    && fail "sandboxfs process was terminated but should not have been"
  local pid1="$(sandboxfs_pid)"
  [[ -n "${pid1}" ]] || fail "sandboxfs process not found in process table"

  # Want sandboxfs to be restarted if the previous build had debugging on.
  build_with_fake_sandboxfs "$(pwd)/log" --sandbox_debug //hello
  local pid2="$(sandboxfs_pid)"
  [[ -n "${pid2}" ]] || fail "sandboxfs process not found in process table"
  [[ "${pid1}" -ne "${pid2}" ]] || fail "sandboxfs was not restarted"

  # Want build to finish successfully and to clear the mount point.
  build_with_fake_sandboxfs "$(pwd)/log" --nosandbox_debug //hello
  local pid3="$(sandboxfs_pid)"
  [[ -z "${pid3}" ]] || fail "sandboxfs was not terminated"
}

function test_always_unmounted_on_exit() {
  create_hello_package

  # Want sandboxfs to be left mounted after a build with debugging on.
  build_with_fake_sandboxfs "$(pwd)/log" --sandbox_debug //hello
  grep -q "ARGS:" log || fail "sandboxfs was not run"
  grep -q "Terminated" log \
    && fail "sandboxfs process was terminated but should not have been"

  # Want Bazel to unmount the sandboxfs instance on exit no matter what.
  #
  # Note that we do not even tell Bazel where the sandboxfs binary lives
  # but we expect changes to the log of the currently-running sandboxfs
  # binary.  This is intentional to verify that the already-mounted
  # instance is the one shut down.
  bazel shutdown
  grep -q "Terminated" log \
    || fail "sandboxfs process was not terminated but should have been"
}

run_suite "sandboxfs-based sandboxing tests"
