#!/usr/bin/env bash # nc-regression.sh — Example 6 orchestrator: end-to-end regression testing # between two Cloverleaf environments. # # Phases: # 1. discover → list inbound threads in scope (uses nc-find-inbound) # 2. sample → grab N messages per inbound from env-A smatdb → input .msgs files # 3. route-A → run route_test on env-A for each inbound → captured outputs/env-a/... # 4. route-B → run route_test on env-B with same inputs → captured outputs/env-b/... # 5. diff → hl7-diff every paired output file with --ignore MSH.7 → per-pair report # 6. summary → one master regression-summary.md compiling everything # # Phases 3 and 4 require Cloverleaf's route_test command on each box. The # command is parameterized via --route-test-cmd with placeholders: # {THREAD} → the inbound thread name # {INPUT} → absolute path to the .msgs input file # {OUTPUT_DIR} → absolute path where output files should land # {HCIROOT} → the env's HCIROOT # {HCISITE} → the env's HCISITE # Default: not set — you must pass it once for your shop's invocation pattern. # # A common pattern: a wrapper script that sources the Cloverleaf profile and # runs ` route_test `, with output redirected to OUTPUT_DIR. # Example template you might pass: # --route-test-cmd 'cd {HCIROOT}/{HCISITE} && . ./.profile && {THREAD} route_test {INPUT} && cp *.out.* {OUTPUT_DIR}/' # # Usage: # nc-regression.sh --scope # --count N # --env-a HCIROOT_A --site-a SITENAME # --env-b HCIROOT_B --site-b SITENAME # --out DIR # --route-test-cmd 'TEMPLATE' # [--ignore "FIELDS"] # [--include-fields "FIELDS"] # [--phase 1|2|3|4|5|6|all] # [--dry-run] # [--inbound-mode tcp-listen|icl-or-file|all] # [--env-b-host HOST] [--env-b-user USER] # for scp of inputs # # Scope formats: # thread:NAME one specific thread # threads:N1,N2,N3 comma-separated list # site every inbound in the configured site # server every inbound in every site under HCIROOT set -o pipefail NC_SELF="$0" LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)" NCP="$LIB_DIR/nc-parse.sh" NCI="$LIB_DIR/nc-inbound.sh" NCM="$LIB_DIR/nc-msgs.sh" HL7DIFF="$LIB_DIR/hl7-diff.sh" die() { printf 'nc-regression: %s\n' "$*" >&2; exit 1; } say() { printf 'nc-regression: %s\n' "$*" >&2; } SCOPE="" COUNT=10 ENV_A="" SITE_A="" ENV_B="" SITE_B="" OUT="" ROUTE_TEST_CMD="" IGNORE="MSH.7" INCLUDE="" PHASE="all" DRY_RUN=0 INBOUND_MODE="all" ENV_B_HOST="" ENV_B_USER="" while [ $# -gt 0 ]; do case "$1" in --scope) shift; SCOPE="$1" ;; --count) shift; COUNT="$1" ;; --env-a) shift; ENV_A="$1" ;; --site-a) shift; SITE_A="$1" ;; --env-b) shift; ENV_B="$1" ;; --site-b) shift; SITE_B="$1" ;; --out) shift; OUT="$1" ;; --route-test-cmd) shift; ROUTE_TEST_CMD="$1" ;; --ignore) shift; IGNORE="$1" ;; --include-fields) shift; INCLUDE="$1" ;; --phase) shift; PHASE="$1" ;; --dry-run) DRY_RUN=1 ;; --inbound-mode) shift; INBOUND_MODE="$1" ;; --env-b-host) shift; ENV_B_HOST="$1" ;; --env-b-user) shift; ENV_B_USER="$1" ;; -h|--help) sed -n '2,55p' "$NC_SELF"; exit 0 ;; -*) die "unknown flag: $1" ;; *) die "extra arg: $1" ;; esac shift done [ -n "$SCOPE" ] || die "missing --scope (thread:NAME | threads:N1,N2 | site | server)" [ -n "$ENV_A" ] || die "missing --env-a HCIROOT_A" [ -n "$ENV_B" ] || die "missing --env-b HCIROOT_B" [ -n "$OUT" ] || die "missing --out DIR" [ -d "$ENV_A" ] || die "env-a is not a directory: $ENV_A" case "$PHASE" in 1|2|3|4|5|6|all) ;; *) die "bad --phase" ;; esac [ "$DRY_RUN" = "1" ] || [ -n "$ROUTE_TEST_CMD" ] || say "WARNING: --route-test-cmd is unset; phases 3 and 4 will be skipped (you can run them manually using the generated input files)" mkdir -p "$OUT" "$OUT/inputs" "$OUT/outputs/env-a" "$OUT/outputs/env-b" "$OUT/diff" 2>/dev/null # ───────────────────────────────────────────────────────────────────────────── # Phase 1: discover inbound threads in scope # ───────────────────────────────────────────────────────────────────────────── discover_inbounds() { case "$SCOPE" in thread:*) echo "${SCOPE#thread:}" ;; threads:*) echo "${SCOPE#threads:}" | tr ',' '\n' ;; site) [ -n "$SITE_A" ] || die "scope=site requires --site-a" "$NCI" "$ENV_A/$SITE_A/NetConfig" --mode "$INBOUND_MODE" --format tsv \ | awk -F'\t' 'NR>1 {print $1}' ;; server) while IFS= read -r nc; do "$NCI" "$nc" --mode "$INBOUND_MODE" --format tsv \ | awk -F'\t' 'NR>1 {print $1}' done < <(find "$ENV_A" -maxdepth 2 -name NetConfig -type f 2>/dev/null) ;; *) die "bad --scope: $SCOPE" ;; esac } phase_1() { say "=== PHASE 1: discover inbounds ===" local inbounds; inbounds=$(discover_inbounds | sort -u | grep -v '^$') if [ -z "$inbounds" ]; then say "no inbounds found in scope $SCOPE" return 1 fi printf '%s\n' "$inbounds" > "$OUT/inbounds.txt" local count; count=$(printf '%s\n' "$inbounds" | wc -l | tr -d ' ') say "discovered $count inbound thread(s) → $OUT/inbounds.txt" printf '%s\n' "$inbounds" | sed 's/^/ - /' } # ───────────────────────────────────────────────────────────────────────────── # Phase 2: sample N messages per inbound from env-A's smatdbs # ───────────────────────────────────────────────────────────────────────────── phase_2() { say "=== PHASE 2: sample $COUNT messages per inbound from env-A ===" [ -f "$OUT/inbounds.txt" ] || { say "no inbounds file from phase 1; running phase 1 first"; phase_1 || return 1; } local thread sitedir local count=0 while IFS= read -r thread; do [ -z "$thread" ] && continue # Locate the site for this thread sitedir="" if [ -n "$SITE_A" ]; then sitedir="$ENV_A/$SITE_A" else # Search across all sites under ENV_A for the smatdb sitedir=$(find "$ENV_A" -maxdepth 4 -name "${thread}.smatdb" -type f 2>/dev/null | head -1 | xargs -I{} dirname {} | sed "s#/exec/processes/.*##") [ -z "$sitedir" ] && { say " skip $thread: smatdb not found under $ENV_A"; continue; } fi local input="$OUT/inputs/${thread}.msgs" if [ "$DRY_RUN" = "1" ]; then say " [dry-run] would sample $COUNT msgs from $thread → $input" else HCISITEDIR="$sitedir" "$NCM" "$thread" --limit "$COUNT" --format raw > "$input" 2>/dev/null local got; got=$(tr -cd $'\x1c' < "$input" | wc -c | tr -d ' ') say " sampled $thread → $input ($got messages)" fi count=$((count+1)) done < "$OUT/inbounds.txt" say "phase 2 done: $count thread(s) processed" } # ───────────────────────────────────────────────────────────────────────────── # Phase 3 / 4: execute route_test on each env # ───────────────────────────────────────────────────────────────────────────── render_cmd() { local tmpl="$1" thread="$2" input="$3" outdir="$4" hciroot="$5" hcisite="$6" local cmd="$tmpl" cmd="${cmd//\{THREAD\}/$thread}" cmd="${cmd//\{INPUT\}/$input}" cmd="${cmd//\{OUTPUT_DIR\}/$outdir}" cmd="${cmd//\{HCIROOT\}/$hciroot}" cmd="${cmd//\{HCISITE\}/$hcisite}" printf '%s' "$cmd" } phase_routes() { local label="$1" hciroot="$2" hcisite="$3" say "=== PHASE ${label}: route_test on env-${label} ===" if [ -z "$ROUTE_TEST_CMD" ]; then say "no --route-test-cmd; skipping phase ${label}" say "to run manually, use the input files at $OUT/inputs/*.msgs" return 0 fi local thread while IFS= read -r thread; do [ -z "$thread" ] && continue local input="$OUT/inputs/${thread}.msgs" local outdir="$OUT/outputs/env-${label}/${thread}" [ -f "$input" ] || { say " skip $thread: no input file"; continue; } mkdir -p "$outdir" local cmd; cmd=$(render_cmd "$ROUTE_TEST_CMD" "$thread" "$input" "$outdir" "$hciroot" "$hcisite") if [ "$DRY_RUN" = "1" ]; then say " [dry-run] $thread:" say " \$ $cmd" else say " $thread:" say " \$ $cmd" bash -c "$cmd" 2>&1 | sed 's/^/ /' || say " (route_test exit non-zero — continuing)" fi done < "$OUT/inbounds.txt" } phase_3() { phase_routes "a" "$ENV_A" "$SITE_A"; } phase_4() { # Phase 4: copy inputs to env-b host (if remote), then route_test on env-B. if [ -n "$ENV_B_HOST" ]; then say "copying input files to ${ENV_B_USER:-$USER}@${ENV_B_HOST}:${OUT}/inputs/" if [ "$DRY_RUN" = "1" ]; then say " [dry-run] scp -r $OUT/inputs/ ${ENV_B_USER:-$USER}@${ENV_B_HOST}:${OUT}/" else ssh "${ENV_B_USER:-$USER}@${ENV_B_HOST}" "mkdir -p $OUT/inputs $OUT/outputs/env-b" || true scp -r "$OUT/inputs/" "${ENV_B_USER:-$USER}@${ENV_B_HOST}:${OUT}/" || say "scp failed; you'll need to copy manually" fi fi phase_routes "b" "$ENV_B" "$SITE_B" } # ───────────────────────────────────────────────────────────────────────────── # Phase 5: diff outputs pair-by-pair # ───────────────────────────────────────────────────────────────────────────── phase_5() { say "=== PHASE 5: diff env-a vs env-b outputs ===" local diff_index="$OUT/diff/_index.md" { printf '# Regression diff index\n\n' printf '- env-A: `%s`\n- env-B: `%s`\n- scope: `%s`\n- count: %s msgs per inbound\n- ignore: `%s`\n%s\n\n' \ "$ENV_A" "$ENV_B" "$SCOPE" "$COUNT" "$IGNORE" \ "$([ -n "$INCLUDE" ] && printf -- '- include-only: `%s`' "$INCLUDE")" printf '| thread | dest | diffs | report |\n|---|---|---|---|\n' } > "$diff_index" local total_diff=0 total_pairs=0 local thread destfile destname while IFS= read -r thread; do [ -z "$thread" ] && continue local a_dir="$OUT/outputs/env-a/${thread}" local b_dir="$OUT/outputs/env-b/${thread}" [ -d "$a_dir" ] || { say " skip $thread: no env-a outputs"; continue; } [ -d "$b_dir" ] || { say " skip $thread: no env-b outputs"; continue; } # Pair up by filename while IFS= read -r destfile; do destname=$(basename "$destfile") local b_pair="$b_dir/$destname" total_pairs=$((total_pairs+1)) if [ ! -f "$b_pair" ]; then echo "| \`$thread\` | \`$destname\` | (missing on env-b) | — |" >> "$diff_index" continue fi local report="$OUT/diff/${thread}.${destname}.md" local count if [ "$DRY_RUN" = "1" ]; then echo " [dry-run] would diff $destfile vs $b_pair → $report" echo "| \`$thread\` | \`$destname\` | [dry-run] | — |" >> "$diff_index" continue fi local diff_args=(--ignore "$IGNORE") [ -n "$INCLUDE" ] && diff_args+=(--include-fields "$INCLUDE") count=$("$HL7DIFF" "${diff_args[@]}" --format count "$destfile" "$b_pair" 2>/dev/null || echo "?") { printf '# Diff: %s → %s\n\n- env-A: `%s`\n- env-B: `%s`\n\n' "$thread" "$destname" "$destfile" "$b_pair" "$HL7DIFF" "${diff_args[@]}" --format text "$destfile" "$b_pair" 2>/dev/null || true } > "$report" echo "| \`$thread\` | \`$destname\` | $count | [report](./$(basename "$report")) |" >> "$diff_index" total_diff=$((total_diff + count)) say " $thread → $destname: $count diff(s)" done < <(find "$a_dir" -maxdepth 1 -type f 2>/dev/null) done < "$OUT/inbounds.txt" { printf '\n## Summary\n\n- pairs compared: %s\n- total field differences (post-ignore): %s\n' "$total_pairs" "$total_diff" } >> "$diff_index" say "phase 5 done: $total_pairs pairs compared, $total_diff total diffs" say "index: $diff_index" } # ───────────────────────────────────────────────────────────────────────────── # Phase 6: master summary # ───────────────────────────────────────────────────────────────────────────── phase_6() { say "=== PHASE 6: master regression-summary.md ===" local summary="$OUT/regression-summary.md" { printf '# Regression test summary\n\n' printf 'Generated: %s\n\n' "$(date -Iseconds 2>/dev/null || date)" printf '## Configuration\n\n' printf '- scope: `%s`\n' "$SCOPE" printf '- count: %s messages per inbound\n' "$COUNT" printf '- env-A: `%s` (site=%s)\n' "$ENV_A" "${SITE_A:-auto}" printf '- env-B: `%s` (site=%s)\n' "$ENV_B" "${SITE_B:-auto}" printf '- ignore: `%s`\n' "$IGNORE" [ -n "$INCLUDE" ] && printf '- include-only: `%s`\n' "$INCLUDE" printf '\n## Inbounds tested\n\n' [ -f "$OUT/inbounds.txt" ] && awk '{print "- `" $0 "`"}' "$OUT/inbounds.txt" printf '\n## Inputs\n\n' find "$OUT/inputs" -maxdepth 1 -type f 2>/dev/null \ | awk '{print "- `" $0 "`"}' || true printf '\n## Output directories\n\n' printf -- '- env-A: `%s`\n' "$OUT/outputs/env-a" printf -- '- env-B: `%s`\n' "$OUT/outputs/env-b" printf '\n## Diff details\n\n' printf 'See [diff/_index.md](./diff/_index.md) for the per-pair table.\n' } > "$summary" say "summary: $summary" } # ───────────────────────────────────────────────────────────────────────────── # Dispatch # ───────────────────────────────────────────────────────────────────────────── case "$PHASE" in 1) phase_1 ;; 2) phase_1 && phase_2 ;; 3) phase_3 ;; 4) phase_4 ;; 5) phase_5 ;; 6) phase_6 ;; all) phase_1 && phase_2 && phase_3 && phase_4 && phase_5 && phase_6 ;; esac say "regression run done. Output root: $OUT"