single_cov 1.5.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: 8a086ee2f32cf635cc63929751ba9440fd473b9a5b6f6f5cae7e4546efd4db56
4
- data.tar.gz: 7a8a8a4d601f53d79ed45b93e1eb7f06fc7b917fd01ffc41d7e3e02bf1f0825c
3
+ metadata.gz: b1061e2ee0841966f31a082926db0474550414ac8e06f395696152ba3b985aa5
4
+ data.tar.gz: 836d974a4b39cbbf4b5454334997f60ac4d2afe5aafd4609bc2e04fb57f5a991
5
5
  SHA512:
6
- metadata.gz: ff29c57455d8fe0b7a16d14a58b5efb2d97a5cbbca25b03da77f322e4443e12e378f7ce5156770f5907f1db10a543907b5a0ea1d87950e2d05237edff9d28a2f
7
- data.tar.gz: 16eb10a22563b11c53e67b5037ce0b5f6d867435844fd3f153f6cdb892ffe4a3a46c113e6732fc1895bbb6c6bd17e7d5c3666c636b24c23c78a48cfd1a46eae9
6
+ metadata.gz: e30055d56774315c96b907ef0d930c83a4ac921855d0000760ade00d47af0e3394a96853f13f669e4fd797e0d8d2d5aeadbfb1e8a8549f45ca36629a80befae6
7
+ data.tar.gz: 6e42c196b4cbb0eccb982b3bb942cf1664d8aa99925eabc7c9fb6165d10dd94de36640527556a9209739058501a36e191fc9136f21c903e0aa57307c63b2bd73
@@ -1,87 +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
- # enable coverage reporting: location of output file, changed by forking-test-runner to combine multiple reports
9
+ # enable coverage reporting: path to output file, changed by forking-test-runner at runtime to combine many reports
10
10
  attr_accessor :coverage_report
11
11
 
12
12
  # emit only line coverage in coverage report for older coverage systems
13
13
  attr_accessor :coverage_report_lines
14
14
 
15
- # optionally rewrite the file we guessed with a lambda
15
+ # optionally rewrite the matching path single-cov guessed with a lambda
16
16
  def rewrite(&block)
17
17
  @rewrite = block
18
18
  end
19
19
 
20
+ # mark a test file as not covering anything to make assert_used pass
20
21
  def not_covered!
21
- store_pid
22
+ main_process!
22
23
  end
23
24
 
25
+ # mark the file under test as needing coverage
24
26
  def covered!(file: nil, uncovered: 0)
25
- file = guess_and_check_covered_file(file)
27
+ file = ensure_covered_file(file)
26
28
  COVERAGES << [file, uncovered]
27
- store_pid
29
+ main_process!
28
30
  end
29
31
 
30
32
  def all_covered?(result)
31
- errors = COVERAGES.map do |file, expected_uncovered|
32
- if coverage = result["#{root}/#{file}"]
33
- line_coverage = (coverage.is_a?(Hash) ? coverage.fetch(:lines) : coverage)
34
- uncovered_lines = line_coverage.each_with_index.map { |c, i| i + 1 if c == 0 }.compact
35
-
36
- branch_coverage = (coverage.is_a?(Hash) && coverage[:branches])
37
- uncovered_branches = (branch_coverage ? uncovered_branches(branch_coverage, uncovered_lines) : [])
38
-
39
- uncovered = uncovered_lines.concat uncovered_branches
40
- next if uncovered.size == expected_uncovered
41
-
42
- # ignore lines that are marked as uncovered via comments
43
- # NOTE: ideally we should also warn when using uncovered but the section is indeed covered
44
- content = File.readlines(file)
45
- uncovered.reject! do |line_start, _, _, _|
46
- content[line_start - 1].include?(UNCOVERED_COMMENT_MARKER)
47
- end
48
- 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}"]
49
35
 
50
- # branches are unsorted and added to the end, only sort when displayed
51
- if branch_coverage
52
- uncovered.sort_by! { |line_start, char_start, _, _| [line_start, char_start || 0] }
53
- end
36
+ uncovered = uncovered(coverage)
37
+ next if uncovered.size == expected_uncovered
54
38
 
