simplecov 0.22.0 → 1.3.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 (210) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +173 -875
  4. data/exe/simplecov +8 -0
  5. data/lib/minitest/simplecov_plugin.rb +18 -7
  6. data/lib/simplecov/atomic_file.rb +67 -0
  7. data/lib/simplecov/autostart.rb +9 -0
  8. data/lib/simplecov/baseline/parser.rb +58 -0
  9. data/lib/simplecov/baseline.rb +170 -0
  10. data/lib/simplecov/cli/affected/changed_files.rb +57 -0
  11. data/lib/simplecov/cli/affected/selection.rb +117 -0
  12. data/lib/simplecov/cli/affected.rb +148 -0
  13. data/lib/simplecov/cli/annotations.rb +166 -0
  14. data/lib/simplecov/cli/badge/svg.rb +70 -0
  15. data/lib/simplecov/cli/badge.rb +62 -0
  16. data/lib/simplecov/cli/clean.rb +86 -0
  17. data/lib/simplecov/cli/command_helpers.rb +91 -0
  18. data/lib/simplecov/cli/completions/scripts.rb +114 -0
  19. data/lib/simplecov/cli/completions.rb +83 -0
  20. data/lib/simplecov/cli/coverage.rb +83 -0
  21. data/lib/simplecov/cli/coverage_file.rb +109 -0
  22. data/lib/simplecov/cli/dead_code/output.rb +100 -0
  23. data/lib/simplecov/cli/dead_code.rb +139 -0
  24. data/lib/simplecov/cli/diff/output.rb +45 -0
  25. data/lib/simplecov/cli/diff.rb +115 -0
  26. data/lib/simplecov/cli/dotfile.rb +156 -0
  27. data/lib/simplecov/cli/git.rb +41 -0
  28. data/lib/simplecov/cli/history/output.rb +135 -0
  29. data/lib/simplecov/cli/history.rb +67 -0
  30. data/lib/simplecov/cli/merge.rb +115 -0
  31. data/lib/simplecov/cli/open.rb +45 -0
  32. data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
  33. data/lib/simplecov/cli/patch/output.rb +151 -0
  34. data/lib/simplecov/cli/patch.rb +175 -0
  35. data/lib/simplecov/cli/ratchet/output.rb +44 -0
  36. data/lib/simplecov/cli/ratchet.rb +83 -0
  37. data/lib/simplecov/cli/report.rb +91 -0
  38. data/lib/simplecov/cli/run.rb +35 -0
  39. data/lib/simplecov/cli/serve/report_preparer.rb +30 -0
  40. data/lib/simplecov/cli/serve/static_file_handler.rb +128 -0
  41. data/lib/simplecov/cli/serve.rb +88 -0
  42. data/lib/simplecov/cli/show/annotator.rb +87 -0
  43. data/lib/simplecov/cli/show/sweep.rb +36 -0
  44. data/lib/simplecov/cli/show.rb +133 -0
  45. data/lib/simplecov/cli/status/facts.rb +87 -0
  46. data/lib/simplecov/cli/status.rb +93 -0
  47. data/lib/simplecov/cli/tests/redundancy.rb +78 -0
  48. data/lib/simplecov/cli/tests.rb +148 -0
  49. data/lib/simplecov/cli/uncovered/misses.rb +33 -0
  50. data/lib/simplecov/cli/uncovered.rb +127 -0
  51. data/lib/simplecov/cli/usage.rb +153 -0
  52. data/lib/simplecov/cli/watch/live_report.rb +73 -0
  53. data/lib/simplecov/cli/watch/narrator.rb +55 -0
  54. data/lib/simplecov/cli/watch/poller.rb +45 -0
  55. data/lib/simplecov/cli/watch/session.rb +134 -0
  56. data/lib/simplecov/cli/watch/test_plan.rb +43 -0
  57. data/lib/simplecov/cli/watch.rb +85 -0
  58. data/lib/simplecov/cli.rb +114 -0
  59. data/lib/simplecov/color.rb +65 -0
  60. data/lib/simplecov/combine/branches_combiner.rb +44 -21
  61. data/lib/simplecov/combine/coverage_accumulator.rb +175 -0
  62. data/lib/simplecov/combine/identity_interner.rb +27 -0
  63. data/lib/simplecov/combine/interned_counts.rb +28 -0
  64. data/lib/simplecov/combine/lines_combiner.rb +35 -29
  65. data/lib/simplecov/combine/methods_combiner.rb +48 -0
  66. data/lib/simplecov/combine/results_combiner.rb +9 -49
  67. data/lib/simplecov/combine.rb +0 -24
  68. data/lib/simplecov/command_guesser.rb +99 -42
  69. data/lib/simplecov/configuration/baseline.rb +31 -0
  70. data/lib/simplecov/configuration/coverage.rb +184 -0
  71. data/lib/simplecov/configuration/coverage_criteria.rb +143 -0
  72. data/lib/simplecov/configuration/deprecations.rb +21 -0
  73. data/lib/simplecov/configuration/eval_coverage.rb +37 -0
  74. data/lib/simplecov/configuration/filters.rb +131 -0
  75. data/lib/simplecov/configuration/formatting.rb +129 -0
  76. data/lib/simplecov/configuration/groups.rb +48 -0
  77. data/lib/simplecov/configuration/history.rb +44 -0
  78. data/lib/simplecov/configuration/ignored_entries.rb +72 -0
  79. data/lib/simplecov/configuration/merging.rb +197 -0
  80. data/lib/simplecov/configuration/missed_caps.rb +51 -0
  81. data/lib/simplecov/configuration/production.rb +23 -0
  82. data/lib/simplecov/configuration/test_tracking.rb +48 -0
  83. data/lib/simplecov/configuration/thresholds.rb +150 -0
  84. data/lib/simplecov/configuration/view_coverage.rb +37 -0
  85. data/lib/simplecov/configuration.rb +100 -443
  86. data/lib/simplecov/configuration_error.rb +5 -0
  87. data/lib/simplecov/context_map/union.rb +81 -0
  88. data/lib/simplecov/context_map.rb +187 -0
  89. data/lib/simplecov/coverage_json.rb +21 -0
  90. data/lib/simplecov/coverage_statistics.rb +24 -28
  91. data/lib/simplecov/coverage_violations.rb +254 -0
  92. data/lib/simplecov/current_run.rb +61 -0
  93. data/lib/simplecov/defaults.rb +20 -28
  94. data/lib/simplecov/deprecation.rb +63 -0
  95. data/lib/simplecov/directive/erb.rb +44 -0
  96. data/lib/simplecov/directive/haml.rb +33 -0
  97. data/lib/simplecov/directive/indented_template.rb +37 -0
  98. data/lib/simplecov/directive/slim.rb +35 -0
  99. data/lib/simplecov/directive/template.rb +29 -0
  100. data/lib/simplecov/directive.rb +152 -0
  101. data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
  102. data/lib/simplecov/exit_codes/check.rb +39 -0
  103. data/lib/simplecov/exit_codes/exit_code_handling.rb +24 -5
  104. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +17 -67
  105. data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
  106. data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
  107. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +27 -0
  108. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +20 -38
  109. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +28 -0
  110. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +37 -34
  111. data/lib/simplecov/exit_codes.rb +26 -0
  112. data/lib/simplecov/exit_handling.rb +163 -0
  113. data/lib/simplecov/file_list.rb +63 -48
  114. data/lib/simplecov/filter.rb +98 -42
  115. data/lib/simplecov/formatter/base.rb +100 -0
  116. data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
  117. data/lib/simplecov/formatter/coverage_json_writer.rb +95 -0
  118. data/lib/simplecov/formatter/html_formatter/public/index.html +93 -0
  119. data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +153 -0
  120. data/lib/simplecov/formatter/html_formatter.rb +88 -0
  121. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +122 -0
  122. data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
  123. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +132 -0
  124. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +97 -0
  125. data/lib/simplecov/formatter/json_formatter.rb +40 -0
  126. data/lib/simplecov/formatter/multi_formatter.rb +8 -10
  127. data/lib/simplecov/formatter/simple_formatter.rb +12 -15
  128. data/lib/simplecov/formatter.rb +21 -1
  129. data/lib/simplecov/group_names.rb +33 -0
  130. data/lib/simplecov/history.rb +115 -0
  131. data/lib/simplecov/last_run.rb +16 -5
  132. data/lib/simplecov/lines_classifier.rb +67 -19
  133. data/lib/simplecov/load_global_config.rb +12 -4
  134. data/lib/simplecov/parallel_adapters/base.rb +50 -0
  135. data/lib/simplecov/parallel_adapters/generic.rb +33 -0
  136. data/lib/simplecov/parallel_adapters/parallel_tests.rb +72 -0
  137. data/lib/simplecov/parallel_adapters.rb +54 -0
  138. data/lib/simplecov/parallel_coordination.rb +124 -0
  139. data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
  140. data/lib/simplecov/parallel_result_merger.rb +171 -0
  141. data/lib/simplecov/process.rb +34 -12
  142. data/lib/simplecov/production/error.rb +11 -0
  143. data/lib/simplecov/production/file_sink.rb +130 -0
  144. data/lib/simplecov/production.rb +249 -0
  145. data/lib/simplecov/profiles/bundler_filter.rb +1 -1
  146. data/lib/simplecov/profiles/hidden_filter.rb +1 -1
  147. data/lib/simplecov/profiles/rails.rb +13 -10
  148. data/lib/simplecov/profiles/root_filter.rb +2 -5
  149. data/lib/simplecov/profiles/strict.rb +22 -0
  150. data/lib/simplecov/profiles/test_frameworks.rb +1 -4
  151. data/lib/simplecov/profiles.rb +26 -19
  152. data/lib/simplecov/report_deferral.rb +45 -0
  153. data/lib/simplecov/report_stamp.rb +25 -0
  154. data/lib/simplecov/result/filter_config.rb +19 -0
  155. data/lib/simplecov/result/missing_source_files_reporter.rb +48 -0
  156. data/lib/simplecov/result/serialization.rb +43 -0
  157. data/lib/simplecov/result/source_file_builder.rb +46 -0
  158. data/lib/simplecov/result.rb +123 -47
  159. data/lib/simplecov/result_adapter.rb +130 -11
  160. data/lib/simplecov/result_merger/contexts.rb +22 -0
  161. data/lib/simplecov/result_merger/legacy_format_adapter.rb +24 -0
  162. data/lib/simplecov/result_merger/resultset_file.rb +67 -0
  163. data/lib/simplecov/result_merger/resultset_run_identity.rb +65 -0
  164. data/lib/simplecov/result_merger/resultset_store.rb +62 -0
  165. data/lib/simplecov/result_merger/unloaded_files.rb +95 -0
  166. data/lib/simplecov/result_merger.rb +120 -124
  167. data/lib/simplecov/result_processing.rb +188 -0
  168. data/lib/simplecov/run_identity.rb +75 -0
  169. data/lib/simplecov/simulate_coverage.rb +72 -17
  170. data/lib/simplecov/source_file/branch.rb +8 -35
  171. data/lib/simplecov/source_file/branch_builder.rb +95 -0
  172. data/lib/simplecov/source_file/builder_context.rb +28 -0
  173. data/lib/simplecov/source_file/line.rb +17 -32
  174. data/lib/simplecov/source_file/line_builder.rb +37 -0
  175. data/lib/simplecov/source_file/method.rb +54 -0
  176. data/lib/simplecov/source_file/method_builder.rb +51 -0
  177. data/lib/simplecov/source_file/ruby_data_parser.rb +103 -0
  178. data/lib/simplecov/source_file/skip_chunks.rb +72 -0
  179. data/lib/simplecov/source_file/source_loader.rb +89 -0
  180. data/lib/simplecov/source_file/statistics.rb +76 -0
  181. data/lib/simplecov/source_file.rb +64 -260
  182. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +155 -0
  183. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +191 -0
  184. data/lib/simplecov/static_coverage_extractor/method_collector.rb +45 -0
  185. data/lib/simplecov/static_coverage_extractor/prism_compat.rb +46 -0
  186. data/lib/simplecov/static_coverage_extractor/value_position.rb +61 -0
  187. data/lib/simplecov/static_coverage_extractor/visitor.rb +163 -0
  188. data/lib/simplecov/static_coverage_extractor.rb +78 -0
  189. data/lib/simplecov/test_tracker/accessors.rb +34 -0
  190. data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
  191. data/lib/simplecov/test_tracker/delta.rb +69 -0
  192. data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
  193. data/lib/simplecov/test_tracker.rb +162 -0
  194. data/lib/simplecov/unloaded_file_injector.rb +62 -0
  195. data/lib/simplecov/useless_results_remover.rb +17 -7
  196. data/lib/simplecov/version.rb +1 -1
  197. data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
  198. data/lib/simplecov/view_coverage.rb +55 -0
  199. data/lib/simplecov-html.rb +5 -0
  200. data/lib/simplecov.rb +143 -394
  201. data/lib/simplecov_json_formatter.rb +5 -0
  202. data/man/simplecov.1 +354 -0
  203. data/sig/simplecov.rbs +1710 -0
  204. metadata +183 -61
  205. data/CHANGELOG.md +0 -191
  206. data/doc/alternate-formatters.md +0 -71
  207. data/doc/commercial-services.md +0 -25
  208. data/doc/editor-integration.md +0 -18
  209. data/lib/simplecov/combine/files_combiner.rb +0 -24
  210. data/lib/simplecov/default_formatter.rb +0 -20
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module ExitCodes
5
+ class MaximumMissedPerFileCheck < Check
6
+ def initialize(result, maximum_missed_per_file, overrides = {}, baseline: nil)
7
+ super(result, maximum_missed_per_file)
8
+ @overrides = overrides
9
+ @baseline = baseline
10
+ end
11
+
12
+ def exit_code
13
+ MINIMUM_COVERAGE
14
+ end
15
+
16
+ private
17
+
18
+ def compute_violations
19
+ CoverageViolations.maximum_missed_by_file(result, thresholds, @overrides, baseline: @baseline)
20
+ end
21
+
22
+ def violation_lines(violation)
23
+ [format(
24
+ "Missed %<units>s (%<actual>d) exceed the configured maximum_missed_per_file (%<maximum>d) " \
25
+ "in %<filename>s.",
26
+ units: UNITS.fetch(SimpleCov.coverage_statistics_key(violation.fetch(:criterion))),
27
+ actual: violation.fetch(:actual),
28
+ maximum: violation.fetch(:maximum),
29
+ filename: violation.fetch(:project_filename)
30
+ )]
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module ExitCodes
5
+ class MaximumOverallCoverageCheck < Check
6
+ def exit_code
7
+ MAXIMUM_COVERAGE
8
+ end
9
+
10
+ private
11
+
12
+ def compute_violations
13
+ CoverageViolations.maximum_overall(result, thresholds)
14
+ end
15
+
16
+ def violation_lines(violation)
17
+ [format(
18
+ "%<criterion>s coverage (%<actual>s) is above the expected maximum coverage (%<expected>.2f%%). " \
19
+ "Time to bump the threshold!",
20
+ criterion: violation.fetch(:criterion).capitalize,
21
+ actual: Color.colorize_percent(violation.fetch(:actual)),
22
+ expected: violation.fetch(:expected)
23
+ )]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -2,52 +2,34 @@
2
2
 
