#!/bin/bash
#
#   Smoke — Run smoke tests via muxscript.
#
#   Imports smoke.flat into SQLite via dbconvert, then runs muxscript
#   which fires @startup and drains the test chain.  No networking,
#   no server process, gdb-friendly.
#
#   On success: prints summary, exits 0.
#   On failure/hang: collects artifacts in smoke.fail/, exits 1.
#
#   Usage: cd testcases && ./tools/Smoke [--workspace DIR] [--flatfile FILE]
#
set -euo pipefail

SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
TC=$(cd "$SCRIPT_DIR/.." && pwd)
WORKSPACE="$TC"
FLATFILE=""

GAMENAME=smoke
BIN="$TC/../mux/game/bin"
DATA=./$GAMENAME.d
LOGFILE=logs/M-smoke.log

ACTIVITY_TIMEOUT=30    # seconds of no log growth = idle-hang
WALLCLOCK_TIMEOUT=300  # total seconds = busy-hang
CHECK_INTERVAL=2       # seconds between checks

# ---------------------------------------------------------------------------
# Parse arguments.
# ---------------------------------------------------------------------------

while [ $# -gt 0 ]; do
    case "$1" in
        --workspace) WORKSPACE="$2"; shift ;;
        --flatfile)  FLATFILE="$2"; shift ;;
        --activity-timeout)  ACTIVITY_TIMEOUT="$2"; shift ;;
        --wallclock-timeout) WALLCLOCK_TIMEOUT="$2"; shift ;;
        *)  echo "Unknown option: $1"; exit 1 ;;
    esac
    shift
done

mkdir -p "$WORKSPACE"
WORKSPACE=$(cd "$WORKSPACE" && pwd)
cd "$WORKSPACE"

if [ -n "$FLATFILE" ]; then
    case "$FLATFILE" in
        /*) ;;
        *) FLATFILE="$WORKSPACE/$FLATFILE" ;;
    esac
else
    FLATFILE="$WORKSPACE/smoke.flat"
fi

# ---------------------------------------------------------------------------
# Portable file-size helper.
# ---------------------------------------------------------------------------

if stat --version >/dev/null 2>&1; then
    filesize() { stat -c%s "$1" 2>/dev/null || echo 0; }
else
    filesize() { stat -f%z "$1" 2>/dev/null || echo 0; }
fi

# ---------------------------------------------------------------------------
# Preflight.
# ---------------------------------------------------------------------------

if [ ! -r "$FLATFILE" ]; then
    echo "ERROR: smoke.flat not found. Run Makesmoke first."
    exit 1
fi

if [ ! -x "$BIN/muxscript" ]; then
    echo "ERROR: $BIN/muxscript not found. Run Build.sh first."
    exit 1
fi

if [ ! -x "$BIN/dbconvert" ]; then
    echo "ERROR: $BIN/dbconvert not found. Run Build.sh first."
    exit 1
fi

# ---------------------------------------------------------------------------
# Clean ALL prior state unconditionally.
# ---------------------------------------------------------------------------

# smoke.fail is deliberately NOT removed here (#1446's lesson, second
# instance).  It is the only surviving record of the previous failure, and
# the failure-collection path below already clears it before writing a new
# one -- so a new failure still replaces the old, but a *successful* run no
# longer destroys evidence on its way past.
#
# This matters most for exactly the bugs it kept eating: #1433 is an
# intermittent SEGV at roughly 0.4% per process and #1340 is a crash that
# stopped reproducing, and for both of those a backtrace and the tail of
# smoke.log are the whole game.  Observed live -- a route-2 SEGV was
# collected into smoke.fail/ and then deleted by the next (clean) run
# before it could be read.
#
rm -rf "$DATA" logs text
rm -f smoke.conf alias.conf compat.conf shutdown.status modprobe.in modprobe.log modprobe.conf
rm -rf modprobe.d
rm -f smoke.log netmux.log
rm -rf bin

# ---------------------------------------------------------------------------
# Create fresh environment.
# ---------------------------------------------------------------------------

mkdir "$DATA"
mkdir -p logs text

rm -rf bin && ln -s "$BIN" bin
cp "$TC/../mux/game/alias.conf" .
cp "$TC/../mux/game/compat.conf" .
touch "$LOGFILE"

cat > text/smokehelp.txt <<'_HELPEOF'
& help
This is the smoke test help system.
& test topic
This is a test topic for mhelp().
& another topic
Another test topic.
_HELPEOF

cat > smoke.conf <<'_EOF'
# smoke.conf - TinyMUX configuration file for smoke testing.
input_database	smoke.d/smoke.db
output_database	smoke.d/smoke.db.new
crash_database	smoke.d/smoke.db.CRASH
mail_database   smoke.d/mail.db
comsys_database smoke.d/comsys.db
port 2860
mud_name SmokeMUX
command_quota_increment 10000
command_quota_max 10000
player_queue_limit 100000
include alias.conf
include compat.conf
raw_helpfile help text/smokehelp
module exp3
module comsys_mod
module mail_mod
_EOF

# Optionally drop module directives, so the suite can be run against the
# engine's BUILT-IN comsys/mail instead of the modules (#1589 stage 0).
#
# Both implementations exist and they demonstrably differ -- #1564, #1585 and
# #1620 are all bugs that only appear when the two disagree, and none of them
# is visible to a run that exercises one side.  Until now there was no way to
# ask for the other side at all: the module directives are written into the
# heredoc above and SMOKE_EXTRA_CONF only appends, so every run tested
# whichever implementation the platform happened to resolve.
#
# Filtering here rather than making the heredoc conditional keeps the two
# checks downstream honest: the module preflight and the implementation guard
# both read `module` lines back out of smoke.conf, so removing the line makes
# them correctly expect -- and require -- the built-in.
#
#   SMOKE_OMIT_MODULES="comsys_mod mail_mod" ./tools/Smoke
#
if [ -n "${SMOKE_OMIT_MODULES:-}" ]; then
    for _mod in $SMOKE_OMIT_MODULES; do
        grep -v "^module[[:space:]]\{1,\}${_mod}[[:space:]]*$" smoke.conf \
            > smoke.conf.tmp && mv smoke.conf.tmp smoke.conf
    done
    echo "Running WITHOUT modules:$SMOKE_OMIT_MODULES"
fi

# Optional extra conf directives (e.g. running the whole suite with
# the Phase 4 JIT bracket toggle on:
#   SMOKE_EXTRA_CONF="jit_eval_brackets 1" ./tools/Smoke ).
if [ -n "${SMOKE_EXTRA_CONF:-}" ]; then
    printf '%s\n' "$SMOKE_EXTRA_CONF" >> smoke.conf
fi

# ---------------------------------------------------------------------------
# Import smoke.flat into SQLite via dbconvert.
# ---------------------------------------------------------------------------

echo "Importing smoke.flat into SQLite..."

LD_LIBRARY_PATH=$BIN
export LD_LIBRARY_PATH

DBCONVERT_CMD=("$BIN/dbconvert" -d "$DATA/$GAMENAME" -i "$FLATFILE" -l)
if "${DBCONVERT_CMD[@]}" > netmux.log 2>&1; then
    :
else
    dbconvert_rc=$?
    echo "ERROR: dbconvert import failed."
    echo "Command: ${DBCONVERT_CMD[*]}"
    echo "Exit code: $dbconvert_rc"
    cat netmux.log
    exit 1
fi

# ---------------------------------------------------------------------------
# Preflight: every module smoke.conf asks for must actually be loaded.
#
# A module that fails to load is completely silent (#1572).  The engine
# carries a built-in comsys, so when comsys_mod does not load the built-in
# services the channel commands, the run proceeds normally, and nothing in
# netmux.log says which implementation answered.  Measured: the 31 comsys
# cases pass identically with and without the module, so the corpus cannot
# tell them apart -- and the two implementations demonstrably differ (#1572
# lists five divergences).
#
# That is exactly how #1564/#1569 reached master: the module silently did
# not load on Windows, the built-in answered, and the suite was green.
#
# `@list modules` reports each as "(loaded)" or "(unloaded)".  Ask it in a
# throwaway read-only run against the same config, before trusting anything
# the suite reports.
# ---------------------------------------------------------------------------

WANT_MODULES=$(sed -n 's/^module[[:space:]]\{1,\}\([A-Za-z0-9_]\{1,\}\).*/\1/p' smoke.conf)
if [ -n "$WANT_MODULES" ]; then
    echo "Preflight: checking modules load..."
    # Its own empty database, so @startup does not fire and the suite is not
    # run twice.  Only the module lines are carried over from smoke.conf.
    rm -rf modprobe.d; mkdir -p modprobe.d
    {
        echo "input_database  modprobe.d/p.db"
        echo "output_database modprobe.d/p.db.new"
        echo "crash_database  modprobe.d/p.db.CRASH"
        echo "mail_database   modprobe.d/mail.db"
        echo "comsys_database modprobe.d/comsys.db"
        echo "port 2861"
        echo "mud_name SmokeModProbe"
        echo "include alias.conf"
        echo "include compat.conf"
        grep -E '^module[[:space:]]' smoke.conf
    } > modprobe.conf
    printf '@list modules\n@shutdown\n' > modprobe.in
    "$BIN/muxscript" -g . -c modprobe.conf --readonly < modprobe.in > modprobe.log 2>&1 || true
    modfail=0
    for m in $WANT_MODULES; do
        if ! grep -qE "^$m \(loaded\)" modprobe.log; then
            echo "ERROR: smoke.conf asks for module '$m', but it is not loaded."
            modfail=1
        fi
    done
    if [ "$modfail" -ne 0 ]; then
        echo
        echo "The suite would still pass: the engine's built-in implementations"
        echo "answer when a module is absent, so the tests exercise the wrong"
        echo "side silently.  See #1572.  Reported modules:"
        sed -n '/^Modules:/,/^$/p' modprobe.log | sed 's/^/  /'
        echo

        # Unconditional again as of #1594.  It was briefly advisory on Windows,
        # where no module could load under muxscript at all: comsys/mail built
        # as comsys.dll/mail.dll instead of the *_mod names smoke.conf needs,
        # and muxscript's SetDllDirectory() call removed the current directory
        # from the DLL search order, so cf_module's relative ".\bin\<name>.dll"
        # resolved nowhere.  Both are fixed; every platform can comply, so a
        # module that does not load is a failure everywhere.
        #
        echo "=== Smoke: FAILED (module preflight) ==="
        exit 1
    fi
    rm -rf modprobe.in modprobe.log modprobe.conf modprobe.d
