simplecov 0.22.0 → 1.0.2

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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +1058 -508
  4. data/doc/alternate-formatters.md +0 -5
  5. data/doc/commercial-services.md +5 -5
  6. data/exe/simplecov +11 -0
  7. data/lib/minitest/simplecov_plugin.rb +13 -5
  8. data/lib/simplecov/autostart.rb +11 -0
  9. data/lib/simplecov/cli/clean.rb +47 -0
  10. data/lib/simplecov/cli/coverage.rb +93 -0
  11. data/lib/simplecov/cli/diff.rb +152 -0
  12. data/lib/simplecov/cli/dotfile.rb +105 -0
  13. data/lib/simplecov/cli/merge.rb +118 -0
  14. data/lib/simplecov/cli/open.rb +50 -0
  15. data/lib/simplecov/cli/report.rb +89 -0
  16. data/lib/simplecov/cli/run.rb +36 -0
  17. data/lib/simplecov/cli/serve.rb +149 -0
  18. data/lib/simplecov/cli/uncovered.rb +108 -0
  19. data/lib/simplecov/cli.rb +150 -0
  20. data/lib/simplecov/color.rb +79 -0
  21. data/lib/simplecov/combine/branches_combiner.rb +28 -5
  22. data/lib/simplecov/combine/files_combiner.rb +47 -1
  23. data/lib/simplecov/combine/lines_combiner.rb +19 -17
  24. data/lib/simplecov/combine/methods_combiner.rb +49 -0
  25. data/lib/simplecov/combine/results_combiner.rb +7 -5
  26. data/lib/simplecov/command_guesser.rb +46 -32
  27. data/lib/simplecov/configuration/coverage.rb +175 -0
  28. data/lib/simplecov/configuration/coverage_criteria.rb +158 -0
  29. data/lib/simplecov/configuration/filters.rb +197 -0
  30. data/lib/simplecov/configuration/formatting.rb +129 -0
  31. data/lib/simplecov/configuration/ignored_entries.rb +63 -0
  32. data/lib/simplecov/configuration/merging.rb +164 -0
  33. data/lib/simplecov/configuration/thresholds.rb +178 -0
  34. data/lib/simplecov/configuration.rb +105 -412
  35. data/lib/simplecov/coverage_statistics.rb +15 -9
  36. data/lib/simplecov/coverage_violations.rb +149 -0
  37. data/lib/simplecov/defaults.rb +29 -22
  38. data/lib/simplecov/deprecation.rb +45 -0
  39. data/lib/simplecov/directive.rb +167 -0
  40. data/lib/simplecov/exit_codes/exit_code_handling.rb +8 -2
  41. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +19 -57
  42. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +45 -0
  43. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +17 -27
  44. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +41 -0
  45. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +38 -21
  46. data/lib/simplecov/exit_codes.rb +15 -0
  47. data/lib/simplecov/exit_handling.rb +158 -0
  48. data/lib/simplecov/file_list.rb +70 -19
  49. data/lib/simplecov/filter.rb +69 -24
  50. data/lib/simplecov/formatter/base.rb +102 -0
  51. data/lib/simplecov/formatter/html_formatter/public/application.css +1 -0
  52. data/lib/simplecov/formatter/html_formatter/public/application.js +18 -0
  53. data/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
  54. data/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
  55. data/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
  56. data/lib/simplecov/formatter/html_formatter/public/index.html +56 -0
  57. data/lib/simplecov/formatter/html_formatter.rb +85 -0
  58. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +90 -0
  59. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +128 -0
  60. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +99 -0
  61. data/lib/simplecov/formatter/json_formatter.rb +89 -0
  62. data/lib/simplecov/formatter/multi_formatter.rb +11 -7
  63. data/lib/simplecov/formatter/simple_formatter.rb +11 -11
  64. data/lib/simplecov/formatter.rb +4 -0
  65. data/lib/simplecov/last_run.rb +10 -3
  66. data/lib/simplecov/lines_classifier.rb +25 -13
  67. data/lib/simplecov/load_global_config.rb +9 -4
  68. data/lib/simplecov/parallel_adapters/base.rb +61 -0
  69. data/lib/simplecov/parallel_adapters/generic.rb +44 -0
  70. data/lib/simplecov/parallel_adapters/parallel_tests.rb +93 -0
  71. data/lib/simplecov/parallel_adapters.rb +83 -0
  72. data/lib/simplecov/parallel_coordination.rb +129 -0
  73. data/lib/simplecov/process.rb +52 -12
  74. data/lib/simplecov/profiles/bundler_filter.rb +1 -1
  75. data/lib/simplecov/profiles/hidden_filter.rb +1 -1
  76. data/lib/simplecov/profiles/rails.rb +24 -10
  77. data/lib/simplecov/profiles/root_filter.rb +6 -5
  78. data/lib/simplecov/profiles/strict.rb +32 -0
  79. data/lib/simplecov/profiles/test_frameworks.rb +1 -4
  80. data/lib/simplecov/profiles.rb +32 -3
  81. data/lib/simplecov/result/missing_source_files_reporter.rb +49 -0
  82. data/lib/simplecov/result/source_file_builder.rb +51 -0
  83. data/lib/simplecov/result.rb +108 -19
  84. data/lib/simplecov/result_adapter.rb +105 -8
  85. data/lib/simplecov/result_merger/legacy_format_adapter.rb +28 -0
  86. data/lib/simplecov/result_merger/resultset_file.rb +38 -0
  87. data/lib/simplecov/result_merger/resultset_store.rb +50 -0
  88. data/lib/simplecov/result_merger.rb +59 -91
  89. data/lib/simplecov/result_processing.rb +179 -0
  90. data/lib/simplecov/simulate_coverage.rb +54 -8
  91. data/lib/simplecov/source_file/branch.rb +1 -3
  92. data/lib/simplecov/source_file/branch_builder.rb +114 -0
  93. data/lib/simplecov/source_file/builder_context.rb +28 -0
  94. data/lib/simplecov/source_file/line.rb +9 -4
  95. data/lib/simplecov/source_file/line_builder.rb +43 -0
  96. data/lib/simplecov/source_file/method.rb +52 -0
  97. data/lib/simplecov/source_file/method_builder.rb +60 -0
  98. data/lib/simplecov/source_file/ruby_data_parser.rb +88 -0
  99. data/lib/simplecov/source_file/skip_chunks.rb +75 -0
  100. data/lib/simplecov/source_file/source_loader.rb +63 -0
  101. data/lib/simplecov/source_file/statistics.rb +57 -0
  102. data/lib/simplecov/source_file.rb +66 -232
  103. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +49 -0
  104. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +247 -0
  105. data/lib/simplecov/static_coverage_extractor/method_collector.rb +55 -0
  106. data/lib/simplecov/static_coverage_extractor/value_position.rb +82 -0
  107. data/lib/simplecov/static_coverage_extractor/visitor.rb +196 -0
  108. data/lib/simplecov/static_coverage_extractor.rb +110 -0
  109. data/lib/simplecov/useless_results_remover.rb +16 -7
  110. data/lib/simplecov/version.rb +1 -1
  111. data/lib/simplecov-html.rb +4 -0
  112. data/lib/simplecov.rb +178 -380
  113. data/lib/simplecov_json_formatter.rb +4 -0
  114. data/schemas/coverage-v1.0.schema.json +306 -0
  115. data/schemas/coverage.schema.json +306 -0
  116. data/sig/simplecov.rbs +1638 -0
  117. metadata +94 -57
  118. data/CHANGELOG.md +0 -191
  119. data/lib/simplecov/default_formatter.rb +0 -20
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module SimpleCov
6
+ module ParallelAdapters
7
+ # Adapter for [grosser/parallel_tests](https://github.com/grosser/parallel_tests).
8
+ # This is the historical default — SimpleCov has special-cased
9
+ # parallel_tests since 0.18 — and remains the most precise option for
10
+ # projects on it. Detection requires the full native coordination
11
+ # contract: the `ParallelTests` constant has been loaded,
12
+ # `TEST_ENV_NUMBER` is set, and `PARALLEL_PID_FILE` is set. The pid-file
13
+ # path is required because the native wait API reads it with `ENV.fetch`.
14
+ # When a runner only provides the env-var convention, GenericAdapter is
15
+ # the correct coordination path.
16
+ class ParallelTestsAdapter < Base
17
+ class << self
18
+ def active?
19
+ return false if SimpleCov.parallel_tests == false
20
+
21
+ ensure_loaded
22
+ # !! to coerce `defined?` (returns nil or "constant") to a proper bool.
23
+ !!(defined?(::ParallelTests) && native_parallel_tests_environment?)
24
+ end
25
+
26
+ # Pick the *first* started process to do the final-result work,
27
+ # not the last. The parallel_tests README recommends
28
+ # `first_process?` for "do something once after every worker
29
+ # finishes" hooks, so user code that has its own
30
+ # `wait_for_other_processes_to_finish` in an `RSpec.after(:suite)`
31
+ # overwhelmingly waits in the first process — picking the same
32
+ # side avoids the cross-process deadlock #922 reported. Also
33
+ # handles `PARALLEL_TEST_GROUPS=1` naturally (the only worker's
34
+ # `TEST_ENV_NUMBER` is "" and `first_process?` tests for that
35
+ # empty string).
36
+ def first_worker?
37
+ ::ParallelTests.first_process?
38
+ end
39
+
40
+ def wait_for_siblings
41
+ return unless native_parallel_tests_environment?
42
+
43
+ ::ParallelTests.wait_for_other_processes_to_finish
44
+ end
45
+
46
+ # The native wait blocks until every sibling process exits, but only
47
+ # when the pid-file contract is present (see `wait_for_siblings`).
48
+ def native_wait?
49
+ native_parallel_tests_environment?
50
+ end
51
+
52
+ def expected_worker_count
53
+ ENV["PARALLEL_TEST_GROUPS"]&.to_i || 1
54
+ end
55
+
56
+ # Auto-require `parallel_tests` when it's installed AND the env
57
+ # vars it sets are present, so callers can rely on
58
+ # `defined?(::ParallelTests)` downstream. parallel_tests is an
59
+ # optional dependency (see https://github.com/grosser/parallel_tests/issues/772),
60
+ # and `TEST_ENV_NUMBER` / `PARALLEL_TEST_GROUPS` are commonly set
61
+ # for other reasons (custom subprocess coordination, CI sharding,
62
+ # the parallel_rspec gem which intentionally mirrors the env-var
63
+ # convention), so a missing gem is treated as "user isn't using
64
+ # parallel_tests" — silently skip and let GenericAdapter handle
65
+ # it. Users who want to override the auto-detect can set
66
+ # `SimpleCov.parallel_tests true` (force on) or `false` (force
67
+ # off). See #1018.
68
+ def ensure_loaded
69
+ return if defined?(::ParallelTests) # simplecov:disable — only true after a previous load
70
+ return if SimpleCov.parallel_tests == false # simplecov:disable — only fires when user opts out
71
+ # simplecov:disable — env-var-only path
72
+ return unless SimpleCov.parallel_tests || env_suggests_parallel_tests?
73
+
74
+ # simplecov:disable — only fires under a real parallel_tests setup
75
+ require "parallel_tests"
76
+ rescue LoadError
77
+ # Gem isn't installed; stay quiet — warning here regressed
78
+ # users who use those env vars for their own subprocess
79
+ # coordination.
80
+ # simplecov:enable
81
+ end
82
+
83
+ def env_suggests_parallel_tests?
84
+ ENV.key?("TEST_ENV_NUMBER") && ENV.key?("PARALLEL_TEST_GROUPS")
85
+ end
86
+
87
+ def native_parallel_tests_environment?
88
+ ENV.key?("TEST_ENV_NUMBER") && ENV.key?("PARALLEL_PID_FILE")
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "parallel_adapters/base"
4
+ require_relative "parallel_adapters/parallel_tests"
5
+ require_relative "parallel_adapters/generic"
6
+
7
+ module SimpleCov
8
+ # Registry + selection for parallel-test-runner adapters. An adapter
9
+ # answers a small fixed set of questions on SimpleCov's behalf:
10
+ #
11
+ # - `active?` — are WE the runner in charge for this process?
12
+ # - `first_worker?` — should this process do the final-result work?
13
+ # - `wait_for_siblings` — block until siblings finish (optional)
14
+ # - `expected_worker_count` — how many workers total
15
+ #
16
+ # `SimpleCov::ParallelAdapters::Base` provides safe no-op defaults; two
17
+ # adapters ship out of the box:
18
+ #
19
+ # - `ParallelTestsAdapter` — wraps the grosser/parallel_tests gem
20
+ # (precise sync + first-process detection via the gem's own API).
21
+ # - `GenericAdapter` — env-var-only detection for runners that follow
22
+ # the parallel_tests `TEST_ENV_NUMBER` convention but don't ship a
23
+ # Ruby API (parallel_rspec, custom CI sharding, knapsack-style
24
+ # splitters). See https://github.com/simplecov-ruby/simplecov/issues/1065.
25
+ #
26
+ # Users can plug in additional adapters:
27
+ #
28
+ # SimpleCov::ParallelAdapters.register MyRunnerAdapter
29
+ #
30
+ # An adapter just needs to be a class responding to the four methods
31
+ # above. Subclass `SimpleCov::ParallelAdapters::Base` to inherit the
32
+ # no-op defaults and override only what you need (the contract methods
33
+ # are defined as class methods, so plain inheritance is what carries
34
+ # them through; `extend Base` won't pick them up).
35
+ module ParallelAdapters
36
+ module_function
37
+
38
+ # Adapters in selection order. ParallelTestsAdapter first (most
39
+ # specific — uses the gem's own API when the gem is loaded); then
40
+ # GenericAdapter as the env-var fallback. User-registered adapters
41
+ # are prepended (#register puts new entries at the front) so
42
+ # downstream code can override the built-ins by registering a more
43
+ # specific match.
44
+ def adapters
45
+ @adapters ||= [ParallelTestsAdapter, GenericAdapter]
46
+ end
47
+
48
+ # Register a custom adapter. Newly registered adapters are inserted
49
+ # at the front of the selection list so a custom adapter for a
50
+ # specific runner takes precedence over the built-in ParallelTests
51
+ # and Generic adapters.
52
+ #
53
+ # class MyRunnerAdapter < SimpleCov::ParallelAdapters::Base
54
+ # def self.active? = ENV["MY_RUNNER_PID"]
55
+ # def self.first_worker? = ENV["MY_RUNNER_PID"].to_i == 1
56
+ # def self.expected_worker_count = ENV["MY_RUNNER_WORKERS"].to_i
57
+ # end
58
+ #
59
+ # SimpleCov::ParallelAdapters.register MyRunnerAdapter
60
+ def register(adapter)
61
+ reset_current!
62
+ adapters.unshift(adapter) unless adapters.include?(adapter)
63
+ adapter
64
+ end
65
+
66
+ # The adapter SimpleCov should consult for this process — the first
67
+ # registered adapter whose `active?` returns true. Returns nil when
68
+ # no adapter is active (i.e., we're not running under any recognized
69
+ # parallel test runner), in which case the caller should treat the
70
+ # process as single-worker.
71
+ def current
72
+ return @current if defined?(@current)
73
+
74
+ @current = adapters.find(&:active?)
75
+ end
76
+
77
+ # Clear the memoized `current` selection. Primarily for tests that
78
+ # mutate env vars between examples; production runs are single-shot.
79
+ def reset_current!
80
+ remove_instance_variable(:@current) if defined?(@current)
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ # simplecov:disable
4
+ # Methods below only fire under a parallel test runner; not reachable
5
+ # from a single-process rspec run. Cucumber's test_projects exercise
6
+ # the parallel_tests integration end-to-end in subprocesses, but
7
+ # those subprocesses don't merge their Coverage data back into the
8
+ # parent this dogfood report measures.
9
+ module SimpleCov
10
+ class << self
11
+ # @api private
12
+ def final_result_process?
13
+ adapter = SimpleCov::ParallelAdapters.current
14
+ # No recognized parallel-test adapter. A subprocess forked while
15
+ # coverage was running is never the final reporter — the process that
16
+ # spawned it merges every slice and produces the report. Without this,
17
+ # fork-based runners that don't set TEST_ENV_NUMBER (e.g. Minitest's
18
+ # `parallelize`) have every worker produce the final report and its
19
+ # warnings. See issue #1171.
20
+ return !forked_subprocess? unless adapter
21
+
22
+ adapter.first_worker?
23
+ end
24
+
25
+ # @api private
26
+ def wait_for_other_processes
27
+ adapter = SimpleCov::ParallelAdapters.current
28
+ return unless adapter && final_result_process?
29
+
30
+ # Native synchronization first (adapters that wrap a runner with a
31
+ # real "wait" primitive — parallel_tests'
32
+ # `wait_for_other_processes_to_finish` — implement this; adapters
33
+ # without a native API no-op and rely on the polling fallback below).
34
+ adapter.wait_for_siblings
35
+
36
+ # The native wait can return before sibling at_exit handlers finish
37
+ # writing resultsets, and adapters without a native wait have
38
+ # nothing else. Either way, poll the resultset cache until all
39
+ # expected workers have reported or a timeout is reached. Capture
40
+ # the outcome so `ready_to_process_results?` can suppress min/max
41
+ # threshold checks against a partial total.
42
+ @parallel_results_complete =
43
+ wait_for_parallel_results(adapter.expected_worker_count, native_wait: adapter.native_wait?)
44
+ end
45
+
46
+ # @api private — true when every sibling reported its resultset
47
+ # before the wait deadline. Defaults to true outside a parallel
48
+ # run (when `wait_for_other_processes` is a no-op).
49
+ def parallel_results_complete?
50
+ defined?(@parallel_results_complete) ? @parallel_results_complete : true
51
+ end
52
+
53
+ # @api private — seconds the resultset count must hold steady, after a
54
+ # native wait, before we accept fewer than `expected` workers as final.
55
+ PARALLEL_RESULTS_SETTLE = 0.5
56
+ private_constant :PARALLEL_RESULTS_SETTLE
57
+
58
+ # @api private — returns true when the reporting worker has every
59
+ # resultset it's going to get, false on timeout. Single-process runs
60
+ # (expected <= 1) short-circuit to true with no waiting.
61
+ #
62
+ # Normally we poll until `expected` (= PARALLEL_TEST_GROUPS) workers have
63
+ # reported or `SimpleCov.parallel_wait_timeout` elapses; raise that setting
64
+ # when a slow worker routinely finishes well after the others.
65
+ #
66
+ # When a native wait already confirmed every sibling PROCESS exited
67
+ # (`native_wait`), no further resultset will appear, so a count below
68
+ # `expected` just means some workers produced none — e.g. parallel_test
69
+ # groups that got no spec file on a machine with more cores than files.
70
+ # Once the count then holds steady for `PARALLEL_RESULTS_SETTLE` we accept
71
+ # it as final rather than blocking for the whole timeout. Without a native
72
+ # wait (GenericAdapter) we can't tell an idle worker from a slow one, so we
73
+ # keep waiting the full timeout.
74
+ def wait_for_parallel_results(expected, native_wait: false)
75
+ return true unless expected > 1 # simplecov:disable branch — only false in real parallel runs
76
+
77
+ deadline = monotonic_time + parallel_wait_timeout
78
+ tracker = {count: 0, since: monotonic_time}
79
+ loop do
80
+ seen = SimpleCov::ResultMerger.read_resultset.size
81
+ return true if seen >= expected
82
+ return true if native_wait && resultset_count_settled?(tracker, seen)
83
+ return false if parallel_wait_timed_out?(deadline, expected, seen)
84
+
85
+ sleep 0.1
86
+ end
87
+ end
88
+
89
+ # Track whether the resultset count has held steady (and positive) for
90
+ # `PARALLEL_RESULTS_SETTLE` seconds. `tracker` carries the last count and
91
+ # the time it last changed across poll iterations.
92
+ def resultset_count_settled?(tracker, count)
93
+ if count > tracker[:count]
94
+ tracker[:count] = count
95
+ tracker[:since] = monotonic_time
96
+ return false
97
+ end
98
+
99
+ count.positive? && (monotonic_time - tracker[:since]) >= PARALLEL_RESULTS_SETTLE
100
+ end
101
+
102
+ def monotonic_time
103
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
104
+ end
105
+
106
+ # @api private — true once the wait deadline has passed; warns on
107
+ # the first timeout so the user knows the merged total is partial.
108
+ def parallel_wait_timed_out?(deadline, expected, seen)
109
+ return false unless monotonic_time > deadline
110
+
111
+ warn_about_incomplete_parallel_results(expected, seen)
112
+ true
113
+ end
114
+
115
+ # @api private
116
+ def warn_about_incomplete_parallel_results(expected, seen)
117
+ return unless print_errors
118
+
119
+ warn SimpleCov::Color.colorize(
120
+ "Only #{seen} of #{expected} parallel-test workers reported within " \
121
+ "#{parallel_wait_timeout}s, so coverage totals are partial and minimum / " \
122
+ "maximum coverage checks are skipped for this run. Increase " \
123
+ "SimpleCov.parallel_wait_timeout if a worker routinely needs longer.",
124
+ :yellow
125
+ )
126
+ end
127
+ end
128
+ end
129
+ # simplecov:enable
@@ -1,19 +1,59 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Process
3
+ # Hooks `Process._fork` (Ruby 3.1+) so child processes inherit SimpleCov's
4
+ # coverage tracking when `SimpleCov.enable_for_subprocesses?` is set.
5
+ #
6
+ # `Process._fork` is the official extension point: `Kernel#fork`,
7
+ # `Process.fork`, `IO.popen("-")`, and similar all funnel through it.
8
+ # Hooking `_fork` (instead of redefining `Process.fork`) composes
9
+ # correctly with other libraries doing the same — they each prepend
10
+ # their own module and chain via `super`.
11
+
12
+ # Reopened here for the fork-related at_exit plumbing that only matters
13
+ # when subprocess support is loaded.
14
+ module SimpleCov
4
15
  class << self
