single_cov 1.0.3 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: db040981049890225a79a6a8fa56ef8cc32a09b1d64a6f2139b0a39df5d50d56
4
- data.tar.gz: 29f47c8a26b0f1a59cfb580247a8919e0192953bd8173a7a54c745b7398edfbb
3
+ metadata.gz: b1061e2ee0841966f31a082926db0474550414ac8e06f395696152ba3b985aa5
4
+ data.tar.gz: 836d974a4b39cbbf4b5454334997f60ac4d2afe5aafd4609bc2e04fb57f5a991
5
5
  SHA512:
6
- metadata.gz: 487db3f37ec46c3b0d0b53fb5be2de2dff18077276b91a0fd11093e7f21d0844ef9f33e10ccbda05ed92579d60b96e6ae4b498a9fa26abd0b75bdd509e736285
7
- data.tar.gz: 4cf118a2d5bfed4cfa7bc357057ed0264b0db62b389e290c79445bbda78272eb377a8a14f16f1b6c3ab10d6a37db99a3e0fcfde11625b02862906108d0e9c573
6
+ metadata.gz: e30055d56774315c96b907ef0d930c83a4ac921855d0000760ade00d47af0e3394a96853f13f669e4fd797e0d8d2d5aeadbfb1e8a8549f45ca36629a80befae6
7
+ data.tar.gz: 6e42c196b4cbb0eccb982b3bb942cf1664d8aa99925eabc7c9fb6165d10dd94de36640527556a9209739058501a36e191fc9136f21c903e0aa57307c63b2bd73
@@ -1,63 +1,63 @@
1
+ # frozen_string_literal: true
1
2
  module SingleCov
2
3
  COVERAGES = []
3
4
  MAX_OUTPUT = 40
4
- APP_FOLDERS = ["models", "serializers", "helpers", "controllers", "mailers", "views", "jobs"]
5
- BRANCH_COVERAGE_SUPPORTED = (RUBY_VERSION >= "2.5.0")
5
+ RAILS_APP_FOLDERS = ["models", "serializers", "helpers", "controllers", "mailers", "views", "jobs", "channels"]
6
+ UNCOVERED_COMMENT_MARKER = /#.*uncovered/
6
7
 
7
8
  class << self
8
- # optionally rewrite the file we guessed with a lambda
9
+ # enable coverage reporting: path to output file, changed by forking-test-runner at runtime to combine many reports
10
+ attr_accessor :coverage_report
11
+
12
+ # emit only line coverage in coverage report for older coverage systems
13
+ attr_accessor :coverage_report_lines
14
+
15
+ # optionally rewrite the matching path single-cov guessed with a lambda
9
16
  def rewrite(&block)
10
17
  @rewrite = block
11
18
  end
12
19
 
20
+ # mark a test file as not covering anything to make assert_used pass
13
21
  def not_covered!
22
+ main_process!
14
23
  end
15
24
 
25
+ # mark the file under test as needing coverage
16
26
  def covered!(file: nil, uncovered: 0)
17
- file = guess_and_check_covered_file(file)
27
+ file = ensure_covered_file(file)
18
28
  COVERAGES << [file, uncovered]
29
+ main_process!
19
30
  end
20
31
 
21
32
  def all_covered?(result)
22
- errors = COVERAGES.map do |file, expected_uncovered|
23
- if coverage = result["#{root}/#{file}"]
24
- line_coverage = (coverage.is_a?(Hash) ? coverage.fetch(:lines) : coverage)
25
- uncovered = line_coverage.each_with_index.map { |c, i| i + 1 if c == 0 }.compact
26
- branch_coverage = (coverage.is_a?(Hash) && coverage[:branches])
27
-
28
- if branch_coverage
29
- uncovered.concat uncovered_branches(file, branch_coverage, uncovered)
30
- end
31
-
32
- next if uncovered.size == expected_uncovered
33
-
34
- # branches are unsorted and added to the end, only sort when necessary
35
- if branch_coverage
36
- uncovered.sort_by! { |line_start, char_start, line_end, char_end| [line_start, char_start || 0] }
37
- end
33
+ errors = COVERAGES.flat_map do |file, expected_uncovered|
34
+ next no_coverage_error(file) unless coverage = result["#{root}/#{file}"]
38
35
 
