simplecov 0.17.1 → 0.21.1

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +100 -434
  3. data/README.md +375 -93
  4. data/doc/alternate-formatters.md +16 -1
  5. data/doc/commercial-services.md +5 -0
  6. data/lib/minitest/simplecov_plugin.rb +15 -0
  7. data/lib/simplecov.rb +294 -128
  8. data/lib/simplecov/combine.rb +30 -0
  9. data/lib/simplecov/combine/branches_combiner.rb +32 -0
  10. data/lib/simplecov/combine/files_combiner.rb +24 -0
  11. data/lib/simplecov/combine/lines_combiner.rb +43 -0
  12. data/lib/simplecov/combine/results_combiner.rb +60 -0
  13. data/lib/simplecov/command_guesser.rb +6 -3
  14. data/lib/simplecov/configuration.rb +191 -15
  15. data/lib/simplecov/coverage_statistics.rb +56 -0
  16. data/lib/simplecov/default_formatter.rb +20 -0
  17. data/lib/simplecov/defaults.rb +14 -13
  18. data/lib/simplecov/exit_codes.rb +5 -0
  19. data/lib/simplecov/exit_codes/exit_code_handling.rb +29 -0
  20. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +73 -0
  21. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +54 -0
  22. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +53 -0
  23. data/lib/simplecov/file_list.rb +72 -13
  24. data/lib/simplecov/filter.rb +9 -6
  25. data/lib/simplecov/formatter.rb +2 -2
  26. data/lib/simplecov/formatter/multi_formatter.rb +5 -7
  27. data/lib/simplecov/formatter/simple_formatter.rb +4 -4
  28. data/lib/simplecov/last_run.rb +3 -1
  29. data/lib/simplecov/lines_classifier.rb +5 -5
  30. data/lib/simplecov/no_defaults.rb +1 -1
  31. data/lib/simplecov/process.rb +19 -0
  32. data/lib/simplecov/profiles.rb +9 -7
  33. data/lib/simplecov/result.rb +18 -12
  34. data/lib/simplecov/result_adapter.rb +30 -0
  35. data/lib/simplecov/result_merger.rb +130 -59
  36. data/lib/simplecov/simulate_coverage.rb +29 -0
  37. data/lib/simplecov/source_file.rb +272 -126
  38. data/lib/simplecov/source_file/branch.rb +84 -0
  39. data/lib/simplecov/source_file/line.rb +72 -0
  40. data/lib/simplecov/useless_results_remover.rb +18 -0
  41. data/lib/simplecov/version.rb +1 -1
  42. metadata +44 -158
  43. data/CONTRIBUTING.md +0 -51
  44. data/ISSUE_TEMPLATE.md +0 -23
  45. data/lib/simplecov/jruby_fix.rb +0 -44
  46. data/lib/simplecov/railtie.rb +0 -9
  47. data/lib/simplecov/railties/tasks.rake +0 -13
  48. data/lib/simplecov/raw_coverage.rb +0 -41
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ # Functionally for combining coverage results
5
+ #
6
+ module Combine
7
+ module_function
8
+
9
+ #
10
+ # Combine two coverage based on the given combiner_module.
11
+ #
12
+ # Combiners should always be called through this interface,
13
+ # as it takes care of short-circuiting of one of the coverages is nil.
14
+ #
15
+ # @return [Hash]
16
+ def combine(combiner_module, coverage_a, coverage_b)
17
+ return existing_coverage(coverage_a, coverage_b) if empty_coverage?(coverage_a, coverage_b)
18
+
19
+ combiner_module.combine(coverage_a, coverage_b)
20
+ end
21
+
22
+ def empty_coverage?(coverage_a, coverage_b)
23
+ !(coverage_a && coverage_b)
24
+ end
25
+
26
+ def existing_coverage(coverage_a, coverage_b)
27
+ coverage_a || coverage_b
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module Combine
5
+ #
6
+ # Combine different branch coverage results on single file.
7
+ #
8
+ # Should be called through `SimpleCov.combine`.
9
+ module BranchesCombiner
10
+ module_function
11
+
12
+ #
13
+ # Return merged branches or the existed brach if other is missing.
14
+ #
15
+ # Branches inside files are always same if they exist, the difference only in coverage count.
16
+ # Branch coverage report for any conditional case is built from hash, it's key is a condition and
17
+ # it's body is a hash << keys from condition and value is coverage rate >>.
18
+ # ex: branches =>{ [:if, 3, 8, 6, 8, 36] => {[:then, 4, 8, 6, 8, 12] => 1, [:else, 5, 8, 6, 8, 36]=>2}, other conditions...}
19
+ # We create copy of result and update it values depending on the combined branches coverage values.
20
+ #
21
+ # @return [Hash]
22
+ #
23
+ def combine(coverage_a, coverage_b)
24
+ coverage_a.merge(coverage_b) do |_condition, branches_inside_a, branches_inside_b|
25
+ branches_inside_a.merge(branches_inside_b) do |_branch, a_count, b_count|
26
+ a_count + b_count
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module Combine
5
+ #
6
+ # Handle combining two coverage results for same file
7
+ #
8
+ # Should be called through `SimpleCov.combine`.
9
+ module FilesCombiner
10
+ module_function
11
+
12
+ #
13
+ # Combines the results for 2 coverages of a file.
14
+ #
15
+ # @return [Hash]
16
+ #
17
+ def combine(coverage_a, coverage_b)
18
+ combination = {"lines" => Combine.combine(LinesCombiner, coverage_a["lines"], coverage_b["lines"])}
19
+ combination["branches"] = Combine.combine(BranchesCombiner, coverage_a["branches"], coverage_b["branches"]) if SimpleCov.branch_coverage?
20
+ combination
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module Combine
5
+ #
6
+ # Combine two different lines coverage results on same file
7
+ #
8
+ # Should be called through `SimpleCov.combine`.
9
+ module LinesCombiner
10
+ module_function
11
+
12
+ def combine(coverage_a, coverage_b)
13
+ coverage_a
14
+ .zip(coverage_b)
15
+ .map do |coverage_a_val, coverage_b_val|
16
+ merge_line_coverage(coverage_a_val, coverage_b_val)
17
+ end
18
+ end
19
+
20
+ # Return depends on coverage in a specific line
21
+ #
22
+ # @param [Integer || nil] first_val
23
+ # @param [Integer || nil] second_val
24
+ #
25
+ # Logic:
26
+ #
27
+ # => nil + 0 = nil
28
+ # => nil + nil = nil
29
+ # => int + int = int
30
+ #
31
+ # @return [Integer || nil]
32
+ def merge_line_coverage(first_val, second_val)
33
+ sum = first_val.to_i + second_val.to_i
34
+
35
+ if sum.zero? && (first_val.nil? || second_val.nil?)
36
+ nil
37
+ else
38
+ sum
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module Combine
5
+ # There might be reports from different kinds of tests,
6
+ # e.g. RSpec and Cucumber. We need to combine their results
7
+ # into unified one. This class does that.
8
+ # To unite the results on file basis, it leverages
9
+ # the combine of lines and branches inside each file within given results.
10
+ module ResultsCombiner
11
+ module_function
12
+
13
+ #
14
+ # Combine process explanation
15
+ # => ResultCombiner: define all present files between results and start combine on file level.
16
+ # ==> FileCombiner: collect result of next combine levels lines and branches.
17
+ # ===> LinesCombiner: combine lines results.
18
+ # ===> BranchesCombiner: combine branches results.
19
+ #
20
+ # @return [Hash]
21
+ #
22
+ def combine(*results)
23
+ results.reduce({}) do |combined_results, next_result|
24
+ combine_result_sets(combined_results, next_result)
25
+ end
26
+ end
27
+
28
+ #
29
+ # Manage combining results on files level
30
+ #
31
+ # @param [Hash] combined_results
32
+ # @param [Hash] result
33
+ #
34
+ # @return [Hash]
35
+ #
36
+ def combine_result_sets(combined_results, result)
37
+ results_files = combined_results.keys | result.keys
38
+
39
+ results_files.each_with_object({}) do |file_name, file_combination|
40
+ file_combination[file_name] = combine_file_coverage(
41
+ combined_results[file_name],
42
+ result[file_name]
43
+ )
44
+ end
45
+ end
46
+
47
+ #
48
+ # Combine two files coverage results
49
+ #
50
+ # @param [Hash] coverage_a
51
+ # @param [Hash] coverage_b
52
+ #
53
+ # @return [Hash]
54
+ #
55
+ def combine_file_coverage(coverage_a, coverage_b)
56
+ Combine.combine(Combine::FilesCombiner, coverage_a, coverage_b)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- #
4
- # Helper that tries to find out what test suite is running (for SimpleCov.command_name)
5
- #
6
3
  module SimpleCov
