cloverleaf-larry/lib/hl7-diff-set.sh
Bryan Johnson 4828862a8e v0.9.10: hl7-diff-set (batch route-set compare) + hl7-diff --format text readability (F-8)
- hl7-diff-set: compare a whole set of Windows-vs-Linux regression outputs in one
  command — takes two prefixes (+ optional --dir), auto-discovers routes by glob,
  runs hl7-diff per pair (--framing auto --ignore MSH.7, overridable), reports
  per-route PASS/DIFF/MISSING/EXTRA + summary, exit 0 iff all clean (MISSING and
  EXTRA both count as regressions). No more hand-written for-loop with hardcoded names.
- F-8: hl7-diff --format text now shows SEG.FIELD left='...' right='...' with visible
  (empty)/<absent> tokens so a dropped component (e.g. Linux MSH.9.3 absent vs Windows
  ADT_A02) is obvious without opening the message files; --------- message N header now
  uses the real message index (was a stale counter). tsv/count formats unchanged.

Consolidated by Clover; Vera CODE-PASS (Deliverables/2026-07-23-cloverleaf-larry-v0910-hl7-diff-set.md) — own-fixture verification of all route classifications + the absent-token render + F-7 no-hang. Live-acceptance = Bryan's regression run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 11:15:24 -07:00

