|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Post-record assertions for the v3 Postgres recorder against the |
| 3 | +# postgres-wire-features sample. Exits non-zero on the first violation |
| 4 | +# so the Woodpecker step shows a clean pass/fail. |
| 5 | +# |
| 6 | +# These assertions codify invariants that the *fixed* v3 recorder must |
| 7 | +# hold. A broken v3 (for example origin/main today) produces mocks that |
| 8 | +# violate at least one of them: empty-query ghost invocations with |
| 9 | +# class=UNKNOWN, or a multi-statement Q packet collapsed into one |
| 10 | +# invocation, or bind values emitted as base64 when the raw bytes are |
| 11 | +# valid UTF-8 text. |
| 12 | +# |
| 13 | +# Usage: ./assertions.sh [mocks_dir] |
| 14 | +# default mocks_dir: ./keploy |
| 15 | + |
| 16 | +set -euo pipefail |
| 17 | + |
| 18 | +MOCKS_DIR="${1:-./keploy}" |
| 19 | + |
| 20 | +if [[ ! -d "$MOCKS_DIR" ]]; then |
| 21 | + echo "assertions: mocks directory not found at $MOCKS_DIR" >&2 |
| 22 | + exit 2 |
| 23 | +fi |
| 24 | + |
| 25 | +# Collect every mocks.yaml beneath $MOCKS_DIR. Keploy writes one |
| 26 | +# mocks.yaml per test-set. |
| 27 | +mapfile -t MOCK_FILES < <(find "$MOCKS_DIR" -type f -name 'mocks.yaml' | sort) |
| 28 | +if [[ ${#MOCK_FILES[@]} -eq 0 ]]; then |
| 29 | + echo "assertions: no mocks.yaml found under $MOCKS_DIR" >&2 |
| 30 | + exit 2 |
| 31 | +fi |
| 32 | + |
| 33 | +echo "assertions: scanning ${#MOCK_FILES[@]} mocks.yaml file(s) under $MOCKS_DIR" |
| 34 | + |
| 35 | +fail() { |
| 36 | + echo "FAIL: $1" >&2 |
| 37 | + exit 1 |
| 38 | +} |
| 39 | + |
| 40 | +pass() { |
| 41 | + echo "PASS: $1" |
| 42 | +} |
| 43 | + |
| 44 | +# ------------------------------------------------------------------ |
| 45 | +# Invariant 1: no invocation is classified as UNKNOWN. |
| 46 | +# Origin/main emits class=UNKNOWN for ghost events and for statements |
| 47 | +# its classifier does not recognize. |
| 48 | +# ------------------------------------------------------------------ |
| 49 | +unknown_count=0 |
| 50 | +for f in "${MOCK_FILES[@]}"; do |
| 51 | + c=$(grep -c -E '^\s*class:\s*UNKNOWN\s*$' "$f" || true) |
| 52 | + unknown_count=$((unknown_count + c)) |
| 53 | +done |
| 54 | +if [[ $unknown_count -ne 0 ]]; then |
| 55 | + echo "--- first few UNKNOWN hits ---" |
| 56 | + grep -n -E '^\s*class:\s*UNKNOWN\s*$' "${MOCK_FILES[@]}" | head -n 10 || true |
| 57 | + fail "found $unknown_count invocation(s) with class: UNKNOWN" |
| 58 | +fi |
| 59 | +pass "no class: UNKNOWN invocations" |
| 60 | + |
| 61 | +# ------------------------------------------------------------------ |
| 62 | +# Invariant 2: no ghost invocation with an empty sqlNormalized. |
| 63 | +# A correct recorder suppresses EmptyQueryResponse paths instead of |
| 64 | +# emitting an invocation shell with sqlNormalized: "". |
| 65 | +# ------------------------------------------------------------------ |
| 66 | +empty_sql_count=0 |
| 67 | +for f in "${MOCK_FILES[@]}"; do |
| 68 | + # Match both sqlNormalized: "" and sqlNormalized: |
| 69 | + c=$(grep -c -E '^\s*sqlNormalized:\s*("")?\s*$' "$f" || true) |
| 70 | + empty_sql_count=$((empty_sql_count + c)) |
| 71 | +done |
| 72 | +if [[ $empty_sql_count -ne 0 ]]; then |
| 73 | + echo "--- first few empty sqlNormalized hits ---" |
| 74 | + grep -n -E '^\s*sqlNormalized:\s*("")?\s*$' "${MOCK_FILES[@]}" | head -n 10 || true |
| 75 | + fail "found $empty_sql_count invocation(s) with empty sqlNormalized (ghost event)" |
| 76 | +fi |
| 77 | +pass "no empty sqlNormalized invocations" |
| 78 | + |
| 79 | +# ------------------------------------------------------------------ |
| 80 | +# Invariant 3: the multistatement Q packet produced distinct |
| 81 | +# invocations for each of its four statements. |
| 82 | +# |
| 83 | +# The sample sends `BEGIN; SELECT 1 AS a; SELECT 2 AS b; COMMIT` |
| 84 | +# as a single simple-Query packet. A recorder that splits the batch |
| 85 | +# with pg_query emits four invocations with distinct sqlNormalized |
| 86 | +# fragments; a recorder that tracks per-packet emits one. |
| 87 | +# ------------------------------------------------------------------ |
| 88 | +begin_count=0 |
| 89 | +commit_count=0 |
| 90 | +select_a_count=0 |
| 91 | +select_b_count=0 |
| 92 | +for f in "${MOCK_FILES[@]}"; do |
| 93 | + begin_count=$((begin_count + $(grep -c -E '^\s*sqlNormalized:\s*["'"'"']?BEGIN["'"'"']?\s*$' "$f" || true))) |
| 94 | + commit_count=$((commit_count + $(grep -c -E '^\s*sqlNormalized:\s*["'"'"']?COMMIT["'"'"']?\s*$' "$f" || true))) |
| 95 | + select_a_count=$((select_a_count + $(grep -c -E 'sqlNormalized:.*SELECT\s+\$1\s+AS\s+a' "$f" || true))) |
| 96 | + select_b_count=$((select_b_count + $(grep -c -E 'sqlNormalized:.*SELECT\s+\$1\s+AS\s+b' "$f" || true))) |
| 97 | +done |
| 98 | +if [[ $begin_count -lt 1 || $commit_count -lt 1 || $select_a_count -lt 1 || $select_b_count -lt 1 ]]; then |
| 99 | + echo " BEGIN=$begin_count COMMIT=$commit_count SELECT_a=$select_a_count SELECT_b=$select_b_count" |
| 100 | + fail "multistatement Q packet was not split into 4 invocations (expected >=1 each of BEGIN, COMMIT, SELECT ... AS a, SELECT ... AS b)" |
| 101 | +fi |
| 102 | +pass "multistatement Q packet split into distinct invocations (BEGIN=$begin_count, COMMIT=$commit_count, SELECT a=$select_a_count, SELECT b=$select_b_count)" |
| 103 | + |
| 104 | +# ------------------------------------------------------------------ |
| 105 | +# Invariant 4: text bind values are emitted as plain YAML strings |
| 106 | +# (not wrapped in a !!binary tag) when the raw bytes are UTF-8 safe. |
| 107 | +# The prepare/execute scenario's bind contains ASCII integers, and |
| 108 | +# the COPY IN scenario's payload is ASCII text — both should survive |
| 109 | +# as readable strings. |
| 110 | +# ------------------------------------------------------------------ |
| 111 | +binary_tag_count=0 |
| 112 | +for f in "${MOCK_FILES[@]}"; do |
| 113 | + c=$(grep -c -E '!!binary' "$f" || true) |
| 114 | + binary_tag_count=$((binary_tag_count + c)) |
| 115 | +done |
| 116 | +echo " info: !!binary tag occurrences across mocks = $binary_tag_count" |
| 117 | +# We don't fail on this count (truly-binary values like lsn/xid bytes |
| 118 | +# legitimately round-trip as !!binary); instead assert that a known- |
| 119 | +# textual bind ('seed-a' from COPY IN) appears as a plain string. |
| 120 | +if ! grep -R -l -E "seed-a" "$MOCKS_DIR" >/dev/null 2>&1; then |
| 121 | + # seed-a might only appear post-COPY; tolerate its absence, but |
| 122 | + # require that at least one readable SQL fragment is present so |
| 123 | + # we know the file isn't entirely base64-encoded. |
| 124 | + if ! grep -R -l -E 'sqlNormalized:.*SELECT' "$MOCKS_DIR" >/dev/null 2>&1; then |
| 125 | + fail "no readable sqlNormalized values found — every field may be base64 encoded" |
| 126 | + fi |
| 127 | +fi |
| 128 | +pass "readable text fields are present in mocks (not exclusively base64)" |
| 129 | + |
| 130 | +echo "assertions: all invariants satisfied" |
0 commit comments