#!/usr/bin/env bash
#
# Copyright 2019 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.
#
# Test sandboxing spawn strategy
#

set -euo pipefail

# Load the test setup defined in the parent directory
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; }

function set_up() {
  add_to_bazelrc "build --spawn_strategy=sandboxed"
  add_to_bazelrc "build --genrule_strategy=sandboxed"

  # Enabled in testenv.sh.tmpl, but not in Bazel by default.
  sed -i.bak '/sandbox_tmpfs_path/d' "$bazelrc"
}

function tear_down() {
  bazel clean --expunge
  bazel shutdown
  rm -rf pkg
}

function test_sandbox_base_keeps_dirs_with_sandbox_debug {
  mkdir pkg
  cat >pkg/BUILD <<EOF
genrule(name = "pkg", outs = ["pkg.out"], cmd = "echo >\$@")
EOF

  local output_base="$(bazel info output_base)"

  bazel build --sandbox_debug //pkg >"${TEST_log}" 2>&1 || fail "Expected build to succeed"
  find "${output_base}" >>"${TEST_log}" 2>&1 || true

  local sandbox_dir="$(echo "${output_base}/sandbox"/*-sandbox)"
  [[ -d "${sandbox_dir}" ]] \
    || fail "${sandbox_dir} is missing; prematurely deleted?"

  bazel shutdown
}

function do_succeed_when_executor_not_initialized_test() {
  local extra_args=( "${@}" )

  mkdir pkg
  mkfifo pkg/BUILD

  bazel build --nobuild "${@}" //pkg:all \
    >"${TEST_log}" 2>&1 &
  local pid="${!}"

  echo "Waiting for Blaze to finish initializing all modules"
  while ! grep "currently loading: pkg" "${TEST_log}"; do
    sleep 1
  done

  echo "Interrupting Blaze before it gets to init the executor"
  kill "${pid}"

  echo "And now giving Blaze a chance to finalize all modules"
  echo "unblock fifo" >pkg/BUILD
  wait "${pid}" || true

  expect_log "Build did NOT complete successfully"
  # Disallow some common messages we might see during a crash.
  expect_not_log "Internal error"
  expect_not_log "stack trace"
  expect_not_log "NullPointerException"
}

function test_succeed_when_executor_not_initialized_with_defaults() {
  # Pass a no-op flag to the test to workaround a bug in macOS's default
  # and ancient bash version which causes it to error out on an empty
  # argument list when $@ is consumed and set -u is enabled.
  local noop=( --nobuild )

  do_succeed_when_executor_not_initialized_test "${noop[@]}"
}

function test_succeed_when_executor_not_initialized_with_async_deletions() {
  do_succeed_when_executor_not_initialized_test \
    --experimental_sandbox_async_tree_delete_idle_threads=auto
}

function test_sandbox_base_can_be_rm_rfed() {
  mkdir pkg
  cat >pkg/BUILD <<EOF
genrule(name = "pkg", outs = ["pkg.out"], cmd = "echo >\$@")
EOF

  local output_base="$(bazel info output_base)"

  do_build() {
    bazel build --sandbox_debug //pkg
  }

  do_build >"${TEST_log}" 2>&1 || fail "Expected build to succeed"
  find "${output_base}" >>"${TEST_log}" 2>&1 || true

  local sandbox_base="${output_base}/sandbox"
  [[ -d "${sandbox_base}" ]] \
    || fail "${sandbox_base} is missing; build did not use sandboxing?"

  # Ensure the sandbox base does not contain protected files that would prevent
  # a simple "rm -rf" from working under an unprivileged user.
  rm -rf "${sandbox_base}" || fail "Cannot clean sandbox base"

  # And now ensure Bazel reconstructs the sandbox base on a second build.
  do_build >"${TEST_log}" 2>&1 || fail "Expected build to succeed"
}

function test_sandbox_local_used_with_proper_strategy() {
  mkdir pkg
  cat >pkg/BUILD <<EOF
genrule(name = "pkg", outs = ["pkg.out"], cmd = "pwd; echo >\$@",
  tags = ["no-sandbox"])
EOF

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

  bazel build --genrule_strategy=sandboxed,standalone //pkg \
    >"${TEST_log}" 2>&1 || fail "Expected build to succeed"

  expect_not_log "${output_base}.*/sandbox/"
}

function test_sandbox_base_top_is_removed() {
  mkdir pkg
  cat >pkg/BUILD <<EOF
genrule(name = "pkg", outs = ["pkg.out"], cmd = "echo >\$@")
EOF

  local output_base="$(bazel info output_base)"

  bazel build //pkg >"${TEST_log}" 2>&1 || fail "Expected build to succeed"
  find "${output_base}" >>"${TEST_log}" 2>&1 || true

  local sandbox_base="${output_base}/sandbox"
  [[ -d "${sandbox_base}/sandbox_stash" ]] \
    || fail "${sandbox_base}/sandbox_stash directory not present"
  [[ -d "${sandbox_base}/_moved_trash_dir" ]] \
    || fail "${sandbox_base}/_moved_trash_dir directory not present"

  [[ $(ls -1 ${sandbox_base} | wc -l | tr -d ' ') == 2 ]] \
    || fail "${sandbox_base} contains stale dirs"

  bazel shutdown
  [[ -d "${sandbox_base}/sandbox_stash" ]] \
    || fail "${sandbox_base}/_moved_trash_dir directory not present"
  bazel build
  [[ ! -d "${sandbox_base}/sandbox_stash" ]] \
    || fail "${sandbox_base}/_moved_trash_dir directory not present"
  [[ $(ls -1 ${sandbox_base} | wc -l | tr -d ' ') == 1 ]] \
    || fail "${sandbox_base} contains stale dirs"
}

