rspec-multiprocess_runner 1.1.0 → 1.2.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.
- 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
|