5
- def fork_with_simplecov(&block)
6
- if defined?(SimpleCov) && SimpleCov.running
7
- fork_without_simplecov do
8
- SimpleCov.at_fork.call(Process.pid)
9
- block.call if block_given?
10
- end
11
- else
12
- fork_without_simplecov(&block)
13
- end
16
+ # Forked children inherit at_exit state that is wrong for them:
17
+ # @at_exit_hook_installed may describe a hook the parent already
18
+ # consumed before forking (Minitest autorun runs the suite inside the
19
+ # parent's at_exit, after SimpleCov's own hook has fired), and
20
+ # external_at_exit may point at a Minitest.after_run deferral that is
21
+ # pid-pinned to the parent. Reset both so the at_fork proc's
22
+ # SimpleCov.start installs a fresh hook that actually fires at child
23
+ # exit. See issue #1227.
24
+ def reset_inherited_at_exit_state!
25
+ @at_exit_hook_installed = false
26
+ self.external_at_exit = false
14
27
  end
28
+ end
15
29
 
16
- alias fork_without_simplecov fork
17
- alias fork fork_with_simplecov
30
+ # Prepended onto Process's singleton class so every fork — direct or
31
+ # via Kernel#fork / IO.popen — re-runs SimpleCov's at_fork callback in
32
+ # the child.
33
+ module ProcessForkHook
34
+ def _fork
35
+ active = defined?(SimpleCov) && Coverage.running?
36
+ # Assign the next serial in the PARENT, before the fork, so the child
37
+ # inherits its own stable ordinal via copy-on-write. The default
38
+ # at_fork uses it (instead of the child pid) to name the subprocess's
39
+ # result, keeping that name identical across runs. See issue #1171.
40
+ SimpleCov.next_subprocess_serial! if active
41
+ pid = super
42
+ if pid.zero? && active
43
+ # Mark the child here, independent of whatever custom at_fork block
44
+ # the user installed, so `final_result_process?` can keep forked
45
+ # workers from each producing the final report. See issue #1171.
46
+ SimpleCov.mark_forked_subprocess!
47
+ # Without this, a child forked under Minitest autorun measures
48
+ # coverage and then silently discards it: every inherited exit
49
+ # route is dead in the child (see the method's comment). A custom
50
+ # at_fork runs afterwards and can still override. See issue #1227.
51
+ SimpleCov.reset_inherited_at_exit_state!
52
+ SimpleCov.at_fork.call(::Process.pid)
53
+ end
54
+ pid
55
+ end
18
56
  end
