Skip to content

Commit 67eb813

Browse files
authored
feat: show files metadata in CLI and move summary to bottom (#117)
* feat(cli): move report totals to bottom * log metadata about files found and duration
1 parent b6c69d8 commit 67eb813

6 files changed

Lines changed: 103 additions & 60 deletions

File tree

src/cli/arguments.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ test.describe('--min-coverage', () => {
4747
test.describe('--min-file-coverage', () => {
4848
let args = ['coverage', '--min-coverage=1']
4949

50-
test('missing --min-file-coverage defaults to 0', () => {
50+
test('missing --min-file-coverage defaults to undefined', () => {
5151
let result = parse_arguments([...args])
52-
expect(result['min-file-coverage']).toEqual(0)
52+
expect(result['min-file-coverage']).toBeUndefined()
5353
})
5454

5555
test('empty --min-file-coverage', () => {

src/cli/arguments.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ type Reporter = (typeof REPORTERS)[number]
1010
export type CliArguments = {
1111
'coverage-dir': string
1212
'min-coverage': number
13-
'min-file-coverage': number
13+
'min-file-coverage'?: number
1414
'show-uncovered': ShowUncovered
1515
reporter: Reporter
1616
}
@@ -21,7 +21,7 @@ export function parse_arguments(args: string[]): CliArguments {
2121
allowPositionals: true,
2222
options: {
2323
'min-coverage': { type: 'string' },
24-
'min-file-coverage': { type: 'string', default: '0' },
24+
'min-file-coverage': { type: 'string' },
2525
'show-uncovered': { type: 'string', default: 'violations' },
2626
reporter: { type: 'string', default: 'pretty' },
2727
},
@@ -50,9 +50,13 @@ export function parse_arguments(args: string[]): CliArguments {
5050
issues.push('--min-coverage must be a number between 0 and 1')
5151
}
5252

53-
let min_file_coverage = Number(values['min-file-coverage'])
54-
if (isNaN(min_file_coverage) || min_file_coverage < 0 || min_file_coverage > 1) {
55-
issues.push('--min-file-coverage must be a number between 0 and 1')
53+
let min_file_coverage
54+
if (values['min-file-coverage'] !== undefined) {
55+
min_file_coverage = Number(values['min-file-coverage'])
56+
57+
if (isNaN(min_file_coverage) || min_file_coverage < 0 || min_file_coverage > 1) {
58+
issues.push('--min-file-coverage must be a number between 0 and 1')
59+
}
5660
}
5761

5862
let show_uncovered = values['show-uncovered'] as ShowUncovered

src/cli/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { print as json } from './reporters/json.js'
99
import { help } from './help.js'
1010

1111
async function cli(cli_args: string[]) {
12+
let start_time = performance.now()
1213
if (
1314
!cli_args ||
1415
cli_args.length === 0 ||
@@ -24,6 +25,7 @@ async function cli(cli_args: string[]) {
2425
{
2526
min_coverage: params['min-coverage'],
2627
min_file_coverage: params['min-file-coverage'],
28+
start_time,
2729
},
2830
coverage_data,
2931
)

src/cli/program.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
export type Report = {
1111
context: {
1212
coverage: CoverageResult
13+
duration: number
1314
}
1415
report: {
1516
ok: boolean
@@ -54,9 +55,11 @@ export function program(
5455
{
5556
min_coverage,
5657
min_file_coverage,
58+
start_time = 0,
5759
}: {
5860
min_coverage: number
5961
min_file_coverage?: number
62+
start_time?: number
6063
},
6164
coverage_data: Coverage[],
6265
) {
@@ -66,10 +69,12 @@ export function program(
6669
Math.min(...coverage.coverage_per_stylesheet.map((sheet) => sheet.line_coverage_ratio)),
6770
min_file_coverage,
6871
)
72+
let end_time = performance.now()
6973

7074
let result: Report = {
7175
context: {
7276
coverage,
77+
duration: end_time - start_time,
7378
},
7479
report: {
7580
ok: min_coverage_result.ok && min_file_coverage_result.ok,

src/cli/reporters/pretty.test.ts

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,24 @@ const show_violations = { 'show-uncovered': 'violations' } as CliArguments
6262

6363
const context_empty = {
6464
context: {
65-
coverage: {} as CoverageResult,
65+
coverage: {
66+
total_files_found: 7,
67+
total_stylesheets: 8,
68+
total_lines: 9999,
69+
} as CoverageResult,
70+
duration: 2,
6671
},
6772
}
6873

6974
const context_with_failures = {
7075
context: {
76+
duration: 2,
7177
coverage: {
7278
line_coverage_ratio: 0.4022222,
7379
covered_lines: 10,
7480
total_lines: 11,
81+
total_files_found: 7,
82+
total_stylesheets: 9,
7583
coverage_per_stylesheet: [
7684
{
7785
url: 'example.com',
@@ -157,7 +165,10 @@ test.describe('only --min-line-coverage', () => {
157165
},
158166
} satisfies Report
159167
let result = print(report, show_none, dependencies)
160-
expect(result).toEqual(['Success: total line coverage is 50.22%'])
168+
expect(result).toEqual([
169+
'Finished in 2ms on 7 JSON files containing 8 stylesheets with 9,999 lines of CSS in total.',
170+
'Success: total line coverage is 50.22%',
171+
])
161172
})
162173

163174
test('failure', () => {
@@ -166,7 +177,10 @@ test.describe('only --min-line-coverage', () => {
166177
coverage: {
167178
total_lines: 10_000,
168179
covered_lines: 5022,
180+
total_files_found: 7,
181+
total_stylesheets: 8,
169182
} as CoverageResult,
183+
duration: 2,
170184
},
171185
report: {
172186
ok: false,
@@ -176,8 +190,9 @@ test.describe('only --min-line-coverage', () => {
176190
} satisfies Report
177191
let result = print(report, show_none, dependencies)
178192
expect(result).toEqual([
193+
'Finished in 2ms on 7 JSON files containing 8 stylesheets with 10,000 lines of CSS in total.',
179194
'Failed: line coverage is 50.22%% which is lower than the threshold of 1',
180-
'Tip: cover 4978 more lines to meet the threshold of 100%',
195+
'Tip: cover 4,978 more lines to meet the threshold of 100%',
181196
])
182197
})
183198
})
@@ -194,6 +209,7 @@ test.describe('with --min-file-line-coverage', () => {
194209
} satisfies Report
195210
let result = print(report, show_none, dependencies)
196211
expect(result).toEqual([
212+
'Finished in 2ms on 7 JSON files containing 8 stylesheets with 9,999 lines of CSS in total.',
197213
'Success: total line coverage is 50.22%',
198214
'Success: all files pass minimum line coverage of 50.00%',
199215
])
@@ -210,21 +226,26 @@ test.describe('with --min-file-line-coverage', () => {
210226
} satisfies Report
211227
let result = print(report, show_none, dependencies)
212228

229+
test('metadata', () => {
230+
expect(result[0]).toEqual(
231+
'Finished in 2ms on 7 JSON files containing 9 stylesheets with 11 lines of CSS in total.',
232+
)
233+
})
213234
test('coverage: pass', () => {
214-
expect(result[0]).toEqual('Success: total line coverage is 50.22%')
235+
expect(result[1]).toEqual('Success: total line coverage is 50.22%')
215236
})
216237
test('file-coverage: fail', () => {
217-
expect(result[1]).toEqual(
238+
expect(result[2]).toEqual(
218239
'Failed: 1 file does not meet the minimum line coverage of 100% (minimum coverage was 50.00%)',
219240
)
220241
})
221242
test('shows hint to --show=violations', () => {
222-
expect(result[2]).toEqual(
243+
expect(result[3]).toEqual(
223244
" Hint: set --show-uncovered=violations to see which files didn't pass",
224245
)
225246
})
226247
test('no files shown', () => {
227-
expect(result).toHaveLength(3)
248+
expect(result).toHaveLength(4)
228249
})
229250
})
230251

@@ -240,20 +261,20 @@ test.describe('with --min-file-line-coverage', () => {
240261
let result = print(report, show_violations, dependencies)
241262

242263
test('coverage: pass', () => {
243-
expect(result[0]).toEqual('Success: total line coverage is 50.22%')
264+
expect(result.at(-2)).toEqual('Success: total line coverage is 50.22%')
244265
})
245266
test('file-coverage: fail', () => {
246-
expect(result[1]).toEqual(
267+
expect(result.at(-1)).toEqual(
247268
'Failed: 1 file does not meet the minimum line coverage of 100% (minimum coverage was 50.00%)',
248269
)
249270
})
250271
test('does not show hint to --show=violations', () => {
251-
expect(result[2]).not.toEqual(
272+
expect(result.at(-1)).not.toEqual(
252273
" Hint: set --show-uncovered=violations to see which files didn't pass",
253274
)
254275
})
255276
test.describe('shows file details', () => {
256-
let lines = result.slice(2)
277+
let lines = result
257278

258279
test('shows header block', () => {
259280
expect(lines[0]).toEqual('─'.repeat(60))
@@ -299,7 +320,10 @@ Tip: cover 11 more lines to meet the file threshold of 100%
299320
▌ 17 │ c {
300321
▌ 18 │ color: red;
301322
▌ 19 │ }
302-
`,
323+
324+
Finished in 2ms on 7 JSON files containing 9 stylesheets with 11 lines of CSS in total.
325+
Success: total line coverage is 50.22%
326+
Failed: 1 file does not meet the minimum line coverage of 100% (minimum coverage was 50.00%)`,
303327
)
304328
})
305329
})

src/cli/reporters/pretty.ts

Lines changed: 49 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ function percentage(ratio: number, decimals: number = 2): string {
1414
return `${(ratio * 100).toFixed(ratio === 1 ? 0 : decimals)}%`
1515
}
1616

17+
function number(num: number): string {
18+
return new Intl.NumberFormat().format(num)
19+
}
20+
1721
export type TextStyle =
1822
| 'bold'
1923
| 'red'
@@ -90,54 +94,13 @@ export function print_lines(
9094
) {
9195
let output: (string | undefined)[] = []
9296

93-
if (report.min_line_coverage.ok) {
94-
output.push(
95-
`${styleText(['bold', 'green'], 'Success')}: total line coverage is ${percentage(report.min_line_coverage.actual)}`,
96-
)
97-
} else {
98-
let { actual, expected } = report.min_line_coverage
99-
output.push(
100-
`${styleText(['bold', 'red'], 'Failed')}: line coverage is ${percentage(actual)}% which is lower than the threshold of ${expected}`,
101-
)
102-
let lines_to_cover = expected * context.coverage.total_lines - context.coverage.covered_lines
103-
output.push(
104-
`Tip: cover ${Math.ceil(lines_to_cover)} more ${lines_to_cover === 1 ? 'line' : 'lines'} to meet the threshold of ${percentage(
105-
expected,
106-
)}`,
107-
)
108-
}
109-
110-
if (report.min_file_line_coverage.expected !== undefined) {
111-
let { expected, actual, ok } = report.min_file_line_coverage
112-
if (ok) {
113-
output.push(
114-
`${styleText(['bold', 'green'], 'Success')}: all files pass minimum line coverage of ${percentage(expected)}`,
115-
)
116-
} else {
117-
let num_files_failed = context.coverage.coverage_per_stylesheet.filter(
118-
(sheet) => sheet.line_coverage_ratio < expected!,
119-
).length
120-
output.push(
121-
`${styleText(['bold', 'red'], 'Failed')}: ${num_files_failed} ${
122-
num_files_failed === 1 ? 'file does' : 'files do'
123-
} not meet the minimum line coverage of ${percentage(expected)} (minimum coverage was ${percentage(actual)})`,
124-
)
125-
if (params['show-uncovered'] === 'none') {
126-
output.push(` Hint: set --show-uncovered=violations to see which files didn't pass`)
127-
}
128-
}
129-
}
130-
13197
// Show un-covered chunks
13298
if (params['show-uncovered'] !== 'none') {
13399
const NUM_LEADING_LINES = 3
134100
const NUM_TRAILING_LINES = NUM_LEADING_LINES
135101
print_width = print_width ?? 80
136102
let min_file_line_coverage = report.min_file_line_coverage.expected
137103

138-
// Show empty line between report header and chunks output
139-
output.push()
140-
141104
for (let sheet of context.coverage.coverage_per_stylesheet.sort(
142105
(a, b) => a.line_coverage_ratio - b.line_coverage_ratio,
143106
)) {
@@ -213,6 +176,51 @@ export function print_lines(
213176
}
214177
}
215178

179+
// Show empty line between report summary and chunks output
180+
output.push()
181+
182+
output.push(
183+
`Finished in ${number(Math.round(context.duration))}ms on ${number(context.coverage.total_files_found)} JSON files containing ${number(context.coverage.total_stylesheets)} stylesheets with ${number(context.coverage.total_lines)} lines of CSS in total.`,
184+
)
185+
186+
if (report.min_line_coverage.ok) {
187+
output.push(
188+
`${styleText(['bold', 'green'], 'Success')}: total line coverage is ${percentage(report.min_line_coverage.actual)}`,
189+
)
190+
} else {
191+
let { actual, expected } = report.min_line_coverage
192+
output.push(
193+
`${styleText(['bold', 'red'], 'Failed')}: line coverage is ${percentage(actual)}% which is lower than the threshold of ${expected}`,
194+
)
195+
let lines_to_cover = expected * context.coverage.total_lines - context.coverage.covered_lines
196+
output.push(
197+
`Tip: cover ${number(Math.ceil(lines_to_cover))} more ${lines_to_cover === 1 ? 'line' : 'lines'} to meet the threshold of ${percentage(
198+
expected,
199+
)}`,
200+
)
201+
}
202+
203+
if (report.min_file_line_coverage.expected !== undefined) {
204+
let { expected, actual, ok } = report.min_file_line_coverage
205+
if (ok) {
206+
output.push(
207+
`${styleText(['bold', 'green'], 'Success')}: all files pass minimum line coverage of ${percentage(expected)}`,
208+
)
209+
} else {
210+
let num_files_failed = context.coverage.coverage_per_stylesheet.filter(
211+
(sheet) => sheet.line_coverage_ratio < expected,
212+
).length
213+
output.push(
214+
`${styleText(['bold', 'red'], 'Failed')}: ${number(num_files_failed)} ${
215+
num_files_failed === 1 ? 'file does' : 'files do'
216+
} not meet the minimum line coverage of ${percentage(expected)} (minimum coverage was ${percentage(actual)})`,
217+
)
218+
if (params['show-uncovered'] === 'none') {
219+
output.push(` Hint: set --show-uncovered=violations to see which files didn't pass`)
220+
}
221+
}
222+
}
223+
216224
return output
217225
}
218226

0 commit comments

Comments
 (0)