3
3
  module SimpleCov
4
4
  module ExitCodes
5
- class MinimumCoverageByFileCheck
6
- def initialize(result, minimum_coverage_by_file)
7
- @result = result
8
- @minimum_coverage_by_file = minimum_coverage_by_file
9
- end
10
-
11
- def failing?
12
- minimum_violations.any?
13
- end
14
-
15
- def report
16
- minimum_violations.each do |violation|
17
- $stderr.printf(
18
- "%<criterion>s coverage by file (%<covered>.2f%%) is below the expected minimum coverage (%<minimum_coverage>.2f%%).\n",
19
- covered: SimpleCov.round_coverage(violation.fetch(:actual)),
20
- minimum_coverage: violation.fetch(:minimum_expected),
21
- criterion: violation.fetch(:criterion).capitalize
22
- )
23
- end
5
+ class MinimumCoverageByFileCheck < Check
6
+ # `baseline:` exempts the file-and-criterion pairs it covers: those answer to
7
+ # their own floor via `BaselineCheck` instead.
8
+ def initialize(result, minimum_coverage_by_file, overrides = {}, baseline: nil)
9
+ super(result, minimum_coverage_by_file)
10
+ @overrides = overrides
11
+ @baseline = baseline
24
12
  end
25
13
 
26
14
  def exit_code
