simplecov 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +148 -5
  3. data/exe/simplecov +0 -3
  4. data/lib/minitest/simplecov_plugin.rb +12 -9
  5. data/lib/simplecov/atomic_file.rb +16 -19
  6. data/lib/simplecov/autostart.rb +5 -7
  7. data/lib/simplecov/baseline/parser.rb +58 -0
  8. data/lib/simplecov/baseline.rb +170 -0
  9. data/lib/simplecov/cli/affected/changed_files.rb +57 -0
  10. data/lib/simplecov/cli/affected/selection.rb +117 -0
  11. data/lib/simplecov/cli/affected.rb +148 -0
  12. data/lib/simplecov/cli/badge/svg.rb +70 -0
  13. data/lib/simplecov/cli/badge.rb +62 -0
  14. data/lib/simplecov/cli/clean.rb +23 -24
  15. data/lib/simplecov/cli/command_helpers.rb +55 -19
  16. data/lib/simplecov/cli/completions/scripts.rb +114 -0
  17. data/lib/simplecov/cli/completions.rb +83 -0
  18. data/lib/simplecov/cli/coverage.rb +25 -28
  19. data/lib/simplecov/cli/coverage_file.rb +58 -14
  20. data/lib/simplecov/cli/dead_code/output.rb +100 -0
  21. data/lib/simplecov/cli/dead_code.rb +139 -0
  22. data/lib/simplecov/cli/diff/output.rb +45 -0
  23. data/lib/simplecov/cli/diff.rb +35 -67
  24. data/lib/simplecov/cli/dotfile.rb +83 -38
  25. data/lib/simplecov/cli/git.rb +41 -0
  26. data/lib/simplecov/cli/history/output.rb +135 -0
  27. data/lib/simplecov/cli/history.rb +67 -0
  28. data/lib/simplecov/cli/merge.rb +34 -39
  29. data/lib/simplecov/cli/open.rb +12 -15
  30. data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
  31. data/lib/simplecov/cli/patch/output.rb +135 -0
  32. data/lib/simplecov/cli/patch.rb +169 -0
  33. data/lib/simplecov/cli/ratchet/output.rb +44 -0
  34. data/lib/simplecov/cli/ratchet.rb +83 -0
  35. data/lib/simplecov/cli/report.rb +18 -24
  36. data/lib/simplecov/cli/run.rb +8 -9
  37. data/lib/simplecov/cli/serve/report_preparer.rb +7 -6
  38. data/lib/simplecov/cli/serve/static_file_handler.rb +62 -56
  39. data/lib/simplecov/cli/serve.rb +27 -24
  40. data/lib/simplecov/cli/show/annotator.rb +87 -0
  41. data/lib/simplecov/cli/show/sweep.rb +36 -0
  42. data/lib/simplecov/cli/show.rb +133 -0
  43. data/lib/simplecov/cli/status/facts.rb +87 -0
  44. data/lib/simplecov/cli/status.rb +93 -0
  45. data/lib/simplecov/cli/tests/redundancy.rb +78 -0
  46. data/lib/simplecov/cli/tests.rb +148 -0
  47. data/lib/simplecov/cli/uncovered/misses.rb +37 -0
  48. data/lib/simplecov/cli/uncovered.rb +70 -32
  49. data/lib/simplecov/cli/usage.rb +152 -0
  50. data/lib/simplecov/cli/watch/live_report.rb +73 -0
  51. data/lib/simplecov/cli/watch/narrator.rb +55 -0
  52. data/lib/simplecov/cli/watch/poller.rb +45 -0
  53. data/lib/simplecov/cli/watch/session.rb +134 -0
  54. data/lib/simplecov/cli/watch/test_plan.rb +43 -0
  55. data/lib/simplecov/cli/watch.rb +85 -0
  56. data/lib/simplecov/cli.rb +43 -92
  57. data/lib/simplecov/color.rb +19 -37
  58. data/lib/simplecov/combine/branches_combiner.rb +7 -31
  59. data/lib/simplecov/combine/coverage_accumulator.rb +49 -142
  60. data/lib/simplecov/combine/identity_interner.rb +10 -13
  61. data/lib/simplecov/combine/interned_counts.rb +7 -9
  62. data/lib/simplecov/combine/lines_combiner.rb +20 -44
  63. data/lib/simplecov/combine/methods_combiner.rb +9 -35
  64. data/lib/simplecov/combine/results_combiner.rb +4 -21
  65. data/lib/simplecov/combine.rb +0 -6
  66. data/lib/simplecov/command_guesser.rb +20 -34
  67. data/lib/simplecov/configuration/baseline.rb +31 -0
  68. data/lib/simplecov/configuration/coverage.rb +91 -79
  69. data/lib/simplecov/configuration/coverage_criteria.rb +43 -54
  70. data/lib/simplecov/configuration/deprecations.rb +21 -0
  71. data/lib/simplecov/configuration/eval_coverage.rb +6 -10
  72. data/lib/simplecov/configuration/filters.rb +40 -73
  73. data/lib/simplecov/configuration/formatting.rb +74 -83
  74. data/lib/simplecov/configuration/groups.rb +19 -13
  75. data/lib/simplecov/configuration/history.rb +44 -0
  76. data/lib/simplecov/configuration/ignored_entries.rb +31 -22
  77. data/lib/simplecov/configuration/merging.rb +94 -62
  78. data/lib/simplecov/configuration/missed_caps.rb +51 -0
  79. data/lib/simplecov/configuration/production.rb +23 -0
  80. data/lib/simplecov/configuration/test_tracking.rb +48 -0
  81. data/lib/simplecov/configuration/thresholds.rb +33 -62
  82. data/lib/simplecov/configuration/view_coverage.rb +38 -0
  83. data/lib/simplecov/configuration.rb +70 -76
  84. data/lib/simplecov/configuration_error.rb +5 -0
  85. data/lib/simplecov/context_map/union.rb +81 -0
  86. data/lib/simplecov/context_map.rb +187 -0
  87. data/lib/simplecov/coverage_json.rb +2 -5
  88. data/lib/simplecov/coverage_statistics.rb +14 -24
  89. data/lib/simplecov/coverage_violations.rb +157 -64
  90. data/lib/simplecov/current_run.rb +61 -0
  91. data/lib/simplecov/defaults.rb +4 -23
  92. data/lib/simplecov/deprecation.rb +45 -27
  93. data/lib/simplecov/directive.rb +46 -62
  94. data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
  95. data/lib/simplecov/exit_codes/check.rb +17 -11
  96. data/lib/simplecov/exit_codes/exit_code_handling.rb +21 -8
  97. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +7 -10
  98. data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
  99. data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
  100. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +7 -12
  101. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +11 -10
  102. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +7 -9
  103. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +16 -17
  104. data/lib/simplecov/exit_codes.rb +16 -10
  105. data/lib/simplecov/exit_handling.rb +94 -59
  106. data/lib/simplecov/file_list.rb +20 -51
  107. data/lib/simplecov/filter.rb +43 -66
  108. data/lib/simplecov/formatter/base.rb +37 -55
  109. data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
  110. data/lib/simplecov/formatter/coverage_json_writer.rb +29 -31
  111. data/lib/simplecov/formatter/html_formatter/public/index.html +15 -9
  112. data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +74 -17
  113. data/lib/simplecov/formatter/html_formatter.rb +24 -41
  114. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +50 -16
  115. data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
  116. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +43 -29
  117. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +14 -10
  118. data/lib/simplecov/formatter/json_formatter.rb +7 -13
  119. data/lib/simplecov/formatter/multi_formatter.rb +5 -11
  120. data/lib/simplecov/formatter/simple_formatter.rb +2 -8
  121. data/lib/simplecov/formatter.rb +14 -10
  122. data/lib/simplecov/group_names.rb +13 -12
  123. data/lib/simplecov/history.rb +115 -0
  124. data/lib/simplecov/last_run.rb +6 -9
  125. data/lib/simplecov/lines_classifier.rb +31 -16
  126. data/lib/simplecov/load_global_config.rb +7 -7
  127. data/lib/simplecov/parallel_adapters/base.rb +13 -41
  128. data/lib/simplecov/parallel_adapters/generic.rb +7 -19
  129. data/lib/simplecov/parallel_adapters/parallel_tests.rb +21 -42
  130. data/lib/simplecov/parallel_adapters.rb +24 -55
  131. data/lib/simplecov/parallel_coordination.rb +39 -49
  132. data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
  133. data/lib/simplecov/parallel_result_merger.rb +67 -126
  134. data/lib/simplecov/process.rb +14 -32
  135. data/lib/simplecov/production/error.rb +11 -0
  136. data/lib/simplecov/production/file_sink.rb +130 -0
  137. data/lib/simplecov/production.rb +249 -0
  138. data/lib/simplecov/profiles/rails.rb +1 -12
  139. data/lib/simplecov/profiles/root_filter.rb +0 -4
  140. data/lib/simplecov/profiles/strict.rb +9 -19
  141. data/lib/simplecov/profiles.rb +11 -33
  142. data/lib/simplecov/report_deferral.rb +11 -15
  143. data/lib/simplecov/report_stamp.rb +6 -9
  144. data/lib/simplecov/result/filter_config.rb +19 -0
  145. data/lib/simplecov/result/missing_source_files_reporter.rb +8 -9
  146. data/lib/simplecov/result/serialization.rb +43 -0
  147. data/lib/simplecov/result/source_file_builder.rb +7 -12
  148. data/lib/simplecov/result.rb +68 -117
  149. data/lib/simplecov/result_adapter.rb +56 -68
  150. data/lib/simplecov/result_merger/contexts.rb +22 -0
  151. data/lib/simplecov/result_merger/legacy_format_adapter.rb +4 -8
  152. data/lib/simplecov/result_merger/resultset_file.rb +20 -27
  153. data/lib/simplecov/result_merger/resultset_run_identity.rb +23 -25
  154. data/lib/simplecov/result_merger/resultset_store.rb +23 -14
  155. data/lib/simplecov/result_merger/unloaded_files.rb +24 -32
  156. data/lib/simplecov/result_merger.rb +59 -58
  157. data/lib/simplecov/result_processing.rb +74 -119
  158. data/lib/simplecov/run_identity.rb +16 -18
  159. data/lib/simplecov/simulate_coverage.rb +34 -50
  160. data/lib/simplecov/source_file/branch.rb +8 -33
  161. data/lib/simplecov/source_file/branch_builder.rb +28 -47
  162. data/lib/simplecov/source_file/builder_context.rb +6 -13
  163. data/lib/simplecov/source_file/line.rb +13 -33
  164. data/lib/simplecov/source_file/line_builder.rb +9 -15
  165. data/lib/simplecov/source_file/method.rb +3 -7
  166. data/lib/simplecov/source_file/method_builder.rb +6 -15
  167. data/lib/simplecov/source_file/ruby_data_parser.rb +59 -66
  168. data/lib/simplecov/source_file/skip_chunks.rb +19 -28
  169. data/lib/simplecov/source_file/source_loader.rb +45 -35
  170. data/lib/simplecov/source_file/statistics.rb +10 -9
  171. data/lib/simplecov/source_file.rb +21 -51
  172. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +63 -114
  173. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +76 -121
  174. data/lib/simplecov/static_coverage_extractor/method_collector.rb +11 -24
  175. data/lib/simplecov/static_coverage_extractor/prism_compat.rb +15 -24
  176. data/lib/simplecov/static_coverage_extractor/value_position.rb +25 -38
  177. data/lib/simplecov/static_coverage_extractor/visitor.rb +47 -64
  178. data/lib/simplecov/static_coverage_extractor.rb +46 -62
  179. data/lib/simplecov/test_tracker/accessors.rb +34 -0
  180. data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
  181. data/lib/simplecov/test_tracker/delta.rb +69 -0
  182. data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
  183. data/lib/simplecov/test_tracker.rb +162 -0
  184. data/lib/simplecov/unloaded_file_injector.rb +18 -31
  185. data/lib/simplecov/useless_results_remover.rb +7 -6
  186. data/lib/simplecov/version.rb +1 -1
  187. data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
  188. data/lib/simplecov/view_coverage.rb +55 -0
  189. data/lib/simplecov-html.rb +1 -0
  190. data/lib/simplecov.rb +57 -113
  191. data/lib/simplecov_json_formatter.rb +1 -0
  192. data/man/simplecov.1 +349 -0
  193. data/sig/simplecov.rbs +523 -595
  194. metadata +72 -5
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module Production
5
+ # Configuration and store-format failures. Sink errors raised from `store` are
6
+ # not wrapped in this: the runtime rescues them, warns, and retries on the
7
+ # next interval.
8
+ class Error < StandardError
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "time"
6
+ require_relative "error"
7
+
8
+ module SimpleCov
9
+ module Production
10
+ # The bundled sink: a single JSON file, union-merged under an exclusive
11
+ # lock so any number of processes on the same host can share it. This is
12
+ # the reference for the sink contract:
13
+ #
14
+ # store(coverage) # {"lib/foo.rb" => [1, 3, 12], ...} of
15
+ # # root-relative paths to sorted line numbers
16
+ #
17
+ # A sink must merge (never replace: many processes each hold only a
18
+ # slice), must be idempotent (the same lines may arrive twice), and
19
+ # signals failure by raising, which makes the runtime keep the delta and
20
+ # retry next interval.
21
+ #
22
+ # `last_seen` stamps each file with the last store that carried it.
23
+ # Oneshot clears on drain, so still-running code re-reports every interval
24
+ # and the stamp tracks real recency, not first sighting. The field is
25
+ # optional on read: a v1 store written before it existed simply has no
26
+ # recency evidence to offer.
27
+ class FileSink
28
+ # The envelope key, which is also how `read` tells a production coverage
29
+ # file from an arbitrary JSON document it must not clobber or misread.
30
+ ENVELOPE = "simplecov_production"
31
+ FORMAT_VERSION = 1
32
+
33
+ attr_reader :path
34
+
35
+ def initialize(path:)
36
+ @path = File.expand_path(path)
37
+ end
38
+
39
+ def store(coverage)
40
+ FileUtils.mkdir_p(File.dirname(path))
41
+ with_exclusive_lock do |file|
42
+ existing = self.class.parse(file.read, path)
43
+ rewrite(file, envelope(existing, coverage))
44
+ end
45
+ true
46
+ end
47
+
48
+ # Truncates any leftover tail from a previously larger document.
49
+ def rewrite(file, payload)
50
+ file.rewind
51
+ file.write(JSON.generate(payload))
52
+ file.truncate(file.pos)
53
+ end
54
+
55
+ # Raises `Error` for anything that is not a production coverage file,
56
+ # naming the path: misreading an arbitrary JSON file as empty coverage
57
+ # would quietly report every line dead.
58
+ def self.read(path)
59
+ parse(File.read(path), path)
60
+ end
61
+
62
+ # @api private -- shared by `read` and `store`'s read-modify-write. An
63
+ # empty or missing file is a fresh store. JSON answers plain hashes,
64
+ # arrays, and scalars, never a subclass of one, so the shape checks ask
65
+ # about the class itself.
66
+ def self.parse(content, path)
67
+ return {"coverage" => {}, "last_seen" => {}, "started_at" => nil} if content.match?(/\A\s*\z/)
68
+
69
+ inner = envelope_of(JSON.parse(content), path)
70
+ inner["coverage"] = {} unless inner["coverage"].instance_of?(Hash)
71
+ inner["last_seen"] = {} unless inner["last_seen"].instance_of?(Hash)
72
+ inner
73
+ rescue JSON::ParserError => e
74
+ # One line of the parser's complaint, which for some inputs quotes the
75
+ # document back and runs long.
76
+ raise Error, "#{path} is not valid JSON (#{e.message.lines.first.to_s.rstrip})"
77
+ end
78
+
79
+ # The refusal is what keeps an arbitrary JSON file from being misread as
80
+ # empty coverage.
81
+ def self.envelope_of(document, path)
82
+ inner = document[ENVELOPE] if document.instance_of?(Hash)
83
+ raise Error, "#{path} is not a SimpleCov production coverage file" unless inner.instance_of?(Hash)
84
+
85
+ inner
86
+ end
87
+
88
+ private
89
+
90
+ # Both halves of the read-modify-write happen through the one handle, so
91
+ # processes sharing the file take turns rather than overwriting each
92
+ # other. The open flags are summed, which for disjoint bits is the same
93
+ # number OR would build and leaves no spelling of the combination
94
+ # without a witness.
95
+ def with_exclusive_lock
96
+ open_file(path, File::RDWR + File::CREAT, 0o644) do |file|
97
+ file.flock(File::LOCK_EX)
98
+ yield file
99
+ end
100
+ end
101
+
102
+ # mutant:disable — Ruby 4.0 removed Kernel#open's leading-pipe
103
+ # command mode, so no test can tell `File.open` from `open` here
104
+ # any more. The explicit receiver is kept: on older rubies it is
105
+ # what refuses to run a store path as a command.
106
+ def open_file(name, mode, perm, &)
107
+ File.open(name, mode, perm, &)
108
+ end
109
+
110
+ def merge(existing, incoming)
111
+ incoming.each_with_object(existing) do |(file, lines), merged|
112
+ merged[file] = ((merged[file] || []) | lines).sort
113
+ end
114
+ end
115
+
116
+ def envelope(existing, incoming)
117
+ now = Time.now.utc.iso8601
118
+ {
119
+ ENVELOPE => {
120
+ "format_version" => FORMAT_VERSION,
121
+ "started_at" => existing["started_at"] || now,
122
+ "updated_at" => now,
123
+ "coverage" => merge(existing.fetch("coverage"), incoming).sort.to_h,
124
+ "last_seen" => existing.fetch("last_seen").merge(incoming.transform_values { now }).sort.to_h
125
+ }
126
+ }
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "coverage"
4
+ require_relative "production/error"
5
+
6
+ #
7
+ # Production coverage mode (#1271). Deliberately loadable on its own:
8
+ # `require "simplecov/production"` pulls in none of the reporting machinery,
9
+ # because a production process wants a measurement tap and nothing else.
10
+ # Nothing here runs unless `SimpleCov::Production.start` is called explicitly.
11
+ #
12
+ module SimpleCov
13
+ module Production
14
+ class << self
15
+ # Answers false, with a warning naming why, when it safely declines
16
+ # (already running, another Coverage owner such as a test suite, or a
17
+ # runtime without oneshot lines). Configuration mistakes raise `Error`
18
+ # instead: a typo'd interval should fail deployment, not quietly measure
19
+ # wrong.
20
+ #
21
+ # In a forking server, call this from the worker-boot hook (Puma's
22
+ # `on_worker_boot`, Unicorn's `after_fork`): the flush thread does not
23
+ # survive a fork, and `start` in the child picks the inherited
24
+ # measurement back up.
25
+ def start(root: Dir.pwd, sink: nil, flush_interval: 60, flush_jitter: nil, sample_rate: 1.0,
26
+ max_buffered_lines: 1_000_000)
27
+ validate!(flush_interval, flush_jitter, sample_rate, max_buffered_lines)
28
+ return warn_decline("SimpleCov::Production is already running") if running?
29
+ return false unless claim_coverage
30
+
31
+ configure(root, sink, flush_interval: flush_interval, flush_jitter: flush_jitter,
32
+ sample_rate: sample_rate, max_buffered_lines: max_buffered_lines)
33
+ @running = true
34
+ @pid = Process.pid
35
+ install_at_exit
36
+ spawn_flush_thread
37
+ true
38
+ end
39
+
40
+ # Both are small Integers, so identity settles it: a measurement belongs to
41
+ # the process that started it and to no other.
42
+ def running?
43
+ !!@running && @pid.equal?(Process.pid)
44
+ end
45
+
46
+ # Called by the flush thread every interval; public so a deploy hook (or a
47
+ # signal handler) can force one. An empty delta counts as accepted.
48
+ def flush
49
+ return false unless running?
50
+
51
+ @mutex.synchronize do
52
+ pull_pending
53
+ push_pending
54
+ end
55
+ end
56
+
57
+ def stop
58
+ return false unless running?
59
+
60
+ @running = false
61
+ wake_flush_thread
62
+ @thread&.join
63
+ @mutex.synchronize do
64
+ pull_pending
65
+ push_pending
66
+ Coverage.result(stop: true, clear: true)
67
+ end
68
+ true
69
+ end
70
+
71
+ # @api private -- clears module state between tests. The at_exit
72
+ # registration flag survives on purpose: one hook per process.
73
+ def reset!
74
+ @running = false
75
+ wake_flush_thread if @thread
76
+ @thread&.join
77
+ @thread = nil
78
+ @pending = {}
79
+ @pid = nil
80
+ @started_coverage = false
81
+ end
82
+
83
+ private
84
+
85
+ def validate!(flush_interval, flush_jitter, sample_rate, max_buffered_lines)
86
+ raise Error, "flush_interval must be a positive number of seconds" unless flush_interval.positive?
87
+
88
+ validate_jitter!(flush_jitter)
89
+ raise Error, "sample_rate must be within (0, 1]" unless sample_rate.positive? && sample_rate <= 1
90
+ raise Error, "max_buffered_lines must be positive" unless max_buffered_lines.positive?
91
+ return unless sample_rate < 1 && !Coverage.respond_to?(:suspend)
92
+
93
+ raise Error, "sample_rate below 1.0 needs Coverage.suspend (Ruby 3.2 or later)"
94
+ end
95
+
96
+ def validate_jitter!(flush_jitter)
97
+ return if flush_jitter.nil? || (flush_jitter.is_a?(Numeric) && flush_jitter >= 0)
98
+
99
+ raise Error, "flush_jitter must be a non-negative number of seconds"
100
+ end
101
+
102
+ # A running Coverage belongs to someone else, unless this very module
103
+ # started it before a fork, in which case the child inherits the
104
+ # measurement and only needs its own flush thread.
105
+ def claim_coverage
106
+ if Coverage.running?
107
+ return true if @started_coverage && @pid && !@pid.equal?(Process.pid)
108
+
109
+ return warn_decline("Coverage is already running (a test suite or another tool owns it)")
110
+ end
111
+ return warn_decline("this runtime has no oneshot lines support") unless oneshot_supported?
112
+
113
+ Coverage.start(oneshot_lines: true)
114
+ @started_coverage = true
115
+ end
116
+
117
+ def oneshot_supported?
118
+ !Coverage.respond_to?(:supported?) || Coverage.supported?(:oneshot_lines)
119
+ end
120
+
121
+ def configure(root, sink, flush_interval:, flush_jitter:, sample_rate:, max_buffered_lines:)
122
+ @root_prefix = File.expand_path(root) + File::SEPARATOR
123
+ @sink = sink || default_sink(root)
124
+ @flush_interval = flush_interval
125
+ @flush_jitter = flush_jitter || (flush_interval / 10.0)
126
+ @sample_rate = sample_rate
127
+ @max_buffered_lines = max_buffered_lines
128
+ @pending = {} #: Hash[String, Set[Integer]]
129
+ @measuring = true
130
+ @mutex = Mutex.new
131
+ @waiter = ConditionVariable.new
132
+ end
133
+
134
+ def default_sink(root)
135
+ FileSink.new(path: File.join(root, "tmp", "simplecov", "production.json"))
136
+ end
137
+
138
+ def spawn_flush_thread
139
+ thread = Thread.new { flush_loop }
140
+ thread.name = "SimpleCov::Production flusher"
141
+ @thread = thread
142
+ end
143
+
144
+ # The condition variable rather than a bare sleep is what lets `stop`
145
+ # return promptly instead of waiting out a long interval.
146
+ def flush_loop
147
+ @mutex.synchronize do
148
+ while @running
149
+ @waiter.wait(@mutex, interval_with_jitter)
150
+ cycle if @running
151
+ end
152
+ end
153
+ end
154
+
155
+ # A fresh share is drawn per wait: a fleet of workers booted together
156
+ # would otherwise contend on the shared sink at the same instant every
157
+ # interval, forever.
158
+ def interval_with_jitter
159
+ @flush_interval + (rand * @flush_jitter)
160
+ end
161
+
162
+ def wake_flush_thread
163
+ @mutex.synchronize { @waiter.signal }
164
+ end
165
+
166
+ def cycle
167
+ resample
168
+ pull_pending
169
+ push_pending
170
+ end
171
+
172
+ # Duty-cycle sampling: with `sample_rate` 0.25, roughly one interval in
173
+ # four measures and the rest are suspended, capping steady-state overhead
174
+ # while every line still gets its chance over time.
175
+ def resample
176
+ return unless @sample_rate < 1
177
+
178
+ wanted = rand < @sample_rate
179
+ if wanted && !@measuring
180
+ Coverage.resume
181
+ elsif !wanted && @measuring
182
+ Coverage.suspend
183
+ end
184
+ @measuring = wanted
185
+ end
186
+
187
+ # Clearing on read means a line executed again later re-reports, which is
188
+ # what lets a dropped buffer heal for still-hot code.
189
+ def pull_pending
190
+ Coverage.result(stop: false, clear: true).each do |path, data|
191
+ lines = data[:oneshot_lines]
192
+ next if lines.nil? || lines.empty?
193
+
194
+ relative = relativize(path)
195
+ next unless relative
196
+
197
+ (@pending[relative] ||= Set.new).merge(lines)
198
+ end
199
+ end
200
+
201
+ # On failure the buffer is kept for the next interval, bounded by the
202
+ # ceiling so a long outage cannot grow memory without limit.
203
+ def push_pending
204
+ return true if @pending.empty?
205
+
206
+ @sink.store(@pending.transform_values(&:sort))
207
+ @pending.clear
208
+ true
209
+ rescue => e
210
+ warn "[SimpleCov::Production] flush failed (#{e.class}: #{e}); retrying next interval"
211
+ enforce_ceiling
212
+ false
213
+ end
214
+
215
+ # Oneshot's re-reporting is what makes dropping safe-ish: lines still being
216
+ # executed report again after the drop, and only code that ran during the
217
+ # outage and never again is lost.
218
+ def enforce_ceiling
219
+ return if @pending.sum { |_path, lines| lines.size } <= @max_buffered_lines
220
+
221
+ @pending.clear
222
+ warn "[SimpleCov::Production] buffer ceiling (#{@max_buffered_lines} lines) exceeded " \
223
+ "while the sink was unreachable; dropping buffered coverage"
224
+ end
225
+
226
+ def relativize(path)
227
+ return nil unless path.start_with?(@root_prefix)
228
+
229
+ path[@root_prefix.length..]
230
+ end
231
+
232
+ def warn_decline(reason)
233
+ warn "[SimpleCov::Production] not starting: #{reason}"
234
+ false
235
+ end
236
+
237
+ def install_at_exit
238
+ return if @at_exit_installed
239
+
240
+ @at_exit_installed = true
241
+ at_exit do
242
+ stop
243
+ end
244
+ end
245
+ end
246
+ end
247
+ end
248
+
249
+ require_relative "production/file_sink"
@@ -11,22 +11,11 @@ SimpleCov.profiles.define "rails" do
11
11
  group "Models", "app/models"
