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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 17242ccb102c17f1a469bf4e105d62f2469372059d9416bcf4ab87df4f698143
4
- data.tar.gz: ab29ad25bded7e52af8b762c532f2d30d9c5451a954abfcd3330430840dda107
3
+ metadata.gz: e35dd999b2161d2767b67b1c798dba7e89595ba8a5e6376780498c57c9bd1405
4
+ data.tar.gz: 2b93eb4f86463e46ab97cf00c8fa619b4dc386e4f31febb006542ff9d3c02a49
5
5
  SHA512:
6
- metadata.gz: 7d7aa04766f0b086c747c2c7046e6dc3050c6376389c05b68db1a83a99570c65e5d0e993df8bcd4b2379acb68d6047f4ec15dfba01423714eda39f8a1cb4da89
7
- data.tar.gz: c801bac92877545bef4083eb0875efb291e58ad1af80961f7ece72fee86e2abbf3f3916c43d823eaeaefb81f605254d22e96a1ae8eddda046e6fd846fe1237de
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
  ![rspec-conductor demo](https://github.com/user-attachments/assets/2b598635-3192-4aa0-bb39-2af01b93bb4a)
14
14
 
15
- ## Usage
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
- # if you like the first-is-1 mode, keeping your parallel test envs separate from your regular env:
23
- PARALLEL_TEST_FIRST_IS_1=true rails 'parallel:drop[16]' 'parallel:setup[16]'
19
+ ```ruby
20
+ gem 'rspec-conductor'
24
21
  ```
25
22
 
26
- Then launch the CLI app (see also `bin/rspec-conductor --help`):
23
+ ## Usage
27
24
 
28
- ```
25
+ ```bash
29
26
  rspec-conductor <OPTIONS> -- <RSPEC_OPTIONS> <SPEC_PATHS>
30
- rspec-conductor --workers 10 -- --tag '~@flaky' spec_ether/system/
31
- rspec-conductor --workers 10 spec_ether/system/ # shorthand when there are no spec options is also supported
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`?
@@ -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
- application = File.expand_path("config/application", Conductor.root)
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?(application)
69
- debug "Preloading config/application.rb..."
70
- require File.expand_path("config/application", Conductor.root)
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
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RSpec
4
4
  module Conductor
5
- VERSION = "1.0.0"
5
+ VERSION = "1.0.1"
6
6
  end
7
7
  end
@@ -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
- initialize_rspec
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 != spec_path && Dir.exist?(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.grep(/spec/)}"
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 initialize_rspec
93
- rails_helper = File.expand_path("rails_helper.rb", Conductor.root)
94
- spec_helper = File.expand_path("spec_helper.rb", Conductor.root)
95
- if File.exist?(rails_helper)
96
- debug "Requiring rails_helper to boot Rails..."
97
- require rails_helper
98
- elsif File.exist?(spec_helper)
99
- debug "Requiring spec_helper..."
100
- require spec_helper
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 "Could detect neither rails_helper nor spec_helper"
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.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-21 00:00:00.000000000 Z
11
+ date: 2025-12-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec-core