| 1 | #!/usr/bin/env bash |
| 2 | # run_h2spec.sh — run the h2spec HTTP/2 conformance suite against the net.http |
| 3 | # HTTP/2 server and gate on a known-failures baseline. |
| 4 | # |
| 5 | # h2spec (https://github.com/summerwind/h2spec) is the reference RFC 9113 / 7541 |
| 6 | # conformance tester. It connects as a client and runs ~146 cases. This harness: |
| 7 | # 1. builds the V h2spec target server (h2spec_server.v), |
| 8 | # 2. starts it on a free port (TLS + ALPN h2), |
| 9 | # 3. runs a PINNED h2spec binary against it, |
| 10 | # 4. compares the set of FAILED case IDs against h2spec_expected_failures.txt, |
| 11 | # 5. fails only on a regression (a case that newly fails, or a recorded failure |
| 12 | # that now passes — so the baseline is kept honest and shrinks over time). |
| 13 | # |
| 14 | # h2spec is NOT installed here. Provide it via $H2SPEC_BIN or on PATH; CI fetches |
| 15 | # a pinned release (see .github/workflows/h2spec.yml). This script never runs |
| 16 | # `go install` — the version must be pinned and reproducible. |
| 17 | # |
| 18 | # Env: |
| 19 | # VEXE path to the V compiler (default: ./v then ./vnew) |
| 20 | # H2SPEC_BIN path to the h2spec binary (default: h2spec on PATH) |
| 21 | # H2SPEC_PORT port for the target server (default: 18443) |
| 22 | # H2SPEC_TIMEOUT h2spec per-case timeout, seconds (default: 5). Several h2spec |
| 23 | # cases wait for the server's GOAWAY/close and FLAKE at h2spec's |
| 24 | # 2s default under load; 5s is comfortably stable here. Raise it |
| 25 | # if a timing case still flakes on a slow runner. |
| 26 | # VFLAGS_CC extra V flags, e.g. '-cc gcc' (mbedtls needs a real C compiler) |
| 27 | |
| 28 | set -uo pipefail |
| 29 | orig_pwd="$(pwd)" # the caller's cwd, captured BEFORE we cd into the script dir |
| 30 | cd "$(dirname "$0")" |
| 31 | here="$(pwd)" |
| 32 | repo_root="$(cd ../../../.. && pwd)" |
| 33 | |
| 34 | # resolve a caller-supplied path argument: an absolute path or a bare command name |
| 35 | # (PATH lookup, e.g. "h2spec") is used as-is; a relative path is resolved against |
| 36 | # the caller's original cwd, since we cd'd into the script dir above. Without this, |
| 37 | # a documented value like `VEXE=./vnew` (relative to the repo root) would wrongly |
| 38 | # resolve to vlib/net/http/h2spec/vnew and the build would fail. (Codex P1/P3.) |
| 39 | resolve_path() { |
| 40 | case "$1" in |
| 41 | /*) printf '%s' "$1" ;; # absolute path |
| 42 | */*) printf '%s' "$orig_pwd/$1" ;; # relative path containing a slash |
| 43 | *) printf '%s' "$1" ;; # bare name -> PATH lookup |
| 44 | esac |
| 45 | } |
| 46 | |
| 47 | VEXE="${VEXE:-}" |
| 48 | if [ -z "$VEXE" ]; then |
| 49 | if [ -x "$repo_root/v" ]; then VEXE="$repo_root/v"; elif [ -x "$repo_root/vnew" ]; then VEXE="$repo_root/vnew"; else VEXE="v"; fi |
| 50 | else |
| 51 | VEXE="$(resolve_path "$VEXE")" |
| 52 | fi |
| 53 | H2SPEC_BIN="$(resolve_path "${H2SPEC_BIN:-h2spec}")" |
| 54 | PORT="${H2SPEC_PORT:-18443}" |
| 55 | TIMEOUT="${H2SPEC_TIMEOUT:-5}" |
| 56 | SERVER_BIN="$here/h2spec_server.bin" |
| 57 | EXPECTED="$here/h2spec_expected_failures.txt" |
| 58 | |
| 59 | if ! command -v "$H2SPEC_BIN" >/dev/null 2>&1 && [ ! -x "$H2SPEC_BIN" ]; then |
| 60 | echo "ERROR: h2spec not found (set H2SPEC_BIN or put it on PATH; CI fetches a pinned release)." >&2 |
| 61 | exit 2 |
| 62 | fi |
| 63 | |
| 64 | echo "==> building h2spec target server with $VEXE ${VFLAGS_CC:-}" |
| 65 | # shellcheck disable=SC2086 |
| 66 | "$VEXE" ${VFLAGS_CC:-} -o "$SERVER_BIN" "$here/h2spec_server.v" || { echo "build failed" >&2; exit 1; } |
| 67 | |
| 68 | cleanup() { [ -n "${SRV_PID:-}" ] && kill "$SRV_PID" 2>/dev/null; rm -f "$SERVER_BIN"; } |
| 69 | trap cleanup EXIT |
| 70 | |
| 71 | echo "==> starting target on 127.0.0.1:$PORT" |
| 72 | H2SPEC_PORT="$PORT" "$SERVER_BIN" & |
| 73 | SRV_PID=$! |
| 74 | |
| 75 | # Wait for the port to accept connections (up to ~10s). |
| 76 | for _ in $(seq 1 100); do |
| 77 | if (exec 3<>"/dev/tcp/127.0.0.1/$PORT") 2>/dev/null; then exec 3>&- 3<&-; break; fi |
| 78 | kill -0 "$SRV_PID" 2>/dev/null || { echo "server exited early" >&2; exit 1; } |
| 79 | sleep 0.1 |
| 80 | done |
| 81 | |
| 82 | echo "==> running h2spec (TLS, insecure cert OK)" |
| 83 | # -t TLS, -k skip cert verification (self-signed). Parse the JUnit XML report — |
| 84 | # its <testcase classname="..." name="..."> identifiers are h2spec's stable |
| 85 | # contract, unlike the pretty console output. A failing case has a <failure> |
| 86 | # (or <error>) child; we use "classname :: name" as the stable case ID. |
| 87 | report_xml="$here/h2spec_report.xml" |
| 88 | "$H2SPEC_BIN" -t -k -h 127.0.0.1 -p "$PORT" --timeout "$TIMEOUT" --junit-report "$report_xml" 2>&1 | tail -40 || true |
| 89 | if [ ! -s "$report_xml" ]; then |
| 90 | echo "ERROR: h2spec produced no JUnit report ($report_xml); check the flags/version." >&2 |
| 91 | exit 2 |
| 92 | fi |
| 93 | # One <testcase ...> ... </testcase> per line is not guaranteed, so track state. |
| 94 | awk ' |
| 95 | /<testcase /{ pkg=""; cls=""; fail=0 |
| 96 | if (match($0, /package="[^"]*"/)) { pkg=substr($0,RSTART+9, RLENGTH-10) } |
| 97 | if (match($0, /classname="[^"]*"/)) { cls=substr($0,RSTART+11,RLENGTH-12) } |
| 98 | if ($0 ~ /<failure|<error/) fail=1 } |
| 99 | /<failure|<error/{ fail=1 } |
| 100 | /<\/testcase>/{ if (pkg!="" && fail) print pkg " :: " cls; pkg=""; cls=""; fail=0 } |
| 101 | ' "$report_xml" | sed 's/"/"/g; s/"/"/g; s/&/\&/g; s/</</g; s/>/>/g' \ |
| 102 | | sort -u > "$here/h2spec_failed_now.txt" |
| 103 | |
| 104 | grep -vE '^\s*#|^\s*$' "$EXPECTED" 2>/dev/null | sort -u > "$here/h2spec_expected.norm.txt" || true |
| 105 | |
| 106 | new_failures="$(comm -23 "$here/h2spec_failed_now.txt" "$here/h2spec_expected.norm.txt" | grep -vE '^\s*$' || true)" |
| 107 | now_passing="$(comm -13 "$here/h2spec_failed_now.txt" "$here/h2spec_expected.norm.txt" | grep -vE '^\s*$' || true)" |
| 108 | |
| 109 | rc=0 |
| 110 | if [ -n "$new_failures" ]; then |
| 111 | echo "::error:: h2spec REGRESSION — these cases newly fail:" >&2 |
| 112 | echo "$new_failures" >&2 |
| 113 | rc=1 |
| 114 | fi |
| 115 | if [ -n "$now_passing" ]; then |
| 116 | # Fail too (not just warn): a recorded failure that now passes must be removed |
| 117 | # from the baseline, otherwise the gate keeps allowing that case and a later |
| 118 | # PR can reintroduce the failure unnoticed. (Codex P2.) The 37 baseline cases |
| 119 | # are deterministic at H2SPEC_TIMEOUT=5, so this does not flap. |
| 120 | echo "::error:: these recorded failures now PASS — remove them from h2spec_expected_failures.txt to keep the gate honest:" >&2 |
| 121 | echo "$now_passing" >&2 |
| 122 | rc=1 |
| 123 | fi |
| 124 | [ "$rc" -eq 0 ] && echo "==> h2spec: no regressions vs baseline" |
| 125 | exit "$rc" |
| 126 | |