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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f8fb56cb7c08de0d53e8894ba0a8354dde4af6a5
4
- data.tar.gz: 3012c5d9e218b278b2a11385de30cd38e50a970a
3
+ metadata.gz: da6e498842698e2a4d310674c27cace4b39ca99d
4
+ data.tar.gz: dbc698c179206e1233bdb6448bc01611ca5aa036
5
5
  SHA512:
6
- metadata.gz: 9cb30e3cd5b4dae9599e62e5fbc5c9d9cbf58987df4468fb30357faee82b7bc7bcfa1b72a810de11b9db2c501f92b6a2a9ccf54dfffcf018f23ca08f706a0e25
7
- data.tar.gz: 4d952d31b662bf8ef7e93ece5de3bfd171164298ffaca4b176d1f3b7ea954f4e2b205c962c2373de8560217c6e61c7b3a9878da7423ea5a348f13b44cab27911
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, potentially allowing substantial performance
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 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.
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
- @spec_files = options[:use_given_order] ? files : sort_files(files)
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 !@spec_files.empty?
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
- !@spec_files.empty?
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
- @stopped_workers.select { |w| w.deactivation_reason }
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(@spec_files.shift)
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
- new_worker.run_file(@spec_files.shift)
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
- (idx[result.status] ||= []) << result
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
- log_failed_files(by_status_and_time["failed"]) if @log_failing_files
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
- (@workers + @stopped_workers).flat_map(&:example_results).sort_by { |r| r.time_finished }
234
+ @file_coordinator.results.sort_by { |r| r.time_finished }
202
235
  end
203
236
 
204
237
  def any_example_failed?
205
- (@workers + @stopped_workers).detect { |w| w.example_results.detect { |r| r.status == "failed" } }
238
+ @file_coordinator.results.detect { |r| r.status == "failed" }
206
239
  end
207
240
 
208
241
  def print_skipped_files_details
209
- return if @spec_files.empty?
242
+ return if !work_left_to_do?
210
243
  puts
211
244
  puts "Skipped files:"
212
- @spec_files.each do |spec_file|
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(failed_example_results)
239
- return if failed_example_results.nil?
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
- failing_files.each do |(k, _)|
250
- io << k
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 = @spec_files.size
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
@@ -1,5 +1,5 @@
1
1
  module RSpec
2
2
  module MultiprocessRunner
3
- VERSION = "1.1.0"
3
+ VERSION = "1.2.0"
4
4
  end
5
5
  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 = Time.now
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.1.0
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-09-08 00:00:00.000000000 Z
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