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,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ #
5
+ # Records which context covered which line: a list of context ids and, per
6
+ # source file, a bitmap of covered lines for each context that touched the
7
+ # file. A context is any labeled region of execution, and the vocabulary is
8
+ # deliberately the general one (coverage.py calls the same idea dynamic
9
+ # contexts), so the stored format doesn't bake simplecov's current use into
10
+ # a name.
11
+ #
12
+ # The naive shape of this data, a list of ids on every line, is
13
+ # O(contexts x lines) strings. Context ids are interned instead, and each
14
+ # context's covered lines within a file are a single Integer bitmap with bit
15
+ # N set when line N+1 was executed, which packs a thousand-line file into
16
+ # ~125 bytes and makes the union of two recordings a bitwise OR.
17
+ #
18
+ # Serialized into `.resultset.json` as `{"version" => 1, "contexts" =>
19
+ # [...ids...], "files" => {path => {context index => bitmap as hex}}}`. The
20
+ # format is tolerated, not trusted: `.from_hash` returns nil for anything
21
+ # malformed or future-versioned, which the merge treats the same as a map
22
+ # that was never recorded.
23
+ #
24
+ class ContextMap
25
+ # Bumped on any change an older reader could misread, so `.from_hash` can
26
+ # treat a future format as absent instead of answering from it wrongly.
27
+ VERSION = 1
28
+
29
+ def initialize
30
+ @contexts = [] #: Array[String]
31
+ @indices = {} #: Hash[String, Integer]
32
+ @files = {} #: Hash[String, Hash[Integer, Integer]]
33
+ end
34
+
35
+ # `lines_by_file` maps a source path to the bitmap of lines the context
36
+ # executed there. The id is interned even when the delta is empty: the
37
+ # context ran, and keeping it distinguishes "covered nothing of its own"
38
+ # from "never recorded".
39
+ def record(context_id, lines_by_file)
40
+ index = intern(context_id)
41
+ lines_by_file.each do |path, bitmap|
42
+ next if bitmap.zero?
43
+
44
+ table = (@files[path] ||= {})
45
+ table[index] = (table[index] || 0) | bitmap
46
+ end
47
+ self
48
+ end
49
+
50
+ # Sorted for stable output, since recording order varies between runs and
51
+ # merges. The path is resolved against `SimpleCov.root`, so callers can
52
+ # pass either an absolute or a project-relative one.
53
+ #
54
+ # A line number below 1 needs no guard of its own: it shifts the probe bit
55
+ # right off the end of the bitmap, leaving a mask no line can match.
56
+ def covering(path, line)
57
+ table = @files[File.expand_path(path, SimpleCov.root)]
58
+ return [] unless table
59
+
60
+ bit = 1 << (line - 1)
61
+ table.filter_map { |index, bitmap| @contexts.fetch(index) if bitmap.anybits?(bit) }.sort
62
+ end
63
+
64
+ def contexts
65
+ @contexts.dup
66
+ end
67
+
68
+ def empty?
69
+ @contexts.empty?
70
+ end
71
+
72
+ # Other's contexts are re-interned, so maps recorded by different processes
73
+ # merge by id, and a context both sides saw contributes one entry.
74
+ def absorb(other)
75
+ remap = other.interned_contexts.map { |context_id| intern(context_id) }
76
+ other.file_tables.each do |path, table|
77
+ target = (@files[path] ||= {})
78
+ table.each do |index, bitmap|
79
+ key = remap.fetch(index)
80
+ target[key] = (target[key] || 0) | bitmap
81
+ end
82
+ end
83
+ self
84
+ end
85
+
86
+ # `only:` restricts the files to a given set of paths, so the map follows
87
+ # the same universe as the coverage it sits beside. The context list is
88
+ # never restricted: which contexts ran is true regardless of which files
89
+ # survived filtering.
90
+ def to_h(only: nil)
91
+ # Selected rather than sliced: `only` is a Set, and splatting one into
92
+ # `slice` is a call `to_a` answers for, which makes dropping the `to_a` a
93
+ # mutation nothing can observe.
94
+ tables = only ? @files.select { |path, _table| only.include?(path) } : @files # rubocop:disable Style/HashSlice
95
+ serialized = tables.transform_values { |table| serialize_table(table) }
96
+ {"version" => VERSION, "contexts" => @contexts.dup, "files" => serialized}
97
+ end
98
+
99
+ # Index string => hex bitmap, the wire encoding `to_h` writes.
100
+ # coverage.json shares the resultset's encoding through this, so the format
101
+ # has one owner.
102
+ def serialized_bitmaps_for(path)
103
+ serialize_table(@files[File.expand_path(path, SimpleCov.root)] || {})
104
+ end
105
+
106
+ # @api private
107
+ def intern(context_id)
108
+ @indices[context_id] ||= begin
109
+ @contexts << context_id
110
+ @contexts.size - 1
111
+ end
112
+ end
113
+
114
+ class << self
115
+ # nil when the data is not a well-formed map of this format version.
116
+ # All-or-nothing on purpose: a partially salvaged map would answer
117
+ # `covering` queries with silent gaps, and the merge already drops an
118
+ # absent map consistently.
119
+ def from_hash(data)
120
+ # `eql?` rather than `==`: a version written as 1.0 is not this format,
121
+ # and reading it as if it were is what the version gate exists to stop.
122
+ return nil unless data.instance_of?(Hash) && data["version"].eql?(VERSION)
123
+
124
+ contexts = data["contexts"]
125
+ files = data["files"]
126
+ return nil unless contexts.instance_of?(Array) && contexts.all?(String) && files.instance_of?(Hash)
127
+
128
+ build(contexts, files)
129
+ end
130
+
131
+ private
132
+
133
+ def build(contexts, files)
134
+ map = new
135
+ contexts.each { |context_id| map.intern(context_id) }
136
+ files.each do |path, table|
137
+ return nil unless path.instance_of?(String) && table.instance_of?(Hash)
138
+ return nil unless absorb_table(map, contexts, path, table)
139
+ end
140
+ map
141
+ end
142
+
143
+ def absorb_table(map, contexts, path, table)
144
+ table.all? do |index, encoded|
145
+ position = parse_index(index, contexts.size)
146
+ bitmap = parse_bitmap(encoded)
147
+ map.record(contexts.fetch(position), path => bitmap) if position && bitmap
148
+ end
149
+ end
150
+
151
+ # A serialized context index is a decimal string pointing into the entry's
152
+ # own context list. An out-of-range one makes the whole map malformed.
153
+ def parse_index(index, size)
154
+ return nil unless index.instance_of?(String) && index.match?(/\A\d+\z/)
155
+
156
+ value = index.to_i
157
+ value if value < size
158
+ end
159
+
160
+ def parse_bitmap(encoded)
161
+ return nil unless encoded.instance_of?(String) && encoded.match?(/\A\h+\z/)
162
+
163
+ encoded.to_i(16)
164
+ end
165
+ end
166
+
167
+ private
168
+
169
+ # Sorted by index: recording and merge order vary run to run, and a
170
+ # deterministic serialization keeps stored resultsets diffable.
171
+ def serialize_table(table)
172
+ table.sort.to_h { |index, bitmap| [index.to_s, bitmap.to_s(16)] }
173
+ end
174
+
175
+ protected
176
+
177
+ # Live state, not copies, which is why these are protected rather than
178
+ # public.
179
+ def interned_contexts
180
+ @contexts
181
+ end
182
+
183
+ def file_tables
184
+ @files
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module SimpleCov
6
+ module CoverageJSON
7
+ class Error < StandardError; end
8
+
9
+ def self.load(path)
10
+ source = File.binread(path).force_encoding(Encoding::UTF_8)
11
+ raise Error, "input is not valid UTF-8" unless source.valid_encoding?
12
+
13
+ document = JSON.parse(source)
14
+ return document if document.instance_of?(Hash)
15
+
16
+ raise Error, "top-level value must be an object"
17
+ rescue JSON::ParserError => e
18
+ raise Error, e
19
+ end
20
+ end
21
+ end
@@ -1,45 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleCov
4
- # Holds the individual data of a coverage result.
5
- #
6
- # This is uniform across coverage criteria as they all have:
7
- #
8
- # * total - how many things to cover there are (total relevant loc/branches)
9
- # * covered - how many of the coverables are hit
10
- # * missed - how many of the coverables are missed
11
- # * percent - percentage as covered/missed
12
- # * strength - average hits per/coverable (will not exist for one shot lines format)
4
+ # One coverage criterion's numbers, uniform across criteria: total, covered,
5
+ # missed, omitted (only meaningful for line coverage), percent, and strength,
6
+ # the average hits per coverable unit, which the oneshot-lines format has no
7
+ # way to produce.
13
8
  class CoverageStatistics
