single_cov 1.5.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: 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
  - - ">="