rspec-conductor 1.0.9 → 1.0.10

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 57c9a96bb487176dd0c40d962ef210ca3caf316fd5135d032cccded1a9726cae
4
- data.tar.gz: 976c271c89bb8847a0e1cbcd2b80f544054fd8467e701225f1f146f14c081d2f
3
+ metadata.gz: 9de36557f5b670ac37493d920d186e4dcbcaca0c958d59914448b91aa5e6e488
4
+ data.tar.gz: 9d2371b5598a12adbc6ac907d80dfc4fbbe5f9c7d1c2b0f75cb17337345f51e4
5
5
  SHA512:
6
- metadata.gz: d4efec9193e711ff163176ef03edcd0a4566eb75f77b528d8a931d1e93c6718810c455d79c595c1c8c55fcddca00a60c40e65104439203605655ec5ae25e9026
7
- data.tar.gz: 9ed9595a493bd54fe6d51d029a5bf693f166dad3d5a6144d0b2bd34f26824173056bae85b0a78ee04e12f4883640b007700e5d3a45e0e7946450c69c21b6c1d4
6
+ metadata.gz: cb67874bb0e5f84176ac601588d7931c97d70e3f8f6caff111e7cc1607b15007fcbc17f0053ff2c92f8e438cdad343530c91667de4d99f4fc7b95ac504f868fd
7
+ data.tar.gz: 4cb10a52cf0a0a6f9948c36287f2b6c5d35183df0ce6370196f9342e67c29ce5332780d8da8172d585be264170958fcbb317d77f03f3c23cb4ff70394b6e4cbe
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## [1.0.10] - 2026-03-08
2
+
3
+ - Add --print-slowest cli param to display the slowest specs in the suite
4
+
1
5
  ## [1.0.9] - 2026-03-01
2
6
 
3
7
  - 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
@@ -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, _results)
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(results, seed:, success:)
20
+ def print_summary(suite_run, seed:, success:)
21
21
  puts "\n\n"
22
22
  puts "Randomized with seed #{seed}"
23
- puts "#{colorize("#{results.examples_passed} passed", :green)}, #{colorize("#{results.examples_failed} failed", :red)}, #{colorize("#{results.examples_pending} pending", :yellow)}"
24
- puts colorize("Worker crashes: #{results.worker_crashes}", :red) if results.worker_crashes.positive?
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 results.errors.any?
26
+ if suite_run.errors.any?
27
27
  puts "\nFailures:\n\n"
28
- results.errors.each_with_index do |error, i|
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: #{results.specs_runtime.round(2)}s"
41
- puts "Total runtime: #{results.total_runtime.round(2)}s"
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 results.errors.any?
44
+ if suite_run.errors.any?
45
45
  puts ""
46
46
  puts "To rerun failed examples:"
47
- puts " rspec #{results.errors.map { |e| e[:location] }.join(" ")}"
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
 
@@ -14,22 +14,22 @@ module RSpec
14
14
  super(**kwargs)
15
15
  end
16
16
 
17
- def handle_worker_message(_worker_process, message, results)
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(results) if @last_printout + @printout_interval < Time.now
19
+ print_status(suite_run) if @last_printout + @printout_interval < Time.now
20
20
  end
21
21
 
22
- def print_status(results)
22
+ def print_status(suite_run)
23
23
  @last_printout = Time.now
24
- pct = results.spec_file_processed_percentage
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: #{results.spec_files_processed} / #{results.spec_files_total} (#{(pct * 100).floor}%)"
29
- puts "#{results.examples_passed} passed, #{results.examples_failed} failed, #{results.examples_pending} pending"
30
- if results.errors.any?
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
- results.errors.each_with_index do |error, i|
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, results)
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, results)
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, results)
69
+ def redraw(worker_process, suite_run)
70
70
  update_worker_status_line(worker_process)
71
- update_results_line(results)
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 update_results_line(results)
98
- pct = results.spec_file_processed_percentage
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, results.spec_files_processed, results.spec_files_total]
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, _results)
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,9 +1,5 @@
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
@@ -21,6 +17,7 @@ module RSpec
21
17
  # @option formatter [String] Use a certain formatter
22
18
  # @option verbose [Boolean] Use especially verbose output
23
19
  # @option display_retry_backtraces [Boolean] Display backtraces for specs retried via rspec-retry
20
+ # @option print_slowest_count [Integer] Print slowest specs in the end of the suite
24
21
  def initialize(worker_count:, rspec_args:, **opts)
25
22
  @worker_count = worker_count
26
23
  @worker_number_offset = opts.fetch(:worker_number_offset, 0)
@@ -30,11 +27,11 @@ module RSpec
30
27
  @seed = opts[:seed] || (Random.new_seed % MAX_SEED)
31
28
  @fail_fast_after = opts[:fail_fast_after]
32
29
  @display_retry_backtraces = opts.fetch(:display_retry_backtraces, false)
30
+ @print_slowest_count = opts.fetch(:print_slowest_count, nil)
33
31
  @verbose = opts.fetch(:verbose, false)
34
32
 
35
33
  @rspec_args = rspec_args
36
34
  @worker_processes = []
37
- @dead_worker_processes = []
38
35
  @spec_queue = []
39
36
  @formatter_class = case opts[:formatter]
40
37
  when "ci"
@@ -47,7 +44,7 @@ module RSpec
47
44
  (!@verbose && Formatters::Fancy.recommended?) ? Formatters::Fancy : Formatters::Plain
48
45
  end
49
46
  @formatter = @formatter_class.new(worker_count: @worker_count)
50
- @results = Results.new
47
+ @suite_run = SuiteRun.new
51
48
 
52
49
  $stdout.sync = true
53
50
  $stdin.echo = false if $stdin.tty?
@@ -65,9 +62,10 @@ module RSpec
65
62
  start_workers
66
63
  run_event_loop
67
64
  wait_for_workers_to_exit
68
- @results.suite_complete
65
+ @suite_run.suite_complete
69
66
 
70
- @formatter.print_summary(@results, seed: @seed, success: success?)
67
+ @formatter.print_summary(@suite_run, seed: @seed, success: success?)
68
+ @formatter.print_slowest(@suite_run, @print_slowest_count) if @print_slowest_count
71
69
  exit_with_status
72
70
  end
73
71
 
@@ -95,7 +93,7 @@ module RSpec
95
93
  debug "RSpec config: #{config.inspect}"
96
94
  debug "Files to run: #{config.files_to_run}"
97
95
  @spec_queue = config.files_to_run.shuffle(random: Random.new(@seed))
98
- @results.spec_files_total = @spec_queue.size
96
+ @suite_run.spec_files_total = @spec_queue.size
99
97
  end
100
98
 
101
99
  def preload_application
@@ -127,21 +125,18 @@ module RSpec
127
125
  @shutdown_status = :shutdown_messages_sent
128
126
  @formatter.print_shutdown_banner
129
127
  @worker_processes.select(&:running?).each do |worker_process|
130
- worker_process.socket.send_message({ type: :shutdown })
128
+ worker_process.send_message({ type: :shutdown })
131
129
  cleanup_worker_process(worker_process)
132
130
  end
133
131
  end
134
132
 
135
- worker_processes_by_io = @worker_processes.select(&:running?).to_h { |w| [w.socket.io, w] }
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))
133
+ WorkerProcess.tick_all(@worker_processes)
139
134
  reap_workers
140
135
  end
141
136
  end
142
137
 
143
138
  def wait_for_workers_to_exit
144
- Util::ChildProcess.wait_all(@worker_processes.map(&:child_process))
139
+ WorkerProcess.wait_all(@worker_processes)
145
140
  end
146
141
 
147
142
  def spawn_worker(worker_number)
