diff --git a/internal/cli/banger.go b/internal/cli/banger.go index 281325a..a9d4e80 100644 --- a/internal/cli/banger.go +++ b/internal/cli/banger.go @@ -71,7 +71,8 @@ to diagnose host readiness problems. } func (d *deps) newDoctorCommand() *cobra.Command { - return &cobra.Command{ + var verbose bool + cmd := &cobra.Command{ Use: "doctor", Short: "Check host and runtime readiness", Long: strings.TrimSpace(` @@ -85,8 +86,10 @@ Run 'banger doctor': - after upgrading the host kernel or banger itself - when 'banger vm run' fails with an unclear error -Exit code is non-zero if any check fails. Warnings are reported but -do not fail the run. +By default, prints failing and warning checks only and a summary +footer; a healthy host collapses to a single line. Pass --verbose to +print every check with its details. Exit code is non-zero if any +check fails. Warnings are reported but do not fail the run. `), Args: noArgsUsage("usage: banger doctor"), RunE: func(cmd *cobra.Command, args []string) error { @@ -94,7 +97,7 @@ do not fail the run. if err != nil { return err } - if err := printDoctorReport(cmd.OutOrStdout(), report); err != nil { + if err := printDoctorReport(cmd.OutOrStdout(), report, verbose); err != nil { return err } if report.HasFailures() { @@ -103,6 +106,8 @@ do not fail the run. return nil }, } + cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "show every check (default: only failures and warnings)") + return cmd } func newVersionCommand() *cobra.Command { diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index ed2ab59..d67ddef 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -133,12 +133,15 @@ func TestDoctorCommandPrintsReportAndFailsOnHardFailures(t *testing.T) { t.Fatalf("Execute() error = %v, want doctor failure", err) } output := stdout.String() - if !strings.Contains(output, "PASS\truntime bundle") { - t.Fatalf("output = %q, want runtime bundle pass", output) + if strings.Contains(output, "PASS\truntime bundle") { + t.Fatalf("output = %q, brief default should hide PASS rows", output) } if !strings.Contains(output, "FAIL\tfeature nat") { t.Fatalf("output = %q, want feature nat fail", output) } + if !strings.Contains(output, "1 passed, 0 warnings, 1 failure") { + t.Fatalf("output = %q, want summary footer", output) + } } func TestDoctorCommandReturnsUnderlyingError(t *testing.T) { diff --git a/internal/cli/printers.go b/internal/cli/printers.go index d4ea646..afedbc8 100644 --- a/internal/cli/printers.go +++ b/internal/cli/printers.go @@ -272,9 +272,34 @@ func printKernelCatalogTable(out anyWriter, entries []api.KernelCatalogEntry) er // -- doctor printer ------------------------------------------------- -func printDoctorReport(out anyWriter, report system.Report) error { +func printDoctorReport(out anyWriter, report system.Report, verbose bool) error { colorWriter, _ := out.(io.Writer) + + var passes, warns, fails int + for _, c := range report.Checks { + switch c.Status { + case system.CheckStatusPass: + passes++ + case system.CheckStatusWarn: + warns++ + case system.CheckStatusFail: + fails++ + } + } + + if !verbose && warns == 0 && fails == 0 { + msg := fmt.Sprintf("all %d checks passed", passes) + if colorWriter != nil { + msg = style.Pass(colorWriter, msg) + } + _, err := fmt.Fprintln(out, msg) + return err + } + for _, check := range report.Checks { + if !verbose && check.Status == system.CheckStatusPass { + continue + } status := strings.ToUpper(string(check.Status)) if colorWriter != nil { switch check.Status { @@ -295,5 +320,19 @@ func printDoctorReport(out anyWriter, report system.Report) error { } } } + + if !verbose { + if _, err := fmt.Fprintf(out, "\n%d passed, %s, %s\n", passes, pluralCount(warns, "warning"), pluralCount(fails, "failure")); err != nil { + return err + } + } + return nil } + +func pluralCount(n int, word string) string { + if n == 1 { + return fmt.Sprintf("%d %s", n, word) + } + return fmt.Sprintf("%d %ss", n, word) +} diff --git a/internal/cli/printers_test.go b/internal/cli/printers_test.go new file mode 100644 index 0000000..3018ca8 --- /dev/null +++ b/internal/cli/printers_test.go @@ -0,0 +1,88 @@ +package cli + +import ( + "bytes" + "strings" + "testing" + + "banger/internal/system" +) + +func TestPrintDoctorReport_BriefAllPass(t *testing.T) { + report := system.Report{} + report.AddPass("first", "detail one") + report.AddPass("second", "detail two") + report.AddPass("third") + + var buf bytes.Buffer + if err := printDoctorReport(&buf, report, false); err != nil { + t.Fatalf("printDoctorReport: %v", err) + } + + got := buf.String() + want := "all 3 checks passed\n" + if got != want { + t.Fatalf("brief all-pass output\n got: %q\nwant: %q", got, want) + } +} + +func TestPrintDoctorReport_BriefHidesPassDetails(t *testing.T) { + report := system.Report{} + report.AddPass("first", "detail one") + report.AddWarn("second", "warn detail") + report.AddPass("third", "detail three") + report.AddFail("fourth", "fail detail") + + var buf bytes.Buffer + if err := printDoctorReport(&buf, report, false); err != nil { + t.Fatalf("printDoctorReport: %v", err) + } + + got := buf.String() + if strings.Contains(got, "PASS") || strings.Contains(got, "first") || strings.Contains(got, "third") { + t.Fatalf("brief mode leaked PASS rows: %q", got) + } + for _, want := range []string{"WARN\tsecond", "warn detail", "FAIL\tfourth", "fail detail"} { + if !strings.Contains(got, want) { + t.Fatalf("missing %q in brief output: %q", want, got) + } + } + if !strings.Contains(got, "2 passed, 1 warning, 1 failure") { + t.Fatalf("missing summary footer in: %q", got) + } +} + +func TestPrintDoctorReport_BriefSummaryPlurals(t *testing.T) { + report := system.Report{} + report.AddPass("a") + report.AddWarn("b") + report.AddWarn("c") + + var buf bytes.Buffer + if err := printDoctorReport(&buf, report, false); err != nil { + t.Fatalf("printDoctorReport: %v", err) + } + if !strings.Contains(buf.String(), "1 passed, 2 warnings, 0 failures") { + t.Fatalf("plural counts wrong: %q", buf.String()) + } +} + +func TestPrintDoctorReport_VerboseShowsEverything(t *testing.T) { + report := system.Report{} + report.AddPass("first", "detail one") + report.AddWarn("second", "warn detail") + + var buf bytes.Buffer + if err := printDoctorReport(&buf, report, true); err != nil { + t.Fatalf("printDoctorReport: %v", err) + } + got := buf.String() + for _, want := range []string{"PASS\tfirst", "detail one", "WARN\tsecond", "warn detail"} { + if !strings.Contains(got, want) { + t.Fatalf("verbose mode missing %q: %q", want, got) + } + } + if strings.Contains(got, "passed,") { + t.Fatalf("verbose mode should not print summary footer: %q", got) + } +}