single_cov 1.0.3 → 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 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.