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

# Set to a host:port address that is outside of the local machine to
# test remote network sandboxing features.
#
# Can be passed in via --test_env=REMOTE_NETWORK_ADDRESS=host:port.
: "${REMOTE_NETWORK_ADDRESS:=}"

# Load test environment
# 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; }
source ${CURRENT_DIR}/../sandboxing_test_utils.sh \
  || { echo "sandboxing_test_utils.sh not found!" >&2; exit 1; }
source ${CURRENT_DIR}/remote_helpers.sh \
  || { echo "remote_helpers.sh not found!" >&2; exit 1; }

cat >>$TEST_TMPDIR/bazelrc <<'EOF'
# Testing the sandboxed strategy requires using the sandboxed strategy. While it is the default,
# we want to make sure that this explicitly fails when the strategy is not available on the system
# running the test.
build --spawn_strategy=sandboxed --genrule_strategy=sandboxed
EOF

function set_up {
  export BAZEL_GENFILES_DIR=$(bazel info bazel-genfiles 2>/dev/null)
  export BAZEL_BIN_DIR=$(bazel info bazel-bin 2>/dev/null)

  sed -i.bak '/sandbox_tmpfs_path/d' $TEST_TMPDIR/bazelrc

  mkdir -p examples/genrule
  cat << 'EOF' > examples/genrule/a.txt
foo bar bz
EOF
  cat << 'EOF' > examples/genrule/b.txt
apples oranges bananas
EOF

  # Create cyclic symbolic links to check whether the strategy catches that.
  ln -sf cyclic2 examples/genrule/cyclic1
  ln -sf cyclic1 examples/genrule/cyclic2

  # Create relative symlinks.
  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

  echo 'stuff to serve' > file_to_serve

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

sh_binary(
    name = "tool",
    srcs = ["tool.sh"],
    data = ["datafile"],
)

genrule(
    name = "tools_work",
    srcs = [],
    outs = ["tools.txt"],
    cmd = "$(location :tool) $@",
    tools = [":tool"],
)

genrule(
   name = "tooldir",
   srcs = [],
   outs = ["tooldir.txt"],
   cmd = "ls -l external/bazel_tools/tools/genrule | tee $@ >&2; " +
       "cat external/bazel_tools/tools/genrule/genrule-setup.sh >&2",
)

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

genrule(
  name = "breaks1",
  srcs = [ "a.txt" ],
  outs = [ "breaks1.txt" ],
  cmd = "wc $(location :a.txt) `dirname $(location :a.txt)`/b.txt &> $@",
)

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,
)

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" ],
)

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" ],
)

genrule(
  name = "breaks3",
  srcs = [ "cyclic1", "cyclic2" ],
  outs = [ "breaks3.txt" ],
  cmd = "wc $(location :cyclic1) > $@",
)

genrule(
  name = "check_sandbox_contain_WORKSPACE",
  outs = [ "check_sandbox_contain_WORKSPACE.txt" ],
  cmd = "ls -l $$(dirname \"$$(pwd)\") &> $@",
)

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
  cat << 'EOF' >> examples/genrule/datafile
this is a datafile
EOF
  # 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
  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_sandboxed_genrule() {
  bazel build examples/genrule:works &> $TEST_log \
    || fail "Hermetic genrule failed: examples/genrule:works"
  [ -f "${BAZEL_GENFILES_DIR}/examples/genrule/works.txt" ] \
    || fail "Genrule did not produce output: examples/genrule:works"
}

function test_sandboxed_tooldir() {
  bazel build examples/genrule:tooldir &> $TEST_log \
    || fail "Hermetic genrule failed: examples/genrule:tooldir"
  [ -f "${BAZEL_GENFILES_DIR}/examples/genrule/tooldir.txt" ] \
    || fail "Genrule did not produce output: examples/genrule:works"
  cat "${BAZEL_GENFILES_DIR}/examples/genrule/tooldir.txt" > $TEST_log
  expect_log "genrule-setup.sh"
}

function test_sandboxed_genrule_with_tools() {
  bazel build examples/genrule:tools_work &> $TEST_log \
    || fail "Hermetic genrule failed: examples/genrule:tools_work"
  [ -f "${BAZEL_GENFILES_DIR}/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() {
  bazel build examples/genrule:relative_symlinks &> $TEST_log \
    || fail "Hermetic genrule failed: examples/genrule:relative_symlinks"
  [ -f "${BAZEL_GENFILES_DIR}/examples/genrule/relative_symlinks.txt" ] \
    || fail "Genrule did not produce output: examples/genrule:relative_symlinks"
}

function test_sandbox_undeclared_deps() {
  output_file="${BAZEL_GENFILES_DIR}/examples/genrule/breaks1.txt"

  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() {
  bazel build 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_DIR}/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() {
  bazel build 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_DIR}/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_starlark() {
  output_file="${BAZEL_BIN_DIR}/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() {
  bazel build 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_DIR}/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_block_filesystem() {
  # The point of this test is to attempt to read something from the filesystem
  # that is blocked via --sandbox_block_path= and thus should't be accessible.
  #
  # /var/log is an arbitrary choice of directory that should exist on all
  # Unix-like systems.
  local block_path
  case "$(uname -s)" in
    Darwin)
      # TODO(jmmv): sandbox-exec does not resolve symlinks, so attempting
      # to block /var/log does not work. Unsure if we should make this work
      # by resolving symlinks or documenting the expected behavior.
      block_path=/private/var/log
      ;;
    *)
      block_path=/var/log
      ;;
  esac

  mkdir pkg
  cat >pkg/BUILD <<EOF
genrule(
  name = "breaks",
  srcs = [ "a.txt" ],
  outs = [ "breaks.txt" ],
  cmd = "ls ${block_path} &> \$@",
)
EOF
  touch pkg/a.txt

  local output_file="${BAZEL_GENFILES_DIR}/pkg/breaks.txt"

  bazel build --sandbox_block_path="${block_path}" pkg:breaks \
    &> $TEST_log \
    && fail "Non-hermetic genrule succeeded: examples/genrule:breaks" || true

  [ -f "$output_file" ] ||
    fail "Action did not produce output: $output_file"
  cat "${output_file}" >$TEST_log

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

  grep -E "(Operation not permitted|Permission denied)" $output_file ||
    fail "Output did not contain expected error message: $output_file"
}

function test_sandbox_cyclic_symlink_in_inputs() {
  bazel build examples/genrule:breaks3 &> $TEST_log \
    && fail "Genrule with cyclic symlinks succeeded: examples/genrule:breaks3" || true
  [ ! -f "${BAZEL_GENFILES_DIR}/examples/genrule/breaks3.txt" ] || {
    output=$(cat "${BAZEL_GENFILES_DIR}/examples/genrule/breaks3.txt")
    fail "Genrule with cyclic symlinks breaks3 succeeded with following output: $output"
  }
}

# Prepares common targets and services to be used by all network-related
# tests.  The tests for remote network access are only enabled if the
# user has requested them by setting REMOTE_NETWORK_ADDRESS in the
# environment.
function setup_network_tests() {
  local tags="${1}"; shift

  serve_file file_to_serve

  local socket_dir
  socket_dir="$(mktemp -d /tmp/test.XXXXXX)" || fail "mktemp failed"
  local socket="${socket_dir}/socket"
  python $python_server --unix_socket="${socket}" always file_to_serve &
  local pid="${!}"

  trap "kill_nc || true; kill '${pid}' || true; rm -f '${socket}'; rmdir '${socket_dir}'" EXIT

  mkdir pkg
  cat <<EOF >pkg/BUILD
genrule(
  name = "localhost",
  outs = [ "localhost.txt" ],
  cmd = "curl -fo \$@ localhost:${nc_port}",
  tags = [ ${tags} ],
)

genrule(
  name = "unix-socket",
  outs = [ "unix-socket.txt" ],
  cmd = "curl --unix-socket ${socket} -fo \$@ irrelevant-url",
  tags = [ ${tags} ],
)

genrule(
  name = "loopback",
  outs = [ "loopback.txt" ],
  cmd = "python $python_server always $(pwd)/file_to_serve >port.txt & "
      + "pid=\$\$!; "
      + "while ! grep started port.txt; do sleep 1; done; "
      + "port=\$\$(head -n 1 port.txt); "
      + "curl -fo \$@ localhost:\$\$port; "
      + "kill \$\$pid",
)
EOF

  if [[ -n "${REMOTE_NETWORK_ADDRESS}" ]]; then
    local hostname="${REMOTE_NETWORK_ADDRESS%:*}"
    local remote_ip
    if which host 2>/dev/null; then
      remote_ip="$(host -t A "${hostname}" | head -n 1 | awk '{print $4}')"
    elif which dig 2>/dev/null; then
      remote_ip="$(dig -t A "${hostname}" | grep "^${hostname}" | awk '{print $5}')"
    else
      fail "Don't know how to query IP of remote host ${hostname}"
    fi
    if [[ -z "${remote_ip}" ]]; then
      fail "No IPv4 connectivity within unsandboxed test"
    fi

    cat <<EOF >>pkg/BUILD
genrule(
  name = "remote-ip",
  outs = [ "remote-ip.txt" ],
  cmd = "curl -fo \$@ ${remote_ip}:80",
  tags = [ ${tags} ],
)

genrule(
  name = "remote-name",
  outs = [ "remote-name.txt" ],
  cmd = "curl -fo \$@ '${REMOTE_NETWORK_ADDRESS}'",
  tags = [ ${tags} ],
)
EOF
  else
    echo "Not registering tests for remote network sandboxing;" \
      "REMOTE_NETWORK_ADDRESS has not been set"
  fi
}

# Checks that the given target name, which must have been created by
# a previous call to setup_network_tests, can access the network.
function check_network_ok() {
  local target="${1}"; shift

  (
    # macOS's /bin/bash is ancient and cannot reference $@ when -u is set.
    # https://unix.stackexchange.com/questions/16560/bash-su-unbound-variable-with-set-u
    set +u

    bazel build "${@}" "pkg:${target}" &>$TEST_log \
      || fail "'${target}' could not access the network"
  )
}

# Checks that the given target name, which must have been created by
# a previous call to setup_network_tests, cannot access the network.
function check_network_not_ok() {
  local target="${1}"; shift

  (
    # macOS's /bin/bash is ancient and cannot reference $@ when -u is set.
    # https://unix.stackexchange.com/questions/16560/bash-su-unbound-variable-with-set-u
    set +u

    bazel build "${@}" "pkg:${target}" &> $TEST_log \
      && fail "'${target}' trying to use network succeeded but should have failed" || true
  )
  [[ ! -f "${BAZEL_GENFILES_DIR}/pkg/${target}.txt" ]] \
    || fail "'${target}' produced output but was expected to fail"
}

function test_sandbox_network_access() {
  setup_network_tests '"some-tag"'

  check_network_ok localhost
  check_network_ok unix-socket
  check_network_ok loopback
  if [[ -n "${REMOTE_NETWORK_ADDRESS}" ]]; then
    check_network_ok remote-ip
    check_network_ok remote-name
  fi
}

function test_sandbox_block_network_access() {
  setup_network_tests '"some-tag"'

  case "$(uname -s)" in
    Linux)
      # TODO(jmmv): The linux-sandbox claims to allow localhost connectivity
      # within the network namespace... but that doesn't seem to be the case.
      check_network_not_ok localhost --experimental_sandbox_default_allow_network=false
      ;;

    *)
      check_network_ok localhost --experimental_sandbox_default_allow_network=false
      ;;
  esac
  check_network_ok unix-socket --experimental_sandbox_default_allow_network=false
  check_network_ok loopback --experimental_sandbox_default_allow_network=false
  if [[ -n "${REMOTE_NETWORK_ADDRESS}" ]]; then
    check_network_not_ok remote-ip --experimental_sandbox_default_allow_network=false
    check_network_not_ok remote-name --experimental_sandbox_default_allow_network=false
  fi
}

function test_sandbox_network_access_with_local() {
  setup_network_tests '"local"'

  check_network_ok localhost
  check_network_ok unix-socket
  check_network_ok loopback
  if [[ -n "${REMOTE_NETWORK_ADDRESS}" ]]; then
    check_network_ok remote-ip
    check_network_ok remote-name
  fi
}

function test_sandbox_network_access_with_requires_network() {
  setup_network_tests '"requires-network"'

  check_network_ok localhost --experimental_sandbox_default_allow_network=false
  check_network_ok unix-socket --experimental_sandbox_default_allow_network=false
  check_network_ok loopback --experimental_sandbox_default_allow_network=false
  if [[ -n "${REMOTE_NETWORK_ADDRESS}" ]]; then
    check_network_ok remote-ip --experimental_sandbox_default_allow_network=false
    check_network_ok remote-name --experimental_sandbox_default_allow_network=false
  fi
}

function test_sandbox_network_access_with_block_network() {
  setup_network_tests '"block-network"'

  case "$(uname -s)" in
    Linux)
      # TODO(jmmv): The linux-sandbox claims to allow localhost connectivity
      # within the network namespace... but that doesn't seem to be the case.
      check_network_not_ok localhost --experimental_sandbox_default_allow_network=true
      ;;

    *)
      check_network_ok localhost --experimental_sandbox_default_allow_network=true
      ;;
  esac
  check_network_ok unix-socket --experimental_sandbox_default_allow_network=true
  check_network_ok loopback --experimental_sandbox_default_allow_network=true
  if [[ -n "${REMOTE_NETWORK_ADDRESS}" ]]; then
    check_network_not_ok remote-ip --experimental_sandbox_default_allow_network=true
    check_network_not_ok remote-name --experimental_sandbox_default_allow_network=true
  fi
}

function test_sandbox_can_resolve_own_hostname() {
  setup_javatest_support
  mkdir -p src/test/java/com/example
  cat > src/test/java/com/example/HostNameTest.java <<'EOF'
package com.example;

import static org.junit.Assert.*;

import org.junit.Test;
import java.net.*;
import java.io.*;

public class HostNameTest {
  @Test
  public void testGetHostName() throws Exception {
    // This will throw an exception, if the local hostname cannot be resolved via DNS.
    assertNotNull(InetAddress.getLocalHost().getHostName());
  }
}
EOF
  cat > src/test/java/com/example/BUILD <<'EOF'
java_test(
  name = "HostNameTest",
  srcs = ["HostNameTest.java"],
  deps = ['//third_party:junit4'],
)
EOF

  bazel test --test_output=streamed src/test/java/com/example:HostNameTest &> $TEST_log \
    || fail "test should have passed"
}

function test_hostname_inside_sandbox_is_localhost_when_using_sandbox_fake_hostname_flag() {
  if [[ "$(uname -s)" != Linux ]]; then
    echo "Skipping test: fake hostnames not supported in this system" 1>&2
    return 0
  fi

  setup_javatest_support
  mkdir -p src/test/java/com/example
  cat > src/test/java/com/example/HostNameIsLocalhostTest.java <<'EOF'
package com.example;

import static org.junit.Assert.*;

import org.junit.Test;
import java.net.*;
import java.io.*;

public class HostNameIsLocalhostTest {
  @Test
  public void testHostNameIsLocalhost() throws Exception {
    // This will throw an exception, if the local hostname cannot be resolved via DNS.
    assertEquals("localhost", InetAddress.getLocalHost().getHostName());
  }
}
EOF
  cat > src/test/java/com/example/BUILD <<'EOF'
java_test(
  name = "HostNameIsLocalhostTest",
  srcs = ["HostNameIsLocalhostTest.java"],
  deps = ['//third_party:junit4'],
)
EOF

  bazel test --sandbox_fake_hostname --test_output=streamed src/test/java/com/example:HostNameIsLocalhostTest &> $TEST_log \
    || fail "test should have passed"
}

