#!/usr/bin/env bash
# vim: set shiftwidth=2 :
# XXX: `bash` shebang because this relies on bashisms, not just `sh`!

set -o pipefail
set -eu

SELF=$(realpath $0)

# we can't rely on /usr/bin/env existing in the nix build environment
bunpen=$(which bunpen)
env=$(which env)
test=$(which test)

die() {
  echo "$1"
  return 1
}

captureExitStatus() {
  # run this command in a subshell, to prevent `set -e` from aborting the main shell.
  # then echo its return code
  ("$@" >&2; echo $?)
}

test_01_invoke_01_trivial() {
  bunpen --bunpen-debug=4 --bunpen-path / "$env" true
}

test_01_invoke_02_by_path() {
  bunpen --bunpen-path / true
}

test_01_invoke_03_errors_on_invalid_file() {
  bunpen --bunpen-path / this_file_does_not_exist && return 1 || true
}

test_01_invoke_04_errors_if_not_in_sandbox() {
  bunpen true && return 1 || true
}

test_01_invoke_05_nested() {
  bunpen --bunpen-path / --bunpen-seal bunpen --bunpen-path / true
}

test_02_env_arg_01_disable() {
  BUNPEN_DISABLE=1 bunpen true
}

test_02_env_arg_02_append() {
  BUNPEN_APPEND="--bunpen-path /" bunpen ls /
}

test_02_env_arg_03_append_tolerates_whitespace() {
  BUNPEN_APPEND="  --bunpen-debug=4  --bunpen-keep-pid --bunpen-path   / " bunpen ls /
}

test_03_propagates_01_rc_when_disabled() {
  test $(captureExitStatus env BUNPEN_DISABLE=1 bunpen sh -c "exit 55") = 55
}

test_03_propagates_02_rc() {
  test $(captureExitStatus bunpen --bunpen-path / sh -c "exit 55") = 55
}

test_03_propagates_03_stdout_when_disabled() {
  test $(BUNPEN_DISABLE=1 bunpen --bunpen-path / sh -c "echo hello") = hello
}

test_03_propagates_04_stdout() {
  local stdout=$(bunpen --bunpen-path / echo "hello")
  test "$stdout" = "hello"
}

test_04_logs_01_disabled_by_default() {
  unset BUNPEN_DEBUG
  local stdout=$(bunpen --bunpen-path / true 2>&1)
  test -z "$stdout"
}

test_04_logs_02_enabled() {
  unset BUNPEN_DEBUG
  local stdout=$(bunpen --bunpen-debug=4 --bunpen-path / true 2>&1)
  test -n "$stdout"
}

test_05_keep_01_path_minimal() {
  bunpen --bunpen-path "$env" --bunpen-path /nix/store "$env"
}

test_05_keep_02_path_no_extra_in_sandbox() {
  touch test_file0
  bunpen --bunpen-path "$test" --bunpen-path /nix/store "$test" ! -f test_file0
}

test_05_keep_03_path_added_to_sandbox() {
  touch test_file0
  bunpen --bunpen-path "$test" --bunpen-path /nix/store --bunpen-path test_file0 "$test" -f test_file0
}

test_06_keep_04_path_handles_non_existent() {
  bunpen --bunpen-path "$test" --bunpen-path /nix/store --bunpen-path test_file0 "$test" ! -f test_file0
}

test_06_keep_05_path_absolute() {
  touch test_file0
  local p=$(realpath test_file0)
  bunpen --bunpen-path "$test" --bunpen-path /nix/store --bunpen-path "$p" "$test" -f "$p"
}

test_06_keep_06_path_home() {
  mkdir test_dir0
  touch test_dir0/test_file0
  export HOME=$PWD/test_dir0
  bunpen --bunpen-path "$test" --bunpen-path /nix/store --bunpen-path '$HOME/test_file0' "$test" -f "$HOME/test_file0"
}

test_06_keep_07_path_xdg_runtime_dir() {
  mkdir test_dir0
  touch test_dir0/test_file0
  mkdir test_dir1
  touch test_dir1/test_file1
  export HOME=$PWD/test_dir0
  export XDG_RUNTIME_DIR=$PWD/test_dir1
  bunpen --bunpen-path "$test" --bunpen-path /nix/store --bunpen-path '$XDG_RUNTIME_DIR/test_file1' "$test" -f "$XDG_RUNTIME_DIR/test_file1"
}

