rspec-core-parallel 0.1.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.
@@ -0,0 +1,253 @@
1
+ require 'rspec/core/bisect/shell_command'
2
+ require 'rspec/core/bisect/fork_runner'
3
+
4
+ module RSpec
5
+ module Core
6
+ module Parallel
7
+ # The changes rspec-core needs to run a suite in parallel, applied to
8
+ # rspec-core 3.13 from outside.
9
+ #
10
+ # @private
11
+ module CoreExt
12
+ # @private
13
+ module Configuration
14
+ # @private
15
+ DEFAULT_PARALLEL_RUN_TIMES_FILE_PATH = 'tmp/rspec_parallel_run_times.txt'
16
+
17
+ def self.included(configuration)
18
+ # @macro add_setting
19
+ # Number of worker processes used to run the suite (default: 1,
20
+ # which runs the suite sequentially in the current process). The
21
+ # suite is loaded once and then forked, so every worker is a
22
+ # separate process: examples running concurrently never share
23
+ # global state. Each worker runs `before(:suite)` and
24
+ # `after(:suite)` hooks, and gets a `TEST_ENV_NUMBER` environment
25
+ # variable (`""`, `"2"`, `"3"`...) which can be used to give every
26
+ # worker its own database.
27
+ # @return [Integer]
28
+ configuration.add_setting :parallel_workers
29
+
30
+ # @macro add_setting
31
+ # Allows parallel runs to split groups having `before(:context)` or
32
+ # `after(:context)` hooks across workers (default: `false`).
33
+ #
34
+ # A top-level group expected to take longer than a worker's share
35
+ # of the suite is split into its child groups and parts of its
36
+ # examples, which run in different workers. By default only groups
37
+ # without context hooks of their own (global ones are fine) are
38
+ # split, since the context hooks of a split group run once for each
39
+ # part of it, rather than once. This is how they run when running
40
+ # only part of a group with `rspec path/to/spec.rb:42`, so it is
41
+ # safe for most suites and lets long groups be spread over workers.
42
+ #
43
+ # Tag a group with `:parallel_split => false` to always run it in a
44
+ # single worker.
45
+ # @return [Boolean]
46
+ configuration.add_setting :parallel_split_groups_with_context_hooks
47
+
48
+ configuration.__send__(:attr_writer, :reporter, :parallel_run_times_file_path)
49
+
50
+ # @return [Integer, nil] the number of the current parallel worker
51
+ # (starting at 1), or nil when not running in a parallel worker.
52
+ configuration.__send__(:attr_accessor, :parallel_worker_number)
53
+ end
54
+
55
+ # The file in which parallel runs persist the run time of every
56
+ # example, so that the next parallel runs can hand out the longest
57
+ # groups first and spread them over workers. Defaults to
58
+ # `tmp/rspec_parallel_run_times.txt`, or to
59
+ # `example_status_persistence_file_path` when it is set (the run
60
+ # times persisted there are used). Set it to `nil` to not persist
61
+ # run times.
62
+ #
63
+ # @return [String, nil] the file path
64
+ def parallel_run_times_file_path
65
+ return @parallel_run_times_file_path if defined?(@parallel_run_times_file_path)
66
+
67
+ DEFAULT_PARALLEL_RUN_TIMES_FILE_PATH unless example_status_persistence_file_path
68
+ end
69
+
70
+ # Registers a block to run in each parallel worker process after it
71
+ # is forked, before its `before(:suite)` hooks. The block receives
72
+ # the worker number. Use it to set up per-worker resources, such as
73
+ # connecting to a per-worker database.
74
+ #
75
+ # @example
76
+ # RSpec.configure do |config|
77
+ # config.parallel_worker_setup do |worker_number|
78
+ # SimpleCov.command_name "rspec-#{worker_number}"
79
+ # end
80
+ # end
81
+ def parallel_worker_setup(&block)
82
+ parallel_worker_hooks(:setup) << block
83
+ end
84
+
85
+ # Registers a block to run in each parallel worker process after its
86
+ # `after(:suite)` hooks, right before the worker exits. The block
87
+ # receives the worker number.
88
+ def parallel_worker_teardown(&block)
89
+ parallel_worker_hooks(:teardown) << block
90
+ end
91
+
92
+ # @private
93
+ def parallel_worker_hooks(type)
94
+ (@parallel_worker_hooks ||= { :setup => [], :teardown => [] })[type]
95
+ end
96
+ end
97
+
98
+ # @private
99
+ module ConfigurationOverrides
100
+ # Loading this gem means running in parallel: one worker per physical
101
+ # core unless set (`1` runs the suite sequentially). Where processes
102
+ # can't be forked, the suite runs sequentially without a warning.
103
+ def parallel_workers
104
+ value = super
105
+ return value unless value.nil?
106
+
107
+ Parallel.supported? ? Parallel.default_worker_count : 1
108
+ end
109
+
110
+ private
111
+
112
+ # rspec-core runs the default path (`spec`) when no files are given
113
+ # only when its command is `rspec`.
114
+ def command
115
+ super == 'rspec-parallel' ? 'rspec' : super
116
+ end
117
+ end
118
+
119
+ # @private
120
+ module Parser
121
+ def parse(source=nil)
122
+ # The long form of an option with an optional argument consumes the
123
+ # next argument even when it is a file path, the short form does not.
124
+ @original_args = original_args.map { |arg| arg == '--parallel' ? '-j' : arg }
125
+ super
126
+ end
127
+
128
+ private
129
+
130
+ def parser(options)
131
+ super.tap do |parser|
132
+ parser.separator ''
133
+ parser.separator ' **** Parallel ****'
134
+ parser.separator ''
135
+ parser.on('-j', '--[no-]parallel [COUNT]', Integer,
136
+ 'Run the suite in COUNT forked worker processes (default: number of physical CPU cores).') do |count|
137
+ options[:parallel_workers] = count.nil? ? Parallel.default_worker_count : (count || 1)
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ # @private
144
+ module Runner
145
+ def run_specs(example_groups)
146
+ parallel_units = Parallel.units_for(@configuration, @world, example_groups)
147
+ return super unless parallel_units
148
+
149
+ examples_count = @world.example_count(example_groups)
150
+ examples_passed = @configuration.reporter.report(examples_count) do |reporter|
151
+ Parallel.run(@configuration, @world, parallel_units, reporter)
152
+ end
153
+
154
+ exit_code(examples_passed)
155
+ end
156
+ end
157
+
158
+ # @private
159
+ module HookCollections
160
+ # Whether any `before(:context)` or `after(:context)` hook, other than
161
+ # those registered from `globals`, is registered directly on the
162
+ # owner of these hooks.
163
+ def context_hooks?(globals)
164
+ [:before, :after].any? do |position|
165
+ (all_hooks_for(position, :context) - globals.all_hooks_for(position, :context)).any?
166
+ end
167
+ end
168
+ end
169
+
170
+ # @private
171
+ module Example
172
+ # Sets the outcome of running this example in another process.
173
+ def restore_exception(exception)
174
+ @exception = exception
175
+ end
176
+ end
177
+
178
+ # @private
179
+ module ExampleGroupClassMethods
180
+ # The memoized `let` values of the instance running a split group's
181
+ # `before(:context)` hooks must not be shared with its examples.
182
+ def each_instance_variable_for_example(group)
183
+ super { |ivar| yield ivar unless ivar.to_sym == :@__memoized }
184
+ end
185
+ end
186
+
187
+ # @private
188
+ module Reporter
189
+ # Overrides the time profiled for a top-level group, for groups run
190
+ # in another process.
191
+ def record_group_run_time(group, run_time)
192
+ return unless @profiler && @profiler.example_groups.key?(group)
193
+
194
+ @profiler.example_groups[group][:total_time] = run_time
195
+ end
196
+ end
197
+
198
+ # Order dependencies can only be reproduced in a single process.
199
+ #
200
+ # @private
201
+ module BisectShellCommand
202
+ PARALLEL_OPTION = /\A(-j\d*|--(no-)?parallel(=\d+)?)\z/
203
+
204
+ def initialize(original_cli_args)
205
+ super(without_parallel_options(original_cli_args))
206
+ end
207
+
208
+ # The bisect runs load this gem, so that a `--parallel` in `.rspec`
209
+ # is a known option there too.
210
+ def command_for(locations, server)
211
+ executable = open3_safe_escape(RSpec::Core.path_to_executable)
212
+ "#{super.sub(executable) { "-rrspec/core/parallel #{executable}" }} --no-parallel"
213
+ end
214
+
215
+ private
216
+
217
+ def without_parallel_options(args)
218
+ kept = []
219
+ count_may_follow = false
220
+ args.each do |arg|
221
+ next count_may_follow = false if count_may_follow && arg =~ /\A\d+\z/
222
+
223
+ count_may_follow = %w[-j --parallel].include?(arg)
224
+ kept << arg unless arg =~ PARALLEL_OPTION
225
+ end
226
+ kept
227
+ end
228
+ end
229
+
230
+ # Order dependencies can only be reproduced in a single process.
231
+ #
232
+ # @private
233
+ module BisectRunDispatcher
234
+ def initialize(runner, channel)
235
+ super
236
+ runner.configuration.force(:parallel_workers => 1)
237
+ end
238
+ end
239
+ end
240
+ end
241
+
242
+ Configuration.__send__(:include, Parallel::CoreExt::Configuration)
243
+ Configuration.__send__(:prepend, Parallel::CoreExt::ConfigurationOverrides)
244
+ Parser.__send__(:prepend, Parallel::CoreExt::Parser)
245
+ Runner.__send__(:prepend, Parallel::CoreExt::Runner)
246
+ Hooks::HookCollections.__send__(:include, Parallel::CoreExt::HookCollections)
247
+ Example.__send__(:include, Parallel::CoreExt::Example)
248
+ ExampleGroup.singleton_class.__send__(:prepend, Parallel::CoreExt::ExampleGroupClassMethods)
249
+ Reporter.__send__(:include, Parallel::CoreExt::Reporter)
250
+ Bisect::ShellCommand.__send__(:prepend, Parallel::CoreExt::BisectShellCommand)
251
+ Bisect::ForkRunner::RunDispatcher.__send__(:prepend, Parallel::CoreExt::BisectRunDispatcher)
252
+ end
253
+ end
@@ -0,0 +1,122 @@
1
+ module RSpec
2
+ module Core
3
+ module Parallel
4
+ # Carries exceptions raised in a worker over to the main process.
5
+ #
6
+ # Exceptions are Marshal-ed whenever possible so that the main process
7
+ # sees the very same object (class, ivars, cause...). Exceptions holding
8
+ # unmarshalable state (procs, IOs, singleton methods...) or whose class
9
+ # does not exist in the main process are rebuilt from their class name,
10
+ # message, backtrace and cause.
11
+ #
12
+ # @private
13
+ module ExceptionSerializer
14
+ MAX_CAUSE_DEPTH = 10
15
+
16
+ # Stands in for exceptions that cannot be reconstructed as instances
17
+ # of their original class.
18
+ class RemoteException < StandardError
19
+ class << self
20
+ attr_accessor :remote_class_name
21
+
22
+ def name
23
+ remote_class_name || super
24
+ end
25
+ alias_method :to_s, :name
26
+ alias_method :inspect, :name
27
+ end
28
+ end
29
+
30
+ def self.dump(exception, depth=0)
31
+ return nil unless exception
32
+
33
+ {
34
+ :marshaled => marshal(exception),
35
+ :class_name => exception.class.name,
36
+ :message => safe_message(exception),
37
+ :backtrace => exception.backtrace,
38
+ :cause => depth < MAX_CAUSE_DEPTH ? dump(exception.cause, depth + 1) : nil
39
+ }
40
+ end
41
+
42
+ def self.load(payload)
43
+ return nil unless payload
44
+
45
+ unmarshal(payload[:marshaled]) || rebuild(payload)
46
+ end
47
+
48
+ def self.marshal(exception)
49
+ Marshal.dump(exception)
50
+ rescue Exception # rubocop:disable Lint/RescueException
51
+ nil
52
+ end
53
+
54
+ def self.unmarshal(data)
55
+ return nil unless data
56
+
57
+ exception = Marshal.load(data) # rubocop:disable Security/MarshalLoad
58
+ exception if Exception === exception && usable?(exception)
59
+ rescue Exception # rubocop:disable Lint/RescueException
60
+ nil
61
+ end
62
+
63
+ def self.safe_message(exception)
64
+ exception.message.to_s
65
+ rescue Exception => e # rubocop:disable Lint/RescueException
66
+ "<#{exception.class.name}#message raised #{e.class}>"
67
+ end
68
+
69
+ # Rebuilds the exception as an instance of its class when possible.
70
+ # Classes whose methods rely on state set by their constructor (such
71
+ # as a wrapped exception) get a stand-in reporting their name instead.
72
+ def self.rebuild(payload)
73
+ cause = load(payload[:cause])
74
+ klass = exception_class(payload[:class_name])
75
+
76
+ if klass && !klass.method_defined?(:all_exceptions)
77
+ exception = build(klass.allocate, payload, cause)
78
+ return exception if usable?(exception)
79
+ end
80
+
81
+ proxy = Class.new(RemoteException)
82
+ proxy.remote_class_name = payload[:class_name]
83
+ build(proxy.allocate, payload, cause)
84
+ end
85
+
86
+ def self.build(exception, payload, cause)
87
+ message = payload[:message]
88
+ Exception.instance_method(:initialize).bind_call(exception, message)
89
+ exception.define_singleton_method(:message) { message }
90
+ exception.define_singleton_method(:to_s) { message }
91
+ exception = attach_cause(exception, cause) if cause
92
+ exception.set_backtrace(payload[:backtrace]) if payload[:backtrace]
93
+ exception
94
+ end
95
+
96
+ def self.usable?(exception)
97
+ exception.message
98
+ exception.backtrace
99
+ exception.inspect
100
+ true
101
+ rescue Exception # rubocop:disable Lint/RescueException
102
+ false
103
+ end
104
+
105
+ def self.exception_class(class_name)
106
+ return nil unless class_name
107
+
108
+ klass = Object.const_get(class_name)
109
+ klass if Class === klass && klass <= Exception
110
+ rescue NameError, ArgumentError
111
+ nil
112
+ end
113
+
114
+ def self.attach_cause(exception, cause)
115
+ raise exception, :cause => cause
116
+ rescue Exception => e # rubocop:disable Lint/RescueException
117
+ e
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,49 @@
1
+ module RSpec
2
+ module Core
3
+ module Parallel
4
+ # Prepares Rails apps for running in parallel workers, as `rails test`
5
+ # does:
6
+ #
7
+ # * runs the hooks Rails provides to parallel test runners, so that
8
+ # every worker gets its own test database (named after the
9
+ # configured one: `app_test_0`, `app_test_1`..., created and loaded
10
+ # from the schema when needed). Set
11
+ # `ActiveSupport.parallelize_test_databases` to `false` to opt out.
12
+ # Workers are numbered from 0 for these hooks, as in Rails. Rails
13
+ # versions before 8.1 have no hooks to run before forking.
14
+ # * loads the translations before forking, as Rails does when eager
15
+ # loading, rather than in every worker.
16
+ #
17
+ # @private
18
+ module RailsIntegration
19
+ def self.before_fork
20
+ ::I18n.eager_load! if defined?(::Rails.application) && ::Rails.application && defined?(::I18n.eager_load!)
21
+ hooks(:before_fork_hooks).each(&:call)
22
+ end
23
+
24
+ def self.after_fork(worker_number)
25
+ hooks(:after_fork_hooks).each { |hook| hook.call(worker_number - 1) }
26
+ end
27
+
28
+ def self.run_cleanup(worker_number)
29
+ hooks(:run_cleanup_hooks).each { |hook| hook.call(worker_number - 1) }
30
+ end
31
+
32
+ def self.hooks(type)
33
+ # Loading it registers the hooks creating a database per worker, as
34
+ # `rails/test_help` does.
35
+ require 'active_record/test_databases' if defined?(::ActiveRecord::TestDatabases)
36
+ return [] unless defined?(::ActiveSupport::Testing::Parallelization)
37
+ return [] unless ::ActiveSupport::Testing::Parallelization.respond_to?(type)
38
+ # Rails checks this setting in its hooks only from version 8.1 on.
39
+ return [] if ::ActiveSupport.respond_to?(:parallelize_test_databases) &&
40
+ ::ActiveSupport.parallelize_test_databases == false
41
+
42
+ ::ActiveSupport::Testing::Parallelization.public_send(type)
43
+ rescue LoadError
44
+ []
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,45 @@
1
+ module RSpec
2
+ module Core
3
+ module Parallel
4
+ # Assigns every example group and example a stable index. Workers are
5
+ # forked after the suite is loaded, so walking the same tree yields the
6
+ # same indices in every process, letting processes refer to groups and
7
+ # examples without relying on their ids being unique.
8
+ #
9
+ # @private
10
+ class Registry
11
+ attr_reader :groups, :examples
12
+
13
+ def initialize(top_level_groups)
14
+ @groups = []
15
+ @examples = []
16
+ @group_indices = {}.compare_by_identity
17
+ @example_indices = {}.compare_by_identity
18
+ top_level_groups.each { |group| register(group) }
19
+ end
20
+
21
+ def group_index(group)
22
+ @group_indices[group]
23
+ end
24
+
25
+ def example_index(example)
26
+ @example_indices[example]
27
+ end
28
+
29
+ private
30
+
31
+ def register(group)
32
+ @group_indices[group] = @groups.size
33
+ @groups << group
34
+
35
+ group.examples.each do |example|
36
+ @example_indices[example] = @examples.size
37
+ @examples << example
38
+ end
39
+
40
+ group.children.each { |child| register(child) }
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,162 @@
1
+ module RSpec
2
+ module Core
3
+ module Parallel
4
+ # Splits the suite into units of work and orders them longest first, so
5
+ # that the longest units do not end up running alone at the end of the
6
+ # run. Durations come from the run times stored in
7
+ # `example_status_persistence_file_path` and
8
+ # `parallel_run_times_file_path`; examples without a stored run time are
9
+ # assumed to take the average time of those with one.
10
+ #
11
+ # A unit is usually a whole top-level group. A group expected to take
12
+ # longer than a worker's share of the suite is split into its child
13
+ # groups and parts of its own examples, unless it is tagged with
14
+ # `:parallel_split => false` or has `before(:context)` or
15
+ # `after(:context)` hooks other than global ones (see
16
+ # `Configuration#parallel_split_groups_with_context_hooks`). The units
17
+ # of a split group are kept adjacent so that its output stays together.
18
+ #
19
+ # @private
20
+ class Scheduler
21
+ # @!attribute group
22
+ # @return [Class] the group to run
23
+ # @!attribute examples_only
24
+ # @return [Boolean] whether to run only the group's own examples,
25
+ # and not its child groups
26
+ # @!attribute split_ancestors
27
+ # @return [Array<Class>] the split groups containing `group`,
28
+ # outermost first
29
+ # @!attribute examples
30
+ # @return [Array<Example>, nil] the examples to run when running
31
+ # only some of the group's own examples
32
+ # @!attribute sequence
33
+ # @return [Integer] the position of the unit among the parts of its
34
+ # top-level group when running them sequentially
35
+ Unit = Struct.new(:group, :examples_only, :split_ancestors, :estimate, :examples, :sequence)
36
+
37
+ def initialize(configuration, world)
38
+ @configuration = configuration
39
+ @world = world
40
+ end
41
+
42
+ # Without run times from a previous run, the duration of groups is
43
+ # unknown: they are split into smaller parts, so that a long one does
44
+ # not end up running alone at the end of the run.
45
+ UNKNOWN_DURATIONS_SPLIT_FACTOR = 16
46
+
47
+ # @return [Array<Unit>] the units to run, longest first
48
+ def units(example_groups, worker_count)
49
+ top_level_units = example_groups.filter_map do |group|
50
+ examples = examples_of(group)
51
+ Unit.new(group, false, [], estimated_duration(examples)) unless examples.empty?
52
+ end
53
+ target = top_level_units.sum(&:estimate) / worker_count
54
+ target /= UNKNOWN_DURATIONS_SPLIT_FACTOR if run_times.empty?
55
+
56
+ families = top_level_units.map do |unit|
57
+ family = split(unit, target)
58
+ family.each_with_index { |part, sequence| part.sequence = sequence }
59
+ family.each_with_index.sort_by { |part, sequence| [-part.estimate, sequence] }.map(&:first)
60
+ end
61
+ sort_longest_first(families).flatten
62
+ end
63
+
64
+ DURATION_UNITS = { 'second' => 1, 'minute' => 60 }.freeze
65
+
66
+ # Parses durations formatted by `Formatters::Helpers.format_duration`.
67
+ def self.parse_duration(text)
68
+ parts = text.to_s.scan(/([\d.]+) (second|minute)s?/)
69
+ return nil if parts.empty?
70
+
71
+ parts.inject(0.0) { |total, (amount, unit)| total + (amount.to_f * DURATION_UNITS.fetch(unit)) }
72
+ end
73
+
74
+ private
75
+
76
+ # @return [Array<Unit>] the parts of the unit, in the order in which
77
+ # they would run sequentially (with the default ordering)
78
+ def split(unit, target)
79
+ return [unit] unless unit.estimate > target && splittable?(unit.group)
80
+
81
+ units = parts_of(unit, target).flat_map { |part| split(part, target) }
82
+ units.size > 1 ? units : [unit]
83
+ end
84
+
85
+ def splittable?(group)
86
+ return false if group.metadata[:parallel_split] == false
87
+
88
+ @configuration.parallel_split_groups_with_context_hooks? || !group.hooks.context_hooks?(@configuration.hooks)
89
+ end
90
+
91
+ def parts_of(unit, target)
92
+ return example_chunks(unit, target) if unit.examples_only
93
+
94
+ ancestors = unit.split_ancestors + [unit.group]
95
+ own_examples = @world.filtered_examples[unit.group]
96
+ parts = own_examples.empty? ? [] : [Unit.new(unit.group, true, ancestors, estimated_duration(own_examples))]
97
+
98
+ unit.group.children.each do |child|
99
+ examples = examples_of(child)
100
+ parts << Unit.new(child, false, ancestors, estimated_duration(examples)) unless examples.empty?
101
+ end
102
+ parts
103
+ end
104
+
105
+ # Cuts the examples of a unit into parts expected to take at most
106
+ # `target` each (unless a single example takes longer).
107
+ def example_chunks(unit, target)
108
+ chunks = []
109
+ (unit.examples || @world.filtered_examples[unit.group]).each do |example|
110
+ run_time = run_time_of(example)
111
+ if chunks.empty? || chunks.last.estimate + run_time > target
112
+ chunks << Unit.new(unit.group, true, unit.split_ancestors, 0.0, [])
113
+ end
114
+ chunks.last.examples << example
115
+ chunks.last.estimate += run_time
116
+ end
117
+ chunks.size > 1 ? chunks : []
118
+ end
119
+
120
+ # Families of units run longest first: those with the longest unit,
121
+ # then those taking the longest overall.
122
+ def sort_longest_first(families)
123
+ families.each_with_index.sort_by do |family, position|
124
+ [-family.first.estimate, -family.sum(&:estimate), position]
125
+ end.map(&:first)
126
+ end
127
+
128
+ def examples_of(group)
129
+ group.descendants.flat_map { |descendant| @world.filtered_examples[descendant] }
130
+ end
131
+
132
+ def estimated_duration(examples)
133
+ examples.inject(0.0) { |total, example| total + run_time_of(example) }
134
+ end
135
+
136
+ def run_time_of(example)
137
+ run_times.fetch(example.id, default_run_time)
138
+ end
139
+
140
+ def default_run_time
141
+ @default_run_time ||= run_times.empty? ? 1.0 : run_times.values.sum / run_times.size
142
+ end
143
+
144
+ def run_times
145
+ @run_times ||= load_run_times
146
+ end
147
+
148
+ def load_run_times
149
+ paths = [@configuration.example_status_persistence_file_path, @configuration.parallel_run_times_file_path]
150
+ paths.compact.uniq.each_with_object({}) do |path, times|
151
+ ExampleStatusPersister.load_from(path).each do |example|
152
+ run_time = self.class.parse_duration(example[:run_time])
153
+ times[example[:example_id]] = run_time if run_time
154
+ end
155
+ rescue SystemCallError
156
+ next
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,30 @@
1
+ module RSpec
2
+ module Core
3
+ module Parallel
4
+ # Leaves SimpleCov's report to the main process. SimpleCov saves the
5
+ # coverage of every process at exit, and the process it considers the
6
+ # final one merges them, writes the report and checks the minimum
7
+ # coverage. It can't tell the workers from the main process (SimpleCov
8
+ # 1.x even takes the first worker for the final process, because of
9
+ # its `TEST_ENV_NUMBER`), so workers finishing early checked the
10
+ # coverage collected so far and printed false "below minimum coverage"
11
+ # warnings. The main process exits after all the workers.
12
+ #
13
+ # @private
14
+ module SimpleCovIntegration
15
+ # @private
16
+ module NotFinalResultProcess
17
+ def final_result_process?
18
+ false
19
+ end
20
+ end
21
+
22
+ def self.after_fork
23
+ return unless defined?(::SimpleCov) && ::SimpleCov.respond_to?(:final_result_process?)
24
+
25
+ ::SimpleCov.singleton_class.__send__(:prepend, NotFinalResultProcess)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,7 @@
1
+ module RSpec
2
+ module Core
3
+ module Parallel
4
+ VERSION = '0.1.0'
5
+ end
6
+ end
7
+ end