function test_sandbox_old_contents_not_reused_in_consecutive_builds() {
  mkdir pkg
  cat >pkg/BUILD <<EOF
genrule(
    name = "pkg",
    srcs = ["pkg.in"],
    outs = ["pkg.out"],
    cmd = "cp \$(location :pkg.in) \$@",
)
EOF
  touch pkg/pkg.in

  for i in $(seq 5); do
    # Ensure that, even if we don't clean up the sandbox at all (with
    # --sandbox_debug), consecutive builds don't step on each other by trying to
    # reuse previous spawn identifiers.
    bazel build --sandbox_debug //pkg \
      >"${TEST_log}" 2>&1 || fail "Expected build to succeed"
    echo foo >>pkg/pkg.in
  done
}

function test_sandbox_hardening_with_cgroups_v1() {
  if ! grep -E '^cgroup +[^ ]+ +cgroup +.*memory.*' /proc/mounts; then
    echo "No cgroup v1 memory controller mounted, skipping test"
    return 0
  fi
  memmount=$(grep -E '^cgroup +[^ ]+ +cgroup +.*memory.*' /proc/mounts | cut -d' ' -f2)
  if ! grep -E '^[0-9]*:[^:]*memory[^:]*:' /proc/self/cgroup &>/dev/null; then
    echo "Does not use cgroups v1, skipping test"
    return 0
  fi
  memsubdir=$(grep -E '^[0-9]*:[^:]*memory[^:]*:' /proc/self/cgroup | cut -d: -f3)
  memdir="$memmount$memsubdir"
  if [[ ! -w "$memdir" ]]; then
    echo "Cgroups v1 directory not writable, skipping test"
    return 0
  fi

  mkdir pkg
  cat >pkg/BUILD <<EOF
genrule(
  name = "pkg",
  outs = ["pkg.out"],
  cmd = "pwd; hexdump -C -n 250000 < /dev/random | sort > /dev/null 2>\$@"
)
EOF
  local genfiles_base="$(bazel info ${PRODUCT_NAME}-genfiles)"

  bazel build --genrule_strategy=linux-sandbox \
    --experimental_sandbox_memory_limit_mb=1000 //pkg \
    >"${TEST_log}" 2>&1 || fail "Expected build to succeed"
  rm -f ${genfiles_base}/pkg/pkg.out
  bazel build --genrule_strategy=linux-sandbox \
    --experimental_sandbox_memory_limit_mb=1 //pkg \
    >"${TEST_log}" 2>&1 && fail "Expected build to fail" || true
}

function test_sandbox_hardening_with_cgroups_v2() {
  if ! grep -E '^cgroup2 +[^ ]+ +cgroup2 ' /proc/mounts; then
    echo "No cgroup2 mounted, skipping test"
    return 0
  fi
  if ! grep -E '^0::' /proc/self/cgroup &>/dev/null; then
    echo "Does not use cgroups v2, skipping test"
    return 0
  fi
  if ! XDG_RUNTIME_DIR=/run/user/$( id -u ) systemd-run --user --scope true; then
    echo "Not able to use systemd, skipping test"
    return 0
  fi

  mkdir pkg
  cat >pkg/BUILD <<EOF
genrule(
  name = "pkg",
  outs = ["pkg.out"],
  cmd = "pwd; hexdump -C -n 250000 < /dev/random | sort > /dev/null 2>\$@"
)
EOF
  local genfiles_base="$(bazel info ${PRODUCT_NAME}-genfiles)"
  # Need to make sure the bazel server runs under systemd, too.
  bazel shutdown

  XDG_RUNTIME_DIR=/run/user/$( id -u ) systemd-run --user --scope \
  bazel build --genrule_strategy=linux-sandbox \
    --experimental_sandbox_memory_limit_mb=1000 //pkg \
    >"${TEST_log}" 2>&1 || fail "Expected build to succeed"
  rm -f ${genfiles_base}/pkg/pkg.out

  bazel shutdown
  XDG_RUNTIME_DIR=/run/user/$( id -u ) systemd-run --user --scope \
  bazel build --genrule_strategy=linux-sandbox \
    --experimental_sandbox_memory_limit_mb=1 //pkg \
    >"${TEST_log}" 2>&1 && fail "Expected build to fail" || true
}

function test_sandboxed_genrule() {
  mkdir -p examples/genrule
  cat << 'EOF' > examples/genrule/a.txt
foo bar bz
EOF
  cat << 'EOF' > examples/genrule/BUILD
genrule(
  name = "works",
  srcs = [ "a.txt" ],
  outs = [ "works.txt" ],
  cmd = "wc $(location :a.txt) > $@",
)
EOF

  bazel build examples/genrule:works &> $TEST_log \
    || fail "Hermetic genrule failed: examples/genrule:works"
  [ -f "bazel-genfiles/examples/genrule/works.txt" ] \
    || fail "Genrule did not produce output: examples/genrule:works"
}

function test_sandboxed_genrule_with_tools() {
  add_rules_shell "MODULE.bazel"
  mkdir -p examples/genrule

  cat << 'EOF' > examples/genrule/BUILD
load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
sh_binary(
    name = "tool",
    srcs = ["tool.sh"],
    data = ["datafile"],
)

genrule(
    name = "tools_work",
    srcs = [],
    outs = ["tools.txt"],
    cmd = "$(location :tool) $@",
    tools = [":tool"],
)
EOF
  cat << 'EOF' >> examples/genrule/datafile
this is a datafile
EOF
  local WORKSPACE_NAME=$TEST_WORKSPACE
  # The workspace name is initialized in testenv.sh; use that var rather than
  # hardcoding it here. The extra sed pass is so we can selectively expand that
  # one var while keeping the rest of the heredoc literal.
  cat | sed "s/{{WORKSPACE_NAME}}/$WORKSPACE_NAME/" >> examples/genrule/tool.sh << 'EOF'
#!/bin/sh

set -e
cp $(dirname $0)/tool.runfiles/{{WORKSPACE_NAME}}/examples/genrule/datafile $1
echo "Tools work!"
EOF
  chmod +x examples/genrule/tool.sh

  bazel build examples/genrule:tools_work &> $TEST_log \
    || fail "Hermetic genrule failed: examples/genrule:tools_work"
  [ -f "bazel-genfiles/examples/genrule/tools.txt" ] \
    || fail "Genrule did not produce output: examples/genrule:tools_work"
}