14
- attr_reader :total, :covered, :missed, :strength, :percent
9
+ attr_reader :total, :covered, :missed, :omitted, :strength, :percent
15
10
 
11
+ ZERO_STATS = [0, 0, 0, 0.0].freeze #: [Integer, Integer, Integer, Float]
12
+
13
+ # Strength is remultiplied by loc because files have different strengths and
14
+ # line counts, giving them a different weight in the total.
16
15
  def self.from(coverage_statistics)
17
- sum_covered, sum_missed, sum_total_strength =
18
- coverage_statistics.reduce([0, 0, 0.0]) do |(covered, missed, total_strength), file_coverage_statistics|
16
+ sum_covered, sum_missed, sum_omitted, sum_total_strength =
17
+ coverage_statistics.reduce(ZERO_STATS) do |(covered, missed, omitted, total_strength), stats|
19
18
  [
20
- covered + file_coverage_statistics.covered,
21
- missed + file_coverage_statistics.missed,
22
- # gotta remultiply with loc because files have different strength and loc
23
- # giving them a different "weight" in total
24
- total_strength + (file_coverage_statistics.strength * file_coverage_statistics.total)
19
+ covered + stats.covered,
20
+ missed + stats.missed,
21
+ omitted + stats.omitted,
22
+ total_strength + (stats.strength * stats.total)
25
23
  ]
