single_cov 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2cf305464b1da18b681573d4e895ba407a3205665d17b2a6ebb344dfcf53d0c
4
- data.tar.gz: baaf64ed79b78aaea3b4d19a60d4f068f38bee2473dae5ec7ac7a85392e8b05d
3
+ metadata.gz: b1061e2ee0841966f31a082926db0474550414ac8e06f395696152ba3b985aa5
4
+ data.tar.gz: 836d974a4b39cbbf4b5454334997f60ac4d2afe5aafd4609bc2e04fb57f5a991
5
5
  SHA512:
6
- metadata.gz: 9c78ebf762c39a959945fba05bea127029d9594dcdf973e146e47ad9a5d1070e1db4196c2deac23e1c8ce44c886547b25e5f092f19f7b2643fe3b8611f67afab
7
- data.tar.gz: 96633f2c63bd2e512a57e45130ce71aacc5d16f4e1636f77be274bfe405be9526728c69ab4d6d14bf9db5a13805cd30ccb6e47690f73db522df52c450d7c7ee2
6
+ metadata.gz: e30055d56774315c96b907ef0d930c83a4ac921855d0000760ade00d47af0e3394a96853f13f669e4fd797e0d8d2d5aeadbfb1e8a8549f45ca36629a80befae6
7
+ data.tar.gz: 6e42c196b4cbb0eccb982b3bb942cf1664d8aa99925eabc7c9fb6165d10dd94de36640527556a9209739058501a36e191fc9136f21c903e0aa57307c63b2bd73
@@ -1,75 +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", "channels"]
5
- BRANCH_COVERAGE_SUPPORTED = (RUBY_VERSION >= "2.5.0")
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
- # 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
10
16
  def rewrite(&block)
11
17
  @rewrite = block
12
18
  end
13
19
 
20
+ # mark a test file as not covering anything to make assert_used pass
14
21
  def not_covered!
15
- store_pid
22
+ main_process!
16
23
  end
17
24
 
25
+ # mark the file under test as needing coverage
18
26
  def covered!(file: nil, uncovered: 0)
19
- file = guess_and_check_covered_file(file)
27
+ file = ensure_covered_file(file)
20
28
  COVERAGES << [file, uncovered]
21
- store_pid
29
+ main_process!
22
30
  end
23
31
 
24
32
  def all_covered?(result)
25
- errors = COVERAGES.map do |file, expected_uncovered|
26
- if coverage = result["#{root}/#{file}"]
27
- line_coverage = (coverage.is_a?(Hash) ? coverage.fetch(:lines) : coverage)
28
- uncovered = line_coverage.each_with_index.map { |c, i| i + 1 if c == 0 }.compact
29
- branch_coverage = (coverage.is_a?(Hash) && coverage[:branches])
30
-
31
- if branch_coverage
32
- uncovered.concat uncovered_branches(branch_coverage, uncovered)
33
- end
33
+ errors = COVERAGES.flat_map do |file, expected_uncovered|
34
+ next no_coverage_error(file) unless coverage = result["#{root}/#{file}"]
34
35
 
35
- next if uncovered.size == expected_uncovered
36
-
37
- # ignore lines that are marked as uncovered via comments
38
- # NOTE: ideally we should also warn when using uncovered but the section is indeed covered
39
- content = File.readlines(file)
40
- uncovered.reject! do |line_start, _, _, _|
41
- content[line_start - 1].include?(UNCOVERED_COMMENT_MARKER)
42
- end
36
+ uncovered = uncovered(coverage)
37
+ next if uncovered.size == expected_uncovered
43
38
 
44
- next if uncovered.size == expected_uncovered
45
-
46
- # branches are unsorted and added to the end, only sort when necessary
47
- if branch_coverage
48
- uncovered.sort_by! { |line_start, char_start, _, _| [line_start, char_start || 0] }
49
- end
50
-
51
- uncovered.map! do |line_start, char_start, line_end, char_end|
52
- char_start ? "#{file}:#{line_start}:#{char_start}-#{line_end}:#{char_end}" : "#{file}:#{line_start}"
53
- end
54
-
55
- warn_about_bad_coverage(file, expected_uncovered, uncovered)
56
- else
57
- 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)
58
44
  end
