simplecov 1.1.0 → 1.2.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 (196) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +165 -57
  3. data/exe/simplecov +0 -3
  4. data/lib/minitest/simplecov_plugin.rb +12 -9
  5. data/lib/simplecov/atomic_file.rb +16 -19
  6. data/lib/simplecov/autostart.rb +5 -7
  7. data/lib/simplecov/baseline/parser.rb +58 -0
  8. data/lib/simplecov/baseline.rb +170 -0
  9. data/lib/simplecov/cli/affected/changed_files.rb +57 -0
  10. data/lib/simplecov/cli/affected/selection.rb +117 -0
  11. data/lib/simplecov/cli/affected.rb +148 -0
  12. data/lib/simplecov/cli/badge/svg.rb +70 -0
  13. data/lib/simplecov/cli/badge.rb +62 -0
  14. data/lib/simplecov/cli/clean.rb +23 -24
  15. data/lib/simplecov/cli/command_helpers.rb +55 -19
  16. data/lib/simplecov/cli/completions/scripts.rb +114 -0
  17. data/lib/simplecov/cli/completions.rb +83 -0
  18. data/lib/simplecov/cli/coverage.rb +25 -28
  19. data/lib/simplecov/cli/coverage_file.rb +58 -14
  20. data/lib/simplecov/cli/dead_code/output.rb +100 -0
  21. data/lib/simplecov/cli/dead_code.rb +139 -0
  22. data/lib/simplecov/cli/diff/output.rb +45 -0
  23. data/lib/simplecov/cli/diff.rb +35 -67
  24. data/lib/simplecov/cli/dotfile.rb +83 -38
  25. data/lib/simplecov/cli/git.rb +41 -0
  26. data/lib/simplecov/cli/history/output.rb +135 -0
  27. data/lib/simplecov/cli/history.rb +67 -0
  28. data/lib/simplecov/cli/merge.rb +34 -39
  29. data/lib/simplecov/cli/open.rb +12 -15
  30. data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
  31. data/lib/simplecov/cli/patch/output.rb +135 -0
  32. data/lib/simplecov/cli/patch.rb +169 -0
  33. data/lib/simplecov/cli/ratchet/output.rb +44 -0
  34. data/lib/simplecov/cli/ratchet.rb +83 -0
  35. data/lib/simplecov/cli/report.rb +18 -24
  36. data/lib/simplecov/cli/run.rb +8 -9
  37. data/lib/simplecov/cli/serve/report_preparer.rb +7 -6
  38. data/lib/simplecov/cli/serve/static_file_handler.rb +62 -56
  39. data/lib/simplecov/cli/serve.rb +27 -24
  40. data/lib/simplecov/cli/show/annotator.rb +87 -0
  41. data/lib/simplecov/cli/show/sweep.rb +36 -0
  42. data/lib/simplecov/cli/show.rb +133 -0
  43. data/lib/simplecov/cli/status/facts.rb +87 -0
  44. data/lib/simplecov/cli/status.rb +93 -0
  45. data/lib/simplecov/cli/tests/redundancy.rb +78 -0
  46. data/lib/simplecov/cli/tests.rb +148 -0
  47. data/lib/simplecov/cli/uncovered/misses.rb +37 -0
  48. data/lib/simplecov/cli/uncovered.rb +70 -32
  49. data/lib/simplecov/cli/usage.rb +152 -0
  50. data/lib/simplecov/cli/watch/live_report.rb +73 -0
  51. data/lib/simplecov/cli/watch/narrator.rb +55 -0
  52. data/lib/simplecov/cli/watch/poller.rb +45 -0
  53. data/lib/simplecov/cli/watch/session.rb +134 -0
  54. data/lib/simplecov/cli/watch/test_plan.rb +43 -0
  55. data/lib/simplecov/cli/watch.rb +85 -0
  56. data/lib/simplecov/cli.rb +43 -92
  57. data/lib/simplecov/color.rb +21 -35
  58. data/lib/simplecov/combine/branches_combiner.rb +7 -31
  59. data/lib/simplecov/combine/coverage_accumulator.rb +49 -142
  60. data/lib/simplecov/combine/identity_interner.rb +10 -13
  61. data/lib/simplecov/combine/interned_counts.rb +7 -9
  62. data/lib/simplecov/combine/lines_combiner.rb +20 -44
  63. data/lib/simplecov/combine/methods_combiner.rb +9 -35
  64. data/lib/simplecov/combine/results_combiner.rb +4 -21
  65. data/lib/simplecov/combine.rb +0 -6
  66. data/lib/simplecov/command_guesser.rb +20 -34
  67. data/lib/simplecov/configuration/baseline.rb +31 -0
  68. data/lib/simplecov/configuration/coverage.rb +91 -79
  69. data/lib/simplecov/configuration/coverage_criteria.rb +43 -54
  70. data/lib/simplecov/configuration/deprecations.rb +21 -0
  71. data/lib/simplecov/configuration/eval_coverage.rb +6 -10
  72. data/lib/simplecov/configuration/filters.rb +40 -73
  73. data/lib/simplecov/configuration/formatting.rb +74 -83
  74. data/lib/simplecov/configuration/groups.rb +19 -13
  75. data/lib/simplecov/configuration/history.rb +44 -0
  76. data/lib/simplecov/configuration/ignored_entries.rb +31 -22
  77. data/lib/simplecov/configuration/merging.rb +94 -62
  78. data/lib/simplecov/configuration/missed_caps.rb +51 -0
  79. data/lib/simplecov/configuration/production.rb +23 -0
  80. data/lib/simplecov/configuration/test_tracking.rb +48 -0
  81. data/lib/simplecov/configuration/thresholds.rb +33 -62
  82. data/lib/simplecov/configuration/view_coverage.rb +38 -0
  83. data/lib/simplecov/configuration.rb +70 -76
  84. data/lib/simplecov/configuration_error.rb +5 -0
  85. data/lib/simplecov/context_map/union.rb +81 -0
  86. data/lib/simplecov/context_map.rb +187 -0
  87. data/lib/simplecov/coverage_json.rb +2 -5
  88. data/lib/simplecov/coverage_statistics.rb +14 -24
  89. data/lib/simplecov/coverage_violations.rb +157 -64
  90. data/lib/simplecov/current_run.rb +61 -0
  91. data/lib/simplecov/defaults.rb +4 -23
  92. data/lib/simplecov/deprecation.rb +45 -27
  93. data/lib/simplecov/directive.rb +46 -62
  94. data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
  95. data/lib/simplecov/exit_codes/check.rb +17 -11
  96. data/lib/simplecov/exit_codes/exit_code_handling.rb +21 -8
  97. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +7 -10
  98. data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
  99. data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
  100. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +7 -12
  101. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +11 -10
  102. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +7 -9
  103. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +16 -17
  104. data/lib/simplecov/exit_codes.rb +17 -7
  105. data/lib/simplecov/exit_handling.rb +94 -59
  106. data/lib/simplecov/file_list.rb +20 -51
  107. data/lib/simplecov/filter.rb +43 -66
  108. data/lib/simplecov/formatter/base.rb +38 -51
  109. data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
  110. data/lib/simplecov/formatter/coverage_json_writer.rb +29 -31
  111. data/lib/simplecov/formatter/html_formatter/public/index.html +17 -10
  112. data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +74 -17
  113. data/lib/simplecov/formatter/html_formatter.rb +24 -41
  114. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +50 -16
  115. data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
  116. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +43 -29
  117. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +14 -10
  118. data/lib/simplecov/formatter/json_formatter.rb +7 -13
  119. data/lib/simplecov/formatter/multi_formatter.rb +5 -11
  120. data/lib/simplecov/formatter/simple_formatter.rb +2 -8
  121. data/lib/simplecov/formatter.rb +14 -10
  122. data/lib/simplecov/group_names.rb +13 -12
  123. data/lib/simplecov/history.rb +115 -0
  124. data/lib/simplecov/last_run.rb +6 -9
  125. data/lib/simplecov/lines_classifier.rb +31 -16
  126. data/lib/simplecov/load_global_config.rb +7 -7
  127. data/lib/simplecov/parallel_adapters/base.rb +13 -41
  128. data/lib/simplecov/parallel_adapters/generic.rb +7 -18
  129. data/lib/simplecov/parallel_adapters/parallel_tests.rb +21 -42
  130. data/lib/simplecov/parallel_adapters.rb +24 -53
  131. data/lib/simplecov/parallel_coordination.rb +39 -49
  132. data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
  133. data/lib/simplecov/parallel_result_merger.rb +67 -126
  134. data/lib/simplecov/process.rb +14 -32
  135. data/lib/simplecov/production/error.rb +11 -0
  136. data/lib/simplecov/production/file_sink.rb +130 -0
  137. data/lib/simplecov/production.rb +249 -0
  138. data/lib/simplecov/profiles/rails.rb +1 -12
  139. data/lib/simplecov/profiles/root_filter.rb +0 -4
  140. data/lib/simplecov/profiles/strict.rb +9 -19
  141. data/lib/simplecov/profiles.rb +11 -33
  142. data/lib/simplecov/report_deferral.rb +11 -15
  143. data/lib/simplecov/report_stamp.rb +6 -9
  144. data/lib/simplecov/result/filter_config.rb +19 -0
  145. data/lib/simplecov/result/missing_source_files_reporter.rb +8 -9
  146. data/lib/simplecov/result/serialization.rb +43 -0
  147. data/lib/simplecov/result/source_file_builder.rb +7 -12
  148. data/lib/simplecov/result.rb +68 -117
  149. data/lib/simplecov/result_adapter.rb +58 -68
  150. data/lib/simplecov/result_merger/contexts.rb +22 -0
  151. data/lib/simplecov/result_merger/legacy_format_adapter.rb +4 -8
  152. data/lib/simplecov/result_merger/resultset_file.rb +20 -27
  153. data/lib/simplecov/result_merger/resultset_run_identity.rb +23 -25
  154. data/lib/simplecov/result_merger/resultset_store.rb +23 -14
  155. data/lib/simplecov/result_merger/unloaded_files.rb +24 -32
  156. data/lib/simplecov/result_merger.rb +59 -58
  157. data/lib/simplecov/result_processing.rb +74 -119
  158. data/lib/simplecov/run_identity.rb +16 -18
  159. data/lib/simplecov/simulate_coverage.rb +35 -50
  160. data/lib/simplecov/source_file/branch.rb +8 -33
  161. data/lib/simplecov/source_file/branch_builder.rb +28 -47
  162. data/lib/simplecov/source_file/builder_context.rb +6 -13
  163. data/lib/simplecov/source_file/line.rb +13 -33
  164. data/lib/simplecov/source_file/line_builder.rb +9 -15
  165. data/lib/simplecov/source_file/method.rb +3 -7
  166. data/lib/simplecov/source_file/method_builder.rb +6 -15
  167. data/lib/simplecov/source_file/ruby_data_parser.rb +59 -66
  168. data/lib/simplecov/source_file/skip_chunks.rb +19 -28
  169. data/lib/simplecov/source_file/source_loader.rb +45 -35
  170. data/lib/simplecov/source_file/statistics.rb +10 -9
  171. data/lib/simplecov/source_file.rb +21 -51
  172. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +63 -114
  173. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +76 -121
  174. data/lib/simplecov/static_coverage_extractor/method_collector.rb +11 -24
  175. data/lib/simplecov/static_coverage_extractor/prism_compat.rb +15 -24
  176. data/lib/simplecov/static_coverage_extractor/value_position.rb +25 -38
  177. data/lib/simplecov/static_coverage_extractor/visitor.rb +47 -64
  178. data/lib/simplecov/static_coverage_extractor.rb +46 -62
  179. data/lib/simplecov/test_tracker/accessors.rb +34 -0
  180. data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
  181. data/lib/simplecov/test_tracker/delta.rb +69 -0
  182. data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
  183. data/lib/simplecov/test_tracker.rb +162 -0
  184. data/lib/simplecov/unloaded_file_injector.rb +18 -31
  185. data/lib/simplecov/useless_results_remover.rb +7 -6
  186. data/lib/simplecov/version.rb +1 -1
  187. data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
  188. data/lib/simplecov/view_coverage.rb +55 -0
  189. data/lib/simplecov-html.rb +1 -0
  190. data/lib/simplecov.rb +59 -114
  191. data/lib/simplecov_json_formatter.rb +1 -0
  192. data/man/simplecov.1 +349 -0
  193. data/sig/simplecov.rbs +523 -595
  194. metadata +72 -7
  195. data/schemas/coverage-v1.0.schema.json +0 -306
  196. data/schemas/coverage.schema.json +0 -306
