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
@@ -2,15 +2,8 @@
2
2
 
3
3
  module SimpleCov
4
4
  #
5
- # Base filter class. Inherit from this to create custom filters,
6
- # and overwrite the passes?(source_file) instance method
7
- #
8
- # # A sample class that rejects all source files.
9
- # class StupidFilter < SimpleCov::Filter
10
- # def passes?(source_file)
11
- # false
12
- # end
13
- # end
5
+ # Base filter class. Inherit from this to create custom filters, and
6
+ # override `matches?(source_file)`.
14
7
  #
15
8
  class Filter
16
9
  attr_reader :filter_argument
@@ -20,61 +13,122 @@ module SimpleCov
20
13
  end
21
14
 
22
15
  def matches?(_source_file)
23
- raise "The base filter class is not intended for direct use"
16
+ raise NotImplementedError, "The base filter class is not intended for direct use"
24
17
  end
25
18
 
26
- def passes?(source_file)
27
- warn "#{Kernel.caller.first}: [DEPRECATION] #passes? is deprecated. Use #matches? instead."
28
- matches?(source_file)
19
+ # Whether this filter's verdict depends only on the file's path, so it can
20
+ # be decided before that file has any coverage. Used when recording which
21
+ # tracked files a process did not load. Defaults to false so a custom
22
+ # filter is never guessed at (#1250).
23
+ def path_only?
24
+ false
29
25
  end
30
26
 
31
- def self.build_filter(filter_argument)
32
- return filter_argument if filter_argument.is_a?(SimpleCov::Filter)
33
-
34
- class_for_argument(filter_argument).new(filter_argument)
27
+ # `string_filter` selects the semantics of bare String arguments:
28
+ # StringFilter's segment-substring match for `skip`, GlobFilter for
29
+ # `cover`. It threads through Array elements so a list gets the same
30
+ # treatment as its members.
31
+ def self.build_filter(filter_argument, string_filter: StringFilter)
32
+ case filter_argument
33
+ when Filter then filter_argument
34
+ when String then string_filter.new(filter_argument)
35
+ when Array
36
+ ArrayFilter.new(filter_argument.map { |arg| build_filter(arg, string_filter: string_filter) })
37
+ else class_for_argument(filter_argument).new(filter_argument)
38
+ end
35
39
  end
36
40
 
37
41
  def self.class_for_argument(filter_argument)
