@@ -3,11 +3,67 @@ package regresql
33import (
44 "fmt"
55 "io"
6+ "os"
67 "strings"
8+
9+ "golang.org/x/term"
710)
811
12+ type ConsoleOptions struct {
13+ Color bool
14+ NoColor bool
15+ FullDiff bool
16+ NoDiff bool
17+ }
18+
919type ConsoleFormatter struct {
10- results []TestResult
20+ results []TestResult
21+ options ConsoleOptions
22+ useColor bool
23+ }
24+
25+ func (f * ConsoleFormatter ) SetOptions (opts ConsoleOptions ) {
26+ f .options = opts
27+ f .useColor = f .shouldUseColor ()
28+ }
29+
30+ func (f * ConsoleFormatter ) shouldUseColor () bool {
31+ // Explicit flags take precedence
32+ if f .options .NoColor {
33+ return false
34+ }
35+ if f .options .Color {
36+ return true
37+ }
38+
39+ // Respect NO_COLOR environment variable (https://no-color.org/)
40+ if _ , exists := os .LookupEnv ("NO_COLOR" ); exists {
41+ return false
42+ }
43+
44+ // Check TERM for dumb terminals
45+ if os .Getenv ("TERM" ) == "dumb" {
46+ return false
47+ }
48+
49+ // Auto-detect: use color if stdout is a terminal
50+ return term .IsTerminal (int (os .Stdout .Fd ()))
51+ }
52+
53+ const (
54+ colorReset = "\033 [0m"
55+ colorRed = "\033 [31m"
56+ colorGreen = "\033 [32m"
57+ colorYellow = "\033 [33m"
58+ colorCyan = "\033 [36m"
59+ colorDim = "\033 [2m"
60+ )
61+
62+ func (f * ConsoleFormatter ) colorize (text , color string ) string {
63+ if ! f .useColor {
64+ return text
65+ }
66+ return color + text + colorReset
1167}
1268
1369func (f * ConsoleFormatter ) Start (w io.Writer ) error {
@@ -19,18 +75,18 @@ func (f *ConsoleFormatter) Start(w io.Writer) error {
1975func (f * ConsoleFormatter ) AddResult (r TestResult , w io.Writer ) error {
2076 f .results = append (f .results , r )
2177
22- // Show progress indicator
78+ // Show progress indicator with color
2379 switch r .Status {
2480 case "passed" :
25- fmt .Fprint (w , "." )
81+ fmt .Fprint (w , f . colorize ( "." , colorGreen ) )
2682 case "failed" :
27- fmt .Fprint (w , "F" )
83+ fmt .Fprint (w , f . colorize ( "F" , colorRed ) )
2884 case "pending" :
29- fmt .Fprint (w , "?" )
85+ fmt .Fprint (w , f . colorize ( "?" , colorCyan ) )
3086 case "warning" :
31- fmt .Fprint (w , "W" )
87+ fmt .Fprint (w , f . colorize ( "W" , colorYellow ) )
3288 case "skipped" :
33- fmt .Fprint (w , "S" )
89+ fmt .Fprint (w , f . colorize ( "S" , colorDim ) )
3490 }
3591 return nil
3692}
@@ -87,18 +143,31 @@ func (f *ConsoleFormatter) printOutputDiff(r TestResult, w io.Writer) {
87143
88144 lines := strings .Split (r .Diff , "\n " )
89145 shown := 0
146+ maxLines := 5
147+ if f .options .FullDiff {
148+ maxLines = len (lines ) // No limit
149+ }
150+
90151 for _ , line := range lines {
91- if shown >= 5 {
152+ if shown >= maxLines {
92153 break
93154 }
94- if strings .HasPrefix (line , "+" ) && ! strings .HasPrefix (line , "+++" ) ||
95- strings .HasPrefix (line , "-" ) && ! strings .HasPrefix (line , "---" ) {
96- fmt .Fprintf (w , " %s\n " , line )
155+ // Show hunk headers to separate different change locations
156+ if strings .HasPrefix (line , "@@" ) {
157+ fmt .Fprintf (w , " %s\n " , f .colorize (line , colorCyan ))
158+ // Don't count headers toward the limit
159+ continue
160+ }
161+ if strings .HasPrefix (line , "+" ) && ! strings .HasPrefix (line , "+++" ) {
162+ fmt .Fprintf (w , " %s\n " , f .colorize (line , colorGreen ))
163+ shown ++
164+ } else if strings .HasPrefix (line , "-" ) && ! strings .HasPrefix (line , "---" ) {
165+ fmt .Fprintf (w , " %s\n " , f .colorize (line , colorRed ))
97166 shown ++
98167 }
99168 }
100- if shown >= 5 {
101- fmt .Fprintln (w , " ..." )
169+ if ! f . options . FullDiff && shown >= maxLines {
170+ fmt .Fprintln (w , f . colorize ( " ... (use --diff to see full output)" , colorDim ) )
102171 }
103172}
104173
@@ -112,49 +181,49 @@ func (f *ConsoleFormatter) printStructuredDiff(diff *StructuredDiff, w io.Writer
112181 case DiffTypeOrdering :
113182 fmt .Fprintln (w , " └─ Result: Same data, different order" )
114183 fmt .Fprintln (w )
115- fmt .Fprintln (w , " ⚠️ Rows are identical but in different order." )
184+ fmt .Fprintln (w , f . colorize ( " ⚠️ Rows are identical but in different order." , colorYellow ) )
116185 fmt .Fprintln (w , " Consider adding ORDER BY clause to ensure deterministic results." )
117186
118187 case DiffTypeRowCount , DiffTypeMultiple :
119188 if diff .RemovedRows > 0 && diff .AddedRows > 0 {
120189 fmt .Fprintf (w , " ├─ Matching: %d rows\n " , diff .MatchingRows )
121- fmt .Fprintf (w , " ├─ Added: %d rows\n " , diff .AddedRows )
122- fmt .Fprintf (w , " └─ Removed: %d rows\n " , diff .RemovedRows )
190+ fmt .Fprintf (w , " ├─ %s \n " , f . colorize ( fmt . Sprintf ( " Added: %d rows" , diff .AddedRows ), colorGreen ) )
191+ fmt .Fprintf (w , " └─ %s \n " , f . colorize ( fmt . Sprintf ( " Removed: %d rows" , diff .RemovedRows ), colorRed ) )
123192 } else if diff .RemovedRows > 0 {
124- fmt .Fprintf (w , " └─ Result: %d rows removed\n " , diff .RemovedRows )
193+ fmt .Fprintf (w , " └─ Result: %s \n " , f . colorize ( fmt . Sprintf ( "% d rows removed" , diff .RemovedRows ), colorRed ) )
125194 } else if diff .AddedRows > 0 {
126- fmt .Fprintf (w , " └─ Result: %d rows added\n " , diff .AddedRows )
195+ fmt .Fprintf (w , " └─ Result: %s \n " , f . colorize ( fmt . Sprintf ( "% d rows added" , diff .AddedRows ), colorGreen ) )
127196 }
128197
129198 fmt .Fprintln (w )
130199
131200 if len (diff .RemovedSamples ) > 0 {
132- fmt .Fprintf (w , " REMOVED ROWS (showing %d of %d):\n " , len (diff .RemovedSamples ), diff .RemovedRows )
201+ fmt .Fprintf (w , " %s \n " , f . colorize ( fmt . Sprintf ( " REMOVED ROWS (showing %d of %d):" , len (diff .RemovedSamples ), diff .RemovedRows ), colorRed ) )
133202 for _ , row := range diff .RemovedSamples {
134- fmt .Fprintf (w , " %s\n " , f .formatRow (diff .Columns , row ))
203+ fmt .Fprintf (w , " %s\n " , f .colorize ( f . formatRow (diff .Columns , row ), colorRed ))
135204 }
136205 fmt .Fprintln (w )
137206 }
138207
139208 if len (diff .AddedSamples ) > 0 {
140- fmt .Fprintf (w , " ADDED ROWS (showing %d of %d):\n " , len (diff .AddedSamples ), diff .AddedRows )
209+ fmt .Fprintf (w , " %s \n " , f . colorize ( fmt . Sprintf ( " ADDED ROWS (showing %d of %d):" , len (diff .AddedSamples ), diff .AddedRows ), colorGreen ) )
141210 for _ , row := range diff .AddedSamples {
142- fmt .Fprintf (w , " %s\n " , f .formatRow (diff .Columns , row ))
211+ fmt .Fprintf (w , " %s\n " , f .colorize ( f . formatRow (diff .Columns , row ), colorGreen ))
143212 }
144213 fmt .Fprintln (w )
145214 }
146215
147216 case DiffTypeValues :
148217 fmt .Fprintf (w , " ├─ Matching: %d rows\n " , diff .MatchingRows )
149- fmt .Fprintf (w , " └─ Modified: %d rows\n " , diff .ModifiedRows )
218+ fmt .Fprintf (w , " └─ %s \n " , f . colorize ( fmt . Sprintf ( " Modified: %d rows" , diff .ModifiedRows ), colorYellow ) )
150219 fmt .Fprintln (w )
151220
152221 if len (diff .ModifiedSamples ) > 0 {
153- fmt .Fprintf (w , " MODIFIED ROWS (showing %d of %d):\n " , len (diff .ModifiedSamples ), diff .ModifiedRows )
222+ fmt .Fprintf (w , " %s \n " , f . colorize ( fmt . Sprintf ( " MODIFIED ROWS (showing %d of %d):" , len (diff .ModifiedSamples ), diff .ModifiedRows ), colorYellow ) )
154223 for i , sample := range diff .ModifiedSamples {
155224 fmt .Fprintf (w , " Row #%d:\n " , i + 1 )
156- fmt .Fprintf (w , " Expected: %s\n " , f .formatRow (diff .Columns , sample .ExpectedRow ))
157- fmt .Fprintf (w , " Actual: %s\n " , f .formatRow (diff .Columns , sample .ActualRow ))
225+ fmt .Fprintf (w , " %s %s\n " , f . colorize ( "Expected:" , colorRed ) , f .formatRow (diff .Columns , sample .ExpectedRow ))
226+ fmt .Fprintf (w , " %s %s\n " , f . colorize ( "Actual: " , colorGreen ) , f .formatRow (diff .Columns , sample .ActualRow ))
158227 }
159228 }
160229 }
@@ -190,9 +259,9 @@ func formatValue(v any) string {
190259func (f * ConsoleFormatter ) printWarnings (warnings []PlanWarning , w io.Writer ) {
191260 for _ , warning := range warnings {
192261 if warning .Severity == "warning" {
193- fmt .Fprintf (w , " ⚠️ %s\n " , warning .Message )
262+ fmt .Fprintf (w , " %s %s\n " , f . colorize ( "⚠️" , colorYellow ) , warning .Message )
194263 if warning .Suggestion != "" {
195- fmt .Fprintf (w , " Suggestion: %s\n " , warning .Suggestion )
264+ fmt .Fprintf (w , " %s %s\n " , f . colorize ( "Suggestion:" , colorDim ) , warning .Suggestion )
196265 }
197266 }
198267 }
@@ -204,32 +273,34 @@ func (f *ConsoleFormatter) Finish(s *TestSummary, w io.Writer) error {
204273
205274 // Summary section
206275 fmt .Fprintln (w , "RESULTS:" )
207- fmt .Fprintf (w , " ✓ %d passing\n " , s .Passed )
276+ fmt .Fprintf (w , " %s %d passing\n " , f . colorize ( "✓" , colorGreen ) , s .Passed )
208277 if s .Failed > 0 {
209- fmt .Fprintf (w , " ✗ %d failing\n " , s .Failed )
278+ fmt .Fprintf (w , " %s %d failing\n " , f . colorize ( "✗" , colorRed ) , s .Failed )
210279 }
211280 if s .Pending > 0 {
212- fmt .Fprintf (w , " ? %d pending (no baseline)\n " , s .Pending )
281+ fmt .Fprintf (w , " %s %d pending (no baseline)\n " , f . colorize ( "?" , colorCyan ) , s .Pending )
213282 }
214283 if s .Skipped > 0 {
215- fmt .Fprintf (w , " - %d skipped\n " , s .Skipped )
284+ fmt .Fprintf (w , " %s %d skipped\n " , f . colorize ( "-" , colorDim ) , s .Skipped )
216285 }
217286 fmt .Fprintf (w , " %.2fs total\n " , s .Duration )
218287
219288 // Failing tests details
220289 if s .Failed > 0 {
221290 fmt .Fprintln (w )
222- fmt .Fprintln (w , "FAILING:" )
291+ fmt .Fprintln (w , f . colorize ( "FAILING:" , colorRed ) )
223292 for _ , r := range f .results {
224293 if r .Status == "failed" {
225294 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 )
295+ if ! f .options .NoDiff {
296+ if r .Type == "cost" {
297+ f .printCostFailure (r , w )
298+ } else if r .Type == "output" {
299+ f .printOutputDiff (r , w )
300+ }
230301 }
231302 if r .Error != "" {
232- fmt .Fprintf (w , " Error: %s\n " , r .Error )
303+ fmt .Fprintf (w , " %s %s\n " , f . colorize ( "Error:" , colorRed ) , r .Error )
233304 }
234305 }
235306 }
@@ -244,7 +315,7 @@ func (f *ConsoleFormatter) Finish(s *TestSummary, w io.Writer) error {
244315 }
245316 if len (warnings ) > 0 {
246317 fmt .Fprintln (w )
247- fmt .Fprintln (w , "WARNINGS:" )
318+ fmt .Fprintln (w , f . colorize ( "WARNINGS:" , colorYellow ) )
248319 for _ , r := range warnings {
249320 fmt .Fprintf (w , " %s\n " , r .Name )
250321 f .printWarnings (r .PlanWarnings , w )
@@ -254,7 +325,7 @@ func (f *ConsoleFormatter) Finish(s *TestSummary, w io.Writer) error {
254325 // Pending tests
255326 if s .Pending > 0 {
256327 fmt .Fprintln (w )
257- fmt .Fprintln (w , "PENDING (no baseline):" )
328+ fmt .Fprintln (w , f . colorize ( "PENDING (no baseline):" , colorCyan ) )
258329 for _ , r := range f .results {
259330 if r .Status == "pending" {
260331 fmt .Fprintf (w , " %s\n " , r .Name )
0 commit comments