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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +11 -0
- data/LICENSE.md +27 -0
- data/README.md +148 -0
- data/exe/rspec-parallel +8 -0
- data/lib/rspec/core/parallel/channel.rb +58 -0
- data/lib/rspec/core/parallel/coordinator.rb +428 -0
- data/lib/rspec/core/parallel/core_ext.rb +253 -0
- data/lib/rspec/core/parallel/exception_serializer.rb +122 -0
- data/lib/rspec/core/parallel/rails_integration.rb +49 -0
- data/lib/rspec/core/parallel/registry.rb +45 -0
- data/lib/rspec/core/parallel/scheduler.rb +162 -0
- data/lib/rspec/core/parallel/simplecov_integration.rb +30 -0
- data/lib/rspec/core/parallel/version.rb +7 -0
- data/lib/rspec/core/parallel/worker.rb +187 -0
- data/lib/rspec/core/parallel/worker_reporter.rb +212 -0
- data/lib/rspec/core/parallel.rb +95 -0
- data/lib/rspec-core-parallel.rb +1 -0
- metadata +85 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
module RSpec
|
|
2
|
+
module Core
|
|
3
|
+
module Parallel
|
|
4
|
+
# A forked process running the units of work it is sent.
|
|
5
|
+
#
|
|
6
|
+
# @private
|
|
7
|
+
class Worker
|
|
8
|
+
attr_reader :number, :pid, :events
|
|
9
|
+
attr_accessor :current_unit, :quit_sent
|
|
10
|
+
|
|
11
|
+
def initialize(number)
|
|
12
|
+
@number = number
|
|
13
|
+
@current_unit = nil
|
|
14
|
+
@quit_sent = false
|
|
15
|
+
@alive = true
|
|
16
|
+
@reaped = false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def alive?
|
|
20
|
+
@alive
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def start(configuration, registry, other_workers)
|
|
24
|
+
@events, events_writer = Channel.pipe
|
|
25
|
+
commands_reader, @commands = Channel.pipe
|
|
26
|
+
|
|
27
|
+
# The process exits when the block returns, running `at_exit` hooks
|
|
28
|
+
# (so that tools such as SimpleCov can save their results) but not
|
|
29
|
+
# the `ensure` clauses of the main process's call stack.
|
|
30
|
+
@pid = Process.fork do
|
|
31
|
+
@events.close
|
|
32
|
+
@commands.close
|
|
33
|
+
other_workers.each(&:close)
|
|
34
|
+
run(configuration, registry, commands_reader, events_writer)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
events_writer.close
|
|
38
|
+
commands_reader.close
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def send_command(*command)
|
|
43
|
+
@commands.write(command)
|
|
44
|
+
true
|
|
45
|
+
rescue IOError, SystemCallError
|
|
46
|
+
false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def close
|
|
50
|
+
@alive = false
|
|
51
|
+
@events.close
|
|
52
|
+
@commands.close
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def exited?
|
|
56
|
+
@reaped ||= !Process.waitpid(@pid, Process::WNOHANG).nil?
|
|
57
|
+
rescue SystemCallError
|
|
58
|
+
@reaped = true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def wait
|
|
62
|
+
return if @reaped
|
|
63
|
+
|
|
64
|
+
Process.waitpid(@pid)
|
|
65
|
+
@reaped = true
|
|
66
|
+
rescue SystemCallError
|
|
67
|
+
@reaped = true
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def run(configuration, registry, commands, events)
|
|
73
|
+
# :nocov: - executed in forked processes, covered by integration specs
|
|
74
|
+
prepare_environment(configuration)
|
|
75
|
+
reporter = WorkerReporter.new(configuration, registry, events, commands)
|
|
76
|
+
configuration.reporter = reporter
|
|
77
|
+
|
|
78
|
+
begin
|
|
79
|
+
RailsIntegration.after_fork(@number)
|
|
80
|
+
SimpleCovIntegration.after_fork
|
|
81
|
+
configuration.parallel_worker_hooks(:setup).each { |hook| hook.call(@number) }
|
|
82
|
+
configuration.with_suite_hooks { process_commands(registry, reporter, commands) }
|
|
83
|
+
configuration.parallel_worker_hooks(:teardown).each { |hook| hook.call(@number) }
|
|
84
|
+
RailsIntegration.run_cleanup(@number)
|
|
85
|
+
rescue SystemExit => e
|
|
86
|
+
finish_interrupted_unit(reporter)
|
|
87
|
+
reporter.exit_requested(e.status)
|
|
88
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
89
|
+
finish_interrupted_unit(reporter)
|
|
90
|
+
reporter.notify_non_example_exception(e, "An error occurred in parallel worker #{@number}.")
|
|
91
|
+
end
|
|
92
|
+
reporter.worker_finished
|
|
93
|
+
rescue IOError, SystemCallError
|
|
94
|
+
# The main process is gone: there is no one left to report to.
|
|
95
|
+
nil
|
|
96
|
+
# :nocov:
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# `PARALLEL_TEST_GROUPS` is not set: with it, SimpleCov assumes it runs
|
|
100
|
+
# under parallel_tests and makes the last worker wait for a parallel_tests
|
|
101
|
+
# pid file. The main process collects the results of all the workers.
|
|
102
|
+
def prepare_environment(configuration)
|
|
103
|
+
ENV['TEST_ENV_NUMBER'] = @number == 1 ? '' : @number.to_s
|
|
104
|
+
configuration.parallel_worker_number = @number
|
|
105
|
+
configuration.world.wants_to_quit = false
|
|
106
|
+
trap_interrupt(configuration.world)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The main process tells the user it is shutting down: workers
|
|
110
|
+
# quietly stop after their current example.
|
|
111
|
+
def trap_interrupt(world)
|
|
112
|
+
trap('INT') { world.wants_to_quit ? exit!(1) : world.wants_to_quit = true }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def process_commands(registry, reporter, commands)
|
|
116
|
+
# Once quitting, groups return without running any example.
|
|
117
|
+
while (command = commands.read) && command.first == :run
|
|
118
|
+
_, unit, group_index, examples_only, ancestor_indices, example_indices = command
|
|
119
|
+
group = registry.groups[group_index]
|
|
120
|
+
ancestors = ancestor_indices.map { |index| registry.groups[index] }
|
|
121
|
+
restrict_examples(group, example_indices.map { |index| registry.examples[index] }) if example_indices
|
|
122
|
+
reporter.unit = unit
|
|
123
|
+
@unit_started_at = RSpec::Core::Time.now
|
|
124
|
+
succeeded = run_unit(group, examples_only, ancestors, reporter)
|
|
125
|
+
reporter.unit = nil
|
|
126
|
+
reporter.unit_finished(unit, succeeded ? true : false, (RSpec::Core::Time.now - @unit_started_at).to_f)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Workers are forked processes: the examples of a group can be
|
|
131
|
+
# restricted without affecting the other workers.
|
|
132
|
+
def restrict_examples(group, examples)
|
|
133
|
+
RSpec.world.filtered_examples[group] = examples
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def finish_interrupted_unit(reporter)
|
|
137
|
+
return unless (unit = reporter.unit)
|
|
138
|
+
|
|
139
|
+
reporter.unit_finished(unit, false, (RSpec::Core::Time.now - @unit_started_at).to_f)
|
|
140
|
+
reporter.unit = nil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def run_unit(group, examples_only, ancestors, reporter)
|
|
144
|
+
return run_part(group, examples_only, reporter) if ancestors.empty?
|
|
145
|
+
|
|
146
|
+
with_context_hooks_of(ancestors.first, group, examples_only, reporter) do
|
|
147
|
+
run_unit(group, examples_only, ancestors.drop(1), reporter)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def run_part(group, examples_only, reporter)
|
|
152
|
+
examples_only ? group.run_examples(reporter) : group.run(reporter)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Runs the context hooks of a split group around part of it, as
|
|
156
|
+
# `ExampleGroup.run` does around the whole group.
|
|
157
|
+
def with_context_hooks_of(split_group, group, examples_only, reporter)
|
|
158
|
+
return if RSpec.world.wants_to_quit
|
|
159
|
+
|
|
160
|
+
begin
|
|
161
|
+
RSpec.current_scope = :before_context_hook
|
|
162
|
+
split_group.run_before_context_hooks(split_group.new('before(:context) hook'))
|
|
163
|
+
yield
|
|
164
|
+
rescue Pending::SkipDeclaredInExample => e
|
|
165
|
+
for_part_examples(group, examples_only, reporter) { |example| example.skip_with_exception(reporter, e) }
|
|
166
|
+
true
|
|
167
|
+
rescue Support::AllExceptionsExceptOnesWeMustNotRescue => e
|
|
168
|
+
for_part_examples(group, examples_only, reporter) { |example| example.fail_with_exception(reporter, e) }
|
|
169
|
+
RSpec.world.wants_to_quit = true if reporter.fail_fast_limit_met?
|
|
170
|
+
false
|
|
171
|
+
ensure
|
|
172
|
+
RSpec.current_scope = :after_context_hook
|
|
173
|
+
split_group.run_after_context_hooks(split_group.new('after(:context) hook'))
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def for_part_examples(group, examples_only, reporter, &block)
|
|
178
|
+
return group.filtered_examples.each(&block) if examples_only
|
|
179
|
+
|
|
180
|
+
reporter.example_group_started(group)
|
|
181
|
+
group.for_filtered_examples(reporter, &block)
|
|
182
|
+
reporter.example_group_finished(group)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
module RSpec
|
|
2
|
+
module Core
|
|
3
|
+
module Parallel
|
|
4
|
+
# Stands in for the reporter inside a worker process: rather than
|
|
5
|
+
# notifying formatters, it forwards every event to the main process.
|
|
6
|
+
#
|
|
7
|
+
# Events are sent in batches, once per example, to keep the main
|
|
8
|
+
# process, which replays the events of every worker, from becoming a
|
|
9
|
+
# bottleneck.
|
|
10
|
+
#
|
|
11
|
+
# Values provided by user code (metadata, notifications) are marshaled
|
|
12
|
+
# on their own, so that one the main process cannot load (for instance
|
|
13
|
+
# of a class defined while running an example) only affects itself.
|
|
14
|
+
#
|
|
15
|
+
# @private
|
|
16
|
+
class WorkerReporter < Reporter
|
|
17
|
+
UNFORWARDED_METADATA_KEYS = [:execution_result, :example_group, :block, :last_run_status].freeze
|
|
18
|
+
MAX_BATCH_SIZE = 200
|
|
19
|
+
|
|
20
|
+
def initialize(configuration, registry, channel, control)
|
|
21
|
+
super(configuration)
|
|
22
|
+
@registry = registry
|
|
23
|
+
@channel = channel
|
|
24
|
+
@control = control
|
|
25
|
+
@metadata_baselines = {}.compare_by_identity
|
|
26
|
+
@unit = nil
|
|
27
|
+
@batch = []
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
attr_accessor :unit
|
|
31
|
+
|
|
32
|
+
def notify(event, notification)
|
|
33
|
+
case notification
|
|
34
|
+
when Notifications::ExampleNotification
|
|
35
|
+
forward_example_event(event, notification.example)
|
|
36
|
+
when Notifications::GroupNotification
|
|
37
|
+
forward_group_event(event, notification.group)
|
|
38
|
+
else
|
|
39
|
+
forward_notification(event, notification)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Custom notifications are instances of anonymous classes, which
|
|
44
|
+
# cannot be sent to the main process: their options are sent instead.
|
|
45
|
+
def publish(event, options={})
|
|
46
|
+
return super if RSPEC_NOTIFICATIONS.include?(event)
|
|
47
|
+
|
|
48
|
+
post([:publish, @unit, event, options.to_h { |key, value| [key, pack(value)] }])
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def notify_non_example_exception(exception, context_description)
|
|
52
|
+
@configuration.world.non_example_failure = true
|
|
53
|
+
post([:non_example_exception, @unit, ExceptionSerializer.dump(exception), context_description])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Sending the start of each example lets the main process report the
|
|
57
|
+
# example that was running if the worker crashes.
|
|
58
|
+
def example_started(example)
|
|
59
|
+
super
|
|
60
|
+
flush
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def example_finished(example)
|
|
64
|
+
super
|
|
65
|
+
poll_control
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def unit_finished(unit, succeeded, run_time)
|
|
69
|
+
post([:unit_done, unit, succeeded, run_time])
|
|
70
|
+
flush
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Asks the main process to exit with the given status once the run is
|
|
74
|
+
# over, as when `exit` is called while running the suite sequentially.
|
|
75
|
+
def exit_requested(status)
|
|
76
|
+
post([:exit, @unit, status])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def worker_finished
|
|
80
|
+
post([:bye, nil])
|
|
81
|
+
flush
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Marshals a value on its own, along with a description of it to
|
|
85
|
+
# report in its place if the main process cannot load it.
|
|
86
|
+
def self.pack(value)
|
|
87
|
+
[Marshal.dump(value), value.inspect]
|
|
88
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
89
|
+
[nil, (value.inspect rescue value.class.name)] # rubocop:disable Style/RescueModifier
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @return the packed value, or `fallback` (by default, the description
|
|
93
|
+
# of the value) if it cannot be loaded.
|
|
94
|
+
def self.unpack(packed, fallback=packed[1])
|
|
95
|
+
data = packed[0]
|
|
96
|
+
data ? Marshal.load(data) : fallback # rubocop:disable Security/MarshalLoad
|
|
97
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
98
|
+
fallback
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def flush
|
|
102
|
+
return if @batch.empty?
|
|
103
|
+
|
|
104
|
+
batch = @batch
|
|
105
|
+
@batch = []
|
|
106
|
+
@channel.write(batch)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def post(message)
|
|
112
|
+
@batch << message
|
|
113
|
+
flush if @batch.size >= MAX_BATCH_SIZE
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def pack(value)
|
|
117
|
+
self.class.pack(value)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def forward_example_event(event, example)
|
|
121
|
+
index = @registry.example_index(example)
|
|
122
|
+
return forward_unknown_example(event, example) unless index
|
|
123
|
+
|
|
124
|
+
post([:example, @unit, event, index, example_payload(event, example)])
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def example_payload(event, example)
|
|
128
|
+
case event
|
|
129
|
+
when :example_started
|
|
130
|
+
@metadata_baselines[example] = metadata_snapshot(example.metadata)
|
|
131
|
+
[nanoseconds(example.execution_result.started_at)]
|
|
132
|
+
when :example_finished
|
|
133
|
+
execution_result_payload(example.execution_result) << metadata_changes(example)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def execution_result_payload(result)
|
|
138
|
+
[
|
|
139
|
+
nanoseconds(result.started_at),
|
|
140
|
+
result.status,
|
|
141
|
+
ExceptionSerializer.dump(result.exception),
|
|
142
|
+
nanoseconds(result.finished_at),
|
|
143
|
+
result.run_time,
|
|
144
|
+
result.pending_message,
|
|
145
|
+
ExceptionSerializer.dump(result.pending_exception),
|
|
146
|
+
result.pending_fixed
|
|
147
|
+
]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def nanoseconds(time)
|
|
151
|
+
time && ((time.to_i * 1_000_000_000) + time.nsec)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Hashes and arrays can be changed in place (as rswag does with
|
|
155
|
+
# `example.metadata[:response]`), so their contents are compared too.
|
|
156
|
+
def metadata_snapshot(metadata)
|
|
157
|
+
metadata.each_with_object({}) do |(key, value), snapshot|
|
|
158
|
+
snapshot[key] = [value, contents_of(value)] unless UNFORWARDED_METADATA_KEYS.include?(key)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def contents_of(value)
|
|
163
|
+
Marshal.dump(value) if value.is_a?(Hash) || value.is_a?(Array)
|
|
164
|
+
rescue TypeError
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def metadata_changes(example)
|
|
169
|
+
baseline = @metadata_baselines.delete(example) || {}
|
|
170
|
+
changes = nil
|
|
171
|
+
example.metadata.each do |key, value|
|
|
172
|
+
next if UNFORWARDED_METADATA_KEYS.include?(key)
|
|
173
|
+
|
|
174
|
+
before, contents = baseline[key]
|
|
175
|
+
next if baseline.key?(key) && before.equal?(value) && contents_of(value) == contents
|
|
176
|
+
|
|
177
|
+
(changes ||= {})[key] = pack(value)
|
|
178
|
+
end
|
|
179
|
+
changes
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def forward_unknown_example(event, example)
|
|
183
|
+
return unless event == :example_failed
|
|
184
|
+
|
|
185
|
+
message = "Failure in an example unknown to the main process: #{example.full_description}\n" +
|
|
186
|
+
ExceptionSerializer.safe_message(example.exception || StandardError.new)
|
|
187
|
+
forward_notification(:message, Notifications::MessageNotification.new(message))
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def forward_group_event(event, group)
|
|
191
|
+
index = @registry.group_index(group)
|
|
192
|
+
post([:group, @unit, event, index]) if index
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def forward_notification(event, notification)
|
|
196
|
+
if event == :message
|
|
197
|
+
post([:message, @unit, notification.message.to_s])
|
|
198
|
+
else
|
|
199
|
+
post([:notify, @unit, event, pack(notification)])
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def poll_control
|
|
204
|
+
return unless @control.readable_now?
|
|
205
|
+
|
|
206
|
+
command = @control.read
|
|
207
|
+
@configuration.world.wants_to_quit = true if command.nil? || command.first == :quit
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
require 'rspec/core'
|
|
2
|
+
require_relative 'parallel/version'
|
|
3
|
+
|
|
4
|
+
module RSpec
|
|
5
|
+
module Core
|
|
6
|
+
# Multi-process execution of a spec suite.
|
|
7
|
+
#
|
|
8
|
+
# The suite is loaded once in the main process, then forked into a pool
|
|
9
|
+
# of workers which share the loaded code copy-on-write. Top-level example
|
|
10
|
+
# groups (or parts of long ones) are dispatched dynamically to idle
|
|
11
|
+
# workers, longest first. Workers stream every
|
|
12
|
+
# reporter event back to the main process, which replays it against its
|
|
13
|
+
# own copies of the examples, so formatters, `--only-failures`, profiling
|
|
14
|
+
# and exit codes behave exactly as in a sequential run.
|
|
15
|
+
#
|
|
16
|
+
# Because every worker is a separate process, global state (constants,
|
|
17
|
+
# stubs, `RSpec.configuration`, database connections...) is never shared
|
|
18
|
+
# between concurrently running examples.
|
|
19
|
+
#
|
|
20
|
+
# @private
|
|
21
|
+
module Parallel
|
|
22
|
+
# @return [Array<Scheduler::Unit>, nil] the units of work to run in
|
|
23
|
+
# parallel, or nil when the groups must run sequentially.
|
|
24
|
+
def self.units_for(configuration, world, example_groups)
|
|
25
|
+
return nil unless configuration.parallel_workers.to_i > 1 && !configuration.dry_run?
|
|
26
|
+
|
|
27
|
+
unless supported?
|
|
28
|
+
RSpec.warning "Running specs in parallel requires `Process.fork`, which is not " \
|
|
29
|
+
"supported here: running them sequentially", :call_site => nil
|
|
30
|
+
return nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
require_components
|
|
34
|
+
units = Scheduler.new(configuration, world).units(example_groups, configuration.parallel_workers.to_i)
|
|
35
|
+
units if units.size > 1
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Loaded only for parallel runs.
|
|
39
|
+
def self.require_components
|
|
40
|
+
%w[
|
|
41
|
+
channel exception_serializer rails_integration simplecov_integration registry scheduler worker_reporter
|
|
42
|
+
worker coordinator
|
|
43
|
+
].each do |file|
|
|
44
|
+
require_relative "parallel/#{file}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Runs the units in forked worker processes.
|
|
49
|
+
#
|
|
50
|
+
# @return [Boolean] whether all examples passed
|
|
51
|
+
def self.run(configuration, world, units, reporter)
|
|
52
|
+
worker_count = [configuration.parallel_workers.to_i, units.size].min
|
|
53
|
+
passed = Coordinator.new(configuration, world, worker_count).run(units, reporter)
|
|
54
|
+
persist_run_times(configuration, world)
|
|
55
|
+
passed
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.persist_run_times(configuration, world)
|
|
59
|
+
return unless (path = configuration.parallel_run_times_file_path)
|
|
60
|
+
|
|
61
|
+
ExampleStatusPersister.persist(world.all_examples, path)
|
|
62
|
+
rescue SystemCallError => e
|
|
63
|
+
RSpec.warning "Could not write example run times to #{path} (configured as " \
|
|
64
|
+
"`config.parallel_run_times_file_path`) due to a system error: " \
|
|
65
|
+
"#{e.inspect}.", :call_site => nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.supported?
|
|
69
|
+
Process.respond_to?(:fork) && RUBY_ENGINE == 'ruby' && !RSpec::Support::OS.windows?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# One worker per physical core: workers sharing a core through
|
|
73
|
+
# simultaneous multithreading (hyper-threading) mostly slow each other
|
|
74
|
+
# down, using more CPU time and memory for the same wall time.
|
|
75
|
+
def self.default_worker_count
|
|
76
|
+
require 'etc'
|
|
77
|
+
[Etc.nprocessors, physical_core_count].compact.min
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.physical_core_count
|
|
81
|
+
siblings = Dir['/sys/devices/system/cpu/cpu[0-9]*/topology/thread_siblings_list']
|
|
82
|
+
return siblings.map { |path| File.read(path).strip }.uniq.size unless siblings.empty?
|
|
83
|
+
|
|
84
|
+
return nil unless RbConfig::CONFIG['host_os'] =~ /darwin/
|
|
85
|
+
|
|
86
|
+
count = IO.popen(%w[sysctl -n hw.physicalcpu], :err => File::NULL, &:read).to_i
|
|
87
|
+
count if count > 0
|
|
88
|
+
rescue SystemCallError, IOError
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
require_relative 'parallel/core_ext'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require 'rspec/core/parallel'
|
metadata
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rspec-core-parallel
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Alessandro Rodi
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rspec-core
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 3.13.0
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '3.14'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: 3.13.0
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '3.14'
|
|
32
|
+
description: 'Adds `rspec --parallel` to rspec-core 3.13: the suite is loaded once,
|
|
33
|
+
forked into one worker per CPU core, and groups are handed out longest first using
|
|
34
|
+
the run times of the previous run. Formatters, --only-failures, --bisect and exit
|
|
35
|
+
codes work as in a sequential run. Rails apps get a test database per worker.'
|
|
36
|
+
email:
|
|
37
|
+
- alessandro.rodi@renuo.ch
|
|
38
|
+
executables:
|
|
39
|
+
- rspec-parallel
|
|
40
|
+
extensions: []
|
|
41
|
+
extra_rdoc_files: []
|
|
42
|
+
files:
|
|
43
|
+
- CHANGELOG.md
|
|
44
|
+
- LICENSE.md
|
|
45
|
+
- README.md
|
|
46
|
+
- exe/rspec-parallel
|
|
47
|
+
- lib/rspec-core-parallel.rb
|
|
48
|
+
- lib/rspec/core/parallel.rb
|
|
49
|
+
- lib/rspec/core/parallel/channel.rb
|
|
50
|
+
- lib/rspec/core/parallel/coordinator.rb
|
|
51
|
+
- lib/rspec/core/parallel/core_ext.rb
|
|
52
|
+
- lib/rspec/core/parallel/exception_serializer.rb
|
|
53
|
+
- lib/rspec/core/parallel/rails_integration.rb
|
|
54
|
+
- lib/rspec/core/parallel/registry.rb
|
|
55
|
+
- lib/rspec/core/parallel/scheduler.rb
|
|
56
|
+
- lib/rspec/core/parallel/simplecov_integration.rb
|
|
57
|
+
- lib/rspec/core/parallel/version.rb
|
|
58
|
+
- lib/rspec/core/parallel/worker.rb
|
|
59
|
+
- lib/rspec/core/parallel/worker_reporter.rb
|
|
60
|
+
homepage: https://github.com/renuo/rspec-core-parallel
|
|
61
|
+
licenses:
|
|
62
|
+
- MIT
|
|
63
|
+
metadata:
|
|
64
|
+
homepage_uri: https://github.com/renuo/rspec-core-parallel
|
|
65
|
+
source_code_uri: https://github.com/renuo/rspec-core-parallel
|
|
66
|
+
changelog_uri: https://github.com/renuo/rspec-core-parallel/blob/main/CHANGELOG.md
|
|
67
|
+
rubygems_mfa_required: 'true'
|
|
68
|
+
rdoc_options: []
|
|
69
|
+
require_paths:
|
|
70
|
+
- lib
|
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '3.3'
|
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '0'
|
|
81
|
+
requirements: []
|
|
82
|
+
rubygems_version: 3.6.9
|
|
83
|
+
specification_version: 4
|
|
84
|
+
summary: 'Run an RSpec suite in forked worker processes: rspec --parallel'
|
|
85
|
+
test_files: []
|