simplecov 0.18.4 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "fileutils"
4
4
  require "docile"
5
- require "simplecov/formatter/multi_formatter"
5
+ require_relative "formatter/multi_formatter"
6
6
 
7
7
  module SimpleCov
8
8
  #
@@ -10,7 +10,7 @@ module SimpleCov
10
10
  # defined here are usable from SimpleCov directly. Please check out
11
11
  # SimpleCov documentation for further info.
12
12
  #
13
- module Configuration # rubocop:disable Metrics/ModuleLength
13
+ module Configuration
14
14
  attr_writer :filters, :groups, :formatter, :print_error_status
15
15
 
16
16
  #
@@ -22,6 +22,7 @@ module SimpleCov
22
22
  def root(root = nil)
23
23
  return @root if defined?(@root) && root.nil?
24
24
 
25
+ @coverage_path = nil # invalidate cache
25
26
  @root = File.expand_path(root || Dir.getwd)
26
27
  end
27
28
 
@@ -190,12 +191,59 @@ module SimpleCov
190
191
  # end
191
192
  #
192
193
  def at_exit(&block)
193
- return proc {} unless running || block_given?
194
+ return Proc.new unless running || block_given?
194
195
 
195
196
  @at_exit = block if block_given?
196
197
  @at_exit ||= proc { SimpleCov.result.format! }
197
198
  end
198
199
 
200
+ # gets or sets the enabled_for_subprocess configuration
201
+ # when true, this will inject SimpleCov code into Process.fork
202
+ def enable_for_subprocesses(value = nil)
203
+ return @enable_for_subprocesses if defined?(@enable_for_subprocesses) && value.nil?
204
+
205
+ @enable_for_subprocesses = value || false
206
+ end
207
+
208
+ # gets the enabled_for_subprocess configuration
209
+ def enabled_for_subprocesses?
210
+ enable_for_subprocesses
211
+ end
212
+
213
+ #
214
+ # Gets or sets the behavior to start a new forked Process.
215
+ #
216
+ # By default, it will add " (Process #{pid})" to the command_name, and start SimpleCov in quiet mode
217
+ #
218
+ # Configure with:
219
+ #
220
+ # SimpleCov.at_fork do |pid|
221
+ # SimpleCov.start do
222
+ # # This needs a unique name so it won't be ovewritten
223
+ # SimpleCov.command_name "#{SimpleCov.command_name} (subprocess: #{pid})"
224
+ # # be quiet, the parent process will be in charge of using the regular formatter and checking coverage totals
225
+ # SimpleCov.print_error_status = false
226
+ # SimpleCov.formatter SimpleCov::Formatter::SimpleFormatter
227
+ # SimpleCov.minimum_coverage 0
228
+ # # start
229
+ # SimpleCov.start
230
+ # end
231
+ # end
232
+ #
233
+ def at_fork(&block)
234
+ @at_fork = block if block_given?
235
+ @at_fork ||= lambda { |pid|
236
+ # This needs a unique name so it won't be ovewritten
237
+ SimpleCov.command_name "#{SimpleCov.command_name} (subprocess: #{pid})"
238
+ # be quiet, the parent process will be in charge of using the regular formatter and checking coverage totals
239
+ SimpleCov.print_error_status = false
240
+ SimpleCov.formatter SimpleCov::Formatter::SimpleFormatter
241
+ SimpleCov.minimum_coverage 0
242
+ # start
243
+ SimpleCov.start
244
+ }
245
+ end
246
+
199
247
  #
200
248
  # Returns the project name - currently assuming the last dirname in
201
249
  # the SimpleCov.root is this.
@@ -242,15 +290,20 @@ module SimpleCov
242
290
  def minimum_coverage(coverage = nil)
243
291
  return @minimum_coverage ||= {} unless coverage
244
292
 
245
- coverage = {DEFAULT_COVERAGE_CRITERION => coverage} if coverage.is_a?(Numeric)
246
- coverage.keys.each { |criterion| raise_if_criterion_disabled(criterion) }
247
- coverage.values.each do |percent|
248
- minimum_possible_coverage_exceeded("minimum_coverage") if percent && percent > 100
249
- end
293
+ coverage = {primary_coverage => coverage} if coverage.is_a?(Numeric)
294
+
295
+ raise_on_invalid_coverage(coverage, "minimum_coverage")
250
296
 