fi

# ---------------------------------------------------------------------------
# Run muxscript.  @startup fires the test chain; after the chain
# completes, @shutdown exits muxscript.
# ---------------------------------------------------------------------------

echo "Starting smoke tests..."

"$BIN/muxscript" -g . -c smoke.conf --readonly < /dev/null >> netmux.log 2>&1 &
MUX_PID=$!

# ---------------------------------------------------------------------------
# Monitor with dual timeouts.
# ---------------------------------------------------------------------------

WALL_START=$SECONDS
LAST_SIZE=0
LAST_ACTIVITY=$SECONDS
HANG_TYPE=""

while kill -0 "$MUX_PID" 2>/dev/null; do
    sleep "$CHECK_INTERVAL"

    ELAPSED=$((SECONDS - WALL_START))
    CURRENT_SIZE=$(filesize "$LOGFILE")

    # Check for log growth.
    if [ "$CURRENT_SIZE" -gt "$LAST_SIZE" ]; then
        LAST_SIZE=$CURRENT_SIZE
        LAST_ACTIVITY=$SECONDS
    fi

    IDLE=$((SECONDS - LAST_ACTIVITY))

    # Wall-clock timeout.
    if [ "$ELAPSED" -ge "$WALLCLOCK_TIMEOUT" ]; then
        HANG_TYPE="busy-hang (wall-clock ${ELAPSED}s >= ${WALLCLOCK_TIMEOUT}s)"
        echo "TIMEOUT: $HANG_TYPE"
        kill "$MUX_PID" 2>/dev/null || true
        sleep 2
        kill -9 "$MUX_PID" 2>/dev/null || true
        break
    fi

    # Activity timeout.
    if [ "$IDLE" -ge "$ACTIVITY_TIMEOUT" ]; then
        HANG_TYPE="idle-hang (no log activity for ${IDLE}s >= ${ACTIVITY_TIMEOUT}s)"
        echo "TIMEOUT: $HANG_TYPE"
        kill "$MUX_PID" 2>/dev/null || true
        sleep 2
        kill -9 "$MUX_PID" 2>/dev/null || true
        break
    fi