207 lines
7.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# hl7-diff-set.sh — run hl7-diff over a WHOLE SET of paired Windows-vs-Linux
# (or any baseline-vs-candidate) regression output files in one command,
# instead of hand-writing a for-loop with hardcoded route names.
#
# Built for hciroutetest's real on-disk layout (Bryan-direct, 2026-07-23):
# one directory holding paired files named <prefix>.<route>, where TWO
# prefixes (a baseline/"windows" set and a candidate/"linux" set) share a set
# of route suffixes after the last dot, e.g.:
# adt_in_regression_out_windows_single.adt_process_out
# adt_in_regression_out_linux_single.adt_process_out
# adt_in_regression_out_windows_single.lab_adt_out
# adt_in_regression_out_linux_single.lab_adt_out
# ...
#
# Usage:
# hl7-diff-set.sh <baseline-prefix> <candidate-prefix>
# [--dir PATH] # directory holding the paired files (default: .)
# [--ignore "FIELD,..."] # forwarded to hl7-diff. Default: MSH.7
# [--include-fields "F,.."] # forwarded to hl7-diff (overrides --ignore for that set)
# [--framing auto|nl|0x1c|len10] # forwarded to hl7-diff. Default: auto
# [--format text|tsv] # how THIS command reports. Default: text
#
# hl7-diff-set.sh adt_in_regression_out_windows_single adt_in_regression_out_linux_single \
# --dir /opt/cloverleaf/cis2025.01/integrator/adt/data/regression_testing/single
#
# What it does:
# 1. Globs "<dir>/<baseline-prefix>.*", derives each route suffix (the part
# after "<baseline-prefix>."), and pairs it with "<dir>/<candidate-prefix>.<suffix>".
# Also globs "<dir>/<candidate-prefix>.*" so a candidate-only route (no
# baseline file) is found too — nothing is silently skipped.
# 2. Runs the existing lib/hl7-diff.sh on every matched pair (this does NOT
# reimplement diffing — same engine, same --ignore/--framing semantics).
# 3. Reports PASS/DIFF/MISSING/EXTRA per route, plus a one-line summary.
#
# Per-route outcomes:
# PASS — both files present, 0 field differences (post-ignore)
# DIFF — both files present, N>0 field differences (--format text also
# shows the full hl7-diff --format text detail, indented)
# MISSING — baseline route has no matching candidate file (a dropped output
# IS a regression — this counts as a failure)
# EXTRA — candidate has a route the baseline does not (reported, also
# counts as non-clean — never silently skipped)
#
# --format text (default) prints a human report to stdout: per-route lines,
# DIFF detail indented beneath, then the summary line.
# --format tsv prints ONE machine row per route to stdout:
# route<TAB>status<TAB>diff_count
# (diff_count is "-" for MISSING/EXTRA, since no diff was actually run) —
# the summary line still prints, to STDERR, so stdout stays clean tsv.
# Recommended for scripting: hl7-diff.sh's OWN v0.9.9 text-mode message-
# header mislabel is fixed as of this release (F-8), but tsv remains the
# safer machine-parseable choice regardless.
#
# Exit codes: 0 = every route PASS (all clean); 1 = at least one route is
# DIFF/MISSING/EXTRA; 2 = usage/input error (bad flag, --dir missing, or
# neither prefix matched any file at all).
set -o pipefail
NC_SELF="$0"
LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)"
HL7DIFF="$LIB_DIR/hl7-diff.sh"
if [ -r "$LIB_DIR/cygwin-safe.sh" ]; then
# shellcheck disable=SC1090,SC1091
. "$LIB_DIR/cygwin-safe.sh"
else
coerce_int() { local r="${1:-}" d="${2:-0}" c; c=$(printf '%s' "$r" | tr -cd '0-9'); printf '%s' "${c:-$d}"; }
fi
die() { printf 'hl7-diff-set: %s\n' "$*" >&2; exit 2; }
DIR="."
IGNORE="MSH.7"
INCLUDE=""
FRAMING="auto"
FORMAT="text"
BASE_PREFIX=""
CAND_PREFIX=""
while [ $# -gt 0 ]; do
case "$1" in
--dir) shift; DIR="$1" ;;
--ignore) shift; IGNORE="$1" ;;
--include-fields) shift; INCLUDE="$1" ;;
--framing) shift; FRAMING="$1" ;;
--format) shift; FORMAT="$1" ;;
-h|--help) sed -n '2,52p' "$NC_SELF"; exit 0 ;;
-*) die "unknown flag: $1" ;;
*)
if [ -z "$BASE_PREFIX" ]; then BASE_PREFIX="$1"
elif [ -z "$CAND_PREFIX" ]; then CAND_PREFIX="$1"
else die "extra arg: $1"
fi
;;
esac
shift
done
[ -x "$HL7DIFF" ] || [ -f "$HL7DIFF" ] || die "backing tool not found: $HL7DIFF"
[ -n "$BASE_PREFIX" ] && [ -n "$CAND_PREFIX" ] || die "usage: hl7-diff-set.sh <baseline-prefix> <candidate-prefix> [--dir PATH] [--ignore FIELDS] [--framing auto|nl|0x1c|len10] [--format text|tsv] (run --help for full usage)"
[ -d "$DIR" ] || die "no such --dir: $DIR"
case "$FORMAT" in text|tsv) ;; *) die "bad --format (want text|tsv)" ;; esac
case "$FRAMING" in auto|nl|0x1c|len10) ;; *) die "bad --framing (want auto|nl|0x1c|len10)" ;; esac
# --- discovery: glob both prefixes, derive route suffixes -------------------
# Portable (bash 3.2 / MobaXterm-safe) glob: a literal no-match pattern stays
# un-expanded, so every candidate is existence-checked with `[ -e ]` before
# use — no reliance on nullglob (bash-4-leaning / shopt-dependent).
_base_suffixes=()
for _f in "$DIR/$BASE_PREFIX".*; do
[ -e "$_f" ] || continue
_bn="$(basename "$_f")"
_base_suffixes+=("${_bn#"$BASE_PREFIX".}")
done
_cand_suffixes=()
for _f in "$DIR/$CAND_PREFIX".*; do
[ -e "$_f" ] || continue
_bn="$(basename "$_f")"
_cand_suffixes+=("${_bn#"$CAND_PREFIX".}")
done
if [ "${#_base_suffixes[@]}" -eq 0 ] && [ "${#_cand_suffixes[@]}" -eq 0 ]; then
die "no files matched '$BASE_PREFIX.*' or '$CAND_PREFIX.*' in $DIR"
fi
# Union of both suffix lists, sorted + de-duped, for a stable/deterministic
# route order regardless of filesystem glob order.
ROUTES="$(printf '%s\n' "${_base_suffixes[@]}" "${_cand_suffixes[@]}" | grep -v '^$' | sort -u)"
# --- run each pair ------------------------------------------------------------
diff_args=(--ignore "$IGNORE" --framing "$FRAMING")
[ -n "$INCLUDE" ] && diff_args+=(--include-fields "$INCLUDE")
n_routes=0 n_pass=0 n_diff=0 n_missing=0 n_extra=0
if [ "$FORMAT" = "text" ]; then
printf 'hl7-diff-set: baseline=%s candidate=%s dir=%s\n' "$BASE_PREFIX" "$CAND_PREFIX" "$DIR"
printf ' ignore: %s framing: %s\n\n' "$IGNORE" "$FRAMING"
fi
while IFS= read -r route; do
[ -z "$route" ] && continue
n_routes=$((n_routes + 1))
bfile="$DIR/$BASE_PREFIX.$route"
cfile="$DIR/$CAND_PREFIX.$route"
if [ ! -f "$bfile" ]; then
n_extra=$((n_extra + 1))
if [ "$FORMAT" = "tsv" ]; then
printf '%s\tEXTRA\t-\n' "$route"
else
printf ' %-24s EXTRA (candidate-only route — no baseline file: %s)\n' "$route" "$bfile"
fi
continue
fi
if [ ! -f "$cfile" ]; then
n_missing=$((n_missing + 1))
if [ "$FORMAT" = "tsv" ]; then
printf '%s\tMISSING\t-\n' "$route"
else
printf ' %-24s MISSING (no candidate output — dropped route: %s)\n' "$route" "$cfile"
fi
continue
fi
count="$("$HL7DIFF" "${diff_args[@]}" --format count "$bfile" "$cfile" 2>&1)"
count="$(coerce_int "$count" 0)"
if [ "$count" -eq 0 ]; then
n_pass=$((n_pass + 1))
if [ "$FORMAT" = "tsv" ]; then
printf '%s\tPASS\t0\n' "$route"
else
printf ' %-24s PASS\n' "$route"
fi
else
n_diff=$((n_diff + 1))
if [ "$FORMAT" = "tsv" ]; then
printf '%s\tDIFF\t%s\n' "$route" "$count"
else
printf ' %-24s DIFF (%s diff(s))\n' "$route" "$count"
"$HL7DIFF" "${diff_args[@]}" --format text "$bfile" "$cfile" 2>&1 | sed 's/^/ /'
printf '\n'
fi
fi
done <<EOF_ROUTES
$ROUTES
EOF_ROUTES
# --- summary ------------------------------------------------------------------
_summary="$n_routes routes: $n_pass clean, $n_diff differs"
[ "$n_missing" -gt 0 ] && _summary="$_summary, $n_missing missing"
[ "$n_extra" -gt 0 ] && _summary="$_summary, $n_extra extra"
if [ "$FORMAT" = "tsv" ]; then
printf '%s\n' "$_summary" >&2
else
printf '%s\n' "$_summary"
fi
if [ "$n_diff" -eq 0 ] && [ "$n_missing" -eq 0 ] && [ "$n_extra" -eq 0 ] && [ "$n_routes" -gt 0 ]; then
exit 0
fi
exit 1