@@ -1,48 +1,71 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleCov
4
- # Computes coverage threshold violations for a given result. Shared by
5
- # the exit-code checks and the JSON formatter's `errors` section.
6
- #
7
- # Each method returns an array of violation hashes. All percents are
8
- # rounded via `SimpleCov.round_coverage` so downstream consumers don't
9
- # need to round again.
10
4
  module CoverageViolations
11
5
  class << self
12
- # @return [Array<Hash>] {:criterion, :expected, :actual}
13
6
  def minimum_overall(result, thresholds)
14
7
  thresholds.filter_map do |criterion, expected|
15
- actual = percent_for(result, criterion) or next
8
+ actual = floored_percent_for(result, criterion) or next
16
9
  {criterion: criterion, expected: expected, actual: actual} if actual < expected
17
10
  end
18
11
  end
19
12
 
20
- # @return [Array<Hash>] {:criterion, :expected, :actual}
21
- # Tolerance: `percent_for` floors the actual percent to two decimal
22
- # places (matching the existing minimum-coverage behavior), so an
23
- # actual of e.g. 95.4287 is treated as 95.42 — meaning a maximum of
24
- # 95.42 still passes. See issue #187 for the rationale.
13
+ # An actual of 95.4287 floors to 95.42, so a maximum of 95.42 passes
14
+ # (#187).
25
15
  def maximum_overall(result, thresholds)