@@ -150,6 +145,7 @@ module RSpec
150
145
  worker_process = WorkerProcess.spawn(
151
146
  number: worker_number,
152
147
  test_env_number: (@first_is_1 || worker_number != 1) ? worker_number.to_s : "",
148
+ on_message: ->(worker_process, message) { handle_worker_message(worker_process, message) },
153
149
  on_stdout: ->(string) { @formatter.handle_worker_stdout(worker_number, string) },
154
150
  on_stderr: ->(string) { @formatter.handle_worker_stderr(worker_number, string) },
155
151
  debug_io: @verbose ? $stderr : nil,
@@ -160,37 +156,34 @@ module RSpec
160
156
  worker_process
161
157
  end
162
158
 
163
- def handle_worker_message(worker_process)
164
- message = worker_process.socket.receive_message
165
- return unless message
166
-
159
+ def handle_worker_message(worker_process, message)
167
160
  debug "Worker #{worker_process.number}: #{message[:type]}"
168
161
 
169
162
  case message[:type].to_sym
170
163
  when :example_passed
171
- @results.example_passed
164
+ @suite_run.example_passed(message)
172
165
  when :example_failed
173
- @results.example_failed(message)
166
+ @suite_run.example_failed(message)
174
167
 
175
- if @fail_fast_after && @results.examples_failed >= @fail_fast_after
176
- debug "Shutting down after #{@results.examples_failed} failures"
168
+ if @fail_fast_after && @suite_run.examples_failed >= @fail_fast_after
169
+ debug "Shutting down after #{@suite_run.examples_failed} failures"
177
170
  initiate_shutdown
178
171
  end
179
172
  when :example_pending
180
- @results.example_pending
173
+ @suite_run.example_pending
181
174
  when :example_retried
182
175
  @formatter.print_retry_message(message) if @display_retry_backtraces
183
176
  when :spec_complete
184
- @results.spec_file_complete
177
+ @suite_run.spec_file_complete
185
178
  worker_process.current_spec = nil
186
179
  assign_work(worker_process)
187
180
  when :spec_error
188
- @results.spec_file_error(message)
181
+ @suite_run.spec_file_error(message)
189
182
  debug "Spec error details: #{message[:error]}"
190
183
  worker_process.current_spec = nil
191
184
  assign_work(worker_process)
192
185
  end
193
- @formatter.handle_worker_message(worker_process, message, @results)
186
+ @formatter.handle_worker_message(worker_process, message, @suite_run)
194
187
  end
195
188
 
196
189
  def assign_work(worker_process)
@@ -198,21 +191,21 @@ module RSpec
198
191
 
199
192
  if shutting_down? || !spec_file
200
193
  debug "No more work for worker #{worker_process.number}, sending shutdown"
201
- worker_process.socket.send_message({ type: :shutdown })
194
+ worker_process.send_message({ type: :shutdown })
202
195
  cleanup_worker_process(worker_process)
203
196
  else
204
- @results.spec_file_assigned
197
+ @suite_run.spec_file_assigned
205
198
  worker_process.current_spec = spec_file
206
199
  debug "Assigning #{spec_file} to worker #{worker_process.number}"
207
200
  message = { type: :worker_assigned_spec, file: spec_file }
208
- worker_process.socket.send_message(message)
209
- @formatter.handle_worker_message(worker_process, message, @results)
201
+ worker_process.send_message(message)
202
+ @formatter.handle_worker_message(worker_process, message, @suite_run)
210
203
  end
211
204
  end
212
205
 
213
206
  def cleanup_worker_process(worker_process, status: :shut_down)
214
207
  worker_process.shut_down(status)
215
- @formatter.handle_worker_message(worker_process, { type: :worker_shut_down }, @results)
208
+ @formatter.handle_worker_message(worker_process, { type: :worker_shut_down }, @suite_run)
216
209
  end
217
210
 
218
211
  def reap_workers
@@ -224,7 +217,7 @@ module RSpec
224
217
 
225
218
  dead_worker_processes.each do |worker_process, exitstatus|
226
219
  cleanup_worker_process(worker_process, status: :terminated)
227
- @results.worker_crashed
220
+ @suite_run.worker_crashed
228
221
  debug "Worker #{worker_process.number} exited with status #{exitstatus.exitstatus}, signal #{exitstatus.termsig}"
229
222
  end
230
223
  end
@@ -243,7 +236,7 @@ module RSpec
243
236
  end
244
237
 
245
238
  def success?
246
- @results.success? && !shutting_down?
239
+ @suite_run.success? && !shutting_down?
247
240
  end
248
241
 
249
242
  def exit_with_status
@@ -2,8 +2,8 @@
2
2
 
3
3
  module RSpec
4
4
  module Conductor
5
- class Results
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RSpec
4
4
  module Conductor
5
- VERSION = "1.0.9"
5
+ VERSION = "1.0.10"
6
6
  end
7
7
  end
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "socket"
4
+
3
5
  module RSpec
4
6
  module Conductor
5
- WorkerProcess = Struct.new(:pid, :child_process, :number, :status, :socket, :current_spec, keyword_init: true) do
6
- def self.spawn(number:, test_env_number:, on_stdout: nil, on_stderr: nil, **worker_init_args)
7
+ WorkerProcess = Struct.new(:pid, :child_process, :number, :on_message, :status, :socket, :current_spec, keyword_init: true) do
8
+ def self.spawn(number:, test_env_number:, on_message:, on_stdout: nil, on_stderr: nil, **worker_init_args)
7
9
  parent_socket, child_socket = Socket.pair(:UNIX, :STREAM, 0)
8
10
  child_process = Util::ChildProcess.fork(on_stdout: on_stdout, on_stderr: on_stderr) do
9
11
  ENV["TEST_ENV_NUMBER"] = test_env_number
@@ -19,6 +21,7 @@ module RSpec
19
21
  new(
20
22
  pid: child_process.pid,
21
23
  child_process: child_process,
24
+ on_message: on_message,
22
25
  number: number,
23
26
  status: :running,
24
27
  socket: Protocol::Socket.new(parent_socket),
@@ -26,6 +29,32 @@ module RSpec
26
29
  )
27
30
  end
28
31
 
32
+ def self.tick_all(worker_processes)
33
+ worker_processes_by_io = worker_processes.select(&:running?).to_h { |w| [w.socket.io, w] }
34
+ readable_ios, = IO.select(worker_processes_by_io.keys, nil, nil, 0)
35
+ readable_ios&.each { |io| worker_processes_by_io.fetch(io).handle_message }
36
+ Util::ChildProcess.tick_all(worker_processes.map(&:child_process))
37
+ end
38
+
39
+ def self.wait_all(worker_processes)
40
+ Util::ChildProcess.wait_all(worker_processes.map(&:child_process))
41
+ end
42
+
43
+ def handle_message
44
+ message = receive_message
45
+ return unless message && on_message
46
+
47
+ on_message.call(self, message)
48
+ end
49
+
50
+ def send_message(message)
51
+ socket.send_message(message)
52
+ end
53
+
54
+ def receive_message
55
+ socket.receive_message
56
+ end
57
+
29
58
  def shut_down(status)
30
59
  return unless running?
31
60
 
@@ -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/results"
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,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-conductor
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.9
4
+ version: 1.0.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark Abramov
@@ -48,9 +48,9 @@ files:
48
48
  - lib/rspec/conductor/formatters/plain.rb
49
49
  - lib/rspec/conductor/protocol.rb
50
50
  - lib/rspec/conductor/railtie.rb
51
- - lib/rspec/conductor/results.rb
52
51
  - lib/rspec/conductor/rspec_subscriber.rb
53
52
  - lib/rspec/conductor/server.rb
53
+ - lib/rspec/conductor/suite_run.rb
54
54
  - lib/rspec/conductor/util/ansi.rb
55
55
  - lib/rspec/conductor/util/child_process.rb
56
56
  - lib/rspec/conductor/util/screen_buffer.rb