done

# Wait for process to fully exit, capturing its status.  A status above
# 128 means death by signal (128+N); when we killed it ourselves on a
# timeout, HANG_TYPE is set and the signal is expected — otherwise the
# process crashed and must be reported as a crash, not a stalled chain
# (#859: a SIGBUS here used to read as "Dispatched: N/M, Crashes: 0").
MUX_RC=0
wait "$MUX_PID" 2>/dev/null || MUX_RC=$?
MUX_CRASH=""
if [ -z "$HANG_TYPE" ] && [ "$MUX_RC" -gt 128 ]; then
    MUX_SIG=$((MUX_RC - 128))
    MUX_SIGNAME=$(kill -l "$MUX_SIG" 2>/dev/null || echo "$MUX_SIG")
    MUX_CRASH="muxscript died: signal $MUX_SIGNAME (exit status $MUX_RC)"
    echo "CRASH: $MUX_CRASH"
fi
ELAPSED=$((SECONDS - WALL_START))
echo "muxscript ran for ${ELAPSED}s."

# ---------------------------------------------------------------------------
# Collect results.
# ---------------------------------------------------------------------------

if [ -r "$LOGFILE" ] && [ -s "$LOGFILE" ]; then
    mv "$LOGFILE" smoke.log
else
    touch smoke.log
fi

# ---------------------------------------------------------------------------
# Print summary.
# ---------------------------------------------------------------------------

# ---------------------------------------------------------------------------
# Pin which comsys/mail implementation actually answered (#1581).
#
# The module preflight above proves each configured module LOADED.  It does
# not prove the module is the one servicing channels and mail:
# discover_comsys_mail_modules() calls mux_CreateInstance and falls back to
# the engine's built-in when that fails, so "loaded" and "live" are separate
# facts.  #1593 made the engine say which it chose -- but on stderr, which
# lands in netmux.log and is deleted on a green run, so the answer existed
# and was thrown away.
#
# Both halves matter.  Report it into smoke.log so anyone reading the
# artifact can see which code the numbers describe, and fail when the
# configuration asked for a module and the built-in answered anyway --
# that silent fallback is how #1564 stayed hidden through a whole
# investigation.
# ---------------------------------------------------------------------------

IMPL_LINES=$(grep -h 'using .* implementation' netmux.log 2>/dev/null | sort -u)
if [ -n "$IMPL_LINES" ]; then
    echo ""
    echo "=== Implementations in this run ==="
    printf '%s\n' "$IMPL_LINES"
    printf '%s\n' "$IMPL_LINES" >> smoke.log