test_06_keep_08_path_symlink_1_layer() {
  touch test_file0
  ln -s test_file0 test_symlink0
  touch test_file1
  bunpen --bunpen-path "$test" --bunpen-path /nix/store --bunpen-path test_symlink0 "$test" -f test_file0 -a -f test_symlink0 -a ! -f test_file1
}

test_06_keep_09_path_symlink_2_layers() {
  touch test_file0
  ln -s test_file0 test_symlink0
  ln -s test_symlink0 test_symlink1
  touch test_file1
  bunpen --bunpen-path "$test" --bunpen-path /nix/store --bunpen-path test_symlink1 "$test" -f test_file0 -a -f test_symlink1 -a -f test_symlink0 -a ! -f test_file1
}

test_06_keep_10_net() {
  # new namespaces get a loopback device, however bunpen doesn't auto-up the loopback.
  # most containers (e.g. nix build environment) do up the lo.
  # it wouldn't necessarily be _bad_ if bunpen changed behavior here,
  # but in the meantime this is the easiest way to see that --bunpen-keep-net *does something*
  # (better would be to try communicating between test runner and bunpen program, e.g. netcat?)
  test -n "$(bunpen --bunpen-path / --bunpen-keep-net ip link show lo up)"
  test -z "$(bunpen --bunpen-path / ip link show lo up)"
}

test_06_keep_11_pid() {
  set -x
  local me=$$
  # /proc should be auto-mounted if not keeping PIDs; exterior PID is invisible
  bunpen --bunpen-path /nix/store "$test" -e /proc/self
  ! bunpen --bunpen-path /nix/store "$test" -e /proc/$me

  # /proc should *not* be auto-mounted if keeping PIDs
  ! bunpen --bunpen-path /nix/store --bunpen-keep-pid "$test" -e /proc/self

  # if keeping PIDs and explicitly mounting, exterior PID should be visible
  bunpen --bunpen-path /nix/store --bunpen-path /proc --bunpen-keep-pid "$test" -e /proc/self
  bunpen --bunpen-path /nix/store --bunpen-path /proc --bunpen-keep-pid "$test" -e /proc/$me

  # if explicitly mounting /proc, but not keeping PIDs... TODO: consider changing this so exterior PIDs are visible.
  # this just documents the existing behavior as per 2024/12/25
  bunpen --bunpen-path /nix/store --bunpen-path /proc "$test" -e /proc/self
  ! bunpen --bunpen-path /nix/store --bunpen-path /proc "$test" -e /proc/$me
}

test_06_keep_12_pid_nested() {
  set -x
  bunpen --bunpen-path /nix/store --bunpen-path "$bunpen" --bunpen-seal \
    "$bunpen" --bunpen-path /nix/store "$test" -e /proc/self
  bunpen --bunpen-path /nix/store --bunpen-path "$bunpen" --bunpen-path /proc --bunpen-keep-pid --bunpen-seal \
    "$bunpen" --bunpen-path /nix/store "$test" -e /proc/self

  bunpen --bunpen-path /nix/store --bunpen-path "$bunpen" --bunpen-seal \
    "$bunpen" --bunpen-path /nix/store --bunpen-path "$bunpen" --bunpen-seal \
    "$bunpen" --bunpen-path /nix/store "$test" -e /proc/self
}

test_07_env_01_keep() {
  ORIG_ENV=orig bunpen --bunpen-path / bash -c '[[ "$ORIG_ENV" = orig && -z "$NOT_ENV" ]]'
}

test_07_env_02_new() {
  bunpen --bunpen-path / --bunpen-env NEW_ENV=new bash -c '[[ "$NEW_ENV" = new && -z "$NOT_ENV" ]]'
}

test_07_env_03_update() {
  UPD_ENV=orig bunpen --bunpen-path / --bunpen-env UPD_ENV=new bash -c '[[ "$UPD_ENV" = new && -z "$NOT_ENV" ]]'
}