39
- uncovered.map! do |line_start, char_start, line_end, char_end|
40
- char_start ? "#{file}:#{line_start}:#{char_start}-#{line_end}:#{char_end}" : "#{file}:#{line_start}"
41
- end
36
+ uncovered = uncovered(coverage)
37
+ next if uncovered.size == expected_uncovered
42
38
 
43
- warn_about_bad_coverage(file, expected_uncovered, uncovered)
44
- else
45
- 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)
46
44
  end
45
+ next if uncovered.size == expected_uncovered
46
+
47
+ bad_coverage_error(file, expected_uncovered, uncovered)
47
48
  end.compact
48
49
 
49
50
  return true if errors.empty?
50
51
 
51
- errors = errors.join("\n").split("\n") # unify arrays with multiline strings
52
52
  errors[MAX_OUTPUT..-1] = "... coverage output truncated" if errors.size >= MAX_OUTPUT
53
53
  warn errors
54
54
 
55
- errors.all? { |l| l.end_with?('?') } # ok if we just have warnings
55
+ errors.all? { |l| warning?(l) }
56
56
  end
57
57
 
58
58
  def assert_used(tests: default_tests)
59
59
  bad = tests.select do |file|
60
- File.read(file) !~ /SingleCov.(not_)?covered\!/
60
+ File.read(file) !~ /SingleCov.(not_)?covered!/
61
61
  end
62
62
  unless bad.empty?
63
63
  raise bad.map { |f| "#{f}: needs to use SingleCov.covered!" }.join("\n")
@@ -65,7 +65,7 @@ module SingleCov
65
65
  end
66
66
 
67
67
  def assert_tested(files: glob('{app,lib}/**/*.rb'), tests: default_tests, untested: [])
68
- missing = files - tests.map { |t| file_under_test(t) }
68
+ missing = files - tests.map { |t| guess_covered_file(t) }
69
69
  fixed = untested - missing
70
70
  missing -= untested
71
71
 
@@ -76,13 +76,10 @@ module SingleCov
76
76
  end
77
77
  end
78
78
 
79
- def setup(framework, root: nil, branches: BRANCH_COVERAGE_SUPPORTED)
79
+ def setup(framework, root: nil, branches: true)
80
80
  if defined?(SimpleCov)
81
81
  raise "Load SimpleCov after SingleCov"
82
82
  end
83
- if branches && !BRANCH_COVERAGE_SUPPORTED
84
- raise "Branch coverage needs ruby >= 2.5.0"
85
- end
86
83
 
87
84
  @branches = branches
88
85
  @root = root
@@ -100,7 +97,11 @@ module SingleCov
100
97
  start_coverage_recording
101
98
 
102
99
  override_at_exit do |status, _exception|
103
- exit 1 if (!defined?(@disabled) || !@disabled) && status == 0 && !SingleCov.all_covered?(coverage_results)
100
+ if enabled? && main_process? && status == 0
101
+ results = coverage_results
102
+ generate_report results
103
+ exit 1 unless SingleCov.all_covered?(results)
104
+ end
104
105
  end
105
106
  end
106
107
 
@@ -111,7 +112,38 @@ module SingleCov
111
112
 
112
113
  private
113
114
 
114
- def uncovered_branches(file, coverage, uncovered_lines)
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
+
133
+ def enabled?
134
+ (!defined?(@disabled) || !@disabled)
135
+ end
136
+
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
140
+ end
141
+
142
+ def main_process?
143
+ (!defined?(@main_process_pid) || @main_process_pid == Process.pid)
144
+ end
145
+
146
+ def uncovered_branches(coverage)
115
147
  # {[branch_id] => {[branch_part] => coverage}} --> {branch_part -> sum-of-coverage}