4
+ #
5
+ # Helper that tries to find out what test suite is running (for SimpleCov.command_name)
6
+ #
7
7
  module CommandGuesser
8
8
  class << self
9
9
  # Storage for the original command line call that invoked the test suite.
@@ -22,6 +22,7 @@ module SimpleCov
22
22
  def from_env
23
23
  # If being run from inside parallel_tests set the command name according to the process number
24
24
  return unless ENV["PARALLEL_TEST_GROUPS"] && ENV["TEST_ENV_NUMBER"]
25
+
25
26
  number = ENV["TEST_ENV_NUMBER"]
26
27
  number = "1" if number.empty?
27
28
  "(#{number}/#{ENV['PARALLEL_TEST_GROUPS']})"
@@ -48,6 +49,8 @@ module SimpleCov
48
49
  "RSpec"
49
50
  elsif defined?(Test::Unit)
50
51
  "Unit Tests"
52
+ elsif defined?(Minitest)
53
+ "Minitest"
51
54
  elsif defined?(MiniTest)
52
55
  "MiniTest"
53
56
  else
@@ -2,15 +2,16 @@
2
2
 
3
3
  require "fileutils"
4
4
  require "docile"
5
- require "simplecov/formatter/multi_formatter"
6
- #
7
- # Bundles the configuration options used for SimpleCov. All methods
8
- # defined here are usable from SimpleCov directly. Please check out
9
- # SimpleCov documentation for further info.
10
- #
5
+ require_relative "formatter/multi_formatter"
6
+
11
7
  module SimpleCov
