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.
- checksums.yaml +4 -4
- data/README.md +14 -0
- data/lib/parallel_cucumber.rb +1 -0
- data/lib/parallel_cucumber/cli.rb +57 -13
- data/lib/parallel_cucumber/dsl.rb +18 -0
- data/lib/parallel_cucumber/helper/command.rb +147 -56
- data/lib/parallel_cucumber/helper/cucumber.rb +28 -22
- data/lib/parallel_cucumber/helper/processes.rb +53 -17
- data/lib/parallel_cucumber/helper/queue.rb +3 -3
- data/lib/parallel_cucumber/hooks.rb +18 -0
- data/lib/parallel_cucumber/logger.rb +38 -0
- data/lib/parallel_cucumber/main.rb +122 -54
- data/lib/parallel_cucumber/version.rb +1 -1
- data/lib/parallel_cucumber/worker.rb +153 -68
- metadata +11 -10
@@ -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
|
-
@
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
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
|
-
|
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}
|
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
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
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
|
-
|
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(
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
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
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
@logger.warn(
|
216
|
-
raise :
|
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.
|
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:
|
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.
|
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.
|
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.
|
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.
|
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:
|
125
|
+
version: '0'
|
124
126
|
requirements: []
|
125
127
|
rubyforge_project:
|
126
|
-
rubygems_version: 2.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:
|