26
16
  thresholds.filter_map do |criterion, expected|
27
- actual = percent_for(result, criterion) or next
17
+ actual = floored_percent_for(result, criterion) or next
28
18
  {criterion: criterion, expected: expected, actual: actual} if actual > expected
29
19
  end
30
20
  end
31
21
 
32
- # @return [Array<Hash>] {:criterion, :expected, :actual, :filename, :project_filename}
33
- #
34
- # `defaults` is the criterion-keyed Hash applied to every file.
35
- # `overrides` is an ordered Hash<pattern, criterion_thresholds> of per-path
36
- # overrides; for each file, defaults are merged with every matching override
37
- # (later wins per criterion, overrides win over defaults).
38
- def minimum_by_file(result, defaults, overrides = {})
22
+ # A file and criterion with a `baseline` floor is exempt here: the baseline
23
+ # check holds it to its own floor instead, which is the fall-through the
24
+ # ratchet workflow rests on (#1268).
25
+ def minimum_by_file(result, defaults, overrides = {}, baseline: nil)
39
26
  result.files.flat_map do |file|
40
27
  effective = effective_per_file_thresholds(file, defaults, overrides)
41
- effective.filter_map { |criterion, expected| file_minimum_violation(file, criterion, expected) }
28
+ effective.filter_map do |criterion, expected|
29
+ next if baseline&.covers?(file.project_filename, criterion)
30
+
31
+ file_minimum_violation(file, criterion, expected)
32
+ end
33
+ end
34
+ end
35
+
36
+ # A baseline violation needs the file below its floor on both axes: a
37
+ # percent under the floor's, and (when the floor records one) more misses
38
+ # than it allows. The missed count is what keeps percent movement from
39
+ # pure edits out of the answer.
40
+ def baseline(result, baseline)
41
+ return [] unless baseline
42
+
43
+ result.files.flat_map do |file|
44
+ entry = baseline.entry_for(file.project_filename)
45
+ next [] unless entry
46
+
47
+ entry.filter_map { |criterion, floor| baseline_violation(file, criterion, floor) }
48
+ end
49
+ end
50
+
51
+ def maximum_missed(result, caps)
52
+ caps.filter_map do |criterion, maximum|
53
+ actual = missed_for(result, criterion) or next
54
+ {criterion: criterion, maximum: maximum, actual: actual} if actual > maximum
55
+ end
56
+ end
57
+
58
+ def maximum_missed_by_file(result, defaults, overrides = {}, baseline: nil)
59
+ result.files.flat_map do |file|
60
+ effective = effective_per_file_thresholds(file, defaults, overrides)
61
+ effective.filter_map do |criterion, maximum|
62
+ next if baseline&.covers?(file.project_filename, criterion)
63
+
64
+ file_missed_cap_violation(file, criterion, maximum)
65
+ end
42
66
  end
