rspec-conductor 1.0.10 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9de36557f5b670ac37493d920d186e4dcbcaca0c958d59914448b91aa5e6e488
4
- data.tar.gz: 9d2371b5598a12adbc6ac907d80dfc4fbbe5f9c7d1c2b0f75cb17337345f51e4
3
+ metadata.gz: 4a4f284fb6887972bbd325f6a5d6ce77b3c384d3cc7b8933c19bd04af2aa3ada
4
+ data.tar.gz: 733f1d8de0935949b77aa54374f37f97976a007c7dc53381b1dc8d8bb3e6922d
5
5
  SHA512:
6
- metadata.gz: cb67874bb0e5f84176ac601588d7931c97d70e3f8f6caff111e7cc1607b15007fcbc17f0053ff2c92f8e438cdad343530c91667de4d99f4fc7b95ac504f868fd
7
- data.tar.gz: 4cb10a52cf0a0a6f9948c36287f2b6c5d35183df0ce6370196f9342e67c29ce5332780d8da8172d585be264170958fcbb317d77f03f3c23cb4ff70394b6e4cbe
6
+ metadata.gz: e23c66e05afe4064b4a6062d6f7880429e641c5e2a3eac73f60a50ba5bd4c75ecb2b36a8484405200e9e8102f0cfd24b20fb99a0fbdf959be376e0087cf4006d
7
+ data.tar.gz: ebc3ad89f178da346ce4a5aee153e280b8b10b98f2c1d4e55894035237ee9f990e34206f272f35128d0e94da7c01847acd15225ab7a8db0516ceb674618f1887
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
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
+
1
8
  ## [1.0.10] - 2026-03-08
2
9
 
3
10
  - Add --print-slowest cli param to display the slowest specs in the suite
@@ -74,7 +74,7 @@ module RSpec
74
74
  #{message[:location]}
75
75
  #{message[:exception_class]}: #{message[:message]}
76
76
  Backtrace:
77
- #{message[:backtrace].map { " #{_1}" }.join("\n")}
77
+ #{message[:backtrace].map { |line| " #{line}" }.join("\n")}
78
78
  EOM
79
79
  end
80
80
 
@@ -4,7 +4,6 @@ module RSpec
4
4
  module Conductor
5
5
  class Server
6
6
  MAX_SEED = 2**16
7
- WORKER_POLL_INTERVAL = 0.01
8
7
 
9
8
  # @option worker_count [Integer] How many workers to spin
10
9
  # @option rspec_args [Array<String>] A list of rspec options
@@ -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.pipes.reject(&:closed?).each { |pipe| memo[pipe] = 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 { |pipe| processes_by_io[pipe].read_available(pipe) }
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 read_available(pipe)
101
+ def handle_available(io)
95
102
  return if done?
96
- return if pipe.closed?
97
-
98
- buffer, callback = if pipe == @stdout_pipe
99
- [@stdout_buffer, @on_stdout]
100
- elsif pipe == @stderr_pipe
101
- [@stderr_buffer, @on_stderr]
102
- else
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
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RSpec
4
4
  module Conductor
5
- VERSION = "1.0.10"
5
+ VERSION = "1.0.11"
6
6
  end
7
7
  end
@@ -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
@@ -1,38 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "socket"
4
-
5
3
  module RSpec
6
4
  module Conductor
7
- WorkerProcess = Struct.new(:pid, :child_process, :number, :on_message, :status, :socket, :current_spec, keyword_init: true) do
5
+ class WorkerProcess
8
6
  def self.spawn(number:, test_env_number:, on_message:, on_stdout: nil, on_stderr: nil, **worker_init_args)
9
- parent_socket, child_socket = Socket.pair(:UNIX, :STREAM, 0)
10
- child_process = Util::ChildProcess.fork(on_stdout: on_stdout, on_stderr: on_stderr) do
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|
11
10
  ENV["TEST_ENV_NUMBER"] = test_env_number
12
- parent_socket.close
13
11
  Worker.new(
14
12
  worker_number: number,
15
13
  socket: Protocol::Socket.new(child_socket),
16
14
  **worker_init_args
17
15
  ).run
18
16
  end
19
- child_socket.close
20
-
21
- new(
22
- pid: child_process.pid,
23
- child_process: child_process,
24
- on_message: on_message,
25
- number: number,
26
- status: :running,
27
- socket: Protocol::Socket.new(parent_socket),
28
- current_spec: nil
29
- )
17
+
18
+ worker_process.child_process = child_process
19
+ worker_process
30
20
  end
31
21
 
32
22
  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
23
  Util::ChildProcess.tick_all(worker_processes.map(&:child_process))
37
24
  end
38
25
 
@@ -40,11 +27,23 @@ module RSpec
40
27
  Util::ChildProcess.wait_all(worker_processes.map(&:child_process))
41
28
  end
42
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
+
43
39
  def handle_message
44
40
  message = receive_message
45
- return unless message && on_message
41
+ unless message
42
+ socket.close
43
+ return
44
+ end
46
45
 
47
- on_message.call(self, message)
46
+ @on_message&.call(self, message)
48
47
  end
49
48
 
50
49
  def send_message(message)
@@ -55,11 +54,18 @@ module RSpec
55
54
  socket.receive_message
56
55
  end
57
56
 
57
+ def socket
58
+ @socket ||= Protocol::Socket.new(child_process.message_socket)
59
+ end
60
+
61
+ def pid
62
+ child_process.pid
63
+ end
64
+
58
65
  def shut_down(status)
59
66
  return unless running?
60
67
 
61
- self.status = status
62
- socket.close
68
+ @status = status
63
69
  end
64
70
 
65
71
  def running?
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.10
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: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-08-02 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: rspec-core
@@ -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: 4.0.6
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: []