55
- uncovered.map! do |line_start, char_start, line_end, char_end|
56
- if char_start # branch coverage
57
- if line_start == line_end
58
- "#{file}:#{line_start}:#{char_start}-#{char_end}"
59
- else # possibly unreachable since branches always seem to be on the same line
60
- "#{file}:#{line_start}:#{char_start}-#{line_end}:#{char_end}"
61
- end
62
- else
63
- "#{file}:#{line_start}"
64
- end
65
- end
66
-
67
- warn_about_bad_coverage(file, expected_uncovered, uncovered)
68
- else
69
- 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)
70
44
  end
45
+ next if uncovered.size == expected_uncovered
46
+
47
+ bad_coverage_error(file, expected_uncovered, uncovered)
71
48
  end.compact
72
49
 
73
50
  return true if errors.empty?
74
51
 
75
- errors = errors.join("\n").split("\n") # unify arrays with multiline strings
76
52
  errors[MAX_OUTPUT..-1] = "... coverage output truncated" if errors.size >= MAX_OUTPUT
77
53
  warn errors
78
54
 
79
- errors.all? { |l| l.end_with?('?') } # ok if we just have warnings
55
+ errors.all? { |l| warning?(l) }
80
56
  end
81
57
 
82
58
  def assert_used(tests: default_tests)
83
59
  bad = tests.select do |file|
84
- File.read(file) !~ /SingleCov.(not_)?covered\!/
60
+ File.read(file) !~ /SingleCov.(not_)?covered!/
85
61
  end
86
62
  unless bad.empty?
87
63
  raise bad.map { |f| "#{f}: needs to use SingleCov.covered!" }.join("\n")
@@ -89,7 +65,7 @@ module SingleCov
89
65
  end
90
66
 
91
67
  def assert_tested(files: glob('{app,lib}/**/*.rb'), tests: default_tests, untested: [])
92
- missing = files - tests.map { |t| file_under_test(t) }
68
+ missing = files - tests.map { |t| guess_covered_file(t) }
93
69
  fixed = untested - missing
94
70
  missing -= untested
95
71
 
@@ -100,13 +76,10 @@ module SingleCov
100
76
  end
101
77
  end
102
78
 
103
- def setup(framework, root: nil, branches: BRANCH_COVERAGE_SUPPORTED)
79
+ def setup(framework, root: nil, branches: true)
104
80
  if defined?(SimpleCov)
105
81
  raise "Load SimpleCov after SingleCov"
106
82
  end
107
- if branches && !BRANCH_COVERAGE_SUPPORTED
108
- raise "Branch coverage needs ruby >= 2.5.0"
109
- end
110
83
 
111
84
  @branches = branches
112
85
  @root = root
@@ -139,19 +112,38 @@ module SingleCov
139
112
 
140
113
  private
141
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
+
142
133
  def enabled?
143
134
  (!defined?(@disabled) || !@disabled)
144
135
  end
145
136
 
146
- def store_pid
147
- @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
148
140
  end
149
141
 
150
142
  def main_process?
151
- (!defined?(@pid) || @pid == Process.pid)
143
+ (!defined?(@main_process_pid) || @main_process_pid == Process.pid)
152
144
  end
153
145
 
154
- def uncovered_branches(coverage, uncovered_lines)
146
+ def uncovered_branches(coverage)
155
147
  # {[branch_id] => {[branch_part] => coverage}} --> {branch_part -> sum-of-coverage}
156
148
  sum = Hash.new(0)
157
149
  coverage.each_value do |branch|
@@ -160,9 +152,8 @@ module SingleCov
160
152
  end
161
153
  end
162
154
 
163
- # show missing coverage
164
- sum.select! { |k, v| v.zero? && !uncovered_lines.include?(k[0]) }
165
- found = sum.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] }
166
157
  found.uniq!
167
158
  found
168
159
  end
@@ -178,7 +169,12 @@ module SingleCov
178
169
  # do not ask for coverage when SimpleCov already does or it conflicts
179
170
  def coverage_results
180
171
  if defined?(SimpleCov) && (result = SimpleCov.instance_variable_get(:@result))
181
- 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
182
178
  else
183
179
  Coverage.result
184
180
  end
@@ -191,7 +187,7 @@ module SingleCov
191
187
  if @branches
192
188
  Coverage.start(lines: true, branches: true)
193
189
  else
194
- Coverage.start
190
+ Coverage.start(lines: true)
195
191
  end