12
- module Configuration # rubocop:disable ModuleLength
13
- attr_writer :filters, :groups, :formatter
8
+ #
9
+ # Bundles the configuration options used for SimpleCov. All methods
10
+ # defined here are usable from SimpleCov directly. Please check out
11
+ # SimpleCov documentation for further info.
12
+ #
13
+ module Configuration
14
+ attr_writer :filters, :groups, :formatter, :print_error_status
14
15
 
15
16
  #
16
17
  # The root for the project. This defaults to the
@@ -20,6 +21,8 @@ module SimpleCov
20
21
  #
21
22
  def root(root = nil)
22
23
  return @root if defined?(@root) && root.nil?
24
+
25
+ @coverage_path = nil # invalidate cache
23
26
  @root = File.expand_path(root || Dir.getwd)
24
27
  end
25
28
 
@@ -30,6 +33,7 @@ module SimpleCov
30
33
  #
31
34
  def coverage_dir(dir = nil)
32
35
  return @coverage_dir if defined?(@coverage_dir) && dir.nil?
36
+
33
37
  @coverage_path = nil # invalidate cache
34
38
  @coverage_dir = (dir || "coverage")
35
39
  end
@@ -93,8 +97,10 @@ module SimpleCov
93
97
  #
94
98
  def formatter(formatter = nil)
95
99
  return @formatter if defined?(@formatter) && formatter.nil?
100
+
96
101
  @formatter = formatter
97
102
  raise "No formatter configured. Please specify a formatter using SimpleCov.formatter = SimpleCov::Formatter::SimpleFormatter" unless @formatter
103
+
98
104
  @formatter
99
105
  end
100
106
 
@@ -116,6 +122,14 @@ module SimpleCov
116
122
  end
117
123
  end
118
124
 
125
+ #
126
+ # Whether we should print non-success status codes. This can be
127
+ # configured with the #print_error_status= method.
128
+ #
129
+ def print_error_status
130
+ defined?(@print_error_status) ? @print_error_status : true
131
+ end
132
+
119
133
  #
120
134
  # Certain code blocks (i.e. Ruby-implementation specific code) can be excluded from
121
135
  # the coverage metrics by wrapping it inside # :nocov: comment blocks. The nocov token
@@ -125,6 +139,7 @@ module SimpleCov
125
139
  #
126
140
  def nocov_token(nocov_token = nil)
127
141
  return @nocov_token if defined?(@nocov_token) && nocov_token.nil?
142
+
128
143
  @nocov_token = (nocov_token || "nocov")
129
144
  end
130
145
  alias skip_token nocov_token
@@ -160,7 +175,6 @@ module SimpleCov
160
175
  # options at once.
161
176
  #
162
177
  def configure(&block)
163
- return false unless SimpleCov.usable?
164
178
  Docile.dsl_eval(self, &block)
165
179
  end
166
180
 
@@ -177,17 +191,66 @@ module SimpleCov
177
191
  # end
178
192
  #
179
193
  def at_exit(&block)
180
- return proc {} unless running || block_given?
194
+ return Proc.new unless running || block_given?
195
+
181
196
  @at_exit = block if block_given?
182
197
  @at_exit ||= proc { SimpleCov.result.format! }
183
198
  end
184
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
+
185
247
  #
186
248
  # Returns the project name - currently assuming the last dirname in
187
249
  # the SimpleCov.root is this.
188
250
  #
189
251
  def project_name(new_name = nil)
190
252
  return @project_name if defined?(@project_name) && @project_name && new_name.nil?
253
+
191
254
  @project_name = new_name if new_name.is_a?(String)
192
255
  @project_name ||= File.basename(root.split("/").last).capitalize.tr("_", " ")
193
256
  end
@@ -225,7 +288,20 @@ module SimpleCov
225
288
  # Default is 0% (disabled)
