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 +4 -4
- data/CHANGELOG.md +7 -0
- data/lib/rspec/conductor/formatters/base.rb +1 -1
- data/lib/rspec/conductor/server.rb +0 -1
- 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 +30 -24
- metadata +6 -3
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,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
|
|
@@ -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
|
|
@@ -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
|
|
5
|
+
class WorkerProcess
|
|
8
6
|
def self.spawn(number:, test_env_number:, on_message:, on_stdout: nil, on_stderr: nil, **worker_init_args)
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
41
|
+
unless message
|
|
42
|
+
socket.close
|
|
43
|
+
return
|
|
44
|
+
end
|
|
46
45
|
|
|
47
|
-
on_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
|
-
|
|
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.
|
|
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
|
|
@@ -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: []
|