better_coverage 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +53 -0
- data/lib/better_coverage.rb +264 -0
- data/lib/minitest_plus/version.rb +5 -0
- metadata +76 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1a4e60897af858e1892d567fe09a3b7db4cd4a68dc00c73dac3ff1ebf02cf6d8
|
|
4
|
+
data.tar.gz: 7705f3ce784cfdffaec20e3c196851076868b960ee1e314e9ce20fedfddddfe5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 90c7595e880ffeeddb5a9ed46570dea21b6b7085039aeeff958bd3805ab5af3d5a1388b8333d480d749d1b332c251d26bc9a1753385f6db3883dbd494778dd1f
|
|
7
|
+
data.tar.gz: f83349baeff5bb74b04037478d3ac8f60f18da104a708b7a5daecf81104905b6ffd89e6c4939a720b17ad53fdfdaf5194c3356a54f0f3ff554e8e26e27f5a17f
|
data/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# better_coverage
|
|
2
|
+
|
|
3
|
+
**better_coverage** is a Minitest reporter that displays SimpleCov coverage results in Jest/Istanbul's console format with a directory tree structure.
|
|
4
|
+
|
|
5
|
+
##### Why?
|
|
6
|
+
|
|
7
|
+
Using better_coverage lets you see coverage in Jest's familiar table format right in your terminal. The reporter organizes files into a directory tree, shows uncovered line ranges (e.g., `5-12,18`), and applies the same color coding as Jest so your Ruby coverage reports look exactly like your JavaScript ones.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Add the gem to your `Gemfile`:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem 'better_coverage'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Then configure in your `test/test_helper.rb`:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
require 'simplecov'
|
|
21
|
+
SimpleCov.start
|
|
22
|
+
|
|
23
|
+
require 'minitest/reporters'
|
|
24
|
+
require 'better_coverage'
|
|
25
|
+
|
|
26
|
+
Minitest::Reporters.use! [
|
|
27
|
+
MinitestPlus::BetterCoverage.new
|
|
28
|
+
]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Run your tests:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
bundle exec rake test
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
#### Options
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
MinitestPlus::BetterCoverage.new(
|
|
41
|
+
max_cols: 120, # Terminal width (default: 80)
|
|
42
|
+
skip_empty: true, # Skip files with no lines (default: false)
|
|
43
|
+
skip_full: true # Skip 100% covered files (default: false)
|
|
44
|
+
)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Contributing
|
|
48
|
+
|
|
49
|
+
Contributions are welcome! If you find a bug or have suggestions for improvement, please open an issue or submit a pull request.
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
Apache License 2.0 © 2025 Mridang Agarwalla
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/reporters'
|
|
4
|
+
|
|
5
|
+
module MinitestPlus
|
|
6
|
+
# Jest-style coverage reporter for Minitest
|
|
7
|
+
# Displays SimpleCov coverage data in Jest's console format
|
|
8
|
+
class BetterCoverage < Minitest::Reporters::BaseReporter # rubocop:disable Metrics/ClassLength
|
|
9
|
+
NAME_COL = 4
|
|
10
|
+
PCT_COLS = 7
|
|
11
|
+
MISSING_COL = 17
|
|
12
|
+
TAB_SIZE = 1
|
|
13
|
+
DELIM = ' | '
|
|
14
|
+
|
|
15
|
+
def initialize(options = {})
|
|
16
|
+
super({})
|
|
17
|
+
@max_cols = options[:max_cols] || 80
|
|
18
|
+
@skip_empty = options[:skip_empty] || false
|
|
19
|
+
@skip_full = options[:skip_full] || false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def report
|
|
23
|
+
super
|
|
24
|
+
return unless defined?(SimpleCov)
|
|
25
|
+
|
|
26
|
+
result = SimpleCov.result
|
|
27
|
+
return unless result
|
|
28
|
+
|
|
29
|
+
print_coverage_table(result)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
35
|
+
def print_coverage_table(result)
|
|
36
|
+
files = result.files.sort_by(&:filename)
|
|
37
|
+
return if files.empty?
|
|
38
|
+
|
|
39
|
+
tree = build_tree(files)
|
|
40
|
+
name_width = [NAME_COL, calculate_max_width(tree, 0)].max
|
|
41
|
+
missing_width = MISSING_COL
|
|
42
|
+
|
|
43
|
+
if @max_cols.positive?
|
|
44
|
+
# Only 3 columns now: File | % Lines | Uncovered
|
|
45
|
+
pct_cols = DELIM.length + PCT_COLS + DELIM.length
|
|
46
|
+
max_remaining = @max_cols - (pct_cols + MISSING_COL)
|
|
47
|
+
|
|
48
|
+
name_width = max_remaining if name_width > max_remaining
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
puts
|
|
52
|
+
puts make_line(name_width, missing_width)
|
|
53
|
+
puts table_header(name_width, missing_width)
|
|
54
|
+
puts make_line(name_width, missing_width)
|
|
55
|
+
print_tree(tree, name_width, missing_width, 0)
|
|
56
|
+
|
|
57
|
+
total_pct = result.covered_percent
|
|
58
|
+
summary_row = summary_line(total_pct, name_width, missing_width)
|
|
59
|
+
puts make_line(name_width, missing_width)
|
|
60
|
+
puts summary_row
|
|
61
|
+
puts make_line(name_width, missing_width)
|
|
62
|
+
end
|
|
63
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
64
|
+
|
|
65
|
+
def build_tree(files) # rubocop:disable Metrics/MethodLength
|
|
66
|
+
tree = {}
|
|
67
|
+
|
|
68
|
+
files.each do |file|
|
|
69
|
+
path = relative_path(file.filename)
|
|
70
|
+
parts = path.split('/')
|
|
71
|
+
|
|
72
|
+
current = tree
|
|
73
|
+
parts.each_with_index do |part, idx|
|
|
74
|
+
if idx == parts.length - 1
|
|
75
|
+
current[part] = { file: file }
|
|
76
|
+
else
|
|
77
|
+
current[part] ||= {}
|
|
78
|
+
current = current[part]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
tree
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def calculate_max_width(tree, depth)
|
|
87
|
+
max = 0
|
|
88
|
+
tree.each do |name, node|
|
|
89
|
+
width = (TAB_SIZE * depth) + name.length
|
|
90
|
+
max = width if width > max
|
|
91
|
+
|
|
92
|
+
next unless node[:file].nil?
|
|
93
|
+
|
|
94
|
+
child_max = calculate_max_width(node, depth + 1)
|
|
95
|
+
max = child_max if child_max > max
|
|
96
|
+
end
|
|
97
|
+
max
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def print_tree(tree, name_width, missing_width, depth) # rubocop:disable Metrics/MethodLength
|
|
101
|
+
tree.keys.sort.each do |name|
|
|
102
|
+
node = tree[name]
|
|
103
|
+
|
|
104
|
+
if node[:file]
|
|
105
|
+
row = file_row(node[:file], name, name_width, missing_width, depth)
|
|
106
|
+
puts row unless row.empty?
|
|
107
|
+
else
|
|
108
|
+
row = dir_row(name, node, name_width, missing_width, depth)
|
|
109
|
+
puts row unless row.empty?
|
|
110
|
+
print_tree(node, name_width, missing_width, depth + 1)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
116
|
+
def dir_row(name, node, name_width, missing_width, depth)
|
|
117
|
+
files = collect_files(node)
|
|
118
|
+
return '' if files.empty?
|
|
119
|
+
return '' if @skip_empty && files.all? { |f| f.lines.empty? }
|
|
120
|
+
|
|
121
|
+
# Calculate average coverage across all files
|
|
122
|
+
pct = files.sum(&:covered_percent) / files.size
|
|
123
|
+
|
|
124
|
+
return '' if @skip_full && pct == 100.0 # rubocop:disable Lint/FloatComparison
|
|
125
|
+
|
|
126
|
+
elements = [
|
|
127
|
+
colorize_by_coverage(fill(name, name_width, tabs: depth), pct),
|
|
128
|
+
colorize_by_coverage(fill(pct.round(2), PCT_COLS, right: true), pct),
|
|
129
|
+
fill('', missing_width)
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
"#{elements.join(DELIM)} "
|
|
133
|
+
end
|
|
134
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
135
|
+
|
|
136
|
+
def collect_files(node)
|
|
137
|
+
files = []
|
|
138
|
+
node.each_value do |value|
|
|
139
|
+
if value[:file]
|
|
140
|
+
files << value[:file]
|
|
141
|
+
else
|
|
142
|
+
files.concat(collect_files(value))
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
files
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def file_row(file, name, name_width, missing_width, depth)
|
|
149
|
+
return '' if @skip_empty && file.lines.empty?
|
|
150
|
+
|
|
151
|
+
pct_lines = file.covered_percent.round(2)
|
|
152
|
+
return '' if @skip_full && pct_lines == 100.0 # rubocop:disable Lint/FloatComparison
|
|
153
|
+
|
|
154
|
+
elements = [
|
|
155
|
+
colorize_by_coverage(fill(name, name_width, tabs: depth), pct_lines),
|
|
156
|
+
colorize_by_coverage(fill(pct_lines, PCT_COLS, right: true), pct_lines),
|
|
157
|
+
colorize_uncovered(fill(uncovered_lines(file), missing_width), pct_lines)
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
"#{elements.join(DELIM)} "
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def summary_line(total_pct, name_width, missing_width)
|
|
164
|
+
total_pct_rounded = total_pct.round(2)
|
|
165
|
+
|
|
166
|
+
elements = [
|
|
167
|
+
colorize_by_coverage(fill('All files', name_width), total_pct),
|
|
168
|
+
colorize_by_coverage(fill(total_pct_rounded, PCT_COLS, right: true), total_pct),
|
|
169
|
+
fill('', missing_width)
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
"#{elements.join(DELIM)} "
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def relative_path(filename)
|
|
176
|
+
filename.sub("#{SimpleCov.root}/", '')
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
180
|
+
def uncovered_lines(file)
|
|
181
|
+
covered_lines = file.lines.reject(&:skipped?).map do |line|
|
|
182
|
+
[line.line_number, line.covered? || line.never?]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
new_range = true
|
|
186
|
+
ranges = covered_lines.each_with_object([]) do |(line, hit), acum|
|
|
187
|
+
if hit
|
|
188
|
+
new_range = true
|
|
189
|
+
elsif new_range
|
|
190
|
+
acum.push([line])
|
|
191
|
+
new_range = false
|
|
192
|
+
else
|
|
193
|
+
acum.last[1] = line
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
ranges.map do |range|
|
|
198
|
+
range.length == 1 ? range[0].to_s : "#{range[0]}-#{range[1]}"
|
|
199
|
+
end.join(',')
|
|
200
|
+
end
|
|
201
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
202
|
+
|
|
203
|
+
def fill(text, width, right: false, tabs: 0) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
204
|
+
text = text.to_s
|
|
205
|
+
leading_spaces = tabs * TAB_SIZE
|
|
206
|
+
remaining = width - leading_spaces
|
|
207
|
+
leader = ' ' * leading_spaces
|
|
208
|
+
|
|
209
|
+
return leader if remaining <= 0
|
|
210
|
+
|
|
211
|
+
if remaining >= text.length
|
|
212
|
+
fill_str = ' ' * (remaining - text.length)
|
|
213
|
+
leader + (right ? fill_str + text : text + fill_str)
|
|
214
|
+
else
|
|
215
|
+
fill_str = '...'
|
|
216
|
+
length = remaining - fill_str.length
|
|
217
|
+
text = text[-length..] || text
|
|
218
|
+
leader + fill_str + text
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def make_line(name_width, missing_width)
|
|
223
|
+
elements = [
|
|
224
|
+
'-' * name_width,
|
|
225
|
+
'-' * PCT_COLS,
|
|
226
|
+
'-' * missing_width
|
|
227
|
+
]
|
|
228
|
+
"#{elements.join(DELIM.gsub(' ', '-'))}-"
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def table_header(name_width, missing_width)
|
|
232
|
+
elements = [
|
|
233
|
+
fill('File', name_width),
|
|
234
|
+
fill('% Lines', PCT_COLS, right: true),
|
|
235
|
+
fill('Uncovered Line #s', missing_width)
|
|
236
|
+
]
|
|
237
|
+
"#{elements.join(DELIM)} "
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def colorize_by_coverage(text, pct)
|
|
241
|
+
case pct
|
|
242
|
+
when 80..100 then green(text)
|
|
243
|
+
when 50...80 then yellow(text)
|
|
244
|
+
else red(text)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def colorize_uncovered(text, pct)
|
|
249
|
+
pct == 100 ? text : red(text)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def green(text)
|
|
253
|
+
"\e[32m#{text}\e[0m"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def yellow(text)
|
|
257
|
+
"\e[33m#{text}\e[0m"
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def red(text)
|
|
261
|
+
"\e[31m#{text}\e[0m"
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: better_coverage
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Mridang Agarwalla
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-11-28 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: minitest-reporters
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.5'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.5'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: simplecov
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0.22'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0.22'
|
|
41
|
+
description: Minitest reporter displaying SimpleCov coverage in Jest/Istanbul format
|
|
42
|
+
with directory tree
|
|
43
|
+
email:
|
|
44
|
+
- mridang.agarwalla@gmail.com
|
|
45
|
+
executables: []
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- README.md
|
|
50
|
+
- lib/better_coverage.rb
|
|
51
|
+
- lib/minitest_plus/version.rb
|
|
52
|
+
homepage: https://github.com/mridang/minitest-reporters
|
|
53
|
+
licenses:
|
|
54
|
+
- Apache-2.0
|
|
55
|
+
metadata:
|
|
56
|
+
rubygems_mfa_required: 'true'
|
|
57
|
+
post_install_message:
|
|
58
|
+
rdoc_options: []
|
|
59
|
+
require_paths:
|
|
60
|
+
- lib
|
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
62
|
+
requirements:
|
|
63
|
+
- - ">="
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '3.0'
|
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '0'
|
|
71
|
+
requirements: []
|
|
72
|
+
rubygems_version: 3.2.33
|
|
73
|
+
signing_key:
|
|
74
|
+
specification_version: 4
|
|
75
|
+
summary: Jest-style coverage reporter for Minitest
|
|
76
|
+
test_files: []
|