Skip to content

Commit d89c5a4

Browse files
committed
feat: categorize console output
1 parent 450aeef commit d89c5a4

2 files changed

Lines changed: 261 additions & 51 deletions

File tree

regresql/formatter_console.go

Lines changed: 88 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -6,66 +6,35 @@ import (
66
"strings"
77
)
88

9-
type ConsoleFormatter struct{
10-
lastQueryGroup string
9+
type ConsoleFormatter struct {
10+
results []TestResult
1111
}
1212

1313
func (f *ConsoleFormatter) Start(w io.Writer) error {
1414
fmt.Fprintln(w, "\nRunning regression tests...")
15-
f.lastQueryGroup = ""
15+
f.results = make([]TestResult, 0)
1616
return nil
1717
}
1818

1919
func (f *ConsoleFormatter) AddResult(r TestResult, w io.Writer) error {
20-
queryGroup := f.extractQueryGroup(r.Name)
21-
if queryGroup != f.lastQueryGroup && f.lastQueryGroup != "" {
22-
fmt.Fprintln(w)
23-
}
24-
f.lastQueryGroup = queryGroup
20+
f.results = append(f.results, r)
2521

22+
// Show progress indicator
2623
switch r.Status {
2724
case "passed":
28-
fmt.Fprintf(w, "✓ %s (%.2fs)\n", r.Name, r.Duration)
29-
f.printWarnings(r.PlanWarnings, w)
30-
25+
fmt.Fprint(w, ".")
3126
case "failed":
32-
fmt.Fprintf(w, "✗ %s (%.2fs)\n", r.Name, r.Duration)
33-
if r.Type == "cost" {
34-
f.printCostFailure(r, w)
35-
} else if r.Type == "output" {
36-
f.printOutputDiff(r, w)
37-
}
38-
if r.Error != "" {
39-
fmt.Fprintf(w, " Error: %s\n", r.Error)
40-
}
41-
fmt.Fprintln(w)
42-
43-
case "warning":
44-
fmt.Fprintf(w, "⚠️ %s (%.2fs)\n", r.Name, r.Duration)
45-
f.printWarnings(r.PlanWarnings, w)
46-
fmt.Fprintln(w)
47-
27+
fmt.Fprint(w, "F")
4828
case "pending":
49-
fmt.Fprintf(w, "? %s (%.2fs)\n", r.Name, r.Duration)
50-
if r.Error != "" {
51-
fmt.Fprintf(w, " %s\n", r.Error)
52-
}
53-
fmt.Fprintln(w)
54-
29+
fmt.Fprint(w, "?")
30+
case "warning":
31+
fmt.Fprint(w, "W")
5532
case "skipped":
56-
return nil
33+
fmt.Fprint(w, "S")
5734
}
5835
return nil
5936
}
6037

61-
func (f *ConsoleFormatter) extractQueryGroup(testName string) string {
62-
parts := strings.Split(testName, ".")
63-
if len(parts) <= 1 {
64-
return testName
65-
}
66-
return parts[0]
67-
}
68-
6938
func (f *ConsoleFormatter) printCostFailure(r TestResult, w io.Writer) {
7039
fmt.Fprintf(w, " Expected cost: %.2f\n", r.ExpectedCost)
7140
fmt.Fprintf(w, " Actual cost: %.2f (+%.1f%%)\n", r.ActualCost, r.PercentIncrease)
@@ -230,22 +199,90 @@ func (f *ConsoleFormatter) printWarnings(warnings []PlanWarning, w io.Writer) {
230199
}
231200

232201
func (f *ConsoleFormatter) Finish(s *TestSummary, w io.Writer) error {
202+
fmt.Fprintln(w) // End progress line
233203
fmt.Fprintln(w)
234-
if s.Failed > 0 || s.Skipped > 0 || s.Pending > 0 {
235-
fmt.Fprintf(w, "Results: %d passed, %d failed", s.Passed, s.Failed)
236-
if s.Skipped > 0 {
237-
fmt.Fprintf(w, ", %d skipped", s.Skipped)
204+
205+
// Summary section
206+
fmt.Fprintln(w, "RESULTS:")
207+
fmt.Fprintf(w, " ✓ %d passing\n", s.Passed)
208+
if s.Failed > 0 {
209+
fmt.Fprintf(w, " ✗ %d failing\n", s.Failed)
210+
}
211+
if s.Pending > 0 {
212+
fmt.Fprintf(w, " ? %d pending (no baseline)\n", s.Pending)
213+
}
214+
if s.Skipped > 0 {
215+
fmt.Fprintf(w, " - %d skipped\n", s.Skipped)
216+
}
217+
fmt.Fprintf(w, " %.2fs total\n", s.Duration)
218+
219+
// Failing tests details
220+
if s.Failed > 0 {
221+
fmt.Fprintln(w)
222+
fmt.Fprintln(w, "FAILING:")
223+
for _, r := range f.results {
224+
if r.Status == "failed" {
225+
fmt.Fprintf(w, " %s\n", r.Name)
226+
if r.Type == "cost" {
227+
f.printCostFailure(r, w)
228+
} else if r.Type == "output" {
229+
f.printOutputDiff(r, w)
230+
}
231+
if r.Error != "" {
232+
fmt.Fprintf(w, " Error: %s\n", r.Error)
233+
}
234+
}
235+
}
236+
}
237+
238+
// Warnings (passed tests with plan warnings)
239+
var warnings []TestResult
240+
for _, r := range f.results {
241+
if r.Status == "passed" && len(r.PlanWarnings) > 0 {
242+
warnings = append(warnings, r)
238243
}
239-
if s.Pending > 0 {
240-
fmt.Fprintf(w, ", %d pending", s.Pending)
244+
}
245+
if len(warnings) > 0 {
246+
fmt.Fprintln(w)
247+
fmt.Fprintln(w, "WARNINGS:")
248+
for _, r := range warnings {
249+
fmt.Fprintf(w, " %s\n", r.Name)
250+
f.printWarnings(r.PlanWarnings, w)
251+
}
252+
}
253+
254+
// Pending tests
255+
if s.Pending > 0 {
256+
fmt.Fprintln(w)
257+
fmt.Fprintln(w, "PENDING (no baseline):")
258+
for _, r := range f.results {
259+
if r.Status == "pending" {
260+
fmt.Fprintf(w, " %s\n", r.Name)
261+
}
241262
}
242-
fmt.Fprintf(w, " (%.2fs)\n", s.Duration)
243-
} else {
244-
fmt.Fprintf(w, "Results: %d passed (%.2fs)\n", s.Passed, s.Duration)
245263
}
264+
265+
// Suggestions
266+
f.printSuggestions(s, w)
267+
246268
return nil
247269
}
248270

271+
func (f *ConsoleFormatter) printSuggestions(s *TestSummary, w io.Writer) {
272+
if s.Failed == 0 && s.Pending == 0 {
273+
return
274+
}
275+
276+
fmt.Fprintln(w)
277+
278+
if s.Failed > 0 {
279+
fmt.Fprintln(w, "To accept changes: regresql update <query-name>")
280+
}
281+
if s.Pending > 0 {
282+
fmt.Fprintln(w, "To create baselines: regresql update --pending")
283+
}
284+
}
285+
249286
func hasAnyCritical(regressions []PlanRegression) bool {
250287
for _, reg := range regressions {
251288
if reg.Severity == "critical" {

regresql/pending_test.go

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package regresql
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/boringsql/queries"
9+
)
10+
11+
func TestTestSummaryAddResultPending(t *testing.T) {
12+
summary := NewTestSummary()
13+
14+
// Add a pending result
15+
summary.AddResult(TestResult{
16+
Name: "test1",
17+
Status: "pending",
18+
})
19+
20+
if summary.Total != 1 {
21+
t.Errorf("Expected Total=1, got %d", summary.Total)
22+
}
23+
if summary.Pending != 1 {
24+
t.Errorf("Expected Pending=1, got %d", summary.Pending)
25+
}
26+
if summary.Passed != 0 {
27+
t.Errorf("Expected Passed=0, got %d", summary.Passed)
28+
}
29+
if summary.Failed != 0 {
30+
t.Errorf("Expected Failed=0, got %d", summary.Failed)
31+
}
32+
}
33+
34+
func TestTestSummaryMixedStatuses(t *testing.T) {
35+
summary := NewTestSummary()
36+
37+
summary.AddResult(TestResult{Name: "passed1", Status: "passed"})
38+
summary.AddResult(TestResult{Name: "pending1", Status: "pending"})
39+
summary.AddResult(TestResult{Name: "failed1", Status: "failed"})
40+
summary.AddResult(TestResult{Name: "pending2", Status: "pending"})
41+
summary.AddResult(TestResult{Name: "skipped1", Status: "skipped"})
42+
43+
if summary.Total != 5 {
44+
t.Errorf("Expected Total=5, got %d", summary.Total)
45+
}
46+
if summary.Passed != 1 {
47+
t.Errorf("Expected Passed=1, got %d", summary.Passed)
48+
}
49+
if summary.Failed != 1 {
50+
t.Errorf("Expected Failed=1, got %d", summary.Failed)
51+
}
52+
if summary.Pending != 2 {
53+
t.Errorf("Expected Pending=2, got %d", summary.Pending)
54+
}
55+
if summary.Skipped != 1 {
56+
t.Errorf("Expected Skipped=1, got %d", summary.Skipped)
57+
}
58+
}
59+
60+
func TestCompareResultSetsToResultsMissingExpectedFile(t *testing.T) {
61+
// Create temp directories
62+
tmpDir, err := os.MkdirTemp("", "regresql-test")
63+
if err != nil {
64+
t.Fatalf("Failed to create temp dir: %v", err)
65+
}
66+
defer os.RemoveAll(tmpDir)
67+
68+
regressDir := filepath.Join(tmpDir, "regress")
69+
outDir := filepath.Join(regressDir, "out")
70+
expectedDir := filepath.Join(regressDir, "expected")
71+
72+
if err := os.MkdirAll(outDir, 0755); err != nil {
73+
t.Fatalf("Failed to create out dir: %v", err)
74+
}
75+
if err := os.MkdirAll(expectedDir, 0755); err != nil {
76+
t.Fatalf("Failed to create expected dir: %v", err)
77+
}
78+
79+
// Create an actual result file (simulating query output)
80+
actualFile := filepath.Join(outDir, "test_query.json")
81+
actualContent := `{"columns":["id"],"rows":[[1]]}`
82+
if err := os.WriteFile(actualFile, []byte(actualContent), 0644); err != nil {
83+
t.Fatalf("Failed to write actual file: %v", err)
84+
}
85+
86+
// Create a minimal query for the plan
87+
bqQuery, _ := queries.NewQuery("test", "test.sql", "SELECT 1", nil)
88+
query := &Query{Query: bqQuery}
89+
90+
// Create a plan with the result set (no expected file exists)
91+
plan := &Plan{
92+
Query: query,
93+
ResultSets: []ResultSet{
94+
{Filename: actualFile},
95+
},
96+
Names: []string{"default"},
97+
Bindings: []map[string]any{{}},
98+
}
99+
100+
// Run comparison - expected file doesn't exist
101+
results := plan.CompareResultSetsToResults(regressDir, expectedDir)
102+
103+
if len(results) != 1 {
104+
t.Fatalf("Expected 1 result, got %d", len(results))
105+
}
106+
107+
result := results[0]
108+
if result.Status != "pending" {
109+
t.Errorf("Expected status='pending', got '%s'", result.Status)
110+
}
111+
if result.Error == "" {
112+
t.Error("Expected error message to be set for pending result")
113+
}
114+
}
115+
116+
func TestCompareResultSetsToResultsWithExpectedFile(t *testing.T) {
117+
// Create temp directories
118+
tmpDir, err := os.MkdirTemp("", "regresql-test")
119+
if err != nil {
120+
t.Fatalf("Failed to create temp dir: %v", err)
121+
}
122+
defer os.RemoveAll(tmpDir)
123+
124+
regressDir := filepath.Join(tmpDir, "regress")
125+
outDir := filepath.Join(regressDir, "out")
126+
expectedDir := filepath.Join(regressDir, "expected")
127+
128+
if err := os.MkdirAll(outDir, 0755); err != nil {
129+
t.Fatalf("Failed to create out dir: %v", err)
130+
}
131+
if err := os.MkdirAll(expectedDir, 0755); err != nil {
132+
t.Fatalf("Failed to create expected dir: %v", err)
133+
}
134+
135+
// Create matching actual and expected files
136+
content := `{"columns":["id"],"rows":[[1]]}`
137+
actualFile := filepath.Join(outDir, "test_query.json")
138+
expectedFile := filepath.Join(expectedDir, "test_query.json")
139+
140+
if err := os.WriteFile(actualFile, []byte(content), 0644); err != nil {
141+
t.Fatalf("Failed to write actual file: %v", err)
142+
}
143+
if err := os.WriteFile(expectedFile, []byte(content), 0644); err != nil {
144+
t.Fatalf("Failed to write expected file: %v", err)
145+
}
146+
147+
bqQuery2, _ := queries.NewQuery("test", "test.sql", "SELECT 1", nil)
148+
query := &Query{Query: bqQuery2}
149+
150+
plan := &Plan{
151+
Query: query,
152+
ResultSets: []ResultSet{
153+
{
154+
Filename: actualFile,
155+
Cols: []string{"id"},
156+
Rows: [][]any{{float64(1)}}, // JSON unmarshals numbers as float64
157+
},
158+
},
159+
Names: []string{"default"},
160+
Bindings: []map[string]any{{}},
161+
}
162+
163+
results := plan.CompareResultSetsToResults(regressDir, expectedDir)
164+
165+
if len(results) != 1 {
166+
t.Fatalf("Expected 1 result, got %d", len(results))
167+
}
168+
169+
result := results[0]
170+
if result.Status != "passed" {
171+
t.Errorf("Expected status='passed', got '%s' (error: %s)", result.Status, result.Error)
172+
}
173+
}

0 commit comments

Comments
 (0)