# TODO(philwo) - this doesn't work on Ubuntu 14.04 due to "unshare" being too
# old and not understanding the --user flag.
function DISABLED_test_sandbox_different_nobody_uid() {
  cat /etc/passwd | sed 's/\(^nobody:[^:]*:\)[0-9]*:[0-9]*/\15000:16000/g' > \
      "${TEST_TMPDIR}/passwd"
  unshare --user --mount --map-root-user -- bash - \
      << EOF || fail "Hermetic genrule with different UID for nobody failed" \
set -e
set -u

mount --bind ${TEST_TMPDIR}/passwd /etc/passwd
bazel build examples/genrule:works &> ${TEST_log}
EOF
}

function test_requires_root() {
  if [[ "$(uname -s)" != Linux ]]; then
    echo "Skipping test: fake usernames not supported in this system" 1>&2
    return 0
  fi

  cat > test.sh <<'EOF'
#!/bin/sh
([ $(id -u) = "0" ] && [ $(id -g) = "0" ]) || exit 1
EOF
  chmod +x test.sh
  cat > BUILD <<'EOF'
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 a pseudoterminal can be opened in linux when --sandbox_explicit_pseudoterminal is active
function test_can_enable_pseudoterminals() {
  if [[ "$(uname -s)" != Linux ]]; then
    echo "Skipping test: flag intended for linux systems"
    return 0
  fi

  cat > test.py <<'EOF'
import pty
pty.openpty()
EOF
  cat > BUILD <<'EOF'
py_test(
  name = "test",
  srcs = ["test.py"],
)
EOF
  bazel test --sandbox_explicit_pseudoterminal :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

  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_DIR}/examples/genrule/check_proc_works.txt")" \
          != "$(tail -n1 "${BAZEL_GENFILES_DIR}/examples/genrule/check_proc_works.txt")" ]] ; then
      fail "Reading PID from /proc/self/stat should have worked, instead have these: $(cat "${BAZEL_GENFILES_DIR}/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 0500 `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 move output artifacts from sandboxed execution"

  # 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 0500 `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 move output artifacts from sandboxed execution"

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

function test_sandbox_debug() {
  cat > BUILD <<'EOF'
genrule(
  name = "broken",
  outs = ["bla.txt"],
  cmd = "exit 1",
)
EOF
  bazel build --verbose_failures :broken &> $TEST_log \
    && fail "build should have failed" || true
  expect_log "Use --sandbox_debug to see verbose messages from the sandbox and retain the sandbox build root for debugging"
  expect_log "Executing genrule //:broken failed"

  bazel build --verbose_failures --sandbox_debug :broken &> $TEST_log \
    && fail "build should have failed" || true
  expect_log "Executing genrule //:broken failed"
  expect_not_log "Use --sandbox_debug to see verbose messages from the sandbox and retain the sandbox build root for debugging"
  # This will appear a lot in the sandbox failure details.
  expect_log "/sandbox/"  # Part of the path to the sandbox location.
}

function test_sandbox_expands_tree_artifacts_in_runfiles_tree() {
  create_workspace_with_default_repos WORKSPACE

  cat > def.bzl <<'EOF'
def _mkdata_impl(ctx):
    out = ctx.actions.declare_directory(ctx.label.name + ".d")
    script = "mkdir -p {out}; touch {out}/file; ln -s file {out}/link".format(out = out.path)
    ctx.actions.run_shell(
        outputs = [out],
        command = script,
    )
    runfiles = ctx.runfiles(files = [out])
    return [DefaultInfo(
        files = depset([out]),
        runfiles = runfiles,
    )]

mkdata = rule(
    _mkdata_impl,
)
EOF

  cat > mkdata_test.sh <<'EOF'
#!/bin/bash

set -euo pipefail

test_dir="$1"
cd "$test_dir"
ls -l | cut -f1,9 -d' ' >&2

if [ ! -f file -o -L file ]; then
  echo "'file' is not a regular file" >&2
  exit 1
fi
EOF
  chmod +x mkdata_test.sh

  cat > BUILD <<'EOF'
load("//:def.bzl", "mkdata")

mkdata(name = "mkdata")

sh_test(
    name = "mkdata_test",
    srcs = ["mkdata_test.sh"],
    args = ["$(location :mkdata)"],
    data = [":mkdata"],
)
EOF

  bazel test --test_output=streamed //:mkdata_test &>$TEST_log && fail "expected test to fail" || true
  expect_log "'file' is not a regular file"
}

