rspec-multiprocess_runner 0.1.0
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.
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +111 -0
- data/Rakefile +6 -0
- data/TODO.md +11 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/exe/multirspec +16 -0
- data/lib/rspec/multiprocess_runner/command_line_options.rb +98 -0
- data/lib/rspec/multiprocess_runner/coordinator.rb +197 -0
- data/lib/rspec/multiprocess_runner/rake_task.rb +114 -0
- data/lib/rspec/multiprocess_runner/reporting_formatter.rb +77 -0
- data/lib/rspec/multiprocess_runner/version.rb +5 -0
- data/lib/rspec/multiprocess_runner/worker.rb +262 -0
- data/lib/rspec/multiprocess_runner.rb +6 -0
- data/rspec-multiprocess_runner.gemspec +26 -0
- metadata +143 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Collaborative Drug Discovery
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
# Rspec::MultiprocessRunner
|
2
|
+
|
3
|
+
This gem provides a mechanism for running a suite of RSpec tests in multiple
|
4
|
+
processes on the same machine, potentially allowing substantial performance
|
5
|
+
improvements.
|
6
|
+
|
7
|
+
It differs from `parallel-tests` in that it uses a coordinator process to manage
|
8
|
+
the workers, hand off work to them, and receive results. This means it can
|
9
|
+
dynamically balance the workload among the processors. It also means it can
|
10
|
+
provide consolidated results in the console.
|
11
|
+
|
12
|
+
It does follow parallel-tests' `TEST_ENV_NUMBER` convention so it's easy to
|
13
|
+
switch.
|
14
|
+
|
15
|
+
## Benefits
|
16
|
+
|
17
|
+
* Running slow (IO-bound) specs in parallel can greatly reduce the wall-clock
|
18
|
+
time needed to run a suite. Even CPU-bound specs can be aided
|
19
|
+
* Provides detailed logging of each example as it completes, including the
|
20
|
+
failure message (you don't have to wait until the end to see the failure
|
21
|
+
reason).
|
22
|
+
* Detects, kills, and reports spec files that take longer than expected (five
|
23
|
+
minutes by default).
|
24
|
+
* Detects and reports spec files that crash (without interrupting the
|
25
|
+
remainder of the suite).
|
26
|
+
|
27
|
+
## Limitations
|
28
|
+
|
29
|
+
* Only works with RSpec 2. Does not work with RSpec 3.
|
30
|
+
* Does not work on Windows or JRuby. Since it relies on `fork(2)`, it probably
|
31
|
+
never will.
|
32
|
+
* Does not support RSpec custom formatters.
|
33
|
+
* The built-in output format is very verbose — it's intended for CI servers,
|
34
|
+
where more logging is better.
|
35
|
+
* Intermediate-quality code. Happy path works, and workers are
|
36
|
+
managed/restarted, but:
|
37
|
+
* There's no test coverage of the runner itself, only auxiliaries.
|
38
|
+
* Does not handle the coordinator process dying (e.g., from `^C`).
|
39
|
+
|
40
|
+
## Installation
|
41
|
+
|
42
|
+
Add this line to your application's Gemfile:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
gem 'rspec-multiprocess_runner'
|
46
|
+
```
|
47
|
+
|
48
|
+
And then execute:
|
49
|
+
|
50
|
+
$ bundle
|
51
|
+
|
52
|
+
Or install it yourself as:
|
53
|
+
|
54
|
+
$ gem install rspec-multiprocess_runner
|
55
|
+
|
56
|
+
## Usage
|
57
|
+
|
58
|
+
### Command line
|
59
|
+
|
60
|
+
Use `multirspec` to run a bunch of spec files:
|
61
|
+
|
62
|
+
$ multirspec spec
|
63
|
+
|
64
|
+
Runs three workers by default — use `-w` to chose another count. `--help` will
|
65
|
+
detail the other options.
|
66
|
+
|
67
|
+
You can provide options that will be passed to the separate RSpec processes by
|
68
|
+
including them after a `--`:
|
69
|
+
|
70
|
+
$ multirspec -w 5 spec -- -b -I ./lib
|
71
|
+
|
72
|
+
In this case, each RSpec process would receive the options `-b -I ./lib`. Note
|
73
|
+
that not that many RSpec options really make sense to pass this way. In
|
74
|
+
particular, file selection and output formatting options are unlikely to work
|
75
|
+
the way you expect.
|
76
|
+
|
77
|
+
### Rake
|
78
|
+
|
79
|
+
There is a rake task wrapper for `multirspec`:
|
80
|
+
|
81
|
+
require 'rspec/multiprocess_runner/rake_task'
|
82
|
+
|
83
|
+
RSpec::MultiprocessRunner::RakeTask.new(:spec) do |t|
|
84
|
+
t.worker_count = 5
|
85
|
+
t.pattern = "spec/**/*_spec.rb"
|
86
|
+
end
|
87
|
+
|
88
|
+
See its source for the full list of options.
|
89
|
+
|
90
|
+
### Code
|
91
|
+
|
92
|
+
Create a coordinator and tell it to run:
|
93
|
+
|
94
|
+
require 'rspec/multiprocess_runner/coordinator'
|
95
|
+
|
96
|
+
worker_count = 4
|
97
|
+
per_file_timeout = 5 * 60 # 5 minutes in seconds
|
98
|
+
rspec_args = %w(--backtrace)
|
99
|
+
files = Dir['**/*_spec.rb']
|
100
|
+
|
101
|
+
coordinator = RSpec::MultiprocessRunner::Coordinator(worker_count, per_file_timeout, rspec_args, files)
|
102
|
+
coordinator.run
|
103
|
+
|
104
|
+
## Contributing
|
105
|
+
|
106
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/cdd/rspec-multiprocess_runner.
|
107
|
+
|
108
|
+
|
109
|
+
## License
|
110
|
+
|
111
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/TODO.md
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# Tests
|
2
|
+
|
3
|
+
Tricky things which I am manually testing:
|
4
|
+
|
5
|
+
* Timing out slow files (as_spec.rb)
|
6
|
+
* Recognizing and handling workers that die (ax_spec.rb, x*_spec.rb)
|
7
|
+
* Preserving the configuration from spec_helper, etc., across runs (w*_spec.rb)
|
8
|
+
|
9
|
+
Need to write automated tests for these (in addition to tests for all the basic
|
10
|
+
behavior). Maybe use Cucumber (like RSpec does for acceptance tests) to avoid
|
11
|
+
the weirdness of testing rspec within rspec.
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "rspec/multiprocess_runner"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/exe/multirspec
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rspec/multiprocess_runner/command_line_options'
|
4
|
+
require 'rspec/multiprocess_runner/coordinator'
|
5
|
+
|
6
|
+
options = RSpec::MultiprocessRunner::CommandLineOptions.new.parse(ARGV.dup)
|
7
|
+
exit(2) unless options
|
8
|
+
|
9
|
+
success = RSpec::MultiprocessRunner::Coordinator.new(
|
10
|
+
options.worker_count,
|
11
|
+
options.file_timeout_seconds,
|
12
|
+
options.rspec_options,
|
13
|
+
options.files_to_run
|
14
|
+
).run
|
15
|
+
|
16
|
+
exit(success ? 0 : 1)
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'rspec/multiprocess_runner'
|
2
|
+
require 'optparse'
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
module RSpec::MultiprocessRunner
|
6
|
+
# @private
|
7
|
+
class CommandLineOptions
|
8
|
+
attr_accessor :worker_count, :file_timeout_seconds, :rspec_options,
|
9
|
+
:explicit_files_or_directories, :pattern
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
self.worker_count = 3
|
13
|
+
self.file_timeout_seconds = 300
|
14
|
+
self.pattern = "**/*_spec.rb"
|
15
|
+
self.rspec_options = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def parse(command_line_args, error_stream=$stderr)
|
19
|
+
args = command_line_args.dup
|
20
|
+
parser = build_parser
|
21
|
+
|
22
|
+
begin
|
23
|
+
parser.parse!(args)
|
24
|
+
rescue OptionParser::ParseError => e
|
25
|
+
error_stream.puts e.to_s
|
26
|
+
error_stream.puts parser
|
27
|
+
return nil
|
28
|
+
end
|
29
|
+
|
30
|
+
if help_requested?
|
31
|
+
error_stream.puts parser
|
32
|
+
return nil
|
33
|
+
end
|
34
|
+
|
35
|
+
extract_files_and_rspec_options(args)
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
def files_to_run
|
40
|
+
self.explicit_files_or_directories = %w(.) unless explicit_files_or_directories
|
41
|
+
relative_root = Pathname.new('.')
|
42
|
+
explicit_files_or_directories.map { |path| Pathname.new(path) }.flat_map do |path|
|
43
|
+
if path.file?
|
44
|
+
path.to_s
|
45
|
+
else
|
46
|
+
Dir[path.join(pattern).relative_path_from(relative_root)]
|
47
|
+
end
|
48
|
+
end.sort_by { |path| path.downcase }
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def help_requested!
|
54
|
+
@help_requested = true
|
55
|
+
end
|
56
|
+
|
57
|
+
def help_requested?
|
58
|
+
@help_requested
|
59
|
+
end
|
60
|
+
|
61
|
+
def build_parser
|
62
|
+
OptionParser.new do |parser|
|
63
|
+
parser.banner = "#{File.basename $0} [options] [files or directories] [-- rspec options]"
|
64
|
+
|
65
|
+
parser.on("-w", "--worker-count COUNT", Integer, "Number of workers to run (default: #{worker_count})") do |n|
|
66
|
+
self.worker_count = n
|
67
|
+
end
|
68
|
+
|
69
|
+
parser.on("-t", "--file-timeout SECONDS", Integer, "Maximum time to allow any single file to run (default: #{file_timeout_seconds})") do |s|
|
70
|
+
self.file_timeout_seconds = s
|
71
|
+
end
|
72
|
+
|
73
|
+
parser.on("-P", "--pattern PATTERN", "A glob to use to select files to run (default: #{pattern})") do |pattern|
|
74
|
+
self.pattern = pattern
|
75
|
+
end
|
76
|
+
|
77
|
+
parser.on_tail("-h", "--help", "Prints this help") do
|
78
|
+
help_requested!
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def extract_files_and_rspec_options(remaining_args)
|
84
|
+
files = []
|
85
|
+
rspec_options = []
|
86
|
+
target = files
|
87
|
+
remaining_args.each do |arg|
|
88
|
+
if arg[0] == '-'
|
89
|
+
target = rspec_options
|
90
|
+
end
|
91
|
+
target << arg
|
92
|
+
end
|
93
|
+
|
94
|
+
self.explicit_files_or_directories = files unless files.empty?
|
95
|
+
self.rspec_options = rspec_options unless rspec_options.empty?
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'rspec/multiprocess_runner'
|
3
|
+
require 'rspec/multiprocess_runner/worker'
|
4
|
+
|
5
|
+
module RSpec::MultiprocessRunner
|
6
|
+
class Coordinator
|
7
|
+
def initialize(worker_count, file_timeout, rspec_options, files)
|
8
|
+
@worker_count = worker_count
|
9
|
+
@file_timeout = file_timeout
|
10
|
+
@rspec_options = rspec_options
|
11
|
+
@spec_files = files
|
12
|
+
@workers = []
|
13
|
+
@deactivated_workers = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def run
|
17
|
+
@start_time = Time.now
|
18
|
+
expected_worker_numbers.each do |n|
|
19
|
+
create_and_start_worker_if_necessary(n)
|
20
|
+
end
|
21
|
+
run_loop
|
22
|
+
quit_all_workers
|
23
|
+
print_summary
|
24
|
+
|
25
|
+
!failed?
|
26
|
+
end
|
27
|
+
|
28
|
+
def failed?
|
29
|
+
!@deactivated_workers.empty? || any_example_failed?
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def worker_sockets
|
35
|
+
@workers.map(&:socket)
|
36
|
+
end
|
37
|
+
|
38
|
+
def run_loop
|
39
|
+
loop do
|
40
|
+
act_on_available_worker_messages(0.3)
|
41
|
+
reap_stalled_workers
|
42
|
+
start_missing_workers
|
43
|
+
break unless @workers.detect(&:working?)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def quit_all_workers
|
48
|
+
quit_threads = @workers.map do |worker|
|
49
|
+
Thread.new do
|
50
|
+
worker.quit
|
51
|
+
worker.wait_until_quit
|
52
|
+
end
|
53
|
+
end
|
54
|
+
quit_threads.each(&:join)
|
55
|
+
end
|
56
|
+
|
57
|
+
def work_left_to_do?
|
58
|
+
!@spec_files.empty?
|
59
|
+
end
|
60
|
+
|
61
|
+
def act_on_available_worker_messages(timeout)
|
62
|
+
while (select_result = IO.select(worker_sockets, nil, nil, timeout))
|
63
|
+
select_result.first.each do |readable_socket|
|
64
|
+
ready_worker = @workers.detect { |worker| worker.socket == readable_socket }
|
65
|
+
worker_status = ready_worker.receive_and_act_on_message_from_worker
|
66
|
+
if worker_status == :dead
|
67
|
+
reap_one_worker(ready_worker, "died")
|
68
|
+
elsif work_left_to_do? && !ready_worker.working?
|
69
|
+
ready_worker.run_file(@spec_files.shift)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def reap_one_worker(worker, reason)
|
76
|
+
worker.reap
|
77
|
+
@deactivated_workers << worker
|
78
|
+
worker.deactivation_reason = reason
|
79
|
+
@workers.reject! { |w| w == worker }
|
80
|
+
end
|
81
|
+
|
82
|
+
def reap_stalled_workers
|
83
|
+
@workers.select(&:stalled?).each do |stalled_worker|
|
84
|
+
reap_one_worker(stalled_worker, "stalled")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def expected_worker_numbers
|
89
|
+
(1..@worker_count).to_a
|
90
|
+
end
|
91
|
+
|
92
|
+
def create_and_start_worker_if_necessary(n)
|
93
|
+
if work_left_to_do?
|
94
|
+
$stderr.puts "(Re)starting worker #{n}"
|
95
|
+
new_worker = Worker.new(n, @file_timeout, @rspec_options)
|
96
|
+
@workers << new_worker
|
97
|
+
new_worker.start
|
98
|
+
new_worker.run_file(@spec_files.shift)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def start_missing_workers
|
103
|
+
if @workers.size < @worker_count && work_left_to_do?
|
104
|
+
running_process_numbers = @workers.map(&:environment_number)
|
105
|
+
missing_process_numbers = expected_worker_numbers - running_process_numbers
|
106
|
+
missing_process_numbers.each do |n|
|
107
|
+
create_and_start_worker_if_necessary(n)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def print_summary
|
113
|
+
elapsed = Time.now - @start_time
|
114
|
+
by_status_and_time = combine_example_results.each_with_object({}) do |result, idx|
|
115
|
+
(idx[result.status] ||= []) << result
|
116
|
+
end
|
117
|
+
print_pending_example_details(by_status_and_time["pending"])
|
118
|
+
print_failed_example_details(by_status_and_time["failed"])
|
119
|
+
print_failed_process_details
|
120
|
+
puts
|
121
|
+
print_elapsed_time(elapsed)
|
122
|
+
puts failed? ? "FAILURE" : "SUCCESS"
|
123
|
+
print_example_counts(by_status_and_time)
|
124
|
+
end
|
125
|
+
|
126
|
+
def combine_example_results
|
127
|
+
(@workers + @deactivated_workers).flat_map(&:example_results).sort_by { |r| r.time_finished }
|
128
|
+
end
|
129
|
+
|
130
|
+
def any_example_failed?
|
131
|
+
(@workers + @deactivated_workers).detect { |w| w.example_results.detect { |r| r.status == "failed" } }
|
132
|
+
end
|
133
|
+
|
134
|
+
def print_pending_example_details(pending_example_results)
|
135
|
+
return if pending_example_results.nil?
|
136
|
+
puts
|
137
|
+
puts "Pending:"
|
138
|
+
pending_example_results.each do |pending|
|
139
|
+
puts
|
140
|
+
puts pending.details.sub(/^\s*Pending:\s*/, '')
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def print_failed_example_details(failed_example_results)
|
145
|
+
return if failed_example_results.nil?
|
146
|
+
puts
|
147
|
+
puts "Failures:"
|
148
|
+
failed_example_results.each_with_index do |failure, i|
|
149
|
+
puts
|
150
|
+
puts " #{i.next}) #{failure.description}"
|
151
|
+
puts failure.details
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Copied from RSpec
|
156
|
+
def pluralize(count, string)
|
157
|
+
"#{count} #{string}#{'s' unless count.to_f == 1}"
|
158
|
+
end
|
159
|
+
|
160
|
+
def print_example_counts(by_status_and_time)
|
161
|
+
example_count = by_status_and_time.map { |status, results| results.size }.inject(0) { |sum, ct| sum + ct }
|
162
|
+
failure_count = by_status_and_time["failed"] ? by_status_and_time["failed"].size : 0
|
163
|
+
pending_count = by_status_and_time["pending"] ? by_status_and_time["pending"].size : 0
|
164
|
+
process_failure_count = @deactivated_workers.size
|
165
|
+
|
166
|
+
# Copied from RSpec
|
167
|
+
summary = pluralize(example_count, "example")
|
168
|
+
summary << ", " << pluralize(failure_count, "failure")
|
169
|
+
summary << ", #{pending_count} pending" if pending_count > 0
|
170
|
+
summary << ", " << pluralize(process_failure_count, "failed proc") if process_failure_count > 0
|
171
|
+
puts summary
|
172
|
+
end
|
173
|
+
|
174
|
+
def print_failed_process_details
|
175
|
+
return if @deactivated_workers.empty?
|
176
|
+
puts
|
177
|
+
puts "Failed processes:"
|
178
|
+
@deactivated_workers.each do |worker|
|
179
|
+
puts " - #{worker.pid} (env #{worker.environment_number}) #{worker.deactivation_reason} on #{worker.current_file}"
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def print_elapsed_time(seconds_elapsed)
|
184
|
+
minutes = seconds_elapsed.to_i / 60
|
185
|
+
seconds = seconds_elapsed % 60
|
186
|
+
m =
|
187
|
+
if minutes > 0
|
188
|
+
"%d minute%s" % [minutes, minutes == 1 ? '' : 's']
|
189
|
+
end
|
190
|
+
s =
|
191
|
+
if seconds > 0
|
192
|
+
"%.2f second%s" % [seconds, seconds == 1 ? '' : 's']
|
193
|
+
end
|
194
|
+
puts "Finished in #{[m, s].compact.join(", ")}"
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'rspec/multiprocess_runner'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/tasklib'
|
4
|
+
require 'shellwords'
|
5
|
+
|
6
|
+
module RSpec::MultiprocessRunner
|
7
|
+
# Rake task to invoke `multispec`. Lots of it is copied from RSpec::Core::RakeTask.
|
8
|
+
#
|
9
|
+
# @see Rakefile
|
10
|
+
class RakeTask < ::Rake::TaskLib
|
11
|
+
include ::Rake::DSL if defined?(::Rake::DSL)
|
12
|
+
|
13
|
+
# Default path to the multirspec executable.
|
14
|
+
DEFAULT_MULTIRSPEC_PATH = File.expand_path('../../../../exe/multirspec', __FILE__)
|
15
|
+
|
16
|
+
# Name of task. Defaults to `:multispec`.
|
17
|
+
attr_accessor :name
|
18
|
+
|
19
|
+
# Files matching this pattern will be loaded.
|
20
|
+
# Defaults to `'**/*_spec.rb'`.
|
21
|
+
attr_accessor :pattern
|
22
|
+
|
23
|
+
# File search will be limited to these directories or specific files.
|
24
|
+
# Defaults to nil.
|
25
|
+
attr_accessor :files_or_directories
|
26
|
+
alias_method :files=, :files_or_directories=
|
27
|
+
alias_method :directories=, :files_or_directories=
|
28
|
+
|
29
|
+
# The number of workers to run. Defaults to 3.
|
30
|
+
attr_accessor :worker_count
|
31
|
+
|
32
|
+
# The maximum number of seconds to allow a single spec file to run
|
33
|
+
# before killing it. Defaults to 300.
|
34
|
+
attr_accessor :file_timeout_seconds
|
35
|
+
|
36
|
+
# Whether or not to fail Rake when an error occurs (typically when
|
37
|
+
# examples fail). Defaults to `true`.
|
38
|
+
attr_accessor :fail_on_error
|
39
|
+
|
40
|
+
# A message to print to stderr when there are failures.
|
41
|
+
attr_accessor :failure_message
|
42
|
+
|
43
|
+
# Use verbose output. If this is set to true, the task will print the
|
44
|
+
# executed spec command to stdout. Defaults to `true`.
|
45
|
+
attr_accessor :verbose
|
46
|
+
|
47
|
+
# Path to the multispec executable. Defaults to the absolute path to the
|
48
|
+
# rspec binary from the loaded rspec-core gem.
|
49
|
+
attr_accessor :multirspec_path
|
50
|
+
|
51
|
+
# Command line options to pass to the RSpec workers. Defaults to `nil`.
|
52
|
+
attr_accessor :rspec_opts
|
53
|
+
|
54
|
+
def initialize(*args, &task_block)
|
55
|
+
@name = args.shift || :multispec
|
56
|
+
@verbose = true
|
57
|
+
@fail_on_error = true
|
58
|
+
@multirspec_path = DEFAULT_MULTIRSPEC_PATH
|
59
|
+
|
60
|
+
define(args, &task_block)
|
61
|
+
end
|
62
|
+
|
63
|
+
# @private
|
64
|
+
def run_task(verbose)
|
65
|
+
command = spec_command
|
66
|
+
puts Shellwords.shelljoin(command) if verbose
|
67
|
+
|
68
|
+
return if system(*command)
|
69
|
+
puts failure_message if failure_message
|
70
|
+
|
71
|
+
return unless fail_on_error
|
72
|
+
$stderr.puts "#{command} failed" if verbose
|
73
|
+
exit $?.exitstatus
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# @private
|
79
|
+
def define(args, &task_block)
|
80
|
+
desc "Run RSpec code examples" unless ::Rake.application.last_comment
|
81
|
+
|
82
|
+
task name, *args do |_, task_args|
|
83
|
+
RakeFileUtils.__send__(:verbose, verbose) do
|
84
|
+
task_block.call(*[self, task_args].slice(0, task_block.arity)) if task_block
|
85
|
+
run_task verbose
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def spec_command
|
91
|
+
cmd_parts = []
|
92
|
+
cmd_parts << RUBY
|
93
|
+
cmd_parts << multirspec_path
|
94
|
+
if worker_count
|
95
|
+
cmd_parts << '--worker-count' << worker_count.to_s
|
96
|
+
end
|
97
|
+
if file_timeout_seconds
|
98
|
+
cmd_parts << '--file-timeout' << file_timeout_seconds.to_s
|
99
|
+
end
|
100
|
+
if pattern
|
101
|
+
cmd_parts << '--pattern' << pattern
|
102
|
+
end
|
103
|
+
if files_or_directories
|
104
|
+
cmd_parts.concat(files_or_directories)
|
105
|
+
end
|
106
|
+
if rspec_opts
|
107
|
+
cmd_parts << '--'
|
108
|
+
cmd_parts.concat(Shellwords.shellsplit rspec_opts)
|
109
|
+
end
|
110
|
+
|
111
|
+
cmd_parts
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'rspec/multiprocess_runner'
|
3
|
+
require 'rspec/core/formatters/base_text_formatter'
|
4
|
+
|
5
|
+
module RSpec::MultiprocessRunner
|
6
|
+
##
|
7
|
+
# RSpec formatter used by workers to communicate spec execution to the
|
8
|
+
# coordinator.
|
9
|
+
#
|
10
|
+
# @private
|
11
|
+
class ReportingFormatter < RSpec::Core::Formatters::BaseTextFormatter
|
12
|
+
class << self
|
13
|
+
# The worker to which to report spec status. This has to be a class-level
|
14
|
+
# attribute because you can't access the formatter instance used by
|
15
|
+
# RSpec's runner.
|
16
|
+
attr_accessor :worker
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(*ignored_args)
|
20
|
+
super(StringIO.new)
|
21
|
+
@current_example_groups = []
|
22
|
+
end
|
23
|
+
|
24
|
+
def example_group_started(example_group)
|
25
|
+
super(example_group)
|
26
|
+
|
27
|
+
@current_example_groups.push(example_group.description.strip)
|
28
|
+
end
|
29
|
+
|
30
|
+
def example_group_finished(example_group)
|
31
|
+
@current_example_groups.pop
|
32
|
+
end
|
33
|
+
|
34
|
+
def example_passed(example)
|
35
|
+
super(example)
|
36
|
+
report_example_result(:passed, current_example_description(example))
|
37
|
+
end
|
38
|
+
|
39
|
+
def example_pending(example)
|
40
|
+
super(example)
|
41
|
+
details = capture_output { dump_pending }
|
42
|
+
pending_examples.clear
|
43
|
+
report_example_result(:pending, current_example_description(example), details)
|
44
|
+
end
|
45
|
+
|
46
|
+
def example_failed(example)
|
47
|
+
super(example)
|
48
|
+
details = capture_output {
|
49
|
+
dump_failure_info(example)
|
50
|
+
dump_backtrace(example)
|
51
|
+
}
|
52
|
+
report_example_result(:failed, current_example_description(example), details)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def capture_output
|
58
|
+
output.string = ""
|
59
|
+
yield
|
60
|
+
captured = output.string
|
61
|
+
output.string = ""
|
62
|
+
captured
|
63
|
+
end
|
64
|
+
|
65
|
+
def worker
|
66
|
+
self.class.worker
|
67
|
+
end
|
68
|
+
|
69
|
+
def current_example_description(example)
|
70
|
+
(@current_example_groups + [example.description.strip]).join('·')
|
71
|
+
end
|
72
|
+
|
73
|
+
def report_example_result(example_status, description, details=nil)
|
74
|
+
worker.report_example_result(example_status, description, details)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,262 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "rspec/multiprocess_runner"
|
3
|
+
require "rspec/multiprocess_runner/reporting_formatter"
|
4
|
+
|
5
|
+
require "rspec/core"
|
6
|
+
require "rspec/core/runner"
|
7
|
+
|
8
|
+
require "socket"
|
9
|
+
require "json"
|
10
|
+
require "timeout"
|
11
|
+
|
12
|
+
module RSpec::MultiprocessRunner
|
13
|
+
##
|
14
|
+
# This object has several roles:
|
15
|
+
# - It forks the worker process
|
16
|
+
# - In the coordinator process, it is used to send messages to the worker and
|
17
|
+
# track the worker's status, completed specs, and example results.
|
18
|
+
# - In the worker process, it is used to send messages to the coordinator and
|
19
|
+
# actually run specs.
|
20
|
+
#
|
21
|
+
# @private
|
22
|
+
class Worker
|
23
|
+
attr_reader :pid, :environment_number, :example_results, :current_file
|
24
|
+
attr_accessor :deactivation_reason
|
25
|
+
|
26
|
+
COMMAND_QUIT = "quit"
|
27
|
+
COMMAND_RUN_FILE = "run_file"
|
28
|
+
|
29
|
+
STATUS_EXAMPLE_COMPLETE = "example_complete"
|
30
|
+
STATUS_RUN_COMPLETE = "run_complete"
|
31
|
+
|
32
|
+
def initialize(environment_number, file_timeout, rspec_arguments=[])
|
33
|
+
@environment_number = environment_number
|
34
|
+
@worker_socket, @coordinator_socket = Socket.pair(:UNIX, :STREAM)
|
35
|
+
@rspec_arguments = rspec_arguments + ["--format", ReportingFormatter.to_s]
|
36
|
+
@file_timeout = file_timeout
|
37
|
+
@example_results = []
|
38
|
+
|
39
|
+
# Use a single configuration and world across all individual runs
|
40
|
+
# This will not be necessary to do manually in RSpec 3 — it does not
|
41
|
+
# reset the globals after each run.
|
42
|
+
@rspec_configuration = RSpec.configuration
|
43
|
+
@rspec_world = RSpec.world
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# Workers can be found in the coordinator process by their coordinator socket.
|
48
|
+
def ==(other)
|
49
|
+
case other
|
50
|
+
when Socket
|
51
|
+
other == @coordinator_socket
|
52
|
+
else
|
53
|
+
super
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# Forks the worker process. In the parent, returns the PID.
|
59
|
+
def start
|
60
|
+
pid = fork
|
61
|
+
if pid
|
62
|
+
@worker_socket.close
|
63
|
+
@pid = pid
|
64
|
+
else
|
65
|
+
@coordinator_socket.close
|
66
|
+
@pid = Process.pid
|
67
|
+
ENV["TEST_ENV_NUMBER"] = environment_number.to_s
|
68
|
+
set_process_name
|
69
|
+
run_loop
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def socket
|
74
|
+
if self.pid == Process.pid
|
75
|
+
@worker_socket
|
76
|
+
else
|
77
|
+
@coordinator_socket
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
###### COORDINATOR METHODS
|
82
|
+
## These are methods that the coordinator process calls on its copy of
|
83
|
+
## the workers.
|
84
|
+
|
85
|
+
public
|
86
|
+
|
87
|
+
def quit
|
88
|
+
send_message_to_worker(command: COMMAND_QUIT)
|
89
|
+
end
|
90
|
+
|
91
|
+
def wait_until_quit
|
92
|
+
Process.wait(self.pid)
|
93
|
+
end
|
94
|
+
|
95
|
+
def run_file(filename)
|
96
|
+
send_message_to_worker(command: COMMAND_RUN_FILE, filename: filename)
|
97
|
+
@current_file = filename
|
98
|
+
@current_file_started_at = Time.now
|
99
|
+
end
|
100
|
+
|
101
|
+
def working?
|
102
|
+
@current_file
|
103
|
+
end
|
104
|
+
|
105
|
+
def stalled?
|
106
|
+
working? && (Time.now - @current_file_started_at > @file_timeout)
|
107
|
+
end
|
108
|
+
|
109
|
+
def reap
|
110
|
+
begin
|
111
|
+
Timeout.timeout(4) do
|
112
|
+
$stderr.puts "Reaping troubled process #{environment_number} (#{pid}; #{@current_file}) with QUIT"
|
113
|
+
Process.kill(:QUIT, pid)
|
114
|
+
Process.wait(pid)
|
115
|
+
end
|
116
|
+
rescue Timeout::Error
|
117
|
+
$stderr.puts "Reaping troubled process #{environment_number} (#{pid}) with KILL"
|
118
|
+
Process.kill(:KILL, pid)
|
119
|
+
Process.wait(pid)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def receive_and_act_on_message_from_worker
|
124
|
+
act_on_message_from_worker(receive_message_from_worker)
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
def receive_message_from_worker
|
130
|
+
receive_message(@coordinator_socket)
|
131
|
+
end
|
132
|
+
|
133
|
+
def act_on_message_from_worker(message_hash)
|
134
|
+
return :dead unless message_hash # ignore EOF
|
135
|
+
case message_hash["status"]
|
136
|
+
when STATUS_RUN_COMPLETE
|
137
|
+
@current_file = nil
|
138
|
+
@current_file_started_at = nil
|
139
|
+
when STATUS_EXAMPLE_COMPLETE
|
140
|
+
example_results << ExampleResult.new(message_hash)
|
141
|
+
suffix =
|
142
|
+
case message_hash["example_status"]
|
143
|
+
when "failed"
|
144
|
+
" - FAILED"
|
145
|
+
when "pending"
|
146
|
+
" - pending"
|
147
|
+
end
|
148
|
+
if message_hash["details"]
|
149
|
+
suffix += "\n#{message_hash["details"]}"
|
150
|
+
end
|
151
|
+
$stdout.puts "#{environment_number} (#{pid}): #{message_hash["description"]}#{suffix}"
|
152
|
+
else
|
153
|
+
$stderr.puts "Received unsupported status #{message_hash["status"].inspect} in worker #{pid}"
|
154
|
+
end
|
155
|
+
return :alive
|
156
|
+
end
|
157
|
+
|
158
|
+
def send_message_to_worker(message_hash)
|
159
|
+
send_message(@coordinator_socket, message_hash)
|
160
|
+
end
|
161
|
+
|
162
|
+
###### WORKER METHODS
|
163
|
+
## These are methods that the worker process calls on the copy of this
|
164
|
+
## object that lives in the fork.
|
165
|
+
|
166
|
+
public
|
167
|
+
|
168
|
+
def report_example_result(example_status, description, details)
|
169
|
+
send_message_to_coordinator(
|
170
|
+
status: STATUS_EXAMPLE_COMPLETE,
|
171
|
+
example_status: example_status,
|
172
|
+
description: description,
|
173
|
+
details: details
|
174
|
+
)
|
175
|
+
end
|
176
|
+
|
177
|
+
private
|
178
|
+
|
179
|
+
def set_process_name
|
180
|
+
name = "RSpec::MultiprocessRunner::Worker #{environment_number}"
|
181
|
+
status = current_file ? "running #{current_file}" : "idle"
|
182
|
+
$0 = "#{name} #{status}"
|
183
|
+
end
|
184
|
+
|
185
|
+
def run_loop
|
186
|
+
loop do
|
187
|
+
select_result = IO.select([@worker_socket], nil, nil, 1)
|
188
|
+
if select_result
|
189
|
+
readables, _, _ = select_result
|
190
|
+
act_on_message_from_coordinator(
|
191
|
+
receive_message_from_coordinator(readables.first)
|
192
|
+
)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def receive_message_from_coordinator(socket)
|
198
|
+
receive_message(socket)
|
199
|
+
end
|
200
|
+
|
201
|
+
def send_message_to_coordinator(message_hash)
|
202
|
+
send_message(@worker_socket, message_hash)
|
203
|
+
end
|
204
|
+
|
205
|
+
def act_on_message_from_coordinator(message_hash)
|
206
|
+
return unless message_hash # ignore EOF
|
207
|
+
case message_hash["command"]
|
208
|
+
when "quit"
|
209
|
+
exit
|
210
|
+
when "run_file"
|
211
|
+
execute_spec(message_hash["filename"])
|
212
|
+
else
|
213
|
+
$stderr.puts "Received unsupported command #{message_hash["command"].inspect} in worker #{pid}"
|
214
|
+
end
|
215
|
+
set_process_name
|
216
|
+
end
|
217
|
+
|
218
|
+
def execute_spec(spec_file)
|
219
|
+
@current_file = spec_file
|
220
|
+
set_process_name
|
221
|
+
|
222
|
+
RSpec.configuration = @rspec_configuration
|
223
|
+
RSpec.world = @rspec_world
|
224
|
+
# If we don't do this, every previous spec is run every time run is called
|
225
|
+
RSpec.world.example_groups.clear
|
226
|
+
|
227
|
+
ReportingFormatter.worker = self
|
228
|
+
RSpec::Core::Runner.run(@rspec_arguments + [spec_file])
|
229
|
+
send_message_to_coordinator(status: STATUS_RUN_COMPLETE, filename: spec_file)
|
230
|
+
ensure
|
231
|
+
@current_file = nil
|
232
|
+
end
|
233
|
+
|
234
|
+
###### UTILITY FUNCTIONS
|
235
|
+
## Methods that used by both the coordinator and worker processes.
|
236
|
+
|
237
|
+
private
|
238
|
+
|
239
|
+
def receive_message(socket)
|
240
|
+
message_json = socket.gets
|
241
|
+
if message_json
|
242
|
+
JSON.parse(message_json)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def send_message(socket, message_hash)
|
247
|
+
socket.puts(message_hash.to_json)
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# @private
|
252
|
+
class ExampleResult
|
253
|
+
attr_reader :status, :description, :details, :time_finished
|
254
|
+
|
255
|
+
def initialize(example_complete_message)
|
256
|
+
@status = example_complete_message["example_status"]
|
257
|
+
@description = example_complete_message["description"]
|
258
|
+
@details = example_complete_message["details"]
|
259
|
+
@time_finished = Time.now
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'rspec/multiprocess_runner/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "rspec-multiprocess_runner"
|
8
|
+
spec.version = RSpec::MultiprocessRunner::VERSION
|
9
|
+
spec.authors = ["Rhett Sutphin"]
|
10
|
+
spec.email = ["rhett@detailedbalance.net"]
|
11
|
+
|
12
|
+
spec.summary = %q{A runner for RSpec 2 that uses multiple processes to execute specs in parallel}
|
13
|
+
spec.homepage = "https://github.com/cdd/rspec-multiprocess_runner"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|manual_test_specs)/}) }
|
17
|
+
spec.bindir = "exe"
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "rspec-core", "~> 2.0", "< 2.99.0"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
24
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
25
|
+
spec.add_development_dependency "rspec"
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rspec-multiprocess_runner
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Rhett Sutphin
|
9
|
+
autorequire:
|
10
|
+
bindir: exe
|
11
|
+
cert_chain: []
|
12
|
+
date: 2015-12-15 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec-core
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '2.0'
|
22
|
+
- - <
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 2.99.0
|
25
|
+
type: :runtime
|
26
|
+
prerelease: false
|
27
|
+
version_requirements: !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2.0'
|
33
|
+
- - <
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: 2.99.0
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: bundler
|
38
|
+
requirement: !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '1.10'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: !ruby/object:Gem::Requirement
|
47
|
+
none: false
|
48
|
+
requirements:
|
49
|
+
- - ~>
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '1.10'
|
52
|
+
- !ruby/object:Gem::Dependency
|
53
|
+
name: rake
|
54
|
+
requirement: !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ~>
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '10.0'
|
60
|
+
type: :development
|
61
|
+
prerelease: false
|
62
|
+
version_requirements: !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ~>
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '10.0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: rspec
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
72
|
+
requirements:
|
73
|
+
- - ! '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ! '>='
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
description:
|
85
|
+
email:
|
86
|
+
- rhett@detailedbalance.net
|
87
|
+
executables:
|
88
|
+
- multirspec
|
89
|
+
extensions: []
|
90
|
+
extra_rdoc_files: []
|
91
|
+
files:
|
92
|
+
- .gitignore
|
93
|
+
- .rspec
|
94
|
+
- .travis.yml
|
95
|
+
- CHANGELOG.md
|
96
|
+
- Gemfile
|
97
|
+
- LICENSE.txt
|
98
|
+
- README.md
|
99
|
+
- Rakefile
|
100
|
+
- TODO.md
|
101
|
+
- bin/console
|
102
|
+
- bin/setup
|
103
|
+
- exe/multirspec
|
104
|
+
- lib/rspec/multiprocess_runner.rb
|
105
|
+
- lib/rspec/multiprocess_runner/command_line_options.rb
|
106
|
+
- lib/rspec/multiprocess_runner/coordinator.rb
|
107
|
+
- lib/rspec/multiprocess_runner/rake_task.rb
|
108
|
+
- lib/rspec/multiprocess_runner/reporting_formatter.rb
|
109
|
+
- lib/rspec/multiprocess_runner/version.rb
|
110
|
+
- lib/rspec/multiprocess_runner/worker.rb
|
111
|
+
- rspec-multiprocess_runner.gemspec
|
112
|
+
homepage: https://github.com/cdd/rspec-multiprocess_runner
|
113
|
+
licenses:
|
114
|
+
- MIT
|
115
|
+
post_install_message:
|
116
|
+
rdoc_options: []
|
117
|
+
require_paths:
|
118
|
+
- lib
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
120
|
+
none: false
|
121
|
+
requirements:
|
122
|
+
- - ! '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
segments:
|
126
|
+
- 0
|
127
|
+
hash: -1609184745420465239
|
128
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ! '>='
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
segments:
|
135
|
+
- 0
|
136
|
+
hash: -1609184745420465239
|
137
|
+
requirements: []
|
138
|
+
rubyforge_project:
|
139
|
+
rubygems_version: 1.8.25
|
140
|
+
signing_key:
|
141
|
+
specification_version: 3
|
142
|
+
summary: A runner for RSpec 2 that uses multiple processes to execute specs in parallel
|
143
|
+
test_files: []
|