45
+ next if uncovered.size == expected_uncovered
46
+
47
+ bad_coverage_error(file, expected_uncovered, uncovered)
59
48
  end.compact
60
49
 
61
50
  return true if errors.empty?
62
51
 
63
- errors = errors.join("\n").split("\n") # unify arrays with multiline strings
64
52
  errors[MAX_OUTPUT..-1] = "... coverage output truncated" if errors.size >= MAX_OUTPUT
65
53
  warn errors
66
54
 
67
- errors.all? { |l| l.end_with?('?') } # ok if we just have warnings
55
+ errors.all? { |l| warning?(l) }
68
56
  end
69
57
 
70
58
  def assert_used(tests: default_tests)
71
59
  bad = tests.select do |file|
72
- File.read(file) !~ /SingleCov.(not_)?covered\!/
60
+ File.read(file) !~ /SingleCov.(not_)?covered!/
73
61
  end
74
62
  unless bad.empty?
75
63
  raise bad.map { |f| "#{f}: needs to use SingleCov.covered!" }.join("\n")
@@ -77,7 +65,7 @@ module SingleCov
77
65
  end
78
66
 
79
67
  def assert_tested(files: glob('{app,lib}/**/*.rb'), tests: default_tests, untested: [])
80
- missing = files - tests.map { |t| file_under_test(t) }
68
+ missing = files - tests.map { |t| guess_covered_file(t) }
81
69
  fixed = untested - missing
82
70
  missing -= untested
83
71
 
@@ -88,13 +76,10 @@ module SingleCov
88
76
  end
89
77
  end
90
78
 
91
- def setup(framework, root: nil, branches: BRANCH_COVERAGE_SUPPORTED)
79
+ def setup(framework, root: nil, branches: true)
92
80
  if defined?(SimpleCov)
93
81
  raise "Load SimpleCov after SingleCov"
94
82
  end
95
- if branches && !BRANCH_COVERAGE_SUPPORTED
96
- raise "Branch coverage needs ruby >= 2.5.0"
97
- end
98
83
 
99
84
  @branches = branches
100
85
  @root = root
@@ -112,7 +97,11 @@ module SingleCov
112
97
  start_coverage_recording
113
98
 
114
99
  override_at_exit do |status, _exception|
115
- exit 1 if enabled? && main_process? && 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
116
105
  end
117
106
  end
118
107
 
@@ -123,19 +112,38 @@ module SingleCov
123
112
 
124
113
  private
125
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
+
126
133
  def enabled?
127
134
  (!defined?(@disabled) || !@disabled)
128
135
  end
129
136
 
130
- def store_pid
131
- @pid = Process.pid
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
132
140
  end
133
141
 
134
142
  def main_process?
135
- (!defined?(@pid) || @pid == Process.pid)
143
+ (!defined?(@main_process_pid) || @main_process_pid == Process.pid)
136
144
  end
137
145
 
138
- def uncovered_branches(coverage, uncovered_lines)
146
+ def uncovered_branches(coverage)
139
147
  # {[branch_id] => {[branch_part] => coverage}} --> {branch_part -> sum-of-coverage}
140
148
  sum = Hash.new(0)
141
149
  coverage.each_value do |branch|
@@ -144,9 +152,8 @@ module SingleCov
144
152
  end
145
153
  end
146
154
 
147
- # show missing coverage
148
- found = sum.select { |k, v| v.zero? && !uncovered_lines.include?(k[0]) }.
149
- 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] }
150
157
  found.uniq!
151
158
  found
152
159
  end
@@ -162,7 +169,12 @@ module SingleCov
162
169
  # do not ask for coverage when SimpleCov already does or it conflicts
163
170
  def coverage_results
164
171
  if defined?(SimpleCov) && (result = SimpleCov.instance_variable_get(:@result))
165
- 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
166
178
  else
167
179
  Coverage.result
168
180
  end