251
297
  @minimum_coverage = coverage
252
298
  end
253
299
 
300
+ def raise_on_invalid_coverage(coverage, coverage_setting)
301
+ coverage.each_key { |criterion| raise_if_criterion_disabled(criterion) }
302
+ coverage.each_value do |percent|
303
+ minimum_possible_coverage_exceeded(coverage_setting) if percent && percent > 100
304
+ end
305
+ end
306
+
254
307
  #
255
308
  # Defines the maximum coverage drop at once allowed for the testsuite to pass.
256
309
  # SimpleCov will return non-zero if the coverage decreases by more than this threshold.
@@ -258,7 +311,13 @@ module SimpleCov
258
311
  # Default is 100% (disabled)
259
312
  #
260
313
  def maximum_coverage_drop(coverage_drop = nil)
261
- @maximum_coverage_drop ||= (coverage_drop || 100).to_f.round(2)
314
+ return @maximum_coverage_drop ||= {} unless coverage_drop
315
+
316
+ coverage_drop = {primary_coverage => coverage_drop} if coverage_drop.is_a?(Numeric)
317
+
318
+ raise_on_invalid_coverage(coverage_drop, "maximum_coverage_drop")
319
+
320
+ @maximum_coverage_drop = coverage_drop
262
321
  end
263
322
 
264
323
  #
@@ -269,16 +328,23 @@ module SimpleCov
269
328
  # Default is 0% (disabled)
270
329
  #
271
330
  def minimum_coverage_by_file(coverage = nil)
272
- minimum_possible_coverage_exceeded("minimum_coverage_by_file") if coverage && coverage > 100
273
- @minimum_coverage_by_file ||= (coverage || 0).to_f.round(2)
331
+ return @minimum_coverage_by_file ||= {} unless coverage
332
+
333
+ coverage = {primary_coverage => coverage} if coverage.is_a?(Numeric)
334
+
335
+ raise_on_invalid_coverage(coverage, "minimum_coverage_by_file")
336
+
337
+ @minimum_coverage_by_file = coverage
274
338
  end
275
339
 
276
340
  #
277
341
  # Refuses any coverage drop. That is, coverage is only allowed to increase.
278
342
  # SimpleCov will return non-zero if the coverage decreases.
279
343
  #
280
- def refuse_coverage_drop
281
- maximum_coverage_drop 0
344
+ def refuse_coverage_drop(*criteria)
345
+ criteria = coverage_criteria if criteria.empty?
346
+
347
+ maximum_coverage_drop(criteria.map { |c| [c, 0] }.to_h)
282
348
  end
283
349
 
284
350
  #
@@ -325,7 +391,7 @@ module SimpleCov
325
391
  # @param [Symbol] criterion
326
392
  #
327
393
  def coverage_criterion(criterion = nil)
328
- return @coverage_criterion ||= DEFAULT_COVERAGE_CRITERION unless criterion
394
+ return @coverage_criterion ||= primary_coverage unless criterion
329
395
 
330
396
  raise_if_criterion_unsupported(criterion)
331
397
 
@@ -338,8 +404,17 @@ module SimpleCov
338
404
  coverage_criteria << criterion
339
405
  end
340
406
 
407
+ def primary_coverage(criterion = nil)
408
+ if criterion.nil?
409
+ @primary_coverage ||= DEFAULT_COVERAGE_CRITERION
410
+ else
411
+ raise_if_criterion_disabled(criterion)
412
+ @primary_coverage = criterion
413
+ end
414
+ end
415
+
341
416
  def coverage_criteria
342
- @coverage_criteria ||= Set[DEFAULT_COVERAGE_CRITERION]
417
+ @coverage_criteria ||= Set[primary_coverage]
343
418
  end
344
419
 
345
420
  def coverage_criterion_enabled?(criterion)
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "simplecov-html"
4
+ module SimpleCov
5
+ module Formatter
6
+ class << self
7
+ def from_env(env)
8
+ formatters = [SimpleCov::Formatter::HTMLFormatter]
9
+
10
+ # When running under a CI that uses CodeClimate, JSON output is expected
11
+ if env.fetch("CC_TEST_REPORTER_ID", nil)
12
+ require "simplecov_json_formatter"
13
+ formatters.push(SimpleCov::Formatter::JSONFormatter)
14
+ end
15
+
16
+ formatters
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,17 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Load default formatter gem
4
- require "simplecov-html"
5
4
  require "pathname"