27
- SimpleCov::ExitCodes::MINIMUM_COVERAGE
15
+ MINIMUM_COVERAGE
28
16
  end
29
17
 
30
- private
31
-
32
- attr_reader :result, :minimum_coverage_by_file
18
+ private
33
19
 
34
- def minimum_violations
35
- @minimum_violations ||=
36
- compute_minimum_coverage_data.select do |achieved|
37
- achieved.fetch(:actual) < achieved.fetch(:minimum_expected)
38
- end
20
+ def compute_violations
21
+ CoverageViolations.minimum_by_file(result, thresholds, @overrides, baseline: @baseline)
39
22
  end
40
23
 
41
- def compute_minimum_coverage_data
42
- minimum_coverage_by_file.flat_map do |criterion, expected_percent|
43
- result.coverage_statistics_by_file.fetch(criterion).map do |actual_coverage|
44
- {
45
- criterion: criterion,
46
- minimum_expected: expected_percent,
47
- actual: SimpleCov.round_coverage(actual_coverage.percent)
48
- }
49
- end
50
- end
24
+ def violation_lines(violation)
25
+ [format(
26
+ "%<criterion>s coverage by file (%<actual>s) is below the expected minimum coverage " \
27
+ "(%<expected>.2f%%) in %<filename>s.",
28
+ criterion: violation.fetch(:criterion).capitalize,
29
+ actual: Color.colorize_percent(violation.fetch(:actual)),
30
+ expected: violation.fetch(:expected),
31
+ filename: violation.fetch(:project_filename)
32
+ )]
51
33
  end
