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.
- 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:
|