6
- require "simplecov/profiles/root_filter"
7
- require "simplecov/profiles/test_frameworks"
8
- require "simplecov/profiles/bundler_filter"
9
- require "simplecov/profiles/hidden_filter"
10
- require "simplecov/profiles/rails"
5
+ require_relative "default_formatter"
6
+ require_relative "profiles/root_filter"
7
+ require_relative "profiles/test_frameworks"
8
+ require_relative "profiles/bundler_filter"
9
+ require_relative "profiles/hidden_filter"
10
+ require_relative "profiles/rails"
11
11
 
12
12
  # Default configuration
13
13
  SimpleCov.configure do
14
- formatter SimpleCov::Formatter::HTMLFormatter
14
+ formatter SimpleCov::Formatter::MultiFormatter.new(
15
+ SimpleCov::Formatter.from_env(ENV)
16
+ )
17
+
15
18
  load_profile "bundler_filter"
16
19
  load_profile "hidden_filter"
17
20
  # Exclude files outside of SimpleCov.root
@@ -28,7 +31,7 @@ at_exit do
28
31
  end
29
32
 
30
33
  # Autoload config from ~/.simplecov if present
31
- require "simplecov/load_global_config"
34
+ require_relative "load_global_config"
32
35
 
33
36
  # Autoload config from .simplecov if present
34
37
  # Recurse upwards until we find .simplecov or reach the root directory
@@ -8,3 +8,8 @@ module SimpleCov
8
8
  MAXIMUM_COVERAGE_DROP = 3
9
9
  end
10
10
  end