116
148
  sum = Hash.new(0)
117
149
  coverage.each_value do |branch|
@@ -120,9 +152,8 @@ module SingleCov
120
152
  end
121
153
  end
122
154
 
123
- # show missing coverage
124
- found = sum.select { |k, v| v.zero? && !uncovered_lines.include?(k[0]) }.
125
- 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] }
126
157
  found.uniq!
127
158
  found
128
159
  end
@@ -138,7 +169,12 @@ module SingleCov
138
169
  # do not ask for coverage when SimpleCov already does or it conflicts
139
170
  def coverage_results
140
171
  if defined?(SimpleCov) && (result = SimpleCov.instance_variable_get(:@result))
141
- 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
142
178
  else
143
179
  Coverage.result
144
180
  end
@@ -151,7 +187,7 @@ module SingleCov
151
187
  if @branches
152
188
  Coverage.start(lines: true, branches: true)
153
189
  else
154
- Coverage.start
190
+ Coverage.start(lines: true)
155
191
  end
156
192
  end
157
193
 
@@ -209,7 +245,7 @@ module SingleCov
209
245
  end
210
246
 
211
247
  def rspec_running_subset_of_tests?
212
- (ARGV & ['-t', '--tag', '-e', '--example']).any? || ARGV.any? { |a| a =~ /\:\d+$|\[[\d:]+\]$/ }
248
+ (ARGV & ['-t', '--tag', '-e', '--example']).any? || ARGV.any? { |a| a =~ /:\d+$|\[[\d:]+\]$/ }
213
249
  end
214
250
 
215
251
  # code stolen from SimpleCov
@@ -234,15 +270,13 @@ module SingleCov
234
270
  end
235
271
  end
236
272
 
237
- def guess_and_check_covered_file(file)
238
- if file && file.start_with?("/")
239
- raise "Use paths relative to root."
240
- end
273
+ def ensure_covered_file(file)
274
+ raise "Use paths relative to project root." if file&.start_with?("/")
241
275
 
242
276
  if file
243
- raise "#{file} does not exist and cannot be covered." unless File.exist?("#{root}/#{file}")
277
+ raise "#{file} does not exist, use paths relative to project root." unless File.exist?("#{root}/#{file}")
244
278
  else
245
- file = file_under_test(caller[1])
279
+ file = guess_covered_file(caller[1])
246
280
  if file.start_with?("/")
247
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."
248
282
  elsif !File.exist?("#{root}/#{file}")
@@ -253,21 +287,39 @@ module SingleCov
253
287
  file
254
288
  end
255
289
 
256
- def warn_about_bad_coverage(file, expected_uncovered, uncovered_lines)
257
- details = "(#{uncovered_lines.size} current vs #{expected_uncovered} configured)"
258
- if expected_uncovered > uncovered_lines.size
290
+ def bad_coverage_error(file, expected_uncovered, uncovered)
291
+ details = "(#{uncovered.size} current vs #{expected_uncovered} configured)"
292
+ if expected_uncovered > uncovered.size
259
293
  if running_single_file?
260
- "#{file} has less uncovered lines #{details}, decrement configured uncovered?"
294
+ warning "#{file} has less uncovered lines #{details}, decrement configured uncovered"
261
295
  end
262
296
  else
263
297
  [
264
298
  "#{file} new uncovered lines introduced #{details}",
265
299
  red("Lines missing coverage:"),
266
- *uncovered_lines
267
- ].join("\n")
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
+ ]
268
312
  end
269
313
  end
270
314
 
315
+ def warning(msg)
316
+ "#{msg}?"
317
+ end
318
+
319
+ def warning?(msg)
320
+ msg.end_with?("?")
321
+ end
322
+
271
323
  def red(text)
272
324
  if $stdin.tty?
273
325
  "\e[31m#{text}\e[0m"
@@ -276,17 +328,17 @@ module SingleCov
276
328
  end
277
329
  end
278
330
 