12
12
  group "Mailers", "app/mailers"
13
13
  group "Helpers", "app/helpers"
14
+ group "Views", "app/views"
14
15
  group "Jobs", %w[app/jobs app/workers]
15
16
  group "Libraries", "lib/"
16
17
 
17
- # Preserve the legacy `track_files` semantics (additive disk-discovery
18
- # without restricting the report's universe): write the ivar directly
19
- # so loading the profile doesn't emit the public-API deprecation. Users
20
- # migrating their own configs should prefer `cover "{app,lib}/**/*.rb"`,
21
- # which both injects unloaded files and scopes the report to the match
22
- # set — usually the intended behavior for a Rails project.
23
18
  @tracked_files = "{app,lib}/**/*.rb"
24
19
 
25
- # `parallelize(workers: ...)` forks worker processes that each run a
26
- # slice of the suite. Without subprocess support, the workers' coverage
27
- # is dropped on the floor and the parent records 0% for everything they
28
- # touched. Hooking `Process._fork` makes each worker re-call
29
- # `SimpleCov.start` with a unique command_name so the resultsets merge
30
- # correctly.
31
20
  merge_subprocesses true
32
21
  end
@@ -1,10 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  SimpleCov.profiles.define "root_filter" do
4
- # Exclude all files outside of simplecov root. Shares the regex with
5
- # SimpleCov::UselessResultsRemover so the root-prefix logic lives in one
6
- # place; this profile is the user-facing entry point that tools like
7
- # `SimpleCov.filtered` apply.
8
4
  skip do |src|