# Test for #400: Linux sandboxing and relative symbolic links.
#
# let A = examples/genrule/symlinks/a/b/x.txt -> ../x.txt
# where   examples/genrule/symlinks/a/b -> examples/genrule/symlinks/ok/sub
# thus the realpath of A is example/genrule/symlinks/ok/x.txt
# but if the code doesn't correctly resolve intermediate symlinks and instead
# uses string operations to handle ".." parts, it will arrive at:
# examples/genrule/symlinks/a/x.txt, which is wrong.
#
function test_sandbox_relative_symlink_in_inputs() {
  mkdir -p examples/genrule

  cat << 'EOF' > examples/genrule/BUILD
genrule(
  name = "relative_symlinks",
  srcs = [ "symlinks/a/b/x.txt" ],
  outs = [ "relative_symlinks.txt" ],
  cmd = "cat $(location :symlinks/a/b/x.txt) > $@",
)
EOF

    mkdir -p examples/genrule/symlinks/{a,ok/sub}
    echo OK > examples/genrule/symlinks/ok/x.txt
    ln -s $PWD/examples/genrule/symlinks/ok/sub examples/genrule/symlinks/a/b
    ln -s ../x.txt examples/genrule/symlinks/a/b/x.txt

  bazel build examples/genrule:relative_symlinks &> $TEST_log \
    || fail "Hermetic genrule failed: examples/genrule:relative_symlinks"
  [ -f "bazel-genfiles/examples/genrule/relative_symlinks.txt" ] \
    || fail "Genrule did not produce output: examples/genrule:relative_symlinks"
}

function test_sandbox_undeclared_deps() {
  output_file="bazel-genfiles/examples/genrule/breaks1.txt"

  mkdir -p examples/genrule
  cat << 'EOF' > examples/genrule/a.txt
foo bar bz
EOF
  cat << 'EOF' > examples/genrule/BUILD
genrule(
  name = "breaks1",
  srcs = [ "a.txt" ],
  outs = [ "breaks1.txt" ],
  cmd = "wc $(location :a.txt) `dirname $(location :a.txt)`/b.txt &> $@",
)
EOF

  bazel build examples/genrule:breaks1 &> $TEST_log \
    && fail "Non-hermetic genrule succeeded: examples/genrule:breaks1" || true

  [ -f "$output_file" ] ||
    fail "Action did not produce output: $output_file"

  if [ $(wc -l $output_file) -gt 1 ]; then
    fail "Output contained more than one line: $output_file"
  fi

  fgrep "No such file or directory" $output_file ||
    fail "Output did not contain expected error message: $output_file"
}

function test_sandbox_undeclared_deps_with_local() {
  mkdir -p examples/genrule
  echo "foo bar bz" > examples/genrule/a.txt
  echo "apples oranges bananas" > examples/genrule/b.txt
  cat << 'EOF' > examples/genrule/BUILD
genrule(
  name = "breaks1_works_with_local",
  srcs = [ "a.txt" ],
  outs = [ "breaks1_works_with_local.txt" ],
  cmd = "wc $(location :a.txt) `dirname $(location :a.txt)`/b.txt > $@",
  local = 1,
)
EOF
  bazel build --strategy=Genrule=sandboxed,local \
    examples/genrule:breaks1_works_with_local &> $TEST_log \
    || fail "Non-hermetic genrule failed even though local=1: examples/genrule:breaks1_works_with_local"
  [ -f "bazel-genfiles/examples/genrule/breaks1_works_with_local.txt" ] \
    || fail "Genrule did not produce output: examples/genrule:breaks1_works_with_local"
}

function test_sandbox_undeclared_deps_with_local_tag() {
  mkdir -p examples/genrule
  echo "foo bar bz" > examples/genrule/a.txt
  echo "apples oranges bananas" > examples/genrule/b.txt
  cat << 'EOF' > examples/genrule/BUILD
genrule(
  name = "breaks1_works_with_local_tag",
  srcs = [ "a.txt" ],
  outs = [ "breaks1_works_with_local_tag.txt" ],
  cmd = "wc $(location :a.txt) `dirname $(location :a.txt)`/b.txt > $@",
  tags = [ "local" ],
)
EOF
  bazel build --strategy=Genrule=sandboxed,local \
    examples/genrule:breaks1_works_with_local_tag &> $TEST_log \
    || fail "Non-hermetic genrule failed even though tags=['local']: examples/genrule:breaks1_works_with_local_tag"
  [ -f "bazel-genfiles/examples/genrule/breaks1_works_with_local_tag.txt" ] \
    || fail "Genrule did not produce output: examples/genrule:breaks1_works_with_local_tag"
}

function test_sandbox_undeclared_deps_with_local_tag_no_fallback() {
  mkdir -p examples/genrule
  echo "foo bar bz" > examples/genrule/a.txt
  cat << 'EOF' > examples/genrule/BUILD
genrule(
  name = "breaks1_works_with_local_tag",
  srcs = [ "a.txt" ],
  outs = [ "breaks1_works_with_local_tag.txt" ],
  cmd = "wc $(location :a.txt) `dirname $(location :a.txt)`/b.txt > $@",
  tags = [ "local" ],
)
EOF
  bazel build examples/genrule:breaks1_works_with_local_tag &> $TEST_log \
    && fail "Non-hermetic genrule suucceeded even though tags=['local']: examples/genrule:breaks1_works_with_local_tag" \
    || true
}