test_07_env_04_substitute_home() {
  # HOME defaults to /homeless-shelter
  # XDG_RUNTIME_DIR defaults to (unset)
  bunpen --bunpen-path / --bunpen-env 'H_ENV=/head$HOME/tail' --bunpen-env 'R_ENV=$XDG_RUNTIME_DIR/tail' bash -c \
    '[[ "$H_ENV" = /head/homeless-shelter/tail && "$R_ENV" = /tail ]]'
}
test_07_env_05_subtitute_home_and_runtime() {
  XDG_RUNTIME_DIR=/r/t bunpen --bunpen-path / --bunpen-env 'H_ENV=/head$HOME/tail' --bunpen-env 'R_ENV=$XDG_RUNTIME_DIR/tail' bash -c \
    '[[ "$H_ENV" = /head/homeless-shelter/tail && "$R_ENV" = /r/t/tail ]]'
}
test_07_env_06_subsitute_escape() {
  bunpen --bunpen-path / --bunpen-env 'H_ENV=/head$$HOME/tail' --bunpen-env 'H_ENV2=$$$HOME/tail' bash -c \
    'echo "$H_ENV2" && [[ "$H_ENV" = '"'"'/head$HOME/tail'"'"' && "$H_ENV2" = '"'"'$/homeless-shelter/tail'"'"' ]]'
}

# runs a command in the sandbox and sends the sandbox helper SIGHUP.
# if the command runs to completion (because e.g. SIGHUP was blocked), it prints "completed"
# if the command aborts (because it received SIGHUP), it doesn't print anything.
# invoke as `signal_test_helper "nohup"` to block the signal inside the sandbox.
# calling once with and without "nohup" lets us be certain that SIGHUP was in fact delivered.
signal_test_helper() {
  # sleep for the test duration inside the bunpen
  exec 3< <(bunpen --bunpen-path / "$@" sh -c "echo loaded; sleep 2; echo completed")
  local parent=$!

  read <&3 line
  test line = "loaded"
  # SIGHUP the bunpen and see if it actually forwards to the child.
  "$env" kill -SIGHUP "$parent"
  read <&3 line
  echo "$line"
}

test_08_signals_01_receives() {
  test -z "$(signal_test_helper)"
}
test_08_signals_02_forwards() {
  test "$(signal_test_helper nohup)" = "completed"
}

test_09_reap_children() {
  # in a PID namespace, PID 1 needs to reap children.
  # that is, any processes which `fork` away from the main program being sandboxed,
  # and then exit, become zombies: PID 1 needs to `wait` on them to properly dispose of the processes.
  bunpen --bunpen-path / bash -c "setsid -f true ; sleep 2" &
  sleep 0.5
  # check for a line like: `225215 ?        Zs     0:00 [true] <defunct>`
  # if this line exists, then we failed to reap
  ps x | grep -E 'Zs +[0-9]+:[0-9]+ \[true\] <defunct>' && return 1 || return 0
}

test_10_tmpfs_root_is_introspectable() {
  echo from_parent > test_file0
  bunpen --bunpen-debug=3 --bunpen-keep-pid --bunpen-path /nix/store --bunpen-path $PWD bash -c "set -x; test -f $PWD/test_file0 && echo 'from_sandbox' > /test_file1 && sleep 2" &
  local sbox_pid=$!
  sleep 0.5

  # /proc/$pid/root is the rootfs as seen from inside the sandbox.
  # within the sandbox, `/unbacked` holds the actual rootfs, without anyting mounted over it;
  # i.e., the portion of the fstree which is backed only by the tmpfs-root
  test ! -e /proc/$sbox_pid/root/unbacked/$PWD/test_file0
  test /proc/$sbox_pid/root/$PWD/test_file0
  local test_file1=$(cat /proc/$sbox_pid/root/unbacked/test_file1)

  # diagnostics, should the test fail:
  echo -n "/proc/$sbox_pid: " && ls /proc/$sbox_pid
  echo -n "/proc/$sbox_pid/root: " && ls /proc/$sbox_pid/root
  echo -n "/proc/$sbox_pid/root/unbacked: " && ls /proc/$sbox_pid/root/unbacked
  echo -n "/proc/$sbox_pid/root/unbacked/test_file1: " && echo "$test_file1"

  # even more diagnostics, to see that `unbacked` does actually have stuff at least
  echo -n "/proc/$sbox_pid/root/unbacked/$(dirname $(dirname $PWD)): " && ls -l /proc/$sbox_pid/root/unbacked/$(dirname $(dirname $PWD))
  echo -n "/proc/$sbox_pid/root/unbacked/$(dirname $PWD): " && ls -l /proc/$sbox_pid/root/unbacked/$(dirname $PWD)
  echo -n "/proc/$sbox_pid/root/unbacked/$PWD: " && ls -l /proc/$sbox_pid/root/unbacked/$PWD

  test "$test_file1" = from_sandbox
}

