#!/usr/bin/env bash # Regression suite for #1019: ci-queue-wait.sh's status parser must return a # state DERIVED FROM ITS INPUT. # # The defect this pins: both python sites piped a payload into `python3 - <<'PY'`. # The heredoc binds stdin to the Python program text, so json.load(sys.stdin) saw # EOF, the bare `except` fired, and the function returned "unknown" for every input # — success, pending and failure alike. "unknown" then reaches an `exit 0` arm, so # the gate-6 queue guard passed unconditionally on both the gitea and github paths. # # Every assertion below is on the RETURNED STATE STRING. A test that asserted only # `rc=0` would have passed against the broken build, which is why this bug survived. # # The functions are extracted from the shipped wrapper rather than reimplemented, so # this suite measures the code that actually runs. set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Overridable so a mutation test can point the suite at a deliberately-broken copy # without touching the tracked file. The earlier version of this suite required # `git stash` + `git checkout` to do that, which left a repo-global stash entry # that any sibling worktree could have popped onto an unrelated branch. WRAPPER="${CI_QUEUE_WAIT_WRAPPER:-$SCRIPT_DIR/ci-queue-wait.sh}" PASS=0 FAIL=0 pass() { printf ' PASS %s\n' "$1"; PASS=$((PASS + 1)); } fail() { printf ' FAIL %s\n' "$1"; FAIL=$((FAIL + 1)); } if [[ ! -f "$WRAPPER" ]]; then printf 'FATAL: wrapper not found at %s\n' "$WRAPPER" >&2 exit 1 fi TMPDIR_T="$(mktemp -d)" trap 'rm -rf "$TMPDIR_T"' EXIT extract_fn() { # $1 = function name, $2 = source file, $3 = destination sed -n "/^$1()/,/^}/p" "$2" > "$3" [[ -s "$3" ]] || { printf 'FATAL: could not extract %s from %s\n' "$1" "$2" >&2; exit 1; } } extract_fn get_state_from_status_json "$WRAPPER" "$TMPDIR_T/state.sh" extract_fn print_pending_contexts "$WRAPPER" "$TMPDIR_T/contexts.sh" state_of() { # shellcheck disable=SC1091 ( source "$TMPDIR_T/state.sh"; printf '%s' "$1" | get_state_from_status_json ) } expect_state() { local label="$1" payload="$2" want="$3" got got="$(state_of "$payload")" if [[ "$got" == "$want" ]]; then pass "$label -> $want" else fail "$label -> got '$got', want '$want'" fi } echo "=== parser returns a state derived from its input (#1019) ===" expect_state "success payload" \ '{"state":"success","statuses":[{"status":"success","context":"ci/build"}]}' \ 'terminal-success' expect_state "pending payload" \ '{"state":"pending","statuses":[{"status":"pending","context":"ci/build"}]}' \ 'pending' expect_state "failure payload" \ '{"state":"failure","statuses":[{"status":"failure","context":"ci/build"}]}' \ 'terminal-failure' expect_state "mixed success+pending is pending" \ '{"state":"pending","statuses":[{"status":"success"},{"status":"pending"}]}' \ 'pending' expect_state "running counts as pending" \ '{"state":"pending","statuses":[{"status":"running"}]}' \ 'pending' expect_state "error counts as failure" \ '{"state":"failure","statuses":[{"status":"error"}]}' \ 'terminal-failure' expect_state "empty status set is no-status" \ '{"state":"","statuses":[]}' \ 'no-status' # Control. This is the ONE input for which "unknown" is correct. Without it, a # regression that hardcoded "unknown" again would still fail the cases above but # the suite would give no signal that "unknown" remains reachable when it should be. expect_state "undecodable payload stays unknown" \ 'not json at all' \ 'unknown' expect_state "unrecognised status vocabulary is unknown" \ '{"state":"weird","statuses":[{"status":"weird"}]}' \ 'unknown' echo "=== pending contexts are reported to the operator ===" contexts_of() { # shellcheck disable=SC1091 ( source "$TMPDIR_T/contexts.sh"; printf '%s' "$1" | print_pending_contexts ) } out="$(contexts_of '{"statuses":[{"status":"pending","context":"ci/alpha"},{"status":"pending","context":"ci/beta"}]}')" if grep -q 'ci/alpha' <<<"$out" && grep -q 'ci/beta' <<<"$out"; then pass "both pending contexts emitted" else fail "pending contexts not emitted; got: $out" fi out="$(contexts_of '{"statuses":[{"status":"success","context":"ci/alpha"}]}')" # Assert the POSITIVE message, not merely the absence of the context name. Absence # alone is satisfied by total silence — and the pre-fix build was silent, so an # absence-only assertion passed against the very defect this suite exists to catch. if grep -q 'ci/alpha' <<<"$out"; then fail "a non-pending context was emitted; got: $out" elif grep -q 'no pending contexts' <<<"$out"; then pass "non-pending context suppressed, and reported as 'no pending contexts'" else fail "expected an explicit 'no pending contexts' report; got: $out" fi echo "=== needle: the broken construct is caught, not merely absent today ===" # Rebuild the pre-fix form and assert this suite would have failed against it. # Without this, the suite proves the current file is correct but not that it can # detect the defect returning. BROKEN="$TMPDIR_T/broken.sh" cat > "$BROKEN" <<'BROKEN_EOF' get_state_from_status_json() { python3 - <<'PY' import json import sys try: payload = json.load(sys.stdin) except Exception: print("unknown") raise SystemExit(0) print("terminal-success" if (payload.get("state") or "") == "success" else "pending") PY } BROKEN_EOF broken_got="$( ( source "$BROKEN"; printf '%s' '{"state":"success","statuses":[]}' | get_state_from_status_json ) )" if [[ "$broken_got" == "unknown" ]]; then pass "[NEEDLE ] pre-fix construct reproduces the defect (returns 'unknown' for a success payload)" else fail "[NEEDLE ] pre-fix construct did NOT reproduce the defect; got '$broken_got' — the needle no longer pins anything" fi # And assert the shipped wrapper does not contain that construct. # # The check must have HEREDOC SEMANTICS, not merely match the text. `json.load(sys.stdin)` # is perfectly correct under `python3 -c '...'` — there the program comes from argv, so # stdin really is the payload, and ci-queue-wait.sh uses that form legitimately in # gitea_get_branch_head_sha. Only `python3 - <