9
5
  src.filename !~ SimpleCov::UselessResultsRemover.root_regex
10
6
  end
@@ -1,26 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # A "no shortfalls allowed" profile: every coverage criterion is
4
- # enabled and held to 100%. Per
5
- # https://github.com/simplecov-ruby/simplecov/issues/1061, this lives as
6
- # an opt-in profile rather than the default — most projects can't
7
- # realistically start at 100% on day one — so a team that has paid down
8
- # the debt can flip the strict switch without re-typing the threshold
9
- # trio every time.
3
+ # Every coverage criterion enabled and held to 100%. An opt-in profile rather
4
+ # than the default (#1061), so a team that has paid down the debt can flip the
5
+ # strict switch without re-typing the threshold trio every time.
10
6
  #
11
- # JRuby gracefully degrades: `enable_coverage :branch` / `:method` are
12
- # accepted (they just add to the configured criteria set), but the
13
- # runtime can't actually measure them, so the corresponding stats never
14
- # materialize. `CoverageViolations` skips threshold lookups for
15
- # criteria not in the stats, so the `:branch` / `:method` clauses below
16
- # silently no-op on JRuby and only `:line` is enforced.
7
+ # JRuby gracefully degrades: `enable_coverage :branch` / `:method` are accepted
8
+ # but the runtime cannot measure them, and `CoverageViolations` skips threshold
9
+ # lookups for criteria not in the stats, so only `:line` is enforced there.
17
10
  #