dbus_daemon_setup() {
  export DBUS_SESSION_BUS_ADDRESS=unix:path=$PWD/dbus/bus
  mkdir -p $PWD/dbus
  dbus-daemon --address="$DBUS_SESSION_BUS_ADDRESS" --config-file $PWD/test/dbus-session.conf --fork
}

test_11_dbus_01_whitelisted_by_path() {
  dbus_daemon_setup

  # busctl exits clean if it can reach the bus
  bunpen --bunpen-path /nix/store --bunpen-path $PWD busctl --user list
  # sanity check that it would exit falsey if it fails to reach the bus
  ! env -u DBUS_SESSION_BUS_ADDRESS \
    bunpen --bunpen-path /nix/store --bunpen-path $PWD busctl --user list
}

test_11_dbus_02_doesnt_bridge_bus_by_default() {
  dbus_daemon_setup

  ! bunpen --bunpen-path /nix/store busctl --user list
  # but still reachable from the outside
  busctl --user list
}

test_11_dbus_03_doesnt_spawn_bus() {
  export DBUS_SESSION_BUS_ADDRESS=unix:path=$PWD/dbus/bus
  ! bunpen --bunpen-path /nix/store --bunpen-path $PWD busctl --user list
}

test_11_dbus_04_proxy_nonexistent() {
  dbus_daemon_setup

  # parameters *have* to be "valid" dbus names, i.e. a-z with dot separators, at least multiple components.
  # they don't have to be actual, existing names
  bunpen --bunpen-path /nix/store --bunpen-dbus-call 'does.not.exist=*' busctl --user list
  bunpen --bunpen-path /nix/store --bunpen-dbus-own 'does.not.exist' busctl --user list
  bunpen --bunpen-path /nix/store --bunpen-dbus-call 'does.not.exist=*' --bunpen-dbus-own 'also.not.exist' busctl --user list
}

test_11_dbus_05_proxy_bypass() {
  dbus_daemon_setup
  dbus-test-tool echo --name=com.example.Echo &
  sleep 1 # wait for readiness

  bunpen --bunpen-path /nix/store --bunpen-path $PWD/dbus/bus --bunpen-dbus-call 'does.not.exist=*' busctl --user list
  bunpen --bunpen-path /nix/store --bunpen-path $PWD/dbus --bunpen-dbus-call 'does.not.exist=*' busctl --user list
  bunpen --bunpen-path /nix/store --bunpen-path $PWD --bunpen-dbus-call 'does.not.exist=*' busctl --user list

  # try calling a method
  bunpen --bunpen-path /nix/store --bunpen-path $PWD/dbus --bunpen-dbus-call 'does.not.exist=*' busctl --user call com.example.Echo / com.example.Echo echo s "hello"
  # call a non-existent service
  ! bunpen --bunpen-path /nix/store --bunpen-path $PWD/dbus --bunpen-dbus-call 'does.not.exist=*' busctl --user call com.example.NoExist / com.example.Echo echo s "hello"
}

test_11_dbus_06_proxy_call() {
  dbus_daemon_setup
  dbus-test-tool echo --name=com.example.Echo1 &
  dbus-test-tool echo --name=com.example.Echo2 &
  dbus-test-tool echo --name=com.example.Echo3 &
  sleep 1 # wait for readiness

  # make sure the service is visible
  bunpen --bunpen-path /nix/store --bunpen-dbus-call 'com.example.Echo1=*' --bunpen-dbus-call 'com.example.Echo2=*' busctl --user status com.example.Echo1
  # call a method on the service. the echo service will reply, but with an empty message
  bunpen --bunpen-path /nix/store --bunpen-dbus-call 'com.example.Echo1=*' --bunpen-dbus-call 'com.example.Echo2=*' busctl --user call com.example.Echo1 / com.example.Echo1 echo s "hello"
  bunpen --bunpen-path /nix/store --bunpen-dbus-call 'com.example.Echo1=*' --bunpen-dbus-call 'com.example.Echo2=*' busctl --user call com.example.Echo2 / com.example.Echo2 echo s "hello"

  # call a method we're not allowed to call
  ! bunpen --bunpen-path /nix/store --bunpen-dbus-call 'com.example.Echo1=*' --bunpen-dbus-call 'com.example.Echo2=*' busctl --user call com.example.Echo3 / com.example.Echo3 echo s "hello"

  # call a non-existent service
  ! bunpen --bunpen-path /nix/store --bunpen-dbus-call 'com.example.Echo1=*' --bunpen-dbus-call 'com.example.Echo2=*' busctl --user call com.example.NoExist / com.example.Echo1 echo s "hello"
}