elif [ -n "$WANT_MODULES" ]; then
    # #1593 should always emit the lines.  Missing them is a harness gap,
    # not proof the suite is clean -- but do not fail on log plumbing alone.
    echo ""
    echo "WARNING: no 'using * implementation' lines in netmux.log"
    echo "         (cannot pin comsys/mail; see #1593 / #1581)."
    echo
fi

IMPL_BAD=""
case "$WANT_MODULES" in
    *comsys_mod*)
        case "$IMPL_LINES" in
            *"Comsys: using built-in"*)
                IMPL_BAD="$IMPL_BAD comsys" ;;
        esac ;;
esac
case "$WANT_MODULES" in
    *mail_mod*)
        case "$IMPL_LINES" in
            *"Mail: using built-in"*)
                IMPL_BAD="$IMPL_BAD mail" ;;
        esac ;;
esac

if [ -n "$IMPL_BAD" ]; then
    echo ""
    # Unconditional, like the load preflight above (#1599 / #1594).
    #
    echo "=== Smoke: FAILED (implementation fallback) ==="
    echo "smoke.conf asks for the module, but the built-in engine answered"
    echo "for:$IMPL_BAD"
    echo ""
    echo "Either the module never loaded (the preflight should have caught"
    echo "that) or discover_comsys_mail_modules() fell back after load.  The"
    echo "results describe the built-in, not the module under test."
    exit 1
fi

echo ""
echo "=== Smoke Test Results ==="
SUCCEEDED=$(grep -c 'Succeeded' smoke.log || true)
# Case-insensitive: a TC whose failure message says "failed" must not
# slip through the classifier (nested_depth TC001 did exactly that).
FAILED=$(grep -ci 'failed' smoke.log || true)
# Skips are a first-class outcome, not a rounding error.  Eleven cases (twelve
# with jitstats TC007) report Skipped instead of a verdict on builds without
# the JIT or Lua, and until now that count was computed solely to feed the
# verdict-loss gate below and never shown.  A build that quietly lost
# --enable-jit therefore printed ALL N TESTS PASSED with a dozen guards inert
# -- the same "SKIP is indistinguishable from PASS" defect #1923 fixed one
# level up, at the make-target level, and missed inside smoke.
SKIPPED=$(grep -c 'Skipped' smoke.log || true)
CRASH_SMOKE=$(grep -cE 'SIGABRT|SIGSEGV|SIGBUS' smoke.log || true)
CRASH_NETMUX=$(grep -cE 'SIGABRT|SIGSEGV|SIGBUS' netmux.log 2>/dev/null || true)
: "${SUCCEEDED:=0}" "${FAILED:=0}" "${SKIPPED:=0}"
: "${CRASH_SMOKE:=0}" "${CRASH_NETMUX:=0}"
CRASHES=$((CRASH_SMOKE + CRASH_NETMUX))
if [ -n "$MUX_CRASH" ]; then
    CRASHES=$((CRASHES + 1))
fi

echo "  Succeeded: $SUCCEEDED"
echo "  Failed:    $FAILED"
echo "  Skipped:   $SKIPPED"
echo "  Crashes:   $CRASHES"
if [ -n "$MUX_CRASH" ]; then
    echo "  Crash:     $MUX_CRASH"
fi
if [ -n "$HANG_TYPE" ]; then
    echo "  Hang:      $HANG_TYPE"
fi
echo "  Log lines: $(wc -l < smoke.log)"

# ---------------------------------------------------------------------------
# Completeness: every test the harness intends to run must dispatch from the
# semaphore chain, and the chain must reach its final link (Ending SmokeMUX).
# Without this check a stalled chain or an early muxscript exit silently drops
# the tail of the suite and still reports "all passed."
# ---------------------------------------------------------------------------