# regression test for https://github.com/bazelbuild/bazel/issues/6262
function test_create_tree_artifact_outputs() {
  create_workspace_with_default_repos WORKSPACE

  cat > def.bzl <<'EOF'
def _r(ctx):
    d = ctx.actions.declare_directory("%s_dir" % ctx.label.name)
    ctx.actions.run_shell(
        outputs = [d],
        command = "cd %s && pwd" % d.path,
    )
    return [DefaultInfo(files = depset([d]))]

r = rule(implementation = _r)
EOF

cat > BUILD <<'EOF'
load(":def.bzl", "r")

r(name = "a")
EOF

  bazel build --test_output=streamed :a &>$TEST_log || fail "expected build to succeed"
}

function test_empty_tree_artifact_as_inputs() {
  # Test that when an empty tree artifact is the input, an empty directory is
  # created in the sandbox for action to read.
  create_workspace_with_default_repos WORKSPACE

  mkdir -p pkg

  cat > pkg/def.bzl <<'EOF'
def _r(ctx):
    empty_d = ctx.actions.declare_directory("%s/empty_dir" % ctx.label.name)
    ctx.actions.run_shell(
        outputs = [empty_d],
        command = "mkdir -p %s" % empty_d.path,
    )
    f = ctx.actions.declare_file("%s/file" % ctx.label.name)
    ctx.actions.run_shell(
        inputs = [empty_d],
        outputs = [f],
        command = "touch %s && cd %s && pwd" % (f.path, empty_d.path),
    )
    return [DefaultInfo(files = depset([f]))]

r = rule(implementation = _r)
EOF

cat > pkg/BUILD <<'EOF'
load(":def.bzl", "r")

r(name = "a")
EOF

  bazel build //pkg:a &>$TEST_log || fail "expected build to succeed"
}

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

  mkdir -p pkg
  cat > pkg/BUILD <<'EOF'
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 {
  if [[ "$(uname -s)" != Linux ]]; then
    echo "Skipping test: --incompatible_sandbox_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

  mkdir -p pkg
  cat > pkg/BUILD <<'EOF'
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 --incompatible_sandbox_hermetic_tmp \
    --test_output=errors &>$TEST_log || fail "Expected test to pass"
}

function test_read_hermetic_tmp_user_override {
  if [[ "$(uname -s)" != Linux ]]; then
    echo "Skipping test: --incompatible_sandbox_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

  mkdir -p pkg
  cat > pkg/BUILD <<'EOF'
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 --incompatible_sandbox_hermetic_tmp --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

  mkdir -p pkg
  cat > pkg/BUILD <<'EOF'
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 \
    --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 [[ "$(uname -s)" != Linux ]]; then
    echo "Skipping test: --incompatible_sandbox_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

  mkdir -p pkg
  cat > pkg/BUILD <<'EOF'
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 --incompatible_sandbox_hermetic_tmp \
    --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 [[ "$(uname -s)" != Linux ]]; then
    echo "Skipping test: --incompatible_sandbox_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

  mkdir -p pkg
  cat > pkg/BUILD <<'EOF'
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 --incompatible_sandbox_hermetic_tmp --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"
}

# The test shouldn't fail if the environment doesn't support running it.
check_sandbox_allowed || exit 0

run_suite "sandbox"