38
- case filter_argument
39
- when String
40
- SimpleCov::StringFilter
41
- when Regexp
42
- SimpleCov::RegexFilter
43
- when Array
44
- SimpleCov::ArrayFilter
45
- when Proc
46
- SimpleCov::BlockFilter
42
+ filter_classes_by_argument_type.find { |type, _| filter_argument.is_a?(type) }&.last ||
43
+ raise(ConfigurationError, "You have provided an unrecognized filter type")
44
+ end
45
+
46
+ def self.filter_classes_by_argument_type
47
+ @filter_classes_by_argument_type ||= {
48
+ String => StringFilter,
49
+ Regexp => RegexFilter,
50
+ Array => ArrayFilter,
51
+ Proc => BlockFilter
52
+ }.freeze
53
+ end
54
+ private_class_method :filter_classes_by_argument_type
55
+ end
56
+
57
+ class StringFilter < Filter
58
+ # Matching is path-segment-aware: the argument must appear immediately
59
+ # after a "/" and be followed by "/" or end-of-string, so "lib" matches
60
+ # "/lib/foo.rb" but not "/app/models/library.rb".
61
+ def matches?(source_file)
62
+ source_file.project_filename.match?(segment_pattern)
63
+ end
64
+
65
+ def path_only?
66
+ true
67
+ end
68
+
69
+ private
70
+
71
+ def segment_pattern
72
+ @segment_pattern ||= compute_segment_pattern
73
+ end
74
+
75
+ def compute_segment_pattern
76
+ normalized = filter_argument.delete_prefix("/")
77
+ escaped = Regexp.escape(normalized)
78
+ boundary = '(?:\A|/)'
79
+
80
+ if normalized.end_with?("/")
81
+ # Trailing slash signals directory-only matching.
82
+ /#{boundary}#{escaped}/
83
+ elsif normalized.include?(".") && !normalized.include?("/")
84
+ # Bare filename pattern ("test.rb" matches "faked_test.rb"): allow a
85
+ # substring match within a single path segment. Multi-segment arguments
86
+ # must not get this relaxation, or "app/models/user.rb" would also
87
+ # match "webapp/models/user.rb". No boundary here: `[^/]*` already
88
+ # reaches back to the segment start, so prefixing one matches the same
89
+ # paths.
90
+ %r{[^/]*#{escaped}(?=[/.]|\z)}
47
91
  else
48
- raise ArgumentError, "You have provided an unrecognized filter type"
92
+ # Require a segment-boundary match so "lib" matches "lib/" but not
93
+ # "library/".
94
+ %r{#{boundary}#{escaped}(?=[/.]|\z)}
49
95
  end
50
96
  end
51
97
  end
52
98
 
53
- class StringFilter < SimpleCov::Filter
54
- # Returns true when the given source file's filename matches the
55
- # string configured when initializing this Filter with StringFilter.new('somestring')
99
+ class RegexFilter < Filter
100
+ # `Regexp#match?` rather than `=~` so the predicate returns a real boolean:
101
+ # `=~` would return the match position, which trips rspec-mocks 4's
102
+ # stricter predicate-matcher type check.
56
103
  def matches?(source_file)
57
- source_file.project_filename.include?(filter_argument)
104
+ filter_argument.match?(source_file.project_filename)
105
+ end
106
+
107
+ def path_only?
108
+ true
58
109
  end
59
110
  end
60
111
 
61
- class RegexFilter < SimpleCov::Filter
62
- # Returns true when the given source file's filename matches the
63
- # regex configured when initializing this Filter with RegexFilter.new(/someregex/)
112
+ class BlockFilter < Filter
64
113
  def matches?(source_file)
65
- (source_file.project_filename =~ filter_argument)
114
+ filter_argument.call(source_file)
66
115
  end
67
116
  end
68
117
 
69
- class BlockFilter < SimpleCov::Filter
70
- # Returns true if the block given when initializing this filter with BlockFilter.new {|src_file| ... }
71
- # returns true for the given source file.
118
+ # Matches when the source file's project path matches the configured shell
119
+ # glob. Used by `cover`, and by `skip` when callers want glob semantics
120
+ # instead of `StringFilter`'s substring match.
121
+ class GlobFilter < Filter
72
122
  def matches?(source_file)
73
- filter_argument.call(source_file)
123
+ File.fnmatch?(filter_argument, source_file.project_filename, File::FNM_PATHNAME | File::FNM_EXTGLOB)
124
+ end
125
+
126
+ def path_only?
127
+ true
74
128
  end
75
129
  end
76
130
 
77
- class ArrayFilter < SimpleCov::Filter
131
+ class ArrayFilter < Filter
78
132
  def initialize(filter_argument)
79
133
  filter_objects = filter_argument.map do |arg|
80
134
  Filter.build_filter(arg)
@@ -83,8 +137,10 @@ module SimpleCov
83
137
  super(filter_objects)
84
138
  end
85
139
 
86
- # Returns true if any of the filters in the array match the given source file.
87
- # Configure this Filter like StringFilter.new(['some/path', /^some_regex/, Proc.new {|src_file| ... }])
140
+ def path_only?
141
+ filter_argument.all?(&:path_only?)
142
+ end
143
+
88
144
  def matches?(source_files_list)
89
145
  filter_argument.any? do |arg|
90
146
  arg.matches?(source_files_list)
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module SimpleCov
6
+ module Formatter
7
+ # @api private
8
+ #
9
+ # Shared scaffolding for formatters that write a coverage report to an
10
+ # output directory and emit a "Coverage report generated for X to Y"
11
+ # summary on stderr. Subclasses override `format`, and may override
12
+ # `message_prefix`.
13
+ class Base
14
+ # `output_dir` defaults to `SimpleCov.coverage_path`. Pass it explicitly
15
+ # to write somewhere else.
16
+ def initialize(silent: false, output_dir: nil)
17
+ @silent = silent
18
+ @output_dir = output_dir
19
+ end
20
+
21
+ private
22
+
23
+ # The one home of the "status lines go to stderr, not through warn"
24
+ # decision (#1225). stderr rather than stdout because this is a status
25
+ # message, not the program's output, so it stays out of pipelines like
26
+ # `rspec -f json`. And `$stderr.puts` rather than `warn` so the line
27
+ # neither reaches `Warning.warn` hooks nor vanishes under `-W0`.
28
+ def emit_status(result)
29
+ $stderr.puts output_message(result) unless @silent # rubocop:disable Style/StderrPuts
30
+ rescue IOError
31
+ # A parallel runner can close a worker's stderr before its at_exit hooks
32
+ # run (rspec-conductor does). Losing the status line must not abort the
33
+ # exit tasks that follow report generation.
34
+ end
35
+
36
+ # Subclasses override to prepend a marker (e.g. "JSON ") to the summary
37
+ # line. Empty for the HTML formatter, historically the unmarked default.
38
+ def message_prefix
39
+ ""
40
+ end
41
+
42
+ def output_path
43
+ @output_dir || SimpleCov.coverage_path
44
+ end
45
+
46
+ # The path shown in the "Coverage report generated for X to Y" status line.
47
+ # Rendered relative to cwd when `output_path` lives inside cwd, with the
48
+ # formatter's `entry_point_filename` appended so the line points at a
49
+ # concrete file a terminal can hyperlink. Paths outside cwd stay
50
+ # absolute; a `../../../tmp/cov` display would be more confusing (#197).
51
+ def displayable_output_path
52
+ directory = relative_or_absolute_output_path
53
+ entry_point = entry_point_filename
54
+ entry_point ? File.join(directory, entry_point) : directory
55
+ end
56
+
57
+ def relative_or_absolute_output_path
58
+ absolute = output_path
59
+ relative = Pathname.new(absolute).relative_path_from(Pathname.pwd).to_s
60
+ relative.start_with?("..") ? absolute : relative
61
+ rescue ArgumentError
62
+ # Pathname#relative_path_from raises across mixed absolute/relative
63
+ # inputs, and across Windows drives.
64
+ output_path
65
+ end
66
+
67
+ # Subclasses override to name the report's entry-point file, which gets
68
+ # appended to the directory in the status line. The empty body answers
69
+ # nil, which leaves the bare directory in place for any third-party
70
+ # formatter that has no single canonical entry point.
71
+ def entry_point_filename
72
+ end
73
+
74
+ # One summary line per criterion the run actually measured, in the order
75
+ # of `result.coverage_statistics`, which reflects what the user enabled.
76
+ def output_message(result)
77
+ header = "#{message_prefix}Coverage report generated for #{result.command_name} to #{displayable_output_path}"
78
+ body = result.coverage_statistics.filter_map { |criterion, stat| stats_line(criterion, stat) }
79
+ [header, *body].join("\n")
80
+ end
81
+
82
+ # Returns nil for branch/method criteria that have nothing to measure.
83
+ # Showing "Branch coverage: 0 / 0 (100.00%)" is noise.
84
+ def stats_line(criterion, stat)
85
+ return if !criterion.equal?(:line) && !stat.total.positive?
86
+
87
+ percent = SimpleCov.round_coverage(stat.percent)
88
+ # `Symbol#capitalize` answers a Symbol, which `%<label>s` renders as its
89
+ # capitalized name, so the label needs no `to_s` step of its own.
90
+ Kernel.format(
91
+ "%<label>s coverage: %<covered>d / %<total>d (%<percent>s)",
92
+ label: criterion.capitalize,
93
+ covered: stat.covered,
94
+ total: stat.total,
95
+ percent: Color.colorize_percent(percent)
96
+ )
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../atomic_file"
5
+
6
+ module SimpleCov
7
+ module Formatter
8
+ # Auto-ratchets the per-file coverage baseline as a formatter, for teams
9
+ # that want floors to tighten on every run instead of by deliberate
10
+ # `simplecov ratchet` invocations. The semantics are exactly the CLI's, and
11
+ # the file is rewritten only when a floor actually moved.
12
+ #
13
+ # The exit checks still judge the run against the floors as they were when
14
+ # it started (the baseline is read once per path), and ratcheting never
15
+ # loosens a floor, so tightening before the checks cannot flip a failing
16
+ # file to passing.
17
+ class BaselineFormatter < Base
18
+ def format(result)
19
+ emit_status(ratchet(result))
20
+ end
21
+
22
+ private
23
+
24
+ def ratchet(result)
25
+ current = current_floors(result)
26
+ existing = SimpleCov.baseline
27
+ return generate(current) unless existing
28
+
29
+ outcome = existing.ratchet(current)
30
+ changed = outcome.tightened.any? || outcome.pruned.any?
31
+ write(outcome.baseline) if changed
32
+ note = summary(outcome, changed)
33
+ regressed = outcome.regressed
34
+ regressed.empty? ? note : "#{note}#{below_floors(regressed)}"
35
+ end
36
+
37
+ def generate(current)
38
+ baseline = Baseline.generate(current)
39
+ write(baseline)
40
+ files = baseline.entries.size
41
+ "Coverage baseline generated to #{displayable_output_path} (#{files} #{files.eql?(1) ? "file" : "files"})"
42
+ end
43
+
44
+ def summary(outcome, changed)
45
+ return "Coverage baseline unchanged at #{displayable_output_path}" unless changed
46
+
47
+ "Coverage baseline ratcheted to #{displayable_output_path} " \
48
+ "(#{outcome.tightened.size} tightened, #{outcome.pruned.size} pruned)"
49
+ end
50
+
51
+ def below_floors(regressed)
52
+ noun = regressed.size.eql?(1) ? "1 file below its floor" : "#{regressed.size} files below their floors"
53
+ ", #{noun}: #{regressed.join(", ")}"
54
+ end
55
+
56
+ # `SourceFile#coverage_statistics` answers for disabled criteria too, as
57
+ # empty statistics, so the criteria are gated on the configuration rather
58
+ # than on the stats' presence.
59
+ def current_floors(result)
60
+ criteria = measured_criteria
61
+ floors = {} #: Baseline::current_floors
62
+ result.files.each do |file|
63
+ floors[file.project_filename] = criteria.to_h { |criterion| [criterion, floor_of(file, criterion)] }
64
+ end
65
+ floors
66
+ end
67
+
68
+ def measured_criteria
69
+ criteria = [] #: Array[Symbol]
70
+ criteria << :line if SimpleCov.line_coverage?
71
+ criteria << :branch if SimpleCov.branch_coverage?
72
+ criteria << :method if SimpleCov.method_coverage?
73
+ criteria
74
+ end
75
+
76
+ def floor_of(file, criterion)
77
+ stats = file.coverage_statistics(criterion)
78
+ {percent: SimpleCov.round_coverage(stats.percent), missed: stats.missed}
79
+ end
80
+
81
+ def write(baseline)
82
+ AtomicFile.write(output_path, baseline.to_yaml)
83
+ end
84
+
85
+ # The baseline lives where the exit check reads it, not under the coverage
86
+ # directory: it is checked-in policy, not a report artifact.
87
+ def output_path
88
+ File.expand_path(SimpleCov.baseline_file, SimpleCov.root)
89
+ end
90
+
91
+ # `format` hands the ratchet's summary to `emit_status`, which passes it
92
+ # through to here in the result's place.
93
+ def output_message(message)
94
+ message
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+ require_relative "../atomic_file"
6
+
7
+ module SimpleCov
8
+ module Formatter
9
+ # Shared writer for the coverage.json artifact, used by JSONFormatter as its
10
+ # report and by HTMLFormatter as the side file feeding `simplecov serve` and
11
+ # external tools. Centralizing the write keeps the two copies byte-identical
12
+ # and gives both formatters the concurrent-overwrite warning of #1171.
13
+ module CoverageJSONWriter
14
+ FILENAME = "coverage.json"
15
+
16
+ META_SCAN_BYTES = 64 * 1024
17
+ private_constant :META_SCAN_BYTES
18
+
19
+ extend self
20
+
21
+ def write(output_path, hash, result)
22
+ path = File.join(output_path, FILENAME)
23
+ warn_if_concurrent_overwrite(path, result)
24
+ AtomicFile.write(path, JSON.pretty_generate(hash), binary: true)
25
+ path
26
+ end
27
+
28
+ # Warns when the existing coverage.json has a timestamp newer than this
29
+ # process's start time, a strong signal that a sibling test process wrote it
30
+ # while we were running. A matching command_name means the same merged
31
+ # result, which is what both formatters configured together produce, so
32
+ # there is nothing to lose by overwriting (#1171).
33
+ def warn_if_concurrent_overwrite(path, result)
34
+ start_time = SimpleCov.process_start_time or return
35
+ existing = existing_meta(path) or return
36
+ return unless existing.fetch(:timestamp) > start_time
37
+
38
+ return if existing.fetch(:command_name).eql?(result.command_name)
39
+
40
+ warn "simplecov: #{path} was written at #{existing[:timestamp].iso8601} — after " \
41
+ "this process started at #{start_time.iso8601}. Overwriting " \
42
+ "likely loses coverage data from a concurrent test run. For " \
43
+ "parallel test setups, use SimpleCov::ResultMerger or run a single " \
44
+ "collation step after all workers finish."
45
+ end
46
+
47
+ def existing_meta(path)
48
+ return nil unless File.exist?(path)
49
+
50
+ meta = parse_meta(path) or return nil
51
+ timestamp = parse_time(meta[:timestamp]) or return nil
52
+
53
+ {timestamp: timestamp, command_name: meta[:command_name]}
54
+ end
55
+
56
+ # Our own writer emits an ISO 8601 string, but coverage.json is whatever
57
+ # wrote it last: third-party formatters rewrite it in their own shape with
58
+ # an epoch integer, and `Time.iso8601` raised TypeError on that, taking the
59
+ # whole report down on the next run. Anything but the two spellings disables
60
+ # the concurrency check for this file instead of failing the write (#1285).
61
+ def parse_time(value)
62
+ case value
63
+ when String then Time.iso8601(value)
64
+ when Numeric then Time.at(value)
65
+ end
66
+ rescue ArgumentError
67
+ nil
68
+ end
69
+
70
+ def parse_meta(path)
71
+ parse_meta_head(path) || parse_meta_full(path)
72
+ end
73
+
74
+ # The meta object is flat and sits at the head of every file this module
75
+ # writes, so the common case parses just that slice instead of a
76
+ # multi-megabyte report. A miss falls back to the full parse.
77
+ def parse_meta_head(path)
78
+ head = File.read(path, META_SCAN_BYTES).to_s
79
+ slice = head[/"meta"\s*:\s*(\{.*?\})/m, 1]
80
+ return nil unless slice
81
+
82
+ JSON.parse(slice, symbolize_names: true)
83
+ rescue JSON::ParserError
84
+ nil
85
+ end
86
+
87
+ def parse_meta_full(path)
88
+ parsed = JSON.parse(File.read(path), symbolize_names: true)
89
+ parsed[:meta] if parsed.is_a?(Hash)
90
+ rescue JSON::ParserError
91
+ nil
92
+ end
93
+ end
94
+ end
95
+ end