test_11_dbus_07_proxy_call_detail() {
  dbus_daemon_setup
  dbus-test-tool echo --name=com.example.Echo1 &
  sleep 1 # wait for readiness

  # busctl call <name> <path> <interface> <method>
  # --bunpen-dbus-call <name>=<interface>.<method>@/object/path
  bunpen --bunpen-path /nix/store --bunpen-dbus-call 'com.example.Echo1=*' busctl --user call com.example.Echo1 /object/path example.Interface echo s "hello"
  bunpen --bunpen-path /nix/store --bunpen-dbus-call 'com.example.Echo1=example.Interface.echo' busctl --user call com.example.Echo1 /object/path example.Interface echo s "hello"
  ! bunpen --bunpen-path /nix/store --bunpen-dbus-call 'com.example.Echo1=example.Interface.echo-not-allowed' busctl --user call com.example.Echo1 /object/path example.Interface echo s "hello"
  bunpen --bunpen-path /nix/store --bunpen-dbus-call 'com.example.Echo1=example.Interface.*' busctl --user call com.example.Echo1 /object/path example.Interface echo s "hello"
  ! bunpen --bunpen-path /nix/store --bunpen-dbus-call 'com.example.Echo1=example.WrongInterface.*' busctl --user call com.example.Echo1 /object/path example.Interface echo s "hello"
  bunpen --bunpen-path /nix/store --bunpen-dbus-call 'com.example.Echo1=example.Interface.echo@/object/path' busctl --user call com.example.Echo1 /object/path example.Interface echo s "hello"
  ! bunpen --bunpen-path /nix/store --bunpen-dbus-call 'com.example.Echo1=example.Interface.echo@/object/path/wrong' busctl --user call com.example.Echo1 /object/path example.Interface echo s "hello"
  bunpen --bunpen-path /nix/store --bunpen-dbus-call 'com.example.Echo1=example.Interface.echo@/object/*' busctl --user call com.example.Echo1 /object/path example.Interface echo s "hello"
}

test_11_dbus_08_proxy_own() {
  dbus_daemon_setup

  bunpen --bunpen-path /nix/store --bunpen-dbus-own 'com.example.Echo1' --bunpen-dbus-own 'com.example.Echo2' dbus-test-tool echo --name="com.example.Echo1" &
  sleep 1  # wait for readiness

  busctl --user list | grep com.example.Echo1
  # busctl --user status com.example.Echo1  # TODO: fails for some reason... maybe because it can't figure out the PID/username to show?
  busctl --user call com.example.Echo1 / com.example.Echo1 echo s "hello"

  # not allowed to own call-only addresses
  ! bunpen --bunpen-path /nix/store --bunpen-dbus-call 'com.example.Echo1=*' dbus-test-tool echo --name="com.example.Echo1"

  # not allowed to own non-whitelisted addresses
  ! bunpen --bunpen-path /nix/store --bunpen-dbus-own 'com.example.Echo1' --bunpen-dbus-own 'com.example.Echo2' dbus-test-tool echo --name="com.example.Echo3"
  ! bunpen --bunpen-path /nix/store dbus-test-tool echo --name="com.example.Echo1"
}

