single_cov 1.3.2 → 1.11.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 +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: []
|