196
192
  end
197
193
 
@@ -249,7 +245,7 @@ module SingleCov
249
245
  end
250
246
 
251
247
  def rspec_running_subset_of_tests?
252
- (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:]+\]$/ }
253
249
  end
254
250
 
255
251
  # code stolen from SimpleCov
@@ -274,15 +270,13 @@ module SingleCov
274
270
  end
275
271
  end
276
272
 
277
- def guess_and_check_covered_file(file)
278
- if file && file.start_with?("/")
279
- raise "Use paths relative to root."
280
- end
273
+ def ensure_covered_file(file)
274
+ raise "Use paths relative to project root." if file&.start_with?("/")
281
275
 
282
276
  if file
283
- 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}")
284
278
  else
285
- file = file_under_test(caller[1])
279
+ file = guess_covered_file(caller[1])
286
280
  if file.start_with?("/")
287
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."
288
282
  elsif !File.exist?("#{root}/#{file}")
@@ -293,21 +287,39 @@ module SingleCov
293
287
  file
294
288
  end
295
289
 
296
- def warn_about_bad_coverage(file, expected_uncovered, uncovered_lines)
297
- details = "(#{uncovered_lines.size} current vs #{expected_uncovered} configured)"
298
- 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
299
293
  if running_single_file?
300
- "#{file} has less uncovered lines #{details}, decrement configured uncovered?"
294
+ warning "#{file} has less uncovered lines #{details}, decrement configured uncovered"
301
295
  end
302
296
  else
303
297
  [
304
298
  "#{file} new uncovered lines introduced #{details}",
305
299
  red("Lines missing coverage:"),
306
- *uncovered_lines
307
- ].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
+ ]
308
312
  end
309
313
  end
310
314
 
315
+ def warning(msg)
316
+ "#{msg}?"
317
+ end
318
+
319
+ def warning?(msg)
320
+ msg.end_with?("?")
321
+ end
322
+
311
323
  def red(text)
312
324
  if $stdin.tty?
313
325
  "\e[31m#{text}\e[0m"
@@ -316,17 +328,17 @@ module SingleCov
316
328
  end
317
329
  end
318
330
 
319
- def warn_about_no_coverage(file)
331
+ def no_coverage_error(file)
320
332
  if $LOADED_FEATURES.include?("#{root}/#{file}")
321
333
  # we cannot enforce $LOADED_FEATURES during covered! since it would fail when multiple files are loaded
322
- "#{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."
323
335
  else
324
- "#{file} was expected to be covered, but never loaded."
336
+ "#{file} was expected to be covered, but was never loaded."
325
337
  end
326
338
  end
327
339
 
328
- def file_under_test(file)
329
- file = file.dup
340
+ def guess_covered_file(test)
341
+ file = test.dup
330
342
 
331
343
  # remove caller junk to get nice error messages when something fails
332
344
  file.sub!(/\.rb\b.*/, '.rb')
@@ -344,7 +356,7 @@ module SingleCov
344
356
  end
345
357
 
346
358
  # rails things live in app
347
- 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('|')})\//
348
360
  "app/"
349
361
  elsif file_part.start_with?("lib/") # don't add lib twice
350
362
  ""
@@ -380,12 +392,12 @@ module SingleCov
380
392
  covered = results.select { |k, _| used.include?(k) }
381
393
 
382
394
  if coverage_report_lines
383
- covered = covered.each_with_object({}) { |(k, v), h| h[k] = v.is_a?(Hash) ? v.fetch(:lines) : v }
395
+ covered = covered.transform_values { |v| v.is_a?(Hash) ? v.fetch(:lines) : v }
384
396
  end
385
397
 
386
398
  # chose "Minitest" because it is what simplecov uses for reports and "Unit Tests" makes sonarqube break
387
399
  data = JSON.pretty_generate(
388
- "Minitest" => {"coverage" => covered, "timestamp" => Time.now.to_i }
400
+ "Minitest" => { "coverage" => covered, "timestamp" => Time.now.to_i }
389
401
  )
390
402
  FileUtils.mkdir_p(File.dirname(report))
391
403
  File.write report, data
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module SingleCov
2
- VERSION = "1.5.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.5.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: 2020-10-26 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,7 +31,7 @@ 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
  - - ">="