#!/bin/sh -x
#
# Wrapper around inotifywait to run tests on file changes.
# If a test file changes, only this file is run (first), while the whole test
# suite is run afterwards (and initially).
#
# The recommended way to run it is via `make testwatch`, or `make testwatchx`
# (to exit on first failure).
#
# # Internal documentation:
#   The default run command is "make %s" (where %s gets replaced with the test
#   file(s)), and can be given as first argument.
#
#   You can specify VADER_OPTIONS=-x to exit on the first test failure:
#       VADER_OPTIONS=-x contrib/run-tests-watch
#   or use the following:
#       contrib/run-tests-watch 'make %s VADER_OPTIONS=-x'
#
#   VADER_ARGS gets used to focus on a specific test file initially, until
#   another test file gets changed.

watch="autoload plugin tests"
echo "Watching: $watch"

if [ -n "$1" ]; then
  cmdpattern="$1"
else
  cmdpattern="make %s VADER_OPTIONS='$VADER_OPTIONS'"
fi

title() {
  printf '\e]1;%s\a' "$*"
  printf '\e]2;%s\a' "$*"
}

status_file=$(mktemp)
handle_cmd_result() {
  if [ "$1" = 0 ]; then
    last_test_succeeded=1
    title "✔ $2"
    if [ "${2#*${alltestsfile}*}" != "$2" ]; then
      alltests_succeeded=1
    fi
  else
    last_test_succeeded=0
    title "✘ $2"
    if [ "${2#*${alltestsfile}*}" != "$2" ]; then
      alltests_succeeded=0
    fi
  fi
  echo "$last_test_succeeded $alltests_succeeded" > "$status_file"
}

# Recursively kill childs - required to get out of a running pdb.
kill_with_childs() {
  for p in $(pgrep -P "$1"); do
    kill_with_childs "$p"
  done
  if kill -0 "$1" 2>/dev/null; then
    kill "$1" || true
  fi
}

alltestsfile='tests/all.vader'
alltests_succeeded=0
pid=
changed_files=
set -e
last_changed_testsfile="${VADER_ARGS:-$alltestsfile}"

while true; do
  testfiles="$last_changed_testsfile"
  if [ -n "$changed_files" ]; then
    # There was a previous run
    echo "================================================================="
    echo "changed file: $changed_files"

    read -r last_test_succeeded alltests_succeeded < "$status_file" || alltests_succeeded=0

    if [ "${changed_files#tests/}" != "$changed_files" ] \
        && [ "${changed_files#tests/include/}" = "$changed_files" ]; then
      # A test file was changed (but not an include file).
      last_changed_testsfile="$changed_files"
      testfiles="$last_changed_testsfile"

      # Run full tests afterwards (if not successful before).
      if [ "$testfiles" != "$alltestsfile" ] && [ "$alltests_succeeded" = 0 ]; then
        testfiles="$testfiles $alltestsfile"
      fi

    elif [ "$last_test_succeeded" = 1 ]; then
      # Run all tests.
      alltests_succeeded=0
      testfiles="$alltestsfile"

      if [ "${VADER_OPTIONS#*-x*}" != "$VADER_OPTIONS" ]; then
        # Run tests matching the changed file first, e.g. tests/config.vader for
        # autoload/neomake/config.vim.
        test_file_for_changed_file="tests/${changed_files#autoload/neomake/}"
        test_file_for_changed_file="${test_file_for_changed_file%.vim}.vader"
        declare -p test_file_for_changed_file
        if [ -f "$test_file_for_changed_file" ]; then
          testfiles="$test_file_for_changed_file $testfiles"
        fi
      fi
    fi

    if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
      echo 'Killing previous run…'
      kill_with_childs "$pid"
    fi
  fi

  # shellcheck disable=SC2059
  cmd="$(printf "$cmdpattern" "$testfiles")"
  echo "Running $cmd"
  title "… $testfiles"
  # shellcheck disable=SC2015
  (set +e; eval "$cmd"; handle_cmd_result "$?" "$testfiles") </dev/tty &
  pid=$!

  sleep 1
  # shellcheck disable=SC2086
  changed_files="$(inotifywait -q -r -e close_write \
    --exclude '/(__pycache__/|\.)|.*\@neomake_.*' \
    --format '%w%f' $watch)"
done
