@@ -30,11 +30,22 @@ type packageCoverage struct {
3030 coverage float64
3131}
3232
33- var verbose bool
33+ type improvement struct {
34+ name string
35+ baseline float64
36+ current float64
37+ }
38+
39+ var (
40+ verbose bool
41+ updateBaseline bool
42+ )
3443
3544func main () {
3645 flag .BoolVar (& verbose , "v" , false , "verbose output (show all checks)" )
3746 flag .BoolVar (& verbose , "verbose" , false , "verbose output (show all checks)" )
47+ flag .BoolVar (& updateBaseline , "u" , false , "update baseline with current coverage" )
48+ flag .BoolVar (& updateBaseline , "update" , false , "update baseline with current coverage" )
3849 flag .Parse ()
3950
4051 if err := run (); err != nil {
@@ -69,11 +80,26 @@ func run() error {
6980 return fmt .Errorf ("failed to load baseline: %w" , err )
7081 }
7182
72- // Check baseline packages for regression
73- passed , regressions := checkBaseline (baseline , currentCoverage )
83+ // Check baseline packages for regression and track improvements
84+ passed , regressions , improvements , improvementList := checkBaseline (baseline , currentCoverage )
7485
7586 // Check new packages
76- newPassed , newFailed := checkNewPackages (baseline , currentCoverage )
87+ newPassed , newFailed , newPackages := checkNewPackages (baseline , currentCoverage )
88+
89+ // Handle update flag
90+ if updateBaseline {
91+ if err := writeUpdatedBaseline (baseline , currentCoverage , newPackages ); err != nil {
92+ return fmt .Errorf ("failed to update baseline: %w" , err )
93+ }
94+ fmt .Printf ("%s✅ Baseline updated successfully%s\n " , colorGreen , colorReset )
95+ if improvements > 0 {
96+ fmt .Printf (" %d package(s) improved\n " , improvements )
97+ }
98+ if len (newPackages ) > 0 {
99+ fmt .Printf (" %d new package(s) added\n " , len (newPackages ))
100+ }
101+ return nil
102+ }
77103
78104 // Summary
79105 if verbose {
@@ -84,6 +110,9 @@ func run() error {
84110 fmt .Printf ("Baseline packages checked: %d\n " , passed + regressions )
85111 fmt .Printf (" - Passed: %d\n " , passed )
86112 fmt .Printf (" - Regressions: %d\n " , regressions )
113+ if improvements > 0 {
114+ fmt .Printf (" - Improvements: %d\n " , improvements )
115+ }
87116 fmt .Printf ("New packages checked: %d\n " , newPassed + newFailed )
88117 if newPassed + newFailed > 0 {
89118 fmt .Printf (" - Passed: %d\n " , newPassed )
@@ -92,9 +121,9 @@ func run() error {
92121 fmt .Println ()
93122 }
94123
124+ // Check for failures (regressions or new packages below threshold)
95125 if regressions > 0 || newFailed > 0 {
96126 if ! verbose {
97- // In quiet mode, print a brief summary of what failed
98127 fmt .Printf ("%s❌ Coverage check FAILED%s\n " , colorRed , colorReset )
99128 if regressions > 0 {
100129 fmt .Printf (" %d package(s) regressed below baseline\n " , regressions )
@@ -110,12 +139,35 @@ func run() error {
110139 fmt .Println ("To fix:" )
111140 fmt .Println (" 1. Add tests to improve coverage for failing packages" )
112141 fmt .Println (" 2. If coverage drop is intentional, update baseline:" )
113- fmt .Println (" - Edit node/. coverage-baseline" )
114- fmt .Println (" - Lower the threshold for the affected package " )
142+ fmt .Println (" - Run: make update- coverage-baseline" )
143+ fmt .Println (" - Or: ./coverage-check -u " )
115144 }
116145 return fmt .Errorf ("coverage check failed" )
117146 }
118147
148+ // Check for improvements - exit with code 1 to force baseline update
149+ if improvements > 0 || len (newPackages ) > 0 {
150+ fmt .Printf ("%s💡 Coverage improved!%s\n " , colorYellow , colorReset )
151+ if improvements > 0 {
152+ fmt .Printf (" %d package(s) have better coverage than baseline:\n " , improvements )
153+ for _ , pkg := range improvementList {
154+ fmt .Printf (" - %s: %.1f%% → %.1f%%\n " , pkg .name , pkg .baseline , pkg .current )
155+ }
156+ }
157+ if len (newPackages ) > 0 {
158+ fmt .Printf (" %d new package(s) with coverage:\n " , len (newPackages ))
159+ for _ , pkg := range newPackages {
160+ fmt .Printf (" - %s: %.1f%%\n " , pkg .name , pkg .coverage )
161+ }
162+ }
163+ fmt .Println ()
164+ fmt .Printf ("%sPlease update the baseline to lock in these improvements:%s\n " , colorYellow , colorReset )
165+ fmt .Println (" Run: make update-coverage-baseline" )
166+ fmt .Println (" Or: ./coverage-check -u" )
167+ return fmt .Errorf ("baseline update required" )
168+ }
169+
170+ // All checks passed, no improvements
119171 if verbose {
120172 fmt .Printf ("%s✅ All coverage checks PASSED%s\n " , colorGreen , colorReset )
121173 }
@@ -202,7 +254,7 @@ func loadBaseline() (map[string]float64, error) {
202254}
203255
204256// checkBaseline compares current coverage against baseline
205- func checkBaseline (baseline , current map [string ]float64 ) (passed , regressions int ) {
257+ func checkBaseline (baseline , current map [string ]float64 ) (passed , regressions , improvements int , improvementList [] improvement ) {
206258 if verbose {
207259 fmt .Println ("Checking baseline packages for regression..." )
208260 fmt .Println ("--------------------------------------------" )
@@ -228,6 +280,19 @@ func checkBaseline(baseline, current map[string]float64) (passed, regressions in
228280 fmt .Printf ("%s Coverage dropped from %.1f%% to %.1f%%%s\n " ,
229281 colorRed , baselineCov , currentCov , colorReset )
230282 regressions ++
283+ } else if currentCov > baselineCov + coverageTolerance {
284+ // Coverage improved
285+ if verbose {
286+ fmt .Printf ("%s📈 %s: %.1f%% (baseline: %.1f%%, +%.1f%%)%s\n " ,
287+ colorGreen , pkg , currentCov , baselineCov , currentCov - baselineCov , colorReset )
288+ }
289+ improvements ++
290+ improvementList = append (improvementList , improvement {
291+ name : pkg ,
292+ baseline : baselineCov ,
293+ current : currentCov ,
294+ })
295+ passed ++
231296 } else {
232297 if verbose {
233298 fmt .Printf ("%s✅ %s: %.1f%% (baseline: %.1f%%)%s\n " ,
@@ -239,15 +304,19 @@ func checkBaseline(baseline, current map[string]float64) (passed, regressions in
239304
240305 if verbose {
241306 fmt .Println ()
242- fmt .Printf ("Baseline check: %d passed, %d regressions\n " , passed , regressions )
307+ fmt .Printf ("Baseline check: %d passed, %d regressions" , passed , regressions )
308+ if improvements > 0 {
309+ fmt .Printf (", %d improvements" , improvements )
310+ }
311+ fmt .Println ()
243312 fmt .Println ()
244313 }
245314
246- return passed , regressions
315+ return passed , regressions , improvements , improvementList
247316}
248317
249318// checkNewPackages checks that new packages meet minimum coverage requirements
250- func checkNewPackages (baseline , current map [string ]float64 ) (passed , failed int ) {
319+ func checkNewPackages (baseline , current map [string ]float64 ) (passed , failed int , newPackages [] packageCoverage ) {
251320 if verbose {
252321 fmt .Println ("Checking new packages for minimum coverage..." )
253322 fmt .Println ("----------------------------------------------" )
@@ -279,6 +348,10 @@ func checkNewPackages(baseline, current map[string]float64) (passed, failed int)
279348 fmt .Printf ("%s✅ NEW PACKAGE: %s has %.1f%% coverage (meets %.1f%% minimum)%s\n " ,
280349 colorGreen , pkg , cov , minNewPkgCoverage , colorReset )
281350 }
351+ newPackages = append (newPackages , packageCoverage {
352+ name : pkg ,
353+ coverage : cov ,
354+ })
282355 passed ++
283356 }
284357 }
@@ -287,7 +360,7 @@ func checkNewPackages(baseline, current map[string]float64) (passed, failed int)
287360 fmt .Println ("No new packages found" )
288361 }
289362
290- return passed , failed
363+ return passed , failed , newPackages
291364}
292365
293366// shouldExclude determines if a package should be excluded from new package checks
@@ -318,3 +391,82 @@ func shouldExclude(pkg string) bool {
318391
319392 return false
320393}
394+
395+ // writeUpdatedBaseline writes an updated baseline file with current coverage
396+ func writeUpdatedBaseline (baseline , current map [string ]float64 , newPackages []packageCoverage ) error {
397+ // Read the original baseline to preserve comments and structure
398+ originalFile , err := os .Open (baselineFile )
399+ if err != nil {
400+ return err
401+ }
402+ defer originalFile .Close ()
403+
404+ // Create a temporary file in the same directory as the baseline to avoid cross-device link issues
405+ tempFile , err := os .CreateTemp ("." , ".coverage-baseline-*.tmp" )
406+ if err != nil {
407+ return err
408+ }
409+ defer os .Remove (tempFile .Name ())
410+
411+ writer := bufio .NewWriter (tempFile )
412+ scanner := bufio .NewScanner (originalFile )
413+
414+ // Track which packages we've updated
415+ updated := make (map [string ]bool )
416+
417+ // Process existing baseline file, updating coverage values
418+ for scanner .Scan () {
419+ line := scanner .Text ()
420+ trimmed := strings .TrimSpace (line )
421+
422+ // Preserve comments and empty lines
423+ if trimmed == "" || strings .HasPrefix (trimmed , "#" ) {
424+ fmt .Fprintln (writer , line )
425+ continue
426+ }
427+
428+ // Parse package line
429+ parts := strings .Fields (trimmed )
430+ if len (parts ) != 2 {
431+ // Invalid format, keep as-is
432+ fmt .Fprintln (writer , line )
433+ continue
434+ }
435+
436+ pkg := parts [0 ]
437+ if currentCov , exists := current [pkg ]; exists {
438+ // Update with current coverage
439+ fmt .Fprintf (writer , "%s %.1f\n " , pkg , currentCov )
440+ updated [pkg ] = true
441+ } else {
442+ // Package not found in current run, keep baseline value (might be removed/renamed)
443+ fmt .Fprintln (writer , line )
444+ }
445+ }
446+
447+ if err := scanner .Err (); err != nil {
448+ return err
449+ }
450+
451+ // Add new packages if any
452+ if len (newPackages ) > 0 {
453+ fmt .Fprintln (writer , "" )
454+ fmt .Fprintln (writer , "# Newly added packages" )
455+ for _ , pkg := range newPackages {
456+ fmt .Fprintf (writer , "%s %.1f\n " , pkg .name , pkg .coverage )
457+ updated [pkg .name ] = true
458+ }
459+ }
460+
461+ if err := writer .Flush (); err != nil {
462+ return err
463+ }
464+ tempFile .Close ()
465+
466+ // Replace original file with updated file
467+ if err := os .Rename (tempFile .Name (), baselineFile ); err != nil {
468+ return err
469+ }
470+
471+ return nil
472+ }
0 commit comments