#!/bin/sh # A manifest-backed collector prevents a lost parallel worker from shrinking a # required corpus into a false green. BIN=${BIN:-out/bin} WW=${WW:-$BIN/ww} TEST_DIR=${TEST_DIR:-test/wcc} TEST_EXPECTED_MIN=${TEST_EXPECTED_MIN:-337} TEST_TIMEOUT=${TEST_TIMEOUT:-900s} TEST_KILL_AFTER=${TEST_KILL_AFTER:-10s} TEST_WORKER=${TEST_WORKER:-$0} # Required-suite results describe the toolchain under BIN, not ambient driver # overrides from a developer shell. Individual tests still set overrides # explicitly when that behavior is what they exercise. unset WW_W6C WW_W6A WW_W6L WW_LIB WW_SRCLIB WW_PKGCACHE atomic_record() { record_path=$1 record_text=$2 record_tmp=$record_path.tmp.$$ if ! printf '%s\n' "$record_text" > "$record_tmp"; then rm -f "$record_tmp" return 1 fi if ! mv "$record_tmp" "$record_path"; then rm -f "$record_tmp" return 1 fi return 0 } # Worker mode owns one result prefix. A result becomes visible only after every # capture and timing file is complete, so the collector cannot accept a torn # terminal record as a finished test. if [ "${1:-}" = "--one" ]; then [ "$#" -eq 3 ] || exit 64 prefix=$2 t=$3 name=${t##*/} name=${name%.c} short=${name#[0-9][0-9][0-9]_} bin=$BIN/test_$short if ! atomic_record "$prefix.started" "WWTEST_STARTED 1"; then exit 70 fi if ! : > "$prefix.out" || ! : > "$prefix.err"; then exit 70 fi t0=$(date +%s.%N) outcome=pass rc=0 if [ ! -x "$bin" ]; then printf 'unwired test: no executable %s\n' "$bin" > "$prefix.err" outcome=harness_error rc=127 else # The child records its own status before the timeout wrapper exits. # That distinguishes a real child exit 124/137 from expiration, and # child exits 125-127 from timeout's reserved infrastructure codes. command_status=$prefix.command-status timeout_diag=$prefix.timeout-diag if LC_ALL=C timeout --verbose --kill-after="$TEST_KILL_AFTER" \ "$TEST_TIMEOUT" sh -c ' "$1" > "$2" 2> "$3" command_rc=$? status_tmp=$4.tmp.$$ printf "%s\n" "$command_rc" > "$status_tmp" || exit 70 mv "$status_tmp" "$4" || exit 70 ' sh "$bin" "$prefix.out" "$prefix.err" "$command_status" \ 2> "$timeout_diag"; then timeout_rc=0 else timeout_rc=$? fi deadline_expired=0 if grep -Eq '^timeout: sending signal (TERM|KILL) to command ' \ "$timeout_diag"; then deadline_expired=1 fi if [ -s "$timeout_diag" ]; then cat "$timeout_diag" >> "$prefix.err" fi if [ "$deadline_expired" -eq 1 ]; then outcome=timeout rc=$timeout_rc elif [ -f "$command_status" ]; then rc="" extra="" IFS=' ' read -r rc extra < "$command_status" case $rc in ''|*[!0-9]*) rc_valid=0 ;; *) rc_valid=1 ;; esac if [ "$rc_valid" -ne 1 ] || [ -n "$extra" ] \ || [ "$timeout_rc" -ne 0 ]; then printf 'timeout wrapper returned %s with child status %s\n' \ "$timeout_rc" "${rc:-missing}" >> "$prefix.err" outcome=harness_error rc=125 elif [ "$rc" -eq 0 ]; then outcome=pass elif [ "$rc" -eq 77 ]; then outcome=skip else outcome=fail fi else printf 'timeout wrapper produced no child status (rc=%s)\n' \ "$timeout_rc" >> "$prefix.err" outcome=harness_error case $timeout_rc in 125|126|127) rc=$timeout_rc ;; *) rc=125 ;; esac fi fi t1=$(date +%s.%N) dur_tmp=$prefix.dur.tmp.$$ if ! awk -v a="$t0" -v b="$t1" -v n="$name" \ 'BEGIN{printf "%.3f\t%s\n", b-a, n}' > "$dur_tmp"; then rm -f "$dur_tmp" exit 70 fi if ! mv "$dur_tmp" "$prefix.dur"; then rm -f "$dur_tmp" exit 70 fi if ! atomic_record "$prefix.result" "WWTEST_RESULT 1 $outcome $rc"; then exit 70 fi exit 0 fi RESULTS=$(mktemp -d "${TMPDIR:-/tmp}/wwtest.XXXXXX") || exit 2 trap 'rm -rf "$RESULTS"' EXIT trap 'exit 1' HUP INT TERM ALL_MANIFEST=$RESULTS/all.manifest PHASE1_MANIFEST=$RESULTS/phase1.manifest PHASE2_MANIFEST=$RESULTS/phase2.manifest RUN1_MANIFEST=$RESULTS/run1.manifest RUN2_MANIFEST=$RESULTS/run2.manifest SELECTED_NAMES=$RESULTS/selected.names CACHED_NAMES=$RESULTS/cached.names ALL_NAMES=$RESULTS/all.names for manifest in "$ALL_MANIFEST" "$PHASE1_MANIFEST" "$PHASE2_MANIFEST" \ "$RUN1_MANIFEST" "$RUN2_MANIFEST" "$SELECTED_NAMES" \ "$CACHED_NAMES" "$ALL_NAMES"; do if ! : > "$manifest"; then printf 'HARNESS suite (cannot initialize manifest %s)\n' "$manifest" exit 2 fi done is_phase2() { case $1 in 950_*|990_*|991_*|992_*|993_*|994_*|995_*|996_*|997_*) return 0 ;; esac return 1 } is_cached_gate() { case $1 in 950_selfcheck.c|989_lib_byteid.c|\ 990_*|991_*|992_*|993_*|994_*|995_*|997_*) return 0 ;; esac return 1 } for t in "$TEST_DIR"/*.c; do [ -f "$t" ] || continue printf '%s\n' "$t" >> "$ALL_MANIFEST" name=${t##*/} name=${name%.c} printf '%s\n' "$name" >> "$ALL_NAMES" if is_phase2 "${t##*/}"; then printf '%s\n' "$t" >> "$PHASE2_MANIFEST" else printf '%s\n' "$t" >> "$PHASE1_MANIFEST" fi done discovered=$(awk 'END{print NR + 0}' "$ALL_MANIFEST") excluded=0 preflight_error=0 harness_error=0 case $TEST_EXPECTED_MIN in ''|*[!0-9]*) printf 'HARNESS suite (invalid TEST_EXPECTED_MIN: %s)\n' "$TEST_EXPECTED_MIN" preflight_error=1 harness_error=$((harness_error + 1)) ;; *) if [ "$discovered" -eq 0 ]; then echo 'HARNESS suite (empty corpus)' preflight_error=1 harness_error=$((harness_error + 1)) elif [ "$discovered" -lt "$TEST_EXPECTED_MIN" ]; then printf 'HARNESS suite (corpus shrank: %d < %d)\n' \ "$discovered" "$TEST_EXPECTED_MIN" preflight_error=1 harness_error=$((harness_error + 1)) fi ;; esac duplicate_names=$(sort "$ALL_NAMES" | uniq -d) if [ -n "$duplicate_names" ]; then echo 'HARNESS suite (duplicate test identifiers)' printf '%s\n' "$duplicate_names" preflight_error=1 harness_error=$((harness_error + 1)) fi JOBS=${JOBS:-$(nproc 2>/dev/null || echo 4)} case $JOBS in ''|*[!0-9]*|0) printf 'HARNESS suite (invalid JOBS: %s)\n' "$JOBS" preflight_error=1 harness_error=$((harness_error + 1)) ;; esac if ! command -v timeout >/dev/null 2>&1; then echo 'HARNESS suite (GNU timeout is unavailable)' preflight_error=1 harness_error=$((harness_error + 1)) fi valid_duration() { awk -v duration="$1" 'BEGIN { if (duration !~ /^[0-9]+([.][0-9]+)?[smhd]?$/) exit 1 sub(/[smhd]$/, "", duration) exit !(duration + 0 > 0) }' } if ! valid_duration "$TEST_TIMEOUT"; then printf 'HARNESS suite (invalid TEST_TIMEOUT: %s)\n' "$TEST_TIMEOUT" preflight_error=1 harness_error=$((harness_error + 1)) fi if ! valid_duration "$TEST_KILL_AFTER"; then printf 'HARNESS suite (invalid TEST_KILL_AFTER: %s)\n' "$TEST_KILL_AFTER" preflight_error=1 harness_error=$((harness_error + 1)) fi OUT_DIR=$(dirname "$BIN") CACHE_DIR=$OUT_DIR/.testcache CACHE_FILE=$CACHE_DIR/last-green # The cache includes nonignored untracked inputs because a local fixture or # source file can change a gate even before it is added to the index. testcache_key() { cache_inputs=$RESULTS/cache-inputs if ! : > "$cache_inputs"; then return 1 fi # Hash the effective built tools, runtime archive, and every executable # that the last-green policy may replace with a cached result. for b in ww w6c w6a w6l wwdump ww_ww w6c_ww w6a_ww w6l_ww wwdump_ww; do printf '%s\0' "$BIN/$b" >> "$cache_inputs" || return 1 done printf '%s\0' "$OUT_DIR/lib/libwwrt.a" >> "$cache_inputs" || return 1 while IFS= read -r t; do [ -n "$t" ] || continue if is_cached_gate "${t##*/}"; then name=${t##*/} name=${name%.c} short=${name#[0-9][0-9][0-9]_} printf '%s\0' "$BIN/test_$short" >> "$cache_inputs" || return 1 fi printf '%s\0' "$t" >> "$cache_inputs" || return 1 done < "$ALL_MANIFEST" if ! git ls-files --cached -z -- \ selfhost lib cmd rt examples test/wcc test/run test/run_test.sh Makefile \ >> "$cache_inputs"; then return 1 fi # Generated package-cache state is not fixture input; hashing it would make # a gate mutate its own key. Other untracked files under data/ remain input. if ! git ls-files --others --exclude-standard -z -- \ selfhost lib cmd rt examples test/wcc test/run test/run_test.sh Makefile \ ':(exclude,glob)test/wcc/data/**/.pkgcache/**' >> "$cache_inputs"; then return 1 fi if ! sort -z -u "$cache_inputs" -o "$cache_inputs"; then return 1 fi for setting in \ "TEST_DIR=$TEST_DIR" \ "TEST_EXPECTED_MIN=$TEST_EXPECTED_MIN" \ "TEST_TIMEOUT=$TEST_TIMEOUT" \ "TEST_KILL_AFTER=$TEST_KILL_AFTER" \ "TEST_WORKER=$TEST_WORKER" \ "JOBS=$JOBS"; do setting_key=$(printf '%s' "$setting" | md5sum) || return 1 printf 'setting %s\n' "${setting_key%% *}" done # The index can name an unstaged deletion. Preserve that state in the # stream instead of letting md5sum abort and silently disable caching. xargs -0 -r sh -c ' for cache_file do path_key=$(printf "%s" "$cache_file" | md5sum) || exit path_key=${path_key%% *} if [ -f "$cache_file" ]; then content_key=$(md5sum < "$cache_file") || exit content_key=${content_key%% *} mode=- [ -x "$cache_file" ] && mode=x printf "file %s %s %s\n" \ "$path_key" "$mode" "$content_key" elif [ ! -e "$cache_file" ] && [ ! -L "$cache_file" ]; then printf "missing %s\n" "$path_key" else printf "unsupported cache input %s\n" "$path_key" >&2 exit 1 fi done ' sh < "$cache_inputs" } KEY="" if [ "${UNIT:-0}" != "1" ] && [ "$TEST_WORKER" = "$0" ]; then if key_stream=$(testcache_key 2>/dev/null); then KEY=$(printf '%s\n' "$key_stream" | md5sum | cut -d' ' -f1) fi fi CACHE_HIT=0 if [ "${COMMIT:-0}" = "1" ] && [ -n "$KEY" ] && [ -f "$CACHE_FILE" ] \ && [ "$(cat "$CACHE_FILE" 2>/dev/null)" = "$KEY" ]; then CACHE_HIT=1 fi emit_cached() { cached_name=$1 cached_prefix=$RESULTS/$cached_name if ! atomic_record "$cached_prefix.result" "WWTEST_RESULT 1 cached 0"; then return 1 fi printf '%s\n' "$cached_name" >> "$CACHED_NAMES" return 0 } plan_manifest() { plan_input=$1 plan_run=$2 while IFS= read -r t; do [ -n "$t" ] || continue name=${t##*/} name=${name%.c} printf '%s\n' "$name" >> "$SELECTED_NAMES" if [ "$CACHE_HIT" = 1 ] && is_cached_gate "${t##*/}"; then if ! emit_cached "$name"; then printf 'HARNESS %s (cannot record cached result)\n' "$name" harness_error=$((harness_error + 1)) fi else printf '%s\n' "$t" >> "$plan_run" fi done < "$plan_input" } if [ "$preflight_error" -eq 0 ]; then plan_manifest "$PHASE1_MANIFEST" "$RUN1_MANIFEST" if [ "${UNIT:-0}" = "1" ]; then while IFS= read -r t; do [ -n "$t" ] || continue name=${t##*/} name=${name%.c} printf 'excluded %s (test-unit phase-2)\n' "$name" excluded=$((excluded + 1)) done < "$PHASE2_MANIFEST" else plan_manifest "$PHASE2_MANIFEST" "$RUN2_MANIFEST" fi fi launch_manifest() { launch_file=$1 [ -s "$launch_file" ] || return 0 while IFS= read -r t; do name=${t##*/} name=${name%.c} printf '%s/%s\0%s\0' "$RESULTS" "$name" "$t" done < "$launch_file" | \ xargs -0 -r -n2 -P "$JOBS" "$TEST_WORKER" --one } run_start=$(date +%s.%N) launcher_error=0 p1_end=$run_start p2par_start="" p2par_end="" if [ "$preflight_error" -eq 0 ]; then if launch_manifest "$RUN1_MANIFEST"; then : else rc=$? printf 'HARNESS launcher (phase 1 xargs rc=%d)\n' "$rc" launcher_error=1 harness_error=$((harness_error + 1)) fi p1_end=$(date +%s.%N) if [ "${UNIT:-0}" != "1" ] && [ -s "$RUN2_MANIFEST" ]; then p2par_start=$(date +%s.%N) if launch_manifest "$RUN2_MANIFEST"; then : else rc=$? printf 'HARNESS launcher (phase 2 xargs rc=%d)\n' "$rc" launcher_error=1 harness_error=$((harness_error + 1)) fi p2par_end=$(date +%s.%N) fi fi emit_output() { output_prefix=$1 output_name=$2 if [ -s "$output_prefix.out" ]; then printf '%s\n' "--- stdout: $output_name ---" cat "$output_prefix.out" fi if [ -s "$output_prefix.err" ]; then printf '%s\n' "--- stderr: $output_name ---" cat "$output_prefix.err" fi } started=0 completed=0 pass=0 fail=0 skip=0 timed_out=0 cached=0 worker_harness_error=0 if [ "$preflight_error" -eq 0 ]; then while IFS= read -r name; do [ -n "$name" ] || continue prefix=$RESULTS/$name expect_cached=0 if grep -Fqx "$name" "$CACHED_NAMES"; then expect_cached=1 fi has_started=0 if [ -f "$prefix.started" ]; then if grep -qx 'WWTEST_STARTED 1' "$prefix.started"; then has_started=1 started=$((started + 1)) else printf 'HARNESS %s (malformed started record)\n' "$name" harness_error=$((harness_error + 1)) fi fi if [ ! -f "$prefix.result" ]; then if [ "$has_started" -eq 1 ]; then printf 'HARNESS %s (started but no terminal result)\n' "$name" else printf 'HARNESS %s (worker never started)\n' "$name" fi harness_error=$((harness_error + 1)) continue fi magic="" version="" outcome="" result_rc="" extra="" IFS=' ' read -r magic version outcome result_rc extra < "$prefix.result" if [ "$magic" != WWTEST_RESULT ] || [ "$version" != 1 ] \ || [ -z "$outcome" ] || [ -z "$result_rc" ] || [ -n "$extra" ]; then printf 'HARNESS %s (malformed terminal result)\n' "$name" harness_error=$((harness_error + 1)) continue fi case $result_rc in ''|*[!0-9]*) printf 'HARNESS %s (nonnumeric terminal rc)\n' "$name" harness_error=$((harness_error + 1)) continue ;; esac if [ "$outcome" = cached ]; then if [ "$expect_cached" -ne 1 ] || [ "$has_started" -ne 0 ] \ || [ "$result_rc" != 0 ]; then printf 'HARNESS %s (invalid cached result)\n' "$name" harness_error=$((harness_error + 1)) continue fi printf 'cached %s\n' "$name" cached=$((cached + 1)) continue fi capture_error=0 for suffix in out err; do if [ ! -f "$prefix.$suffix" ]; then printf 'HARNESS %s (missing %s capture)\n' "$name" "$suffix" capture_error=1 fi done if [ ! -s "$prefix.dur" ]; then printf 'HARNESS %s (missing duration record)\n' "$name" capture_error=1 fi if [ "$capture_error" -ne 0 ]; then harness_error=$((harness_error + 1)) continue fi completed=$((completed + 1)) if [ "$expect_cached" -eq 1 ] || [ "$has_started" -ne 1 ]; then printf 'HARNESS %s (terminal result without matching start)\n' "$name" harness_error=$((harness_error + 1)) fi case $outcome:$result_rc in pass:0) printf 'ok %s\n' "$name" pass=$((pass + 1)) ;; skip:77) printf 'skip %s (rc=77)\n' "$name" skip=$((skip + 1)) if [ ! -s "$prefix.out" ] && [ ! -s "$prefix.err" ]; then echo 'skip reason: exit 77 (no output)' fi ;; timeout:124|timeout:137) printf 'TIMEOUT %s\n' "$name" timed_out=$((timed_out + 1)) ;; fail:*) printf 'FAIL %s (rc=%s)\n' "$name" "$result_rc" fail=$((fail + 1)) ;; harness_error:125|harness_error:126|harness_error:127) printf 'HARNESS %s (worker rc=%s)\n' "$name" "$result_rc" worker_harness_error=$((worker_harness_error + 1)) harness_error=$((harness_error + 1)) ;; *) printf 'HARNESS %s (invalid outcome/rc %s/%s)\n' \ "$name" "$outcome" "$result_rc" worker_harness_error=$((worker_harness_error + 1)) harness_error=$((harness_error + 1)) ;; esac emit_output "$prefix" "$name" done < "$SELECTED_NAMES" for record in "$RESULTS"/*.result "$RESULTS"/*.started; do [ -f "$record" ] || continue record_name=${record##*/} record_name=${record_name%.result} record_name=${record_name%.started} if ! grep -Fqx "$record_name" "$SELECTED_NAMES"; then printf 'HARNESS %s (unexpected worker record)\n' "$record_name" harness_error=$((harness_error + 1)) fi done fi selected=$((discovered - excluded)) planned=$((selected - cached)) terminal=$((pass + fail + skip + timed_out + worker_harness_error)) if [ "$preflight_error" -eq 0 ]; then if [ "$started" -ne "$planned" ]; then printf 'HARNESS accounting (started %d != planned %d)\n' \ "$started" "$planned" harness_error=$((harness_error + 1)) fi if [ "$completed" -ne "$planned" ]; then printf 'HARNESS accounting (completed %d != planned %d)\n' \ "$completed" "$planned" harness_error=$((harness_error + 1)) fi if [ "$terminal" -ne "$completed" ]; then printf 'HARNESS accounting (terminal outcomes %d != completed %d)\n' \ "$terminal" "$completed" harness_error=$((harness_error + 1)) fi if [ $((completed + cached + excluded)) -ne "$discovered" ]; then printf 'HARNESS accounting (discovered corpus is incomplete)\n' harness_error=$((harness_error + 1)) fi fi # A cache decision is valid only if its complete input fingerprint is still # current after all workers have finished. This applies to hits as well as to # newly published keys. if [ "${UNIT:-0}" != "1" ] && [ -n "$KEY" ]; then end_key="" if end_stream=$(testcache_key 2>/dev/null); then end_key=$(printf '%s\n' "$end_stream" | md5sum | cut -d' ' -f1) fi if [ -z "$end_key" ] || [ "$end_key" != "$KEY" ]; then echo 'HARNESS cache (inputs changed during test run)' harness_error=$((harness_error + 1)) fi fi not_run=$((discovered - completed - cached)) run_end=$(date +%s.%N) echo '--- timing: per-test (sec, desc) ---' cat "$RESULTS"/*.dur 2>/dev/null | sort -rn echo '--- timing: phase walls (sec) ---' awk -v rs="$run_start" -v p1e="$p1_end" \ -v ppar_s="$p2par_start" -v ppar_e="$p2par_end" \ -v re="$run_end" 'BEGIN{ printf "phase1_parallel\t%.3f\n", p1e - rs if (ppar_e != "") printf "phase2_parallel\t%.3f\n", ppar_e - ppar_s printf "total\t%.3f\n", re - rs }' printf 'summary: discovered=%d started=%d completed=%d pass=%d fail=%d skip=%d timeout=%d harness_error=%d cached=%d not_run=%d\n' \ "$discovered" "$started" "$completed" "$pass" "$fail" "$skip" \ "$timed_out" "$harness_error" "$cached" "$not_run" green=1 if [ "$preflight_error" -ne 0 ] || [ "$launcher_error" -ne 0 ] \ || [ "$fail" -ne 0 ] || [ "$skip" -ne 0 ] || [ "$timed_out" -ne 0 ] \ || [ "$harness_error" -ne 0 ]; then green=0 fi if [ "$preflight_error" -eq 0 ] && [ "$not_run" -ne "$excluded" ]; then green=0 fi if [ "$green" -eq 1 ] && [ "${UNIT:-0}" != "1" ] \ && [ "$CACHE_HIT" != 1 ] && [ -n "$KEY" ]; then if ! mkdir -p "$CACHE_DIR" \ || ! atomic_record "$CACHE_FILE" "$KEY"; then echo 'HARNESS cache (cannot publish last-green key)' exit 1 fi fi [ "$green" -eq 1 ] || exit 1 exit 0