@@ -175,7 +187,7 @@ module SingleCov
175
187
  if @branches
176
188
  Coverage.start(lines: true, branches: true)
177
189
  else
178
- Coverage.start
190
+ Coverage.start(lines: true)
179
191
  end
180
192
  end
181
193
 
@@ -233,7 +245,7 @@ module SingleCov
233
245
  end
234
246
 
235
247
  def rspec_running_subset_of_tests?
236
- (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:]+\]$/ }
237
249
  end
238
250
 
239
251
  # code stolen from SimpleCov
@@ -258,15 +270,13 @@ module SingleCov
258
270
  end
259
271
  end
260
272
 
261
- def guess_and_check_covered_file(file)
262
- if file && file.start_with?("/")
263
- raise "Use paths relative to root."
264
- end
273
+ def ensure_covered_file(file)
274
+ raise "Use paths relative to project root." if file&.start_with?("/")
265
275
 
266
276
  if file
267
- 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}")
268
278
  else
269
- file = file_under_test(caller[1])
279
+ file = guess_covered_file(caller[1])
270
280
  if file.start_with?("/")
271
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."
272
282
  elsif !File.exist?("#{root}/#{file}")
@@ -277,21 +287,39 @@ module SingleCov
277
287
  file
278
288
  end
279
289
 
280
- def warn_about_bad_coverage(file, expected_uncovered, uncovered_lines)
281
- details = "(#{uncovered_lines.size} current vs #{expected_uncovered} configured)"
282
- 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
283
293
  if running_single_file?
284
- "#{file} has less uncovered lines #{details}, decrement configured uncovered?"
294
+ warning "#{file} has less uncovered lines #{details}, decrement configured uncovered"
285
295
  end
286
296
  else
287
297
  [
288
298
  "#{file} new uncovered lines introduced #{details}",
289
299
  red("Lines missing coverage:"),
290
- *uncovered_lines
291
- ].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
+ ]
292
312
  end
293
313
  end
294
314
 
315
+ def warning(msg)
316
+ "#{msg}?"
317
+ end
318
+
319
+ def warning?(msg)
320
+ msg.end_with?("?")
321
+ end
322
+
295
323
  def red(text)
296
324
  if $stdin.tty?
297
325
  "\e[31m#{text}\e[0m"
@@ -300,17 +328,17 @@ module SingleCov
300
328
  end
301
329
  end
302
330
 
303
- def warn_about_no_coverage(file)
331
+ def no_coverage_error(file)
304
332
  if $LOADED_FEATURES.include?("#{root}/#{file}")
305
333
  # we cannot enforce $LOADED_FEATURES during covered! since it would fail when multiple files are loaded
306
- "#{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."
307
335
  else
308
- "#{file} was expected to be covered, but never loaded."
336
+ "#{file} was expected to be covered, but was never loaded."
309
337
  end
310
338
  end
311
339
 
312
- def file_under_test(file)
313
- file = file.dup
340
+ def guess_covered_file(test)
341
+ file = test.dup
314
342
 
315
343
  # remove caller junk to get nice error messages when something fails
316
344
  file.sub!(/\.rb\b.*/, '.rb')
@@ -328,7 +356,7 @@ module SingleCov
328
356
  end
329
357
 
330
358
  # rails things live in app
331
- 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('|')})\//
332
360
  "app/"
333
361
  elsif file_part.start_with?("lib/") # don't add lib twice
334
362
  ""
@@ -337,8 +365,8 @@ module SingleCov
337
365
  end
338
366
 
339
367
  # remove test extension
340
- unless file_part.sub!(/_(?:test|spec)\.rb\b.*/, '.rb')
341
- 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"
342
370
  end
343
371
 
344
372
  # put back the subfolder
@@ -352,5 +380,27 @@ module SingleCov
352
380
  def root
353
381
  @root ||= (defined?(Bundler) && Bundler.root.to_s.sub(/\/gemfiles$/, '')) || Dir.pwd
354
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
355
405
  end
356
406
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module SingleCov
2
- VERSION = "1.3.0"
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.3.0
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-12-14 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.