52
34
  end
53
35
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module ExitCodes
5
+ class MinimumCoverageByGroupCheck < Check
6
+ def exit_code
7
+ MINIMUM_COVERAGE
8
+ end
9
+
10
+ private
11
+
12
+ def compute_violations
13
+ CoverageViolations.minimum_by_group(result, thresholds)
14
+ end
15
+
16
+ def violation_lines(violation)
17
+ [format(
18
+ "%<criterion>s coverage by group (%<actual>s) is below the expected minimum coverage " \
19
+ "(%<expected>.2f%%) in %<group_name>s.",
20
+ criterion: violation.fetch(:criterion).capitalize,
21
+ actual: Color.colorize_percent(violation.fetch(:actual)),
22
+ expected: violation.fetch(:expected),
23
+ group_name: violation.fetch(:group_name)
24
+ )]
25
+ end
26
+ end
27
+ end
28
+ end
@@ -2,51 +2,54 @@
2
2
 
3
3
  module SimpleCov
4
4
  module ExitCodes
5
- class MinimumOverallCoverageCheck
6
- def initialize(result, minimum_coverage)
7
- @result = result
8
- @minimum_coverage = minimum_coverage
5
+ class MinimumOverallCoverageCheck < Check
6
+ def exit_code
7
+ MINIMUM_COVERAGE
9
8
  end
