simplecov 0.22.0 → 1.0.2

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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +1058 -508
  4. data/doc/alternate-formatters.md +0 -5
  5. data/doc/commercial-services.md +5 -5
  6. data/exe/simplecov +11 -0
  7. data/lib/minitest/simplecov_plugin.rb +13 -5
  8. data/lib/simplecov/autostart.rb +11 -0
  9. data/lib/simplecov/cli/clean.rb +47 -0
  10. data/lib/simplecov/cli/coverage.rb +93 -0
  11. data/lib/simplecov/cli/diff.rb +152 -0
  12. data/lib/simplecov/cli/dotfile.rb +105 -0
  13. data/lib/simplecov/cli/merge.rb +118 -0
  14. data/lib/simplecov/cli/open.rb +50 -0
  15. data/lib/simplecov/cli/report.rb +89 -0
  16. data/lib/simplecov/cli/run.rb +36 -0
  17. data/lib/simplecov/cli/serve.rb +149 -0
  18. data/lib/simplecov/cli/uncovered.rb +108 -0
  19. data/lib/simplecov/cli.rb +150 -0
  20. data/lib/simplecov/color.rb +79 -0
  21. data/lib/simplecov/combine/branches_combiner.rb +28 -5
  22. data/lib/simplecov/combine/files_combiner.rb +47 -1
  23. data/lib/simplecov/combine/lines_combiner.rb +19 -17
  24. data/lib/simplecov/combine/methods_combiner.rb +49 -0
  25. data/lib/simplecov/combine/results_combiner.rb +7 -5
  26. data/lib/simplecov/command_guesser.rb +46 -32
  27. data/lib/simplecov/configuration/coverage.rb +175 -0
  28. data/lib/simplecov/configuration/coverage_criteria.rb +158 -0
  29. data/lib/simplecov/configuration/filters.rb +197 -0
  30. data/lib/simplecov/configuration/formatting.rb +129 -0
  31. data/lib/simplecov/configuration/ignored_entries.rb +63 -0
  32. data/lib/simplecov/configuration/merging.rb +164 -0
  33. data/lib/simplecov/configuration/thresholds.rb +178 -0
  34. data/lib/simplecov/configuration.rb +105 -412
  35. data/lib/simplecov/coverage_statistics.rb +15 -9
  36. data/lib/simplecov/coverage_violations.rb +149 -0
  37. data/lib/simplecov/defaults.rb +29 -22
  38. data/lib/simplecov/deprecation.rb +45 -0
  39. data/lib/simplecov/directive.rb +167 -0
  40. data/lib/simplecov/exit_codes/exit_code_handling.rb +8 -2
  41. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +19 -57
  42. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +45 -0
  43. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +17 -27
  44. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +41 -0
  45. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +38 -21
  46. data/lib/simplecov/exit_codes.rb +15 -0
  47. data/lib/simplecov/exit_handling.rb +158 -0
  48. data/lib/simplecov/file_list.rb +70 -19
  49. data/lib/simplecov/filter.rb +69 -24
  50. data/lib/simplecov/formatter/base.rb +102 -0
  51. data/lib/simplecov/formatter/html_formatter/public/application.css +1 -0
  52. data/lib/simplecov/formatter/html_formatter/public/application.js +18 -0
  53. data/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
  54. data/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
  55. data/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
  56. data/lib/simplecov/formatter/html_formatter/public/index.html +56 -0
  57. data/lib/simplecov/formatter/html_formatter.rb +85 -0
  58. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +90 -0
  59. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +128 -0
  60. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +99 -0
  61. data/lib/simplecov/formatter/json_formatter.rb +89 -0
  62. data/lib/simplecov/formatter/multi_formatter.rb +11 -7
  63. data/lib/simplecov/formatter/simple_formatter.rb +11 -11
  64. data/lib/simplecov/formatter.rb +4 -0
  65. data/lib/simplecov/last_run.rb +10 -3
  66. data/lib/simplecov/lines_classifier.rb +25 -13
  67. data/lib/simplecov/load_global_config.rb +9 -4
  68. data/lib/simplecov/parallel_adapters/base.rb +61 -0
  69. data/lib/simplecov/parallel_adapters/generic.rb +44 -0
  70. data/lib/simplecov/parallel_adapters/parallel_tests.rb +93 -0
  71. data/lib/simplecov/parallel_adapters.rb +83 -0
  72. data/lib/simplecov/parallel_coordination.rb +129 -0
  73. data/lib/simplecov/process.rb +52 -12
  74. data/lib/simplecov/profiles/bundler_filter.rb +1 -1
  75. data/lib/simplecov/profiles/hidden_filter.rb +1 -1
  76. data/lib/simplecov/profiles/rails.rb +24 -10
  77. data/lib/simplecov/profiles/root_filter.rb +6 -5
  78. data/lib/simplecov/profiles/strict.rb +32 -0
  79. data/lib/simplecov/profiles/test_frameworks.rb +1 -4
  80. data/lib/simplecov/profiles.rb +32 -3
  81. data/lib/simplecov/result/missing_source_files_reporter.rb +49 -0
  82. data/lib/simplecov/result/source_file_builder.rb +51 -0
  83. data/lib/simplecov/result.rb +108 -19
  84. data/lib/simplecov/result_adapter.rb +105 -8
  85. data/lib/simplecov/result_merger/legacy_format_adapter.rb +28 -0
  86. data/lib/simplecov/result_merger/resultset_file.rb +38 -0
  87. data/lib/simplecov/result_merger/resultset_store.rb +50 -0
  88. data/lib/simplecov/result_merger.rb +59 -91
  89. data/lib/simplecov/result_processing.rb +179 -0
  90. data/lib/simplecov/simulate_coverage.rb +54 -8
  91. data/lib/simplecov/source_file/branch.rb +1 -3
  92. data/lib/simplecov/source_file/branch_builder.rb +114 -0
  93. data/lib/simplecov/source_file/builder_context.rb +28 -0
  94. data/lib/simplecov/source_file/line.rb +9 -4
  95. data/lib/simplecov/source_file/line_builder.rb +43 -0
  96. data/lib/simplecov/source_file/method.rb +52 -0
  97. data/lib/simplecov/source_file/method_builder.rb +60 -0
  98. data/lib/simplecov/source_file/ruby_data_parser.rb +88 -0
  99. data/lib/simplecov/source_file/skip_chunks.rb +75 -0
  100. data/lib/simplecov/source_file/source_loader.rb +63 -0
  101. data/lib/simplecov/source_file/statistics.rb +57 -0
  102. data/lib/simplecov/source_file.rb +66 -232
  103. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +49 -0
  104. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +247 -0
  105. data/lib/simplecov/static_coverage_extractor/method_collector.rb +55 -0
  106. data/lib/simplecov/static_coverage_extractor/value_position.rb +82 -0
  107. data/lib/simplecov/static_coverage_extractor/visitor.rb +196 -0
  108. data/lib/simplecov/static_coverage_extractor.rb +110 -0
  109. data/lib/simplecov/useless_results_remover.rb +16 -7
  110. data/lib/simplecov/version.rb +1 -1
  111. data/lib/simplecov-html.rb +4 -0
  112. data/lib/simplecov.rb +178 -380
  113. data/lib/simplecov_json_formatter.rb +4 -0
  114. data/schemas/coverage-v1.0.schema.json +306 -0
  115. data/schemas/coverage.schema.json +306 -0
  116. data/sig/simplecov.rbs +1638 -0
  117. metadata +94 -57
  118. data/CHANGELOG.md +0 -191
  119. data/lib/simplecov/default_formatter.rb +0 -20
@@ -9,16 +9,62 @@ module SimpleCov
9
9
  module FilesCombiner
10
10
  module_function
11
11
 
12
+ empty_table = {} #: Hash[untyped, untyped]
13
+ EMPTY_TABLE = empty_table.freeze
14
+ private_constant :EMPTY_TABLE
15
+
16
+ # Branch/method tuples drawn from a simulated (never-loaded) file
17
+ # when the other side of the merge was actually executed.
18
+ NO_SYNTHESIZED = {"branches" => EMPTY_TABLE, "methods" => EMPTY_TABLE}.freeze
19
+
12
20
  #
13
21
  # Combines the results for 2 coverages of a file.
14
22
  #
15
23
  # @return [Hash]
16
24
  #
17
25
  def combine(coverage_a, coverage_b)
26
+ source_a, source_b = reconcile_synthesized(coverage_a, coverage_b)
27
+
18
28
  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?
29
+ if SimpleCov.branch_coverage?
30
+ combined_branches = Combine.combine(BranchesCombiner, source_a["branches"], source_b["branches"])
31
+ combination["branches"] = combined_branches || {}
32
+ end
33
+ if SimpleCov.method_coverage?
34
+ combination["methods"] = Combine.combine(MethodsCombiner, source_a["methods"], source_b["methods"])
35
+ end
20
36
  combination
21
37
  end
38
+
39
+ # When exactly one side of the merge was actually executed, its branch
40
+ # and method tuples are authoritative and the other side's are dropped.
41
+ # A simulated entry (SimulateCoverage backfills tracked-but-unloaded
42
+ # files) synthesizes those tuples statically, so a location that drifts
43
+ # from what Coverage emits would otherwise be unioned in by position
44
+ # and survive as a phantom, permanently-missed branch (see #1233). This
45
+ # contains any such drift to denominator inflation for files no process
46
+ # loaded, rather than a false miss on a covered file. Lines are never
47
+ # dropped: a simulated file's line shape is correct and carries the
48
+ # unloaded-file denominator (#1059).
49
+ #
50
+ # Returns the two coverages to draw branch/method tuples from, blanking
51
+ # the non-executed side only when the other side was executed. When
52
+ # both sides were executed (two real runs) or neither was (all
53
+ # simulated), both are returned unchanged and combine normally.
54
+ def reconcile_synthesized(coverage_a, coverage_b)
55
+ executed_a = executed?(coverage_a)
56
+ executed_b = executed?(coverage_b)
57
+ return [coverage_a, coverage_b] if executed_a == executed_b
58
+
59
+ executed_a ? [coverage_a, NO_SYNTHESIZED] : [NO_SYNTHESIZED, coverage_b]
60
+ end
61
+
62
+ # A file some process actually loaded has at least one executed line;
63
+ # a simulated (never-loaded) file's lines are all `nil` or `0`.
64
+ def executed?(coverage)
65
+ lines = Array(coverage["lines"]) #: Array[Integer?]
66
+ lines.any? { |count| count&.positive? }
67
+ end
22
68
  end
23
69
  end
24
70
  end
@@ -9,34 +9,36 @@ module SimpleCov
9
9
  module LinesCombiner
10
10
  module_function
11
11
 
12
+ # Build a fresh array sized to the longer input. The previous
13
+ # implementation mutated whichever input was longer in place,
14
+ # which could surprise callers holding a reference to that array
15
+ # (e.g. the parsed `coverage` key of a resultset hash being
16
+ # passed into a second merge).
12
17
  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
+ size = [coverage_a.size, coverage_b.size].max
19
+ Array.new(size) { |i| merge_line_coverage(coverage_a[i], coverage_b[i]) }
18
20
  end
19
21
 
20
- # Return depends on coverage in a specific line
21
- #
22
- # @param [Integer || nil] first_val
23
- # @param [Integer || nil] second_val
22
+ # Two runs of the same source file should agree on which lines
23
+ # are coverage-relevant (`nil` for comments / whitespace, `0`+
24
+ # for executable). When they don't, treat "relevant on either
25
+ # side" as relevant rather than masking a real `0` as `nil`,
26
+ # which would silently drop an uncovered line from the
27
+ # denominator and inflate the percentage.
24
28
  #
25
29
  # Logic:
26
30
  #
27
- # => nil + 0 = nil
28
31
  # => nil + nil = nil
29
- # => int + int = int
32
+ # => nil + int = int (preserves a relevant-but-uncovered 0)
33
+ # => int + int = int (sum)
30
34
  #
35
+ # @param [Integer || nil] first_val
36
+ # @param [Integer || nil] second_val
31
37
  # @return [Integer || nil]
32
38
  def merge_line_coverage(first_val, second_val)
33
- sum = first_val.to_i + second_val.to_i
39
+ return nil if first_val.nil? && second_val.nil?
34
40
 
35
- if sum.zero? && (first_val.nil? || second_val.nil?)
36
- nil
37
- else
38
- sum
39
- end
41
+ first_val.to_i + second_val.to_i
40
42
  end
41
43
  end
42
44
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../source_file/ruby_data_parser"
4
+
5
+ module SimpleCov
6
+ module Combine
7
+ #
8
+ # Combine different method coverage results on a single file.
9
+ #
10
+ # Should be called through `SimpleCov.combine`.
11
+ module MethodsCombiner
12
+ module_function
13
+
14
+ #
15
+ # Return merged methods or the existing methods if other is missing.
16
+ #
17
+ # Method coverage maps `[class, name, start_line, start_col, end_line,
18
+ # end_col]` keys to hit counts. Keys are matched on their SOURCE
19
+ # identity — (name, location), ignoring the class element — because
20
+ # Ruby records one entry per receiver: the same `define_method` block
21
+ # defined onto different classes in different processes arrives with
22
+ # different (normalized) receivers for the same source method, and
23
+ # matching on the full key would keep both, letting a never-called
24
+ # receiver's 0 shadow a covered method after merge (issue #1234).
25
+ # Combining sums the hit counts for matching methods and preserves
26
+ # methods that only appear in one result.
27
+ #
28
+ # @return [Hash]
29
+ #
30
+ def combine(coverage_a, coverage_b)
31
+ merged = {} #: Hash[untyped, [untyped, Integer]]
32
+ [coverage_a, coverage_b].each_with_object(merged) do |coverage, memo|
33
+ coverage.each do |key, count|
34
+ method_key = source_identity(key)
35
+ retained_key, existing = memo[method_key] || [key, 0]
36
+ memo[method_key] = [retained_key, existing + count]
37
+ end
38
+ end
39
+
40
+ merged.values.to_h
41
+ end
42
+
43
+ def source_identity(key)
44
+ _class_name, *identity = SourceFile::RubyDataParser.call(key)
45
+ identity
46
+ end
47
+ end
48
+ end
49
+ end
@@ -13,14 +13,16 @@ module SimpleCov
13
13
  #
14
14
  # Combine process explanation
15
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.
16
+ # ==> FileCombiner: collect result of next combine levels lines, branches, and methods.
17
17
  # ===> LinesCombiner: combine lines results.
18
18
  # ===> BranchesCombiner: combine branches results.
19
+ # ===> MethodsCombiner: combine methods results.
19
20
  #
20
21
  # @return [Hash]
21
22
  #
22
23
  def combine(*results)
23
- results.reduce({}) do |combined_results, next_result|
24
+ initial = {} #: Hash[untyped, untyped]
25
+ results.reduce(initial) do |combined_results, next_result|
24
26
  combine_result_sets(combined_results, next_result)
25
27
  end
26
28
  end
@@ -36,11 +38,11 @@ module SimpleCov
36
38
  def combine_result_sets(combined_results, result)
37
39
  results_files = combined_results.keys | result.keys
38
40
 
39
- results_files.each_with_object({}) do |file_name, file_combination|
40
- file_combination[file_name] = combine_file_coverage(
41
+ results_files.to_h do |file_name|
42
+ [file_name, combine_file_coverage(
41
43
  combined_results[file_name],
42
44
  result[file_name]
43
- )
45
+ )]
44
46
  end
45
47
  end
46
48
 
@@ -14,50 +14,64 @@ module SimpleCov
14
14
  attr_accessor :original_run_command
15
15
 
16
16
  def guess
17
- from_env || from_command_line_options || from_defined_constants
17
+ [from_command_line_options || from_defined_constants, parallel_data].compact.join(" ")
18
18
  end
19
19
 
20
20
  private
21
21
 
22
- def from_env
23
- # If being run from inside parallel_tests set the command name according to the process number
24
- return unless ENV["PARALLEL_TEST_GROUPS"] && ENV["TEST_ENV_NUMBER"]
22
+ # When parallel_tests (or a compatible runner) is driving the suite,
23
+ # tag the command name with this worker's position in the pool.
24
+ def parallel_data
25
+ groups, number = ENV.values_at("PARALLEL_TEST_GROUPS", "TEST_ENV_NUMBER")
26
+ return unless groups && number
25
27
 
26
- number = ENV["TEST_ENV_NUMBER"]
28
+ # parallel_tests sets the first worker's TEST_ENV_NUMBER to "" rather
29
+ # than "1"; restore the position so the rendered label reads cleanly.
27
30
  number = "1" if number.empty?
28
- "(#{number}/#{ENV['PARALLEL_TEST_GROUPS']})"
31
+ "(#{number}/#{groups})"
29
32
  end
30
33
 
34
+ COMMAND_LINE_FRAMEWORKS = {
35
+ %r{test/functional/} => "Functional Tests",
36
+ %r{test/\{.*functional.*\}/} => "Functional Tests",
37
+ %r{test/integration/} => "Integration Tests",
38
+ %r{test/} => "Unit Tests",
39
+ /spec/ => "RSpec",
40
+ /cucumber/ => "Cucumber Features",
41
+ /features/ => "Cucumber Features"
42
+ }.freeze
43
+ private_constant :COMMAND_LINE_FRAMEWORKS
44
+
31
45
  def from_command_line_options
32
- case original_run_command
33
- when /test\/functional\//, /test\/\{.*functional.*\}\//
34
- "Functional Tests"
35
- when /test\/integration\//
36
- "Integration Tests"
37
- when /test\//
38
- "Unit Tests"
39
- when /spec/
40
- "RSpec"
41
- when /cucumber/, /features/
42
- "Cucumber Features"
43
- end
46
+ COMMAND_LINE_FRAMEWORKS.find { |pattern, _| pattern.match?(original_run_command.to_s) }&.last
44
47
  end
45
48
 
49
+ # Inner array literals after the first are flagged uncovered by Ruby's
50
+ # Coverage module even though the constant evaluates as a whole — known
51
+ # quirk with multi-line array literals.
52
+ DEFINED_CONSTANT_FRAMEWORKS = [
53
+ ["RSpec", -> { defined?(::RSpec) }],
54
+ ["Unit Tests", -> { defined?(Test::Unit) }], # simplecov:disable
55
+ ["Minitest", -> { defined?(::Minitest) }], # simplecov:disable
56
+ ["MiniTest", -> { defined?(MiniTest) }] # simplecov:disable
57
+ ].freeze
58
+ private_constant :DEFINED_CONSTANT_FRAMEWORKS
59
+
60
+ # If the command regexps fail, let's try checking defined constants.
46
61
  def from_defined_constants
47
- # If the command regexps fail, let's try checking defined constants.
48
- if defined?(RSpec)
49
- "RSpec"
50
- elsif defined?(Test::Unit)
51
- "Unit Tests"
52
- elsif defined?(Minitest)
53
- "Minitest"
54
- elsif defined?(MiniTest)
55
- "MiniTest"
56
- else
57
- # TODO: Provide link to docs/wiki article
58
- warn "SimpleCov failed to recognize the test framework and/or suite used. Please specify manually using SimpleCov.command_name 'Unit Tests'."
59
- "Unknown Test Framework"
60
- end
62
+ # simplecov:disable branch first iter returns when ::RSpec is defined; later branches unreachable
63
+ DEFINED_CONSTANT_FRAMEWORKS.each { |name, defined_check| return name if defined_check.call }
64
+ # simplecov:enable branch
65
+
66
+ # TODO: Provide link to docs/wiki article
67
+ # simplecov:disable — only fires when no framework is detected, which
68
+ # is impossible while our own specs are running under rspec
69
+ warn(
70
+ "SimpleCov failed to recognize the test framework and/or suite used. " \
71
+ "Please specify manually using SimpleCov.command_name 'Unit Tests'."
72
+ )
73
+ "Unknown Test Framework"
74
+ # simplecov:enable
61
75
  end
62
76
  end
63
77
  end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ # The `coverage` configuration method configures each coverage criterion
5
+ # (`:line`, `:branch`, `:method`, `:eval`) uniformly in one place: naming a
6
+ # criterion enables it,
7
+ # and every threshold is declared with the same syntax regardless of which
8
+ # criterion it applies to. Because the criterion is fixed by the enclosing
9
+ # `coverage` call, threshold values are always plain percentages — there is
10
+ # no per-criterion Hash competing with the value for a slot.
11
+ #
12
+ # SimpleCov.start do
13
+ # coverage :line do
14
+ # minimum 90
15
+ # minimum_per_file 80
16
+ # minimum_per_file 100, only: "app/mailers/request_mailer.rb"
17
+ # maximum_drop 5
18
+ # end
19
+ #
20
+ # coverage :branch, minimum: 80
21
+ # coverage :method, minimum: 100
22
+ # end
23
+ #
24
+ # Line coverage is enabled by default, so `coverage :line` is only needed to
25
+ # set line thresholds or options. Thresholds feed the same internal stores
26
+ # as the flat `minimum_coverage` family, so enforcement is unchanged.
27
+ module Configuration
28
+ # One-liner keyword options `coverage` accepts, each forwarding to the
29
+ # `CoverageCriterion` verb of the same name. `minimum_per_group` is omitted
30
+ # because it needs an `only:` target, so it's block-only.
31
+ COVERAGE_THRESHOLD_OPTIONS = %i[minimum maximum exact maximum_drop minimum_per_file].freeze
32
+
33
+ #
34
+ # Configure (and, unless `enabled: false`, enable) a coverage criterion.
35
+ #
36
+ # Threshold options mirror the block verbs for one-liner use:
37
+ # coverage :branch, minimum: 80, maximum_drop: 5
38
+ #
39
+ # `primary: true` makes this the report's leading criterion (and the one a
40
+ # bare `minimum_coverage 90` targets). `oneshot: true` (valid only for
41
+ # `:line`) selects the faster oneshot-lines mode. `:eval` is enable-only.
42
+ #
43
+ def coverage(criterion, primary: false, enabled: true, oneshot: false, **thresholds, &block)
44
+ criterion = enable_coverage_criterion(criterion, enabled: enabled, oneshot: oneshot)
45
+ # The cast admits :eval, which primary_coverage rejects at runtime
46
+ # (it is enable-only and never in the enabled-criteria set).
47
+ primary_coverage(_ = criterion) if primary
48
+
49
+ configurator = CoverageCriterion.new(self, criterion)
50
+ apply_threshold_options(configurator, thresholds)
51
+ configurator.instance_eval(&block) if block
52
+
53
+ criterion
54
+ end
55
+
56
+ private
57
+
58
+ # Forward the one-liner threshold keywords (`coverage :branch, minimum: 80`)
59
+ # to the matching `CoverageCriterion` verbs, rejecting anything that isn't a
60
+ # recognized threshold option.
61
+ def apply_threshold_options(configurator, options)
62
+ options.each do |verb, value|
63
+ unless COVERAGE_THRESHOLD_OPTIONS.include?(verb)
64
+ raise SimpleCov::ConfigurationError,
65
+ "Unknown `coverage` option #{verb.inspect}. " \
66
+ "Supported options are #{COVERAGE_THRESHOLD_OPTIONS.inspect}."
67
+ end
68
+
69
+ configurator.public_send(verb, value)
70
+ end
71
+ end
72
+
73
+ # Enable the criterion (or its oneshot / eval variant) and return the
74
+ # criterion symbol that thresholds should be stored under.
75
+ def enable_coverage_criterion(criterion, enabled:, oneshot:)
76
+ return enable_oneshot_line(criterion) if oneshot
77
+ return enable_eval_coverage_criterion if criterion == :eval
78
+
79
+ enabled ? enable_coverage(criterion) : disable_coverage(criterion)
80
+ criterion
81
+ end
82
+
83
+ def enable_oneshot_line(criterion)
84
+ unless criterion == :line
85
+ raise SimpleCov::ConfigurationError, "`oneshot: true` is only valid for `coverage :line`"
86
+ end
87
+
88
+ enable_coverage(ONESHOT_LINE_COVERAGE_CRITERION)
89
+ ONESHOT_LINE_COVERAGE_CRITERION
90
+ end
91
+
92
+ def enable_eval_coverage_criterion
93
+ enable_coverage(:eval)
94
+ :eval
95
+ end
96
+
97
+ # @api private — threshold-store writers used by CoverageCriterion. They
98
+ # write the same `@minimum_coverage` / `@maximum_coverage` / ... hashes the
99
+ # flat threshold methods populate, so the exit-code checks are unchanged.
100
+ def store_overall_threshold(setting, criterion, percent)
101
+ raise_on_invalid_coverage({criterion => percent}, setting.to_s)
102
+ public_send(setting)[criterion] = percent
103
+ end
104
+
105
+ def store_minimum_per_file(criterion, percent, target)
106
+ raise_on_invalid_coverage({criterion => percent}, "minimum_coverage_by_file")
107
+ return minimum_coverage_by_file[criterion] = percent if target.nil?
108
+
109
+ unless target.is_a?(String) || target.is_a?(Regexp)
110
+ raise SimpleCov::ConfigurationError, "`only:` must be a String path or Regexp, got #{target.inspect}"
111
+ end
112
+
113
+ (minimum_coverage_by_file_overrides[target] ||= {})[criterion] = percent
114
+ end
115
+
116
+ def store_minimum_per_group(criterion, percent, group_name)
117
+ raise_on_invalid_coverage({criterion => percent}, "minimum_coverage_by_group")
118
+ (minimum_coverage_by_group[group_name] ||= {})[criterion] = percent
119
+ end
120
+
121
+ #
122
+ # Receiver for a `coverage <criterion> do ... end` block. Each verb writes a
123
+ # threshold for the single criterion the block configures, so the value is
124
+ # always a plain percentage (`minimum_per_file 100` is unambiguous) and the
125
+ # syntax is identical across line, branch, and method coverage.
126
+ #
127
+ class CoverageCriterion
128
+ def initialize(config, criterion)
129
+ @config = config
130
+ @criterion = criterion
131
+ end
132
+
133
+ # Overall (suite-wide) minimum for this criterion.
134
+ def minimum(percent)
135
+ @config.send(:store_overall_threshold, :minimum_coverage, @criterion, percent)
136
+ end
137
+
138
+ # Overall maximum: fails the build if coverage rises above it. Paired with
139
+ # `minimum` (or via `exact`) this pins coverage so an unexpected jump fails.
140
+ def maximum(percent)
141
+ @config.send(:store_overall_threshold, :maximum_coverage, @criterion, percent)
142
+ end
143
+
144
+ # Pin coverage to an exact figure (sets both `minimum` and `maximum`).
145
+ def exact(percent)
146
+ minimum(percent)
147
+ maximum(percent)
148
+ end
149
+
150
+ # Maximum allowed drop between runs (`maximum_drop 0` refuses any drop).
151
+ def maximum_drop(percent)
152
+ @config.send(:store_overall_threshold, :maximum_coverage_drop, @criterion, percent)
153
+ end
154
+
155
+ # Per-file minimum. With no `only:`, sets the default applied to every
156
+ # file; with `only:` (a String path or Regexp), overrides that default
157
+ # for the matching files.
158
+ def minimum_per_file(percent, only: nil)
159
+ @config.send(:store_minimum_per_file, @criterion, percent, only)
160
+ end
161
+
162
+ # Per-group minimum for the named group (defined via `group`).
163
+ def minimum_per_group(percent, only:)
164
+ @config.send(:store_minimum_per_group, @criterion, percent, only)
165
+ end
166
+
167
+ # Make this criterion the report's primary (leading) criterion.
168
+ def primary
169
+ # @criterion is Symbol-wide because this receiver is also built for
170
+ # :eval; primary_coverage validates at runtime.
171
+ @config.primary_coverage(_ = @criterion)
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ # Selection and validation of the coverage criteria Ruby's `Coverage`
5
+ # library should track. Supports `:line` (the historical default),
6
+ # `:branch`, `:method`, and `:oneshot_line`, plus the standalone
7
+ # `:eval` toggle for instrumenting `eval`'d code.
8
+ module Configuration
9
+ SUPPORTED_COVERAGE_CRITERIA = %i[line branch method oneshot_line].freeze
10
+ DEFAULT_COVERAGE_CRITERION = :line
11
+ ONESHOT_LINE_COVERAGE_CRITERION = :oneshot_line
12
+
13
+ # Enable one or more coverage criteria. `:eval` is accepted as a
14
+ # shorthand for the standalone eval-coverage toggle.
15
+ def enable_coverage(*criteria)
16
+ criteria.each do |criterion|
17
+ if criterion == :eval
18
+ enable_eval_coverage
19
+ else
20
+ raise_if_criterion_unsupported(criterion)
21
+ # :oneshot_lines can not be combined with :lines
22
+ coverage_criteria.delete(DEFAULT_COVERAGE_CRITERION) if criterion == ONESHOT_LINE_COVERAGE_CRITERION
23
+ coverage_criteria << criterion
24
+ end
25
+ end
26
+ end
27
+
28
+ # Remove `criterion` from the set of enabled coverage criteria.
29
+ # Disabling every criterion raises at `start_tracking` (not here),
30
+ # so config files that toggle criteria in arbitrary order don't
31
+ # have to worry about transient empty states.
32
+ def disable_coverage(criterion)
33
+ raise_if_criterion_unsupported(criterion)
34
+ coverage_criteria.delete(criterion)
35
+ @primary_coverage = nil if @primary_coverage == criterion
36
+ end
37
+
38
+ def primary_coverage(criterion = nil)
39
+ if criterion.nil?
40
+ @primary_coverage ||= default_primary_coverage
41
+ else
42
+ raise_if_criterion_disabled(criterion)
43
+ @primary_coverage = criterion
44
+ end
45
+ end
46
+
47
+ def coverage_criteria
48
+ @coverage_criteria ||= Set[DEFAULT_COVERAGE_CRITERION]
49
+ end
50
+
51
+ def coverage_criterion_enabled?(criterion)
52
+ coverage_criteria.member?(criterion)
53
+ end
54
+
55
+ # Reset the criteria back to the lazy default (`Set[:line]`).
56
+ def clear_coverage_criteria
57
+ @coverage_criteria = nil
58
+ @primary_coverage = nil
59
+ end
60
+
61
+ # @api private — called from `SimpleCov.start_tracking` to fail
62
+ # fast when the user has disabled every coverage criterion.
63
+ def validate_coverage_criteria!
64
+ return unless coverage_criteria.empty?
65
+
66
+ raise SimpleCov::ConfigurationError,
67
+ "At least one coverage criterion must be enabled. " \
68
+ "Re-enable one with `enable_coverage :line`, `:branch`, or `:method`."
69
+ end
70
+
71
+ def branch_coverage?
72
+ branch_coverage_supported? && coverage_criterion_enabled?(:branch)
73
+ end
74
+
75
+ def branch_coverage_supported?
76
+ coverage_criterion_supported?(:branches)
77
+ end
78
+
79
+ def method_coverage?
80
+ method_coverage_supported? && coverage_criterion_enabled?(:method)
81
+ end
82
+
83
+ def method_coverage_supported?
84
+ coverage_criterion_supported?(:methods)
85
+ end
86
+
87
+ def coverage_for_eval_supported?
88
+ coverage_criterion_supported?(:eval)
89
+ end
90
+
91
+ # Ask the Coverage runtime itself whether a criterion is supported
92
+ # (Ruby >= 3.2). Older Rubies don't expose `Coverage.supported?`, so
93
+ # fall back to the historical engine check that line/branch/method
94
+ # were unavailable on JRuby. `:eval` was added later, so on older
95
+ # Rubies its fallback is "always unsupported" rather than the
96
+ # JRuby-only one above. The fallback arm is unreachable from the
97
+ # dogfood report, which runs on a newer Ruby.
98
+ # simplecov:disable
99
+ def coverage_criterion_supported?(criterion)
100
+ require "coverage"
101
+ return Coverage.supported?(criterion) if Coverage.respond_to?(:supported?)
102
+
103
+ criterion != :eval && RUBY_ENGINE != "jruby"
104
+ end
105
+ # simplecov:enable
106
+
107
+ def coverage_for_eval_enabled?
108
+ @coverage_for_eval_enabled ||= false
109
+ end
110
+
111
+ # DEPRECATED: prefer `enable_coverage :eval`.
112
+ def enable_coverage_for_eval
113
+ SimpleCov::Deprecation.warn("`SimpleCov.enable_coverage_for_eval` is deprecated. " \
114
+ "Replace with `SimpleCov.enable_coverage :eval`.")
115
+ enable_eval_coverage
116
+ end
117
+
118
+ private
119
+
120
+ # Shared implementation backing both `enable_coverage :eval` and
121
+ # the deprecated `enable_coverage_for_eval`.
122
+ def enable_eval_coverage
123
+ if coverage_for_eval_supported?
124
+ @coverage_for_eval_enabled = true
125
+ else
126
+ warn "Coverage for eval is not available; Use Ruby 3.2.0 or later"
127
+ end
128
+ end
129
+
130
+ # If `:line` is enabled, it's the default primary; otherwise fall
131
+ # back to whichever criterion the user actually enabled (in
132
+ # insertion order). Returning `:line` even when disabled would
133
+ # propagate broken state into `minimum_coverage 90`.
134
+ def default_primary_coverage
135
+ return DEFAULT_COVERAGE_CRITERION if coverage_criterion_enabled?(DEFAULT_COVERAGE_CRITERION)
136
+
137
+ # Set#first types as nilable, but an empty criteria set is rejected at
138
+ # start_tracking before any caller can observe a nil here.
139
+ _ = coverage_criteria.first
140
+ end
141
+
142
+ def raise_if_criterion_disabled(criterion)
143
+ raise_if_criterion_unsupported(criterion)
144
+ return if coverage_criterion_enabled?(criterion)
145
+
146
+ raise SimpleCov::ConfigurationError,
147
+ "Coverage criterion #{criterion}, is disabled! " \
148
+ "Please enable it first through enable_coverage #{criterion} (if supported)"
149
+ end
150
+
151
+ def raise_if_criterion_unsupported(criterion)
152
+ return if SUPPORTED_COVERAGE_CRITERIA.member?(criterion)
153
+
154
+ raise SimpleCov::ConfigurationError,
155
+ "Unsupported coverage criterion #{criterion}, supported values are #{SUPPORTED_COVERAGE_CRITERIA}"
156
+ end
157
+ end
158
+ end