226
289
  #
227
290
  def minimum_coverage(coverage = nil)
228
- @minimum_coverage ||= (coverage || 0).to_f.round(2)
291
+ return @minimum_coverage ||= {} unless coverage
292
+
293
+ coverage = {primary_coverage => coverage} if coverage.is_a?(Numeric)
294
+
295
+ raise_on_invalid_coverage(coverage, "minimum_coverage")
296
+
297
+ @minimum_coverage = coverage
298
+ end
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
229
305
  end
230
306
 
231
307
  #
@@ -235,7 +311,13 @@ module SimpleCov
235
311
  # Default is 100% (disabled)
236
312
  #
237
313
  def maximum_coverage_drop(coverage_drop = nil)
238
- @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
239
321
  end
240
322
 
241
323
  #
@@ -246,15 +328,23 @@ module SimpleCov
246
328
  # Default is 0% (disabled)
247
329
  #
248
330
  def minimum_coverage_by_file(coverage = nil)
249
- @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
250
338
  end
251
339
 
252
340
  #
253
341
  # Refuses any coverage drop. That is, coverage is only allowed to increase.
254
342
  # SimpleCov will return non-zero if the coverage decreases.
255
343
  #
256
- def refuse_coverage_drop
257
- 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)
258
348
  end
259
349
 
260
350
  #
@@ -287,8 +377,94 @@ module SimpleCov
287
377
  groups[group_name] = parse_filter(filter_argument, &filter_proc)
288
378
  end
289
379
 
380
+ SUPPORTED_COVERAGE_CRITERIA = %i[line branch].freeze
381
+ DEFAULT_COVERAGE_CRITERION = :line
382
+ #
383
+ # Define which coverage criterion should be evaluated.
384
+ #
385
+ # Possible coverage criteria:
386
+ # * :line - coverage based on lines aka has this line been executed?
387
+ # * :branch - coverage based on branches aka has this branch (think conditions) been executed?
388
+ #
389
+ # If not set the default is `:line`
390
+ #
391
+ # @param [Symbol] criterion
392
+ #
393
+ def coverage_criterion(criterion = nil)
394
+ return @coverage_criterion ||= primary_coverage unless criterion
395
+
396
+ raise_if_criterion_unsupported(criterion)
397
+
398
+ @coverage_criterion = criterion
399
+ end
400
+
401
+ def enable_coverage(criterion)
402
+ raise_if_criterion_unsupported(criterion)
403
+
404
+ coverage_criteria << criterion
405
+ end
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
+
416
+ def coverage_criteria
417
+ @coverage_criteria ||= Set[primary_coverage]
418
+ end
419
+
420
+ def coverage_criterion_enabled?(criterion)
421
+ coverage_criteria.member?(criterion)
422
+ end
423
+
424
+ def clear_coverage_criteria
425
+ @coverage_criteria = nil
426
+ end
427
+
428
+ def branch_coverage?
429
+ branch_coverage_supported? && coverage_criterion_enabled?(:branch)
430
+ end
431
+
432
+ def coverage_start_arguments_supported?
433
+ # safe to cache as within one process this value should never
434
+ # change
435
+ return @coverage_start_arguments_supported if defined?(@coverage_start_arguments_supported)
436
+
437
+ @coverage_start_arguments_supported = begin
438
+ require "coverage"
439
+ !Coverage.method(:start).arity.zero?
440
+ end
441
+ end
442
+
443
+ alias branch_coverage_supported? coverage_start_arguments_supported?
444
+
290
445
  private
291
446
 
447
+ def raise_if_criterion_disabled(criterion)
448
+ raise_if_criterion_unsupported(criterion)
449
+ # rubocop:disable Style/IfUnlessModifier
450
+ unless coverage_criterion_enabled?(criterion)
451
+ raise "Coverage criterion #{criterion}, is disabled! Please enable it first through enable_coverage #{criterion} (if supported)"
452
+ end
453
+ # rubocop:enable Style/IfUnlessModifier
454
+ end
455
+
456
+ def raise_if_criterion_unsupported(criterion)
457
+ # rubocop:disable Style/IfUnlessModifier
458
+ unless SUPPORTED_COVERAGE_CRITERIA.member?(criterion)
459
+ raise "Unsupported coverage criterion #{criterion}, supported values are #{SUPPORTED_COVERAGE_CRITERIA}"
460
+ end
461
+ # rubocop:enable Style/IfUnlessModifier
462
+ end
463
+
464
+ def minimum_possible_coverage_exceeded(coverage_option)
465
+ warn "The coverage you set for #{coverage_option} is greater than 100%"
466
+ end
467
+
292
468
  #
293
469
  # The actual filter processor. Not meant for direct use
294
470
  #