10
9
 
11
- def failing?
12
- minimum_violations.any?
13
- end
10
+ private
14
11
 
15
- def report
16
- minimum_violations.each do |violation|
17
- $stderr.printf(
18
- "%<criterion>s coverage (%<covered>.2f%%) is below the expected minimum coverage (%<minimum_coverage>.2f%%).\n",
19
- covered: SimpleCov.round_coverage(violation.fetch(:actual)),
20
- minimum_coverage: violation.fetch(:minimum_expected),
21
- criterion: violation.fetch(:criterion).capitalize
22
- )
23
- end
24
- end
12
+ WORST_FILES_LIMIT = 5
13
+ private_constant :WORST_FILES_LIMIT
25
14
 
26
- def exit_code
27
- SimpleCov::ExitCodes::MINIMUM_COVERAGE
15
+ def compute_violations
16
+ CoverageViolations.minimum_overall(result, thresholds)
28
17
  end
29
18
 
30
- private
31
-
32
- attr_reader :result, :minimum_coverage
33
-
34
- def minimum_violations
35
- @minimum_violations ||= calculate_minimum_violations
19
+ def violation_lines(violation)
20
+ criterion = violation.fetch(:criterion)
21
+ headline = format(
22
+ "%<criterion>s coverage (%<actual>s) is below the expected minimum coverage (%<expected>.2f%%).",
23
+ criterion: criterion.capitalize,
24
+ actual: Color.colorize_percent(violation.fetch(:actual)),
25
+ expected: violation.fetch(:expected)
26
+ )
27
+ [headline, *worst_files_lines(criterion)]
36
28
  end
37
29
 
38
- def calculate_minimum_violations
39
- coverage_achieved = minimum_coverage.map do |criterion, percent|
40
- {
41
- criterion: criterion,
42
- minimum_expected: percent,
43
- actual: result.coverage_statistics.fetch(criterion).percent
44
- }
30
+ def worst_files_lines(criterion)
31
+ worst = worst_files_for(criterion)
32
+ return [] if worst.empty?
33
+
34
+ [" Lowest-coverage files (#{criterion}):"] + worst.map do |path, percent|
35
+ format(
36
+ " %<percent>s %<path>s",
37
+ percent: Color.colorize_percent(percent, format("%6.2f%%", percent)),
38
+ path: path
39
+ )
45
40
  end
41
+ end
46
42
 
47
- coverage_achieved.select do |achieved|
48
- achieved.fetch(:actual) < achieved.fetch(:minimum_expected)
43
+ # The list is a hint at where to add tests, so a fully covered file has no
44
+ # business on it: without the filter, files at 100% padded the list out to
45
+ # its five entries (#1286).
46
+ def worst_files_for(criterion)
47
+ stats_key = SimpleCov.coverage_statistics_key(criterion)
48
+ with_stats = result.files.filter_map do |source_file|
49
+ stats = source_file.coverage_statistics[stats_key]
50
+ [source_file.project_filename, stats.percent] if stats && stats.percent < 100
49
51
  end
52
+ with_stats.sort_by { |_path, percent| percent }.first(WORST_FILES_LIMIT)
50
53
  end
51
54
  end
52
55
  end
@@ -6,10 +6,36 @@ module SimpleCov
6
6
  EXCEPTION = 1
7
7
  MINIMUM_COVERAGE = 2
8
8
  MAXIMUM_COVERAGE_DROP = 3
9
+ MAXIMUM_COVERAGE = 4
10
+
11
+ # The plural unit each criterion's misses count in, for the checks that report
12
+ # counts rather than percents.
13
+ UNITS = {line: "lines", branch: "branches", method: "methods"}.freeze
14
+
15
+ # Threshold-violation reports and exit-status notices are the output of the
16
+ # enforcement feature, not Ruby warnings: routing them through `Kernel#warn`
17
+ # made `-W0` swallow the explanation for a failing exit code, let
18
+ # `Warning.warn` hooks intercept them mid-at_exit, and fed colorized text to
19
+ # warning logs. `print_errors false` remains the intended opt-out.
20
+ #
21
+ # A parallel runner can close a worker's stderr before its at_exit hooks
22
+ # run, hence the rescue.
23
+ def self.print_error(message)
24
+ $stderr.puts message # rubocop:disable Style/StderrPuts
25
+ rescue IOError
26
+ # The violation still has to set the exit status even when its
27
+ # explanation cannot be printed.
28
+ end
9
29
  end