function write_starlark_breaks1() {
  cat << 'EOF' >> examples/genrule/starlark.bzl
def _starlark_breaks1_impl(ctx):
  print(ctx.outputs.output.path)
  ctx.actions.run_shell(
    inputs = [ ctx.file.input ],
    outputs = [ ctx.outputs.output ],
    command = "wc %s `dirname %s`/b.txt &> %s" % (ctx.file.input.path,
                                                 ctx.file.input.path,
                                                 ctx.outputs.output.path),
    execution_requirements = { tag: '' for tag in ctx.attr.action_tags },
  )

starlark_breaks1 = rule(
  _starlark_breaks1_impl,
  attrs = {
    "input": attr.label(mandatory=True, allow_single_file=True),
    "output": attr.output(mandatory=True),
    "action_tags": attr.string_list(),
  },
)
EOF
}

function test_sandbox_undeclared_deps_starlark() {
  mkdir -p examples/genrule
  echo "foo bar bz" > examples/genrule/a.txt
  echo "apples oranges bananas" > examples/genrule/b.txt
  cat << 'EOF' > examples/genrule/BUILD
load('//examples/genrule:starlark.bzl', 'starlark_breaks1')

starlark_breaks1(
  name = "starlark_breaks1",
  input = "a.txt",
  output = "starlark_breaks1.txt",
)
EOF
  write_starlark_breaks1
  output_file="bazel-bin/examples/genrule/starlark_breaks1.txt"
  bazel build examples/genrule:starlark_breaks1 &> $TEST_log \
    && fail "Non-hermetic genrule succeeded: examples/genrule:starlark_breaks1" || true

  [ -f "$output_file" ] ||
    fail "Action did not produce output: $output_file"

  if [ $(wc -l $output_file) -gt 1 ]; then
    fail "Output contained more than one line: $output_file"
  fi

  fgrep "No such file or directory" $output_file ||
    fail "Output did not contain expected error message: $output_file"
}

function test_sandbox_undeclared_deps_starlark_with_local_tag() {
  mkdir -p examples/genrule
  echo "foo bar bz" > examples/genrule/a.txt
  echo "apples oranges bananas" > examples/genrule/b.txt
  cat << 'EOF' > examples/genrule/BUILD
load('//examples/genrule:starlark.bzl', 'starlark_breaks1')

starlark_breaks1(
  name = "starlark_breaks1",
  input = "a.txt",
  output = "starlark_breaks1.txt",
)

starlark_breaks1(
  name = "starlark_breaks1_works_with_local_tag",
  input = "a.txt",
  output = "starlark_breaks1_works_with_local_tag.txt",
  action_tags = [ "local" ],
)
EOF
  write_starlark_breaks1

  bazel build --spawn_strategy=sandboxed,standalone,local \
    examples/genrule:starlark_breaks1_works_with_local_tag &> $TEST_log \
    || fail "Non-hermetic genrule failed even though tags=['local']: examples/genrule:starlark_breaks1_works_with_local_tag"
  [ -f "bazel-bin/examples/genrule/starlark_breaks1_works_with_local_tag.txt" ] \
    || fail "Action did not produce output: examples/genrule:starlark_breaks1_works_with_local_tag"
}

function test_sandbox_cyclic_symlink_in_inputs() {
  mkdir -p examples/genrule
  # Create cyclic symbolic links to check whether the strategy catches that.
  ln -sf cyclic2 examples/genrule/cyclic1
  ln -sf cyclic1 examples/genrule/cyclic2
  cat << 'EOF' > examples/genrule/BUILD
genrule(
  name = "breaks3",
  srcs = [ "cyclic1", "cyclic2" ],
  outs = [ "breaks3.txt" ],
  cmd = "wc $(location :cyclic1) > $@",
)
EOF
  bazel build examples/genrule:breaks3 &> $TEST_log \
    && fail "Genrule with cyclic symlinks succeeded: examples/genrule:breaks3" || true
  [ ! -f "bazel-genfiles/examples/genrule/breaks3.txt" ] || {
    output=$(cat "bazel-genfiles/examples/genrule/breaks3.txt")
    fail "Genrule with cyclic symlinks breaks3 succeeded with following output: $output"
  }
}


function test_requires_root() {
  if ! is_linux; then
    echo "Skipping test: fake usernames not supported in this system" 1>&2
    return 0
  fi
  add_rules_shell "MODULE.bazel"
  cat > test.sh <<'EOF'
#!/bin/sh
([ $(id -u) = "0" ] && [ $(id -g) = "0" ]) || exit 1
EOF
  chmod +x test.sh
  cat > BUILD <<'EOF'
load("@rules_shell//shell:sh_test.bzl", "sh_test")
sh_test(
  name = "test",
  srcs = ["test.sh"],
  tags = ["requires-fakeroot"],
)
EOF
  bazel test --test_output=errors :test || fail "test did not pass"
  bazel test --nocache_test_results --sandbox_fake_username --test_output=errors :test || fail "test did not pass"
}

# Tests that /proc/self == /proc/$$. This should always be true unless the PID namespace is active without /proc being remounted correctly.
function test_sandbox_proc_self() {
  if [[ ! -d /proc/self ]]; then
    echo "Skipping tests: requires /proc" 1>&2
    return 0
  fi
  mkdir -p examples/genrule
  cat << 'EOF' > examples/genrule/BUILD
genrule(
  name = "check_proc_works",
  outs = [ "check_proc_works.txt" ],
  cmd = "sh -c 'cd /proc/self && echo $$$$ && exec cat stat | sed \"s/\\([^ ]*\\) .*/\\1/g\"' > $@",
)
EOF

  bazel build examples/genrule:check_proc_works >& $TEST_log || fail "build should have succeeded"

  (
    # Catch the head and tail commands failing.
    set -e
    if [[ "$(head -n1 "bazel-genfiles/examples/genrule/check_proc_works.txt")" \
          != "$(tail -n1 "bazel-genfiles/examples/genrule/check_proc_works.txt")" ]] ; then
      fail "Reading PID from /proc/self/stat should have worked, instead have these: $(cat "bazel-genfiles/examples/genrule/check_proc_works.txt")"
    fi
  )
}

function test_succeeding_action_with_ioexception_while_copying_outputs_throws_correct_exception() {
  cat > BUILD <<'EOF'
genrule(
  name = "test",
  outs = ["readonlydir/output.txt"],
  cmd = """touch $(location readonlydir/output.txt) && \
           chmod 0 $(location readonlydir/output.txt) && \
           chmod 0 `dirname $(location readonlydir/output.txt)`""",
)
EOF
  bazel build :test &> $TEST_log \
    && fail "build should have failed" || true

  # This is the generic "we caught an IOException" log message used by the
  # SandboxedStrategy. We don't want to see this in this case, because we have
  # special handling that prints a better error message and then lets the
  # sandbox code throw the actual ExecException.
  expect_not_log "I/O error during sandboxed execution"

  # There was no ExecException during sandboxed execution, because the action
  # returned an exit code of 0.
  expect_not_log "Executing genrule //:test failed: linux-sandbox failed: error executing command"

  # This is the error message telling us that some output artifacts couldn't be copied.
  expect_log "Could not copy outputs from sandbox:.*readonlydir/output.txt (Permission denied)"

  # The build fails, because the action didn't generate its output artifact.
  expect_log "ERROR:.*Executing genrule //:test failed"
}

function test_failing_action_with_ioexception_while_copying_outputs_throws_correct_exception() {
  cat > BUILD <<'EOF'
genrule(
  name = "test",
  outs = ["readonlydir/output.txt"],
  cmd = """touch $(location readonlydir/output.txt) && \
           chmod 0 $(location readonlydir/output.txt) && \
           chmod 0 `dirname $(location readonlydir/output.txt)` && \
           exit 1""",
)
EOF
  bazel build :test &> $TEST_log \
    && fail "build should have failed" || true

  # This is the generic "we caught an IOException" log message used by the
  # SandboxedStrategy. We don't want to see this in this case, because we have
  # special handling that prints a better error message and then lets the
  # sandbox code throw the actual ExecException.
  expect_not_log "I/O error during sandboxed execution"

  # This is the error message printed by the EventHandler telling us that some
  # output artifacts couldn't be copied.
  expect_log "Could not copy outputs from sandbox:.*readonlydir/output.txt (Permission denied)"

  # This is the UserExecException telling us that the build failed.
  expect_log "ERROR:.*Executing genrule //:test failed:"
}

function test_read_non_hermetic_tmp {
  temp_dir=$(mktemp -d /tmp/test.XXXXXX)
  trap 'rm -rf ${temp_dir}' EXIT

  add_rules_shell "MODULE.bazel"
  mkdir -p pkg
  cat > pkg/BUILD <<'EOF'
load("@rules_shell//shell:sh_test.bzl", "sh_test")
sh_test(
  name = "tmp_test",
  srcs = ["tmp_test.sh"],
)
EOF

  cat > pkg/tmp_test.sh <<EOF
[[ -f "${temp_dir}/file" ]]
EOF
  chmod +x pkg/tmp_test.sh

  touch "${temp_dir}/file"
  bazel test //pkg:tmp_test \
    --sandbox_add_mount_pair=/tmp \
    --test_output=errors &>$TEST_log || fail "Expected test to pass"
}

function test_read_hermetic_tmp {
  if ! is_linux; then
    echo "Skipping test: hermetic /tmp is only supported in Linux" 1>&2
    return 0
  fi

  temp_dir=$(mktemp -d /tmp/test.XXXXXX)
  trap 'rm -rf ${temp_dir}' EXIT

  add_rules_shell "MODULE.bazel"
  mkdir -p pkg
  cat > pkg/BUILD <<'EOF'
load("@rules_shell//shell:sh_test.bzl", "sh_test")
sh_test(
  name = "tmp_test",
  srcs = ["tmp_test.sh"],
)
EOF

  cat > pkg/tmp_test.sh <<EOF
[[ ! -f "${temp_dir}/file" ]]
EOF
  chmod +x pkg/tmp_test.sh

  touch "${temp_dir}/file"
  bazel test //pkg:tmp_test --test_output=errors &>$TEST_log || fail "Expected test to pass"
}

function test_read_hermetic_tmp_user_override {
  if ! is_linux; then
    echo "Skipping test: hermetic /tmp is only supported in Linux" 1>&2
    return 0
  fi

  temp_dir=$(mktemp -d /tmp/test.XXXXXX)
  trap 'rm -rf ${temp_dir}' EXIT

  add_rules_shell "MODULE.bazel"
  mkdir -p pkg
  cat > pkg/BUILD <<'EOF'
load("@rules_shell//shell:sh_test.bzl", "sh_test")
sh_test(
  name = "tmp_test",
  srcs = ["tmp_test.sh"],
)
EOF

  cat > pkg/tmp_test.sh <<EOF
[[ -f "${temp_dir}/file" ]]
EOF
  chmod +x pkg/tmp_test.sh

  touch "${temp_dir}/file"
  bazel test //pkg:tmp_test --sandbox_add_mount_pair=/tmp --test_output=errors &>$TEST_log || fail "Expected test to pass"
}

function test_write_non_hermetic_tmp {
  temp_dir=$(mktemp -d /tmp/test.XXXXXX)
  trap 'rm -rf ${temp_dir}' EXIT

  add_rules_shell "MODULE.bazel"
  mkdir -p pkg
  cat > pkg/BUILD <<'EOF'
load("@rules_shell//shell:sh_test.bzl", "sh_test")
sh_test(
  name = "tmp_test",
  srcs = ["tmp_test.sh"],
)
EOF

  cat > pkg/tmp_test.sh <<EOF
touch "${temp_dir}/file"
EOF
  chmod +x pkg/tmp_test.sh

  bazel test //pkg:tmp_test \
    --sandbox_add_mount_pair=/tmp \
    --test_output=errors &>$TEST_log || fail "Expected test to pass"
  [[ -f "${temp_dir}/file" ]] || fail "Expected ${temp_dir}/file to exist"
}

