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.
- checksums.yaml +4 -4
- data/README.md +148 -5
- data/exe/simplecov +0 -3
- data/lib/minitest/simplecov_plugin.rb +12 -9
- data/lib/simplecov/atomic_file.rb +16 -19
- data/lib/simplecov/autostart.rb +5 -7
- data/lib/simplecov/baseline/parser.rb +58 -0
- data/lib/simplecov/baseline.rb +170 -0
- data/lib/simplecov/cli/affected/changed_files.rb +57 -0
- data/lib/simplecov/cli/affected/selection.rb +117 -0
- data/lib/simplecov/cli/affected.rb +148 -0
- data/lib/simplecov/cli/badge/svg.rb +70 -0
- data/lib/simplecov/cli/badge.rb +62 -0
- data/lib/simplecov/cli/clean.rb +23 -24
- data/lib/simplecov/cli/command_helpers.rb +55 -19
- data/lib/simplecov/cli/completions/scripts.rb +114 -0
- data/lib/simplecov/cli/completions.rb +83 -0
- data/lib/simplecov/cli/coverage.rb +25 -28
- data/lib/simplecov/cli/coverage_file.rb +58 -14
- data/lib/simplecov/cli/dead_code/output.rb +100 -0
- data/lib/simplecov/cli/dead_code.rb +139 -0
- data/lib/simplecov/cli/diff/output.rb +45 -0
- data/lib/simplecov/cli/diff.rb +35 -67
- data/lib/simplecov/cli/dotfile.rb +83 -38
- data/lib/simplecov/cli/git.rb +41 -0
- data/lib/simplecov/cli/history/output.rb +135 -0
- data/lib/simplecov/cli/history.rb +67 -0
- data/lib/simplecov/cli/merge.rb +34 -39
- data/lib/simplecov/cli/open.rb +12 -15
- data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
- data/lib/simplecov/cli/patch/output.rb +135 -0
- data/lib/simplecov/cli/patch.rb +169 -0
- data/lib/simplecov/cli/ratchet/output.rb +44 -0
- data/lib/simplecov/cli/ratchet.rb +83 -0
- data/lib/simplecov/cli/report.rb +18 -24
- data/lib/simplecov/cli/run.rb +8 -9
- data/lib/simplecov/cli/serve/report_preparer.rb +7 -6
- data/lib/simplecov/cli/serve/static_file_handler.rb +62 -56
- data/lib/simplecov/cli/serve.rb +27 -24
- data/lib/simplecov/cli/show/annotator.rb +87 -0
- data/lib/simplecov/cli/show/sweep.rb +36 -0
- data/lib/simplecov/cli/show.rb +133 -0
- data/lib/simplecov/cli/status/facts.rb +87 -0
- data/lib/simplecov/cli/status.rb +93 -0
- data/lib/simplecov/cli/tests/redundancy.rb +78 -0
- data/lib/simplecov/cli/tests.rb +148 -0
- data/lib/simplecov/cli/uncovered/misses.rb +37 -0
- data/lib/simplecov/cli/uncovered.rb +70 -32
- data/lib/simplecov/cli/usage.rb +152 -0
- data/lib/simplecov/cli/watch/live_report.rb +73 -0
- data/lib/simplecov/cli/watch/narrator.rb +55 -0
- data/lib/simplecov/cli/watch/poller.rb +45 -0
- data/lib/simplecov/cli/watch/session.rb +134 -0
- data/lib/simplecov/cli/watch/test_plan.rb +43 -0
- data/lib/simplecov/cli/watch.rb +85 -0
- data/lib/simplecov/cli.rb +43 -92
- data/lib/simplecov/color.rb +19 -37
- data/lib/simplecov/combine/branches_combiner.rb +7 -31
- data/lib/simplecov/combine/coverage_accumulator.rb +49 -142
- data/lib/simplecov/combine/identity_interner.rb +10 -13
- data/lib/simplecov/combine/interned_counts.rb +7 -9
- data/lib/simplecov/combine/lines_combiner.rb +20 -44
- data/lib/simplecov/combine/methods_combiner.rb +9 -35
- data/lib/simplecov/combine/results_combiner.rb +4 -21
- data/lib/simplecov/combine.rb +0 -6
- data/lib/simplecov/command_guesser.rb +20 -34
- data/lib/simplecov/configuration/baseline.rb +31 -0
- data/lib/simplecov/configuration/coverage.rb +91 -79
- data/lib/simplecov/configuration/coverage_criteria.rb +43 -54
- data/lib/simplecov/configuration/deprecations.rb +21 -0
- data/lib/simplecov/configuration/eval_coverage.rb +6 -10
- data/lib/simplecov/configuration/filters.rb +40 -73
- data/lib/simplecov/configuration/formatting.rb +74 -83
- data/lib/simplecov/configuration/groups.rb +19 -13
- data/lib/simplecov/configuration/history.rb +44 -0
- data/lib/simplecov/configuration/ignored_entries.rb +31 -22
- data/lib/simplecov/configuration/merging.rb +94 -62
- data/lib/simplecov/configuration/missed_caps.rb +51 -0
- data/lib/simplecov/configuration/production.rb +23 -0
- data/lib/simplecov/configuration/test_tracking.rb +48 -0
- data/lib/simplecov/configuration/thresholds.rb +33 -62
- data/lib/simplecov/configuration/view_coverage.rb +38 -0
- data/lib/simplecov/configuration.rb +70 -76
- data/lib/simplecov/configuration_error.rb +5 -0
- data/lib/simplecov/context_map/union.rb +81 -0
- data/lib/simplecov/context_map.rb +187 -0
- data/lib/simplecov/coverage_json.rb +2 -5
- data/lib/simplecov/coverage_statistics.rb +14 -24
- data/lib/simplecov/coverage_violations.rb +157 -64
- data/lib/simplecov/current_run.rb +61 -0
- data/lib/simplecov/defaults.rb +4 -23
- data/lib/simplecov/deprecation.rb +45 -27
- data/lib/simplecov/directive.rb +46 -62
- data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
- data/lib/simplecov/exit_codes/check.rb +17 -11
- data/lib/simplecov/exit_codes/exit_code_handling.rb +21 -8
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +7 -10
- data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
- data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +7 -12
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +11 -10
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +7 -9
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +16 -17
- data/lib/simplecov/exit_codes.rb +16 -10
- data/lib/simplecov/exit_handling.rb +94 -59
- data/lib/simplecov/file_list.rb +20 -51
- data/lib/simplecov/filter.rb +43 -66
- data/lib/simplecov/formatter/base.rb +37 -55
- data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +29 -31
- data/lib/simplecov/formatter/html_formatter/public/index.html +15 -9
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +74 -17
- data/lib/simplecov/formatter/html_formatter.rb +24 -41
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +50 -16
- data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +43 -29
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +14 -10
- data/lib/simplecov/formatter/json_formatter.rb +7 -13
- data/lib/simplecov/formatter/multi_formatter.rb +5 -11
- data/lib/simplecov/formatter/simple_formatter.rb +2 -8
- data/lib/simplecov/formatter.rb +14 -10
- data/lib/simplecov/group_names.rb +13 -12
- data/lib/simplecov/history.rb +115 -0
- data/lib/simplecov/last_run.rb +6 -9
- data/lib/simplecov/lines_classifier.rb +31 -16
- data/lib/simplecov/load_global_config.rb +7 -7
- data/lib/simplecov/parallel_adapters/base.rb +13 -41
- data/lib/simplecov/parallel_adapters/generic.rb +7 -19
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +21 -42
- data/lib/simplecov/parallel_adapters.rb +24 -55
- data/lib/simplecov/parallel_coordination.rb +39 -49
- data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
- data/lib/simplecov/parallel_result_merger.rb +67 -126
- data/lib/simplecov/process.rb +14 -32
- data/lib/simplecov/production/error.rb +11 -0
- data/lib/simplecov/production/file_sink.rb +130 -0
- data/lib/simplecov/production.rb +249 -0
- data/lib/simplecov/profiles/rails.rb +1 -12
- data/lib/simplecov/profiles/root_filter.rb +0 -4
- data/lib/simplecov/profiles/strict.rb +9 -19
- data/lib/simplecov/profiles.rb +11 -33
- data/lib/simplecov/report_deferral.rb +11 -15
- data/lib/simplecov/report_stamp.rb +6 -9
- data/lib/simplecov/result/filter_config.rb +19 -0
- data/lib/simplecov/result/missing_source_files_reporter.rb +8 -9
- data/lib/simplecov/result/serialization.rb +43 -0
- data/lib/simplecov/result/source_file_builder.rb +7 -12
- data/lib/simplecov/result.rb +68 -117
- data/lib/simplecov/result_adapter.rb +56 -68
- data/lib/simplecov/result_merger/contexts.rb +22 -0
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +4 -8
- data/lib/simplecov/result_merger/resultset_file.rb +20 -27
- data/lib/simplecov/result_merger/resultset_run_identity.rb +23 -25
- data/lib/simplecov/result_merger/resultset_store.rb +23 -14
- data/lib/simplecov/result_merger/unloaded_files.rb +24 -32
- data/lib/simplecov/result_merger.rb +59 -58
- data/lib/simplecov/result_processing.rb +74 -119
- data/lib/simplecov/run_identity.rb +16 -18
- data/lib/simplecov/simulate_coverage.rb +34 -50
- data/lib/simplecov/source_file/branch.rb +8 -33
- data/lib/simplecov/source_file/branch_builder.rb +28 -47
- data/lib/simplecov/source_file/builder_context.rb +6 -13
- data/lib/simplecov/source_file/line.rb +13 -33
- data/lib/simplecov/source_file/line_builder.rb +9 -15
- data/lib/simplecov/source_file/method.rb +3 -7
- data/lib/simplecov/source_file/method_builder.rb +6 -15
- data/lib/simplecov/source_file/ruby_data_parser.rb +59 -66
- data/lib/simplecov/source_file/skip_chunks.rb +19 -28
- data/lib/simplecov/source_file/source_loader.rb +45 -35
- data/lib/simplecov/source_file/statistics.rb +10 -9
- data/lib/simplecov/source_file.rb +21 -51
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +63 -114
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +76 -121
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +11 -24
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +15 -24
- data/lib/simplecov/static_coverage_extractor/value_position.rb +25 -38
- data/lib/simplecov/static_coverage_extractor/visitor.rb +47 -64
- data/lib/simplecov/static_coverage_extractor.rb +46 -62
- data/lib/simplecov/test_tracker/accessors.rb +34 -0
- data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
- data/lib/simplecov/test_tracker/delta.rb +69 -0
- data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
- data/lib/simplecov/test_tracker.rb +162 -0
- data/lib/simplecov/unloaded_file_injector.rb +18 -31
- data/lib/simplecov/useless_results_remover.rb +7 -6
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
- data/lib/simplecov/view_coverage.rb +55 -0
- data/lib/simplecov-html.rb +1 -0
- data/lib/simplecov.rb +57 -113
- data/lib/simplecov_json_formatter.rb +1 -0
- data/man/simplecov.1 +349 -0
- data/sig/simplecov.rbs +523 -595
- metadata +72 -5
|
@@ -5,81 +5,50 @@ require_relative "parallel_adapters/parallel_tests"
|
|
|
5
5
|
require_relative "parallel_adapters/generic"
|
|
6
6
|
|
|
7
7
|
module SimpleCov
|
|
8
|
-
# Registry
|
|
9
|
-
# answers a small fixed set of questions on SimpleCov's behalf:
|
|
8
|
+
# Registry and selection for parallel-test-runner adapters. An adapter
|
|
9
|
+
# answers a small fixed set of questions on SimpleCov's behalf: `active?`,
|
|
10
|
+
# `first_worker?`, `wait_for_siblings`, and `expected_worker_count`.
|
|
10
11
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
12
|
+
# `Base` provides safe no-op defaults. `ParallelTestsAdapter` wraps the
|
|
13
|
+
# grosser/parallel_tests gem; `GenericAdapter` is env-var-only detection for
|
|
14
|
+
# runners that follow the `TEST_ENV_NUMBER` convention without shipping a
|
|
15
|
+
# Ruby API (#1065, #1156).
|
|
15
16
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
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, rspec-conductor 1.0.7+, custom CI
|
|
24
|
-
# sharding, knapsack-style splitters). See
|
|
25
|
-
# https://github.com/simplecov-ruby/simplecov/issues/1065 and
|
|
26
|
-
# https://github.com/simplecov-ruby/simplecov/issues/1156.
|
|
27
|
-
#
|
|
28
|
-
# Users can plug in additional adapters:
|
|
29
|
-
#
|
|
30
|
-
# SimpleCov::ParallelAdapters.register MyRunnerAdapter
|
|
31
|
-
#
|
|
32
|
-
# An adapter just needs to be a class responding to the four methods
|
|
33
|
-
# above. Subclass `SimpleCov::ParallelAdapters::Base` to inherit the
|
|
34
|
-
# no-op defaults and override only what you need (the contract methods
|
|
35
|
-
# are defined as class methods, so plain inheritance is what carries
|
|
36
|
-
# them through; `extend Base` won't pick them up).
|
|
17
|
+
# Users register their own with `ParallelAdapters.register MyAdapter`.
|
|
18
|
+
# Subclass `Base` to inherit the no-op defaults: the contract methods are
|
|
19
|
+
# class methods, so plain inheritance is what carries them through and
|
|
20
|
+
# `extend Base` won't pick them up.
|
|
37
21
|
module ParallelAdapters
|
|
38
|
-
|
|
22
|
+
extend self
|
|
39
23
|
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
# are prepended (#register puts new entries at the front) so
|
|
44
|
-
# downstream code can override the built-ins by registering a more
|
|
45
|
-
# specific match.
|
|
24
|
+
# ParallelTestsAdapter first (most specific: it uses the gem's own API when
|
|
25
|
+
# the gem is loaded), then GenericAdapter as the env-var fallback.
|
|
26
|
+
# User-registered adapters are prepended.
|
|
46
27
|
def adapters
|
|
47
28
|
@adapters ||= [ParallelTestsAdapter, GenericAdapter]
|
|
48
29
|
end
|
|
49
30
|
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
# and Generic adapters.
|
|
54
|
-
#
|
|
55
|
-
# class MyRunnerAdapter < SimpleCov::ParallelAdapters::Base
|
|
56
|
-
# def self.active? = ENV["MY_RUNNER_PID"]
|
|
57
|
-
# def self.first_worker? = ENV["MY_RUNNER_PID"].to_i == 1
|
|
58
|
-
# def self.expected_worker_count = ENV["MY_RUNNER_WORKERS"].to_i
|
|
59
|
-
# end
|
|
60
|
-
#
|
|
61
|
-
# SimpleCov::ParallelAdapters.register MyRunnerAdapter
|
|
31
|
+
# Newly registered adapters are inserted at the front of the selection
|
|
32
|
+
# list, so a custom adapter for a specific runner takes precedence over the
|
|
33
|
+
# built-ins.
|
|
62
34
|
def register(adapter)
|
|
63
35
|
reset_current!
|
|
64
36
|
adapters.unshift(adapter) unless adapters.include?(adapter)
|
|
65
37
|
adapter
|
|
66
38
|
end
|
|
67
39
|
|
|
68
|
-
# The
|
|
69
|
-
#
|
|
70
|
-
# no adapter is active (i.e., we're not running under any recognized
|
|
71
|
-
# parallel test runner), in which case the caller should treat the
|
|
72
|
-
# process as single-worker.
|
|
40
|
+
# The first registered adapter whose `active?` answers true, or nil when
|
|
41
|
+
# none is, in which case the caller treats the process as single-worker.
|
|
73
42
|
def current
|
|
74
|
-
return @current if
|
|
43
|
+
return @current if instance_variable_defined?(:@current)
|
|
75
44
|
|
|
76
45
|
@current = adapters.find(&:active?)
|
|
77
46
|
end
|
|
78
47
|
|
|
79
|
-
#
|
|
80
|
-
#
|
|
48
|
+
# Primarily for tests that mutate env vars between examples; production
|
|
49
|
+
# runs are single-shot.
|
|
81
50
|
def reset_current!
|
|
82
|
-
remove_instance_variable(:@current) if
|
|
51
|
+
remove_instance_variable(:@current) if instance_variable_defined?(:@current)
|
|
83
52
|
end
|
|
84
53
|
end
|
|
85
54
|
end
|
|
@@ -10,13 +10,12 @@ module SimpleCov
|
|
|
10
10
|
class << self
|
|
11
11
|
# @api private
|
|
12
12
|
def final_result_process?
|
|
13
|
-
adapter =
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# warnings. See issue #1171.
|
|
13
|
+
adapter = ParallelAdapters.current
|
|
14
|
+
# A subprocess forked while coverage was running is never the final
|
|
15
|
+
# reporter: the process that spawned it merges every slice and produces
|
|
16
|
+
# the report. Without this, fork-based runners that don't set
|
|
17
|
+
# TEST_ENV_NUMBER (Minitest's `parallelize`) have every worker produce
|
|
18
|
+
# the final report and its warnings (#1171).
|
|
20
19
|
return !forked_subprocess? unless adapter
|
|
21
20
|
|
|
22
21
|
adapter.first_worker?
|
|
@@ -24,53 +23,43 @@ module SimpleCov
|
|
|
24
23
|
|
|
25
24
|
# @api private
|
|
26
25
|
def wait_for_other_processes
|
|
27
|
-
adapter =
|
|
26
|
+
adapter = ParallelAdapters.current
|
|
28
27
|
return unless adapter && final_result_process?
|
|
29
28
|
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
# `wait_for_other_processes_to_finish` — implement this; adapters
|
|
33
|
-
# without a native API no-op and rely on the polling fallback below).
|
|
29
|
+
# Adapters that wrap a runner with a real "wait" primitive implement
|
|
30
|
+
# this; the rest no-op and rely on the polling fallback below.
|
|
34
31
|
adapter.wait_for_siblings
|
|
35
32
|
|
|
36
33
|
# The native wait can return before sibling at_exit handlers finish
|
|
37
|
-
# writing resultsets, and adapters without a native wait have
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
# threshold checks against a partial total.
|
|
34
|
+
# writing resultsets, and adapters without a native wait have nothing
|
|
35
|
+
# else. Either way, poll the resultset cache until all expected workers
|
|
36
|
+
# have reported or a timeout is reached. The outcome is captured so
|
|
37
|
+
# threshold checks can be suppressed against a partial total.
|
|
42
38
|
@parallel_results_complete =
|
|
43
39
|
wait_for_parallel_results(adapter.expected_worker_count, native_wait: adapter.native_wait?)
|
|
44
40
|
end
|
|
45
41
|
|
|
46
|
-
# @api private
|
|
47
|
-
#
|
|
48
|
-
# run (when `wait_for_other_processes` is a no-op).
|
|
42
|
+
# @api private -- true when every sibling reported its resultset before the
|
|
43
|
+
# wait deadline. Defaults to true outside a parallel run.
|
|
49
44
|
def parallel_results_complete?
|
|
50
|
-
|
|
45
|
+
instance_variable_defined?(:@parallel_results_complete) ? @parallel_results_complete : true
|
|
51
46
|
end
|
|
52
47
|
|
|
53
|
-
# @api private
|
|
48
|
+
# @api private -- seconds the resultset count must hold steady, after a
|
|
54
49
|
# native wait, before we accept fewer than `expected` workers as final.
|
|
55
50
|
PARALLEL_RESULTS_SETTLE = 0.5
|
|
56
51
|
private_constant :PARALLEL_RESULTS_SETTLE
|
|
57
52
|
|
|
58
|
-
# @api private
|
|
59
|
-
#
|
|
60
|
-
# (expected <= 1) short-circuit to true with no waiting.
|
|
53
|
+
# @api private -- true when the reporting worker has every resultset it's
|
|
54
|
+
# going to get, false on timeout. Single-process runs short-circuit.
|
|
61
55
|
#
|
|
62
|
-
# Normally we poll until `expected`
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
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.
|
|
56
|
+
# Normally we poll until `expected` workers have reported or
|
|
57
|
+
# `SimpleCov.parallel_wait_timeout` elapses. When a native wait already
|
|
58
|
+
# confirmed every sibling process exited, no further resultset will appear,
|
|
59
|
+
# so a count below `expected` just means some workers produced none, and
|
|
60
|
+
# once the count holds steady for `PARALLEL_RESULTS_SETTLE` we accept it as
|
|
61
|
+
# final. Without a native wait we can't tell an idle worker from a slow
|
|
62
|
+
# one, so we keep waiting the full timeout.
|
|
74
63
|
def wait_for_parallel_results(expected, native_wait: false)
|
|
75
64
|
return true unless expected > 1 # simplecov:disable branch — only false in real parallel runs
|
|
76
65
|
|
|
@@ -87,29 +76,30 @@ module SimpleCov
|
|
|
87
76
|
end
|
|
88
77
|
|
|
89
78
|
def current_parallel_worker_count
|
|
90
|
-
resultset =
|
|
91
|
-
|
|
79
|
+
resultset = ResultMerger.read_resultset
|
|
80
|
+
ResultMerger.worker_identities_for_run(resultset, run_id, process_start_time).size
|
|
92
81
|
end
|
|
93
82
|
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
83
|
+
# `tracker` carries the last count and the time it last changed across poll
|
|
84
|
+
# iterations. Answered through an if/else rather than an early return
|
|
85
|
+
# because falling through after a climb would read the clock a second time
|
|
86
|
+
# and answer the same `false` anyway.
|
|
97
87
|
def resultset_count_settled?(tracker, count)
|
|
98
|
-
if count > tracker
|
|
88
|
+
if count > tracker.fetch(:count)
|
|
99
89
|
tracker[:count] = count
|
|
100
90
|
tracker[:since] = monotonic_time
|
|
101
|
-
|
|
91
|
+
false
|
|
92
|
+
else
|
|
93
|
+
count.positive? && (monotonic_time - tracker.fetch(:since)) >= PARALLEL_RESULTS_SETTLE
|
|
102
94
|
end
|
|
103
|
-
|
|
104
|
-
count.positive? && (monotonic_time - tracker[:since]) >= PARALLEL_RESULTS_SETTLE
|
|
105
95
|
end
|
|
106
96
|
|
|
107
97
|
def monotonic_time
|
|
108
98
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
109
99
|
end
|
|
110
100
|
|
|
111
|
-
# @api private
|
|
112
|
-
#
|
|
101
|
+
# @api private -- warns on the first timeout so the user knows the merged
|
|
102
|
+
# total is partial.
|
|
113
103
|
def parallel_wait_timed_out?(deadline, expected, seen)
|
|
114
104
|
return false unless monotonic_time > deadline
|
|
115
105
|
|
|
@@ -121,7 +111,7 @@ module SimpleCov
|
|
|
121
111
|
def warn_about_incomplete_parallel_results(expected, seen)
|
|
122
112
|
return unless print_errors
|
|
123
113
|
|
|
124
|
-
warn
|
|
114
|
+
warn Color.colorize(
|
|
125
115
|
"Only #{seen} of #{expected} parallel-test workers reported within " \
|
|
126
116
|
"#{parallel_wait_timeout}s, so coverage totals are partial and minimum / " \
|
|
127
117
|
"maximum coverage checks are skipped for this run. Increase " \
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module ParallelResultMerger
|
|
5
|
+
# What a worker ships back over its pipe: the `[command_names, coverage]` pair
|
|
6
|
+
# its slice folded to, the tracked paths the slice carried, and the slice's
|
|
7
|
+
# context-map union. Building and folding live together so the pipe format
|
|
8
|
+
# has one owner. The parent needs the union across every worker of the
|
|
9
|
+
# tracked paths, to know what nothing loaded, and of the maps, to judge the
|
|
10
|
+
# all-or-nothing rule over the whole run rather than per slice.
|
|
11
|
+
module WorkerPayload
|
|
12
|
+
extend self
|
|
13
|
+
|
|
14
|
+
def build(chunk, ignore_timeout:)
|
|
15
|
+
tracked_files = Set.new
|
|
16
|
+
context_maps = ContextMap::Union.new
|
|
17
|
+
pair = ResultMerger.absorb_results(chunk, ignore_timeout: ignore_timeout,
|
|
18
|
+
&ResultMerger.entry_collector(tracked_files, context_maps))
|
|
19
|
+
[pair, tracked_files.to_a, context_maps]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def absorb(payload, tracked_files, context_maps)
|
|
23
|
+
_pair, tracked, maps = payload
|
|
24
|
+
tracked_files.merge(tracked)
|
|
25
|
+
context_maps.absorb_union(maps)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def pair(payload)
|
|
29
|
+
payload.first
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -1,35 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "parallel_result_merger/worker_payload"
|
|
4
|
+
|
|
3
5
|
module SimpleCov
|
|
4
6
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# `ResultMerger.absorb_results` is a fold over N independent
|
|
9
|
-
# read-parse-combine steps, so it splits cleanly: each worker runs that
|
|
10
|
-
# same fold over a contiguous slice of the file list and ships the pair
|
|
11
|
-
# back over a pipe, and the parent combines the handful of per-worker
|
|
12
|
-
# pairs it gets back. Reading and parsing the shards — where a collate
|
|
13
|
-
# over a few hundred CI jobs spends most of its time — is what actually
|
|
14
|
-
# parallelises.
|
|
7
|
+
# `ResultMerger.absorb_results` fanned out across forked workers, driving
|
|
8
|
+
# `SimpleCov.collate(..., processes: N)`. Each worker folds a contiguous
|
|
9
|
+
# slice of the file list and ships the pair back over a pipe.
|
|
15
10
|
#
|
|
16
|
-
# The slices are contiguous and merged back in order, so the fold visits
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# Every failure path returns nil rather than a partial merge, so the caller
|
|
21
|
-
# can redo the fold serially: reporting coverage for a subset of the
|
|
22
|
-
# resultsets would silently understate it.
|
|
11
|
+
# The slices are contiguous and merged back in order, so the fold visits the
|
|
12
|
+
# resultsets in the order the serial fold visits them and the merged result
|
|
13
|
+
# is identical to `SimpleCov.collate`'s, not merely equivalent.
|
|
23
14
|
#
|
|
24
15
|
module ParallelResultMerger
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
#
|
|
28
|
-
# `ResultMerger.merge_and_store` across `processes` forked workers. One
|
|
29
|
-
# worker hands straight back to `ResultMerger`, so the default `collate`
|
|
30
|
-
# takes exactly the path it always has and never reaches this module's
|
|
31
|
-
# machinery at all.
|
|
32
|
-
#
|
|
16
|
+
extend self
|
|
17
|
+
|
|
33
18
|
def merge_and_store(*file_paths, processes:, ignore_timeout: false)
|
|
34
19
|
return ResultMerger.merge_and_store(*file_paths, ignore_timeout: ignore_timeout) if processes < 2
|
|
35
20
|
|
|
@@ -38,176 +23,132 @@ module SimpleCov
|
|
|
38
23
|
result
|
|
39
24
|
end
|
|
40
25
|
|
|
41
|
-
#
|
|
42
|
-
# `ResultMerger.merge_results` across `processes` forked workers, merging
|
|
43
|
-
# in this process instead whenever the fan-out did not produce a complete
|
|
44
|
-
# merge — a runtime that cannot fork, nothing worth splitting, or a worker
|
|
45
|
-
# that died. The result is the same either way; only the time it took to
|
|
46
|
-
# get there differs.
|
|
47
|
-
#
|
|
48
26
|
def merge_results(*file_paths, processes:, ignore_timeout: false)
|
|
49
27
|
tracked_files = Set.new
|
|
28
|
+
context_maps = ContextMap::Union.new
|
|
50
29
|
pair = absorb_results(file_paths, processes: processes, ignore_timeout: ignore_timeout,
|
|
51
|
-
|
|
52
|
-
# A nil pair means nothing was fanned out at all, so the serial path
|
|
53
|
-
# is exactly `ResultMerger.merge_results` — delegate rather than
|
|
54
|
-
# re-implement its collector wiring.
|
|
30
|
+
tracked_files: tracked_files, context_maps: context_maps)
|
|
55
31
|
return ResultMerger.merge_results(*file_paths, ignore_timeout: ignore_timeout) unless pair
|
|
56
32
|
|
|
57
33
|
command_names, coverage = pair
|
|
58
|
-
ResultMerger.create_result(command_names, coverage, tracked_files: tracked_files)
|
|
34
|
+
ResultMerger.create_result(command_names, coverage, tracked_files: tracked_files, contexts: context_maps.map)
|
|
59
35
|
end
|
|
60
36
|
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
#
|
|
69
|
-
# @return [Array(Array<String>, Hash), nil] the pair
|
|
70
|
-
# `ResultMerger.create_result` consumes, or nil when the work could not
|
|
71
|
-
# be fanned out and the caller should merge in this process instead.
|
|
72
|
-
#
|
|
73
|
-
def absorb_results(file_paths, processes:, ignore_timeout: false, tracked_files: Set.new)
|
|
74
|
-
# One worker folds the whole list anyway, and one file is a fold of
|
|
75
|
-
# one — in both cases the fork and the round trip are pure overhead.
|
|
37
|
+
# Answers nil when the work could not be fanned out and the caller should
|
|
38
|
+
# merge in this process instead. The tracked paths and context-map union a
|
|
39
|
+
# worker's slice carried come back with its payload rather than through a
|
|
40
|
+
# collector block, since that block would be mutating state in the wrong
|
|
41
|
+
# process.
|
|
42
|
+
def absorb_results(file_paths, processes:, ignore_timeout: false, tracked_files: Set.new,
|
|
43
|
+
context_maps: ContextMap::Union.new)
|
|
76
44
|
return nil if processes < 2 || file_paths.size < 2
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
# detect that without rescuing an exception — TruffleRuby's compatibility
|
|
81
|
-
# guide names this as the correct check. They do still define
|
|
82
|
-
# `Kernel#fork` and raise `NotImplementedError` from it, so probing that
|
|
83
|
-
# instead would answer true and send them down the fan-out.
|
|
45
|
+
# JRuby cannot fork on the JVM and deliberately answers false here, but
|
|
46
|
+
# still defines `Kernel#fork` and raises `NotImplementedError` from it,
|
|
47
|
+
# so probing that instead would send it down the fan-out.
|
|
84
48
|
return nil unless Process.respond_to?(:fork)
|
|
85
49
|
|
|
86
|
-
fan_out(chunk(file_paths, processes), ignore_timeout: ignore_timeout, tracked_files: tracked_files
|
|
50
|
+
fan_out(chunk(file_paths, processes), ignore_timeout: ignore_timeout, tracked_files: tracked_files,
|
|
51
|
+
context_maps: context_maps)
|
|
87
52
|
end
|
|
88
53
|
|
|
89
|
-
# Contiguous slices whose sizes differ by at most one, so no worker is
|
|
90
|
-
# left folding twice its share while the others idle. There are never
|
|
91
|
-
# more slices than files: asking for more processes than there are
|
|
92
|
-
# resultsets just gives one resultset per process.
|
|
93
54
|
def chunk(file_paths, processes)
|
|
94
55
|
groups = [processes, file_paths.size].min
|
|
95
56
|
base, remainder = file_paths.size.divmod(groups)
|
|
96
57
|
remaining = file_paths.dup
|
|
97
58
|
|
|
98
|
-
Array.new(groups) { |index| remaining.shift(base + (index < remainder ? 1 : 0)) }
|
|
59
|
+
Array.new(groups) { |index| remaining.shift(base + ((index < remainder) ? 1 : 0)) }
|
|
99
60
|
end
|
|
100
61
|
|
|
101
|
-
# A `fork` that fails here raises, and is left to
|
|
102
|
-
# already excluded
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
|
|
106
|
-
def fan_out(chunks, ignore_timeout:, tracked_files: Set.new)
|
|
62
|
+
# A `fork` that fails here raises, and is left to: the runtimes that never
|
|
63
|
+
# fork are already excluded, so what remains is the OS refusing a process
|
|
64
|
+
# we expected to get, which says something is wrong with the machine rather
|
|
65
|
+
# than with the merge.
|
|
66
|
+
def fan_out(chunks, ignore_timeout:, tracked_files:, context_maps:)
|
|
107
67
|
workers = spawn_workers(chunks, ignore_timeout: ignore_timeout)
|
|
108
|
-
payloads =
|
|
68
|
+
payloads = collect_payloads(workers)
|
|
109
69
|
return nil unless payloads
|
|
110
70
|
|
|
111
|
-
payloads.each { |(
|
|
112
|
-
ResultMerger.merge_coverage(*payloads.map(
|
|
71
|
+
payloads.each { |payload| WorkerPayload.absorb(payload, tracked_files, context_maps) }
|
|
72
|
+
ResultMerger.merge_coverage(*payloads.map { |payload| WorkerPayload.pair(payload) })
|
|
113
73
|
end
|
|
114
74
|
|
|
115
75
|
def spawn_workers(chunks, ignore_timeout:)
|
|
116
76
|
workers = [] #: Array[Hash[Symbol, untyped]]
|
|
117
|
-
|
|
118
77
|
chunks.each do |chunk|
|
|
119
78
|
workers << spawn_worker(chunk, ignore_timeout: ignore_timeout)
|
|
120
|
-
rescue
|
|
79
|
+
rescue
|
|
121
80
|
abandon(workers)
|
|
122
81
|
raise
|
|
123
82
|
end
|
|
124
|
-
|
|
125
83
|
workers
|
|
126
84
|
end
|
|
127
85
|
|
|
128
86
|
def abandon(workers)
|
|
129
87
|
workers.each do |worker|
|
|
130
|
-
worker
|
|
131
|
-
succeeded?(worker
|
|
88
|
+
worker.fetch(:reader).close
|
|
89
|
+
succeeded?(worker.fetch(:pid))
|
|
132
90
|
end
|
|
133
91
|
end
|
|
134
92
|
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
#
|
|
138
|
-
# no child was spawned to feed it. Steep cannot type body locals
|
|
139
|
-
# inside an ensure, hence the ignore markers; the safe navigation
|
|
140
|
-
# keeps the cleanup well-defined when `IO.pipe` itself raised.
|
|
93
|
+
# The caller cleans up the workers it knows about, but this pipe is ours.
|
|
94
|
+
# The safe navigation keeps the cleanup well-defined when `IO.pipe` itself
|
|
95
|
+
# raised, and Steep cannot type body locals inside an ensure.
|
|
141
96
|
def spawn_worker(chunk, ignore_timeout:)
|
|
142
97
|
reader, writer = IO.pipe
|
|
143
98
|
pid = fork { run_in_child(reader, writer, chunk, ignore_timeout) }
|
|
144
99
|
{pid: pid, reader: reader}
|
|
145
100
|
ensure
|
|
146
|
-
#
|
|
101
|
+
# @type var writer: IO?
|
|
102
|
+
# @type var reader: IO?
|
|
103
|
+
# @type var pid: Integer?
|
|
147
104
|
writer&.close
|
|
148
105
|
reader&.close unless pid
|
|
149
|
-
# steep:ignore:end
|
|
150
106
|
end
|
|
151
107
|
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
#
|
|
108
|
+
# `exit!` rather than `exit` because the child must never fall through to
|
|
109
|
+
# the collating process's inherited `at_exit` handlers, SimpleCov's own
|
|
110
|
+
# report generation included.
|
|
155
111
|
def run_in_child(reader, writer, chunk, ignore_timeout)
|
|
156
112
|
reader.close
|
|
157
113
|
exit!(run_worker(chunk, writer, ignore_timeout: ignore_timeout))
|
|
158
114
|
end
|
|
159
115
|
|
|
160
|
-
#
|
|
161
|
-
# status the child should terminate with. Kept free of the exit itself so
|
|
162
|
-
# it can be exercised in-process.
|
|
163
|
-
#
|
|
164
|
-
# The slice's tracked paths travel with the pair because the parent needs
|
|
165
|
-
# the union across every worker to know what nothing loaded.
|
|
116
|
+
# Kept free of the exit itself so it can be exercised in-process.
|
|
166
117
|
def run_worker(chunk, writer, ignore_timeout:)
|
|
167
|
-
|
|
168
|
-
pair = ResultMerger.absorb_results(chunk, ignore_timeout: ignore_timeout,
|
|
169
|
-
&ResultMerger::UnloadedFiles.collector(tracked_files))
|
|
170
|
-
Marshal.dump([pair, tracked_files.to_a], writer)
|
|
118
|
+
Marshal.dump(WorkerPayload.build(chunk, ignore_timeout: ignore_timeout), writer)
|
|
171
119
|
writer.close
|
|
172
120
|
0
|
|
173
|
-
rescue
|
|
174
|
-
warn "[SimpleCov]: parallel merge worker failed: #{e.class}: #{e
|
|
121
|
+
rescue => e
|
|
122
|
+
warn "[SimpleCov]: parallel merge worker failed: #{e.class}: #{e}" if SimpleCov.print_errors
|
|
175
123
|
1
|
|
176
124
|
end
|
|
177
125
|
|
|
178
|
-
#
|
|
179
|
-
#
|
|
180
|
-
|
|
181
|
-
# reaping a worker that can never finish.
|
|
182
|
-
#
|
|
183
|
-
# Returns nil if any worker failed, so the caller can fall back to the
|
|
184
|
-
# serial fold rather than report a subset of the resultsets as the whole.
|
|
185
|
-
def collect(workers)
|
|
126
|
+
# Answers nil if any worker failed, so the caller redoes the fold serially
|
|
127
|
+
# rather than report a subset of the resultsets as the whole.
|
|
128
|
+
def collect_payloads(workers)
|
|
186
129
|
payloads = drain(workers)
|
|
187
|
-
failed = workers.count { |worker| !succeeded?(worker
|
|
188
|
-
return
|
|
130
|
+
failed = workers.count { |worker| !succeeded?(worker.fetch(:pid)) }
|
|
131
|
+
return warn_about_failed_workers(failed, workers.size) unless failed.zero? && payloads.all?
|
|
189
132
|
|
|
190
|
-
|
|
191
|
-
nil
|
|
133
|
+
payloads
|
|
192
134
|
ensure
|
|
193
|
-
workers.each { |worker| worker
|
|
135
|
+
workers.each { |worker| worker.fetch(:reader).close }
|
|
194
136
|
end
|
|
195
137
|
|
|
138
|
+
# A thread per worker, so every pipe is drained while the workers are still
|
|
139
|
+
# writing. A payload larger than the pipe buffer would otherwise block its
|
|
140
|
+
# worker mid-write, and the parent would block reaping a worker that can
|
|
141
|
+
# never finish.
|
|
196
142
|
def drain(workers)
|
|
197
|
-
workers.map { |worker| Thread.new { read_payload(worker
|
|
143
|
+
workers.map { |worker| Thread.new { read_payload(worker.fetch(:reader)) } }.map(&:value)
|
|
198
144
|
end
|
|
199
145
|
|
|
200
146
|
def read_payload(reader)
|
|
201
|
-
# The writer is a fork of this very process and the pipe never leaves
|
|
202
|
-
#
|
|
203
|
-
#
|
|
204
|
-
# truncated, which Marshal reports by raising rather than returning.
|
|
205
|
-
# RBS types `Marshal.load`'s source as `_Source`, which IO satisfies
|
|
206
|
-
# structurally but not nominally.
|
|
207
|
-
# steep:ignore:start
|
|
147
|
+
# The writer is a fork of this very process and the pipe never leaves it,
|
|
148
|
+
# so this is our own data coming back through our own kernel buffer, not
|
|
149
|
+
# input.
|
|
208
150
|
Marshal.load(reader) # rubocop:disable Security/MarshalLoad
|
|
209
|
-
|
|
210
|
-
rescue StandardError
|
|
151
|
+
rescue
|
|
211
152
|
nil
|
|
212
153
|
end
|
|
213
154
|
|
|
@@ -215,7 +156,7 @@ module SimpleCov
|
|
|
215
156
|
_pid, status = Process.wait2(pid)
|
|
216
157
|
status.success?
|
|
217
158
|
rescue SystemCallError
|
|
218
|
-
# Errno::ECHILD
|
|
159
|
+
# Errno::ECHILD: nothing left to reap, so there is no status to judge
|
|
219
160
|
# this worker's slice by and we have to assume it did not finish.
|
|
220
161
|
false
|
|
221
162
|
end
|
data/lib/simplecov/process.rb
CHANGED
|
@@ -1,53 +1,35 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
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
3
|
module SimpleCov
|
|
15
4
|
class << self
|
|
16
5
|
# Forked children inherit at_exit state that is wrong for them:
|
|
17
|
-
# @at_exit_hook_installed may describe a hook the parent already
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
# SimpleCov.start installs a fresh hook that actually fires at child
|
|
23
|
-
# exit. See issue #1227.
|
|
6
|
+
# @at_exit_hook_installed may describe a hook the parent already consumed
|
|
7
|
+
# before forking (Minitest autorun runs the suite inside the parent's
|
|
8
|
+
# at_exit), and external_at_exit may point at a Minitest.after_run deferral
|
|
9
|
+
# that is pid-pinned to the parent. Resetting both is what lets the at_fork
|
|
10
|
+
# proc's `SimpleCov.start` install a hook that actually fires (#1227).
|
|
24
11
|
def reset_inherited_at_exit_state!
|
|
25
12
|
@at_exit_hook_installed = false
|
|
26
13
|
self.external_at_exit = false
|
|
27
14
|
end
|
|
28
15
|
end
|
|
29
16
|
|
|
30
|
-
# Prepended onto Process's singleton class so every fork
|
|
31
|
-
#
|
|
32
|
-
#
|
|
17
|
+
# Prepended onto Process's singleton class so every fork re-runs SimpleCov's
|
|
18
|
+
# at_fork callback in the child. `Process._fork` is the official extension
|
|
19
|
+
# point: `Kernel#fork`, `Process.fork`, and `IO.popen("-")` all funnel
|
|
20
|
+
# through it, and prepending composes with other libraries doing the same.
|
|
33
21
|
module ProcessForkHook
|
|
22
|
+
# The next serial is assigned in the parent, before the fork, so the child
|
|
23
|
+
# inherits its own stable ordinal via copy-on-write, and the child is marked
|
|
24
|
+
# here independent of whatever custom at_fork block the user installed so
|
|
25
|
+
# `final_result_process?` can keep forked workers from each producing the
|
|
26
|
+
# final report (#1171, #1227).
|
|
34
27
|
def _fork
|
|
35
28
|
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
29
|
SimpleCov.next_subprocess_serial! if active
|
|
41
30
|
pid = super
|
|
42
31
|
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
32
|
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
33
|
SimpleCov.reset_inherited_at_exit_state!
|
|
52
34
|
SimpleCov.at_fork.call(::Process.pid)
|
|
53
35
|
end
|