test_11_dbus_09_proxy_partial_sandbox() {
  dbus_daemon_setup
  dbus-test-tool echo --name=com.example.Echo1 &
  sleep 1 # wait for readiness

  bunpen --bunpen-path /nix/store --bunpen-cap all --bunpen-dbus-call 'com.example.Echo1=*' busctl --user call com.example.Echo1 / com.example.Echo1 echo s "hello"
  bunpen --bunpen-path /nix/store --bunpen-keep-ipc --bunpen-dbus-call 'com.example.Echo1=*' busctl --user call com.example.Echo1 / com.example.Echo1 echo s "hello"
  bunpen --bunpen-path /nix/store --bunpen-keep-net --bunpen-dbus-call 'com.example.Echo1=*' busctl --user call com.example.Echo1 / com.example.Echo1 echo s "hello"
  bunpen --bunpen-path /nix/store --bunpen-keep-pid --bunpen-path /proc --bunpen-dbus-call 'com.example.Echo1=*' busctl --user call com.example.Echo1 / com.example.Echo1 echo s "hello"
  bunpen --bunpen-path /nix/store --bunpen-keep-pid --bunpen-dbus-call 'com.example.Echo1=*' busctl --user call com.example.Echo1 / com.example.Echo1 echo s "hello"
  bunpen --bunpen-path /nix/store --bunpen-try-keep-users --bunpen-dbus-call 'com.example.Echo1=*' busctl --user call com.example.Echo1 / com.example.Echo1 echo s "hello"
  mkdir file-for-test ;
    bunpen --bunpen-path /nix/store --bunpen-path file-for-test --bunpen-dbus-call 'com.example.Echo1=*' busctl --user call com.example.Echo1 / com.example.Echo1 echo s "hello"
  bunpen --bunpen-path /nix/store --bunpen-path /proc --bunpen-dbus-call 'com.example.Echo1=*' busctl --user call com.example.Echo1 / com.example.Echo1 echo s "hello"
}

TODO_DISABLED_test_11_dbus_10_proxy_nested_call() {
  dbus_daemon_setup
  dbus-test-tool echo --name=com.example.Echo1 &
  sleep 1 # wait for readiness

  # TODO: nested dbus proxies fail with `Invalid client serial: Exceeds maximum value of 4294901759`
  # could possibly be related to <https://github.com/flatpak/xdg-dbus-proxy/issues/46>
  # especially if they went with the solution where they MUX based on the high-bit instead of the low bit
  bunpen --bunpen-path /nix/store --bunpen-path "$bunpen"  --bunpen-dbus-call 'com.example.Echo1=*' --bunpen-seal \
    "$bunpen" --bunpen-path /nix/store --bunpen-path "$env" --bunpen-dbus-call 'com.example.Echo1=*' \
    "$env" busctl --user call com.example.Echo1 / com.example.Echo1 echo s "hello"
}

runTests() {
  local testsToRun=("$@")
  rc=0
  succeeded=()
  failed=()
  basedir=$PWD
  for f in "${testsToRun[@]}"; do
    mkdir "$basedir/$f"
    cp -r ./test "$basedir/$f/test"
    echo -n "$f: ..."
    cd "$basedir/$f"
    if timeout 10 $SELF "$f" > "$basedir/$f/stdall" 2>&1; then
      echo " SUCCESS"
      succeeded+=("$f")
    else
      rc=1
      echo " FAIL"
      cat "$basedir/$f/stdall"
      failed+=("$f")
    fi
  done

  if [[ "${#failed[@]}" != 0 ]]; then
    echo
    echo "FAILED TESTS:"
  fi

  for t in "${failed[@]}"; do
    echo "- $t"
  done

  echo "${#succeeded[@]} tests succeeded"
  test -n "${#succeeded[@]}" || return 1
  return "$rc"
}


self_test_succeed() {
  true
}
self_test_fail() {
  false
}
self_test_pipe_fail() {
  true | false | true
}
self_test_step_fail() {
  true
  false
  true
}

selfTest() {
  # bash is dumb; make sure that if a test would fail, the toplevel test runner would also fail
  if "$SELF" self_test_succeed \
    && ! "$SELF" self_test_fail \
    && ! "$SELF" self_test_pipe_fail \
    && ! "$SELF" self_test_step_fail \
    && ! "$SELF" self_test_succeed self_test_fail self_test_succeed \
  ; then
    echo "self-test: success"
  else
    echo "self-test: FAIL"
    exit 1
  fi
}

allTests=()
for f in $(declare -F); do
  if [[ "$f" =~ ^test_* ]]; then
    allTests+=("$f")
  fi
done

if [ $# = 1 ]; then
  # run the test inline
  ( set -x ; BUNPEN_DEBUG=4 "$@" )
elif [ $# = 0 ]; then
  selfTest
  runTests "${allTests[@]}"
else
  runTests "$@"
fi
