single_cov 1.3.2 → 1.11.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/single_cov/version.rb +2 -1
- data/lib/single_cov.rb +194 -101
- metadata +8 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3ffed2c03e29890449010148c9492563266983dd70d0f32e7a600692fa6b6a57
|
4
|
+
data.tar.gz: a9395fb02ed4dd4f5490cf4faf8a7c8ef3bd8b084081958c3a86fabb3cbbedcf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dddfdd5778b9e3c34b92db87d4288970a6a0bd27154d0c40572bf5fa581f92806214827974b55c2612b19f1298bd1d5827072006fb1d131bf5feb36ccca33513
|
7
|
+
data.tar.gz: 6b82b17a84a018334c140027bab24ef3ac9c08cb0d52973f87d580006d19b0c7cfd3a80b8331b7d487e38af34c8e51f817066170ddee360378587b0b8af03981
|
data/lib/single_cov/version.rb
CHANGED
data/lib/single_cov.rb
CHANGED
@@ -1,81 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module SingleCov
|
2
3
|
COVERAGES = []
|
3
|
-
MAX_OUTPUT = 40
|
4
|
-
|
5
|
-
|
6
|
-
|
4
|
+
MAX_OUTPUT = Integer(ENV["SINGLE_COV_MAX_OUTPUT"] || "40")
|
5
|
+
RAILS_APP_FOLDERS = ["models", "serializers", "helpers", "controllers", "mailers", "views", "jobs", "channels"]
|
6
|
+
UNCOVERED_COMMENT_MARKER = /#.*uncovered/
|
7
|
+
PREFIXES_TO_IGNORE = [] # things to not prefix with lib/ etc
|
7
8
|
|
8
9
|
class << self
|
9
|
-
#
|
10
|
+
# enable coverage reporting: path to output file, changed by forking-test-runner at runtime to combine many reports
|
11
|
+
attr_accessor :coverage_report
|
12
|
+
|
13
|
+
# emit only line coverage in coverage report for older coverage systems
|
14
|
+
attr_accessor :coverage_report_lines
|
15
|
+
|
16
|
+
# optionally rewrite the matching path single-cov guessed with a lambda
|
10
17
|
def rewrite(&block)
|
11
18
|
@rewrite = block
|
12
19
|
end
|
13
20
|
|
21
|
+
# mark a test file as not covering anything to make assert_used pass
|
14
22
|
def not_covered!
|
15
|
-
|
23
|
+
main_process!
|
16
24
|
end
|
17
25
|
|
26
|
+
# mark the file under test as needing coverage
|
18
27
|
def covered!(file: nil, uncovered: 0)
|
19
|
-
file =
|
28
|
+
file = ensure_covered_file(file)
|
20
29
|
COVERAGES << [file, uncovered]
|
21
|
-
|
30
|
+
main_process!
|
22
31
|
end
|
23
32
|
|
24
33
|
def all_covered?(result)
|
25
|
-
errors = COVERAGES.
|
26
|
-
|
27
|
-
line_coverage = (coverage.is_a?(Hash) ? coverage.fetch(:lines) : coverage)
|
28
|
-
uncovered_lines = line_coverage.each_with_index.map { |c, i| i + 1 if c == 0 }.compact
|
29
|
-
|
30
|
-
branch_coverage = (coverage.is_a?(Hash) && coverage[:branches])
|
31
|
-
uncovered_branches = (branch_coverage ? uncovered_branches(branch_coverage, uncovered_lines) : [])
|
32
|
-
|
33
|
-
uncovered = uncovered_lines.concat uncovered_branches
|
34
|
-
next if uncovered.size == expected_uncovered
|
35
|
-
|
36
|
-
# ignore lines that are marked as uncovered via comments
|
37
|
-
# NOTE: ideally we should also warn when using uncovered but the section is indeed covered
|
38
|
-
content = File.readlines(file)
|
39
|
-
uncovered.reject! do |line_start, _, _, _|
|
40
|
-
content[line_start - 1].include?(UNCOVERED_COMMENT_MARKER)
|
41
|
-
end
|
42
|
-
next if uncovered.size == expected_uncovered
|
43
|
-
|
44
|
-
# branches are unsorted and added to the end, only sort when displayed
|
45
|
-
if branch_coverage
|
46
|
-
uncovered.sort_by! { |line_start, char_start, _, _| [line_start, char_start || 0] }
|
47
|
-
end
|
34
|
+
errors = COVERAGES.flat_map do |file, expected_uncovered|
|
35
|
+
next no_coverage_error(file) unless coverage = result["#{root}/#{file}"]
|
48
36
|
|
49
|
-
|
50
|
-
|
51
|
-
if line_start == line_end
|
52
|
-
"#{file}:#{line_start}:#{char_start}-#{char_end}"
|
53
|
-
else # possibly unreachable since branches always seem to be on the same line
|
54
|
-
"#{file}:#{line_start}:#{char_start}-#{line_end}:#{char_end}"
|
55
|
-
end
|
56
|
-
else
|
57
|
-
"#{file}:#{line_start}"
|
58
|
-
end
|
59
|
-
end
|
37
|
+
uncovered = uncovered(coverage)
|
38
|
+
next if uncovered.size == expected_uncovered
|
60
39
|
|
61
|
-
|
62
|
-
|
63
|
-
|
40
|
+
# ignore lines that are marked as uncovered via comments
|
41
|
+
# TODO: warn when using uncovered but the section is indeed covered
|
42
|
+
content = File.readlines("#{root}/#{file}")
|
43
|
+
uncovered.reject! do |line_start, _, _, _, _|
|
44
|
+
content[line_start - 1].match?(UNCOVERED_COMMENT_MARKER)
|
64
45
|
end
|
46
|
+
next if uncovered.size == expected_uncovered
|
47
|
+
|
48
|
+
bad_coverage_error(file, expected_uncovered, uncovered)
|
65
49
|
end.compact
|
66
50
|
|
67
51
|
return true if errors.empty?
|
68
52
|
|
69
|
-
|
70
|
-
|
71
|
-
|
53
|
+
if errors.size >= MAX_OUTPUT
|
54
|
+
errors[MAX_OUTPUT..-1] = "... coverage output truncated (use SINGLE_COV_MAX_OUTPUT=999 to expand)"
|
55
|
+
end
|
56
|
+
@error_logger.puts errors
|
72
57
|
|
73
|
-
errors.all? { |l|
|
58
|
+
errors.all? { |l| warning?(l) }
|
74
59
|
end
|
75
60
|
|
76
61
|
def assert_used(tests: default_tests)
|
77
62
|
bad = tests.select do |file|
|
78
|
-
File.read(file) !~ /SingleCov.(not_)?covered
|
63
|
+
File.read(file) !~ /SingleCov.(not_)?covered!/
|
79
64
|
end
|
80
65
|
unless bad.empty?
|
81
66
|
raise bad.map { |f| "#{f}: needs to use SingleCov.covered!" }.join("\n")
|
@@ -83,7 +68,7 @@ module SingleCov
|
|
83
68
|
end
|
84
69
|
|
85
70
|
def assert_tested(files: glob('{app,lib}/**/*.rb'), tests: default_tests, untested: [])
|
86
|
-
missing = files - tests.map { |t|
|
71
|
+
missing = files - tests.map { |t| guess_covered_file(t) }
|
87
72
|
fixed = untested - missing
|
88
73
|
missing -= untested
|
89
74
|
|
@@ -94,13 +79,40 @@ module SingleCov
|
|
94
79
|
end
|
95
80
|
end
|
96
81
|
|
97
|
-
def
|
82
|
+
def assert_full_coverage(tests: default_tests, currently_complete: [], location: nil)
|
83
|
+
location ||= caller(0..1)[1].split(':in').first
|
84
|
+
complete = tests.select { |file| File.read(file) =~ /SingleCov.covered!(?:(?!uncovered).)*(\s*|\s*\#.*)$/ }
|
85
|
+
missing_complete = currently_complete - complete
|
86
|
+
newly_complete = complete - currently_complete
|
87
|
+
errors = []
|
88
|
+
|
89
|
+
if missing_complete.any?
|
90
|
+
errors << <<~MSG
|
91
|
+
The following file(s) were previously marked as having 100% SingleCov test coverage (had no `coverage:` option) but are no longer marked as such.
|
92
|
+
#{missing_complete.join("\n")}
|
93
|
+
Please increase test coverage in these files to maintain 100% coverage and remove `coverage:` usage.
|
94
|
+
|
95
|
+
If this test fails during a file removal, make it pass by removing all references to the removed file's path from the code base.
|
96
|
+
MSG
|
97
|
+
end
|
98
|
+
|
99
|
+
if newly_complete.any?
|
100
|
+
errors << <<~MSG
|
101
|
+
The following files are newly at 100% SingleCov test coverage.
|
102
|
+
Please add the following to #{location} to ensure 100% coverage is maintained moving forward.
|
103
|
+
#{newly_complete.join("\n")}
|
104
|
+
MSG
|
105
|
+
end
|
106
|
+
|
107
|
+
raise errors.join("\n") if errors.any?
|
108
|
+
end
|
109
|
+
|
110
|
+
def setup(framework, root: nil, branches: true, err: $stderr)
|
111
|
+
@error_logger = err
|
112
|
+
|
98
113
|
if defined?(SimpleCov)
|
99
114
|
raise "Load SimpleCov after SingleCov"
|
100
115
|
end
|
101
|
-
if branches && !BRANCH_COVERAGE_SUPPORTED
|
102
|
-
raise "Branch coverage needs ruby >= 2.5.0"
|
103
|
-
end
|
104
116
|
|
105
117
|
@branches = branches
|
106
118
|
@root = root
|
@@ -118,7 +130,11 @@ module SingleCov
|
|
118
130
|
start_coverage_recording
|
119
131
|
|
120
132
|
override_at_exit do |status, _exception|
|
121
|
-
|
133
|
+
if enabled? && main_process? && status == 0
|
134
|
+
results = coverage_results
|
135
|
+
generate_report results
|
136
|
+
exit 1 unless SingleCov.all_covered?(results)
|
137
|
+
end
|
122
138
|
end
|
123
139
|
end
|
124
140
|
|
@@ -129,32 +145,47 @@ module SingleCov
|
|
129
145
|
|
130
146
|
private
|
131
147
|
|
148
|
+
def uncovered(coverage)
|
149
|
+
return coverage unless coverage.is_a?(Hash) # just lines
|
150
|
+
|
151
|
+
uncovered_lines = indexes(coverage.fetch(:lines), 0).map! { |i| i + 1 }
|
152
|
+
uncovered_branches = uncovered_branches(coverage[:branches] || {})
|
153
|
+
uncovered_branches.reject! { |br| uncovered_lines.include?(br[0]) } # ignore branch when whole line is uncovered
|
154
|
+
|
155
|
+
# combine lines and branches while keeping them sorted
|
156
|
+
all = uncovered_lines.concat uncovered_branches
|
157
|
+
all.sort_by! { |line_start, char_start, _, _, _| [line_start, char_start || 0] } # branches are unsorted
|
158
|
+
all
|
159
|
+
end
|
160
|
+
|
132
161
|
def enabled?
|
133
162
|
(!defined?(@disabled) || !@disabled)
|
134
163
|
end
|
135
164
|
|
136
|
-
|
137
|
-
|
165
|
+
# assuming that the main process will load all the files, we store it's pid
|
166
|
+
def main_process!
|
167
|
+
@main_process_pid = Process.pid
|
138
168
|
end
|
139
169
|
|
140
170
|
def main_process?
|
141
|
-
(!defined?(@
|
171
|
+
(!defined?(@main_process_pid) || @main_process_pid == Process.pid)
|
142
172
|
end
|
143
173
|
|
144
|
-
|
145
|
-
|
146
|
-
sum =
|
174
|
+
# {[branch_id] => {[branch_part] => coverage}} --> uncovered location
|
175
|
+
def uncovered_branches(coverage)
|
176
|
+
sum = {}
|
147
177
|
coverage.each_value do |branch|
|
148
|
-
branch.
|
149
|
-
|
178
|
+
branch.filter_map do |part, c|
|
179
|
+
location = [part[2], part[3] + 1, part[4], part[5] + 1] # locations can be duplicated
|
180
|
+
type = part[0]
|
181
|
+
info = (sum[location] ||= [0, nil])
|
182
|
+
info[0] += c
|
183
|
+
info[1] = type if type == :else # only else is important to track since it often is not in the code
|
150
184
|
end
|
151
185
|
end
|
152
186
|
|
153
|
-
#
|
154
|
-
sum.
|
155
|
-
found = sum.map { |k, _| [k[0], k[1]+1, k[2], k[3]+1] }
|
156
|
-
found.uniq!
|
157
|
-
found
|
187
|
+
# keep location and type of missing coverage
|
188
|
+
sum.filter_map { |k, v| k + [v[1]] if v[0] == 0 }
|
158
189
|
end
|
159
190
|
|
160
191
|
def default_tests
|
@@ -165,10 +196,19 @@ module SingleCov
|
|
165
196
|
Dir["#{root}/#{pattern}"].map! { |f| f.sub("#{root}/", '') }
|
166
197
|
end
|
167
198
|
|
199
|
+
def indexes(list, find)
|
200
|
+
list.each_with_index.filter_map { |v, i| i if v == find }
|
201
|
+
end
|
202
|
+
|
168
203
|
# do not ask for coverage when SimpleCov already does or it conflicts
|
169
204
|
def coverage_results
|
170
205
|
if defined?(SimpleCov) && (result = SimpleCov.instance_variable_get(:@result))
|
171
|
-
result.original_result
|
206
|
+
result = result.original_result
|
207
|
+
# singlecov 1.18+ puts string "lines" into the result that we cannot read
|
208
|
+
if result.each_value.first.is_a?(Hash)
|
209
|
+
result = result.transform_values { |v| v.transform_keys(&:to_sym) }
|
210
|
+
end
|
211
|
+
result
|
172
212
|
else
|
173
213
|
Coverage.result
|
174
214
|
end
|
@@ -181,7 +221,7 @@ module SingleCov
|
|
181
221
|
if @branches
|
182
222
|
Coverage.start(lines: true, branches: true)
|
183
223
|
else
|
184
|
-
Coverage.start
|
224
|
+
Coverage.start(lines: true)
|
185
225
|
end
|
186
226
|
end
|
187
227
|
|
@@ -239,7 +279,7 @@ module SingleCov
|
|
239
279
|
end
|
240
280
|
|
241
281
|
def rspec_running_subset_of_tests?
|
242
|
-
(ARGV & ['-t', '--tag', '-e', '--example']).any? || ARGV.any? { |a| a =~
|
282
|
+
(ARGV & ['-t', '--tag', '-e', '--example']).any? || ARGV.any? { |a| a =~ /:\d+$|\[[\d:]+\]$/ }
|
243
283
|
end
|
244
284
|
|
245
285
|
# code stolen from SimpleCov
|
@@ -264,15 +304,12 @@ module SingleCov
|
|
264
304
|
end
|
265
305
|
end
|
266
306
|
|
267
|
-
def
|
268
|
-
if file && file.start_with?("/")
|
269
|
-
raise "Use paths relative to root."
|
270
|
-
end
|
271
|
-
|
307
|
+
def ensure_covered_file(file)
|
272
308
|
if file
|
273
|
-
raise "
|
309
|
+
raise "Use paths relative to project root." if file.start_with?("/")
|
310
|
+
raise "#{file} does not exist, use paths relative to project root." unless File.exist?("#{root}/#{file}")
|
274
311
|
else
|
275
|
-
file =
|
312
|
+
file = guess_covered_file(caller[1])
|
276
313
|
if file.start_with?("/")
|
277
314
|
raise "Found file #{file} which is not relative to the root #{root}.\nUse `SingleCov.covered! file: 'target_file.rb'` to set covered file location."
|
278
315
|
elsif !File.exist?("#{root}/#{file}")
|
@@ -283,21 +320,39 @@ module SingleCov
|
|
283
320
|
file
|
284
321
|
end
|
285
322
|
|
286
|
-
def
|
287
|
-
details = "(#{
|
288
|
-
if expected_uncovered >
|
323
|
+
def bad_coverage_error(file, expected_uncovered, uncovered)
|
324
|
+
details = "(#{uncovered.size} current vs #{expected_uncovered} configured)"
|
325
|
+
if expected_uncovered > uncovered.size
|
289
326
|
if running_single_file?
|
290
|
-
"#{file} has less uncovered lines #{details}, decrement configured uncovered
|
327
|
+
warning "#{file} has less uncovered lines #{details}, decrement configured uncovered"
|
291
328
|
end
|
292
329
|
else
|
293
330
|
[
|
294
331
|
"#{file} new uncovered lines introduced #{details}",
|
295
332
|
red("Lines missing coverage:"),
|
296
|
-
*
|
297
|
-
|
333
|
+
*uncovered.map do |line_start, char_start, line_end, char_end, type|
|
334
|
+
if char_start # branch coverage
|
335
|
+
if line_start == line_end
|
336
|
+
"#{file}:#{line_start}:#{char_start}-#{char_end}"
|
337
|
+
else # possibly unreachable since branches always seem to be on the same line
|
338
|
+
"#{file}:#{line_start}:#{char_start}-#{line_end}:#{char_end}"
|
339
|
+
end + (type ? " # #{type}" : "")
|
340
|
+
else
|
341
|
+
"#{file}:#{line_start}"
|
342
|
+
end
|
343
|
+
end
|
344
|
+
]
|
298
345
|
end
|
299
346
|
end
|
300
347
|
|
348
|
+
def warning(msg)
|
349
|
+
"#{msg}?"
|
350
|
+
end
|
351
|
+
|
352
|
+
def warning?(msg)
|
353
|
+
msg.end_with?("?")
|
354
|
+
end
|
355
|
+
|
301
356
|
def red(text)
|
302
357
|
if $stdin.tty?
|
303
358
|
"\e[31m#{text}\e[0m"
|
@@ -306,17 +361,17 @@ module SingleCov
|
|
306
361
|
end
|
307
362
|
end
|
308
363
|
|
309
|
-
def
|
364
|
+
def no_coverage_error(file)
|
310
365
|
if $LOADED_FEATURES.include?("#{root}/#{file}")
|
311
366
|
# we cannot enforce $LOADED_FEATURES during covered! since it would fail when multiple files are loaded
|
312
|
-
"#{file} was expected to be covered, but was already loaded before
|
367
|
+
"#{file} was expected to be covered, but was already loaded before coverage started, which makes it uncoverable."
|
313
368
|
else
|
314
|
-
"#{file} was expected to be covered, but never loaded."
|
369
|
+
"#{file} was expected to be covered, but was never loaded."
|
315
370
|
end
|
316
371
|
end
|
317
372
|
|
318
|
-
def
|
319
|
-
file =
|
373
|
+
def guess_covered_file(test)
|
374
|
+
file = test.dup
|
320
375
|
|
321
376
|
# remove caller junk to get nice error messages when something fails
|
322
377
|
file.sub!(/\.rb\b.*/, '.rb')
|
@@ -333,18 +388,21 @@ module SingleCov
|
|
333
388
|
raise "#{file} includes neither 'test' nor 'spec' folder ... unable to resolve"
|
334
389
|
end
|
335
390
|
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
391
|
+
without_ignored_prefixes file_part do
|
392
|
+
# rails things live in app
|
393
|
+
file_part[0...0] =
|
394
|
+
if file_part =~ /^(?:#{RAILS_APP_FOLDERS.map { |f| Regexp.escape(f) }.join('|')})\//
|
395
|
+
"app/"
|
396
|
+
elsif file_part.start_with?("lib/") # don't add lib twice
|
397
|
+
""
|
398
|
+
else # everything else lives in lib
|
399
|
+
"lib/"
|
400
|
+
end
|
344
401
|
|
345
|
-
|
346
|
-
|
347
|
-
|
402
|
+
# remove test extension
|
403
|
+
if !file_part.sub!(/_(?:test|spec)\.rb\b.*/, '.rb') && !file_part.sub!(/\/test_/, "/")
|
404
|
+
raise "Unable to remove test extension from #{file} ... /test_, _test.rb and _spec.rb are supported"
|
405
|
+
end
|
348
406
|
end
|
349
407
|
|
350
408
|
# put back the subfolder
|
@@ -358,5 +416,40 @@ module SingleCov
|
|
358
416
|
def root
|
359
417
|
@root ||= (defined?(Bundler) && Bundler.root.to_s.sub(/\/gemfiles$/, '')) || Dir.pwd
|
360
418
|
end
|
419
|
+
|
420
|
+
def generate_report(results)
|
421
|
+
return unless report = coverage_report
|
422
|
+
|
423
|
+
# not a hard dependency for the whole library
|
424
|
+
require "json"
|
425
|
+
require "fileutils"
|
426
|
+
|
427
|
+
used = COVERAGES.map { |f, _| "#{root}/#{f}" }
|
428
|
+
covered = results.select { |k, _| used.include?(k) }
|
429
|
+
|
430
|
+
if coverage_report_lines
|
431
|
+
covered = covered.transform_values { |v| v.is_a?(Hash) ? v.fetch(:lines) : v }
|
432
|
+
end
|
433
|
+
|
434
|
+
# chose "Minitest" because it is what simplecov uses for reports and "Unit Tests" makes sonarqube break
|
435
|
+
data = JSON.pretty_generate(
|
436
|
+
"Minitest" => { "coverage" => covered, "timestamp" => Time.now.to_i }
|
437
|
+
)
|
438
|
+
FileUtils.mkdir_p(File.dirname(report))
|
439
|
+
File.write report, data
|
440
|
+
end
|
441
|
+
|
442
|
+
# file_part is modified during yield so we have to make sure to also modify in place
|
443
|
+
def without_ignored_prefixes(file_part)
|
444
|
+
folders = file_part.split('/')
|
445
|
+
return yield unless PREFIXES_TO_IGNORE.include?(folders.first)
|
446
|
+
|
447
|
+
prefix = folders.shift
|
448
|
+
file_part.replace folders.join('/')
|
449
|
+
|
450
|
+
yield
|
451
|
+
|
452
|
+
file_part[0...0] = "#{prefix}/"
|
453
|
+
end
|
361
454
|
end
|
362
455
|
end
|
metadata
CHANGED
@@ -1,16 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: single_cov
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.11.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Grosser
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-06-03 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
|
-
description:
|
13
|
+
description:
|
14
14
|
email: michael@grosser.it
|
15
15
|
executables: []
|
16
16
|
extensions: []
|
@@ -23,7 +23,7 @@ homepage: https://github.com/grosser/single_cov
|
|
23
23
|
licenses:
|
24
24
|
- MIT
|
25
25
|
metadata: {}
|
26
|
-
post_install_message:
|
26
|
+
post_install_message:
|
27
27
|
rdoc_options: []
|
28
28
|
require_paths:
|
29
29
|
- lib
|
@@ -31,16 +31,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
31
31
|
requirements:
|
32
32
|
- - ">="
|
33
33
|
- !ruby/object:Gem::Version
|
34
|
-
version: 2.
|
34
|
+
version: 2.7.0
|
35
35
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
36
36
|
requirements:
|
37
37
|
- - ">="
|
38
38
|
- !ruby/object:Gem::Version
|
39
39
|
version: '0'
|
40
40
|
requirements: []
|
41
|
-
|
42
|
-
|
43
|
-
signing_key:
|
41
|
+
rubygems_version: 3.3.3
|
42
|
+
signing_key:
|
44
43
|
specification_version: 4
|
45
44
|
summary: Actionable code coverage.
|
46
45
|
test_files: []
|