#!/bin/bash
#
# run-tests - run all the test-* scripts in this directory
#
# Usage: ./dots/tests/run-tests [NAME...]
#
# With no arguments, runs every test-* script found here. With arguments, runs just those,
# named either bare ("bashy-paths") or in full ("test-bashy-paths").
#
# Runs every script even if an earlier one fails -- a single failure shouldn't hide the
# state of everything else. Exits non-zero if any script failed, so it is usable from CI.

set -o errexit
set -o nounset
set -o pipefail

PROGRAM_NAME=$(basename "$0")
PROGRAM_DIR=$(dirname "$0")
readonly PROGRAM_NAME PROGRAM_DIR

TESTS_DIR=
N_PASSED=0
N_FAILED=0
FAILED_NAMES=()
SELECTED_TESTS=()

# Its own message helpers on purpose: the runner takes no dependency on JXL, so that it
# still works when JXL is what is broken. A test harness that cannot run until the code
# under test is healthy is not much of a harness.
#
# shellcheck disable=SC2329  # info is part of that set, whether or not this file uses it
function info()  { emit "$*"; }
function error() { emit "ERROR: $*"; }
function die()   { error "$*"; exit 1; }
function emit()  { echo >&2 "$PROGRAM_NAME: $*"; }


function main() {
  local test_script rc
  cd "$PROGRAM_DIR"
  TESTS_DIR="$PWD"
  select_test_scripts "$@"
  # The +"..." guard is for bash 3.2, where expanding an empty array under `set -u` errors.
  for test_script in ${SELECTED_TESTS[@]+"${SELECTED_TESTS[@]}"}; do
    heading "$test_script"
    rc=0
    if [[ -x "$TESTS_DIR/$test_script" ]]; then
      "$TESTS_DIR/$test_script" || rc=$?
    else
      # Only reachable from the glob, which takes whatever matches; a named test that
      # isn't executable already died in select_test_scripts. Count it and keep going, so
      # one broken file doesn't hide the state of everything else.
      error "not executable, skipping: $test_script ($TESTS_DIR/$test_script)"
      rc=1
    fi
    if [[ $rc == 0 ]]; then
      N_PASSED=$((N_PASSED + 1))
    else
      N_FAILED=$((N_FAILED + 1)); FAILED_NAMES+=("$test_script")
    fi
    echo ''
  done
  print_summary
  return $(( N_FAILED > 0 ? 1 : 0 ))
}

function select_test_scripts() {
  # Populates $SELECTED_TESTS with the test script names to run, in glob (sorted) order.
  #
  # Sets a global instead of echoing, because a command substitution would put this in a
  # subshell, where die()'s exit would kill only the subshell and let the run continue
  # with an empty list -- reporting success on a bogus test name.
  local arg name note
  SELECTED_TESTS=()

  if [[ $# -eq 0 ]]; then
    # all tests
    for name in test-*; do
      # An unmatched glob expands to the pattern itself, so check before taking it.
      if [[ -e "$name" ]]; then SELECTED_TESTS+=("$name"); fi
    done
    if [[ ${#SELECTED_TESTS[@]} -eq 0 ]]; then
      die "no test scripts found in $TESTS_DIR"
    fi
  else
    # named requested tests
    for arg in "$@"; do
      case "$arg" in
        test-*) name="$arg" ;;
        *)      name="test-$arg" ;;
      esac
      if [[ ! -x "$TESTS_DIR/$name" ]]; then
        note=''
        if [[ -e "$TESTS_DIR/$name" ]]; then note=' (file exists but is not executable)'; fi
        die "no such test script: $name${note} ($TESTS_DIR/$name)"
      fi
      SELECTED_TESTS+=("$name")
    done
  fi
}

function print_summary() {
  local n_total=$((N_PASSED + N_FAILED))
  if [[ $N_FAILED -eq 0 ]]; then
    heading "all $n_total test scripts passed"
  else
    heading "$N_FAILED of $n_total test scripts FAILED: ${FAILED_NAMES[*]}"
  fi
}

function heading() {
  local msg="$*"
  local bar="##########"
  echo "${bar} ${msg} ${bar}"
}


# ========== Main script ==========

main "$@"
exit $?
