From b94a540673523130443f4d4430d0d3cd93c58eec Mon Sep 17 00:00:00 2001 From: huntedbytheirs Date: Sat, 29 Aug 2026 01:10:10 -0400 Subject: [PATCH] test: add end-to-end integration across dash/bash/zsh --- tests/integration/fixture/Makefile.in | 27 ++ tests/integration/fixture/demo.c | 16 + tests/integration/fixture/demo.h | 10 + tests/integration/fixture/main.c | 63 ++++ tests/integration/fixture/project-fail.kdl | 37 ++ tests/integration/fixture/project.kdl | 39 +++ tests/integration/run.sh | 381 +++++++++++++++++++++ 7 files changed, 573 insertions(+) create mode 100644 tests/integration/fixture/Makefile.in create mode 100644 tests/integration/fixture/demo.c create mode 100644 tests/integration/fixture/demo.h create mode 100644 tests/integration/fixture/main.c create mode 100644 tests/integration/fixture/project-fail.kdl create mode 100644 tests/integration/fixture/project.kdl create mode 100755 tests/integration/run.sh diff --git a/tests/integration/fixture/Makefile.in b/tests/integration/fixture/Makefile.in new file mode 100644 index 0000000..9547216 --- /dev/null +++ b/tests/integration/fixture/Makefile.in @@ -0,0 +1,27 @@ +# Makefile.in - integration-fixture template (plan todo 26). +# +# The substitution placeholders are filled by the generated ./configure +# (they must stay literal here - never expand them in this file). CC and +# CFLAGS come from the C language module's detection, LIBS from the feature +# probes' link flags (-lpthread/-lm), prefix from the --prefix default or +# override. +# +# NOTE: the substitution is LINE-BASED and runs on every line, comments +# included - so no comment may contain a placeholder-shaped token (the +# configure script would treat it as a variable to substitute). +# +# This file is NOT generated by stupidtools: the tool only substitutes +# placeholders into it (Makefile generation is out of v1 scope by design). + +CC = @CC@ +CFLAGS = @CFLAGS@ -std=c23 -Wall -Wextra -Wpedantic +LIBS = @LIBS@ +prefix = @prefix@ + +all: demo + +demo: main.c demo.c demo.h + $(CC) $(CFLAGS) main.c demo.c -o demo $(LIBS) + +clean: + rm -f demo diff --git a/tests/integration/fixture/demo.c b/tests/integration/fixture/demo.c new file mode 100644 index 0000000..6400ba1 --- /dev/null +++ b/tests/integration/fixture/demo.c @@ -0,0 +1,16 @@ +/* + * demo.c - second translation unit of the integration fixture. + * + * Gives the fixture a real multi-file shape (the header demo.h is part of + * the deliverable "sources + headers"). Deliberately libm-free: the only + * libm dependency lives in main.c (sin), so a broken -lm accumulation is + * caught at LINK time there. + */ + +#include "demo.h" + +int +demo_compute(int x) +{ + return x * 3 + 1; +} diff --git a/tests/integration/fixture/demo.h b/tests/integration/fixture/demo.h new file mode 100644 index 0000000..80856b1 --- /dev/null +++ b/tests/integration/fixture/demo.h @@ -0,0 +1,10 @@ +/* + * demo.h - fixture header, included by main.c and demo.c. + */ + +#ifndef HELLOTHREADS_DEMO_H +#define HELLOTHREADS_DEMO_H + +int demo_compute(int x); + +#endif /* HELLOTHREADS_DEMO_H */ diff --git a/tests/integration/fixture/main.c b/tests/integration/fixture/main.c new file mode 100644 index 0000000..cacf443 --- /dev/null +++ b/tests/integration/fixture/main.c @@ -0,0 +1,63 @@ +/* + * main.c - integration-fixture entry point (plan todo 26). + * + * A REAL C23 program exercising both fixture features end to end: + * - pthread: pthread_create()/pthread_join() run a worker thread (guarded + * by the HAVE_PTHREAD define the generated config.h wrote), printing a + * grep-able marker the runner asserts on; + * - math: sin(0.5) from libm, printed with a fixed format the runner + * greps (the link would fail without -lm, so a successful run proves + * the LIBS accumulation reached the Makefile). + * + * _POSIX_C_SOURCE must be defined BEFORE the first include: under strict + * -std=c23 glibc sets __STRICT_ANSI__ and hides pthread_create's + * declaration, which C23 turns into an implicit-declaration ERROR + * (the same trap recorded for src/ in the project learnings). + */ + +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L +#endif + +#include "config.h" +#include "demo.h" + +#include +#include +#include + +#ifdef HAVE_PTHREAD +#include + +static void * +worker(void *arg) +{ + (void)arg; + printf("worker thread ran\n"); + return NULL; +} +#endif + +int +main(void) +{ + double s = sin(0.5); + + printf("hellothreads demo: sin(0.5) = %.4f\n", s); + printf("demo_compute(2) = %d\n", demo_compute(2)); +#ifdef HAVE_PTHREAD + { + pthread_t t; + + if (pthread_create(&t, NULL, worker, NULL) != 0) { + fprintf(stderr, "pthread_create failed\n"); + return EXIT_FAILURE; + } + if (pthread_join(t, NULL) != 0) { + fprintf(stderr, "pthread_join failed\n"); + return EXIT_FAILURE; + } + } +#endif + return EXIT_SUCCESS; +} diff --git a/tests/integration/fixture/project-fail.kdl b/tests/integration/fixture/project-fail.kdl new file mode 100644 index 0000000..21c9ce7 --- /dev/null +++ b/tests/integration/fixture/project-fail.kdl @@ -0,0 +1,37 @@ +/* project-fail.kdl - the FAILURE variant of project.kdl (plan todo 26). + * + * Same shape as project.kdl, but the pthread feature is replaced by a + * feature checking a header that does not exist on any system: + * + * feature "nope" { header "nope_missing_xyz.h" } + * + * The generated ./configure must handle the failed probe CLEANLY under + * every shell in the matrix: 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 reports a syntax error. A healthy + * second feature ("posix", unistd.h) proves configure continues after a + * failed check and the healthy feature still resolves. + * + * 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 here. run.sh asserts the generator's REAL contract and exercises + * the genuinely-non-zero configure-error path via an unrecognized option. + */ + +project "hellothreads-fail" version "1.0.0" + +feature "nope" { + header "nope_missing_xyz.h" +} + +feature "posix" { + header "unistd.h" +} + +target "default" { + src "main.c" + feature "nope" + feature "posix" +} diff --git a/tests/integration/fixture/project.kdl b/tests/integration/fixture/project.kdl new file mode 100644 index 0000000..fb55599 --- /dev/null +++ b/tests/integration/fixture/project.kdl @@ -0,0 +1,39 @@ +/* project.kdl - integration-fixture build file (plan todo 26). + * + * A small but REAL C project exercising the DSL the way a user would: + * - feature "pthread": a header check (pthread.h) AND a library check + * (pthread -> -lpthread), like the classic autoconf AC_CHECK_HEADER + + * AC_CHECK_LIB pair; + * - feature "math": an optional library check (m -> -lm). Not strictly + * required by every build (an option maps to --enable-math/--disable + * --math), but present so the generated Makefile accumulates -lm + * when it resolves; + * - target "default": the real source list of the fixture (descriptive + * in v1 - the generated configure substitutes @VAR@ into Makefile.in, + * it does not generate Makefiles); + * - option "debug": an option node, proving the --enable-debug / + * --disable-debug surface shows up in the generated --help. + * + * DSL grammar (src/kdl/schema.h): `project` first, then targets/features/ + * options; feature children are checks (header/library/... from the 8 + * registry kinds). */ + +project "hellothreads" version "1.0.0" + +feature "pthread" { + header "pthread.h" + library "pthread" +} + +feature "math" { + library "m" +} + +target "default" { + src "main.c" + src "demo.c" + feature "pthread" + feature "math" +} + +option "debug" default=#false diff --git a/tests/integration/run.sh b/tests/integration/run.sh new file mode 100755 index 0000000..2bb681d --- /dev/null +++ b/tests/integration/run.sh @@ -0,0 +1,381 @@ +#!/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"