168 lines
4.3 KiB
Bash
Executable File
168 lines
4.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
MODE="compact"
|
|
ONLY="all"
|
|
TITLE="${REPORT_TITLE:-Jeannie Report}"
|
|
INPUT_FILE=""
|
|
|
|
cleanup() {
|
|
if [ -n "$INPUT_FILE" ] && [ -f "$INPUT_FILE" ]; then
|
|
rm -f "$INPUT_FILE"
|
|
fi
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: report-render [--title TITLE] [--details] [--only failures|warnings|problems|all] [--json]
|
|
|
|
Reads TSV rows from stdin:
|
|
status<TAB>area<TAB>check<TAB>summary<TAB>detail<TAB>fix<TAB>graph<TAB>query
|
|
|
|
Statuses: ok, warn, fail, skip
|
|
EOF
|
|
}
|
|
|
|
while (($# > 0)); do
|
|
case "$1" in
|
|
--title)
|
|
TITLE="${2:-}"
|
|
shift 2
|
|
;;
|
|
--details)
|
|
MODE="details"
|
|
shift
|
|
;;
|
|
--only)
|
|
ONLY="${2:-all}"
|
|
shift 2
|
|
;;
|
|
--json)
|
|
MODE="json"
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
usage >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/jeannie-report.XXXXXX")"
|
|
cat >"$INPUT_FILE"
|
|
|
|
python3 - "$TITLE" "$MODE" "$ONLY" "$INPUT_FILE" <<'PY'
|
|
import json
|
|
import sys
|
|
from collections import OrderedDict
|
|
|
|
title, mode, only, input_file = sys.argv[1:5]
|
|
rows = []
|
|
|
|
with open(input_file, encoding="utf-8") as handle:
|
|
for raw in handle:
|
|
raw = raw.rstrip("\n")
|
|
if not raw:
|
|
continue
|
|
parts = raw.split("\t")
|
|
while len(parts) < 8:
|
|
parts.append("")
|
|
status, area, check, summary, detail, fix, graph, query = parts[:8]
|
|
status = status.strip().lower() or "skip"
|
|
if status == "ok":
|
|
graph = ""
|
|
query = ""
|
|
rows.append(
|
|
{
|
|
"status": status,
|
|
"area": area.strip() or "General",
|
|
"check": check.strip(),
|
|
"summary": summary.strip(),
|
|
"detail": detail.strip(),
|
|
"fix": fix.strip(),
|
|
"graph": graph.strip(),
|
|
"query": query.strip(),
|
|
}
|
|
)
|
|
|
|
def include(row):
|
|
status = row["status"]
|
|
if only == "all":
|
|
return True
|
|
if only == "failures":
|
|
return status == "fail"
|
|
if only == "warnings":
|
|
return status == "warn"
|
|
if only == "problems":
|
|
return status in {"fail", "warn"}
|
|
return True
|
|
|
|
rows = [row for row in rows if include(row)]
|
|
|
|
if mode == "json":
|
|
print(json.dumps({"title": title, "rows": rows}, indent=2))
|
|
raise SystemExit(0)
|
|
|
|
counts = {"fail": 0, "warn": 0, "ok": 0, "skip": 0}
|
|
for row in rows:
|
|
counts[row["status"]] = counts.get(row["status"], 0) + 1
|
|
|
|
print(title)
|
|
print("=" * len(title))
|
|
print(
|
|
f"fail={counts.get('fail', 0)} "
|
|
f"warn={counts.get('warn', 0)} "
|
|
f"ok={counts.get('ok', 0)} "
|
|
f"skip={counts.get('skip', 0)}"
|
|
)
|
|
|
|
if not rows:
|
|
if only == "problems":
|
|
print("\nNo failing or warning items.")
|
|
elif only == "failures":
|
|
print("\nNo failing items.")
|
|
elif only == "warnings":
|
|
print("\nNo warning items.")
|
|
else:
|
|
print("\nNo report rows.")
|
|
raise SystemExit(0)
|
|
|
|
groups = OrderedDict()
|
|
for row in rows:
|
|
groups.setdefault(row["area"], []).append(row)
|
|
|
|
status_rank = {"fail": 0, "warn": 1, "skip": 2, "ok": 3}
|
|
status_label = {"fail": "fail", "warn": "warn", "ok": "ok", "skip": "skip"}
|
|
|
|
for area, area_rows in groups.items():
|
|
print(f"\n== {area} ==")
|
|
for row in sorted(area_rows, key=lambda item: (status_rank.get(item["status"], 9), item["check"])):
|
|
status = status_label.get(row["status"], row["status"])
|
|
check = row["check"][:30]
|
|
summary = row["summary"]
|
|
print(f"{status:<5} {check:<30} {summary}")
|
|
if mode == "details" or row["status"] in {"fail", "warn"}:
|
|
if row["detail"]:
|
|
print(f" detail: {row['detail']}")
|
|
if row["fix"]:
|
|
print(f" fix: {row['fix']}")
|
|
if row["graph"]:
|
|
print(f" graph: {row['graph']}")
|
|
if row["query"]:
|
|
print(f" query: {row['query']}")
|
|
|
|
problems = [row for row in rows if row["status"] in {"fail", "warn"}]
|
|
if problems:
|
|
first = problems[0]
|
|
print("\nNext Fix")
|
|
if first["fix"]:
|
|
print(first["fix"])
|
|
else:
|
|
print(f"Investigate {first['area']} / {first['check']}")
|
|
PY
|