# SUITE-EXPECTED is what the *database* thinks the suite is: the runtime
# computes it with words() over the same attributes it is meant to check.
# When the upload loses names, that number shrinks with them and the run
# reports a clean "N / N" -- which is how #1387 hid 46 test files.
#
# suite.manifest is the generator's own record, written to the workspace
# instead of through the upload, so it cannot shrink the same way.  A
# disagreement between the two means the upload did not store what was
# generated, and that is now a failure rather than a silence.
EXPECTED=$(grep -oE 'SUITE-EXPECTED: [0-9]+' smoke.log | grep -oE '[0-9]+' | head -1)
DISPATCHED=$(grep -c 'SUITE-DISPATCH:' smoke.log || true)
ENDED=$(grep -c 'Ending SmokeMUX' smoke.log || true)
GENERATED=0
EXPECTED_VERDICTS=0
if [ -r suite.manifest ]; then
    GENERATED=$(grep -c '^[^#]' suite.manifest || true)
    EXPECTED_VERDICTS=$(sed -n 's/^#expected-verdicts //p' suite.manifest | head -1)
fi
: "${EXPECTED:=0}" "${DISPATCHED:=0}" "${ENDED:=0}" "${GENERATED:=0}"
: "${EXPECTED_VERDICTS:=0}"

# Verdict accounting (#1396).  SUCCEEDED and FAILED are grep counts over the
# log, so a case that dispatches and then logs neither is counted in neither
# and the run still reports ALL TESTS PASSED -- reintroducing #1386's defect
# lost 149 assertions exactly that way, silently.  INCOMPLETE does not cover
# it either: that guards dispatch, which is per-FILE, and a file can dispatch
# and still lose individual cases.
#
# The expected count comes from the generator, out of band, for the same
# reason the suite manifest does: a number derived from the run cannot detect
# the run losing something.
#
# Skipped counts as conclusive.  Eleven cases report it instead of a verdict
# on builds without the JIT or Lua, and those builds must not read as a loss.
# The test is a deficit only -- reporting more than expected is not a fault.
# (SKIPPED itself is counted up with the other outcomes, and printed.)
VERDICTS=$((SUCCEEDED + FAILED + SKIPPED))

if [ "$GENERATED" -gt 0 ]; then
    echo "  Dispatched: $DISPATCHED / $GENERATED generated (database says $EXPECTED)"
else
    echo "  Dispatched: $DISPATCHED / $EXPECTED expected"
fi

INCOMPLETE=false
if [ "$EXPECTED" -gt 0 ] && [ "$DISPATCHED" -lt "$EXPECTED" ]; then INCOMPLETE=true; fi
if [ "$ENDED" -eq 0 ]; then INCOMPLETE=true; fi
LOST=false
if [ "$GENERATED" -gt 0 ] && [ "$EXPECTED" -ne "$GENERATED" ]; then
    LOST=true; INCOMPLETE=true
fi
if [ "$GENERATED" -gt 0 ] && [ "$DISPATCHED" -lt "$GENERATED" ]; then INCOMPLETE=true; fi

VERDICT_LOSS=false
if [ "$EXPECTED_VERDICTS" -gt 0 ] && [ "$VERDICTS" -lt "$EXPECTED_VERDICTS" ]; then
    VERDICT_LOSS=true; INCOMPLETE=true
fi

if $VERDICT_LOSS; then
    echo "  VERDICT LOSS: the sources contain $EXPECTED_VERDICTS verdict-reporting"
    echo "                cases but the run logged $VERDICTS.  Cases dispatched"
    echo "                and then reported nothing, so they were counted"
    echo "                neither as passed nor as failed (#1396)."
fi

if $LOST; then
    echo "  SUITE LOSS: the generator emitted $GENERATED test names but the"
    echo "              database holds $EXPECTED.  The upload did not store"
    echo "              what was generated, so the missing tests never ran"
    echo "              and the runtime count agreed with the loss (#1387)."
fi