19
57
  end
58
+
59
+ Process.singleton_class.prepend(SimpleCov::ProcessForkHook)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  SimpleCov.profiles.define "bundler_filter" do
4
- add_filter "/vendor/bundle/"
4
+ skip "/vendor/bundle/"
5
5
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  SimpleCov.profiles.define "hidden_filter" do
4
- add_filter %r{^/\..*}
4
+ skip(/\A\..*/)
5
5
  end
@@ -3,16 +3,30 @@
3
3
  SimpleCov.profiles.define "rails" do
4
4
  load_profile "test_frameworks"
5
5
 
6
- add_filter %r{^/config/}
7
- add_filter %r{^/db/}
6
+ skip %r{\Aconfig/}
7
+ skip %r{\Adb/}
8
8
 
9
- add_group "Controllers", "app/controllers"
10
- add_group "Channels", "app/channels"
11
- add_group "Models", "app/models"
12
- add_group "Mailers", "app/mailers"
13
- add_group "Helpers", "app/helpers"
14
- add_group "Jobs", %w[app/jobs app/workers]
15
- add_group "Libraries", "lib/"
9
+ group "Controllers", "app/controllers"
10
+ group "Channels", "app/channels"
11
+ group "Models", "app/models"
12
+ group "Mailers", "app/mailers"
13
+ group "Helpers", "app/helpers"
14
+ group "Jobs", %w[app/jobs app/workers]
15
+ group "Libraries", "lib/"
16
16
 
