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 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MinitestPlus
4
+ VERSION = '1.0.0'
5
+ 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: []