18
- # `:eval` widens the universe of code held to 100% to include strings
19
- # passed through `Kernel#eval` (typically ERB templates, when the user
20
- # sets `ERB#filename=`). Guarded on `coverage_for_eval_supported?` so
21
- # the profile stays quiet on Ruby < 3.2, where `enable_coverage :eval`
22
- # would otherwise warn about the missing runtime support every time
23
- # the profile loads.
11
+ # `:eval` is guarded on `coverage_for_eval_supported?` so the profile stays
12
+ # quiet on Ruby < 3.2, where enabling it would warn every time it loads.
13
+
24
14
  SimpleCov.profiles.define "strict" do
25
15
  enable_coverage :branch
26
16
  enable_coverage :method
@@ -1,63 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleCov
4
- #
5
- # Profiles are SimpleCov configuration procs that can be easily
6
- # loaded using SimpleCov.start :rails and defined using
7
- # SimpleCov.profiles.define :foo do
8
- # # SimpleCov configuration here, same as in SimpleCov.configure
9
- # end
10
- #
4
+ # Configuration procs loaded with `SimpleCov.start :rails` and defined with
5
+ # `SimpleCov.profiles.define`.
11
6
  class Profiles < Hash
12
- #
13
- # Define a SimpleCov profile:
14
- # SimpleCov.profiles.define 'rails' do
15
- # # Same as SimpleCov.configure do .. here
16
- # end
17
- #
18
7
  def define(name, &blk)
