v4 / vlib / net / http / h2spec / run_h2spec.sh
125 lines · 113 sloc · 5.86 KB · 19d2c622078c21ad9a14cfa7a30fdd42f67cd01c
Raw
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
28set -uo pipefail
29orig_pwd="$(pwd)" # the caller's cwd, captured BEFORE we cd into the script dir
30cd "$(dirname "$0")"
31here="$(pwd)"
32repo_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.)
39resolve_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
47VEXE="${VEXE:-}"
48if [ -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
50else
51 VEXE="$(resolve_path "$VEXE")"
52fi
53H2SPEC_BIN="$(resolve_path "${H2SPEC_BIN:-h2spec}")"
54PORT="${H2SPEC_PORT:-18443}"
55TIMEOUT="${H2SPEC_TIMEOUT:-5}"
56SERVER_BIN="$here/h2spec_server.bin"
57EXPECTED="$here/h2spec_expected_failures.txt"
58
59if ! 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
62fi
63
64echo "==> 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
68cleanup() { [ -n "${SRV_PID:-}" ] && kill "$SRV_PID" 2>/dev/null; rm -f "$SERVER_BIN"; }
69trap cleanup EXIT
70
71echo "==> starting target on 127.0.0.1:$PORT"
72H2SPEC_PORT="$PORT" "$SERVER_BIN" &
73SRV_PID=$!
74
75# Wait for the port to accept connections (up to ~10s).
76for _ 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
80done
81
82echo "==> 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.
87report_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
89if [ ! -s "$report_xml" ]; then
90 echo "ERROR: h2spec produced no JUnit report ($report_xml); check the flags/version." >&2
91 exit 2
92fi
93# One <testcase ...> ... </testcase> per line is not guaranteed, so track state.
94awk '
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
104grep -vE '^\s*#|^\s*$' "$EXPECTED" 2>/dev/null | sort -u > "$here/h2spec_expected.norm.txt" || true
105
106new_failures="$(comm -23 "$here/h2spec_failed_now.txt" "$here/h2spec_expected.norm.txt" | grep -vE '^\s*$' || true)"
107now_passing="$(comm -13 "$here/h2spec_failed_now.txt" "$here/h2spec_expected.norm.txt" | grep -vE '^\s*$' || true)"
108
109rc=0
110if [ -n "$new_failures" ]; then
111 echo "::error:: h2spec REGRESSION — these cases newly fail:" >&2
112 echo "$new_failures" >&2
113 rc=1
114fi
115if [ -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
123fi
124[ "$rc" -eq 0 ] && echo "==> h2spec: no regressions vs baseline"
125exit "$rc"
126