11
+
12
+ require_relative "exit_codes/exit_code_handling"
13
+ require_relative "exit_codes/maximum_coverage_drop_check"
14
+ require_relative "exit_codes/minimum_coverage_by_file_check"
15
+ require_relative "exit_codes/minimum_overall_coverage_check"
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module ExitCodes
5
+ module ExitCodeHandling
6
+ module_function
7
+
8
+ def call(result, coverage_limits:)
9
+ checks = coverage_checks(result, coverage_limits)
10
+
11
+ failing_check = checks.find(&:failing?)
12
+ if failing_check
13
+ failing_check.report
14
+ failing_check.exit_code
15
+ else
16
+ SimpleCov::ExitCodes::SUCCESS
17
+ end
18
+ end
19
+
20
+ def coverage_checks(result, coverage_limits)
21
+ [
22
+ MinimumOverallCoverageCheck.new(result, coverage_limits.minimum_coverage),
23
+ MinimumCoverageByFileCheck.new(result, coverage_limits.minimum_coverage_by_file),
24
+ MaximumCoverageDropCheck.new(result, coverage_limits.maximum_coverage_drop)
25
+ ]
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module ExitCodes
5
+ class MaximumCoverageDropCheck
6
+ def initialize(result, maximum_coverage_drop)
7
+ @result = result
8
+ @maximum_coverage_drop = maximum_coverage_drop
9
+ end
10
+
11
+ def failing?
12
+ return false unless maximum_coverage_drop && last_run
13
+
14
+ coverage_drop_violations.any?
15
+ end
16
+
17
+ def report
18
+ coverage_drop_violations.each do |violation|
19
+ $stderr.printf(
20
+ "%<criterion>s coverage has dropped by %<drop_percent>.2f%% since the last time (maximum allowed: %<max_drop>.2f%%).\n",
21
+ criterion: violation[:criterion].capitalize,
22
+ drop_percent: SimpleCov.round_coverage(violation[:drop_percent]),
23
+ max_drop: violation[:max_drop]
24
+ )
25
+ end
26
+ end
27
+
28
+ def exit_code
29
+ SimpleCov::ExitCodes::MAXIMUM_COVERAGE_DROP
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :result, :maximum_coverage_drop
35
+
36
+ def last_run
37
+ return @last_run if defined?(@last_run)
38
+
39
+ @last_run = SimpleCov::LastRun.read
40
+ end
41
+
42
+ def coverage_drop_violations
43
+ @coverage_drop_violations ||=
44
+ compute_coverage_drop_data.select do |achieved|
45
+ achieved.fetch(:max_drop) < achieved.fetch(:drop_percent)
46
+ end
47
+ end
48
+
49
+ def compute_coverage_drop_data
50
+ maximum_coverage_drop.map do |criterion, percent|
51
+ {
52
+ criterion: criterion,
53
+ max_drop: percent,
54
+ drop_percent: last_coverage(criterion) -
55
+ SimpleCov.round_coverage(
56
+ result.coverage_statistics.fetch(criterion).percent
57
+ )
58
+ }
59
+ end
60
+ end
61
+
62
+ def last_coverage(criterion)
63
+ last_coverage_percent = last_run[:result][criterion]
64
+
65
+ if !last_coverage_percent && criterion == "line"
66
+ last_run[:result][:covered_percent]
67
+ else
68
+ last_coverage_percent
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module ExitCodes
5
+ class MinimumCoverageByFileCheck
6
+ def initialize(result, minimum_coverage_by_file)
7
+ @result = result
8
+ @minimum_coverage_by_file = minimum_coverage_by_file
9
+ end
10
+
11
+ def failing?
12
+ minimum_violations.any?
13
+ end
14
+
15
+ def report
16
+ minimum_violations.each do |violation|
17
+ $stderr.printf(
18
+ "%<criterion>s coverage (%<covered>.2f%%) is below the expected minimum coverage (%<minimum_coverage>.2f%%).\n",
19
+ covered: SimpleCov.round_coverage(violation.fetch(:actual)),
20
+ minimum_coverage: violation.fetch(:minimum_expected),
21
+ criterion: violation.fetch(:criterion).capitalize
22
+ )
23
+ end
24
+ end
25
+
26
+ def exit_code
27
+ SimpleCov::ExitCodes::MINIMUM_COVERAGE
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :result, :minimum_coverage_by_file
33
+
34
+ def minimum_violations
35
+ @minimum_violations ||=
36
+ compute_minimum_coverage_data.select do |achieved|
37
+ achieved.fetch(:actual) < achieved.fetch(:minimum_expected)
38
+ end
39
+ end
40
+
41
+ def compute_minimum_coverage_data
42
+ minimum_coverage_by_file.flat_map do |criterion, expected_percent|
43
+ result.coverage_statistics_by_file[criterion].map do |actual_percent|
44
+ {
45
+ criterion: criterion,
46
+ minimum_expected: expected_percent,
47
+ actual: SimpleCov.round_coverage(actual_percent)
48
+ }
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module ExitCodes
5
+ class MinimumOverallCoverageCheck
6
+ def initialize(result, minimum_coverage)
7
+ @result = result
8
+ @minimum_coverage = minimum_coverage
9
+ end
10
+
11
+ def failing?
12
+ minimum_violations.any?
13
+ end
14
+
15
+ def report
16
+ minimum_violations.each do |violation|
17
+ $stderr.printf(
18
+ "%<criterion>s coverage (%<covered>.2f%%) is below the expected minimum coverage (%<minimum_coverage>.2f%%).\n",
19
+ covered: SimpleCov.round_coverage(violation.fetch(:actual)),
20
+ minimum_coverage: violation.fetch(:minimum_expected),
21
+ criterion: violation.fetch(:criterion).capitalize
22
+ )
23
+ end
24
+ end
25
+
26
+ def exit_code
27
+ SimpleCov::ExitCodes::MINIMUM_COVERAGE
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :result, :minimum_coverage
33
+
34
+ def minimum_violations
35
+ @minimum_violations ||= calculate_minimum_violations
36
+ end
37
+
38
+ def calculate_minimum_violations
39
+ coverage_achieved = minimum_coverage.map do |criterion, percent|
40
+ {
41
+ criterion: criterion,
42
+ minimum_expected: percent,
43
+ actual: result.coverage_statistics.fetch(criterion).percent
44
+ }
45
+ end
46
+
47
+ coverage_achieved.select do |achieved|
48
+ achieved.fetch(:actual) < achieved.fetch(:minimum_expected)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end