single_cov 1.3.0 → 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: 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.