266 lines
5.9 KiB
Go
266 lines
5.9 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
type reportRow struct {
|
|
Status string `json:"status"`
|
|
Area string `json:"area"`
|
|
Check string `json:"check"`
|
|
Summary string `json:"summary"`
|
|
Detail string `json:"detail"`
|
|
Fix string `json:"fix"`
|
|
Graph string `json:"graph"`
|
|
Query string `json:"query"`
|
|
}
|
|
|
|
func main() {
|
|
if len(os.Args) < 2 {
|
|
usage(os.Stderr)
|
|
os.Exit(2)
|
|
}
|
|
|
|
switch os.Args[1] {
|
|
case "report-render":
|
|
if err := runReportRender(os.Args[2:], os.Stdin, os.Stdout); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
case "help", "-h", "--help":
|
|
usage(os.Stdout)
|
|
default:
|
|
usage(os.Stderr)
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
|
|
func usage(out io.Writer) {
|
|
fmt.Fprintln(out, "Usage: jeannie-core report-render [--title TITLE] [--details] [--only failures|warnings|problems|all] [--json]")
|
|
}
|
|
|
|
func runReportRender(args []string, in io.Reader, out io.Writer) error {
|
|
flags := flag.NewFlagSet("report-render", flag.ContinueOnError)
|
|
flags.SetOutput(io.Discard)
|
|
title := flags.String("title", envDefault("REPORT_TITLE", "Jeannie Report"), "")
|
|
details := flags.Bool("details", false, "")
|
|
only := flags.String("only", "all", "")
|
|
asJSON := flags.Bool("json", false, "")
|
|
if err := flags.Parse(args); err != nil {
|
|
usage(os.Stderr)
|
|
return err
|
|
}
|
|
|
|
rows, err := parseReportRows(in)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rows = filterReportRows(rows, *only)
|
|
|
|
if *asJSON {
|
|
encoder := json.NewEncoder(out)
|
|
encoder.SetIndent("", " ")
|
|
return encoder.Encode(map[string]any{"title": *title, "rows": rows})
|
|
}
|
|
|
|
renderReportText(out, *title, rows, *details, *only)
|
|
return nil
|
|
}
|
|
|
|
func envDefault(name, fallback string) string {
|
|
if value := os.Getenv(name); value != "" {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func parseReportRows(in io.Reader) ([]reportRow, error) {
|
|
data, err := io.ReadAll(in)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var rows []reportRow
|
|
for _, raw := range strings.Split(string(data), "\n") {
|
|
raw = strings.TrimSuffix(raw, "\r")
|
|
if strings.TrimSpace(raw) == "" {
|
|
continue
|
|
}
|
|
parts := strings.Split(raw, "\t")
|
|
for len(parts) < 8 {
|
|
parts = append(parts, "")
|
|
}
|
|
|
|
row := reportRow{
|
|
Status: normalizeStatus(parts[0]),
|
|
Area: strings.TrimSpace(parts[1]),
|
|
Check: strings.TrimSpace(parts[2]),
|
|
Summary: strings.TrimSpace(parts[3]),
|
|
Detail: strings.TrimSpace(parts[4]),
|
|
Fix: strings.TrimSpace(parts[5]),
|
|
Graph: strings.TrimSpace(parts[6]),
|
|
Query: strings.TrimSpace(parts[7]),
|
|
}
|
|
if row.Area == "" {
|
|
row.Area = "General"
|
|
}
|
|
if row.Status == "ok" {
|
|
row.Graph = ""
|
|
row.Query = ""
|
|
}
|
|
rows = append(rows, row)
|
|
}
|
|
return rows, nil
|
|
}
|
|
|
|
func normalizeStatus(value string) string {
|
|
status := strings.ToLower(strings.TrimSpace(value))
|
|
if status == "" {
|
|
return "skip"
|
|
}
|
|
return status
|
|
}
|
|
|
|
func filterReportRows(rows []reportRow, only string) []reportRow {
|
|
var filtered []reportRow
|
|
for _, row := range rows {
|
|
if includeReportRow(row, only) {
|
|
filtered = append(filtered, row)
|
|
}
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func includeReportRow(row reportRow, only string) bool {
|
|
switch only {
|
|
case "all":
|
|
return true
|
|
case "failures":
|
|
return row.Status == "fail"
|
|
case "warnings":
|
|
return row.Status == "warn"
|
|
case "problems":
|
|
return row.Status == "fail" || row.Status == "warn"
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
func renderReportText(out io.Writer, title string, rows []reportRow, details bool, only string) {
|
|
counts := map[string]int{"fail": 0, "warn": 0, "ok": 0, "skip": 0}
|
|
for _, row := range rows {
|
|
counts[row.Status]++
|
|
}
|
|
|
|
fmt.Fprintln(out, title)
|
|
fmt.Fprintln(out, strings.Repeat("=", len(title)))
|
|
fmt.Fprintf(out, "fail=%d warn=%d ok=%d skip=%d\n", counts["fail"], counts["warn"], counts["ok"], counts["skip"])
|
|
|
|
if len(rows) == 0 {
|
|
switch only {
|
|
case "problems":
|
|
fmt.Fprintln(out, "\nNo failing or warning items.")
|
|
case "failures":
|
|
fmt.Fprintln(out, "\nNo failing items.")
|
|
case "warnings":
|
|
fmt.Fprintln(out, "\nNo warning items.")
|
|
default:
|
|
fmt.Fprintln(out, "\nNo report rows.")
|
|
}
|
|
return
|
|
}
|
|
|
|
for _, area := range orderedReportAreas(rows) {
|
|
fmt.Fprintf(out, "\n== %s ==\n", area)
|
|
areaRows := rowsForReportArea(rows, area)
|
|
sort.SliceStable(areaRows, func(i, j int) bool {
|
|
leftRank := reportStatusRank(areaRows[i].Status)
|
|
rightRank := reportStatusRank(areaRows[j].Status)
|
|
if leftRank != rightRank {
|
|
return leftRank < rightRank
|
|
}
|
|
return areaRows[i].Check < areaRows[j].Check
|
|
})
|
|
|
|
for _, row := range areaRows {
|
|
fmt.Fprintf(out, "%-5s %-30s %s\n", row.Status, truncate(row.Check, 30), row.Summary)
|
|
if details || row.Status == "fail" || row.Status == "warn" {
|
|
if row.Detail != "" {
|
|
fmt.Fprintf(out, " detail: %s\n", row.Detail)
|
|
}
|
|
if row.Fix != "" {
|
|
fmt.Fprintf(out, " fix: %s\n", row.Fix)
|
|
}
|
|
if row.Graph != "" {
|
|
fmt.Fprintf(out, " graph: %s\n", row.Graph)
|
|
}
|
|
if row.Query != "" {
|
|
fmt.Fprintf(out, " query: %s\n", row.Query)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, row := range rows {
|
|
if row.Status == "fail" || row.Status == "warn" {
|
|
fmt.Fprintln(out, "\nNext Fix")
|
|
if row.Fix != "" {
|
|
fmt.Fprintln(out, row.Fix)
|
|
} else {
|
|
fmt.Fprintf(out, "Investigate %s / %s\n", row.Area, row.Check)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func orderedReportAreas(rows []reportRow) []string {
|
|
seen := map[string]bool{}
|
|
var areas []string
|
|
for _, row := range rows {
|
|
if !seen[row.Area] {
|
|
seen[row.Area] = true
|
|
areas = append(areas, row.Area)
|
|
}
|
|
}
|
|
return areas
|
|
}
|
|
|
|
func rowsForReportArea(rows []reportRow, area string) []reportRow {
|
|
var areaRows []reportRow
|
|
for _, row := range rows {
|
|
if row.Area == area {
|
|
areaRows = append(areaRows, row)
|
|
}
|
|
}
|
|
return areaRows
|
|
}
|
|
|
|
func reportStatusRank(status string) int {
|
|
switch status {
|
|
case "fail":
|
|
return 0
|
|
case "warn":
|
|
return 1
|
|
case "skip":
|
|
return 2
|
|
case "ok":
|
|
return 3
|
|
default:
|
|
return 9
|
|
}
|
|
}
|
|
|
|
func truncate(value string, max int) string {
|
|
if len(value) <= max {
|
|
return value
|
|
}
|
|
return value[:max]
|
|
}
|