43
67
  end
44
68
 
45
- # @return [Array<Hash>] {:group_name, :criterion, :expected, :actual}
46
69
  def minimum_by_group(result, thresholds)
47
70
  thresholds.flat_map do |group_name, minimums|
48
71
  group = lookup_group(result, group_name)
@@ -51,50 +74,38 @@ module SimpleCov
51
74
  end
52
75
  end
53
76
 
54
- # @return [Array<Hash>] {:criterion, :maximum, :actual} where `actual`
55
- # is the observed drop (in percentage points) vs. the last run.
56
- def maximum_drop(result, thresholds, last_run: SimpleCov::LastRun.read)
57
- return [] unless last_run
77
+ def maximum_drop(result, thresholds, last_run: nil, mode: SimpleCov.drop_baseline)
78
+ baseline = drop_baseline_percents(mode, last_run)
79
+ return [] unless baseline
58
80
 
59
81
  thresholds.filter_map do |criterion, maximum|
60
- actual = compute_drop(criterion, result, last_run)
82
+ actual = compute_drop(criterion, result, baseline)
61
83
  {criterion: criterion, maximum: maximum, actual: actual} if actual && actual > maximum
62
84
  end
63
85
  end
64
86
 
65
- private
87
+ private
66
88
 
67
- # Look up a criterion's percent on any coverage_statistics-bearing
68
- # object (Result, SourceFile, FileList). Returns nil and the
69
- # caller silently skips when the criterion was configured but not
70
- # actually measured by the runtime (e.g. `minimum_coverage branch:
71
- # 100` under the "strict" profile on JRuby, where the Coverage
72
- # module doesn't emit branch data). The config-time
73
- # `raise_if_criterion_disabled` check still catches the genuine
74
- # "forgot to enable the criterion" mistake before we ever get here.
75
- def percent_for(stats_source, criterion)
89
+ # Answers nil, and the caller silently skips, when the criterion was
90
+ # configured but not measured by the runtime (`branch: 100` under the
91
+ # "strict" profile on JRuby, say). The config-time
92
+ # `raise_if_criterion_disabled` check catches the genuine mistake earlier.
93
+ def floored_percent_for(stats_source, criterion)
76
94
  stats = stats_source.coverage_statistics[SimpleCov.coverage_statistics_key(criterion)]
77
95
  round(stats.percent) if stats
78
96
  end
79
97
 
80
- # Walk the overrides in declaration order, merging each one that matches
81
- # the file's project path into the running effective threshold (so the
82
- # most-specific or latest-declared override wins per criterion). Returns
83
- # the defaults Hash unchanged when nothing matches.
84
98
  def effective_per_file_thresholds(file, defaults, overrides)
85
- return defaults if overrides.empty?
86
-
87
99
  path = file.project_filename
88
100
  overrides.reduce(defaults) do |acc, (pattern, criterion_thresholds)|
89
101
  path_matches?(path, pattern) ? acc.merge(criterion_thresholds) : acc
90
102
  end
91
103
  end
92
104
 
93
- # Per-path matching for `minimum_coverage_by_file` overrides. Strings
94
- # ending in `/` are treated as directory prefixes; otherwise they must
95
- # match `project_filename` exactly. Regexps are tested via `match?`.
96
- # The configuration setter rejects anything other than String/Regexp,
97
- # so no dead `else` branch is needed here.
105
+ # `==` and `eql?` are indistinguishable for the String operands, so that
106
+ # mutation is equivalent. The override examples pin every arm instead.
107
+ # mutant:disable
108
+ # mutant:disable
98
109
  def path_matches?(project_filename, pattern)
99
110
  return project_filename.match?(pattern) if pattern.is_a?(Regexp)
100
111
  return project_filename.start_with?(pattern) if pattern.end_with?("/")
@@ -102,8 +113,33 @@ module SimpleCov
102
113
  project_filename == pattern
103
114
  end
104
115
 
116
+ def missed_for(stats_source, criterion)
117
+ stats = stats_source.coverage_statistics[SimpleCov.coverage_statistics_key(criterion)]
118
+ stats&.missed
119
+ end
120
+
121
+ def file_missed_cap_violation(file, criterion, maximum)
122
+ actual = missed_for(file, criterion) or return
123
+ return unless actual > maximum
124
+
125
+ {criterion: criterion, maximum: maximum, actual: actual,
126
+ filename: file.filename, project_filename: file.project_filename}
127
+ end
128
+
129
+ def baseline_violation(file, criterion, floor)
130
+ stats = file.coverage_statistics[SimpleCov.coverage_statistics_key(criterion)]
131
+ return unless stats
132
+
133
+ actual = round(stats.percent)
134
+ return unless actual < floor.percent
135
+ return if floor.missed && stats.missed <= floor.missed
136
+
137
+ {criterion: criterion, expected: floor.percent, allowed_missed: floor.missed, actual: actual,
138
+ actual_missed: stats.missed, filename: file.filename, project_filename: file.project_filename}
139
+ end
140
+
105
141
  def file_minimum_violation(file, criterion, expected)
106
- actual = percent_for(file, criterion) or return
142
+ actual = floored_percent_for(file, criterion) or return
107
143
  return unless actual < expected
108
144
 
109
145
  {
@@ -117,37 +153,94 @@ module SimpleCov
117
153
 
118
154
  def group_minimum_violations(group_name, group, minimums)
119
155
  minimums.filter_map do |criterion, expected|
120
- actual = percent_for(group, criterion) or next
156
+ actual = floored_percent_for(group, criterion) or next
121
157
  {group_name: group_name, criterion: criterion, expected: expected, actual: actual} if actual < expected
122
158
  end
123
159
  end
124
160
 
125
- # The misconfiguration notice is enforcement output, not a Ruby
126
- # warning: like every other enforcement message it must survive
127
- # `-W0` and `Warning.warn` hooks (see ExitCodes.print_error) and
128
- # honor the `print_errors` opt-out.
161
+ # The misconfiguration notice is enforcement output, not a Ruby warning: it
162
+ # must survive `-W0` and `Warning.warn` hooks, and honor `print_errors`.
129
163
  def lookup_group(result, group_name)
130
164
  group = result.groups[group_name]
131
165
  if group.nil? && SimpleCov.print_errors
132
166
  ExitCodes.print_error "minimum_coverage_by_group: no group named '#{group_name}' exists. " \
133
- "Available groups: #{result.groups.keys.join(', ')}"
167
+ "Available groups: #{result.groups.keys.join(", ")}"
134
168
  end
135
169
  group
136
170
  end
137
171
 
138
- def compute_drop(criterion, result, last_run)
139
- stats_key = SimpleCov.coverage_statistics_key(criterion)
140
- last_coverage_percent = last_run.dig(:result, stats_key)
141
- if last_coverage_percent.nil? && stats_key == :line
142
- last_coverage_percent = last_run.dig(:result, :covered_percent)
172
+ def drop_baseline_percents(mode, last_run)
173
+ case mode
174
+ when :median then median_baseline
175
+ when :branch then branch_baseline
176
+ else last_run_baseline(last_run || LastRun.read)
177
+ end
178
+ end
179
+
180
+ # A hand-edited .last_run.json can carry any value type, and `LastRun.read`
181
+ # only vouches for the top level being a Hash. `:covered_percent` is the
182
+ # pre-criteria file format's spelling of the line percent.
183
+ def last_run_baseline(last_run)
184
+ return nil unless last_run.is_a?(Hash)
185
+
186
+ previous = (_ = last_run)[:result]
187
+ return nil unless previous.is_a?(Hash)
188
+
189
+ previous = _ = previous
190
+ {line: previous[:line] || previous[:covered_percent],
191
+ branch: previous[:branch], method: previous[:method]}
192
+ end
193
+
194
+ def median_baseline
195
+ totals = history_totals
196
+ baseline = {} #: Hash[Symbol, untyped]
197
+ %i[line branch method].each do |criterion|
198
+ values = totals.map { |entry| entry[criterion.to_s] }.grep(Numeric)
199
+ baseline[criterion] = median(values) unless values.empty?
200
+ end
201
+ baseline
202
+ end
203
+
204
+ def history_totals
205
+ History.read.filter_map do |entry|
206
+ entry.fetch("totals") if entry.is_a?(Hash) && entry["totals"].is_a?(Hash)
143
207
  end
144
- # A hand-edited .last_run.json can carry any value type, and
145
- # LastRun.read only vouches for the top level being a Hash.
208
+ end
209
+
210
+ def median(values)
211
+ sorted = values.sort
212
+ mid = sorted.length / 2
213
+ sorted.length.odd? ? sorted.fetch(mid) : (sorted.fetch(mid - 1) + sorted.fetch(mid)).fdiv(2)
214
+ end
215
+
216
+ def branch_baseline
217
+ branch, = History.git_info
218
+ return nil unless branch
219
+
220
+ entry = History.read.reverse_each.find { |candidate| branch_entry?(candidate, branch) }
221
+ return nil unless entry
222
+
223
+ totals = {} #: Hash[Symbol, untyped]
224
+ entry.fetch("totals").each { |key, value| totals[key.to_sym] = value }
225
+ totals
226
+ end
227
+
228
+ # `==` and `eql?` are indistinguishable for the String operands, so that
229
+ # mutation is equivalent. The branch-baseline examples pin every clause.
230
+ # mutant:disable
231
+ # mutant:disable
232
+ def branch_entry?(candidate, branch)
233
+ candidate.is_a?(Hash) && candidate["branch"] == branch && candidate["totals"].is_a?(Hash)
234
+ end
235
+
236
+ def compute_drop(criterion, result, baseline)
237
+ stats_key = SimpleCov.coverage_statistics_key(criterion)
238
+ last_coverage_percent = baseline[stats_key]
146
239
  # Treat a non-numeric percent like a missing one instead of
147
240
  # raising out of the at_exit hook.
148
241
  return unless last_coverage_percent.is_a?(Numeric)
149
242
 
150
- current = percent_for(result, criterion) or return
243
+ current = floored_percent_for(result, criterion) or return
151
244
  (last_coverage_percent - current).floor(10)
152
245
  end
153
246
 
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ # The state one measured run accumulates in one process: the pid and start
5
+ # time that identify it, the result it computes, the flags the merge
6
+ # coordination reads, and the serial the parent hands each forked
7
+ # subprocess. A new run is a new object, which is what lets anything needing
8
+ # a clean slate begin one instead of unsetting state by hand.
9
+ class CurrentRun
10
+ attr_accessor :pid, :process_start_time, :result
11
+ attr_writer :collating_result
12
+
13
+ # The result, the coordination flags and the identity belong to the run that
14
+ # ends; the fork genealogy belongs to the process and carries over. A child
15
+ # that restarts tracking must not forget it was forked, or it would produce
16
+ # the final report itself, and a parent must not recount from zero, or its
17
+ # next children's names would collide with its first ones (#1171).
18
+ def successor
19
+ following = self.class.new
20
+ following.subprocess_serial = @subprocess_serial
21
+ following.mark_forked_subprocess! if forked_subprocess?
22
+ following
23
+ end
24
+
25
+ # Answers exactly false while no result was ever stored, which is how "never
26
+ # computed" stays distinct from "cleared to nil".
27
+ def result?
28
+ instance_variable_defined?(:@result) && (_ = @result)
29
+ end
30
+
31
+ def collating_result?
32
+ !!@collating_result
33
+ end
34
+
35
+ # Reading a mark that was never set answers nil, which is the answer wanted.
36
+ def forked_subprocess?
37
+ !!@forked_subprocess
38
+ end
39
+
40
+ def mark_forked_subprocess!
41
+ @forked_subprocess = true
42
+ end
43
+
44
+ # A monotonically increasing serial the parent assigns to each forked
45
+ # subprocess. The default `at_fork` builds the worker's command_name from
46
+ # this rather than the OS pid: the serial sequence is the same from one run
47
+ # to the next, so a re-run overwrites the previous run's resultset entries
48
+ # instead of writing uniquely-named ones that pile up (#1171).
49
+ def subprocess_serial
50
+ @subprocess_serial ||= 0
51
+ end
52
+
53
+ def next_subprocess_serial!
54
+ @subprocess_serial = subprocess_serial + 1
55
+ end
56
+
57
+ protected
58
+
59
+ attr_writer :subprocess_serial
60
+ end
61
+ end
@@ -4,46 +4,27 @@ require "English"
4
4
  require "pathname"
5
5
  require_relative "formatter/html_formatter"
6
6
 
7
- # Default configuration. Profiles autoload on first reference via
8
- # `SimpleCov.profiles.fetch_proc`; the unused ones (e.g. "rails")
9
- # never get required unless a user opts in.
10
7
  SimpleCov.configure do
11
8
  formatter SimpleCov::Formatter::HTMLFormatter
12
9
 
13
10
  load_profile "bundler_filter"
14
11
  load_profile "hidden_filter"
15
- # Exclude files outside of SimpleCov.root. Mirrors the early prune done
16
- # by SimpleCov::UselessResultsRemover so the user-facing filter chain
17
- # honors the same boundary; both share the regex.
18
12
  load_profile "root_filter"
19
- # Exclude test framework directories (`test/`, `spec/`, `features/`,
20
- # `autotest/`). The test suite runs 100% of the test files themselves,
21
- # which inflates totals and obscures the application coverage that
22
- # actually matters. Drop with `remove_filter %r{\A(test|features|spec|autotest)/}`
23
- # if you want test files counted (e.g. to surface dead helpers).
24
13
  load_profile "test_frameworks"
25
14
  end
26
15
 
27
- # Gotta stash this a-s-a-p, see the CommandGuesser class and i.e. #110 for further info.
28
- # The program name is kept separately as well: the join below is lossy for a
29
- # path containing a space, and the guesser reads the executable from it.
30
- SimpleCov::CommandGuesser.original_run_command = "#{$PROGRAM_NAME} #{ARGV.join(' ')}"
16
+ # Stashed as early as possible, before rake or test/unit tamper with ARGV
17
+ # (#110). The program name is kept separately as well: the join is lossy for
18
+ # a path containing a space, and the guesser reads the executable from it.
19
+ SimpleCov::CommandGuesser.original_run_command = "#{$PROGRAM_NAME} #{ARGV.join(" ")}"
31
20
  SimpleCov::CommandGuesser.original_program_name = $PROGRAM_NAME
32
21
 
33
- # Autoload config from ~/.simplecov if present
34
22
  require_relative "load_global_config"
35
23
 
36
- # Autoload config from .simplecov if present
37
- # Recurse upwards until we find .simplecov or reach the root directory
38
-
39
24
  config_path = Pathname.new(SimpleCov.root)
40
25
  loop do
41
26
  filename = config_path.join(".simplecov")
42
27
  if filename.exist?
43
- # `.simplecov` is a configuration file; SimpleCov.start calls inside
44
- # it draw a deprecation warning, and tracking still begins for
45
- # backward compatibility. See `SimpleCov.with_dot_simplecov_autoload`
46
- # and issue #581.
47
28
  SimpleCov.with_dot_simplecov_autoload do
48
29
  load filename.to_s
49
30
  rescue LoadError, StandardError => e
@@ -1,45 +1,63 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "configuration_error"
4
+
3
5
  module SimpleCov
4
- # Emits legacy-API deprecation warnings, deduplicated by the source
5
- # location that triggered them. A deprecated method called in a loop
6
- # or a config block re-evaluated once per parallel worker / spec file
7
- # otherwise repeats the same notice until stderr is unreadable. Keying
8
- # on the caller location collapses those repeats to a single line while
9
- # still warning separately about each distinct call site the user needs
10
- # to fix. See issue #1204.
6
+ # Emits legacy-API deprecation warnings, deduplicated by the source location
7
+ # that triggered them. A deprecated method called in a loop, or a config
8
+ # block re-evaluated once per parallel worker, otherwise repeats the same
9
+ # notice until stderr is unreadable. Keying on the caller location collapses
10
+ # those repeats while still warning separately about each distinct call site
11
+ # the user needs to fix (#1204).
11
12
  module Deprecation
12
- module_function
13
-
14
- # Warn about a deprecated API. `message` is the notice without the
15
- # `[DEPRECATION]` tag or location prefix (both are added here).
16
- #
17
- # `location` defaults to the caller of the deprecated method that
18
- # called us every shipped call site is a one-level alias such as
19
- # `track_files`, so the frame two up is the user code. Pass `location:`
20
- # explicitly when the relevant site isn't that frame (e.g. a source
21
- # file and line discovered while parsing).
22
- # `Array(...)` coerces a missing backtrace (nil) to `[]` so `.first`
23
- # yields nil rather than raising — and, unlike `&.`, adds no branch for
24
- # the unreachable no-caller case to the project's 100% coverage target.
25
- def warn(message, location: Array(Kernel.caller(2..2)).first)
26
- # Key on location when we have one (collapses a deprecated call in a
27
- # loop to a single warning); fall back to the message so a missing
28
- # backtrace never silently swallows every notice.
13
+ extend self
14
+
15
+ MODES = %i[warn raise].freeze
16
+
17
+ # The mode lives here rather than on the configuration singleton so that
18
+ # `warn` depends on nothing but itself. `SimpleCov.deprecations` is the
19
+ # front door that writes it, and the CLI, which loads none of the
20
+ # configuration surface, can still warn.
21
+ def mode
22
+ @mode ||= :warn
23
+ end
24
+
25
+ def mode=(mode)
26
+ raise ConfigurationError, "deprecations takes :warn or :raise, got #{mode.inspect}" unless MODES.include?(mode)
27
+
28
+ @mode = mode
29
+ end
30
+
31
+ # `message` is the notice without the `[DEPRECATION]` tag or location
32
+ # prefix, both of which are added here. `deprecations :raise` turns every
33
+ # deprecated API into an error before the dedup: an error is not a notice to
34
+ # collapse, and a CI guard must fail on the second offender as surely as on
35
+ # the first. A missing backtrace falls back to keying on the message, so it
36
+ # never silently swallows every notice.
37
+ def warn(message, location: caller_location)
38
+ raise ConfigurationError, message if mode.equal?(:raise)
39
+
29
40
  return unless emitted.add?(location || message)
30
41
 
31
42
  Kernel.warn "#{"#{location}: " if location}[DEPRECATION] #{message}"
32
43
  end
33
44
 
34
- # Already-emitted dedup keys for this process. Parallel workers are
35
- # separate processes with their own set, so each warns at most once.
45
+ # Every shipped call site is a one-level alias such as `track_files`, so the
46
+ # frame three up from here is the user's own code. `Array(...)` coerces a
47
+ # missing backtrace to `[]` so `.first` yields nil rather than raising.
48
+ def caller_location
49
+ Array(Kernel.caller(3..3)).first
50
+ end
51
+
52
+ # Parallel workers are separate processes with their own set, so each warns
53
+ # at most once.
36
54
  def emitted
37
55
  @emitted ||= Set.new
38
56
  end
39
57
 
40
- # @api private — reset emitted state between tests.
41
58
  def reset!
42
59
  @emitted = Set.new
60
+ @mode = :warn
43
61
  end
44
62
  end
45
63
  end