rspec-conductor 1.0.0 → 1.0.1
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 +6 -0
- data/README.md +27 -11
- data/lib/rspec/conductor/cli.rb +19 -2
- data/lib/rspec/conductor/server.rb +18 -6
- data/lib/rspec/conductor/version.rb +1 -1
- data/lib/rspec/conductor/worker.rb +31 -21
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e35dd999b2161d2767b67b1c798dba7e89595ba8a5e6376780498c57c9bd1405
|
|
4
|
+
data.tar.gz: 2b93eb4f86463e46ab97cf00c8fa619b4dc386e4f31febb006542ff9d3c02a49
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ef4fc413c0198396cba25765275c27dfd57968d8abc2286989817edfdc94b1b8e63370c8b12cd5dd747b8684e386042f61ecc8e9770a9bb780ac9ef8043e875c
|
|
7
|
+
data.tar.gz: 00e4eb9136dd92fa375dd441f056d759e847c48fb0d422f39acb458cfa60d1569caf662af94e95e059dca2b3cfd6b23fc660df66ee3d72241b4457da8c65bf35
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [1.0.1] - 2025-12-21
|
|
2
|
+
|
|
3
|
+
- Fix spec_helper/rails_helper path finding [Thanks @diego-aslz]
|
|
4
|
+
- Add --prefork-require and --no-prefork-require CLI options for non-rails apps or rails setups where loading config/application.rb is not entirely safe
|
|
5
|
+
- Add --postfork-require and --no-postfork-require CLI options for flexibility
|
|
6
|
+
|
|
1
7
|
## [1.0.0] - 2025-12-21
|
|
2
8
|
|
|
3
9
|
- Initial release
|
data/README.md
CHANGED
|
@@ -12,27 +12,36 @@ User experience was designed to serve as a simple, almost drop-in, replacement f
|
|
|
12
12
|
|
|
13
13
|

|
|
14
14
|
|
|
15
|
-
##
|
|
16
|
-
|
|
17
|
-
Set up the databases:
|
|
15
|
+
## Installation
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
rails 'parallel:drop[10]' 'parallel:setup[10]'
|
|
17
|
+
Add to your Gemfile:
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
```ruby
|
|
20
|
+
gem 'rspec-conductor'
|
|
24
21
|
```
|
|
25
22
|
|
|
26
|
-
|
|
23
|
+
## Usage
|
|
27
24
|
|
|
28
|
-
```
|
|
25
|
+
```bash
|
|
29
26
|
rspec-conductor <OPTIONS> -- <RSPEC_OPTIONS> <SPEC_PATHS>
|
|
30
|
-
rspec-conductor --workers 10 -- --tag '~@flaky'
|
|
31
|
-
|
|
27
|
+
rspec-conductor --workers 10 -- --tag '~@flaky' spec
|
|
28
|
+
# shorthand for setting the paths when there are no rspec options is also supported
|
|
29
|
+
rspec-conductor --workers 10 spec
|
|
32
30
|
```
|
|
33
31
|
|
|
34
32
|
`--verbose` flag is especially useful for troubleshooting.
|
|
35
33
|
|
|
34
|
+
To set up the databases (if you are using this with rails) you can use a rake task from the parallel_tests gem
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
rails 'parallel:drop[10]' 'parallel:setup[10]'
|
|
38
|
+
|
|
39
|
+
# if you like the first-is-1 mode, keeping your parallel test envs separate from your regular env:
|
|
40
|
+
PARALLEL_TEST_FIRST_IS_1=true rails 'parallel:drop[10]' 'parallel:setup[10]'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
I might consider porting that rake task in the near future.
|
|
44
|
+
|
|
36
45
|
## Mechanics
|
|
37
46
|
|
|
38
47
|
Server process preloads the `rails_helper`, prepares a list of files to work, then spawns the workers, each with `ENV['TEST_ENV_NUMBER'] = <worker_number>` (same as parallel-tests). The two communicate over a standard unix socket. Message format is basically a tuple of `(size, json_payload)`. It should also be possible to run this process over the network, but I haven't found a solid usecase for this.
|
|
@@ -41,6 +50,13 @@ Server process preloads the `rails_helper`, prepares a list of files to work, th
|
|
|
41
50
|
|
|
42
51
|
* In order to make the CLI executable load and run fast, do not add any dependencies. That includes `active_support`.
|
|
43
52
|
|
|
53
|
+
## Troubleshooting
|
|
54
|
+
|
|
55
|
+
* `+[__NSCFConstantString initialize] may have been in progress in another thread when fork() was called.` on M-based Mac machines
|
|
56
|
+
* This is a common issue with ruby code, compiled libraries and forking. Set `OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES` environment variable to work around this
|
|
57
|
+
* Something gets loaded that shouldn't get loaded, or in a different order
|
|
58
|
+
* There are two simple ways to hook into preloads, exposed as CLI flags, `--prefork-require` (defaults to `config/application.rb`) and `--postfork-require` (defaults to either `rails_helper.rb` or `spec_helper.rb`, whichever is present on your machine). You can set any of those to whatever you need and control the load order
|
|
59
|
+
|
|
44
60
|
## FAQ
|
|
45
61
|
|
|
46
62
|
* Why not preload the whole rails environment before spawning the workers instead of just `rails_helper`?
|
data/lib/rspec/conductor/cli.rb
CHANGED
|
@@ -13,6 +13,8 @@ module RSpec
|
|
|
13
13
|
fail_fast_after: nil,
|
|
14
14
|
verbose: false,
|
|
15
15
|
display_retry_backtraces: false,
|
|
16
|
+
prefork_require: 'config/application.rb',
|
|
17
|
+
postfork_require: :spec_helper,
|
|
16
18
|
}.freeze
|
|
17
19
|
|
|
18
20
|
def self.run(argv)
|
|
@@ -60,6 +62,22 @@ module RSpec
|
|
|
60
62
|
@conductor_options[:offset] = n
|
|
61
63
|
end
|
|
62
64
|
|
|
65
|
+
opts.on("--prefork-require FILENAME", String, "Require this file before forking (default: config/application.rb)") do |f|
|
|
66
|
+
@conductor_options[:prefork_require] = f
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
opts.on("--no-prefork-require", "Do not preload config/application.rb") do
|
|
70
|
+
@conductor_options[:prefork_require] = nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
opts.on("--postfork-require FILENAME", String, "Require this file after forking (default: either rails_helper.rb or spec_helper.rb, whichever is present)") do |f|
|
|
74
|
+
@conductor_options[:postfork_require] = f
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
opts.on("--no-postfork-require", "Do not load anything post-fork") do
|
|
78
|
+
@conductor_options[:postfork_require] = nil
|
|
79
|
+
end
|
|
80
|
+
|
|
63
81
|
opts.on("--first-is-1", 'ENV["TEST_ENV_NUMBER"] for the worker 1 is "1" rather than ""') do
|
|
64
82
|
@conductor_options[:first_is_1] = true
|
|
65
83
|
end
|
|
@@ -97,11 +115,10 @@ module RSpec
|
|
|
97
115
|
end
|
|
98
116
|
|
|
99
117
|
def start_server
|
|
100
|
-
require_relative "server"
|
|
101
|
-
|
|
102
118
|
Server.new(
|
|
103
119
|
worker_count: @conductor_options[:workers],
|
|
104
120
|
worker_number_offset: @conductor_options[:offset],
|
|
121
|
+
prefork_require: @conductor_options[:prefork_require],
|
|
105
122
|
first_is_1: @conductor_options[:first_is_1],
|
|
106
123
|
seed: @conductor_options[:seed],
|
|
107
124
|
fail_fast_after: @conductor_options[:fail_fast_after],
|
|
@@ -11,6 +11,8 @@ module RSpec
|
|
|
11
11
|
# @option worker_count [Integer] How many workers to spin
|
|
12
12
|
# @option rspec_args [Array<String>] A list of rspec options
|
|
13
13
|
# @option worker_number_offset [Integer] Start worker numbering with an offset
|
|
14
|
+
# @option prefork_require [String] File required prior to forking
|
|
15
|
+
# @option postfork_require [String, Symbol] File required after forking
|
|
14
16
|
# @option first_is_1 [Boolean] TEST_ENV_NUMBER for the first worker is "1" instead of ""
|
|
15
17
|
# @option seed [Integer] Set a predefined starting seed
|
|
16
18
|
# @option fail_fast_after [Integer, NilClass] Shut down the workers after a certain number of failures
|
|
@@ -20,6 +22,8 @@ module RSpec
|
|
|
20
22
|
def initialize(worker_count:, rspec_args:, **opts)
|
|
21
23
|
@worker_count = worker_count
|
|
22
24
|
@worker_number_offset = opts.fetch(:worker_number_offset, 0)
|
|
25
|
+
@prefork_require = opts.fetch(:prefork_require, nil)
|
|
26
|
+
@postfork_require = opts.fetch(:postfork_require, nil)
|
|
23
27
|
@first_is_one = opts.fetch(:first_is_1, false)
|
|
24
28
|
@seed = opts[:seed] || (Random.new_seed % 65_536)
|
|
25
29
|
@fail_fast_after = opts[:fail_fast_after]
|
|
@@ -39,7 +43,7 @@ module RSpec
|
|
|
39
43
|
when "plain"
|
|
40
44
|
Formatters::Plain.new
|
|
41
45
|
else
|
|
42
|
-
Formatters::Fancy.recommended? ? Formatters::Fancy.new : Formatters::Plain.new
|
|
46
|
+
(!@verbose && Formatters::Fancy.recommended?) ? Formatters::Fancy.new : Formatters::Plain.new
|
|
43
47
|
end
|
|
44
48
|
@results = { passed: 0, failed: 0, pending: 0, errors: [], worker_crashes: 0, started_at: @started_at, spec_files_total: 0, spec_files_processed: 0 }
|
|
45
49
|
end
|
|
@@ -63,11 +67,18 @@ module RSpec
|
|
|
63
67
|
private
|
|
64
68
|
|
|
65
69
|
def preload_application
|
|
66
|
-
|
|
70
|
+
if !@prefork_require
|
|
71
|
+
debug "Prefork require not set, skipping..."
|
|
72
|
+
return
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
preload = File.expand_path(@prefork_require, Conductor.root)
|
|
67
76
|
|
|
68
|
-
if File.exist?(
|
|
69
|
-
debug "Preloading
|
|
70
|
-
require
|
|
77
|
+
if File.exist?(preload)
|
|
78
|
+
debug "Preloading #{@prefork_require}..."
|
|
79
|
+
require preload
|
|
80
|
+
else
|
|
81
|
+
debug "#{@prefork_require} not found, skipping..."
|
|
71
82
|
end
|
|
72
83
|
|
|
73
84
|
debug "Application preloaded, autoload paths configured"
|
|
@@ -133,7 +144,8 @@ module RSpec
|
|
|
133
144
|
worker_number: worker_number,
|
|
134
145
|
socket: Protocol::Socket.new(child_socket),
|
|
135
146
|
rspec_args: @rspec_args,
|
|
136
|
-
verbose: @verbose
|
|
147
|
+
verbose: @verbose,
|
|
148
|
+
postfork_require: @postfork_require,
|
|
137
149
|
).run
|
|
138
150
|
end
|
|
139
151
|
|
|
@@ -17,11 +17,13 @@ end
|
|
|
17
17
|
module RSpec
|
|
18
18
|
module Conductor
|
|
19
19
|
class Worker
|
|
20
|
-
def initialize(worker_number:, socket:, rspec_args: [], verbose: false)
|
|
20
|
+
def initialize(worker_number:, socket:, rspec_args: [], verbose: false, postfork_require: :spec_helper)
|
|
21
21
|
@worker_number = worker_number
|
|
22
22
|
@socket = socket
|
|
23
23
|
@rspec_args = rspec_args
|
|
24
24
|
@verbose = verbose
|
|
25
|
+
@postfork_require = postfork_require
|
|
26
|
+
|
|
25
27
|
@message_queue = []
|
|
26
28
|
end
|
|
27
29
|
|
|
@@ -29,7 +31,7 @@ module RSpec
|
|
|
29
31
|
suppress_output unless @verbose
|
|
30
32
|
debug "Worker #{@worker_number} starting"
|
|
31
33
|
setup_load_path
|
|
32
|
-
|
|
34
|
+
require_postfork_preloads
|
|
33
35
|
|
|
34
36
|
loop do
|
|
35
37
|
debug "Waiting for message"
|
|
@@ -69,18 +71,14 @@ module RSpec
|
|
|
69
71
|
|
|
70
72
|
def setup_load_path
|
|
71
73
|
parsed_options.configure(RSpec.configuration)
|
|
72
|
-
default_path = RSpec.configuration.default_path || "spec"
|
|
73
|
-
|
|
74
|
-
spec_path = File.expand_path("spec", Conductor.root)
|
|
75
|
-
default_full_path = File.expand_path(default_path, Conductor.root)
|
|
76
|
-
|
|
77
|
-
$LOAD_PATH.unshift(spec_path) if Dir.exist?(spec_path) && !$LOAD_PATH.include?(spec_path)
|
|
74
|
+
@default_path = RSpec.configuration.default_path || "spec"
|
|
75
|
+
@default_full_path = File.expand_path(@default_path, Conductor.root)
|
|
78
76
|
|
|
79
|
-
if default_full_path
|
|
80
|
-
$LOAD_PATH.unshift(default_full_path)
|
|
77
|
+
if Dir.exist?(@default_full_path) && !$LOAD_PATH.include?(@default_full_path)
|
|
78
|
+
$LOAD_PATH.unshift(@default_full_path)
|
|
81
79
|
end
|
|
82
80
|
|
|
83
|
-
debug "Load path (spec dirs): #{$LOAD_PATH.
|
|
81
|
+
debug "Load path (spec dirs): #{$LOAD_PATH.inspect}"
|
|
84
82
|
end
|
|
85
83
|
|
|
86
84
|
def suppress_output
|
|
@@ -89,17 +87,29 @@ module RSpec
|
|
|
89
87
|
$stdin.reopen(null_io_in)
|
|
90
88
|
end
|
|
91
89
|
|
|
92
|
-
def
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
90
|
+
def require_postfork_preloads
|
|
91
|
+
if @postfork_require == :spec_helper
|
|
92
|
+
rails_helper = File.expand_path("rails_helper.rb", @default_full_path)
|
|
93
|
+
spec_helper = File.expand_path("spec_helper.rb", @default_full_path)
|
|
94
|
+
if File.exist?(rails_helper)
|
|
95
|
+
debug "Requiring rails_helper to boot Rails..."
|
|
96
|
+
require rails_helper
|
|
97
|
+
elsif File.exist?(spec_helper)
|
|
98
|
+
debug "Requiring spec_helper..."
|
|
99
|
+
require spec_helper
|
|
100
|
+
else
|
|
101
|
+
debug "Neither rails_helper, nor spec_helper found, skipping..."
|
|
102
|
+
end
|
|
103
|
+
elsif @postfork_require
|
|
104
|
+
required_file = File.expand_path(@postfork_require, @default_full_path)
|
|
105
|
+
if File.exist?(required_file)
|
|
106
|
+
debug "Requiring #{required_file}..."
|
|
107
|
+
require required_file
|
|
108
|
+
else
|
|
109
|
+
debug "#{required_file} not found, skipping..."
|
|
110
|
+
end
|
|
101
111
|
else
|
|
102
|
-
debug "
|
|
112
|
+
debug "Skipping postfork require..."
|
|
103
113
|
end
|
|
104
114
|
|
|
105
115
|
debug "RSpec initialized, running before(:suite) hooks"
|
metadata
CHANGED
|
@@ -1,14 +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.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mark Abramov
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-12-
|
|
11
|
+
date: 2025-12-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rspec-core
|