function test_write_hermetic_tmp {
  if ! is_linux; then
    echo "Skipping test: hermetic /tmp is only supported in Linux" 1>&2
    return 0
  fi

  temp_dir=$(mktemp -d /tmp/test.XXXXXX)
  trap 'rm -rf ${temp_dir}' EXIT

  add_rules_shell "MODULE.bazel"
  mkdir -p pkg
  cat > pkg/BUILD <<'EOF'
load("@rules_shell//shell:sh_test.bzl", "sh_test")
sh_test(
  name = "tmp_test",
  srcs = ["tmp_test.sh"],
)
EOF

  cat > pkg/tmp_test.sh <<EOF
mkdir -p "${temp_dir}"
touch "${temp_dir}/file"
EOF
  chmod +x pkg/tmp_test.sh

  bazel test //pkg:tmp_test --test_output=errors &>$TEST_log || fail "Expected test to pass"
  [[ ! -f "${temp_dir}" ]] || fail "Expected ${temp_dir} to not exit"
}

function test_write_hermetic_tmp_user_override {
  if ! is_linux; then
    echo "Skipping test: hermetic /tmp is only supported in Linux" 1>&2
    return 0
  fi

  temp_dir=$(mktemp -d /tmp/test.XXXXXX)
  trap 'rm -rf ${temp_dir}' EXIT

  add_rules_shell "MODULE.bazel"
  mkdir -p pkg
  cat > pkg/BUILD <<'EOF'
load("@rules_shell//shell:sh_test.bzl", "sh_test")
sh_test(
  name = "tmp_test",
  srcs = ["tmp_test.sh"],
)
EOF

  cat > pkg/tmp_test.sh <<EOF
touch "${temp_dir}/file"
EOF
  chmod +x pkg/tmp_test.sh

  bazel test //pkg:tmp_test --sandbox_add_mount_pair=/tmp \
    --test_output=errors &>$TEST_log || fail "Expected test to pass"
  [[ -f "${temp_dir}/file" ]] || fail "Expected ${temp_dir}/file to exist"
}

function test_sandbox_reuse_stashes_sandbox() {
  mkdir pkg
  cat >pkg/BUILD <<'EOF'
genrule(
  name = "a",
  srcs = [ "a.txt" ],
  outs = [ "aout.txt" ],
  cmd = "wc $(location :a.txt) > $@",
)
genrule(
  name = "b",
  srcs = [ "b.txt" ],
  outs = [ "bout.txt" ],
  cmd = "wc $(location :b.txt) > $@",
)
EOF
  echo A > pkg/a.txt
  echo BB > pkg/b.txt
  local output_base="$(bazel info output_base)"
  local execroot="$(bazel info execution_root)"
  local execroot_reldir="${execroot#$output_base}"

  bazel build --reuse_sandbox_directories //pkg:a >"${TEST_log}" 2>&1 \
    || fail "Expected build to succeed"

  local sandbox_stash="${output_base}/sandbox/sandbox_stash"
  [[ -d "${sandbox_stash}" ]] \
    || fail "${sandbox_stash} not present"
  [[ -d "${sandbox_stash}/Genrule/3" ]] \
    || fail "${sandbox_stash} did not stash anything"
  [[ -L "${sandbox_stash}/Genrule/3/$execroot_reldir/pkg/a.txt" ]] \
    || fail "${sandbox_stash} did not have a link to a.txt"

  bazel build --reuse_sandbox_directories //pkg:b >"${TEST_log}" 2>&1 \
    || fail "Expected build to succeed"
  ls -R "${sandbox_stash}/Genrule/"
  [[ ! -L "${sandbox_stash}/Genrule/6/$execroot_reldir/pkg/a.txt" ]] \
    || fail "${sandbox_stash} should no longer have a link to a.txt"
  [[ -L "${sandbox_stash}/Genrule/6/$execroot_reldir/pkg/b.txt" ]] \
    || fail "${sandbox_stash} should now have a link to b.txt"

  bazel clean
  [[ ! -d "${sandbox_stash}" ]] \
    || fail "${sandbox_stash} present after clean"

  bazel build --reuse_sandbox_directories //pkg:a >"${TEST_log}" 2>&1 \
    || fail "Expected build to succeed"
}

function test_sandbox_reuse_stashes_sandbox_with_changing_hermetic_tmp_and_clean() {
  mkdir pkg
  cat >pkg/BUILD <<'EOF'
genrule(
  name = "a",
  srcs = [ "a.txt" ],
  outs = [ "aout.txt" ],
  cmd = "wc $(location :a.txt) > $@",
)
genrule(
  name = "b",
  srcs = [ "b.txt" ],
  outs = [ "bout.txt" ],
  cmd = "wc $(location :b.txt) > $@",
)
EOF
  echo A > pkg/a.txt
  echo BB > pkg/b.txt
  local output_base="$(bazel info output_base)"
  local execroot="$(bazel info execution_root)"
  local execroot_reldir="${execroot#$output_base}"

  bazel build //pkg:a >"${TEST_log}" 2>&1 \
    || fail "Expected build to succeed"

  local sandbox_stash="${output_base}/sandbox/sandbox_stash"
  [[ -d "${sandbox_stash}" ]] \
    || fail "${sandbox_stash} not present"
  [[ -d "${sandbox_stash}/Genrule/3" ]] \
    || fail "${sandbox_stash} did not stash anything"
  [[ -L "${sandbox_stash}/Genrule/3/$execroot_reldir/pkg/a.txt" ]] \
    || fail "${sandbox_stash} did not have a link to a.txt"

  bazel build //pkg:b >"${TEST_log}" 2>&1 \
    || fail "Expected build to succeed"
  ls -R "${sandbox_stash}/Genrule/"
  [[ ! -L "${sandbox_stash}/Genrule/6/$execroot_reldir/pkg/a.txt" ]] \
    || fail "${sandbox_stash} should no longer have a link to a.txt"
  [[ -L "${sandbox_stash}/Genrule/6/$execroot_reldir/pkg/b.txt" ]] \
    || fail "${sandbox_stash} should now have a link to b.txt"

  bazel clean
  [[ ! -d "${sandbox_stash}" ]] \
    || fail "${sandbox_stash} present after clean"

  bazel build //pkg:a >"${TEST_log}" 2>&1 \
    || fail "Expected build to succeed"

  bazel shutdown
}

