test: add end-to-end integration across dash/bash/zsh

This commit is contained in:
2026-08-29 01:10:28 -04:00
parent 22e22f2575
commit b94a540673
7 changed files with 573 additions and 0 deletions
+27
View File
@@ -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
+16
View File
@@ -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;
}
+10
View File
@@ -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 */
+63
View File
@@ -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 <math.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef HAVE_PTHREAD
#include <pthread.h>
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;
}
@@ -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_<name>=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"
}
+39
View File
@@ -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
+381
View File
@@ -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_<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"