Files

382 lines
15 KiB
Bash
Executable File

#!/bin/sh
# tests/integration/run.sh - end-to-end integration test (plan todo 26).
#
# Drives a REAL fixture C project (tests/integration/fixture/) through the
# full stupidtools pipeline, once per shell in the portability matrix
# (dash/bash/zsh; sh stands in for dash when dash is not installed):
#
# 1. the wired binary ($TOP_DIR/src/stupidtools) generates ./configure
# from project.kdl into an isolated temp dir (one dir per shell);
# 2. that ./configure runs UNDER THE SHELL (any non-POSIX emission dies
# as a syntax error right here), writing Makefile/config.h/config.log/
# config.status;
# 3. make builds the fixture binary; assertions verify the build, the
# substituted Makefile (CC/CFLAGS/LIBS/prefix), config.h defines,
# config.log probe results and config.status --config.
#
# A second fixture (project-fail.kdl) checks a deliberately MISSING header:
# the generated configure must handle the failed probe CLEANLY under every
# shell - the failure is recorded (have_nope=no), the check is named in
# config.log (the compiler's diagnostic), config.h gets NO HAVE_NOPE
# define, and no shell may report a syntax error.
#
# NOTE (deviation, tracked in .omo/notepads/stupidtools/issues.md): the v1
# generator (todos 16/17, frozen to todo 26) follows autoconf semantics -
# a failed OPTIONAL check records have_<name>=no and configure still exits
# 0; it does not abort. The plan/todo text expected rc!=0 for a missing
# header. This runner asserts the generator's REAL contract (rc=0 + the
# failure recorded), and additionally exercises the genuinely-non-zero
# configure-error path (an unrecognized option exits 1 with a readable
# message and no shell syntax error) so BOTH halves of "fails cleanly
# (non-zero, readable message)" are covered by a real assertion.
#
# POSIX sh only (dash/bash/zsh safe): no [[ ]], arrays, local, ==, <<<, &>.
# Exit 0 = every shell's happy case AND failure case passed.
TOP_DIR=$(cd "$(dirname "$0")/../.." && pwd) || {
echo "FAIL: cannot resolve project root" >&2
exit 1
}
BIN="$TOP_DIR/src/stupidtools"
FIX="$TOP_DIR/tests/integration/fixture"
TESTS_RUN=0
TESTS_FAILED=0
# temp dirs created by new_work() are recorded in a file list; the trap
# cleans them on every exit path (a file list + $(cat ...) splits under
# every POSIX shell AND native zsh, unlike an unquoted variable).
WORK_LIST="${TMPDIR:-/tmp}/stupidtools-int-worklist.$$"
: > "$WORK_LIST"
cleanup_dirs() {
[ -s "$WORK_LIST" ] && rm -rf $(cat "$WORK_LIST")
rm -f "$WORK_LIST"
}
trap cleanup_dirs EXIT HUP INT TERM
pass() {
TESTS_RUN=$((TESTS_RUN + 1))
printf 'ok %d - %s\n' "$TESTS_RUN" "$1"
}
fail() {
TESTS_RUN=$((TESTS_RUN + 1))
TESTS_FAILED=$((TESTS_FAILED + 1))
printf 'not ok %d - %s\n' "$TESTS_RUN" "$1" >&2
}
# new_work: mktemp an isolated dir and record it for trap cleanup.
new_work() {
WORK=$(mktemp -d "${TMPDIR:-/tmp}/stupidtools-int.XXXXXX") || {
fail "cannot create temp dir"
return 1
}
printf '%s\n' "$WORK" >> "$WORK_LIST"
return 0
}
# --- preconditions (fail honestly, never silently skip) ------------------
if [ -x "$BIN" ]; then
pass "binary exists and is executable: $BIN"
else
fail "binary missing: $BIN (run 'make' first)"
fi
for f in project.kdl project-fail.kdl Makefile.in main.c demo.c demo.h; do
if [ -f "$FIX/$f" ]; then
pass "fixture file present: $f"
else
fail "fixture file missing: $FIX/$f"
fi
done
# --- the shell matrix -----------------------------------------------------
# Built as positional parameters and iterated with "$@" - the one portable
# multi-word construct that behaves identically under POSIX sh AND native
# zsh (an unquoted variable is NOT word-split in native zsh). dash when
# available (it now is on this host), else sh as stand-in; the runner
# prints which one it used - never a silent substitution.
set --
if command -v dash >/dev/null 2>&1; then
set -- "$@" dash
DASH_USED=yes
else
set -- "$@" sh
DASH_USED=no
printf '# note: dash: unavailable - matrix uses sh instead of dash\n'
fi
for cand in bash zsh; do
if command -v "$cand" >/dev/null 2>&1; then
set -- "$@" "$cand"
else
fail "required shell not installed: $cand"
fi
done
SHELLS="$*"
printf '# shell matrix: %s (dash used: %s)\n' "$SHELLS" "$DASH_USED"
printf '# note: failed-check semantics per the frozen v1 generator: a failed\n'
printf '# optional check records have_<name>=no and configure exits 0;\n'
printf '# the non-zero configure-error path is asserted via an unknown\n'
printf '# option instead (see header + issues.md).\n'
# --- happy case: one shell, one isolated dir ------------------------------
run_happy() {
shell=$1
new_work || return 1
if cp "$FIX/project.kdl" "$FIX/Makefile.in" "$FIX/main.c" "$FIX/demo.c" \
"$FIX/demo.h" "$WORK/" \
&& (cd "$WORK" && XDG_DATA_HOME="$WORK/xdg" STUPIDTOOLS_EXT= \
"$BIN" project.kdl >gen.log 2>&1) \
&& [ -s "$WORK/configure" ]; then
pass "$shell: stupidtools generated ./configure from project.kdl"
else
fail "$shell: configure generation failed: $(cat "$WORK/gen.log" 2>/dev/null)"
return 1
fi
# real dash is the strict oracle when it is in the matrix
if [ "$shell" = "dash" ]; then
if dash -n "$WORK/configure" 2>"$WORK/dashn.err"; then
pass "dash: generated ./configure passes dash -n"
else
fail "dash: generated ./configure fails dash -n: $(cat "$WORK/dashn.err" 2>/dev/null)"
fi
fi
# run configure UNDER THE SHELL. CC/CFLAGS/CXX/CXXFLAGS are unset so
# the generated ${VAR:-default} toolchain defaults apply (and zsh's
# no-word-split on unquoted $CFLAGS cannot be fed a multi-word value).
conf_rc=0
(cd "$WORK" && unset CC CFLAGS CXX CXXFLAGS
"$shell" ./configure --prefix="$WORK/install" \
>configure.out 2>configure.err) || conf_rc=$?
if [ "$conf_rc" -eq 0 ]; then
pass "$shell: ./configure exited 0"
else
fail "$shell: ./configure exited rc=$conf_rc: $(cat "$WORK/configure.err" 2>/dev/null)"
fi
if grep -iE 'syntax error|parse error' "$WORK/configure.out" \
"$WORK/configure.err" >/dev/null 2>&1; then
fail "$shell: configure output carries a shell-syntax-error signature"
else
pass "$shell: no shell-syntax-error signature in configure output"
fi
# the substituted Makefile: no @VAR@ left, LIBS accumulated, prefix set
if [ -f "$WORK/Makefile" ]; then
pass "$shell: configure wrote Makefile"
if grep -E '@[A-Za-z_][A-Za-z0-9_]*@' "$WORK/Makefile" >/dev/null 2>&1; then
fail "$shell: Makefile still contains @VAR@ placeholders"
else
pass "$shell: no @VAR@ placeholders left in Makefile"
fi
if grep '^LIBS' "$WORK/Makefile" | grep -q -- '-lpthread'; then
pass "$shell: Makefile LIBS contains -lpthread"
else
fail "$shell: Makefile LIBS missing -lpthread: $(grep '^LIBS' "$WORK/Makefile")"
fi
if grep '^LIBS' "$WORK/Makefile" | grep -q -- '-lm'; then
pass "$shell: Makefile LIBS contains -lm"
else
fail "$shell: Makefile LIBS missing -lm: $(grep '^LIBS' "$WORK/Makefile")"
fi
if grep -qF "prefix = $WORK/install" "$WORK/Makefile"; then
pass "$shell: Makefile prefix substituted with --prefix value"
else
fail "$shell: Makefile prefix unexpected: $(grep '^prefix' "$WORK/Makefile")"
fi
else
fail "$shell: configure wrote no Makefile"
fi
# make (bounded via timeout when available)
make_rc=0
if command -v timeout >/dev/null 2>&1; then
(cd "$WORK" && timeout 300 make >make.log 2>&1) || make_rc=$?
else
(cd "$WORK" && make >make.log 2>&1) || make_rc=$?
fi
if [ "$make_rc" -eq 0 ] && [ -x "$WORK/demo" ]; then
pass "$shell: make built the demo binary"
else
fail "$shell: make failed (rc=$make_rc): $(tail -n 5 "$WORK/make.log" 2>/dev/null)"
fi
# the binary must RUN and print the markers (pthread + libm proven live)
if [ -x "$WORK/demo" ]; then
demo_out=$("$WORK/demo" 2>"$WORK/demo.err")
demo_rc=$?
if [ "$demo_rc" -eq 0 ]; then
pass "$shell: demo binary runs (exit 0)"
else
fail "$shell: demo binary exited rc=$demo_rc: $(cat "$WORK/demo.err" 2>/dev/null)"
fi
if printf '%s\n' "$demo_out" | grep -qF 'sin(0.5) = 0.4794'; then
pass "$shell: demo prints the libm marker 'sin(0.5) = 0.4794'"
else
fail "$shell: demo output missing libm marker: '$demo_out'"
fi
if printf '%s\n' "$demo_out" | grep -qF 'worker thread ran'; then
pass "$shell: demo prints the pthread marker 'worker thread ran'"
else
fail "$shell: demo output missing pthread marker: '$demo_out'"
fi
else
fail "$shell: skipping binary checks (no demo binary)"
fi
# config.h: HAVE_ defines for the resolved features
if grep -qF '#define HAVE_PTHREAD 1' "$WORK/config.h"; then
pass "$shell: config.h defines HAVE_PTHREAD 1"
else
fail "$shell: config.h missing '#define HAVE_PTHREAD 1': $(cat "$WORK/config.h" 2>/dev/null)"
fi
if grep -qF '#define HAVE_MATH 1' "$WORK/config.h"; then
pass "$shell: config.h defines HAVE_MATH 1"
else
fail "$shell: config.h missing '#define HAVE_MATH 1'"
fi
# config.log: the ## results summary records every feature's resolution
if grep -qF 'have_pthread=yes' "$WORK/config.log"; then
pass "$shell: config.log records have_pthread=yes"
else
fail "$shell: config.log missing have_pthread=yes"
fi
if grep -qF 'have_math=yes' "$WORK/config.log"; then
pass "$shell: config.log records have_math=yes"
else
fail "$shell: config.log missing have_math=yes"
fi
# config.status --config must print the original invocation
cs_rc=0
cs_out=$("$shell" "$WORK/config.status" --config 2>"$WORK/cs.err") || cs_rc=$?
if [ "$cs_rc" -eq 0 ]; then
case "$cs_out" in
*"$WORK/install"*)
pass "$shell: config.status --config prints the original args" ;;
*)
fail "$shell: config.status --config output unexpected: '$cs_out'" ;;
esac
else
fail "$shell: config.status --config exited rc=$cs_rc: $(cat "$WORK/cs.err" 2>/dev/null)"
fi
# --help lists the option node's --enable-debug surface
help_rc=0
help_out=$(cd "$WORK" && unset CC CFLAGS CXX CXXFLAGS
"$shell" ./configure --help 2>&1) || help_rc=$?
if [ "$help_rc" -eq 0 ] \
&& printf '%s\n' "$help_out" | grep -qF -- '--enable-debug'; then
pass "$shell: --help exits 0 and lists --enable-debug"
else
fail "$shell: --help rc=$help_rc or --enable-debug missing"
fi
}
# --- failure case: missing header must fail cleanly, never a syntax error -
run_failure() {
shell=$1
new_work || return 1
if cp "$FIX/project-fail.kdl" "$FIX/Makefile.in" "$WORK/" \
&& (cd "$WORK" && XDG_DATA_HOME="$WORK/xdg" STUPIDTOOLS_EXT= \
"$BIN" project-fail.kdl >gen.log 2>&1) \
&& [ -s "$WORK/configure" ]; then
pass "$shell: stupidtools generated ./configure from project-fail.kdl"
else
fail "$shell: failure-fixture generation failed: $(cat "$WORK/gen.log" 2>/dev/null)"
return 1
fi
# the frozen v1 generator records the failed check and exits 0 (see the
# header NOTE); rc=0 here IS the clean-failure contract being asserted
conf_rc=0
(cd "$WORK" && unset CC CFLAGS CXX CXXFLAGS
"$shell" ./configure >configure.out 2>configure.err) || conf_rc=$?
if [ "$conf_rc" -eq 0 ]; then
pass "$shell: missing-header configure completed cleanly (rc=0, v1 generator semantics)"
else
fail "$shell: missing-header configure exited rc=$conf_rc: $(cat "$WORK/configure.err" 2>/dev/null)"
fi
if grep -iE 'syntax error|parse error' "$WORK/configure.out" \
"$WORK/configure.err" >/dev/null 2>&1; then
fail "$shell: missing-header configure output carries a shell-syntax-error signature"
else
pass "$shell: no shell-syntax-error signature on the missing-header run"
fi
# the failure is honestly recorded: have_nope=no, the healthy feature
# still yes, and the compiler's diagnostic names the missing check
if grep -qF 'have_nope=no' "$WORK/config.log"; then
pass "$shell: config.log records have_nope=no"
else
fail "$shell: config.log missing have_nope=no"
fi
if grep -qF 'have_posix=yes' "$WORK/config.log"; then
pass "$shell: config.log records have_posix=yes (configure kept going)"
else
fail "$shell: config.log missing have_posix=yes"
fi
if grep -qF 'nope_missing_xyz.h' "$WORK/config.log"; then
pass "$shell: config.log names the failed check (nope_missing_xyz.h)"
else
fail "$shell: config.log does not name the failed check"
fi
# config.h: no HAVE_NOPE define; the healthy feature still gets one
if grep -qF 'HAVE_NOPE' "$WORK/config.h"; then
fail "$shell: config.h must NOT define HAVE_NOPE"
else
pass "$shell: config.h has no HAVE_NOPE define"
fi
if grep -qF '#define HAVE_POSIX 1' "$WORK/config.h"; then
pass "$shell: config.h still defines HAVE_POSIX 1"
else
fail "$shell: config.h missing '#define HAVE_POSIX 1'"
fi
# config.status --config works on the failure fixture too
cs_rc=0
"$shell" "$WORK/config.status" --config >"$WORK/cs.out" 2>&1 || cs_rc=$?
if [ "$cs_rc" -eq 0 ]; then
pass "$shell: config.status --config exits 0 (failure fixture)"
else
fail "$shell: config.status --config exited rc=$cs_rc"
fi
# the genuinely-non-zero configure-error path: an unknown option must
# exit 1 with a readable message and no shell syntax error
bogus_rc=0
bogus_err=$(cd "$WORK" && "$shell" ./configure --definitely-bogus-flag \
2>&1) || bogus_rc=$?
if [ "$bogus_rc" -eq 1 ] \
&& printf '%s\n' "$bogus_err" | grep -qF 'unrecognized option'; then
pass "$shell: unknown option exits 1 with a readable message"
else
fail "$shell: unknown option rc=$bogus_rc: '$bogus_err'"
fi
if printf '%s\n' "$bogus_err" | grep -iE 'syntax error|parse error' \
>/dev/null 2>&1; then
fail "$shell: unknown-option failure output carries a syntax-error signature"
else
pass "$shell: no syntax-error signature on the unknown-option failure"
fi
}
# --- the matrix ------------------------------------------------------------
for shell in "$@"; do
run_happy "$shell"
run_failure "$shell"
done
# --- summary ---------------------------------------------------------------
if [ "$TESTS_FAILED" -ne 0 ]; then
printf 'FAILED: %d/%d checks failed\n' "$TESTS_FAILED" "$TESTS_RUN" >&2
exit 1
fi
printf 'All %d integration checks passed (matrix:%s).\n' "$TESTS_RUN" "$SHELLS"