# This test does not currently work in Blaze. Not due to the inaccessible dirs
# in runfiles. Even if the action didn't set the chmod 000 permissions the test
# would still not work for Blaze. The problem is the difference in how the
# runfiles are laid out in Blaze vs Bazel:
# Blaze: <somedir>/sandbox/sandbox_stash/TestRunner/3/execroot/{workspace_name}/runfiles/
# Bazel: <somedir>/sandbox/sandbox_stash/TestRunner/3//execroot/_main/bazel-out/k8-fastbuild/bin/pkg/create_readonly_dir_in_pwd.runfiles

# The runfiles directory for Blaze has permissions r_xr_xr_x while the runfiles
# directory for Bazel has permissions rwxr_xr_x. The latter allows writing a
# new directory from an action (regardless of the permission of that new directory)
# and then deleting it when reusing the sandbox but when running this test under
# Blaze due to the missing write permissions we get an error.
function test_sandbox_reuse_stashes_works_for_actions_creating_inaccessible_dirs_in_runfiles() {
  if ! is_bazel; then
    return 0
  fi

  add_rules_shell "MODULE.bazel"

  mkdir pkg
  cat >pkg/BUILD <<EOF
load("@rules_shell//shell:sh_test.bzl", "sh_test")
sh_test(
  name = "create_readonly_dir_in_pwd",
  srcs = [ "create_readonly_dir_in_pwd.sh" ],
)
EOF

  cat > pkg/create_readonly_dir_in_pwd.sh <<EOF
#!/bin/sh
set -e
mkdir readonly_dir
touch readonly_dir/some_file
chmod 000 readonly_dir/some_file
chmod 000 readonly_dir
EOF
  chmod +x pkg/create_readonly_dir_in_pwd.sh

  local output_base="$(bazel info output_base)"
  local bazel_bin="$(bazel info bazel-bin)"
  local bazel_bin_reldir="${bazel_bin#$output_base}"

  bazel test --reuse_sandbox_directories //pkg:create_readonly_dir_in_pwd >"${TEST_log}" 2>&1 \
    || fail "Expected first test to succeed"

  local sandbox_stash="${output_base}/sandbox/sandbox_stash"
  [[ -d "${sandbox_stash}/TestRunner/3/$bazel_bin_reldir/pkg/create_readonly_dir_in_pwd.runfiles/_main/readonly_dir" ]] \
    || fail "${sandbox_stash} did not stash readonly_dir"

  bazel test --reuse_sandbox_directories --nocache_test_results //pkg:create_readonly_dir_in_pwd >"${TEST_log}" 2>&1 \
    || fail "Expected second test to succeed"
}

function test_hermetic_tmp_with_tmp_sandbox_base() {
  mkdir pkg
  cat >pkg/BUILD <<EOF
genrule(name = "pkg", outs = ["pkg.out"], cmd = "echo >\$@")
EOF
  mkdir /tmp/sandbox_base
  bazel build --sandbox_base=/tmp/sandbox_base  //pkg >"${TEST_log}" 2>&1 \
        || fail "Expected build to succeed"
}

function test_hermetic_tmp_with_tmpdir_under_tmp() {
  mkdir pkg
  cat >pkg/BUILD <<EOF
genrule(name = "pkg", outs = ["pkg.out"], cmd = "echo >\$@")
EOF
  mkdir /tmp/my_tmpdir
  TMPDIR=/tmp/my_tmpdir \
    bazel build //pkg >"${TEST_log}" 2>&1 || fail "Expected build to succeed"
}

function test_runfiles_from_tests_get_reused_and_tmp_clean() {
  do_test_runfiles_from_tests_get_reused_and_tmp_clean \
    "--noexperimental_inmemory_sandbox_stashes"
}

function test_runfiles_from_tests_get_reused_and_tmp_clean_in_mem_stashes() {
  do_test_runfiles_from_tests_get_reused_and_tmp_clean \
    "--experimental_inmemory_sandbox_stashes"
}