17
- track_files "{app,lib}/**/*.rb"
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
+ @tracked_files = "{app,lib}/**/*.rb"
24
+
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
+ merge_subprocesses true
18
32
  end
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  SimpleCov.profiles.define "root_filter" do
4
- # Exclude all files outside of simplecov root
5
- root_filter = nil
6
- add_filter do |src|
7
- root_filter ||= /\A#{Regexp.escape(SimpleCov.root + File::SEPARATOR)}/io
8
- src.filename !~ root_filter
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
+ skip do |src|
9
+ src.filename !~ SimpleCov::UselessResultsRemover.root_regex
9
10
  end
10
11
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
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.
10
+ #
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.
17
+ #
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.
24
+ SimpleCov.profiles.define "strict" do
25
+ enable_coverage :branch
26
+ enable_coverage :method
27
+ # simplecov:disable branch — dogfood runs on Ruby >= 3.2 only, so
28
+ # the else arm (eval coverage not supported) is unreachable from CI.
29
+ enable_coverage :eval if coverage_for_eval_supported?
30
+ # simplecov:enable
31
+ minimum_coverage line: 100, branch: 100, method: 100
32
+ end
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  SimpleCov.profiles.define "test_frameworks" do
4
- add_filter "/test/"
5
- add_filter "/features/"
6
- add_filter "/spec/"
7
- add_filter "/autotest/"
4
+ skip %r{\A(test|features|spec|autotest)/}
8
5
  end