19
8
  name = name.to_sym
20
- raise SimpleCov::ConfigurationError, "SimpleCov Profile '#{name}' is already defined" unless self[name].nil?
9
+ raise ConfigurationError, "SimpleCov Profile '#{name}' is already defined" unless self[name].nil?
21
10
 
22
11
  self[name] = blk
23
12
  end
24
13
 
25
- #
26
- # Applies the profile of given name on SimpleCov.configure
27
- #
28
14
  def load(name)
29
15
  SimpleCov.configure(&fetch_proc(name))
30
16
  end
31
17
 
32
- #
33
- # Returns the proc registered for the given profile name, autoloading
34
- # bundled or plugin-gem profiles on first lookup. Raises if the profile
35
- # cannot be located.
36
- #
37
- # Lookup order:
38
- # 1. already registered via #define
39
- # 2. require "simplecov/profiles/<name>" (bundled profiles)
40
- # 3. require "simplecov-profile-<name>" (third-party plugin gems)
41
- #
18
+ # Lookup order: already registered via #define, then
19
+ # `require "simplecov/profiles/<name>"` for the bundled profiles, then
20
+ # `require "simplecov-profile-<name>"` for third-party plugin gems.
42
21
  def fetch_proc(name)
43
22
  name = name.to_sym
44
23
  autoload_profile(name) unless key?(name)
45
- return self[name] if key?(name)
24
+ return fetch(name) if key?(name)
46
25
 
47
- raise SimpleCov::ConfigurationError, "Could not find SimpleCov Profile called '#{name}'"
26
+ raise ConfigurationError, "Could not find SimpleCov Profile called '#{name}'"
48
27
  end
49
28
 
50
- private
29
+ private
51
30
 
52
31
  def autoload_profile(name)
53
32
  require "simplecov/profiles/#{name}"
54
33
  rescue LoadError
55
34
  begin
56
- # simplecov:disable — third-party gem fallback (no such gem in test env)
57
35
  require "simplecov-profile-#{name}"
58
- # simplecov:enable
59
36
  rescue LoadError
60
- # fall through; #fetch_proc raises the user-facing error
37
+ # Neither a bundled profile nor a plugin gem; `fetch_proc` raises the
38
+ # user-facing error.
61
39
  end
62
40
  end
63
41
  end
@@ -1,15 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # The clobber-prevention backstop: an empty process stands down instead
4
- # of formatting over a fresher report a sibling produced. Split from
5
- # exit_handling.rb, which orchestrates the at_exit flow that consults
6
- # this. See issue #581.
3
+ # The clobber-prevention backstop: an empty process stands down instead of
4
+ # formatting over a fresher report a sibling produced. Typically fires when
5
+ # `SimpleCov.start` ran in a parent process that shelled out to the test runner
6
+ # (#581).
7
+
7
8
  module SimpleCov
8
9
  class << self
9
- # Returns true when our process has no coverage data to contribute
10
- # (after the resultset merge) and a newer report already exists on
11
- # disk. Typically fires when `SimpleCov.start` ran in a parent
12
- # process that shelled out to the test runner. See issue #581.
13
10
  def defer_to_existing_report?
14
11
  return false unless existing_report_newer_than_us?
15
12
 
@@ -19,15 +16,14 @@ module SimpleCov
19
16
  empty
20
17
  end
21
18
 
22
- # `.last_run.json` only exists after fully successful runs, so alone
23
- # it left the backstop inert when the child run failed the case
24
- # where clobbering its report hurts most. The report stamp is
25
- # touched by every formatting process regardless of exit status. The
26
- # rescue covers a file vanishing mid-at_exit (`rm -rf coverage`).
19
+ # `.last_run.json` only exists after fully successful runs, so alone it left
20
+ # the backstop inert when the child run failed, the case where clobbering its
21
+ # report hurts most. The report stamp is touched by every formatting process
22
+ # regardless of exit status. The rescue covers a file vanishing mid-at_exit.
27
23
  def existing_report_newer_than_us?
28
24
  return false unless process_start_time
29
25
 
30
- [SimpleCov::LastRun.last_run_path, SimpleCov::ReportStamp.path].any? do |path|
26
+ [LastRun.last_run_path, ReportStamp.path].any? do |path|
31
27
  File.mtime(path) > process_start_time
32
28
  rescue SystemCallError
33
29
  false
@@ -37,7 +33,7 @@ module SimpleCov
37
33
  def warn_about_deferred_report
38
34
  return unless print_errors
39
35
 
40
- ExitCodes.print_error SimpleCov::Color.colorize(
36
+ ExitCodes.print_error Color.colorize(
41
37
  "Skipping SimpleCov report — this process tracked no application code and a newer " \
42
38
  "report already exists at #{coverage_path}. This usually means SimpleCov.start ran in a " \
43
39
  "parent process (e.g. a Rakefile or Rails' Bundler.require) that shelled out to the test " \
@@ -3,24 +3,21 @@
3
3
  require "fileutils"
4
4
 
5
5
  module SimpleCov
6
- # Touches coverage/.report_stamp whenever a report is formatted, no
7
- # matter how the run ends. The clobber-prevention backstop
8
- # (`SimpleCov.defer_to_existing_report?`) needs an on-disk signal that
9
- # a fresher report exists, and `.last_run.json` can't be it alone:
10
- # that file is only written after fully successful runs, which left
11
- # the backstop inert exactly when the child run failed and its report
12
- # mattered most.
6
+ # Touches coverage/.report_stamp whenever a report is formatted, no matter how
7
+ # the run ends. The clobber-prevention backstop needs an on-disk signal that a
8
+ # fresher report exists, and `.last_run.json` can't be it alone: that file is
9
+ # only written after fully successful runs.
13
10
  module ReportStamp
14
11
  class << self
15
12
  def path
16
13
  File.join(SimpleCov.coverage_path, ".report_stamp")
17
14
  end
18
15
 
16
+ # A read-only or vanished coverage dir must not crash reporting; the stamp
17
+ # only powers the deferral heuristic.
19
18
  def touch
20
19
  FileUtils.touch(path)
21
20
  rescue SystemCallError
22
- # A read-only or vanished coverage dir must not crash reporting;
23
- # the stamp only powers the deferral heuristic.
24
21
  nil
25
22
  end
26
23
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ class Result
5
+ # The filter and grouping configuration a Result applies to its source files
6
+ # after building them. Each field defaults to the SimpleCov singleton's
7
+ # configuration, so ordinary callers never construct one; tests pass a custom
8
+ # instance to opt out of the project's filters or groups.
9
+ class FilterConfig
10
+ attr_reader :filters, :cover_filters, :groups
11
+
12
+ def initialize(filters: SimpleCov.filters, cover_filters: SimpleCov.cover_filters, groups: SimpleCov.groups)
13
+ @filters = filters
14
+ @cover_filters = cover_filters
15
+ @groups = groups
16
+ end
17
+ end
18
+ end
19
+ end