#!/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_=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_=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"