10
30
  end
11
31
 
32
+ require_relative "exit_codes/check"
33
+ require_relative "exit_codes/baseline_check"
12
34
  require_relative "exit_codes/exit_code_handling"
13
35
  require_relative "exit_codes/maximum_coverage_drop_check"
36
+ require_relative "exit_codes/maximum_missed_check"
37
+ require_relative "exit_codes/maximum_missed_per_file_check"
38
+ require_relative "exit_codes/maximum_overall_coverage_check"
14
39
  require_relative "exit_codes/minimum_coverage_by_file_check"
40
+ require_relative "exit_codes/minimum_coverage_by_group_check"
15
41
  require_relative "exit_codes/minimum_overall_coverage_check"
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "English"
4
+
5
+ #
6
+ # `at_exit` orchestration: post-suite report generation, threshold checks,
7
+ # deferral when a sibling subprocess already wrote a fresher report, and
8
+ # exit-status propagation.
9
+ #
10
+ module SimpleCov
11
+ class << self
12
+ # @api private
13
+ CoverageLimits = Data.define(
14
+ :minimum_coverage,
15
+ :minimum_coverage_by_file,
16
+ :minimum_coverage_by_file_overrides,
17
+ :minimum_coverage_by_group,
18
+ :maximum_coverage,
19
+ :maximum_coverage_drop,
20
+ :maximum_missed,
21
+ :maximum_missed_per_file,
22
+ :maximum_missed_per_file_overrides,
23
+ :baseline
24
+ )
25
+
26
+ def at_exit_behavior
27
+ # A different process than the one that called start; don't interfere.
28
+ return unless started_in_this_process?
29
+
30
+ # Coverage is no longer running (someone stopped it manually, or a test
31
+ # consumed the result), so don't run exit tasks.
32
+ return unless Coverage.running?
33
+
34
+ # Captured BEFORE the deferral probe: its freshness check rescues
35
+ # filesystem errors, and completing any rescue inside an at_exit handler
36
+ # resets $ERROR_INFO to nil.
37
+ error_exit_status = exit_status_from_exception
38
+
39
+ # Stand down when we'd only clobber a fresher report (#581).
40
+ return if defer_to_existing_report?
41
+
42
+ run_exit_tasks!(error_exit_status)
43
+ end
44
+
45
+ # @api private -- everything the end of a measured run does, answered as the
46
+ # exit status the process should end with rather than performed as an exit.
47
+ # The one side effect left to the caller is ending the process, which is
48
+ # what makes this whole path answerable from inside a test.
49
+ def run_exit_tasks(error_exit_status = exit_status_from_exception)
50
+ at_exit.call
51
+
52
+ if previous_error?(error_exit_status)
53
+ report_previous_error
54
+ error_exit_status
55
+ elsif ready_to_process_results?
56
+ report_processing_failure(process_result(result))
57
+ else
58
+ ExitCodes::SUCCESS
59
+ end
60
+ end
61
+
62
+ # @api private -- the exiting adapter over `run_exit_tasks`, called from the
63
+ # at_exit block (which pre-captures the status, see there) and by `collate`.
64
+ def run_exit_tasks!(error_exit_status = exit_status_from_exception)
65
+ status = run_exit_tasks(error_exit_status)
66
+
67
+ # Force exit with stored status (#5).
68
+ Kernel.exit(status) unless successful?(status)
69
+ end
70
+
71
+ # @api private
72
+ def exit_status_from_exception
73
+ @exit_exception = $ERROR_INFO
74
+ return nil unless @exit_exception
75
+
76
+ if @exit_exception.is_a?(SystemExit)
77
+ @exit_exception.status
78
+ else
79
+ ExitCodes::EXCEPTION
80
+ end
81
+ end
82
+
83
+ # @api private -- strict boolean so rspec-mocks 4's predicate matcher
84
+ # accepts it. test_unit sets status 0 on success, so SUCCESS must also be
85
+ # treated as "not a previous error".
86
+ def previous_error?(error_exit_status)
87
+ return false unless error_exit_status
88
+
89
+ !successful?(error_exit_status)
90
+ end
91
+
92
+ # @api private -- the notice alone; the status it accompanies is the
93
+ # caller's to answer with.
94
+ def report_previous_error
95
+ return unless print_errors
96
+
97
+ ExitCodes.print_error Color.colorize(
98
+ "Stopped processing SimpleCov as a previous error not related to SimpleCov has been detected",
99
+ :yellow
100
+ )
101
+ end
102
+
103
+ # @api private. The process that owns final merge processing is the only one
104
+ # that reports against thresholds, and only when its
105
+ # `wait_for_other_processes` confirmed every sibling reported. When the wait
106
+ # times out the merged total is partial, and comparing it against the
107
+ # thresholds would surface a spurious violation about the missing slice.
108
+ def ready_to_process_results?
109
+ merge_finalization_owner? && result? &&
110
+ (collating_result? || parallel_results_complete?)
111
+ end
112
+
113
+ # @api private -- answers the status it was handed, explaining it first when
114
+ # it is a coverage failure worth explaining.
115
+ def report_processing_failure(exit_status)
116
+ if exit_status.positive? && print_errors
117
+ ExitCodes.print_error Color.colorize(
118
+ "SimpleCov failed with exit #{exit_status} due to a coverage related error", :red
119
+ )
120
+ end
121
+ exit_status
122
+ end
123
+
124
+ # @api private. The history entry is recorded on the same successful-run
125
+ # condition `.last_run.json` uses, so a run that failed its thresholds
126
+ # becomes neither the drop baseline nor a data point in the trend.
127
+ def process_result(result)
128
+ result_exit_status = result_exit_status(result)
129
+ if successful?(result_exit_status)
130
+ write_last_run(result)
131
+ History.record(result)
132
+ end
133
+ result_exit_status
134
+ end
135
+
136
+ # mutant:disable -- a pid is an Integer, and Integers answer ==, eql? and
137
+ # equal? alike for the same value.
138
+ def started_in_this_process?
139
+ SimpleCov.pid == Process.pid
140
+ end
141
+
142
+ # Asked as a predicate rather than compared against `ExitCodes::SUCCESS`,
143
+ # because two equal Integers are equal through every spelling of the
144
+ # comparison.
145
+ def successful?(status)
146
+ status.zero?
147
+ end
148
+
149
+ def result_exit_status(result)
150
+ ExitCodes::ExitCodeHandling.call(result, coverage_limits: build_coverage_limits)
151
+ end
152
+
153
+ private
154
+
155
+ # Every CoverageLimits member is named after the configuration reader that
156
+ # supplies it, so a new limit only has to be added to the Data definition
157
+ # and the checks.
158
+ def build_coverage_limits
159
+ limits = CoverageLimits.members.to_h { |name| [name, public_send(name)] } #: Hash[Symbol, untyped]
160
+ CoverageLimits.new(**limits)
161
+ end
162
+ end
163
+ end
@@ -1,99 +1,79 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleCov
4
- # An array of SimpleCov SourceFile instances with additional collection helper
5
- # methods for calculating coverage across them etc.
6
4
  class FileList
