#! /bin/bash
#
# autopkgtest driver for the DUNE Python bindings (python3-dune-*). It runs a
# Python script the way a user would:
#
#   python3 -m venv --system-site-packages ~/venv
#   . ~/venv/bin/activate
#   python3 SCRIPT
#
# The first run has to generate the dune-py module inside the venv and
# compile the bindings just in time; the second run has to load them from
# that cache without compiling anything.
#
# The script runs as an unprivileged user, so any attempt to write below
# /usr fails. If started as root, the driver re-runs itself as
# AUTOPKGTEST_NORMAL_USER or fails.
#
# usage: dune-python-autopkgtest [--compiles NAME]... [--modules N] SCRIPT
#   --compiles NAME  the first run must log "DUNE-INFO: Compiling NAME (new)"
#   --modules N      after the first run dune-py must contain exactly N
#                    JIT-compiled modules
# SCRIPT is copied to the working directory and run there.

set -e
set -u

fail() {
    echo "E: ${1:-Unknown error}"
    exit 1
}

[ -n "${AUTOPKGTEST_TMP:-}" ] || fail "must be run by autopkgtest (AUTOPKGTEST_TMP is not set)"

compiles=()
nmodules=
while [ $# -ge 1 ]; do
    case "$1" in
        --compiles) compiles+=("$2"); shift 2 ;;
        --modules) nmodules=$2; shift 2 ;;
        -*) fail "unknown option $1" ;;
        *) break ;;
    esac
done
[ $# -eq 1 ] || fail "usage: $0 [--compiles NAME]... [--modules N] SCRIPT"
script=$(realpath "$1")
[ -f "${script}" ] || fail "${script} not found"
[ ${#compiles[@]} -gt 0 ] || [ -n "${nmodules}" ] || fail "nothing to check: give --compiles or --modules"

if [ "$(id -u)" = 0 ]; then
    user=${AUTOPKGTEST_NORMAL_USER:-}
    if [ -z "${user}" ]; then
        fail "started as root, but AUTOPKGTEST_NORMAL_USER is not set"
    fi
    echo "I: running as user ${user}"
    args=()
    for c in "${compiles[@]}"; do args+=(--compiles "$c"); done
    [ -z "${nmodules}" ] || args+=(--modules "${nmodules}")
    # not exec, so that the trap removes the user again
    runuser -u "${user}" -- "$0" "${args[@]}" "${script}"
    exit 0
fi

[ "$(id -u)" != 0 ] || fail "must not run as root"
pkgdir=/usr/lib/python3/dist-packages/dune
[ -d "${pkgdir}" ] || fail "${pkgdir} is missing"
! [ -w "${pkgdir}" ] || fail "${pkgdir} is writable by $(id -un)"

workdir=$(mktemp -d)
cd "${workdir}"
export HOME="${workdir}/home"
mkdir "${HOME}"
unset DUNE_PY_DIR DUNE_CMAKE_FLAGS CMAKE_FLAGS CXXFLAGS DUNE_LOG_LEVEL || :
cp "${script}" smoke.py

echo "I: creating venv in ${workdir}/venv"
python3 -m venv --system-site-packages venv
. venv/bin/activate
dunepy="${VIRTUAL_ENV}/.cache/dune-py"
generated="${dunepy}/python/dune/generated"
site=$(python3 -c 'import sysconfig; print(sysconfig.get_path("purelib"))')
ls -A "${site}" > site-packages.before

echo "I: smoke test $(basename "${script}"):"
awk '{ print "  > " $0 }' smoke.py

run() {
    echo "I: $1: python3 smoke.py"
    failed=
    python3 smoke.py > "$1.log" 2>&1 || failed=1
    sed 's/^/  | /' "$1.log"
    if [ "${failed}" = 1 ]; then
        fail "$1 failed"
    fi
}

# First run: generates dune-py and compiles the modules
run run1
grep -qx "DUNE-INFO: Generating dune-py module in ${dunepy}" run1.log ||
    fail "run1: dune-py was not generated in ${dunepy}"
for c in "${compiles[@]}"; do
    grep -qxF "DUNE-INFO: Compiling ${c} (new)" run1.log ||
        fail "run1: ${c} was not compiled"
done
[ -f "${dunepy}/dune.module" ] || fail "run1: no dune-py module in ${dunepy}"
# the JIT modules; extractCompiler.so is built by dune-py itself when it is
# generated (to extract the compiler command)
modules=$(find "${generated}" -maxdepth 1 -name '*.so' ! -name extractCompiler.so | sort)
[ -n "${modules}" ] || fail "run1: no compiled module in ${generated}"
if [ -n "${nmodules}" ]; then
    [ "$(echo "${modules}" | wc -w)" = "${nmodules}" ] ||
        fail "run1: expected ${nmodules} compiled module(s), got:" ${modules}
fi
echo "I: compiled modules:" ${modules}
sha256sum "${generated}"/*.so > modules.before
stat -c '%n %Y' "${generated}"/*.so "${generated}"/CMakeFiles/*/*.o > mtimes.before

# Second run: must be served from the cache. Upstream logs builds at INFO
# level ("Compiling X (new|updated)", "X (rebuilding)", "Generating dune-py
# ..."), loading a cached module logs nothing at that level. As builds are
# only logged if make runs longer than 2 s, also compare the files.
run run2
! grep -q '^DUNE-INFO: Generating dune-py' run2.log || fail "run2: dune-py was generated again"
! grep -q '^DUNE-INFO: Compiling' run2.log || fail "run2: something was compiled again"
! grep -q '^DUNE-INFO: .*(rebuilding)' run2.log || fail "run2: something was rebuilt"
! grep -q '^DUNE-INFO' run2.log || fail "run2: unexpected DUNE-INFO output"
sha256sum "${generated}"/*.so > modules.after
stat -c '%n %Y' "${generated}"/*.so "${generated}"/CMakeFiles/*/*.o > mtimes.after
cmp modules.before modules.after || fail "run2: compiled modules changed"
cmp mtimes.before mtimes.after || fail "run2: compiled modules or objects were rebuilt"

# Nothing may have been installed into the venv (e.g. by pip when dune-py
# is configured).
ls -A "${site}" > site-packages.after
diff -u site-packages.before site-packages.after || fail "packages were installed into the venv"

echo "I: OK: dune-py generated and modules compiled on first use, loaded from the cache on second use"
