parallel_cucumber 0.2.0.pre.36 → 0.2.3

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.
@@ -1,3 +1,3 @@
1
1
  module ParallelCucumber
2
- VERSION = '0.2.0.pre.36'.freeze
2
+ VERSION = '0.2.3'.freeze
3
3
  end
@@ -1,5 +1,6 @@
1
1
  require 'English'
2
2
  require 'timeout'
3
+ require 'tmpdir' # I loathe Ruby.
3
4
 
4
5
  module ParallelCucumber
5
6
  class Tracker
@@ -22,15 +23,17 @@ module ParallelCucumber
22
23
  class Worker
23
24
  include ParallelCucumber::Helper::Utils
24
25
 
25
- def initialize(options, index)
26
+ def initialize(options, index, stdout_logger)
26
27
  @batch_size = options[:batch_size]
28
+ @group_by = options[:group_by]
27
29
  @batch_timeout = options[:batch_timeout]
30
+ @batch_error_timeout = options[:batch_error_timeout]
31
+ @precheck_timeout = options[:precheck_timeout]
28
32
  @setup_timeout = options[:setup_timeout]
29
33
  @cucumber_options = options[:cucumber_options]
30
34
  @test_command = options[:test_command]
31
35
  @pre_check = options[:pre_check]
32
- @pretty = options[:pretty]
33
- @env_variables = options[:env_variables]
36
+ @on_batch_error = options[:on_batch_error]
34
37
  @index = index
35
38
  @queue_connection_params = options[:queue_connection_params]
36
39
  @setup_worker = options[:setup_worker]
@@ -40,19 +43,43 @@ module ParallelCucumber
40
43
  @log_decoration = options[:log_decoration]
41
44
  @log_dir = options[:log_dir]
42
45
  @log_file = "#{@log_dir}/worker_#{index}.log"
46
+ @stdout_logger = stdout_logger # .sync writes only.
47
+ end
48
+
49
+ def autoshutting_file
50
+ file_handle = { log_file: @log_file }
51
+
52
+ def file_handle.write(message)
53
+ File.open(self[:log_file], 'a') { |f| f << message }
54
+ rescue => e
55
+ STDERR.puts "Log failure: #{e} writing '#{message.to_s.chomp}' to #{self[:log_file]}"
56
+ end
57
+
58
+ def file_handle.close
59
+ end
60
+
61
+ def file_handle.fsync
62
+ end
63
+
64
+ def file_handle.path
65
+ self[:log_file]
66
+ end
67
+
68
+ file_handle
43
69
  end
44
70
 
45
71
  def start(env)
46
72
  env = env.dup.merge!('WORKER_LOG' => @log_file)
47
73
 
48
74
  File.delete(@log_file) if File.exist?(@log_file)
49
- File.open(@log_file, 'a') do |file_handle|
50
- file_handle.sync = true
51
- @logger = ParallelCucumber::CustomLogger.new(MultiDelegator.delegate(:write, :close).to(STDOUT, file_handle))
52
- @logger.progname = "Worker-#{@index}"
53
- @logger.level = @debug ? ParallelCucumber::CustomLogger::DEBUG : ParallelCucumber::CustomLogger::INFO
54
75
 
55
- @logger.info("Starting, also logging to #{@log_file}")
76
+ @logger = ParallelCucumber::CustomLogger.new(autoshutting_file)
77
+ @logger.progname = "Worker-#{@index}"
78
+ @logger.level = @debug ? ParallelCucumber::CustomLogger::DEBUG : ParallelCucumber::CustomLogger::INFO
79
+
80
+ results = {}
81
+ begin
82
+ @logger.info("Logging to #{@log_file}")
56
83
 
57
84
  unless @worker_delay.zero?
58
85
  @logger.info("Waiting #{@worker_delay * @index} seconds before start")
@@ -62,39 +89,51 @@ module ParallelCucumber
62
89
  @logger.debug(<<-LOG)
63
90
  Additional environment variables: #{env.map { |k, v| "#{k}=#{v}" }.join(' ')}
64
91
  LOG
92
+ @logger.update_into(@stdout_logger)
65
93
 
66
- results = {}
67
- running_total = Hash.new(0)
94
+ # TODO: Replace running total with queues for passed, failed, unknown, skipped.
95
+ running_total = Hash.new(0) # Default new keys to 0
96
+ running_total[:group] = env[@group_by] if @group_by
68
97
  begin
69
98
  setup(env)
70
99
 
71
100
  queue = ParallelCucumber::Helper::Queue.new(@queue_connection_params)
101
+ directed_queue = ParallelCucumber::Helper::Queue.new(@queue_connection_params, "_#{@index}")
72
102
  queue_tracker = Tracker.new(queue)
73
103
 
74
104
  loop_mm, loop_ss = time_it do