7
5
  include Enumerable
8
6
  extend Forwardable
9
7
 
10
- def_delegators :@files,
11
- # For Enumerable
12
- :each,
13
- # also delegating methods implemented in Enumerable as they have
14
- # custom Array implementations which are presumably better/more
15
- # resource efficient
16
- :size, :map, :count,
17
- # surprisingly not in Enumerable
18
- :empty?, :length,
19
- # still act like we're kinda an array
20
- :to_a, :to_ary
8
+ # Enumerable's own implementations are skipped where Array has a better
9
+ # one, and `to_a` / `to_ary` keep this acting like the array it wraps.
10
+ def_delegators :@files, :each, :size, :map, :count, :empty?, :length, :to_a, :to_ary
21
11
 
22
12
  def initialize(files)
23
13
  @files = files
24
14
  end
25
15
 
26
- def coverage_statistics
27
- @coverage_statistics ||= compute_coverage_statistics
16
+ # With no argument answers the `{line:, branch:, method:}` Hash, and with a
17
+ # criterion symbol that one CoverageStatistics.
18
+ def coverage_statistics(criterion = nil)
19
+ stats = (@coverage_statistics ||= compute_coverage_statistics)
20
+ criterion ? stats[criterion] : stats
28
21
  end
29
22
 
30
23
  def coverage_statistics_by_file
31
24
  @coverage_statistics_by_file ||= compute_coverage_statistics_by_file
32
25
  end
33
26
 
34
- # Returns the count of lines that have coverage
35
27
  def covered_lines
36
28
  coverage_statistics[:line]&.covered
37
29
  end
38
30
 
39
- # Returns the count of lines that have been missed
40
31
  def missed_lines
41
32
  coverage_statistics[:line]&.missed
42
33
  end
43
34
 
44
- # Returns the count of lines that are not relevant for coverage
45
35
  def never_lines
46
- return 0.0 if empty?
47
-
48
- map { |f| f.never_lines.count }.inject(:+)
36
+ sum { |f| f.never_lines.size }
49
37
  end
50
38
 
51
- # Returns the count of skipped lines
52
39
  def skipped_lines
53
- return 0.0 if empty?
54
-
55
- map { |f| f.skipped_lines.count }.inject(:+)
40
+ sum { |f| f.skipped_lines.size }
56
41
  end
