rspec-conductor 1.0.9 → 1.0.11
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/CHANGELOG.md +11 -0
- data/lib/rspec/conductor/cli.rb +6 -0
- data/lib/rspec/conductor/formatters/base.rb +19 -11
- data/lib/rspec/conductor/formatters/ci.rb +8 -8
- data/lib/rspec/conductor/formatters/fancy.rb +7 -7
- data/lib/rspec/conductor/formatters/plain.rb +1 -1
- data/lib/rspec/conductor/server.rb +27 -35
- data/lib/rspec/conductor/{results.rb → suite_run.rb} +6 -3
- data/lib/rspec/conductor/util/child_process.rb +37 -18
- data/lib/rspec/conductor/version.rb +1 -1
- data/lib/rspec/conductor/worker.rb +17 -4
- data/lib/rspec/conductor/worker_process.rb +51 -16
- data/lib/rspec/conductor.rb +1 -1
- metadata +7 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a4f284fb6887972bbd325f6a5d6ce77b3c384d3cc7b8933c19bd04af2aa3ada
|
|
4
|
+
data.tar.gz: 733f1d8de0935949b77aa54374f37f97976a007c7dc53381b1dc8d8bb3e6922d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e23c66e05afe4064b4a6062d6f7880429e641c5e2a3eac73f60a50ba5bd4c75ecb2b36a8484405200e9e8102f0cfd24b20fb99a0fbdf959be376e0087cf4006d
|
|
7
|
+
data.tar.gz: ebc3ad89f178da346ce4a5aee153e280b8b10b98f2c1d4e55894035237ee9f990e34206f272f35128d0e94da7c01847acd15225ab7a8db0516ceb674618f1887
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
## [1.0.11] - 2026-08-03
|
|
2
|
+
|
|
3
|
+
- Fix a bug in 2.x that would cause an exception because of the 3.x syntax usage (`_1`) in the formatter base
|
|
4
|
+
- Remove a potential bottleneck when spec files run under 10ms on average
|
|
5
|
+
- Fix performance issue with recalculating the file/location list over and over (which would coincidentally also take around 10ms for large directories)
|
|
6
|
+
- In other words, if you had a lot of super fast spec files, it will run them fast as well
|
|
7
|
+
|
|
8
|
+
## [1.0.10] - 2026-03-08
|
|
9
|
+
|
|
10
|
+
- Add --print-slowest cli param to display the slowest specs in the suite
|
|
11
|
+
|
|
1
12
|
## [1.0.9] - 2026-03-01
|
|
2
13
|
|
|
3
14
|
- Handle workers stdout/stderr better. It is no longer necessary to use --verbose to see worker output. Verbose now only controls whether you see the debug output of the workers
|
data/lib/rspec/conductor/cli.rb
CHANGED
|
@@ -15,6 +15,7 @@ module RSpec
|
|
|
15
15
|
display_retry_backtraces: false,
|
|
16
16
|
prefork_require: 'config/application.rb',
|
|
17
17
|
postfork_require: :spec_helper,
|
|
18
|
+
print_slowest_count: nil,
|
|
18
19
|
}.freeze
|
|
19
20
|
|
|
20
21
|
def self.run(argv)
|
|
@@ -97,6 +98,10 @@ module RSpec
|
|
|
97
98
|
@conductor_options[:display_retry_backtraces] = true
|
|
98
99
|
end
|
|
99
100
|
|
|
101
|
+
opts.on("--print-slowest COUNT", Integer, "Print slowest specs with their execution times") do |n|
|
|
102
|
+
@conductor_options[:print_slowest_count] = n
|
|
103
|
+
end
|
|
104
|
+
|
|
100
105
|
opts.on("--verbose", "Enable debug output") do
|
|
101
106
|
@conductor_options[:verbose] = true
|
|
102
107
|
end
|
|
@@ -120,6 +125,7 @@ module RSpec
|
|
|
120
125
|
rspec_args: @rspec_args,
|
|
121
126
|
formatter: @conductor_options[:formatter],
|
|
122
127
|
display_retry_backtraces: @conductor_options[:display_retry_backtraces],
|
|
128
|
+
print_slowest_count: @conductor_options[:print_slowest_count],
|
|
123
129
|
verbose: @conductor_options[:verbose],
|
|
124
130
|
).run
|
|
125
131
|
end
|
|
@@ -9,7 +9,7 @@ module RSpec
|
|
|
9
9
|
def initialize(**_kwargs)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
def handle_worker_message(_worker_process, _message,
|
|
12
|
+
def handle_worker_message(_worker_process, _message, _suite_run)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def print_startup_banner(worker_count:, seed:, spec_files_count:)
|
|
@@ -17,15 +17,15 @@ module RSpec
|
|
|
17
17
|
puts "Running #{spec_files_count} spec files\n\n"
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def print_summary(
|
|
20
|
+
def print_summary(suite_run, seed:, success:)
|
|
21
21
|
puts "\n\n"
|
|
22
22
|
puts "Randomized with seed #{seed}"
|
|
23
|
-
puts "#{colorize("#{
|
|
24
|
-
puts colorize("Worker crashes: #{
|
|
23
|
+
puts "#{colorize("#{suite_run.examples_passed} passed", :green)}, #{colorize("#{suite_run.examples_failed} failed", :red)}, #{colorize("#{suite_run.examples_pending} pending", :yellow)}"
|
|
24
|
+
puts colorize("Worker crashes: #{suite_run.worker_crashes}", :red) if suite_run.worker_crashes.positive?
|
|
25
25
|
|
|
26
|
-
if
|
|
26
|
+
if suite_run.errors.any?
|
|
27
27
|
puts "\nFailures:\n\n"
|
|
28
|
-
|
|
28
|
+
suite_run.errors.each_with_index do |error, i|
|
|
29
29
|
puts " #{i + 1}) #{error[:description]}"
|
|
30
30
|
puts colorize(" #{error[:message]}", :red) if error[:message]
|
|
31
31
|
puts colorize(" #{error[:location]}", :cyan)
|
|
@@ -37,14 +37,22 @@ module RSpec
|
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
puts "Specs took: #{
|
|
41
|
-
puts "Total runtime: #{
|
|
40
|
+
puts "Specs took: #{suite_run.specs_runtime.round(2)}s"
|
|
41
|
+
puts "Total runtime: #{suite_run.total_runtime.round(2)}s"
|
|
42
42
|
puts "Suite: #{success ? colorize("PASSED", :green) : colorize("FAILED", :red)}"
|
|
43
43
|
|
|
44
|
-
if
|
|
44
|
+
if suite_run.errors.any?
|
|
45
45
|
puts ""
|
|
46
46
|
puts "To rerun failed examples:"
|
|
47
|
-
puts " rspec #{
|
|
47
|
+
puts " rspec #{suite_run.errors.map { |e| e[:location] }.join(" ")}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def print_slowest(suite_run, n)
|
|
52
|
+
puts "\n\n"
|
|
53
|
+
puts "Slowest #{n} specs:"
|
|
54
|
+
suite_run.example_stats.sort_by { |e| -e[:run_time] }.take(n).each_with_index do |e, i|
|
|
55
|
+
puts "%3d. (%8.2fms) %s @ %s" % [i + 1, e[:run_time] * 1000, colorize(e[:description], :dim), colorize(e[:location], :cyan)]
|
|
48
56
|
end
|
|
49
57
|
end
|
|
50
58
|
|
|
@@ -66,7 +74,7 @@ module RSpec
|
|
|
66
74
|
#{message[:location]}
|
|
67
75
|
#{message[:exception_class]}: #{message[:message]}
|
|
68
76
|
Backtrace:
|
|
69
|
-
#{message[:backtrace].map { " #{
|
|
77
|
+
#{message[:backtrace].map { |line| " #{line}" }.join("\n")}
|
|
70
78
|
EOM
|
|
71
79
|
end
|
|
72
80
|
|
|
@@ -14,22 +14,22 @@ module RSpec
|
|
|
14
14
|
super(**kwargs)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
def handle_worker_message(_worker_process, message,
|
|
17
|
+
def handle_worker_message(_worker_process, message, suite_run)
|
|
18
18
|
public_send(message[:type], message) if respond_to?(message[:type])
|
|
19
|
-
print_status(
|
|
19
|
+
print_status(suite_run) if @last_printout + @printout_interval < Time.now
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def print_status(
|
|
22
|
+
def print_status(suite_run)
|
|
23
23
|
@last_printout = Time.now
|
|
24
|
-
pct =
|
|
24
|
+
pct = suite_run.spec_file_processed_percentage
|
|
25
25
|
|
|
26
26
|
puts "-" * tty_width
|
|
27
27
|
puts "Current status [#{Time.now.strftime("%H:%M:%S")}]:"
|
|
28
|
-
puts "Processed: #{
|
|
29
|
-
puts "#{
|
|
30
|
-
if
|
|
28
|
+
puts "Processed: #{suite_run.spec_files_processed} / #{suite_run.spec_files_total} (#{(pct * 100).floor}%)"
|
|
29
|
+
puts "#{suite_run.examples_passed} passed, #{suite_run.examples_failed} failed, #{suite_run.examples_pending} pending"
|
|
30
|
+
if suite_run.errors.any?
|
|
31
31
|
puts "Failures:\n"
|
|
32
|
-
|
|
32
|
+
suite_run.errors.each_with_index do |error, i|
|
|
33
33
|
puts " #{i + 1}) #{error[:description]}"
|
|
34
34
|
puts " #{error[:location]}"
|
|
35
35
|
puts " #{error[:message]}" if error[:message]
|
|
@@ -30,9 +30,9 @@ module RSpec
|
|
|
30
30
|
super(**kwargs)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
def handle_worker_message(worker_process, message,
|
|
33
|
+
def handle_worker_message(worker_process, message, suite_run)
|
|
34
34
|
public_send(message[:type], worker_process, message) if respond_to?(message[:type])
|
|
35
|
-
redraw(worker_process,
|
|
35
|
+
redraw(worker_process, suite_run)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def example_passed(_worker_process, _message)
|
|
@@ -66,9 +66,9 @@ module RSpec
|
|
|
66
66
|
|
|
67
67
|
private
|
|
68
68
|
|
|
69
|
-
def redraw(worker_process,
|
|
69
|
+
def redraw(worker_process, suite_run)
|
|
70
70
|
update_worker_status_line(worker_process)
|
|
71
|
-
|
|
71
|
+
update_suite_run_line(suite_run)
|
|
72
72
|
update_errors_line
|
|
73
73
|
@terminal.redraw
|
|
74
74
|
@terminal.scroll_to_bottom
|
|
@@ -94,13 +94,13 @@ module RSpec
|
|
|
94
94
|
@worker_lines[worker_process.number].update(status, redraw: false)
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
-
def
|
|
98
|
-
pct =
|
|
97
|
+
def update_suite_run_line(suite_run)
|
|
98
|
+
pct = suite_run.spec_file_processed_percentage
|
|
99
99
|
bar_width = [tty_width - 20, 20].max
|
|
100
100
|
filled = (pct * bar_width).floor
|
|
101
101
|
empty = bar_width - filled
|
|
102
102
|
|
|
103
|
-
percentage = " %3d%% (%d/%d)" % [(pct * 100).floor,
|
|
103
|
+
percentage = " %3d%% (%d/%d)" % [(pct * 100).floor, suite_run.spec_files_processed, suite_run.spec_files_total]
|
|
104
104
|
bar = colorize("[", :reset) + colorize("▓", :green) * filled + colorize(" ", :reset) * empty + colorize("]", :reset)
|
|
105
105
|
|
|
106
106
|
@progress_bar_line.update(bar + percentage, redraw: false)
|
|
@@ -4,7 +4,7 @@ module RSpec
|
|
|
4
4
|
module Conductor
|
|
5
5
|
module Formatters
|
|
6
6
|
class Plain < Base
|
|
7
|
-
def handle_worker_message(_worker_process, message,
|
|
7
|
+
def handle_worker_message(_worker_process, message, _suite_run)
|
|
8
8
|
public_send(message[:type], message) if respond_to?(message[:type])
|
|
9
9
|
end
|
|
10
10
|
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "English"
|
|
4
|
-
require "socket"
|
|
5
|
-
require "json"
|
|
6
|
-
|
|
7
3
|
module RSpec
|
|
8
4
|
module Conductor
|
|
9
5
|
class Server
|
|
10
6
|
MAX_SEED = 2**16
|
|
11
|
-
WORKER_POLL_INTERVAL = 0.01
|
|
12
7
|
|
|
13
8
|
# @option worker_count [Integer] How many workers to spin
|
|
14
9
|
# @option rspec_args [Array<String>] A list of rspec options
|
|
@@ -21,6 +16,7 @@ module RSpec
|
|
|
21
16
|
# @option formatter [String] Use a certain formatter
|
|
22
17
|
# @option verbose [Boolean] Use especially verbose output
|
|
23
18
|
# @option display_retry_backtraces [Boolean] Display backtraces for specs retried via rspec-retry
|
|
19
|
+
# @option print_slowest_count [Integer] Print slowest specs in the end of the suite
|
|
24
20
|
def initialize(worker_count:, rspec_args:, **opts)
|
|
25
21
|
@worker_count = worker_count
|
|
26
22
|
@worker_number_offset = opts.fetch(:worker_number_offset, 0)
|
|
@@ -30,11 +26,11 @@ module RSpec
|
|
|
30
26
|
@seed = opts[:seed] || (Random.new_seed % MAX_SEED)
|
|
31
27
|
@fail_fast_after = opts[:fail_fast_after]
|
|
32
28
|
@display_retry_backtraces = opts.fetch(:display_retry_backtraces, false)
|
|
29
|
+
@print_slowest_count = opts.fetch(:print_slowest_count, nil)
|
|
33
30
|
@verbose = opts.fetch(:verbose, false)
|
|
34
31
|
|
|
35
32
|
@rspec_args = rspec_args
|
|
36
33
|
@worker_processes = []
|
|
37
|
-
@dead_worker_processes = []
|
|
38
34
|
@spec_queue = []
|
|
39
35
|
@formatter_class = case opts[:formatter]
|
|
40
36
|
when "ci"
|
|
@@ -47,7 +43,7 @@ module RSpec
|
|
|
47
43
|
(!@verbose && Formatters::Fancy.recommended?) ? Formatters::Fancy : Formatters::Plain
|
|
48
44
|
end
|
|
49
45
|
@formatter = @formatter_class.new(worker_count: @worker_count)
|
|
50
|
-
@
|
|
46
|
+
@suite_run = SuiteRun.new
|
|
51
47
|
|
|
52
48
|
$stdout.sync = true
|
|
53
49
|
$stdin.echo = false if $stdin.tty?
|
|
@@ -65,9 +61,10 @@ module RSpec
|
|
|
65
61
|
start_workers
|
|
66
62
|
run_event_loop
|
|
67
63
|
wait_for_workers_to_exit
|
|
68
|
-
@
|
|
64
|
+
@suite_run.suite_complete
|
|
69
65
|
|
|
70
|
-
@formatter.print_summary(@
|
|
66
|
+
@formatter.print_summary(@suite_run, seed: @seed, success: success?)
|
|
67
|
+
@formatter.print_slowest(@suite_run, @print_slowest_count) if @print_slowest_count
|
|
71
68
|
exit_with_status
|
|
72
69
|
end
|
|
73
70
|
|
|
@@ -95,7 +92,7 @@ module RSpec
|
|
|
95
92
|
debug "RSpec config: #{config.inspect}"
|
|
96
93
|
debug "Files to run: #{config.files_to_run}"
|
|
97
94
|
@spec_queue = config.files_to_run.shuffle(random: Random.new(@seed))
|
|
98
|
-
@
|
|
95
|
+
@suite_run.spec_files_total = @spec_queue.size
|
|
99
96
|
end
|
|
100
97
|
|
|
101
98
|
def preload_application
|
|
@@ -127,21 +124,18 @@ module RSpec
|
|
|
127
124
|
@shutdown_status = :shutdown_messages_sent
|
|
128
125
|
@formatter.print_shutdown_banner
|
|
129
126
|
@worker_processes.select(&:running?).each do |worker_process|
|
|
130
|
-
worker_process.
|
|
127
|
+
worker_process.send_message({ type: :shutdown })
|
|
131
128
|
cleanup_worker_process(worker_process)
|
|
132
129
|
end
|
|
133
130
|
end
|
|
134
131
|
|
|
135
|
-
|
|
136
|
-
readable_ios, = IO.select(worker_processes_by_io.keys, nil, nil, 0)
|
|
137
|
-
readable_ios&.each { |io| handle_worker_message(worker_processes_by_io.fetch(io)) }
|
|
138
|
-
Util::ChildProcess.tick_all(@worker_processes.map(&:child_process))
|
|
132
|
+
WorkerProcess.tick_all(@worker_processes)
|
|
139
133
|
reap_workers
|
|
140
134
|
end
|
|
141
135
|
end
|
|
142
136
|
|
|
143
137
|
def wait_for_workers_to_exit
|
|
144
|
-
|
|
138
|
+
WorkerProcess.wait_all(@worker_processes)
|
|
145
139
|
end
|
|
146
140
|
|
|
147
141
|
def spawn_worker(worker_number)
|
|
@@ -150,6 +144,7 @@ module RSpec
|
|
|
150
144
|
worker_process = WorkerProcess.spawn(
|
|
151
145
|
number: worker_number,
|
|
152
146
|
test_env_number: (@first_is_1 || worker_number != 1) ? worker_number.to_s : "",
|
|
147
|
+
on_message: ->(worker_process, message) { handle_worker_message(worker_process, message) },
|
|
153
148
|
on_stdout: ->(string) { @formatter.handle_worker_stdout(worker_number, string) },
|
|
154
149
|
on_stderr: ->(string) { @formatter.handle_worker_stderr(worker_number, string) },
|
|
155
150
|
debug_io: @verbose ? $stderr : nil,
|
|
@@ -160,37 +155,34 @@ module RSpec
|
|
|
160
155
|
worker_process
|
|
161
156
|
end
|
|
162
157
|
|
|
163
|
-
def handle_worker_message(worker_process)
|
|
164
|
-
message = worker_process.socket.receive_message
|
|
165
|
-
return unless message
|
|
166
|
-
|
|
158
|
+
def handle_worker_message(worker_process, message)
|
|
167
159
|
debug "Worker #{worker_process.number}: #{message[:type]}"
|
|
168
160
|
|
|
169
161
|
case message[:type].to_sym
|
|
170
162
|
when :example_passed
|
|
171
|
-
@
|
|
163
|
+
@suite_run.example_passed(message)
|
|
172
164
|
when :example_failed
|
|
173
|
-
@
|
|
165
|
+
@suite_run.example_failed(message)
|
|
174
166
|
|
|
175
|
-
if @fail_fast_after && @
|
|
176
|
-
debug "Shutting down after #{@
|
|
167
|
+
if @fail_fast_after && @suite_run.examples_failed >= @fail_fast_after
|
|
168
|
+
debug "Shutting down after #{@suite_run.examples_failed} failures"
|
|
177
169
|
initiate_shutdown
|
|
178
170
|
end
|
|
179
171
|
when :example_pending
|
|
180
|
-
@
|
|
172
|
+
@suite_run.example_pending
|
|
181
173
|
when :example_retried
|
|
182
174
|
@formatter.print_retry_message(message) if @display_retry_backtraces
|
|
183
175
|
when :spec_complete
|
|
184
|
-
@
|
|
176
|
+
@suite_run.spec_file_complete
|
|
185
177
|
worker_process.current_spec = nil
|
|
186
178
|
assign_work(worker_process)
|
|
187
179
|
when :spec_error
|
|
188
|
-
@
|
|
180
|
+
@suite_run.spec_file_error(message)
|
|
189
181
|
debug "Spec error details: #{message[:error]}"
|
|
190
182
|
worker_process.current_spec = nil
|
|
191
183
|
assign_work(worker_process)
|
|
192
184
|
end
|
|
193
|
-
@formatter.handle_worker_message(worker_process, message, @
|
|
185
|
+
@formatter.handle_worker_message(worker_process, message, @suite_run)
|
|
194
186
|
end
|
|
195
187
|
|
|
196
188
|
def assign_work(worker_process)
|
|
@@ -198,21 +190,21 @@ module RSpec
|
|
|
198
190
|
|
|
199
191
|
if shutting_down? || !spec_file
|
|
200
192
|
debug "No more work for worker #{worker_process.number}, sending shutdown"
|
|
201
|
-
worker_process.
|
|
193
|
+
worker_process.send_message({ type: :shutdown })
|
|
202
194
|
cleanup_worker_process(worker_process)
|
|
203
195
|
else
|
|
204
|
-
@
|
|
196
|
+
@suite_run.spec_file_assigned
|
|
205
197
|
worker_process.current_spec = spec_file
|
|
206
198
|
debug "Assigning #{spec_file} to worker #{worker_process.number}"
|
|
207
199
|
message = { type: :worker_assigned_spec, file: spec_file }
|
|
208
|
-
worker_process.
|
|
209
|
-
@formatter.handle_worker_message(worker_process, message, @
|
|
200
|
+
worker_process.send_message(message)
|
|
201
|
+
@formatter.handle_worker_message(worker_process, message, @suite_run)
|
|
210
202
|
end
|
|
211
203
|
end
|
|
212
204
|
|
|
213
205
|
def cleanup_worker_process(worker_process, status: :shut_down)
|
|
214
206
|
worker_process.shut_down(status)
|
|
215
|
-
@formatter.handle_worker_message(worker_process, { type: :worker_shut_down }, @
|
|
207
|
+
@formatter.handle_worker_message(worker_process, { type: :worker_shut_down }, @suite_run)
|
|
216
208
|
end
|
|
217
209
|
|
|
218
210
|
def reap_workers
|
|
@@ -224,7 +216,7 @@ module RSpec
|
|
|
224
216
|
|
|
225
217
|
dead_worker_processes.each do |worker_process, exitstatus|
|
|
226
218
|
cleanup_worker_process(worker_process, status: :terminated)
|
|
227
|
-
@
|
|
219
|
+
@suite_run.worker_crashed
|
|
228
220
|
debug "Worker #{worker_process.number} exited with status #{exitstatus.exitstatus}, signal #{exitstatus.termsig}"
|
|
229
221
|
end
|
|
230
222
|
end
|
|
@@ -243,7 +235,7 @@ module RSpec
|
|
|
243
235
|
end
|
|
244
236
|
|
|
245
237
|
def success?
|
|
246
|
-
@
|
|
238
|
+
@suite_run.success? && !shutting_down?
|
|
247
239
|
end
|
|
248
240
|
|
|
249
241
|
def exit_with_status
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module RSpec
|
|
4
4
|
module Conductor
|
|
5
|
-
class
|
|
6
|
-
attr_accessor :examples_passed, :examples_failed, :examples_pending, :worker_crashes, :errors, :started_at, :spec_files_total, :spec_files_processed
|
|
5
|
+
class SuiteRun
|
|
6
|
+
attr_accessor :examples_passed, :examples_failed, :examples_pending, :worker_crashes, :errors, :started_at, :spec_files_total, :spec_files_processed, :example_stats
|
|
7
7
|
|
|
8
8
|
def initialize
|
|
9
9
|
@examples_passed = 0
|
|
@@ -16,17 +16,20 @@ module RSpec
|
|
|
16
16
|
@specs_completed_at = nil
|
|
17
17
|
@spec_files_total = 0
|
|
18
18
|
@spec_files_processed = 0
|
|
19
|
+
@example_stats = []
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def success?
|
|
22
23
|
@examples_failed.zero? && @errors.empty? && @worker_crashes.zero? && @spec_files_total == @spec_files_processed
|
|
23
24
|
end
|
|
24
25
|
|
|
25
|
-
def example_passed
|
|
26
|
+
def example_passed(message)
|
|
27
|
+
@example_stats << message.slice(:location, :run_time, :description)
|
|
26
28
|
@examples_passed += 1
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
def example_failed(message)
|
|
32
|
+
@example_stats << message.slice(:location, :run_time, :description)
|
|
30
33
|
@examples_failed += 1
|
|
31
34
|
@errors << message
|
|
32
35
|
end
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "socket"
|
|
4
|
+
|
|
3
5
|
module RSpec
|
|
4
6
|
module Conductor
|
|
5
7
|
module Util
|
|
6
8
|
class ChildProcess
|
|
7
9
|
POLL_INTERVAL = 0.01
|
|
8
10
|
|
|
9
|
-
attr_reader :pid, :exit_status
|
|
11
|
+
attr_reader :pid, :message_socket, :exit_status, :ios
|
|
10
12
|
|
|
11
13
|
def self.fork(**args, &block)
|
|
12
14
|
new(**args).fork(&block)
|
|
@@ -22,44 +24,47 @@ module RSpec
|
|
|
22
24
|
|
|
23
25
|
def self.tick_all(processes, poll_interval: POLL_INTERVAL)
|
|
24
26
|
processes_by_io = processes.each_with_object({}) do |process, memo|
|
|
25
|
-
process.
|
|
27
|
+
process.ios.reject(&:closed?).each { |io| memo[io] = process }
|
|
26
28
|
end
|
|
27
29
|
return false if processes_by_io.empty?
|
|
28
30
|
|
|
29
31
|
ready, = IO.select(processes_by_io.keys, nil, nil, poll_interval)
|
|
30
|
-
ready&.each { |
|
|
32
|
+
ready&.each { |io| processes_by_io[io].handle_available(io) }
|
|
31
33
|
|
|
32
34
|
true
|
|
33
35
|
end
|
|
34
36
|
|
|
35
|
-
def initialize(on_stdout: nil, on_stderr: nil)
|
|
37
|
+
def initialize(on_stdout: nil, on_stderr: nil, on_message: nil)
|
|
36
38
|
@on_stdout = on_stdout
|
|
37
39
|
@on_stderr = on_stderr
|
|
40
|
+
@on_message = on_message
|
|
38
41
|
@pid = nil
|
|
39
42
|
@exit_status = nil
|
|
40
43
|
@stdout_pipe = nil
|
|
41
44
|
@stderr_pipe = nil
|
|
42
45
|
@stdout_buffer = +""
|
|
43
46
|
@stderr_buffer = +""
|
|
47
|
+
@message_socket = nil
|
|
48
|
+
@ios = []
|
|
44
49
|
@done = false
|
|
45
50
|
end
|
|
46
51
|
|
|
47
|
-
def pipes
|
|
48
|
-
[@stdout_pipe, @stderr_pipe].compact
|
|
49
|
-
end
|
|
50
|
-
|
|
51
52
|
def fork(&block)
|
|
52
53
|
raise ArgumentError, '.fork should be called with a block' unless block_given?
|
|
53
54
|
|
|
54
55
|
stdout_read, stdout_write = IO.pipe
|
|
55
56
|
stderr_read, stderr_write = IO.pipe
|
|
57
|
+
parent_socket, child_socket = Socket.pair(:UNIX, :STREAM, 0) if @on_message
|
|
56
58
|
|
|
57
59
|
@stdout_pipe = stdout_read
|
|
58
60
|
@stderr_pipe = stderr_read
|
|
61
|
+
@message_socket = parent_socket
|
|
62
|
+
@ios = [@stdout_pipe, @stderr_pipe, @message_socket].compact
|
|
59
63
|
|
|
60
64
|
@pid = Kernel.fork do
|
|
61
65
|
stdout_read.close
|
|
62
66
|
stderr_read.close
|
|
67
|
+
parent_socket&.close
|
|
63
68
|
|
|
64
69
|
$stdout = stdout_write
|
|
65
70
|
$stderr = stderr_write
|
|
@@ -69,13 +74,14 @@ module RSpec
|
|
|
69
74
|
STDIN.reopen($stdin)
|
|
70
75
|
|
|
71
76
|
begin
|
|
72
|
-
yield self
|
|
77
|
+
yield self, child_socket
|
|
73
78
|
rescue => e
|
|
74
79
|
stderr_write.puts "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
|
|
75
80
|
exit 1
|
|
76
81
|
ensure
|
|
77
82
|
stdout_write.close
|
|
78
83
|
stderr_write.close
|
|
84
|
+
child_socket&.close
|
|
79
85
|
end
|
|
80
86
|
|
|
81
87
|
exit 0
|
|
@@ -83,6 +89,7 @@ module RSpec
|
|
|
83
89
|
|
|
84
90
|
stdout_write.close
|
|
85
91
|
stderr_write.close
|
|
92
|
+
child_socket&.close
|
|
86
93
|
|
|
87
94
|
self
|
|
88
95
|
end
|
|
@@ -91,17 +98,27 @@ module RSpec
|
|
|
91
98
|
@done
|
|
92
99
|
end
|
|
93
100
|
|
|
94
|
-
def
|
|
101
|
+
def handle_available(io)
|
|
95
102
|
return if done?
|
|
96
|
-
return if
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return
|
|
103
|
+
return if io.closed?
|
|
104
|
+
|
|
105
|
+
case io
|
|
106
|
+
when @stdout_pipe, @stderr_pipe
|
|
107
|
+
read_pipe(io)
|
|
108
|
+
when @message_socket
|
|
109
|
+
@on_message&.call
|
|
104
110
|
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def read_pipe(pipe)
|
|
114
|
+
buffer, callback = case pipe
|
|
115
|
+
when @stdout_pipe
|
|
116
|
+
[@stdout_buffer, @on_stdout]
|
|
117
|
+
when @stderr_pipe
|
|
118
|
+
[@stderr_buffer, @on_stderr]
|
|
119
|
+
else
|
|
120
|
+
return
|
|
121
|
+
end
|
|
105
122
|
|
|
106
123
|
begin
|
|
107
124
|
data = pipe.read_nonblock(4096, exception: false)
|
|
@@ -132,6 +149,8 @@ module RSpec
|
|
|
132
149
|
rescue Errno::ECHILD
|
|
133
150
|
end
|
|
134
151
|
|
|
152
|
+
@message_socket&.close
|
|
153
|
+
|
|
135
154
|
self
|
|
136
155
|
end
|
|
137
156
|
|
|
@@ -48,8 +48,6 @@ module RSpec
|
|
|
48
48
|
debug "Worker crashed: #{e.class}: #{e.message}"
|
|
49
49
|
debug e.backtrace.join("\n")
|
|
50
50
|
raise
|
|
51
|
-
ensure
|
|
52
|
-
@socket.close
|
|
53
51
|
end
|
|
54
52
|
|
|
55
53
|
private
|
|
@@ -109,12 +107,12 @@ module RSpec
|
|
|
109
107
|
def run_spec(file)
|
|
110
108
|
RSpec.world.reset
|
|
111
109
|
RSpec.configuration.reset
|
|
112
|
-
RSpec.configuration.files_or_directories_to_run = [file]
|
|
113
110
|
RSpec.configuration.output_stream = null_io_out
|
|
114
111
|
RSpec.configuration.error_stream = null_io_out
|
|
115
112
|
RSpec.configuration.formatter_loader.formatters.clear
|
|
116
113
|
RSpec.configuration.add_formatter(RSpecSubscriber.new(@socket, file, -> { check_for_shutdown }))
|
|
117
114
|
parsed_options.configure(RSpec.configuration)
|
|
115
|
+
RSpec.configuration.files_or_directories_to_run = location_entries_for(file)
|
|
118
116
|
RSpec.configuration.files_to_run # this seemingly random line is necessary for rspec to set up the inclusion filters (e.g. hello_spec.rb:123 -> the :123 part is an inclusion filter)
|
|
119
117
|
|
|
120
118
|
begin
|
|
@@ -133,7 +131,7 @@ module RSpec
|
|
|
133
131
|
|
|
134
132
|
@socket.send_message(
|
|
135
133
|
type: :spec_complete,
|
|
136
|
-
file: file
|
|
134
|
+
file: file,
|
|
137
135
|
)
|
|
138
136
|
rescue StandardError => e
|
|
139
137
|
debug "Spec error: #{e.class}: #{e.message}"
|
|
@@ -167,6 +165,21 @@ module RSpec
|
|
|
167
165
|
@parsed_options ||= RSpec::Core::ConfigurationOptions.new(@rspec_args).tap { |co| co.options.delete(:requires) }
|
|
168
166
|
end
|
|
169
167
|
|
|
168
|
+
# Returns all the location entries for a filename
|
|
169
|
+
# RSpec syntax for locations:
|
|
170
|
+
# * ./a_spec.rb - run whole file
|
|
171
|
+
# * ./a_spec.rb:18 - run specific spec
|
|
172
|
+
# * ./a_spec.rb[1:2:1] - run specific spec group
|
|
173
|
+
# You can specify locations more than once for a single file, so you need to find all relevant entries in the runner
|
|
174
|
+
def location_entries_for(filename)
|
|
175
|
+
filename = File.expand_path(filename)
|
|
176
|
+
entries = Array(parsed_options.options[:files_or_directories_to_run]).select do |entry|
|
|
177
|
+
entry_filename = File.expand_path(entry.sub(/(\[\d+(:\d+)*\])+\z/, "").sub(/(:\d+)+\z/, ""))
|
|
178
|
+
entry_filename == filename
|
|
179
|
+
end
|
|
180
|
+
entries.empty? ? filename : entries
|
|
181
|
+
end
|
|
182
|
+
|
|
170
183
|
def debug(message)
|
|
171
184
|
@debug_io.puts message if @debug_io
|
|
172
185
|
end
|
|
@@ -2,35 +2,70 @@
|
|
|
2
2
|
|
|
3
3
|
module RSpec
|
|
4
4
|
module Conductor
|
|
5
|
-
WorkerProcess
|
|
6
|
-
def self.spawn(number:, test_env_number:, on_stdout: nil, on_stderr: nil, **worker_init_args)
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
class WorkerProcess
|
|
6
|
+
def self.spawn(number:, test_env_number:, on_message:, on_stdout: nil, on_stderr: nil, **worker_init_args)
|
|
7
|
+
worker_process = new(number: number, on_message: on_message)
|
|
8
|
+
|
|
9
|
+
child_process = Util::ChildProcess.fork(on_stdout: on_stdout, on_stderr: on_stderr, on_message: proc { worker_process.handle_message }) do |_, child_socket|
|
|
9
10
|
ENV["TEST_ENV_NUMBER"] = test_env_number
|
|
10
|
-
parent_socket.close
|
|
11
11
|
Worker.new(
|
|
12
12
|
worker_number: number,
|
|
13
13
|
socket: Protocol::Socket.new(child_socket),
|
|
14
14
|
**worker_init_args
|
|
15
15
|
).run
|
|
16
16
|
end
|
|
17
|
-
child_socket.close
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
worker_process.child_process = child_process
|
|
19
|
+
worker_process
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.tick_all(worker_processes)
|
|
23
|
+
Util::ChildProcess.tick_all(worker_processes.map(&:child_process))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.wait_all(worker_processes)
|
|
27
|
+
Util::ChildProcess.wait_all(worker_processes.map(&:child_process))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
attr_reader :number
|
|
31
|
+
attr_accessor :current_spec, :child_process, :status
|
|
32
|
+
|
|
33
|
+
def initialize(number:, on_message: nil)
|
|
34
|
+
@number = number
|
|
35
|
+
@on_message = on_message
|
|
36
|
+
@status = :running
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def handle_message
|
|
40
|
+
message = receive_message
|
|
41
|
+
unless message
|
|
42
|
+
socket.close
|
|
43
|
+
return
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
@on_message&.call(self, message)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def send_message(message)
|
|
50
|
+
socket.send_message(message)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def receive_message
|
|
54
|
+
socket.receive_message
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def socket
|
|
58
|
+
@socket ||= Protocol::Socket.new(child_process.message_socket)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def pid
|
|
62
|
+
child_process.pid
|
|
27
63
|
end
|
|
28
64
|
|
|
29
65
|
def shut_down(status)
|
|
30
66
|
return unless running?
|
|
31
67
|
|
|
32
|
-
|
|
33
|
-
socket.close
|
|
68
|
+
@status = status
|
|
34
69
|
end
|
|
35
70
|
|
|
36
71
|
def running?
|
data/lib/rspec/conductor.rb
CHANGED
|
@@ -35,7 +35,7 @@ require_relative "conductor/version"
|
|
|
35
35
|
require_relative "conductor/protocol"
|
|
36
36
|
require_relative "conductor/server"
|
|
37
37
|
require_relative "conductor/worker"
|
|
38
|
-
require_relative "conductor/
|
|
38
|
+
require_relative "conductor/suite_run"
|
|
39
39
|
require_relative "conductor/worker_process"
|
|
40
40
|
require_relative "conductor/cli"
|
|
41
41
|
require_relative "conductor/rspec_subscriber"
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rspec-conductor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mark Abramov
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-08-02 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: rspec-core
|
|
@@ -48,9 +49,9 @@ files:
|
|
|
48
49
|
- lib/rspec/conductor/formatters/plain.rb
|
|
49
50
|
- lib/rspec/conductor/protocol.rb
|
|
50
51
|
- lib/rspec/conductor/railtie.rb
|
|
51
|
-
- lib/rspec/conductor/results.rb
|
|
52
52
|
- lib/rspec/conductor/rspec_subscriber.rb
|
|
53
53
|
- lib/rspec/conductor/server.rb
|
|
54
|
+
- lib/rspec/conductor/suite_run.rb
|
|
54
55
|
- lib/rspec/conductor/util/ansi.rb
|
|
55
56
|
- lib/rspec/conductor/util/child_process.rb
|
|
56
57
|
- lib/rspec/conductor/util/screen_buffer.rb
|
|
@@ -68,6 +69,7 @@ metadata:
|
|
|
68
69
|
homepage_uri: https://github.com/markiz/rspec-conductor
|
|
69
70
|
source_code_uri: https://github.com/markiz/rspec-conductor
|
|
70
71
|
changelog_uri: https://github.com/markiz/rspec-conductor/blob/master/CHANGELOG.md
|
|
72
|
+
post_install_message:
|
|
71
73
|
rdoc_options: []
|
|
72
74
|
require_paths:
|
|
73
75
|
- lib
|
|
@@ -82,7 +84,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
82
84
|
- !ruby/object:Gem::Version
|
|
83
85
|
version: '0'
|
|
84
86
|
requirements: []
|
|
85
|
-
rubygems_version:
|
|
87
|
+
rubygems_version: 3.3.26
|
|
88
|
+
signing_key:
|
|
86
89
|
specification_version: 4
|
|
87
90
|
summary: Queue-based parallel test runner for rspec
|
|
88
91
|
test_files: []
|