279
- def warn_about_no_coverage(file)
331
+ def no_coverage_error(file)
280
332
  if $LOADED_FEATURES.include?("#{root}/#{file}")
281
333
  # we cannot enforce $LOADED_FEATURES during covered! since it would fail when multiple files are loaded
282
- "#{file} was expected to be covered, but was already loaded before tests started, which makes it uncoverable."
334
+ "#{file} was expected to be covered, but was already loaded before coverage started, which makes it uncoverable."
283
335
  else
284
- "#{file} was expected to be covered, but never loaded."
336
+ "#{file} was expected to be covered, but was never loaded."
285
337
  end
286
338
  end
287
339
 
288
- def file_under_test(file)
289
- file = file.dup
340
+ def guess_covered_file(test)
341
+ file = test.dup
290
342
 
291
343
  # remove caller junk to get nice error messages when something fails
292
344
  file.sub!(/\.rb\b.*/, '.rb')
@@ -304,7 +356,7 @@ module SingleCov
304
356
  end
305
357
 
306
358
  # rails things live in app
307
- file_part[0...0] = if file_part =~ /^(?:#{APP_FOLDERS.map { |f| Regexp.escape(f) }.join('|')})\//
359
+ file_part[0...0] = if file_part =~ /^(?:#{RAILS_APP_FOLDERS.map { |f| Regexp.escape(f) }.join('|')})\//
308
360
  "app/"
309
361
  elsif file_part.start_with?("lib/") # don't add lib twice
310
362
  ""
@@ -313,8 +365,8 @@ module SingleCov
313
365
  end
314
366
 
315
367
  # remove test extension
316
- unless file_part.sub!(/_(?:test|spec)\.rb\b.*/, '.rb')
317
- raise "Unable to remove test extension from #{file} ... _test.rb and _spec.rb are supported"
368
+ if !file_part.sub!(/_(?:test|spec)\.rb\b.*/, '.rb') && !file_part.sub!(/\/test_/, "/")
369
+ raise "Unable to remove test extension from #{file} ... /test_, _test.rb and _spec.rb are supported"
318
370
  end
319
371
 
320
372
  # put back the subfolder
@@ -328,5 +380,27 @@ module SingleCov
328
380
  def root
329
381
  @root ||= (defined?(Bundler) && Bundler.root.to_s.sub(/\/gemfiles$/, '')) || Dir.pwd
330
382
  end
383
+
384
+ def generate_report(results)
385
+ return unless report = coverage_report
386
+
387
+ # not a hard dependency for the whole library
388
+ require "json"
389
+ require "fileutils"
390
+
391
+ used = COVERAGES.map { |f, _| "#{root}/#{f}" }
392
+ covered = results.select { |k, _| used.include?(k) }
393
+
394
+ if coverage_report_lines
395
+ covered = covered.transform_values { |v| v.is_a?(Hash) ? v.fetch(:lines) : v }
396
+ end
397
+
398
+ # chose "Minitest" because it is what simplecov uses for reports and "Unit Tests" makes sonarqube break
399
+ data = JSON.pretty_generate(
400
+ "Minitest" => { "coverage" => covered, "timestamp" => Time.now.to_i }
401
+ )
402
+ FileUtils.mkdir_p(File.dirname(report))
403
+ File.write report, data
404
+ end
331
405
  end
332
406
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module SingleCov
2
- VERSION = "1.0.3"
3
+ VERSION = "1.6.0"
3
4
  end
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.0.3
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: 2018-04-30 00:00:00.000000000 Z
11
+ date: 2020-11-07 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email: michael@grosser.it
@@ -31,15 +31,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
31
31
  requirements:
32
32
  - - ">="
33
33
  - !ruby/object:Gem::Version
34
- version: 2.0.0
34
+ version: 2.5.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
- rubyforge_project:
42
- rubygems_version: 2.7.6
41
+ rubygems_version: 3.1.3
43
42
  signing_key:
44
43
  specification_version: 4
45
44
  summary: Actionable code coverage.