single_cov 1.3.2 → 1.7.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: 91538588abc7b40a632293cb64315edb846af54cee5b894925716c84b5209423
4
- data.tar.gz: 9e2cdd8fb800e72cd957eb80e29a631045aad84075d8c016b5e5ac40cec14829
3
+ metadata.gz: 9b93f5dd228599154416958a87e4f96ddd97a5477d92b7fc6c43c80ff3d6485f
4
+ data.tar.gz: ccd0f0c8b95df16fae3c071000b2499cca41bffea096bd84a8d761444335b2c4
5
5
  SHA512:
6
- metadata.gz: 5daa193ee245aa4a55c49a5f5767286b5f0e1eb6a680f6977efe18eaaffa1867fd7c9fa941d4eafecb141995202a971df490fa4e6814b3662fc788c4509a939e
7
- data.tar.gz: fd0bbe8516e1bbf194886d87b38be3a5cf388d6afbecc796dbf6287a39778e81b251a44be890bd43ecc6f39ff8b93f246b272378430a7772b5cd06ee86274101
6
+ metadata.gz: 827b9ed7610e88def4654d18d8e2d4fa01c1f6c296a260c64ef8f4a722d1a478e195fefbfaf45afbeb8d6710a644f071f48e117cb927daf2d5bb0b42decd14e9
7
+ data.tar.gz: 3a21bb814833a4a73b5303f4b354218014f0a7b5e05eac89863b3508f7b6b488a06177577ce383d6a706847716dbc6706b37583d499deafee1a516a5d8a1e5c2
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module SingleCov
2
- VERSION = "1.3.2"
3
+ VERSION = "1.7.0"
3
4
  end
data/lib/single_cov.rb CHANGED
@@ -1,81 +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_lines = line_coverage.each_with_index.map { |c, i| i + 1 if c == 0 }.compact
29
-
30
- branch_coverage = (coverage.is_a?(Hash) && coverage[:branches])
31
- uncovered_branches = (branch_coverage ? uncovered_branches(branch_coverage, uncovered_lines) : [])
32
-
33
- uncovered = uncovered_lines.concat uncovered_branches
34
- next if uncovered.size == expected_uncovered
35
-
36
- # ignore lines that are marked as uncovered via comments
37
- # NOTE: ideally we should also warn when using uncovered but the section is indeed covered
38
- content = File.readlines(file)
39
- uncovered.reject! do |line_start, _, _, _|
40
- content[line_start - 1].include?(UNCOVERED_COMMENT_MARKER)
41
- end
42
- 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}"]
43
35
 
44
- # branches are unsorted and added to the end, only sort when displayed
45
- if branch_coverage
46
- uncovered.sort_by! { |line_start, char_start, _, _| [line_start, char_start || 0] }
47
- end
36
+ uncovered = uncovered(coverage)
37
+ next if uncovered.size == expected_uncovered
48
38
 
49
- uncovered.map! do |line_start, char_start, line_end, char_end|
50
- if char_start # branch coverage
51
- if line_start == line_end
52
- "#{file}:#{line_start}:#{char_start}-#{char_end}"
53
- else # possibly unreachable since branches always seem to be on the same line
54
- "#{file}:#{line_start}:#{char_start}-#{line_end}:#{char_end}"
55
- end
56
- else
57
- "#{file}:#{line_start}"
58
- end
59
- end
60
-
61
- warn_about_bad_coverage(file, expected_uncovered, uncovered)
62
- else
63
- 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)
64
44
  end
45
+ next if uncovered.size == expected_uncovered
46
+
47
+ bad_coverage_error(file, expected_uncovered, uncovered)
65
48
  end.compact
66
49
 
67
50
  return true if errors.empty?
68
51
 
69
- errors = errors.join("\n").split("\n") # unify arrays with multiline strings
70
52
  errors[MAX_OUTPUT..-1] = "... coverage output truncated" if errors.size >= MAX_OUTPUT
71
- warn errors
53
+ @error_logger.puts errors
72
54
 
73
- errors.all? { |l| l.end_with?('?') } # ok if we just have warnings
55
+ errors.all? { |l| warning?(l) }
74
56
  end
75
57
 
76
58
  def assert_used(tests: default_tests)
77
59
  bad = tests.select do |file|
78
- File.read(file) !~ /SingleCov.(not_)?covered\!/
60
+ File.read(file) !~ /SingleCov.(not_)?covered!/
79
61
  end
80
62
  unless bad.empty?
81
63
  raise bad.map { |f| "#{f}: needs to use SingleCov.covered!" }.join("\n")
@@ -83,7 +65,7 @@ module SingleCov
83
65
  end
84
66
 
85
67
  def assert_tested(files: glob('{app,lib}/**/*.rb'), tests: default_tests, untested: [])
86
- missing = files - tests.map { |t| file_under_test(t) }
68
+ missing = files - tests.map { |t| guess_covered_file(t) }
87
69
  fixed = untested - missing
88
70
  missing -= untested
89
71
 
@@ -94,13 +76,12 @@ module SingleCov
94
76
  end
95
77
  end
96
78
 
97
- def setup(framework, root: nil, branches: BRANCH_COVERAGE_SUPPORTED)
79
+ def setup(framework, root: nil, branches: true, err: $stderr)
80
+ @error_logger = err
81
+
98
82
  if defined?(SimpleCov)
99
83
  raise "Load SimpleCov after SingleCov"
100
84
  end
101
- if branches && !BRANCH_COVERAGE_SUPPORTED
102
- raise "Branch coverage needs ruby >= 2.5.0"
103
- end
104
85
 
105
86
  @branches = branches
106
87
  @root = root
@@ -118,7 +99,11 @@ module SingleCov
118
99
  start_coverage_recording
119
100
 
120
101
  override_at_exit do |status, _exception|
121
- exit 1 if enabled? && main_process? && status == 0 && !SingleCov.all_covered?(coverage_results)
102
+ if enabled? && main_process? && status == 0
103
+ results = coverage_results
104
+ generate_report results
105
+ exit 1 unless SingleCov.all_covered?(results)
106
+ end
122
107
  end
123
108
  end
124
109
 
@@ -129,19 +114,38 @@ module SingleCov
129
114
 
130
115
  private
131
116
 
117
+ def uncovered(coverage)
118
+ return coverage unless coverage.is_a?(Hash) # just lines
119
+
120
+ # [nil, 1, 0, 1, 0] -> [3, 5]
121
+ uncovered_lines = coverage.fetch(:lines)
122
+ .each_with_index
123
+ .select { |c, _| c == 0 }
124
+ .map { |_, i| i + 1 }
125
+ .compact
126
+
127
+ uncovered_branches = uncovered_branches(coverage[:branches] || {})
128
+ uncovered_branches.reject! { |k| uncovered_lines.include?(k[0]) } # remove duplicates
129
+
130
+ all = uncovered_lines.concat uncovered_branches
131
+ all.sort_by! { |line_start, char_start, _, _| [line_start, char_start || 0] } # branches are unsorted
132
+ all
133
+ end
134
+
132
135
  def enabled?
133
136
  (!defined?(@disabled) || !@disabled)
134
137
  end
135
138
 
136
- def store_pid
137
- @pid = Process.pid
139
+ # assuming that the main process will load all the files, we store it's pid
140
+ def main_process!
141
+ @main_process_pid = Process.pid
138
142
  end
139
143
 
140
144
  def main_process?
141
- (!defined?(@pid) || @pid == Process.pid)
145
+ (!defined?(@main_process_pid) || @main_process_pid == Process.pid)
142
146
  end
143
147
 
144
- def uncovered_branches(coverage, uncovered_lines)
148
+ def uncovered_branches(coverage)
145
149
  # {[branch_id] => {[branch_part] => coverage}} --> {branch_part -> sum-of-coverage}
146
150
  sum = Hash.new(0)
147
151
  coverage.each_value do |branch|