@@ -17,7 +17,7 @@ module SimpleCov
17
17
  #
18
18
  def define(name, &blk)
19
19
  name = name.to_sym
20
- raise "SimpleCov Profile '#{name}' is already defined" unless self[name].nil?
20
+ raise SimpleCov::ConfigurationError, "SimpleCov Profile '#{name}' is already defined" unless self[name].nil?
21
21
 
22
22
  self[name] = blk
23
23
  end
@@ -26,10 +26,39 @@ module SimpleCov
26
26
  # Applies the profile of given name on SimpleCov.configure
27
27
  #
28
28
  def load(name)
29
+ SimpleCov.configure(&fetch_proc(name))
30
+ end
31
+
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
+ #
42
+ def fetch_proc(name)
29
43
  name = name.to_sym
30
- raise "Could not find SimpleCov Profile called '#{name}'" unless key?(name)
44
+ autoload_profile(name) unless key?(name)
45
+ return self[name] if key?(name)
46
+
47
+ raise SimpleCov::ConfigurationError, "Could not find SimpleCov Profile called '#{name}'"
48
+ end
49
+
50
+ private
31
51
 
32
- SimpleCov.configure(&self[name])
52
+ def autoload_profile(name)
53
+ require "simplecov/profiles/#{name}"
54
+ rescue LoadError
55
+ begin
56
+ # simplecov:disable — third-party gem fallback (no such gem in test env)
57
+ require "simplecov-profile-#{name}"
58
+ # simplecov:enable
59
+ rescue LoadError
60
+ # fall through; #fetch_proc raises the user-facing error
61
+ end
33
62
  end