75
105
  loop do
76
- break if queue.empty?
106
+ break if queue.empty? && directed_queue.empty?
77
107
  batch = []
78
- precheck(env)
108
+ precmd = precheck(env)
109
+ if (m = precmd.match(/precmd:retry-after-(\d+)-seconds/))
110
+ sleep(1 + m[1].to_i)
111
+ next
112
+ end
79
113
  @batch_size.times do
80
- # TODO: Handle recovery of dequeued tests, if a worker dies mid-processing.
81
- batch << queue.dequeue
114
+ # TODO: Handle recovery of possibly toxic dequeued undirected tests if a worker dies mid-processing.
115
+ batch << (directed_queue.empty? ? queue : directed_queue).dequeue
82
116
  end
83
117
  batch.compact!
84
- batch.sort!
118
+ batch.sort! # Workaround for https://github.com/cucumber/cucumber-ruby/issues/952
85
119
  break if batch.empty?
86
120
 
87
121
  run_batch(env, queue_tracker, results, running_total, batch)
88
122
  end
89
123
  end
90
124
  @logger.debug("Loop took #{loop_mm} minutes #{loop_ss} seconds")
125
+ @logger.update_into(@stdout_logger)
126
+ rescue => e
127
+ trace = e.backtrace.join("\n\t").sub("\n\t", ": #{$ERROR_INFO}#{e.class ? " (#{e.class})" : ''}\n\t")
128
+ @logger.error("Threw: #{e.inspect} #{trace}")
91
129
  ensure
92
- teardown(env)
93
-
94
130
  results[":worker-#{@index}"] = running_total
95
- results
131
+ teardown(env)
96
132
  end
133
+ ensure
134
+ @logger.update_into(@stdout_logger)
97
135
  end
136
+ results
98
137
  end
99
138
 
100
139
  def run_batch(env, queue_tracker, results, running_total, tests)
@@ -104,24 +143,56 @@ module ParallelCucumber
104
143
 
105
144
  batch_mm, batch_ss = time_it do
106
145
  batch_results = test_batch(batch_id, env, running_total, tests)
107
-
146
+ begin
147
+ Hooks.fire_after_batch_hooks(batch_results, batch_id, env)
148
+ rescue => e
149
+ trace = e.backtrace.join("\n\t")
150
+ @logger.warn("There was exception in after_batch hook #{e.message} \n #{trace}")
151
+ end
108
152
  process_results(batch_results, tests)
109
-
110
153
  running_totals(batch_results, running_total)
111
154
  results.merge!(batch_results)
112
155
  end
113
-
156
+ ensure
114
157
  @logger.debug("Batch #{batch_id} took #{batch_mm} minutes #{batch_ss} seconds")
158
+ @logger.update_into(@stdout_logger)
115
159
  end
116
160
 
117
161
  def precheck(env)
118
- return unless @pre_check
119
- continue = Helper::Command.exec_command(
120
- env, 'precheck', @pre_check, @log_file, @logger, @log_decoration, @batch_timeout
121
- )
122
- return if continue
123
- @logger.error('Pre-check failed: quitting immediately')
124
- raise :prechek_failed
162
+ return 'default no-op pre_check' unless @pre_check
163
+ begin
164
+ return Helper::Command.exec_command(
165
+ env, 'precheck', @pre_check, @logger, @log_decoration, timeout: @precheck_timeout, capture: true
166
+ )
167
+ rescue
168
+ @logger.error('Pre-check failed: quitting immediately')
169
+ raise 'Pre-check failed: quitting immediately'
170
+ end
171
+ end
172
+
173
+ def on_batch_error(batch_env, batch_id, error_file, tests, error)
174
+ return unless @on_batch_error
175
+
176
+ begin
177
+ error_info = {
178
+ class: error.class,
179
+ message: error.message,
180
+ backtrace: error.backtrace
181
+ }
182
+ batch_error_info = {
183
+ batch_id: batch_id,
184
+ tests: tests,
185
+ error: error_info
186
+ }
187
+ File.write(error_file, batch_error_info.to_json)
188
+ command = "#{@on_batch_error} #{error_file}"
189
+ Helper::Command.exec_command(
190
+ batch_env, 'on_batch_error', command, @logger, @log_decoration, timeout: @batch_error_timeout
191
+ )
192
+ rescue => e
193
+ message = "on-batch-error failed: #{e.message}"
194
+ @logger.warn(message)
195
+ end
125
196
  end
126
197
 
127
198
  def running_totals(batch_results, running_total)
@@ -134,7 +205,7 @@ module ParallelCucumber
134
205
  running_total[s] += tt.count unless tt.empty?
135
206
  end
136
207
  running_total[:batches] += 1
137
- @logger.info(running_total.sort.to_s)
208
+ @logger.info(running_total.sort.to_s + ' t=' + Time.now.to_s)
138
209
  end
139
210
 
140
211
  def process_results(batch_results, tests)
@@ -152,12 +223,14 @@ module ParallelCucumber
152
223
  end
153
224
 
154
225
  def test_batch(batch_id, env, running_total, tests)
155
- test_batch_dir = "/tmp/w-#{batch_id}"
226
+ # Prefer /tmp to Mac's brain-dead /var/folders/y8/8kqjszcs2slchjx2z5lrw2t80000gp/T/w-1497514590-0 nonsense
227
+ prefer_tmp = ENV.fetch('PREFER_TMP', Dir.tmpdir)
228
+ test_batch_dir = "#{Dir.exist?(prefer_tmp) ? prefer_tmp : Dir.tmpdir}/w-#{batch_id}"
156
229
  FileUtils.rm_rf(test_batch_dir)
157
230
  FileUtils.mkpath(test_batch_dir)
158
231
 
159
232
  test_state = "#{test_batch_dir}/test_state.json"
160
- cmd = "#{@test_command} #{@pretty} --format json --out #{test_state} #{@cucumber_options} "
233
+ cmd = "#{@test_command} --format json --out #{test_state} #{@cucumber_options} "
161
234
  batch_env = {
162
235
  :TEST_BATCH_ID.to_s => batch_id,
163
236
  :TEST_BATCH_DIR.to_s => test_batch_dir,
@@ -166,74 +239,86 @@ module ParallelCucumber
166
239
  mapped_batch_cmd, file_map = Helper::Cucumber.batch_mapped_files(cmd, test_batch_dir, batch_env)
167
240
  file_map.each { |_user, worker| FileUtils.mkpath(worker) if worker =~ %r{\/$} }
168
241
  mapped_batch_cmd += ' ' + tests.join(' ')
169
- res = ParallelCucumber::Helper::Command.exec_command(
170
- batch_env, 'batch', mapped_batch_cmd, @log_file, @logger, @log_decoration, @batch_timeout
171
- )
172
- batch_results = if res.nil?
173
- {}
174
- else
175
- Helper::Command.wrap_block(@log_decoration, 'file copy', @logger) do
176
- # Use system cp -r because Ruby's has crap diagnostics in weird situations.
177
- # Copy files we might have renamed or moved
178
- file_map.each do |user, worker|
179
- unless worker == user
180
- cp_out = `cp -Rv #{worker} #{user} 2>&1`
181
- @logger.debug("Copy of #{worker} to #{user} said: #{cp_out}")
182
- end
183
- end
184
- # Copy everything else too, in case it's interesting.
185
- cp_out = `cp -Rv #{test_batch_dir}/* #{@log_dir} 2>&1`
186
- @logger.debug("Copy of #{test_batch_dir}/* to #{@log_dir} said: #{cp_out}")
187
- parse_results(test_state)
188
- end
189
- end
242
+ begin
243
+ ParallelCucumber::Helper::Command.exec_command(
244
+ batch_env, 'batch', mapped_batch_cmd, @logger, @log_decoration,
245
+ timeout: @batch_timeout, return_script_error: true
246
+ )
247
+ rescue => e
248
+ @logger << "ERROR #{e} #{e.backtrace.first(5)}"
249
+ error_file = "#{test_batch_dir}/error.json"
250
+ on_batch_error(batch_env, batch_id, error_file, tests, e)
251
+ return { script_failure: 1 }
252
+ end
253
+ parse_results(test_state)
190
254
  ensure
255
+ Helper::Command.wrap_block(@log_decoration, "file copy #{Time.now}", @logger) do
256
+ # Copy files we might have renamed or moved
257
+ file_map.each do |user, worker|
258
+ next if worker == user
259
+ Helper::Processes.cp_rv(worker, user, @logger)
260
+ end
261
+ @logger << "\nCopied files in map: #{file_map.first(5)}...#{file_map.count} #{Time.now}\n"
262
+ # Copy everything else too, in case it's interesting.
263
+ Helper::Processes.cp_rv("#{test_batch_dir}/*", @log_dir, @logger)
264
+ @logger << "\nCopied everything else #{Time.now} #{Time.now}\n"
265
+ end
266
+ @logger.update_into(@stdout_logger)
191
267
  FileUtils.rm_rf(test_batch_dir)
192
- batch_results
268
+ @logger << "\nRemoved all files #{Time.now}\n" # Tracking down 30 minute pause!
269
+ @logger.update_into(@stdout_logger)
193
270
  end
194
271
 
195
272
  def teardown(env)
196
273
  return unless @teardown_worker
197
274
  mm, ss = time_it do
198
- @logger.info('Teardown running')
199
- success = Helper::Command.exec_command(
200
- env, 'teardown', @teardown_worker, @log_file, @logger, @log_decoration
201
- )
202
- @logger.warn('Teardown finished with error') unless success
275
+ @logger.info("\nTeardown running at #{Time.now}\n")
276
+
277
+ begin
278
+ Helper::Command.exec_command(
279
+ env, 'teardown', @teardown_worker, @logger, @log_decoration, timeout: @setup_timeout
280
+ )
281
+ rescue
282
+ @logger.warn('Teardown finished with error')
283
+ end
203
284
  end
285
+ ensure
204
286
  @logger.debug("Teardown took #{mm} minutes #{ss} seconds")
287
+ @logger.update_into(@stdout_logger)
205
288
  end
206
289
 
207
290
  def setup(env)
208
291
  return unless @setup_worker
209
292
  mm, ss = time_it do
210
293
  @logger.info('Setup running')
211
- success = Helper::Command.exec_command(
212
- env, 'setup', @setup_worker, @log_file, @logger, @log_decoration, @setup_timeout
213
- )
214
- unless success
215
- @logger.warn('Setup failed: quitting immediately')
216
- raise :setup_failed
294
+
295
+ begin
296
+ Helper::Command.exec_command(env, 'setup', @setup_worker, @logger, @log_decoration, timeout: @setup_timeout)
297
+ rescue
298
+ @logger.warn("Setup failed: #{@index} quitting immediately")
299
+ raise 'Setup failed: quitting immediately'
217
300
  end
218
301
  end
302
+ ensure
219
303
  @logger.debug("Setup took #{mm} minutes #{ss} seconds")
304
+ @logger.update_into(@stdout_logger)
220
305
  end
221
306
 
222
307
  def parse_results(f)
223
308
  unless File.file?(f)
224
309
  @logger.error("Results file does not exist: #{f}")
225
- return {}
310
+ return { results_missing: 1 }
226
311
  end
227
312
  json_report = File.read(f)
228
313
  if json_report.empty?
229
314
  @logger.error("Results file is empty: #{f}")
230
- return {}
315
+ return { results_empty: 1 }
231
316
  end
232
317
  Helper::Cucumber.parse_json_report(json_report)
233
318
  rescue => e
234
319
  trace = e.backtrace.join("\n\t").sub("\n\t", ": #{$ERROR_INFO}#{e.class ? " (#{e.class})" : ''}\n\t")
235
320
  @logger.error("Threw: JSON parse of results caused #{trace}")
236
- {}
321
+ { json_fail: 1 }
237
322
  end
238
323
  end
239
324
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parallel_cucumber
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0.pre.36
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Bayandin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-02-28 00:00:00.000000000 Z
11
+ date: 2018-03-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cucumber
@@ -58,28 +58,28 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '0.31'
61
+ version: '0.41'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '0.31'
68
+ version: '0.41'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rubocop
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '0.43'
75
+ version: '0.51'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '0.43'
82
+ version: '0.51'
83
83
  description: Our own parallel cucumber with queue and workers
84
84
  email: a.bayandin@gmail.com
85
85
  executables:
@@ -91,6 +91,7 @@ files:
91
91
  - bin/parallel_cucumber
92
92
  - lib/parallel_cucumber.rb
93
93
  - lib/parallel_cucumber/cli.rb
94
+ - lib/parallel_cucumber/dsl.rb
94
95
  - lib/parallel_cucumber/helper/command.rb
95
96
  - lib/parallel_cucumber/helper/cucumber.rb
96
97
  - lib/parallel_cucumber/helper/multi_delegator.rb
@@ -98,6 +99,7 @@ files:
98
99
  - lib/parallel_cucumber/helper/queue.rb
99
100
  - lib/parallel_cucumber/helper/unittest/cucumber_test.rb
100
101
  - lib/parallel_cucumber/helper/utils.rb
102
+ - lib/parallel_cucumber/hooks.rb
101
103
  - lib/parallel_cucumber/logger.rb
102
104
  - lib/parallel_cucumber/main.rb
103
105
  - lib/parallel_cucumber/status.rb
@@ -118,14 +120,13 @@ required_ruby_version: !ruby/object:Gem::Requirement
118
120
  version: '0'
119
121
  required_rubygems_version: !ruby/object:Gem::Requirement
120
122
  requirements:
121
- - - ">"
123
+ - - ">="
122
124
  - !ruby/object:Gem::Version
123
- version: 1.3.1
125
+ version: '0'
124
126
  requirements: []
125
127
  rubyforge_project:
126
- rubygems_version: 2.6.6
128
+ rubygems_version: 2.6.14
127
129
  signing_key:
128
130
  specification_version: 4
129
131
  summary: Run cucumber in parallel
130
132
  test_files: []
131
- has_rdoc: