simplecov 1.0.2 → 1.1.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.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +65 -1425
  3. data/lib/simplecov/atomic_file.rb +70 -0
  4. data/lib/simplecov/cli/clean.rb +43 -3
  5. data/lib/simplecov/cli/command_helpers.rb +55 -0
  6. data/lib/simplecov/cli/coverage.rb +21 -28
  7. data/lib/simplecov/cli/coverage_file.rb +65 -0
  8. data/lib/simplecov/cli/diff.rb +43 -48
  9. data/lib/simplecov/cli/dotfile.rb +12 -6
  10. data/lib/simplecov/cli/merge.rb +12 -10
  11. data/lib/simplecov/cli/open.rb +3 -5
  12. data/lib/simplecov/cli/report.rb +31 -23
  13. data/lib/simplecov/cli/serve/report_preparer.rb +29 -0
  14. data/lib/simplecov/cli/serve/static_file_handler.rb +122 -0
  15. data/lib/simplecov/cli/serve.rb +30 -94
  16. data/lib/simplecov/cli/uncovered.rb +20 -25
  17. data/lib/simplecov/cli.rb +15 -2
  18. data/lib/simplecov/combine/branches_combiner.rb +43 -19
  19. data/lib/simplecov/combine/coverage_accumulator.rb +268 -0
  20. data/lib/simplecov/combine/identity_interner.rb +30 -0
  21. data/lib/simplecov/combine/interned_counts.rb +30 -0
  22. data/lib/simplecov/combine/lines_combiner.rb +48 -20
  23. data/lib/simplecov/combine/methods_combiner.rb +46 -21
  24. data/lib/simplecov/combine/results_combiner.rb +12 -37
  25. data/lib/simplecov/combine.rb +5 -23
  26. data/lib/simplecov/command_guesser.rb +66 -9
  27. data/lib/simplecov/configuration/coverage.rb +12 -15
  28. data/lib/simplecov/configuration/coverage_criteria.rb +34 -38
  29. data/lib/simplecov/configuration/eval_coverage.rb +41 -0
  30. data/lib/simplecov/configuration/filters.rb +11 -44
  31. data/lib/simplecov/configuration/formatting.rb +18 -9
  32. data/lib/simplecov/configuration/groups.rb +42 -0
  33. data/lib/simplecov/configuration/merging.rb +8 -7
  34. data/lib/simplecov/configuration/thresholds.rb +14 -13
  35. data/lib/simplecov/configuration.rb +13 -43
  36. data/lib/simplecov/coverage_json.rb +24 -0
  37. data/lib/simplecov/coverage_statistics.rb +1 -1
  38. data/lib/simplecov/coverage_violations.rb +15 -5
  39. data/lib/simplecov/defaults.rb +7 -3
  40. data/lib/simplecov/directive.rb +1 -1
  41. data/lib/simplecov/exit_codes/check.rb +33 -0
  42. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +11 -20
  43. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +3 -16
  44. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +14 -23
  45. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +14 -25
  46. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +4 -17
  47. data/lib/simplecov/exit_codes.rb +1 -0
  48. data/lib/simplecov/exit_handling.rb +11 -41
  49. data/lib/simplecov/file_list.rb +5 -10
  50. data/lib/simplecov/filter.rb +43 -9
  51. data/lib/simplecov/formatter/base.rb +11 -0
  52. data/lib/simplecov/formatter/coverage_json_writer.rb +97 -0
  53. data/lib/simplecov/formatter/html_formatter/public/index.html +35 -5
  54. data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +96 -0
  55. data/lib/simplecov/formatter/html_formatter.rb +67 -47
  56. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +54 -56
  57. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +78 -88
  58. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +70 -76
  59. data/lib/simplecov/formatter/json_formatter.rb +5 -48
  60. data/lib/simplecov/formatter/multi_formatter.rb +1 -1
  61. data/lib/simplecov/formatter/simple_formatter.rb +8 -5
  62. data/lib/simplecov/formatter.rb +12 -0
  63. data/lib/simplecov/group_names.rb +32 -0
  64. data/lib/simplecov/last_run.rb +16 -9
  65. data/lib/simplecov/lines_classifier.rb +29 -8
  66. data/lib/simplecov/load_global_config.rb +5 -2
  67. data/lib/simplecov/parallel_adapters/base.rb +17 -0
  68. data/lib/simplecov/parallel_adapters/generic.rb +2 -2
  69. data/lib/simplecov/parallel_adapters/parallel_tests.rb +2 -2
  70. data/lib/simplecov/parallel_coordination.rb +6 -1
  71. data/lib/simplecov/parallel_result_merger.rb +230 -0
  72. data/lib/simplecov/report_deferral.rb +49 -0
  73. data/lib/simplecov/report_stamp.rb +28 -0
  74. data/lib/simplecov/result.rb +40 -10
  75. data/lib/simplecov/result_adapter.rb +48 -16
  76. data/lib/simplecov/result_merger/resultset_file.rb +43 -7
  77. data/lib/simplecov/result_merger/resultset_run_identity.rb +67 -0
  78. data/lib/simplecov/result_merger/resultset_store.rb +15 -12
  79. data/lib/simplecov/result_merger/unloaded_files.rb +103 -0
  80. data/lib/simplecov/result_merger.rb +69 -42
  81. data/lib/simplecov/result_processing.rb +82 -43
  82. data/lib/simplecov/run_identity.rb +77 -0
  83. data/lib/simplecov/simulate_coverage.rb +35 -11
  84. data/lib/simplecov/source_file/method.rb +7 -1
  85. data/lib/simplecov/source_file/ruby_data_parser.rb +25 -3
  86. data/lib/simplecov/source_file/skip_chunks.rb +7 -10
  87. data/lib/simplecov/source_file/source_loader.rb +23 -7
  88. data/lib/simplecov/source_file/statistics.rb +24 -16
  89. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +203 -13
  90. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +19 -30
  91. data/lib/simplecov/static_coverage_extractor/method_collector.rb +7 -0
  92. data/lib/simplecov/static_coverage_extractor/prism_compat.rb +55 -0
  93. data/lib/simplecov/static_coverage_extractor/value_position.rb +6 -14
  94. data/lib/simplecov/static_coverage_extractor/visitor.rb +23 -36
  95. data/lib/simplecov/unloaded_file_injector.rb +75 -0
  96. data/lib/simplecov/version.rb +1 -1
  97. data/lib/simplecov.rb +11 -5
  98. data/schemas/coverage-v1.0.schema.json +2 -2
  99. data/schemas/coverage.schema.json +2 -2
  100. data/sig/simplecov.rbs +206 -67
  101. metadata +28 -14
  102. data/doc/alternate-formatters.md +0 -66
  103. data/doc/commercial-services.md +0 -25
  104. data/doc/editor-integration.md +0 -18
  105. data/lib/simplecov/combine/files_combiner.rb +0 -70
  106. data/lib/simplecov/formatter/html_formatter/public/application.css +0 -1
  107. data/lib/simplecov/formatter/html_formatter/public/application.js +0 -18
  108. data/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
  109. data/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
  110. data/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
@@ -11,10 +11,30 @@ module SimpleCov
11
11
  # have a habit of tampering with ARGV, which makes i.e. the automatic distinction
12
12
  # between rails unit/functional/integration tests impossible without this cached
13
13
  # item.
14
- attr_accessor :original_run_command
14
+ attr_reader :original_run_command
15
+
16
+ # Assigning the flattened command clears any separately recorded program
17
+ # name so the two can never describe different runs. Callers that know the
18
+ # real one assign it immediately afterwards, as `defaults.rb` does.
19
+ def original_run_command=(command)
20
+ @original_program_name = nil
21
+ @original_run_command = command
22
+ end
23
+
24
+ # The invoked program on its own. `original_run_command` joins
25
+ # `$PROGRAM_NAME` and `ARGV` with a space, so a program path containing
26
+ # one (`/opt/My Ruby/bin/rspec`) cannot be recovered from it afterwards.
27
+ # Falls back to the first token for callers that set only the flattened
28
+ # command, which is every caller outside `defaults.rb`.
29
+ attr_writer :original_program_name
30
+
31
+ def original_program_name
32
+ @original_program_name || original_run_command.to_s[/\A\s*(\S+)/, 1]
33
+ end
15
34
 
16
35
  def guess
17
- [from_command_line_options || from_defined_constants, parallel_data].compact.join(" ")
36
+ framework = from_executable_name || from_command_line_options || from_defined_constants
37
+ [framework, parallel_data].compact.join(" ")
18
38
  end
19
39
 
20
40
  private
@@ -31,14 +51,31 @@ module SimpleCov
31
51
  "(#{number}/#{groups})"
32
52
  end
33
53
 
54
+ # The command is `"#{$PROGRAM_NAME} #{ARGV.join(' ')}"`, so a keyword only
55
+ # carries meaning when it begins a path segment or an argument. Matching it
56
+ # as a bare substring lets any part of the *interpreter* path decide the
57
+ # suite name: a Ruby installed under `latest/bin` (as mise does) contains
58
+ # "test/", so every run through it was labelled "Unit Tests". The same
59
+ # applies within ARGV, where `spec/greatest/` used to match just as wrongly.
60
+ #
61
+ # A backslash bounds a segment too, or a Windows `spec\foo_spec.rb` would
62
+ # lose a match the bare substrings used to make. The `test/` patterns below
63
+ # still spell their separator as `/` only, so a backslash `test\functional\`
64
+ # goes unrecognised there, but that is how it has always been rather than
65
+ # something the anchoring took away.
66
+ SEGMENT_START = %r{(?<![^\s/\\])}
67
+ private_constant :SEGMENT_START
68
+ SEGMENT_END = %r{(?=[\s/\\]|\z)}
69
+ private_constant :SEGMENT_END
70
+
34
71
  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"
72
+ %r{#{SEGMENT_START}test/functional/} => "Functional Tests",
73
+ %r{#{SEGMENT_START}test/\{.*functional.*\}/} => "Functional Tests",
74
+ %r{#{SEGMENT_START}test/integration/} => "Integration Tests",
75
+ %r{#{SEGMENT_START}test/} => "Unit Tests",
76
+ /#{SEGMENT_START}spec#{SEGMENT_END}/ => "RSpec",
77
+ /#{SEGMENT_START}cucumber#{SEGMENT_END}/ => "Cucumber Features",
78
+ /#{SEGMENT_START}features#{SEGMENT_END}/ => "Cucumber Features"
42
79
  }.freeze
43
80
  private_constant :COMMAND_LINE_FRAMEWORKS
44
81
 
@@ -46,6 +83,26 @@ module SimpleCov
46
83
  COMMAND_LINE_FRAMEWORKS.find { |pattern, _| pattern.match?(original_run_command.to_s) }&.last
47
84
  end
48
85
 
86
+ # Which binary was invoked outranks everything else on the command line.
87
+ # `rspec features` is an RSpec suite whose examples live in features/, not
88
+ # a Cucumber run, and only the executable name says so. Generic runners
89
+ # (`ruby`, `rake`, a bare `*_test.rb` script) are absent on purpose: they
90
+ # name no framework, so those runs fall through to the path patterns, which
91
+ # is what keeps the test/functional vs test/integration split working.
92
+ EXECUTABLE_FRAMEWORKS = {
93
+ "rspec" => "RSpec",
94
+ "cucumber" => "Cucumber Features"
95
+ }.freeze
96
+ private_constant :EXECUTABLE_FRAMEWORKS
97
+
98
+ def from_executable_name
99
+ program = original_program_name
100
+ return unless program
101
+
102
+ # Trim any extension so Windows' rspec.bat is recognized too.
103
+ EXECUTABLE_FRAMEWORKS[File.basename(program, ".*")]
104
+ end
105
+
49
106
  # Inner array literals after the first are flagged uncovered by Ruby's
50
107
  # Coverage module even though the constant evaluates as a whole — known
51
108
  # quirk with multi-line array literals.
@@ -38,12 +38,12 @@ module SimpleCov
38
38
  #
39
39
  # `primary: true` makes this the report's leading criterion (and the one a
40
40
  # bare `minimum_coverage 90` targets). `oneshot: true` (valid only for
41
- # `:line`) selects the faster oneshot-lines mode. `:eval` is enable-only.
41
+ # `:line`) selects the faster oneshot-lines mode.
42
42
  #
43
43
  def coverage(criterion, primary: false, enabled: true, oneshot: false, **thresholds, &block)
44
44
  criterion = enable_coverage_criterion(criterion, enabled: enabled, oneshot: oneshot)
45
45
  # The cast admits :eval, which primary_coverage rejects at runtime
46
- # (it is enable-only and never in the enabled-criteria set).
46
+ # (it is a standalone toggle, never in the enabled-criteria set).
47
47
  primary_coverage(_ = criterion) if primary
48
48
 
49
49
  configurator = CoverageCriterion.new(self, criterion)
@@ -70,30 +70,24 @@ module SimpleCov
70
70
  end
71
71
  end
72
72
 
73
- # Enable the criterion (or its oneshot / eval variant) and return the
74
- # criterion symbol that thresholds should be stored under.
73
+ # Enable or disable the criterion (or its oneshot / eval variant) and
74
+ # return the criterion symbol that thresholds should be stored under.
75
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
-
76
+ criterion = resolve_criterion_variant(criterion, oneshot)
79
77
  enabled ? enable_coverage(criterion) : disable_coverage(criterion)
80
78
  criterion
81
79
  end
82
80
 
83
- def enable_oneshot_line(criterion)
81
+ def resolve_criterion_variant(criterion, oneshot)
82
+ return criterion unless oneshot
83
+
84
84
  unless criterion == :line
85
85
  raise SimpleCov::ConfigurationError, "`oneshot: true` is only valid for `coverage :line`"
86
86
  end
87
87
 
88
- enable_coverage(ONESHOT_LINE_COVERAGE_CRITERION)
89
88
  ONESHOT_LINE_COVERAGE_CRITERION
90
89
  end
91
90
 
92
- def enable_eval_coverage_criterion
93
- enable_coverage(:eval)
94
- :eval
95
- end
96
-
97
91
  # @api private — threshold-store writers used by CoverageCriterion. They
98
92
  # write the same `@minimum_coverage` / `@maximum_coverage` / ... hashes the
99
93
  # flat threshold methods populate, so the exit-code checks are unchanged.
@@ -115,7 +109,10 @@ module SimpleCov
115
109
 
116
110
  def store_minimum_per_group(criterion, percent, group_name)
117
111
  raise_on_invalid_coverage({criterion => percent}, "minimum_coverage_by_group")
118
- (minimum_coverage_by_group[group_name] ||= {})[criterion] = percent
112
+ # Normalize like `group` does, so `only: :Models` finds the group
113
+ # defined as `group "Models"` at check time instead of storing a
114
+ # Symbol key no lookup ever matches.
115
+ (minimum_coverage_by_group[GroupNames.normalize(group_name)] ||= {})[criterion] = percent
119
116
  end
120
117
 
121
118
  #
@@ -3,33 +3,29 @@
3
3
  module SimpleCov
4
4
  # Selection and validation of the coverage criteria Ruby's `Coverage`
5
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.
6
+ # `:branch`, `:method`, and `:oneshot_line`. The standalone `:eval`
7
+ # toggle lives in `eval_coverage.rb`.
8
8
  module Configuration
9
9
  SUPPORTED_COVERAGE_CRITERIA = %i[line branch method oneshot_line].freeze
10
10
  DEFAULT_COVERAGE_CRITERION = :line
11
11
  ONESHOT_LINE_COVERAGE_CRITERION = :oneshot_line
12
+ LINE_COVERAGE_ALTERNATIVES = {line: :oneshot_line, oneshot_line: :line}.freeze #: line_coverage_alternatives
13
+ private_constant :LINE_COVERAGE_ALTERNATIVES
12
14
 
13
15
  # Enable one or more coverage criteria. `:eval` is accepted as a
14
16
  # shorthand for the standalone eval-coverage toggle.
15
17
  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
18
+ criteria.each { |criterion| criterion == :eval ? enable_eval_coverage : add_coverage_criterion(criterion) }
26
19
  end
27
20
 
28
21
  # 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.
22
+ # `:eval` turns the standalone eval-coverage toggle back off,
23
+ # mirroring `enable_coverage`. Disabling every criterion raises at
24
+ # `start_tracking` (not here), so config files that toggle criteria
25
+ # in arbitrary order don't have to worry about transient empty states.
32
26
  def disable_coverage(criterion)
27
+ return disable_eval_coverage if criterion == :eval
28
+
33
29
  raise_if_criterion_unsupported(criterion)
34
30
  coverage_criteria.delete(criterion)
35
31
  @primary_coverage = nil if @primary_coverage == criterion
@@ -68,6 +64,15 @@ module SimpleCov
68
64
  "Re-enable one with `enable_coverage :line`, `:branch`, or `:method`."
69
65
  end
70
66
 
67
+ # Whether this run produces line data at all. The oneshot variant counts:
68
+ # `ResultAdapter` turns its executed-line list back into a line array. A
69
+ # branch-only or method-only run produces none, and `Coverage.result`
70
+ # entries for the files it loaded carry no `"lines"` key.
71
+ def line_coverage?
72
+ coverage_criterion_enabled?(DEFAULT_COVERAGE_CRITERION) ||
73
+ coverage_criterion_enabled?(ONESHOT_LINE_COVERAGE_CRITERION)
74
+ end
75
+
71
76
  def branch_coverage?
72
77
  branch_coverage_supported? && coverage_criterion_enabled?(:branch)
73
78
  end
@@ -84,10 +89,6 @@ module SimpleCov
84
89
  coverage_criterion_supported?(:methods)
85
90
  end
86
91
 
87
- def coverage_for_eval_supported?
88
- coverage_criterion_supported?(:eval)
89
- end
90
-
91
92
  # Ask the Coverage runtime itself whether a criterion is supported
92
93
  # (Ruby >= 3.2). Older Rubies don't expose `Coverage.supported?`, so
93
94
  # fall back to the historical engine check that line/branch/method
@@ -104,27 +105,13 @@ module SimpleCov
104
105
  end
105
106
  # simplecov:enable
106
107
 
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
108
  private
119
109
 
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
110
+ def add_coverage_criterion(criterion)
111
+ raise_if_criterion_unsupported(criterion)
112
+ incompatible = LINE_COVERAGE_ALTERNATIVES[criterion]
113
+ disable_coverage(incompatible) if incompatible
114
+ coverage_criteria << criterion
128
115
  end
129
116
 
130
117
  # If `:line` is enabled, it's the default primary; otherwise fall
@@ -140,6 +127,15 @@ module SimpleCov
140
127
  end
141
128
 
142
129
  def raise_if_criterion_disabled(criterion)
130
+ # `coverage :eval` by itself IS supported — it's a standalone
131
+ # toggle, never in the enabled-criteria set — so the generic
132
+ # "unsupported criterion" message below would mislead here.
133
+ if criterion == :eval
134
+ raise SimpleCov::ConfigurationError,
135
+ "Coverage criterion :eval only toggles measuring eval'd code; " \
136
+ "it cannot carry thresholds or serve as the primary criterion"
137
+ end
138
+
143
139
  raise_if_criterion_unsupported(criterion)
144
140
  return if coverage_criterion_enabled?(criterion)
145
141
 
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ # The standalone eval-coverage toggle (`enable_coverage :eval`),
5
+ # instrumenting `eval`'d code on Rubies that support it. Kept separate
6
+ # from the criteria-set logic in `coverage_criteria.rb` because `:eval`
7
+ # is a boolean toggle, never a member of the enabled-criteria set.
8
+ module Configuration
9
+ def coverage_for_eval_supported?
10
+ coverage_criterion_supported?(:eval)
11
+ end
12
+
13
+ def coverage_for_eval_enabled?
14
+ @coverage_for_eval_enabled ||= false
15
+ end
16
+
17
+ # DEPRECATED: prefer `enable_coverage :eval`.
18
+ def enable_coverage_for_eval
19
+ SimpleCov::Deprecation.warn("`SimpleCov.enable_coverage_for_eval` is deprecated. " \
20
+ "Replace with `SimpleCov.enable_coverage :eval`.")
21
+ enable_eval_coverage
22
+ end
23
+
24
+ private
25
+
26
+ # Shared implementation backing both `enable_coverage :eval` and
27
+ # the deprecated `enable_coverage_for_eval`.
28
+ def enable_eval_coverage
29
+ if coverage_for_eval_supported?
30
+ @coverage_for_eval_enabled = true
31
+ else
32
+ warn "Coverage for eval is not available; Use Ruby 3.2.0 or later"
33
+ end
34
+ end
35
+
36
+ # No support check needed: off is a safe state on every Ruby.
37
+ def disable_eval_coverage
38
+ @coverage_for_eval_enabled = false
39
+ end
40
+ end
41
+ end
@@ -1,12 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleCov
4
- # Inclusion / exclusion / grouping methods: `cover`, `skip`, `group`,
5
- # plus the deprecated `track_files` / `add_filter` / `add_group`
6
- # aliases. Mutates the same `filters`, `groups`, and `cover_filters`
7
- # collections the main Configuration module exposes.
4
+ # Inclusion and exclusion methods: `cover` and `skip`, plus the deprecated
5
+ # `track_files` and `add_filter` aliases.
8
6
  module Configuration
9
- attr_writer :filters, :groups
7
+ attr_writer :filters
10
8
 
11
9
  #
12
10
  # Restrict the universe of files in the coverage report to those matching
@@ -43,7 +41,7 @@ module SimpleCov
43
41
  end
44
42
 
45
43
  # Returns the list of string globs passed to `cover` — used by the
46
- # disk-discovery pass in `SimpleCov.add_not_loaded_files` so files
44
+ # disk-discovery pass in `SimpleCov.tracked_file_paths` so files
47
45
  # matching a `cover` glob appear in the report even when they were
48
46
  # never required during the suite.
49
47
  #
@@ -121,33 +119,6 @@ module SimpleCov
121
119
  @filters = []
122
120
  end
123
121
 
124
- # Returns the configured groups. Add groups using SimpleCov.group.
125
- def groups
126
- @groups ||= {}
127
- end
128
-
129
- # Define a display group for files. Same matcher grammar as `skip`,
130
- # but instead of dropping the matching files it bins them under
131
- # `group_name` for the formatter. Files matched by no group fall
132
- # into the implicit "Ungrouped" bucket.
133
- def group(group_name, filter_argument = nil, &)
134
- groups[group_name] = parse_filter(filter_argument, &)
135
- end
136
-
137
- # DEPRECATED: alias for `group`. Same arguments, same behavior.
138
- def add_group(group_name, filter_argument = nil, &block)
139
- example = if block
140
- "`SimpleCov.group #{group_name.inspect} { ... }`"
141
- else
142
- "`SimpleCov.group #{group_name.inspect}, #{filter_argument.inspect}`"
143
- end
144
- SimpleCov::Deprecation.warn(
145
- "`SimpleCov.add_group` is deprecated. " \
146
- "Replace with `SimpleCov.group` (same arguments, same behavior). Example: #{example}."
147
- )
148
- group(group_name, filter_argument, &block)
149
- end
150
-
151
122
  # Drop every filter previously installed (defaults plus anything
152
123
  # earlier in this block) so subsequent `skip` calls start from a
153
124
  # clean slate. Order matters — call this before your own `skip`
@@ -168,18 +139,14 @@ module SimpleCov
168
139
  end
169
140
 
170
141
  # Build a filter for a `cover` argument. Strings are treated as
171
- # globs (not substrings — that's `skip`/`add_filter`'s semantics).
142
+ # globs (not substrings — that's `skip`/`add_filter`'s semantics);
143
+ # everything else dispatches exactly like `add_filter`.
172
144
  def build_cover_filter(arg)
173
- case arg
174
- when String then SimpleCov::GlobFilter.new(arg)
175
- when Regexp then SimpleCov::RegexFilter.new(arg)
176
- when Proc then SimpleCov::BlockFilter.new(arg)
177
- when SimpleCov::Filter then arg
178
- when Array then SimpleCov::ArrayFilter.new(arg.map { |a| build_cover_filter(a) })
179
- else raise SimpleCov::ConfigurationError, "Unsupported `cover` argument #{arg.inspect}; " \
180
- "expected a String glob, Regexp, Proc, " \
181
- "SimpleCov::Filter, or Array of those."
182
- end
145
+ SimpleCov::Filter.build_filter(arg, string_filter: SimpleCov::GlobFilter)
146
+ rescue SimpleCov::ConfigurationError
147
+ raise SimpleCov::ConfigurationError, "Unsupported `cover` argument #{arg.inspect}; " \
148
+ "expected a String glob, Regexp, Proc, " \
149
+ "SimpleCov::Filter, or Array of those."
183
150
  end
184
151
 
185
152
  # Walk a list of cover filters and return the string globs they hold,
@@ -9,11 +9,16 @@ module SimpleCov
9
9
  attr_writer :formatter, :print_error_status
10
10
 
11
11
  #
12
- # Gets or sets the configured formatter. Pass `false` (or `nil`) to
13
- # opt out of formatting entirely worker processes in big parallel
14
- # CI setups (see #964) only need their `.resultset.json` on disk so
15
- # a final `SimpleCov.collate` job can produce the report; running
16
- # them without a formatter saves the per-job HTML/multi-formatter
12
+ # Gets or sets the configured formatter. Accepts a formatter class
13
+ # (instantiated fresh for every report) or a ready-built instance,
14
+ # which is how constructor options are passed — e.g.
15
+ # `formatter SimpleCov::Formatter::HTMLFormatter.new(silent: true)`
16
+ # to suppress the "Coverage report generated" status line (see
17
+ # #1240). Pass `false` (or `nil`) to opt out of formatting
18
+ # entirely — worker processes in big parallel CI setups (see #964)
19
+ # only need their `.resultset.json` on disk so a final
20
+ # `SimpleCov.collate` job can produce the report; running them
21
+ # without a formatter saves the per-job HTML/multi-formatter
17
22
  # overhead.
18
23
  #
19
24
  def formatter(formatter = :__no_arg__)
@@ -40,9 +45,13 @@ module SimpleCov
40
45
 
41
46
  # Sets the configured formatters. Equivalent to `formatters [...]`.
42
47
  # Accepts a single formatter as well as an Array, matching the pre-1.0 behavior
43
- # where `MultiFormatter.new` normalized its input.
48
+ # where `MultiFormatter.new` normalized its input. Elements may be
49
+ # formatter classes or ready-built instances; see `formatter`.
50
+ # `nil`, `false`, and `[]` all opt out of formatting entirely —
51
+ # `false` normalized like `formatter false` does, since `Array(false)`
52
+ # would otherwise smuggle it in as a "formatter" that can only fail.
44
53
  def formatters=(formatters)
45
- formatters = Array(formatters)
54
+ formatters = Array(formatters || nil)
46
55
  @formatter = formatters.empty? ? nil : SimpleCov::Formatter::MultiFormatter.new(formatters)
47
56
  end
48
57
 
@@ -83,8 +92,8 @@ module SimpleCov
83
92
  # a copy of the source tree (which dominates the payload on larger
84
93
  # projects).
85
94
  #
86
- # The HTML viewer's `coverage_data.js` always includes source —
87
- # the client-side renderer needs it. Only `coverage.json` honors
95
+ # The HTML report's embedded data always includes source — the
96
+ # client-side renderer needs it. Only `coverage.json` honors
88
97
  # this setting.
89
98
  #
90
99
  # SimpleCov.start do
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ # Group configuration is kept separate from inclusion and exclusion filters,
5
+ # while sharing their matcher parser through the main Configuration module.
6
+ module Configuration
7
+ # Returns the configured groups. Add groups using SimpleCov.group.
8
+ def groups
9
+ @groups ||= {}
10
+ end
11
+
12
+ def groups=(new_groups)
13
+ new_groups = new_groups.to_h { |name, filter| [GroupNames.normalize(name), filter] }
14
+ GroupNames.validate!(new_groups.keys)
15
+ @groups = new_groups
16
+ end
17
+
18
+ # Define a display group for files. Same matcher grammar as `skip`,
19
+ # but instead of dropping the matching files it bins them under
20
+ # `group_name` for the formatter. Files matched by no group fall
21
+ # into the implicit "Ungrouped" bucket.
22
+ def group(group_name, filter_argument = nil, &)
23
+ group_name = GroupNames.normalize(group_name)
24
+ GroupNames.validate!([group_name])
25
+ groups[group_name] = parse_filter(filter_argument, &)
26
+ end
27
+
28
+ # DEPRECATED: alias for `group`. Same arguments, same behavior.
29
+ def add_group(group_name, filter_argument = nil, &block)
30
+ example = if block
31
+ "`SimpleCov.group #{group_name.inspect} { ... }`"
32
+ else
33
+ "`SimpleCov.group #{group_name.inspect}, #{filter_argument.inspect}`"
34
+ end
35
+ SimpleCov::Deprecation.warn(
36
+ "`SimpleCov.add_group` is deprecated. " \
37
+ "Replace with `SimpleCov.group` (same arguments, same behavior). Example: #{example}."
38
+ )
39
+ group(group_name, filter_argument, &block)
40
+ end
41
+ end
42
+ end
@@ -38,9 +38,7 @@ module SimpleCov
38
38
  def enable_for_subprocesses(value = nil)
39
39
  SimpleCov::Deprecation.warn("`SimpleCov.enable_for_subprocesses` is deprecated. " \
40
40
  "Replace with `SimpleCov.merge_subprocesses` (same value, same behavior).")
41
- return @enable_for_subprocesses if defined?(@enable_for_subprocesses) && value.nil?
42
-
43
- @enable_for_subprocesses = value || false
41
+ merge_subprocesses(value)
44
42
  end
45
43
 
46
44
  #
@@ -55,7 +53,7 @@ module SimpleCov
55
53
  end
56
54
 
57
55
  #
58
- # Get or set whether this process owns final merge processing:
56
+ # Get or set whether SimpleCov's selected final process owns merge processing:
59
57
  # waiting for sibling workers, building the merged result, formatting,
60
58
  # enforcing thresholds, and writing `.last_run.json`.
61
59
  #
@@ -83,15 +81,18 @@ module SimpleCov
83
81
 
84
82
  # @api private
85
83
  def merge_finalization_owner?
86
- collating_result? || finalize_merge?
84
+ collating_result? || (finalize_merge? && final_result_process?)
87
85
  end
88
86
 
89
87
  # DEPRECATED: alias for `merging`. Same value, same behavior.
88
+ # Delegating (rather than duplicating the body) also fixes the
89
+ # return value: this used to return nil where `merging` returns
90
+ # false, because the final expression was the skipped guard
91
+ # assignment rather than the stored value.
90
92
  def use_merging(use = nil)
91
93
  SimpleCov::Deprecation.warn("`SimpleCov.use_merging` is deprecated. " \
92
94
  "Replace with `SimpleCov.merging` (same value, same behavior).")
93
- @use_merging = use unless use.nil?
94
- @use_merging = true unless defined?(@use_merging) && @use_merging == false
95
+ merging(use)
95
96
  end
96
97
 
97
98
  #
@@ -14,9 +14,7 @@ module SimpleCov
14
14
  def minimum_coverage(coverage = nil)
15
15
  return @minimum_coverage ||= {} unless coverage
16
16
 
17
- coverage = {primary_coverage => coverage} if coverage.is_a?(Numeric)
18
- raise_on_invalid_coverage(coverage, "minimum_coverage")
19
- @minimum_coverage = coverage
17
+ @minimum_coverage = normalized_threshold(coverage, "minimum_coverage")
20
18
  end
21
19
 
22
20
  def raise_on_invalid_coverage(coverage, coverage_setting)
@@ -35,9 +33,7 @@ module SimpleCov
35
33
  def maximum_coverage(coverage = nil)
36
34
  return @maximum_coverage ||= {} unless coverage
37
35
 
38
- coverage = {primary_coverage => coverage} if coverage.is_a?(Numeric)
39
- raise_on_invalid_coverage(coverage, "maximum_coverage")
40
- @maximum_coverage = coverage
36
+ @maximum_coverage = normalized_threshold(coverage, "maximum_coverage")
41
37
  end
42
38
 
43
39
  #
@@ -58,9 +54,7 @@ module SimpleCov
58
54
  def maximum_coverage_drop(coverage_drop = nil)
59
55
  return @maximum_coverage_drop ||= {} unless coverage_drop
60
56
 
61
- coverage_drop = {primary_coverage => coverage_drop} if coverage_drop.is_a?(Numeric)
62
- raise_on_invalid_coverage(coverage_drop, "maximum_coverage_drop")
63
- @maximum_coverage_drop = coverage_drop
57
+ @maximum_coverage_drop = normalized_threshold(coverage_drop, "maximum_coverage_drop")
64
58
  end
65
59
 
66
60
  #
@@ -100,10 +94,8 @@ module SimpleCov
100
94
 
101
95
  SimpleCov::Deprecation.warn("`SimpleCov.minimum_coverage_by_group` is deprecated. " \
102
96
  "Replace it with:\n#{per_group_coverage_replacement(coverage)}")
103
- @minimum_coverage_by_group = coverage.dup.transform_values do |group_coverage|
104
- group_coverage = {primary_coverage => group_coverage} if group_coverage.is_a?(Numeric)
105
- raise_on_invalid_coverage(group_coverage, "minimum_coverage_by_group")
106
- group_coverage
97
+ @minimum_coverage_by_group = coverage.to_h do |group_name, group_coverage|
98
+ [GroupNames.normalize(group_name), normalized_threshold(group_coverage, "minimum_coverage_by_group")]
107
99
  end
108
100
  end
109
101
 
@@ -117,6 +109,15 @@ module SimpleCov
117
109
 
118
110
  private
119
111
 
112
+ # Shared normalize-and-validate step behind every threshold setter:
113
+ # a bare Numeric targets the primary criterion, and the resulting
114
+ # per-criterion hash is validated before it is stored.
115
+ def normalized_threshold(coverage, setting)
116
+ coverage = {primary_coverage => coverage} if coverage.is_a?(Numeric)
117
+ raise_on_invalid_coverage(coverage, setting)
118
+ coverage
119
+ end
120
+
120
121
  # Split a `minimum_coverage_by_file` argument into Symbol-keyed
121
122
  # criterion defaults and String/Regexp-keyed per-path overrides;
122
123
  # normalize Numeric override values to `{primary_coverage => N}`