26
24
  end
27
25
 
28
- new(covered: sum_covered, missed: sum_missed, total_strength: sum_total_strength)
26
+ new(covered: sum_covered, missed: sum_missed, omitted: sum_omitted, total_strength: sum_total_strength)
29
27
  end
30
28
 
31
- # Requires only covered, missed and strength to be initialized.
32
- #
33
- # Other values are computed by this class.
34
- def initialize(covered:, missed:, total_strength: 0.0)
35
- @covered = covered
36
- @missed = missed
37
- @total = covered + missed
38
- @percent = compute_percent(covered, missed, total)
29
+ def initialize(covered:, missed:, omitted: 0, total_strength: 0, percent: nil)
30
+ @covered = covered
31
+ @missed = missed
32
+ @omitted = omitted
33
+ @total = covered + missed
34
+ @percent = percent || compute_percent(covered, missed, total)
39
35
  @strength = compute_strength(total_strength, total)
40
36
  end
41
37
 
42
- private
38
+ private
43
39
 
44
40
  def compute_percent(covered, missed, total)
45
41
  return 100.0 if missed.zero?
@@ -50,7 +46,7 @@ module SimpleCov
50
46
  def compute_strength(total_strength, total)
51
47
  return 0.0 if total.zero?
52
48
 
53
- total_strength.to_f / total
49
+ total_strength.fdiv(total)
54
50
  end
55
51
  end
56
52
  end
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module CoverageViolations
5
+ class << self
6
+ def minimum_overall(result, thresholds)
7
+ thresholds.filter_map do |criterion, expected|
8
+ actual = floored_percent_for(result, criterion) or next
9
+ {criterion: criterion, expected: expected, actual: actual} if actual < expected
10
+ end
11
+ end
12
+
13
+ # An actual of 95.4287 floors to 95.42, so a maximum of 95.42 passes
14
+ # (#187).
15
+ def maximum_overall(result, thresholds)
16
+ thresholds.filter_map do |criterion, expected|
17
+ actual = floored_percent_for(result, criterion) or next
18
+ {criterion: criterion, expected: expected, actual: actual} if actual > expected
19
+ end
20
+ end
21
+
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)
26
+ result.files.flat_map do |file|
27
+ effective = effective_per_file_thresholds(file, defaults, overrides)
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
66
+ end
67
+ end
68
+
69
+ def minimum_by_group(result, thresholds)
70
+ thresholds.flat_map do |group_name, minimums|
71
+ group = lookup_group(result, group_name)
72
+ none = [] # : Array[Hash[Symbol, untyped]]
73
+ group ? group_minimum_violations(group_name, group, minimums) : none
74
+ end
75
+ end
76
+
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
80
+
81
+ thresholds.filter_map do |criterion, maximum|
82
+ actual = compute_drop(criterion, result, baseline)
83
+ {criterion: criterion, maximum: maximum, actual: actual} if actual && actual > maximum
84
+ end
85
+ end
86
+
87
+ private
88
+
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)
94
+ stats = stats_source.coverage_statistics[SimpleCov.coverage_statistics_key(criterion)]
95
+ round(stats.percent) if stats
96
+ end
97
+
98
+ def effective_per_file_thresholds(file, defaults, overrides)
99
+ path = file.project_filename
100
+ overrides.reduce(defaults) do |acc, (pattern, criterion_thresholds)|
101
+ path_matches?(path, pattern) ? acc.merge(criterion_thresholds) : acc
102
+ end
103
+ end
104
+
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
109
+ def path_matches?(project_filename, pattern)
110
+ return project_filename.match?(pattern) if pattern.is_a?(Regexp)
111
+ return project_filename.start_with?(pattern) if pattern.end_with?("/")
112
+
113
+ project_filename == pattern
114
+ end
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
+
141
+ def file_minimum_violation(file, criterion, expected)
142
+ actual = floored_percent_for(file, criterion) or return
143
+ return unless actual < expected
144
+
145
+ {
146
+ criterion: criterion,
147
+ expected: expected,
148
+ actual: actual,
149
+ filename: file.filename,
150
+ project_filename: file.project_filename
151
+ }
152
+ end
153
+
154
+ def group_minimum_violations(group_name, group, minimums)
155
+ minimums.filter_map do |criterion, expected|
156
+ actual = floored_percent_for(group, criterion) or next
157
+ {group_name: group_name, criterion: criterion, expected: expected, actual: actual} if actual < expected
158
+ end
159
+ end
160
+
161
+ # The misconfiguration notice is enforcement output, not a Ruby warning: it
162
+ # must survive `-W0` and `Warning.warn` hooks, and honor `print_errors`. A
163
+ # configured group that matched no files is absent from `result.groups`
164
+ # too, but has nothing to gate rather than nothing to look up.
165
+ def lookup_group(result, group_name)
166
+ group = result.groups[group_name]
167
+ if group.nil? && SimpleCov.print_errors && !result.configured_group?(group_name)
168
+ ExitCodes.print_error "minimum_coverage_by_group: no group named '#{group_name}' exists. " \
169
+ "Available groups: #{result.groups.keys.join(", ")}"
170
+ end
171
+ group
172
+ end
173
+
174
+ def drop_baseline_percents(mode, last_run)
175
+ case mode
176
+ when :median then median_baseline
177
+ when :branch then branch_baseline
178
+ else last_run_baseline(last_run || LastRun.read)
179
+ end
180
+ end
181
+
182
+ # A hand-edited .last_run.json can carry any value type, and `LastRun.read`
183
+ # only vouches for the top level being a Hash. `:covered_percent` is the
184
+ # pre-criteria file format's spelling of the line percent.
185
+ def last_run_baseline(last_run)
186
+ return nil unless last_run.is_a?(Hash)
187
+
188
+ previous = (_ = last_run)[:result]
189
+ return nil unless previous.is_a?(Hash)
190
+
191
+ previous = _ = previous
192
+ {line: previous[:line] || previous[:covered_percent],
193
+ branch: previous[:branch], method: previous[:method]}
194
+ end
195
+
196
+ def median_baseline
197
+ totals = history_totals
198
+ baseline = {} #: Hash[Symbol, untyped]
199
+ %i[line branch method].each do |criterion|
200
+ values = totals.map { |entry| entry[criterion.to_s] }.grep(Numeric)
201
+ baseline[criterion] = median(values) unless values.empty?
202
+ end
203
+ baseline
204
+ end
205
+
206
+ def history_totals
207
+ History.read.filter_map do |entry|
208
+ entry.fetch("totals") if entry.is_a?(Hash) && entry["totals"].is_a?(Hash)
209
+ end
210
+ end
211
+
212
+ def median(values)
213
+ sorted = values.sort
214
+ mid = sorted.length / 2
215
+ sorted.length.odd? ? sorted.fetch(mid) : (sorted.fetch(mid - 1) + sorted.fetch(mid)).fdiv(2)
216
+ end
217
+
218
+ def branch_baseline
219
+ branch, = History.git_info
220
+ return nil unless branch
221
+
222
+ entry = History.read.reverse_each.find { |candidate| branch_entry?(candidate, branch) }
223
+ return nil unless entry
224
+
225
+ totals = {} #: Hash[Symbol, untyped]
226
+ entry.fetch("totals").each { |key, value| totals[key.to_sym] = value }
227
+ totals
228
+ end
229
+
230
+ # `==` and `eql?` are indistinguishable for the String operands, so that
231
+ # mutation is equivalent. The branch-baseline examples pin every clause.
232
+ # mutant:disable
233
+ # mutant:disable
234
+ def branch_entry?(candidate, branch)
235
+ candidate.is_a?(Hash) && candidate["branch"] == branch && candidate["totals"].is_a?(Hash)
236
+ end
237
+
238
+ def compute_drop(criterion, result, baseline)
239
+ stats_key = SimpleCov.coverage_statistics_key(criterion)
240
+ last_coverage_percent = baseline[stats_key]
241
+ # Treat a non-numeric percent like a missing one instead of
242
+ # raising out of the at_exit hook.
243
+ return unless last_coverage_percent.is_a?(Numeric)
244
+
245
+ current = floored_percent_for(result, criterion) or return
246
+ (last_coverage_percent - current).floor(10)
247
+ end
248
+
249
+ def round(percent)
250
+ SimpleCov.round_coverage(percent)
251
+ end
252
+ end
253
+ end
254
+ end
@@ -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
@@ -1,53 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Load default formatter gem
3
+ require "English"
4
4
  require "pathname"
5
- require_relative "default_formatter"
6
- require_relative "profiles/root_filter"
7
- require_relative "profiles/test_frameworks"
8
- require_relative "profiles/bundler_filter"
9
- require_relative "profiles/hidden_filter"
10
- require_relative "profiles/rails"
5
+ require_relative "formatter/html_formatter"
11
6
 
12
- # Default configuration
13
7
  SimpleCov.configure do
14
- formatter SimpleCov::Formatter::MultiFormatter.new(
15
- SimpleCov::Formatter.from_env(ENV)
16
- )
8
+ formatter SimpleCov::Formatter::HTMLFormatter
17
9
 
18
10
  load_profile "bundler_filter"
19
11
  load_profile "hidden_filter"
20
- # Exclude files outside of SimpleCov.root
21
12
  load_profile "root_filter"
13
+ load_profile "test_frameworks"
22
14
  end
23
15
 
24
- # Gotta stash this a-s-a-p, see the CommandGuesser class and i.e. #110 for further info
25
- 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(" ")}"
20
+ SimpleCov::CommandGuesser.original_program_name = $PROGRAM_NAME
26
21
 
27
- at_exit do
28
- next if SimpleCov.external_at_exit?
29
-
30
- SimpleCov.at_exit_behavior
31
- end
32
-
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
- begin
44
- load filename
45
- rescue LoadError, StandardError
28
+ SimpleCov.with_dot_simplecov_autoload do
29
+ load filename.to_s
30
+ rescue LoadError, StandardError => e
31
+ # simplecov:disable — only fires when .simplecov is unreadable
32
+ # or raises during load
46
33
  warn "Warning: Error occurred while trying to load #{filename}. " \
47
- "Error message: #{$!.message}"
34
+ "Error message: #{e.message}"
35
+ # simplecov:enable
48
36
  end
49
37
  break
50
38
  end
39
+ # simplecov:disable — only fires when no .simplecov is found up to
40
+ # the filesystem root; simplecov's own dogfood run finds the repo's
41
+ # .simplecov on the first iteration and breaks before getting here.
51
42
  config_path, = config_path.split
52
43
  break if config_path.root?
44
+ # simplecov:enable
53
45
  end