parallel_cucumber 0.2.0.pre.36 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: