rspec-multiprocess_runner 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +33 -7
- data/exe/multirspec +5 -1
- data/lib/rspec/multiprocess_runner/command_line_options.rb +21 -1
- data/lib/rspec/multiprocess_runner/coordinator.rb +61 -25
- data/lib/rspec/multiprocess_runner/file_coordinator.rb +148 -0
- data/lib/rspec/multiprocess_runner/rake_task.rb +24 -0
- data/lib/rspec/multiprocess_runner/version.rb +1 -1
- data/lib/rspec/multiprocess_runner/worker.rb +31 -2
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: da6e498842698e2a4d310674c27cace4b39ca99d
|
4
|
+
data.tar.gz: dbc698c179206e1233bdb6448bc01611ca5aa036
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ef0ef92395a94c984f6f253cddbec507aac1f4f4e0d330009e2a383063fc35be8d3d7463892ad45a8d7c3810b09724de08ec323f4f551ff1641d7070f682c489
|
7
|
+
data.tar.gz: 87582349643f471ba15e90da662ab759539572eebc42f47af8f719b491bac36a2764aaf098da705f18d025f738583dc97bb4493b136d82eeb78a423e4d206383
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,16 @@
|
|
1
|
+
# 1.2.0
|
2
|
+
|
3
|
+
* Can now run tests on multiple machines at a time by running in head node mode
|
4
|
+
and others in node mode. Nodes get which files to run when from the head node
|
5
|
+
in a manner similar to how workers got files from the coordinator (which
|
6
|
+
currently also still happens).
|
7
|
+
|
8
|
+
* Uses unencoded TCP, but works with SSH tunnels
|
9
|
+
|
10
|
+
* Adds associated command line and rake task options
|
11
|
+
|
12
|
+
* Reruns once locally if any files are missing from disconnects
|
13
|
+
|
1
14
|
# 1.1.0
|
2
15
|
|
3
16
|
* Redo exit codes for arguments, signal hanlding, and exceptions
|
data/README.md
CHANGED
@@ -1,13 +1,15 @@
|
|
1
1
|
# Rspec::MultiprocessRunner
|
2
2
|
|
3
3
|
This gem provides a mechanism for running a suite of RSpec tests in multiple
|
4
|
-
processes on the same machine
|
5
|
-
improvements.
|
4
|
+
processes on the same machine and multiple machines (all reporting back to the
|
5
|
+
head node [machine]), potentially allowing substantial performance improvements.
|
6
6
|
|
7
|
-
It differs from `parallel_tests` in that it uses a coordinator process
|
8
|
-
the workers, hand off work to them, and receive results.
|
9
|
-
|
10
|
-
|
7
|
+
It differs from `parallel_tests` in that it uses a coordinator process on each
|
8
|
+
machine to manage the workers, hand off work to them, and receive results. These
|
9
|
+
coordinators communicate via simple TCP messages with a head node coordinator to
|
10
|
+
remain in sync when running on multiple machines. This means it can dynamically
|
11
|
+
balance the workload among the processors and machines. It also means it can
|
12
|
+
provide consolidated results in one console.
|
11
13
|
|
12
14
|
It follows parallel_tests' environment variable conventions so it's easy to
|
13
15
|
use them together. (E.g., parallel_tests has a very nice set of rake tasks
|
@@ -19,11 +21,14 @@ for setting up parallel environments.)
|
|
19
21
|
time needed to run a suite. Even CPU-bound specs can be aided
|
20
22
|
* Provides detailed logging of each example as it completes, including the
|
21
23
|
failure message (you don't have to wait until the end to see the failure
|
22
|
-
reason).
|
24
|
+
reason). This is only on the machine running the spec, a final print out does
|
25
|
+
occur on the main machine upon completion, however.
|
23
26
|
* Detects, kills, and reports spec files that take longer than expected (five
|
24
27
|
minutes by default).
|
25
28
|
* Detects and reports spec files that crash (without interrupting the
|
26
29
|
remainder of the suite).
|
30
|
+
* Head node detects and reruns spec files that are not reported back by the
|
31
|
+
nodes.
|
27
32
|
|
28
33
|
## Limitations
|
29
34
|
|
@@ -37,6 +42,9 @@ for setting up parallel environments.)
|
|
37
42
|
* Intermediate-quality code. Happy path works, and workers are
|
38
43
|
managed/restarted, but:
|
39
44
|
* There's no test coverage of the runner itself, only auxiliaries.
|
45
|
+
* No security in the TCP messaging, but can work over SSH tunnels. Nodes do
|
46
|
+
verify the file name given against the known files so it will not execute
|
47
|
+
maliciously.
|
40
48
|
|
41
49
|
## Installation
|
42
50
|
|
@@ -75,6 +83,24 @@ that not that many RSpec options really make sense to pass this way. In
|
|
75
83
|
particular, file selection and output formatting options are unlikely to work
|
76
84
|
the way you expect.
|
77
85
|
|
86
|
+
Runs as a head node node by default. The following command mimics the defaults:
|
87
|
+
|
88
|
+
$ multirspec -p 2222 -n 5 [Files]
|
89
|
+
|
90
|
+
A corresponding node node, for a head node on `head_node.local` would be:
|
91
|
+
|
92
|
+
$ multirspec -H head_node.local -p 2222 -n [Files]
|
93
|
+
|
94
|
+
N.B. You must include the same files for the nodes as the head node.
|
95
|
+
|
96
|
+
A corresponding set up for a node node using SSH would be:
|
97
|
+
|
98
|
+
$ ssh -nNT -L 2500:localhost:2222 head_node.local &
|
99
|
+
$ multirspec -H localhost -p 2500 -n [Files]
|
100
|
+
$ kill $(jobs -p)
|
101
|
+
|
102
|
+
N.B. Ensure that the head node has tcp local port forwarding permitted.
|
103
|
+
|
78
104
|
### Rake
|
79
105
|
|
80
106
|
There is a rake task wrapper for `multirspec`:
|
data/exe/multirspec
CHANGED
@@ -15,7 +15,11 @@ coordinator = RSpec::MultiprocessRunner::Coordinator.new(
|
|
15
15
|
test_env_number_first_is_1: options.first_is_1,
|
16
16
|
rspec_options: options.rspec_options,
|
17
17
|
log_failing_files: options.log_failing_files,
|
18
|
-
use_given_order: options.use_given_order
|
18
|
+
use_given_order: options.use_given_order,
|
19
|
+
head_node: options.head_node,
|
20
|
+
port: options.port,
|
21
|
+
hostname: options.hostname,
|
22
|
+
max_nodes: options.max_nodes
|
19
23
|
}
|
20
24
|
)
|
21
25
|
|
@@ -7,7 +7,7 @@ module RSpec::MultiprocessRunner
|
|
7
7
|
class CommandLineOptions
|
8
8
|
attr_accessor :worker_count, :file_timeout_seconds, :example_timeout_seconds,
|
9
9
|
:rspec_options, :explicit_files_or_directories, :pattern, :log_failing_files,
|
10
|
-
:first_is_1, :use_given_order
|
10
|
+
:first_is_1, :use_given_order, :port, :head_node, :hostname, :max_nodes
|
11
11
|
|
12
12
|
DEFAULT_WORKER_COUNT = 3
|
13
13
|
|
@@ -20,6 +20,10 @@ module RSpec::MultiprocessRunner
|
|
20
20
|
self.rspec_options = []
|
21
21
|
self.first_is_1 = default_first_is_1
|
22
22
|
self.use_given_order = false
|
23
|
+
self.port = 2222
|
24
|
+
self.hostname = "localhost"
|
25
|
+
self.head_node = true
|
26
|
+
self.max_nodes = 5
|
23
27
|
end
|
24
28
|
|
25
29
|
def parse(command_line_args, error_stream=$stderr)
|
@@ -119,6 +123,22 @@ module RSpec::MultiprocessRunner
|
|
119
123
|
self.use_given_order = true
|
120
124
|
end
|
121
125
|
|
126
|
+
parser.on("-p", "--port PORT", Integer, "Communicate using port (#{print_default port})") do |port|
|
127
|
+
self.port = port
|
128
|
+
end
|
129
|
+
|
130
|
+
parser.on("-H", "--hostname HOSTNAME", "Hostname of the head node (#{print_default hostname})") do |hostname|
|
131
|
+
self.hostname = hostname
|
132
|
+
end
|
133
|
+
|
134
|
+
parser.on("-n", "--node", "This node is controlled by a head node") do
|
135
|
+
self.head_node = false
|
136
|
+
end
|
137
|
+
|
138
|
+
parser.on("-m", "--max-nodes MAX_NODES", Integer, "Maximum number of nodes (excluding master) permitted (#{print_default max_nodes})") do |max_nodes|
|
139
|
+
self.max_nodes = max_nodes
|
140
|
+
end
|
141
|
+
|
122
142
|
parser.on_tail("-h", "--help", "Prints this help") do
|
123
143
|
help_requested!
|
124
144
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
require 'rspec/multiprocess_runner'
|
3
3
|
require 'rspec/multiprocess_runner/worker'
|
4
|
+
require 'rspec/multiprocess_runner/file_coordinator'
|
4
5
|
|
5
6
|
module RSpec::MultiprocessRunner
|
6
7
|
class Coordinator
|
@@ -11,9 +12,11 @@ module RSpec::MultiprocessRunner
|
|
11
12
|
@test_env_number_first_is_1 = options[:test_env_number_first_is_1]
|
12
13
|
@log_failing_files = options[:log_failing_files]
|
13
14
|
@rspec_options = options[:rspec_options]
|
14
|
-
@
|
15
|
+
@file_buffer = []
|
15
16
|
@workers = []
|
16
17
|
@stopped_workers = []
|
18
|
+
@worker_results = []
|
19
|
+
@file_coordinator = FileCoordinator.new(files, options)
|
17
20
|
end
|
18
21
|
|
19
22
|
def run
|
@@ -23,6 +26,12 @@ module RSpec::MultiprocessRunner
|
|
23
26
|
end
|
24
27
|
run_loop
|
25
28
|
quit_all_workers
|
29
|
+
@file_coordinator.finished
|
30
|
+
if @file_coordinator.missing_files.any?
|
31
|
+
run_loop
|
32
|
+
quit_all_workers
|
33
|
+
@file_coordinator.finished
|
34
|
+
end
|
26
35
|
print_summary
|
27
36
|
|
28
37
|
exit_code
|
@@ -36,7 +45,7 @@ module RSpec::MultiprocessRunner
|
|
36
45
|
exit_code = 0
|
37
46
|
exit_code |= 1 if any_example_failed?
|
38
47
|
exit_code |= 2 if !failed_workers.empty?
|
39
|
-
exit_code |= 4 if
|
48
|
+
exit_code |= 4 if work_left_to_do? || @file_coordinator.missing_files.any?
|
40
49
|
exit_code
|
41
50
|
end
|
42
51
|
|
@@ -72,6 +81,7 @@ module RSpec::MultiprocessRunner
|
|
72
81
|
end
|
73
82
|
|
74
83
|
def run_loop
|
84
|
+
add_file_to_buffer
|
75
85
|
loop do
|
76
86
|
act_on_available_worker_messages(0.3)
|
77
87
|
reap_stalled_workers
|
@@ -104,11 +114,23 @@ module RSpec::MultiprocessRunner
|
|
104
114
|
end
|
105
115
|
|
106
116
|
def work_left_to_do?
|
107
|
-
|
117
|
+
@file_buffer.any?
|
118
|
+
end
|
119
|
+
|
120
|
+
def add_file_to_buffer
|
121
|
+
file = @file_coordinator.get_file
|
122
|
+
@file_buffer << file if file
|
123
|
+
end
|
124
|
+
|
125
|
+
def get_file
|
126
|
+
if work_left_to_do?
|
127
|
+
add_file_to_buffer
|
128
|
+
@file_buffer.shift
|
129
|
+
end
|
108
130
|
end
|
109
131
|
|
110
132
|
def failed_workers
|
111
|
-
@
|
133
|
+
@file_coordinator.failed_workers
|
112
134
|
end
|
113
135
|
|
114
136
|
def act_on_available_worker_messages(timeout)
|
@@ -120,12 +142,19 @@ module RSpec::MultiprocessRunner
|
|
120
142
|
if worker_status == :dead
|
121
143
|
reap_one_worker(ready_worker, "died")
|
122
144
|
elsif work_left_to_do? && !ready_worker.working?
|
123
|
-
ready_worker.run_file(
|
145
|
+
ready_worker.run_file(get_file)
|
146
|
+
send_results(ready_worker)
|
124
147
|
end
|
125
148
|
end
|
126
149
|
end
|
127
150
|
end
|
128
151
|
|
152
|
+
def send_results(worker)
|
153
|
+
results_to_send = worker.example_results - @worker_results
|
154
|
+
@worker_results += results_to_send
|
155
|
+
@file_coordinator.send_results(results_to_send)
|
156
|
+
end
|
157
|
+
|
129
158
|
def reap_one_worker(worker, reason)
|
130
159
|
worker.reap
|
131
160
|
worker.deactivation_reason = reason
|
@@ -135,6 +164,8 @@ module RSpec::MultiprocessRunner
|
|
135
164
|
def mark_worker_as_stopped(worker)
|
136
165
|
@stopped_workers << worker
|
137
166
|
@workers.reject! { |w| w == worker }
|
167
|
+
send_results(worker)
|
168
|
+
@file_coordinator.send_worker_status(worker) if worker.deactivation_reason
|
138
169
|
end
|
139
170
|
|
140
171
|
def reap_stalled_workers
|
@@ -159,7 +190,8 @@ module RSpec::MultiprocessRunner
|
|
159
190
|
)
|
160
191
|
@workers << new_worker
|
161
192
|
new_worker.start
|
162
|
-
|
193
|
+
file = get_file
|
194
|
+
new_worker.run_file(file)
|
163
195
|
end
|
164
196
|
end
|
165
197
|
|
@@ -182,14 +214,15 @@ module RSpec::MultiprocessRunner
|
|
182
214
|
|
183
215
|
def print_summary
|
184
216
|
elapsed = Time.now - @start_time
|
185
|
-
by_status_and_time = combine_example_results.each_with_object({}) do |result, idx|
|
186
|
-
|
217
|
+
by_status_and_time = combine_example_results.each_with_object(Hash.new { |h, k| h[k] = [] }) do |result, idx|
|
218
|
+
idx[result.status] << result
|
187
219
|
end
|
188
220
|
|
189
221
|
print_skipped_files_details
|
190
222
|
print_pending_example_details(by_status_and_time["pending"])
|
191
223
|
print_failed_example_details(by_status_and_time["failed"])
|
192
|
-
|
224
|
+
print_missing_files
|
225
|
+
log_failed_files(by_status_and_time["failed"].map(&:file_path).uniq + @file_coordinator.missing_files.to_a) if @log_failing_files
|
193
226
|
print_failed_process_details
|
194
227
|
puts
|
195
228
|
print_elapsed_time(elapsed)
|
@@ -198,18 +231,18 @@ module RSpec::MultiprocessRunner
|
|
198
231
|
end
|
199
232
|
|
200
233
|
def combine_example_results
|
201
|
-
|
234
|
+
@file_coordinator.results.sort_by { |r| r.time_finished }
|
202
235
|
end
|
203
236
|
|
204
237
|
def any_example_failed?
|
205
|
-
|
238
|
+
@file_coordinator.results.detect { |r| r.status == "failed" }
|
206
239
|
end
|
207
240
|
|
208
241
|
def print_skipped_files_details
|
209
|
-
return if
|
242
|
+
return if !work_left_to_do?
|
210
243
|
puts
|
211
244
|
puts "Skipped files:"
|
212
|
-
@
|
245
|
+
@file_coordinator.remaining_files.each do |spec_file|
|
213
246
|
puts " - #{spec_file}"
|
214
247
|
end
|
215
248
|
end
|
@@ -235,19 +268,13 @@ module RSpec::MultiprocessRunner
|
|
235
268
|
end
|
236
269
|
end
|
237
270
|
|
238
|
-
def log_failed_files(
|
239
|
-
return if
|
240
|
-
|
241
|
-
failing_files = Hash.new { |h, k| h[k] = 0 }
|
242
|
-
failed_example_results.each do |failure|
|
243
|
-
failing_files[failure.file_path] += 1
|
244
|
-
end
|
245
|
-
|
271
|
+
def log_failed_files(failed_files)
|
272
|
+
return if failed_files.nil?
|
246
273
|
puts
|
247
274
|
puts "Writing failures to file: #{@log_failing_files}"
|
248
275
|
File.open(@log_failing_files, "w+") do |io|
|
249
|
-
|
250
|
-
io <<
|
276
|
+
failed_files.each do |file|
|
277
|
+
io << file
|
251
278
|
io << "\n"
|
252
279
|
end
|
253
280
|
end
|
@@ -262,8 +289,9 @@ module RSpec::MultiprocessRunner
|
|
262
289
|
example_count = by_status_and_time.map { |status, results| results.size }.inject(0) { |sum, ct| sum + ct }
|
263
290
|
failure_count = by_status_and_time["failed"] ? by_status_and_time["failed"].size : 0
|
264
291
|
pending_count = by_status_and_time["pending"] ? by_status_and_time["pending"].size : 0
|
292
|
+
missing_count = @file_coordinator.missing_files.size
|
265
293
|
process_failure_count = failed_workers.size
|
266
|
-
skipped_count = @
|
294
|
+
skipped_count = @file_coordinator.remaining_files.size
|
267
295
|
|
268
296
|
# Copied from RSpec
|
269
297
|
summary = pluralize(example_count, "example")
|
@@ -271,6 +299,7 @@ module RSpec::MultiprocessRunner
|
|
271
299
|
summary << ", #{pending_count} pending" if pending_count > 0
|
272
300
|
summary << ", " << pluralize(process_failure_count, "failed proc") if process_failure_count > 0
|
273
301
|
summary << ", " << pluralize(skipped_count, "skipped file") if skipped_count > 0
|
302
|
+
summary << ", " << pluralize(missing_count, "missing file") if missing_count > 0
|
274
303
|
puts summary
|
275
304
|
end
|
276
305
|
|
@@ -279,10 +308,17 @@ module RSpec::MultiprocessRunner
|
|
279
308
|
puts
|
280
309
|
puts "Failed processes:"
|
281
310
|
failed_workers.each do |worker|
|
282
|
-
puts " - #{worker.pid} (env #{worker.environment_number}) #{worker.deactivation_reason} on #{worker.current_file}"
|
311
|
+
puts " - #{worker.node}:#{worker.pid} (env #{worker.environment_number}) #{worker.deactivation_reason} on #{worker.current_file}"
|
283
312
|
end
|
284
313
|
end
|
285
314
|
|
315
|
+
def print_missing_files
|
316
|
+
return if @file_coordinator.missing_files.empty?
|
317
|
+
puts
|
318
|
+
puts "Missing files from disconnects:"
|
319
|
+
@file_coordinator.missing_files.each { |file| puts " + #{file} was given to a node, which disconnected" }
|
320
|
+
end
|
321
|
+
|
286
322
|
def print_elapsed_time(seconds_elapsed)
|
287
323
|
minutes = seconds_elapsed.to_i / 60
|
288
324
|
seconds = seconds_elapsed % 60
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'rspec/multiprocess_runner'
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
module RSpec::MultiprocessRunner
|
6
|
+
class FileCoordinator
|
7
|
+
attr_reader :results, :failed_workers
|
8
|
+
|
9
|
+
COMMAND_FILE = "file"
|
10
|
+
COMMAND_RESULTS = "results"
|
11
|
+
COMMAND_PROCESS = "process"
|
12
|
+
COMMAND_FINISHED = "finished"
|
13
|
+
COMMAND_START = "start"
|
14
|
+
|
15
|
+
def initialize(files, options={})
|
16
|
+
@spec_files = []
|
17
|
+
@results = Set.new
|
18
|
+
@threads = []
|
19
|
+
@failed_workers = []
|
20
|
+
@spec_files_reference = files.to_set
|
21
|
+
@hostname = options[:hostname]
|
22
|
+
@port = options[:port]
|
23
|
+
@max_threads = options[:max_nodes]
|
24
|
+
@head_node = options[:head_node]
|
25
|
+
if @head_node
|
26
|
+
@spec_files = options[:use_given_order] ? files : sort_files(files)
|
27
|
+
Thread.start { run_tcp_server }
|
28
|
+
@node_socket, head_node_socket = Socket.pair(:UNIX, :STREAM)
|
29
|
+
Thread.start { server_connection_established(head_node_socket) }
|
30
|
+
else
|
31
|
+
count = 100
|
32
|
+
while @node_socket.nil? do
|
33
|
+
begin
|
34
|
+
@node_socket = TCPSocket.new @hostname, @port
|
35
|
+
raise unless start?
|
36
|
+
rescue
|
37
|
+
@node_socket = nil
|
38
|
+
raise if count < 0
|
39
|
+
count -= 1
|
40
|
+
sleep(6)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
puts
|
44
|
+
end
|
45
|
+
ObjectSpace.define_finalizer( self, proc { @node_socket.close } )
|
46
|
+
end
|
47
|
+
|
48
|
+
def remaining_files
|
49
|
+
@spec_files
|
50
|
+
end
|
51
|
+
|
52
|
+
def missing_files
|
53
|
+
if @head_node
|
54
|
+
@spec_files_reference - @results.map(&:file_path) - @failed_workers.map(&:current_file)
|
55
|
+
else
|
56
|
+
[]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def get_file
|
61
|
+
begin
|
62
|
+
@node_socket.puts [COMMAND_FILE].to_json
|
63
|
+
file = @node_socket.gets.chomp
|
64
|
+
if @spec_files_reference.include? file
|
65
|
+
return file
|
66
|
+
else
|
67
|
+
return nil # Malformed response, assume done, cease function
|
68
|
+
end
|
69
|
+
rescue
|
70
|
+
return nil # If Error, assume done, cease function
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def send_results(results)
|
75
|
+
@node_socket.puts [COMMAND_RESULTS, results].to_json
|
76
|
+
end
|
77
|
+
|
78
|
+
def send_worker_status(worker)
|
79
|
+
@node_socket.puts [COMMAND_PROCESS, worker, Socket.gethostname].to_json
|
80
|
+
end
|
81
|
+
|
82
|
+
def finished
|
83
|
+
if @head_node
|
84
|
+
@tcp_server_running = false
|
85
|
+
@threads.each(&:join)
|
86
|
+
@spec_files += missing_files.to_a
|
87
|
+
else
|
88
|
+
@node_socket.puts [COMMAND_FINISHED].to_json
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
# Sorting by decreasing size attempts to ensure we don't send the slowest
|
95
|
+
# file to a worker right before all the other workers finish and then end up
|
96
|
+
# waiting for that one process to finish.
|
97
|
+
# In the future it would be nice to log execution time and sort by that.
|
98
|
+
def sort_files(files)
|
99
|
+
# #sort_by caches the File.size result so we only call it once per file.
|
100
|
+
files.sort_by { |file| -File.size(file) }
|
101
|
+
end
|
102
|
+
|
103
|
+
def run_tcp_server
|
104
|
+
server = TCPServer.new @port
|
105
|
+
@tcp_server_running = true
|
106
|
+
ObjectSpace.define_finalizer( self, proc { server.close } )
|
107
|
+
while @threads.size < @max_threads && @tcp_server_running
|
108
|
+
@threads << Thread.start(server.accept) do |client|
|
109
|
+
server_connection_established(client) if @tcp_server_running
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def server_connection_established(socket)
|
115
|
+
loop do
|
116
|
+
raw_response = socket.gets
|
117
|
+
break unless raw_response
|
118
|
+
command, results, node = JSON.parse(raw_response)
|
119
|
+
if command == COMMAND_START
|
120
|
+
socket.puts COMMAND_START
|
121
|
+
elsif command == COMMAND_FILE
|
122
|
+
socket.puts @spec_files.shift
|
123
|
+
elsif command == COMMAND_PROCESS && results
|
124
|
+
@failed_workers << MockWorker.from_json_parse(results, node || "unknown")
|
125
|
+
elsif command == COMMAND_RESULTS && results = results.map { |result|
|
126
|
+
ExampleResult.from_json_parse(result) }
|
127
|
+
@results += results
|
128
|
+
elsif command == COMMAND_FINISHED
|
129
|
+
break
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def work_left_to_do?
|
135
|
+
!@spec_files.empty?
|
136
|
+
end
|
137
|
+
|
138
|
+
def start?
|
139
|
+
begin
|
140
|
+
@node_socket.puts [COMMAND_START].to_json
|
141
|
+
response = @node_socket.gets
|
142
|
+
response && response.chomp == COMMAND_START
|
143
|
+
rescue Errno::EPIPE
|
144
|
+
false
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -65,6 +65,18 @@ module RSpec::MultiprocessRunner
|
|
65
65
|
# Command line options to pass to the RSpec workers. Defaults to `nil`.
|
66
66
|
attr_accessor :rspec_opts
|
67
67
|
|
68
|
+
# Port to use for TCP communication. Defaults to `2222`.
|
69
|
+
attr_accessor :port
|
70
|
+
|
71
|
+
# Be a node to a head node at hostname. Defaults to `false`
|
72
|
+
attr_accessor :node
|
73
|
+
|
74
|
+
# Hostname of head node. Defaults to `localhost`
|
75
|
+
attr_accessor :hostname
|
76
|
+
|
77
|
+
# Max number of connections to head_node. Defaults to `5`
|
78
|
+
attr_accessor :max_nodes
|
79
|
+
|
68
80
|
def initialize(*args, &task_block)
|
69
81
|
@name = args.shift || :multispec
|
70
82
|
@verbose = true
|
@@ -126,6 +138,18 @@ module RSpec::MultiprocessRunner
|
|
126
138
|
if use_given_order
|
127
139
|
cmd_parts << '--use-given-order'
|
128
140
|
end
|
141
|
+
if port
|
142
|
+
cmd_parts << '--port' << port.to_s
|
143
|
+
end
|
144
|
+
if node
|
145
|
+
cmd_parts << '--node'
|
146
|
+
end
|
147
|
+
if hostname
|
148
|
+
cmd_parts << '--hostname' << hostname
|
149
|
+
end
|
150
|
+
if max_nodes
|
151
|
+
cmd_parts << '--max-nodes' << max_nodes.to_s
|
152
|
+
end
|
129
153
|
if files_or_directories
|
130
154
|
cmd_parts.concat(files_or_directories)
|
131
155
|
end
|
@@ -146,6 +146,10 @@ module RSpec::MultiprocessRunner
|
|
146
146
|
act_on_message_from_worker(receive_message_from_worker)
|
147
147
|
end
|
148
148
|
|
149
|
+
def to_json(options = nil)
|
150
|
+
{ "pid" => @pid, "environment_number" => @environment_number, "current_file" => @current_file, "deactivation_reason" => @deactivation_reason }.to_json
|
151
|
+
end
|
152
|
+
|
149
153
|
private
|
150
154
|
|
151
155
|
def terminate_then_kill(timeout, message=nil)
|
@@ -300,16 +304,41 @@ module RSpec::MultiprocessRunner
|
|
300
304
|
end
|
301
305
|
end
|
302
306
|
|
307
|
+
class MockWorker
|
308
|
+
attr_reader :pid, :environment_number, :current_file, :deactivation_reason, :node
|
309
|
+
|
310
|
+
def initialize(hash, node)
|
311
|
+
@pid = hash["pid"]
|
312
|
+
@environment_number = hash["environment_number"]
|
313
|
+
@current_file = hash["current_file"]
|
314
|
+
@deactivation_reason = hash["deactivation_reason"]
|
315
|
+
@node = node
|
316
|
+
end
|
317
|
+
|
318
|
+
def self.from_json_parse(hash, node)
|
319
|
+
MockWorker.new(hash, node)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
303
323
|
# @private
|
304
324
|
class ExampleResult
|
305
325
|
attr_reader :status, :description, :details, :file_path, :time_finished
|
306
326
|
|
307
|
-
def initialize(example_complete_message)
|
327
|
+
def initialize(example_complete_message, time = Time.now)
|
328
|
+
@hash = example_complete_message
|
308
329
|
@status = example_complete_message["example_status"]
|
309
330
|
@description = example_complete_message["description"]
|
310
331
|
@details = example_complete_message["details"]
|
311
332
|
@file_path = example_complete_message["file_path"]
|
312
|
-
@time_finished =
|
333
|
+
@time_finished = time
|
334
|
+
end
|
335
|
+
|
336
|
+
def to_json(options = nil)
|
337
|
+
{ hash: @hash, time: @time_finished.iso8601(9) }.to_json
|
338
|
+
end
|
339
|
+
|
340
|
+
def self.from_json_parse(hash)
|
341
|
+
ExampleResult.new(hash["hash"], Time.iso8601(hash["time"]))
|
313
342
|
end
|
314
343
|
end
|
315
344
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rspec-multiprocess_runner
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rhett Sutphin
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-10-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -105,6 +105,7 @@ files:
|
|
105
105
|
- lib/rspec/multiprocess_runner.rb
|
106
106
|
- lib/rspec/multiprocess_runner/command_line_options.rb
|
107
107
|
- lib/rspec/multiprocess_runner/coordinator.rb
|
108
|
+
- lib/rspec/multiprocess_runner/file_coordinator.rb
|
108
109
|
- lib/rspec/multiprocess_runner/rake_task.rb
|
109
110
|
- lib/rspec/multiprocess_runner/reporting_formatter.rb
|
110
111
|
- lib/rspec/multiprocess_runner/version.rb
|