@@ -150,9 +154,8 @@ module SingleCov
150
154
  end
151
155
  end
152
156
 
153
- # show missing coverage
154
- sum.select! { |k, v| v.zero? && !uncovered_lines.include?(k[0]) }
155
- found = sum.map { |k, _| [k[0], k[1]+1, k[2], k[3]+1] }
157
+ sum.select! { |_, v| v == 0 } # keep missing coverage
158
+ found = sum.map { |k, _| [k[0], k[1] + 1, k[2], k[3] + 1] }
156
159
  found.uniq!
157
160
  found
158
161
  end
@@ -168,7 +171,12 @@ module SingleCov
168
171
  # do not ask for coverage when SimpleCov already does or it conflicts
169
172
  def coverage_results
170
173
  if defined?(SimpleCov) && (result = SimpleCov.instance_variable_get(:@result))
171
- result.original_result
174
+ result = result.original_result
175
+ # singlecov 1.18+ puts string "lines" into the result that we cannot read
176
+ if result.each_value.first.is_a?(Hash)
177
+ result = result.transform_values { |v| v.transform_keys(&:to_sym) }
178
+ end
179
+ result
172
180
  else
173
181
  Coverage.result
174
182
  end
@@ -181,7 +189,7 @@ module SingleCov
181
189
  if @branches
182
190
  Coverage.start(lines: true, branches: true)
183
191
  else
184
- Coverage.start
192
+ Coverage.start(lines: true)
185
193
  end
186
194
  end
187
195
 
@@ -239,7 +247,7 @@ module SingleCov
239
247
  end
240
248
 
241
249
  def rspec_running_subset_of_tests?
242
- (ARGV & ['-t', '--tag', '-e', '--example']).any? || ARGV.any? { |a| a =~ /\:\d+$|\[[\d:]+\]$/ }
250
+ (ARGV & ['-t', '--tag', '-e', '--example']).any? || ARGV.any? { |a| a =~ /:\d+$|\[[\d:]+\]$/ }
243
251
  end
244
252
 
245
253
  # code stolen from SimpleCov
@@ -264,15 +272,13 @@ module SingleCov
264
272
  end
265
273
  end
266
274
 
267
- def guess_and_check_covered_file(file)
268
- if file && file.start_with?("/")
269
- raise "Use paths relative to root."
270
- end
275
+ def ensure_covered_file(file)
276
+ raise "Use paths relative to project root." if file&.start_with?("/")
271
277
 
272
278
  if file
273
- raise "#{file} does not exist and cannot be covered." unless File.exist?("#{root}/#{file}")
279
+ raise "#{file} does not exist, use paths relative to project root." unless File.exist?("#{root}/#{file}")
274
280
  else
275
- file = file_under_test(caller[1])
281
+ file = guess_covered_file(caller[1])
276
282
  if file.start_with?("/")
277
283
  raise "Found file #{file} which is not relative to the root #{root}.\nUse `SingleCov.covered! file: 'target_file.rb'` to set covered file location."
278
284
  elsif !File.exist?("#{root}/#{file}")
@@ -283,21 +289,39 @@ module SingleCov
283
289
  file
284
290
  end
285
291
 
286
- def warn_about_bad_coverage(file, expected_uncovered, uncovered_lines)
287
- details = "(#{uncovered_lines.size} current vs #{expected_uncovered} configured)"
288
- if expected_uncovered > uncovered_lines.size
292
+ def bad_coverage_error(file, expected_uncovered, uncovered)
293
+ details = "(#{uncovered.size} current vs #{expected_uncovered} configured)"
294
+ if expected_uncovered > uncovered.size
289
295
  if running_single_file?
290
- "#{file} has less uncovered lines #{details}, decrement configured uncovered?"
296
+ warning "#{file} has less uncovered lines #{details}, decrement configured uncovered"
291
297
  end
292
298
  else
293
299
  [
294
300
  "#{file} new uncovered lines introduced #{details}",
295
301
  red("Lines missing coverage:"),
296
- *uncovered_lines
297
- ].join("\n")
302
+ *uncovered.map do |line_start, char_start, line_end, char_end|
303
+ if char_start # branch coverage
304
+ if line_start == line_end
305
+ "#{file}:#{line_start}:#{char_start}-#{char_end}"
306
+ else # possibly unreachable since branches always seem to be on the same line
307
+ "#{file}:#{line_start}:#{char_start}-#{line_end}:#{char_end}"
308
+ end
309
+ else
310
+ "#{file}:#{line_start}"
311
+ end
312
+ end
313
+ ]
298
314
  end
299
315
  end
300
316
 
317
+ def warning(msg)
318
+ "#{msg}?"
319
+ end
320
+
321
+ def warning?(msg)
322
+ msg.end_with?("?")
323
+ end
324
+
301
325
  def red(text)
302
326
  if $stdin.tty?
303
327
  "\e[31m#{text}\e[0m"
@@ -306,17 +330,17 @@ module SingleCov
306
330
  end
307
331
  end
308
332
 
309
- def warn_about_no_coverage(file)
333
+ def no_coverage_error(file)
310
334
  if $LOADED_FEATURES.include?("#{root}/#{file}")
311
335
  # we cannot enforce $LOADED_FEATURES during covered! since it would fail when multiple files are loaded
312
- "#{file} was expected to be covered, but was already loaded before tests started, which makes it uncoverable."
336
+ "#{file} was expected to be covered, but was already loaded before coverage started, which makes it uncoverable."
313
337
  else
314
- "#{file} was expected to be covered, but never loaded."
338
+ "#{file} was expected to be covered, but was never loaded."
315
339
  end
316
340
  end
317
341
 
318
- def file_under_test(file)
319
- file = file.dup
342
+ def guess_covered_file(test)
343
+ file = test.dup
320
344
 
321
345
  # remove caller junk to get nice error messages when something fails
322
346
  file.sub!(/\.rb\b.*/, '.rb')
@@ -334,7 +358,7 @@ module SingleCov
334
358
  end
335
359
 
336
360
  # rails things live in app
337
- file_part[0...0] = if file_part =~ /^(?:#{APP_FOLDERS.map { |f| Regexp.escape(f) }.join('|')})\//
361
+ file_part[0...0] = if file_part =~ /^(?:#{RAILS_APP_FOLDERS.map { |f| Regexp.escape(f) }.join('|')})\//
338
362
  "app/"
339
363
  elsif file_part.start_with?("lib/") # don't add lib twice
340
364
  ""
@@ -358,5 +382,27 @@ module SingleCov
358
382
  def root
359
383
  @root ||= (defined?(Bundler) && Bundler.root.to_s.sub(/\/gemfiles$/, '')) || Dir.pwd
360
384
  end
385
+
386
+ def generate_report(results)
387
+ return unless report = coverage_report
388
+
389
+ # not a hard dependency for the whole library
390
+ require "json"
391
+ require "fileutils"
392
+
393
+ used = COVERAGES.map { |f, _| "#{root}/#{f}" }
394
+ covered = results.select { |k, _| used.include?(k) }
395
+
396
+ if coverage_report_lines
397
+ covered = covered.transform_values { |v| v.is_a?(Hash) ? v.fetch(:lines) : v }
398
+ end
399
+
400
+ # chose "Minitest" because it is what simplecov uses for reports and "Unit Tests" makes sonarqube break
401
+ data = JSON.pretty_generate(
402
+ "Minitest" => { "coverage" => covered, "timestamp" => Time.now.to_i }
403
+ )
404
+ FileUtils.mkdir_p(File.dirname(report))
405
+ File.write report, data
406
+ end
361
407
  end
362
408
  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.2
4
+ version: 1.7.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: 2019-05-09 00:00:00.000000000 Z
11
+ date: 2022-01-13 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.6
43
42
  signing_key:
44
43
  specification_version: 4
45
44
  summary: Actionable code coverage.