34
63
  end
35
64
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ class Result
5
+ # When a resultset references source files that don't exist on the local
6
+ # filesystem they're silently dropped — which produces an empty `0 / 0
7
+ # (100.00%)` report that looks like success but isn't. Emit a single
8
+ # warning summarizing the drop and, when every entry was lost, point at
9
+ # the typical cause (`SimpleCov.collate` invoked from a machine or path
10
+ # different from where the resultsets were generated). See #980.
11
+ class MissingSourceFilesReporter
12
+ def initialize(missing_paths, input_size:, every_entry_dropped:)
13
+ @missing_paths = missing_paths
14
+ @input_size = input_size
15
+ @every_entry_dropped = every_entry_dropped
16
+ end
17
+
18
+ def warn!
19
+ warn SimpleCov::Color.colorize(message, :yellow)
20
+ end
21
+
22
+ def message
23
+ @every_entry_dropped ? all_missing_warning : partial_missing_warning
24
+ end
25
+
26
+ private
27
+
28
+ def all_missing_warning
29
+ "SimpleCov dropped all #{@missing_paths.size} source file(s) from the result — " \
30
+ "none of the paths in the resultset exist on this filesystem: " \
31
+ "#{summary}. If you're running `SimpleCov.collate`, the source " \
32
+ "files must be available at the same absolute paths as when the individual resultsets " \
33
+ "were generated."
34
+ end
35
+
36
+ def partial_missing_warning
37
+ "SimpleCov dropped #{@missing_paths.size} source file(s) from the result because " \
38
+ "they don't exist on this filesystem: #{summary}. They were " \
39
+ "tracked in the resultset but have since moved or been removed."
40
+ end
41
+
42
+ def summary
43
+ sample = @missing_paths.first(5).join(", ")
44
+ remaining = @missing_paths.size - 5
45
+ remaining.positive? ? "#{sample} (+#{remaining} more)" : sample
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ class Result
5
+ # Constructs `SimpleCov::SourceFile` instances from a raw coverage
6
+ # hash, sorts them by filename, and surfaces filenames whose source
7
+ # is no longer present on disk so the caller can warn about the
8
+ # silent drop (see #980).
9
+ class SourceFileBuilder
10
+ attr_reader :missing_source_files
11
+
12
+ def initialize(original_result, not_loaded_files:)
13
+ @original_result = original_result
14
+ @not_loaded_files = not_loaded_files
15
+ @missing_source_files = []
16
+ end
17
+
18
+ def call
19
+ SimpleCov::FileList.new(
20
+ @original_result
21
+ .filter_map { |filename, coverage| build_source_file(filename, coverage) }
22
+ .sort_by(&:filename)
23
+ )
24
+ end
25
+
26
+ private
27
+
28
+ def build_source_file(filename, coverage)
29
+ unless File.file?(filename)
30
+ @missing_source_files << filename
31
+ return
32
+ end
33
+
34
+ SimpleCov::SourceFile.new(
35
+ filename,
36
+ stringify_outer_keys(coverage),
37
+ loaded: !@not_loaded_files.include?(filename)
38
+ )
39
+ end
40
+
41
+ # `Coverage.result` returns symbol keys (`:lines`, `:branches`,
42
+ # `:methods`); resultsets loaded from disk are already string-keyed.
43
+ # SourceFile reads with strings, and handles both Array and
44
+ # stringified-Array branch/method keys via `restore_ruby_data_structure`,
45
+ # so only the outer hash needs normalizing.
46
+ def stringify_outer_keys(coverage)
47
+ coverage.transform_keys(&:to_s)
48
+ end
49
+ end
50
+ end
51
+ end