@@ -10,12 +10,13 @@ interface CoverageData {
1010 statements : string ;
1111}
1212
13+ // Allowed coverage drift (percent)
14+ const COVERAGE_TOLERANCE = 0.2 ;
15+
1316/**
1417 * Parses coverage from lcov.info file
1518 */
1619function parseLcovCoverage ( lcovPath : string ) : CoverageData {
17- // Normalize line endings to handle both CRLF (Windows) and LF (Unix)
18- // This ensures consistent parsing regardless of how lcov.info was generated
1920 const content = fs . readFileSync ( lcovPath , "utf8" ) . replace ( / \r \n / g, "\n" ) ;
2021
2122 let linesFound = 0 ;
@@ -43,23 +44,21 @@ function parseLcovCoverage(lcovPath: string): CoverageData {
4344 }
4445
4546 return {
46- lines : linesFound > 0 ? ( linesHit / linesFound * 100 ) . toFixed ( 2 ) : "0" ,
47- functions : functionsFound > 0 ? ( functionsHit / functionsFound * 100 ) . toFixed ( 2 ) : "0" ,
48- branches : branchesFound > 0 ? ( branchesHit / branchesFound * 100 ) . toFixed ( 2 ) : "0" ,
49- statements : linesFound > 0 ? ( linesHit / linesFound * 100 ) . toFixed ( 2 ) : "0"
47+ lines : linesFound > 0 ? ( ( linesHit / linesFound ) * 100 ) . toFixed ( 2 ) : "0" ,
48+ functions : functionsFound > 0 ? ( ( functionsHit / functionsFound ) * 100 ) . toFixed ( 2 ) : "0" ,
49+ branches : branchesFound > 0 ? ( ( branchesHit / branchesFound ) * 100 ) . toFixed ( 2 ) : "0" ,
50+ statements : linesFound > 0 ? ( ( linesHit / linesFound ) * 100 ) . toFixed ( 2 ) : "0" ,
5051 } ;
5152}
5253
5354// Main
5455const lcovPath = path . join ( __dirname , ".." , "coverage" , "lcov.info" ) ;
5556
56- // Check if custom baseline path provided (for CI to compare against main)
5757const baselineArg = process . argv . find ( arg => arg . startsWith ( "--baseline=" ) ) ;
5858const baselinePath = baselineArg
5959 ? baselineArg . split ( "=" ) [ 1 ]
6060 : path . join ( __dirname , ".." , "coverage-baseline.json" ) ;
6161
62- // Check if we're updating baseline
6362const isUpdatingBaseline = process . argv . includes ( "--update-baseline" ) ;
6463
6564if ( ! fs . existsSync ( lcovPath ) ) {
@@ -69,7 +68,7 @@ if (!fs.existsSync(lcovPath)) {
6968
7069const current = parseLcovCoverage ( lcovPath ) ;
7170
72- // If updating baseline, save and exit
71+ // Update baseline mode
7372if ( isUpdatingBaseline ) {
7473 fs . writeFileSync ( baselinePath , JSON . stringify ( current , null , 2 ) ) ;
7574 console . log ( "\n✅ Coverage baseline updated:" ) ;
@@ -81,7 +80,13 @@ if (isUpdatingBaseline) {
8180}
8281
8382// Load baseline
84- let baseline : CoverageData = { lines : "0" , functions : "0" , branches : "0" , statements : "0" } ;
83+ let baseline : CoverageData = {
84+ lines : "0" ,
85+ functions : "0" ,
86+ branches : "0" ,
87+ statements : "0" ,
88+ } ;
89+
8590if ( fs . existsSync ( baselinePath ) ) {
8691 baseline = JSON . parse ( fs . readFileSync ( baselinePath , "utf8" ) ) as CoverageData ;
8792}
@@ -95,27 +100,34 @@ console.log(`Branches: ${baseline.branches}% → ${current.branches}%`);
95100console . log ( `Statements: ${ baseline . statements } % → ${ current . statements } %` ) ;
96101console . log ( "─" . repeat ( 50 ) ) ;
97102
98- // Check for drops
99- const drops : string [ ] = [ ] ;
100- if ( parseFloat ( current . lines ) < parseFloat ( baseline . lines ) ) {
101- drops . push ( `Lines dropped: ${ baseline . lines } % → ${ current . lines } %` ) ;
102- }
103- if ( parseFloat ( current . functions ) < parseFloat ( baseline . functions ) ) {
104- drops . push ( `Functions dropped: ${ baseline . functions } % → ${ current . functions } %` ) ;
105- }
106- if ( parseFloat ( current . branches ) < parseFloat ( baseline . branches ) ) {
107- drops . push ( `Branches dropped: ${ baseline . branches } % → ${ current . branches } %` ) ;
108- }
109- if ( parseFloat ( current . statements ) < parseFloat ( baseline . statements ) ) {
110- drops . push ( `Statements dropped: ${ baseline . statements } % → ${ current . statements } %` ) ;
103+ // Tolerant comparison
104+ function checkDrop ( metric : keyof CoverageData ) : string | null {
105+ const base = parseFloat ( baseline [ metric ] ) ;
106+ const curr = parseFloat ( current [ metric ] ) ;
107+ const diff = curr - base ;
108+
109+ if ( diff < - COVERAGE_TOLERANCE ) {
110+ return `${ metric } dropped: ${ base } % → ${ curr } % (Δ ${ diff . toFixed ( 2 ) } %)` ;
111+ }
112+
113+ return null ;
111114}
112115
116+ const drops = [
117+ checkDrop ( "lines" ) ,
118+ checkDrop ( "functions" ) ,
119+ checkDrop ( "branches" ) ,
120+ checkDrop ( "statements" ) ,
121+ ] . filter ( Boolean ) as string [ ] ;
122+
113123if ( drops . length > 0 ) {
114- console . log ( "\n❌ Coverage decreased:\n" ) ;
115- drops . forEach ( ( drop : string ) => console . log ( ` • ${ drop } ` ) ) ;
116- console . log ( " \n💡 Please add tests to maintain or improve coverage.\n" ) ;
124+ console . log ( "\n❌ Coverage decreased beyond tolerance :\n" ) ;
125+ drops . forEach ( d => console . log ( ` • ${ d } ` ) ) ;
126+ console . log ( ` \n💡 Allowed tolerance: ± ${ COVERAGE_TOLERANCE } %\n` ) ;
117127 process . exit ( 1 ) ;
118128}
119129
120- console . log ( "\n✅ Coverage maintained or improved!\n" ) ;
130+ console . log (
131+ `\n✅ Coverage maintained within tolerance (±${ COVERAGE_TOLERANCE } %)\n`
132+ ) ;
121133process . exit ( 0 ) ;
0 commit comments