if $INCOMPLETE; then
    echo "  INCOMPLETE: the suite did not run every test (chain stalled or"
    echo "              muxscript exited before the queue drained)."
    # Prefer the generator's manifest: smoke.mux's hardcoded lists are stale
    # by construction now that the generator is authoritative, and reading
    # them here is part of why #1387 was hard to see from the failure output
    # -- the names it called missing were not the ones that had gone.
    if [ -r suite.manifest ]; then
        # Skip the leading #expected-verdicts line: it is metadata, not a
        # test name, and listing it as "never dispatched" sent a reader
        # looking for a test called "#expected-verdicts 1508".
        grep '^[^#]' suite.manifest | sort -u > "smoke.expected.$$"
    elif [ -r "$TC/smoke.mux" ]; then
        awk '/&suite.list.[12] smoke=/{grab=1;next} /^-/{grab=0} grab{gsub(/\\/,"");print}' \
            "$TC/smoke.mux" | tr ' ' '\n' | grep -v '^$' | sort -u > "smoke.expected.$$"
    fi
    if [ -r "smoke.expected.$$" ]; then
        grep -oE 'SUITE-DISPATCH: [^ ]+' smoke.log | awk '{print $2}' | sort -u > "smoke.dispatched.$$"
        MISSING=$(comm -23 "smoke.expected.$$" "smoke.dispatched.$$" | tr '\n' ' ')
        rm -f "smoke.expected.$$" "smoke.dispatched.$$"
        [ -n "$MISSING" ] && echo "  Never dispatched: $MISSING"
    fi
fi
echo ""

# Show failures if any.
if [ "$FAILED" -gt 0 ]; then
    echo "--- Failed tests ---"
    grep -i 'failed' smoke.log
    echo ""
fi

# ---------------------------------------------------------------------------
# Determine overall status.
# ---------------------------------------------------------------------------

ALL_PASS=true
if [ "$FAILED" -gt 0 ]; then ALL_PASS=false; fi
if [ "$CRASHES" -gt 0 ]; then ALL_PASS=false; fi
if [ -n "$HANG_TYPE" ]; then ALL_PASS=false; fi
if [ "$SUCCEEDED" -eq 0 ]; then ALL_PASS=false; fi
if $INCOMPLETE; then ALL_PASS=false; fi

# ---------------------------------------------------------------------------
# Failure artifact collection.
# ---------------------------------------------------------------------------

if ! $ALL_PASS; then
    echo "Collecting failure artifacts into smoke.fail/..."
    rm -rf smoke.fail
    mkdir -p smoke.fail
    cp smoke.log smoke.fail/ 2>/dev/null || true
    cp netmux.log smoke.fail/ 2>/dev/null || true
    cp "$DATA"/*.log smoke.fail/ 2>/dev/null || true
    cp "$DATA"/*.sqlite smoke.fail/ 2>/dev/null || true
fi

# ---------------------------------------------------------------------------
# Clean up runtime state.
# ---------------------------------------------------------------------------

rm -f smoke.conf alias.conf compat.conf shutdown.status modprobe.in modprobe.log modprobe.conf
rm -rf modprobe.d
rm -rf "$DATA" logs text
rm -f netmux.log
rm -rf bin

# ---------------------------------------------------------------------------
# Exit status.
# ---------------------------------------------------------------------------

if $ALL_PASS; then
    if [ "$SKIPPED" -gt 0 ]; then
        echo "=== Smoke: ALL $SUCCEEDED TESTS PASSED ($SKIPPED SKIPPED) ==="
        echo "    $SKIPPED cases reported no verdict -- typically a build"
        echo "    without --enable-jit or Lua.  Those guards did not run."
    else
        echo "=== Smoke: ALL $SUCCEEDED TESTS PASSED ==="
    fi
    exit 0
else
    echo "=== Smoke: FAILED ==="
    if [ -d smoke.fail ]; then
        echo "    Artifacts saved in testcases/smoke.fail/"
    fi
    exit 1
fi