57
42
 
58
- # Computes the coverage based upon lines covered and lines missed for each file
59
- # Returns an array with all coverage percentages
60
43
  def covered_percentages
61
44
  map(&:covered_percent)
62
45
  end
63
46
 
64
- # Finds the least covered file and returns that file's name
47
+ # Nil for an empty list, such as a fully filtered result.
65
48
  def least_covered_file
66
- min_by(&:covered_percent).filename
49
+ # `covered_percent` is nil only for an unmeasured criterion, and :line
50
+ # is always measured, so the `|| 0.0` arm never fires at runtime; it
51
+ # exists to satisfy min_by's Comparable requirement.
52
+ min_by { |file| file.covered_percent || 0.0 }&.filename
67
53
  end
68
54
 
69
- # Returns the overall amount of relevant lines of code across all files in this list
70
55
  def lines_of_code
71
56
  coverage_statistics[:line]&.total
72
57
  end
73
58
 
74
- # Computes the coverage based upon lines covered and lines missed
75
- # @return [Float]
76
- def covered_percent
77
- coverage_statistics[:line]&.percent
59
+ # Nil if the criterion was not measured.
60
+ def covered_percent(criterion = :line)
61
+ coverage_statistics(criterion)&.percent
78
62
  end
79
63
 
80
- # Computes the strength (hits / line) based upon lines covered and lines missed
81
- # @return [Float]
82
- def covered_strength
83
- coverage_statistics[:line]&.strength
64
+ # The strength (average hits per relevant unit) for the given criterion.
65
+ def covered_strength(criterion = :line)
66
+ coverage_statistics(criterion)&.strength
84
67
  end
85
68
 
86
- # Return total count of branches in all files
87
69
  def total_branches
88
70
  coverage_statistics[:branch]&.total
89
71
  end
90
72
 
91
- # Return total count of covered branches
92
73
  def covered_branches
93
74
  coverage_statistics[:branch]&.covered
94
75
  end
95
76
 
96
- # Return total count of covered branches
97
77
  def missed_branches
98
78
  coverage_statistics[:branch]&.missed
99
79
  end
@@ -102,19 +82,54 @@ module SimpleCov
102
82
  coverage_statistics[:branch]&.percent
103
83
  end
104
84
 
105
- private
85
+ def total_methods
86
+ coverage_statistics[:method]&.total
87
+ end
88
+
89
+ def covered_methods
90
+ coverage_statistics[:method]&.covered
91
+ end
106
92
 
93
+ def missed_methods
94
+ coverage_statistics[:method]&.missed
95
+ end
96
+
97
+ def method_covered_percent
98
+ coverage_statistics[:method]&.percent
99
+ end
100
+
101
+ private
102
+
103
+ # Seeded with one entry per criterion the user enabled, so an empty FileList
104
+ # still yields the right shape. `SourceFile#coverage_statistics` always
105
+ # reports all three criteria; FileList is the layer that filters to the
106
+ # enabled set so disabled criteria don't surface in totals, JSON, or the
107
+ # HTML report.
107
108
  def compute_coverage_statistics_by_file
108
- @files.each_with_object(line: [], branch: []) do |file, together|
109
- together[:line] << file.coverage_statistics.fetch(:line)
110
- together[:branch] << file.coverage_statistics.fetch(:branch) if SimpleCov.branch_coverage?
109
+ seed = enabled_criteria_for_reporting.to_h do |criterion|
110
+ bucket = [] #: Array[CoverageStatistics]
111
+ [criterion, bucket]
112
+ end
113
+ each_with_object(seed) do |file, together|
114
+ file.coverage_statistics.each do |criterion, stats|
115
+ together.fetch(criterion) << stats if together.key?(criterion)
116
+ end
111
117
  end
112
118
  end
113
119
 
114
120
  def compute_coverage_statistics
115
- coverage_statistics = {line: CoverageStatistics.from(coverage_statistics_by_file[:line])}
116
- coverage_statistics[:branch] = CoverageStatistics.from(coverage_statistics_by_file[:branch]) if SimpleCov.branch_coverage?
117
- coverage_statistics
121
+ coverage_statistics_by_file.transform_values { |stats| CoverageStatistics.from(stats) }
122
+ end
123
+
124
+ # `:line` (or its `:oneshot_line` synonym) is reported when either criterion
125
+ # is enabled; the JRuby-gated branch/method criteria are reported when they
126
+ # pass their own engine-support check.
127
+ def enabled_criteria_for_reporting
128
+ criteria = [] #: Array[SimpleCov::criterion]
129
+ criteria << :line if SimpleCov.line_coverage?
130
+ criteria << :branch if SimpleCov.branch_coverage?
131
+ criteria << :method if SimpleCov.method_coverage?
132
+ criteria
118
133
  end
119
134
  end
120
135
  end