| | | 1 | | // Package reporter contains implementations of ports.Reporter for each |
| | | 2 | | // supported output format. |
| | | 3 | | package reporter |
| | | 4 | | |
| | | 5 | | import ( |
| | | 6 | | "context" |
| | | 7 | | "fmt" |
| | | 8 | | "io" |
| | | 9 | | |
| | | 10 | | "github.com/jedi-knights/neospec/internal/domain" |
| | | 11 | | ) |
| | | 12 | | |
| | | 13 | | // Console writes a human-readable test and coverage summary to the writer. |
| | | 14 | | // It uses ANSI color codes when the writer is likely a TTY; callers that need |
| | | 15 | | // plain text can wrap the writer with a color-stripping adapter. |
| | | 16 | | type Console struct { |
| | | 17 | | // Color controls whether ANSI escape codes are emitted. |
| | | 18 | | Color bool |
| | | 19 | | } |
| | | 20 | | |
| | | 21 | | // NewConsole creates a Console reporter. Pass color=true for terminal output. |
| | | 22 | | func NewConsole(color bool) *Console { |
| | | 23 | | return &Console{Color: color} |
| | | 24 | | } |
| | | 25 | | |
| | 9 | 26 | | func (c *Console) Write(_ context.Context, w io.Writer, suite *domain.SuiteResult, cov *domain.CoverageData) error { |
| | 9 | 27 | | pass, fail, skip, errors := suite.Counts() |
| | 9 | 28 | | |
| | 9 | 29 | | for _, t := range suite.Tests { |
| | 13 | 30 | | symbol, color := c.statusSymbol(t.Status) |
| | 13 | 31 | | c.fprintColor(w, color, fmt.Sprintf(" %s %s\n", symbol, t.Name)) |
| | 3 | 32 | | if t.Error != "" { |
| | 3 | 33 | | c.fprintColor(w, colorRed, fmt.Sprintf(" %s\n", t.Error)) |
| | 3 | 34 | | } |
| | | 35 | | } |
| | | 36 | | |
| | 9 | 37 | | fmt.Fprintln(w) |
| | 9 | 38 | | summary := fmt.Sprintf( |
| | 9 | 39 | | "Tests: %d passed, %d failed, %d skipped, %d errors (%.2fs)\n", |
| | 9 | 40 | | pass, fail, skip, errors, |
| | 9 | 41 | | suite.Duration.Seconds(), |
| | 9 | 42 | | ) |
| | 2 | 43 | | if fail > 0 || errors > 0 { |
| | 2 | 44 | | c.fprintColor(w, colorRed, summary) |
| | 2 | 45 | | } else { |
| | 7 | 46 | | c.fprintColor(w, colorGreen, summary) |
| | 7 | 47 | | } |
| | | 48 | | |
| | 7 | 49 | | if cov != nil && cov.TotalLines() > 0 { |
| | 7 | 50 | | pct := cov.Percentage() |
| | 7 | 51 | | covLine := fmt.Sprintf("Coverage: %s (%d/%d lines)\n", |
| | 7 | 52 | | domain.BadgeLabel(pct), cov.HitLines(), cov.TotalLines()) |
| | 7 | 53 | | c.fprintColor(w, colorForPct(pct), covLine) |
| | 7 | 54 | | } |
| | | 55 | | |
| | 9 | 56 | | return nil |
| | | 57 | | } |
| | | 58 | | |
| | | 59 | | const ( |
| | | 60 | | colorReset = "\033[0m" |
| | | 61 | | colorRed = "\033[31m" |
| | | 62 | | colorGreen = "\033[32m" |
| | | 63 | | colorYellow = "\033[33m" |
| | | 64 | | colorOrange = "\033[38;5;208m" |
| | | 65 | | ) |
| | | 66 | | |
| | | 67 | | func colorForPct(pct float64) string { |
| | | 68 | | switch domain.BadgeColor(pct) { |
| | | 69 | | case "brightgreen", "green": |
| | | 70 | | return colorGreen |
| | | 71 | | case "yellow": |
| | | 72 | | return colorYellow |
| | | 73 | | case "orange": |
| | | 74 | | return colorOrange |
| | | 75 | | default: |
| | | 76 | | return colorRed |
| | | 77 | | } |
| | | 78 | | } |
| | | 79 | | |
| | 32 | 80 | | func (c *Console) fprintColor(w io.Writer, color, s string) { |
| | 3 | 81 | | if c.Color { |
| | 3 | 82 | | fmt.Fprintf(w, "%s%s%s", color, s, colorReset) |
| | 3 | 83 | | } else { |
| | 29 | 84 | | fmt.Fprint(w, s) |
| | 29 | 85 | | } |
| | | 86 | | } |
| | | 87 | | |
| | 13 | 88 | | func (c *Console) statusSymbol(s domain.TestStatus) (string, string) { |
| | 13 | 89 | | switch s { |
| | 9 | 90 | | case domain.StatusPass: |
| | 9 | 91 | | return "✓", colorGreen |
| | 2 | 92 | | case domain.StatusFail: |
| | 2 | 93 | | return "✗", colorRed |
| | 1 | 94 | | case domain.StatusSkip: |
| | 1 | 95 | | return "○", colorYellow |
| | 1 | 96 | | default: |
| | 1 | 97 | | return "!", colorOrange |
| | | 98 | | } |
| | | 99 | | } |