single_cov 1.5.0 → 1.6.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.rb +95 -83
- data/lib/single_cov/version.rb +2 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b1061e2ee0841966f31a082926db0474550414ac8e06f395696152ba3b985aa5
|
4
|
+
data.tar.gz: 836d974a4b39cbbf4b5454334997f60ac4d2afe5aafd4609bc2e04fb57f5a991
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e30055d56774315c96b907ef0d930c83a4ac921855d0000760ade00d47af0e3394a96853f13f669e4fd797e0d8d2d5aeadbfb1e8a8549f45ca36629a80befae6
|
7
|
+
data.tar.gz: 6e42c196b4cbb0eccb982b3bb942cf1664d8aa99925eabc7c9fb6165d10dd94de36640527556a9209739058501a36e191fc9136f21c903e0aa57307c63b2bd73
|
data/lib/single_cov.rb
CHANGED
@@ -1,87 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module SingleCov
|
2
3
|
COVERAGES = []
|
3
4
|
MAX_OUTPUT = 40
|
4
|
-
|
5
|
-
|
6
|
-
UNCOVERED_COMMENT_MARKER = "uncovered"
|
5
|
+
RAILS_APP_FOLDERS = ["models", "serializers", "helpers", "controllers", "mailers", "views", "jobs", "channels"]
|
6
|
+
UNCOVERED_COMMENT_MARKER = /#.*uncovered/
|
7
7
|
|
8
8
|
class << self
|
9
|
-
# enable coverage reporting:
|
9
|
+
# enable coverage reporting: path to output file, changed by forking-test-runner at runtime to combine many reports
|
10
10
|
attr_accessor :coverage_report
|
11
11
|
|
12
12
|
# emit only line coverage in coverage report for older coverage systems
|
13
13
|
attr_accessor :coverage_report_lines
|
14
14
|
|
15
|
-
# optionally rewrite the
|
15
|
+
# optionally rewrite the matching path single-cov guessed with a lambda
|
16
16
|
def rewrite(&block)
|
17
17
|
@rewrite = block
|
18
18
|
end
|
19
19
|
|
20
|
+
# mark a test file as not covering anything to make assert_used pass
|
20
21
|
def not_covered!
|
21
|
-
|
22
|
+
main_process!
|
22
23
|
end
|
23
24
|
|
25
|
+
# mark the file under test as needing coverage
|
24
26
|
def covered!(file: nil, uncovered: 0)
|
25
|
-
file =
|
27
|
+
file = ensure_covered_file(file)
|
26
28
|
COVERAGES << [file, uncovered]
|
27
|
-
|
29
|
+
main_process!
|
28
30
|
end
|
29
31
|
|
30
32
|
def all_covered?(result)
|
31
|
-
errors = COVERAGES.
|
32
|
-
|
33
|
-
line_coverage = (coverage.is_a?(Hash) ? coverage.fetch(:lines) : coverage)
|
34
|
-
uncovered_lines = line_coverage.each_with_index.map { |c, i| i + 1 if c == 0 }.compact
|
35
|
-
|
36
|
-
branch_coverage = (coverage.is_a?(Hash) && coverage[:branches])
|
37
|
-
uncovered_branches = (branch_coverage ? uncovered_branches(branch_coverage, uncovered_lines) : [])
|
38
|
-
|
39
|
-
uncovered = uncovered_lines.concat uncovered_branches
|
40
|
-
next if uncovered.size == expected_uncovered
|
41
|
-
|
42
|
-
# ignore lines that are marked as uncovered via comments
|
43
|
-
# NOTE: ideally we should also warn when using uncovered but the section is indeed covered
|
44
|
-
content = File.readlines(file)
|
45
|
-
uncovered.reject! do |line_start, _, _, _|
|
46
|
-
content[line_start - 1].include?(UNCOVERED_COMMENT_MARKER)
|
47
|
-
end
|
48
|
-
next if uncovered.size == expected_uncovered
|
33
|
+
errors = COVERAGES.flat_map do |file, expected_uncovered|
|
34
|
+
next no_coverage_error(file) unless coverage = result["#{root}/#{file}"]
|
49
35
|
|
50
|
-
|
51
|
-
|
52
|
-
uncovered.sort_by! { |line_start, char_start, _, _| [line_start, char_start || 0] }
|
53
|
-
end
|
36
|
+
uncovered = uncovered(coverage)
|
37
|
+
next if uncovered.size == expected_uncovered
|
54
38
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
"#{file}:#{line_start}:#{char_start}-#{line_end}:#{char_end}"
|
61
|
-
end
|
62
|
-
else
|
63
|
-
"#{file}:#{line_start}"
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
warn_about_bad_coverage(file, expected_uncovered, uncovered)
|
68
|
-
else
|
69
|
-
warn_about_no_coverage(file)
|
39
|
+
# ignore lines that are marked as uncovered via comments
|
40
|
+
# TODO: warn when using uncovered but the section is indeed covered
|
41
|
+
content = File.readlines(file)
|
42
|
+
uncovered.reject! do |line_start, _, _, _|
|
43
|
+
content[line_start - 1].match?(UNCOVERED_COMMENT_MARKER)
|
70
44
|
end
|
45
|
+
next if uncovered.size == expected_uncovered
|
46
|
+
|
47
|
+
bad_coverage_error(file, expected_uncovered, uncovered)
|
71
48
|
end.compact
|
72
49
|
|
73
50
|
return true if errors.empty?
|
74
51
|
|
75
|
-
errors = errors.join("\n").split("\n") # unify arrays with multiline strings
|
76
52
|
errors[MAX_OUTPUT..-1] = "... coverage output truncated" if errors.size >= MAX_OUTPUT
|
77
53
|
warn errors
|
78
54
|
|
79
|
-
errors.all? { |l|
|
55
|
+
errors.all? { |l| warning?(l) }
|
80
56
|
end
|
81
57
|
|
82
58
|
def assert_used(tests: default_tests)
|
83
59
|
bad = tests.select do |file|
|
84
|
-
File.read(file) !~ /SingleCov.(not_)?covered
|
60
|
+
File.read(file) !~ /SingleCov.(not_)?covered!/
|
85
61
|
end
|
86
62
|
unless bad.empty?
|
87
63
|
raise bad.map { |f| "#{f}: needs to use SingleCov.covered!" }.join("\n")
|
@@ -89,7 +65,7 @@ module SingleCov
|
|
89
65
|
end
|
90
66
|
|
91
67
|
def assert_tested(files: glob('{app,lib}/**/*.rb'), tests: default_tests, untested: [])
|
92
|
-
missing = files - tests.map { |t|
|
68
|
+
missing = files - tests.map { |t| guess_covered_file(t) }
|
93
69
|
fixed = untested - missing
|
94
70
|
missing -= untested
|
95
71
|
|
@@ -100,13 +76,10 @@ module SingleCov
|
|
100
76
|
end
|
101
77
|
end
|
102
78
|
|
103
|
-
def setup(framework, root: nil, branches:
|
79
|
+
def setup(framework, root: nil, branches: true)
|
104
80
|
if defined?(SimpleCov)
|
105
81
|
raise "Load SimpleCov after SingleCov"
|
106
82
|
end
|
107
|
-
if branches && !BRANCH_COVERAGE_SUPPORTED
|
108
|
-
raise "Branch coverage needs ruby >= 2.5.0"
|
109
|
-
end
|
110
83
|
|
111
84
|
@branches = branches
|
112
85
|
@root = root
|
@@ -139,19 +112,38 @@ module SingleCov
|
|
139
112
|
|
140
113
|
private
|
141
114
|
|
115
|
+
def uncovered(coverage)
|
116
|
+
return coverage unless coverage.is_a?(Hash) # just lines
|
117
|
+
|
118
|
+
# [nil, 1, 0, 1, 0] -> [3, 5]
|
119
|
+
uncovered_lines = coverage.fetch(:lines)
|
120
|
+
.each_with_index
|
121
|
+
.select { |c, _| c == 0 }
|
122
|
+
.map { |_, i| i + 1 }
|
123
|
+
.compact
|
124
|
+
|
125
|
+
uncovered_branches = uncovered_branches(coverage[:branches] || {})
|
126
|
+
uncovered_branches.reject! { |k| uncovered_lines.include?(k[0]) } # remove duplicates
|
127
|
+
|
128
|
+
all = uncovered_lines.concat uncovered_branches
|
129
|
+
all.sort_by! { |line_start, char_start, _, _| [line_start, char_start || 0] } # branches are unsorted
|
130
|
+
all
|
131
|
+
end
|
132
|
+
|
142
133
|
def enabled?
|
143
134
|
(!defined?(@disabled) || !@disabled)
|
144
135
|
end
|
145
136
|
|
146
|
-
|
147
|
-
|
137
|
+
# assuming that the main process will load all the files, we store it's pid
|
138
|
+
def main_process!
|
139
|
+
@main_process_pid = Process.pid
|
148
140
|
end
|
149
141
|
|
150
142
|
def main_process?
|
151
|
-
(!defined?(@
|
143
|
+
(!defined?(@main_process_pid) || @main_process_pid == Process.pid)
|
152
144
|
end
|
153
145
|
|
154
|
-
def uncovered_branches(coverage
|
146
|
+
def uncovered_branches(coverage)
|
155
147
|
# {[branch_id] => {[branch_part] => coverage}} --> {branch_part -> sum-of-coverage}
|
156
148
|
sum = Hash.new(0)
|
157
149
|
coverage.each_value do |branch|
|
@@ -160,9 +152,8 @@ module SingleCov
|
|
160
152
|
end
|
161
153
|
end
|
162
154
|
|
163
|
-
#
|
164
|
-
sum.
|
165
|
-
found = sum.map { |k, _| [k[0], k[1]+1, k[2], k[3]+1] }
|
155
|
+
sum.select! { |_, v| v == 0 } # keep missing coverage
|
156
|
+
found = sum.map { |k, _| [k[0], k[1] + 1, k[2], k[3] + 1] }
|
166
157
|
found.uniq!
|
167
158
|
found
|
168
159
|
end
|
@@ -178,7 +169,12 @@ module SingleCov
|
|
178
169
|
# do not ask for coverage when SimpleCov already does or it conflicts
|
179
170
|
def coverage_results
|
180
171
|
if defined?(SimpleCov) && (result = SimpleCov.instance_variable_get(:@result))
|
181
|
-
result.original_result
|
172
|
+
result = result.original_result
|
173
|
+
# singlecov 1.18+ puts string "lines" into the result that we cannot read
|
174
|
+
if result.each_value.first.is_a?(Hash)
|
175
|
+
result = result.transform_values { |v| v.transform_keys(&:to_sym) }
|
176
|
+
end
|
177
|
+
result
|
182
178
|
else
|
183
179
|
Coverage.result
|
184
180
|
end
|
@@ -191,7 +187,7 @@ module SingleCov
|
|
191
187
|
if @branches
|
192
188
|
Coverage.start(lines: true, branches: true)
|
193
189
|
else
|
194
|
-
Coverage.start
|
190
|
+
Coverage.start(lines: true)
|
195
191
|
end
|
196
192
|
end
|
197
193
|
|
@@ -249,7 +245,7 @@ module SingleCov
|
|
249
245
|
end
|
250
246
|
|
251
247
|
def rspec_running_subset_of_tests?
|
252
|
-
(ARGV & ['-t', '--tag', '-e', '--example']).any? || ARGV.any? { |a| a =~
|
248
|
+
(ARGV & ['-t', '--tag', '-e', '--example']).any? || ARGV.any? { |a| a =~ /:\d+$|\[[\d:]+\]$/ }
|
253
249
|
end
|
254
250
|
|
255
251
|
# code stolen from SimpleCov
|
@@ -274,15 +270,13 @@ module SingleCov
|
|
274
270
|
end
|
275
271
|
end
|
276
272
|
|
277
|
-
def
|
278
|
-
|
279
|
-
raise "Use paths relative to root."
|
280
|
-
end
|
273
|
+
def ensure_covered_file(file)
|
274
|
+
raise "Use paths relative to project root." if file&.start_with?("/")
|
281
275
|
|
282
276
|
if file
|
283
|
-
raise "#{file} does not exist
|
277
|
+
raise "#{file} does not exist, use paths relative to project root." unless File.exist?("#{root}/#{file}")
|
284
278
|
else
|
285
|
-
file =
|
279
|
+
file = guess_covered_file(caller[1])
|
286
280
|
if file.start_with?("/")
|
287
281
|
raise "Found file #{file} which is not relative to the root #{root}.\nUse `SingleCov.covered! file: 'target_file.rb'` to set covered file location."
|
288
282
|
elsif !File.exist?("#{root}/#{file}")
|
@@ -293,21 +287,39 @@ module SingleCov
|
|
293
287
|
file
|
294
288
|
end
|
295
289
|
|
296
|
-
def
|
297
|
-
details = "(#{
|
298
|
-
if expected_uncovered >
|
290
|
+
def bad_coverage_error(file, expected_uncovered, uncovered)
|
291
|
+
details = "(#{uncovered.size} current vs #{expected_uncovered} configured)"
|
292
|
+
if expected_uncovered > uncovered.size
|
299
293
|
if running_single_file?
|
300
|
-
"#{file} has less uncovered lines #{details}, decrement configured uncovered
|
294
|
+
warning "#{file} has less uncovered lines #{details}, decrement configured uncovered"
|
301
295
|
end
|
302
296
|
else
|
303
297
|
[
|
304
298
|
"#{file} new uncovered lines introduced #{details}",
|
305
299
|
red("Lines missing coverage:"),
|
306
|
-
*
|
307
|
-
|
300
|
+
*uncovered.map do |line_start, char_start, line_end, char_end|
|
301
|
+
if char_start # branch coverage
|
302
|
+
if line_start == line_end
|
303
|
+
"#{file}:#{line_start}:#{char_start}-#{char_end}"
|
304
|
+
else # possibly unreachable since branches always seem to be on the same line
|
305
|
+
"#{file}:#{line_start}:#{char_start}-#{line_end}:#{char_end}"
|
306
|
+
end
|
307
|
+
else
|
308
|
+
"#{file}:#{line_start}"
|
309
|
+
end
|
310
|
+
end
|
311
|
+
]
|
308
312
|
end
|
309
313
|
end
|
310
314
|
|
315
|
+
def warning(msg)
|
316
|
+
"#{msg}?"
|
317
|
+
end
|
318
|
+
|
319
|
+
def warning?(msg)
|
320
|
+
msg.end_with?("?")
|
321
|
+
end
|
322
|
+
|
311
323
|
def red(text)
|
312
324
|
if $stdin.tty?
|
313
325
|
"\e[31m#{text}\e[0m"
|
@@ -316,17 +328,17 @@ module SingleCov
|
|
316
328
|
end
|
317
329
|
end
|
318
330
|
|
319
|
-
def
|
331
|
+
def no_coverage_error(file)
|
320
332
|
if $LOADED_FEATURES.include?("#{root}/#{file}")
|
321
333
|
# we cannot enforce $LOADED_FEATURES during covered! since it would fail when multiple files are loaded
|
322
|
-
"#{file} was expected to be covered, but was already loaded before
|
334
|
+
"#{file} was expected to be covered, but was already loaded before coverage started, which makes it uncoverable."
|
323
335
|
else
|
324
|
-
"#{file} was expected to be covered, but never loaded."
|
336
|
+
"#{file} was expected to be covered, but was never loaded."
|
325
337
|
end
|
326
338
|
end
|
327
339
|
|
328
|
-
def
|
329
|
-
file =
|
340
|
+
def guess_covered_file(test)
|
341
|
+
file = test.dup
|
330
342
|
|
331
343
|
# remove caller junk to get nice error messages when something fails
|
332
344
|
file.sub!(/\.rb\b.*/, '.rb')
|
@@ -344,7 +356,7 @@ module SingleCov
|
|
344
356
|
end
|
345
357
|
|
346
358
|
# rails things live in app
|
347
|
-
file_part[0...0] = if file_part =~ /^(?:#{
|
359
|
+
file_part[0...0] = if file_part =~ /^(?:#{RAILS_APP_FOLDERS.map { |f| Regexp.escape(f) }.join('|')})\//
|
348
360
|
"app/"
|
349
361
|
elsif file_part.start_with?("lib/") # don't add lib twice
|
350
362
|
""
|
@@ -380,12 +392,12 @@ module SingleCov
|
|
380
392
|
covered = results.select { |k, _| used.include?(k) }
|
381
393
|
|
382
394
|
if coverage_report_lines
|
383
|
-
covered = covered.
|
395
|
+
covered = covered.transform_values { |v| v.is_a?(Hash) ? v.fetch(:lines) : v }
|
384
396
|
end
|
385
397
|
|
386
398
|
# chose "Minitest" because it is what simplecov uses for reports and "Unit Tests" makes sonarqube break
|
387
399
|
data = JSON.pretty_generate(
|
388
|
-
"Minitest" => {"coverage" => covered, "timestamp" => Time.now.to_i }
|
400
|
+
"Minitest" => { "coverage" => covered, "timestamp" => Time.now.to_i }
|
389
401
|
)
|
390
402
|
FileUtils.mkdir_p(File.dirname(report))
|
391
403
|
File.write report, data
|
data/lib/single_cov/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: single_cov
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Grosser
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-11-07 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email: michael@grosser.it
|
@@ -31,7 +31,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
31
31
|
requirements:
|
32
32
|
- - ">="
|
33
33
|
- !ruby/object:Gem::Version
|
34
|
-
version: 2.
|
34
|
+
version: 2.5.0
|
35
35
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
36
36
|
requirements:
|
37
37
|
- - ">="
|