function do_test_runfiles_from_tests_get_reused_and_tmp_clean() {
  local -r in_memory_stashes="$1"
  mkdir pkg
  mkdir pkg/b
  touch pkg/file1.txt
  touch pkg/file2.txt
  touch pkg/file3.txt
  cat >pkg/reusing_test.bzl <<'EOF'
def _reused_runfiles_test_impl(ctx):
    output = ctx.actions.declare_file(ctx.label.name + ".sh")

    runfiles = ctx.runfiles(files = ctx.files.files)
    runfiles = runfiles.merge(runfiles)

    test_code = """
    #!/bin/bash
    dir_inode_number=$(ls -di $TEST_SRCDIR | cut -f1 -d" ")
    echo "The directory inode is $dir_inode_number"
    file_inode_number=$(ls -i $TEST_SRCDIR/_main/pkg/file1.txt | cut -f1 -d" ")
    echo "The file inode is $file_inode_number"
    find -L $TEST_SRCDIR -type f
    """

    ctx.actions.run_shell(
        outputs = [output],
        mnemonic = "myexample",
        command = """
        output_path={}
        echo '{}' > $output_path
        chmod 777 $output_path
        """.format(output.path, test_code)
    )

    return [DefaultInfo(executable = output, runfiles = runfiles)]

reused_runfiles_test = rule(
    implementation = _reused_runfiles_test_impl,
    test = True,
    attrs = {
        "files" : attr.label_list(allow_files=True),
    }
)
EOF

  cat >pkg/BUILD <<'EOF'
load(":reusing_test.bzl", "reused_runfiles_test")
exports_files([
  "file1.txt",
  "file2.txt",
  "file3.txt",
])
reused_runfiles_test(
    name = "a",
    files = [
      "file1.txt",
      "file2.txt",
    ],
)
EOF
  cat >pkg/b/BUILD <<'EOF'
load("//pkg:reusing_test.bzl", "reused_runfiles_test")
reused_runfiles_test(
    name = "b",
    files = [
      "//pkg:file1.txt",
      "//pkg:file3.txt",
    ],
)
EOF

  test_output="reuse_test_output.txt"
  local out_directory
  if is_bazel; then
    bazel coverage --test_output=streamed \
      "$in_memory_stashes" --experimental_split_coverage_postprocessing=1 \
      --experimental_fetch_all_coverage_outputs //pkg:a > ${test_output} \
      || fail "Expected build to succeed"
    out_directory="bazel-out"
  else
    bazel test "$in_memory_stashes" --test_output=streamed \
     //pkg:a > ${test_output} || fail "Expected build to succeed"
    out_directory="blaze-out"
  fi
  grep -q "file1.txt" ${test_output} || fail "Missing file1.txt"
  grep -q "file2.txt" ${test_output} || fail "Missing file2.txt"

  dir_inode_a=$(awk '/The directory inode is/ {print $5}' ${test_output})
  file_inode_a=$(awk '/The file inode is/ {print $5}' ${test_output})

  local output_base="$(bazel info output_base)"
  local WORKSPACE_NAME=$TEST_WORKSPACE
  local stashed_test_dir="${output_base}/sandbox/sandbox_stash/TestRunner/6/execroot/$WORKSPACE_NAME"
  touch $(find "$stashed_test_dir/$out_directory/" -name a.sh.runfiles -type d)"/$WORKSPACE_NAME/pkg/file4.txt"

  [[ -d "${stashed_test_dir}/$out_directory" ]] \
    || fail "${stashed_test_dir}/$out_directory directory not present"
  [[ -d "${stashed_test_dir}/_tmp" ]] \
      && fail "${stashed_test_dir}/_tmp directory is present"

  if is_bazel; then
    bazel coverage --test_output=streamed //pkg/b:b  \
      "$in_memory_stashes" --experimental_split_coverage_postprocessing=1 \
      --experimental_fetch_all_coverage_outputs > ${test_output} \
      || fail "Expected build to succeed"
  else
    bazel test "$in_memory_stashes" --test_output=streamed \
      //pkg/b:b > ${test_output} || fail "Expected build to succeed"
  fi
  grep -q "file1.txt" ${test_output} || fail "Missing file1.txt"
  grep -q "file3.txt" ${test_output} || fail "Missing file3.txt"
  grep -q "file2.txt" ${test_output} && fail "Present file2.txt"

  if [[ "$in_memory_stashes" =~ ^"--no" ]]; then
    grep -q "file4.txt" ${test_output} \
      && fail "Present file4.txt which was added artificially and should" \
          " have been cleaned up with disk clean-up stashes"
  else
    grep -q "file4.txt" ${test_output} \
      || fail "Missing file4.txt which was added artificially and shouldn't" \
          " have been cleaned up with in-memory stashes"
  fi

  dir_inode_b=$(awk '/The directory inode is/ {print $5}' ${test_output})
  file_inode_b=$(awk '/The file inode is/ {print $5}' ${test_output})

  [[ ${dir_inode_a} == ${dir_inode_b} ]] \
    || fail "Test //pkg:b didn't reuse runfiles directory"
  [[ ${file_inode_a} == ${file_inode_b} ]] \
    || fail "Test //pkg:b didn't reuse runfiles file"
}

function test_changed_async_deleter_filesystem() {
  if [ ! -d /dev/shm ]; then
    return
  fi

  add_rules_cc MODULE.bazel
  mkdir pkg
  cat >pkg/BUILD <<'EOF'
load("@rules_cc//cc:cc_library.bzl", "cc_library")
cc_library(
  name = "a",
  srcs = [ "a.cc" ],
)
EOF
  touch pkg/a.cc

  bazel build //pkg:a \
    || fail "Expected build to succeed"
  bazel clean
  bazel build --sandbox_base=/dev/shm //pkg:a \
    || fail "Expected build to succeed"
}

function test_bad_state_linux_sandboxing() {
  mkdir pkg

  # This test is meant to catch a bad state being left over by an unfinished
  # linux-sandboxing initialization. Since it's difficult to replicate the same
  # conditions that end up in that state, this instead runs a null build
  # where linux-sandboxing is unsupported by passing -1 grace seconds.
  # Then we create inaccessibleHelperFile/Dir (the bad state) artificially and
  # run a null build again making sure there is no crash.
  bazel build --local_termination_grace_seconds=-1 \
    || fail "Expected build to succeed"
  file_path="$(bazel info output_base)/sandbox/inaccessibleHelperFile"
  dir_path="$(bazel info output_base)/sandbox/inaccessibleHelperDir"

  touch $file_path
  mkdir $dir_path
  chmod 000 $file_path
  chmod 000 $dir_path

  bazel build --local_termination_grace_seconds=-1 \
    || fail "Expected build to succeed"
}

function is_bazel() {
  [ $TEST_WORKSPACE == "_main" ]
}

run_suite "sandboxing"
