parallel_cucumber 0.2.16 → 0.2.17
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/parallel_cucumber.rb +2 -0
- data/lib/parallel_cucumber/cli.rb +6 -1
- data/lib/parallel_cucumber/job.rb +13 -0
- data/lib/parallel_cucumber/main.rb +7 -38
- data/lib/parallel_cucumber/version.rb +1 -1
- data/lib/parallel_cucumber/worker.rb +39 -42
- data/lib/parallel_cucumber/worker_manager.rb +152 -0
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5664f82c63144d99bec9dc1880905601d0de45b2431b929eee77c0c4abb4521f
|
4
|
+
data.tar.gz: 9c5fce5710b7a36270c5f84431bc32195bf54a37779380ea584e04f892b8e616
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c51f9ebe8c466e9e31936ddf59bc1944f5ddab128e317d4185b4047d7d7c2f63f091c242556ac2185bd67bbd3de7145fe31fbb3fc51239e56b1b046b3412087e
|
7
|
+
data.tar.gz: bac0d600b9e52aacc815a7ad2e3138470c5efafa2aad96d4c94ba82bae7d82df2bdd48ce21b2b78b09c98045f1f40e3e13b760ef371a8f16babd7bfdeef180c4
|
data/lib/parallel_cucumber.rb
CHANGED
@@ -7,8 +7,10 @@ require 'parallel_cucumber/helper/processes'
|
|
7
7
|
require 'parallel_cucumber/helper/queue'
|
8
8
|
require 'parallel_cucumber/helper/utils'
|
9
9
|
require 'parallel_cucumber/hooks'
|
10
|
+
require 'parallel_cucumber/job'
|
10
11
|
require 'parallel_cucumber/logger'
|
11
12
|
require 'parallel_cucumber/main'
|
12
13
|
require 'parallel_cucumber/status'
|
13
14
|
require 'parallel_cucumber/version'
|
15
|
+
require 'parallel_cucumber/worker_manager'
|
14
16
|
require 'parallel_cucumber/worker'
|
@@ -19,7 +19,8 @@ module ParallelCucumber
|
|
19
19
|
n: 0, # Default: computed from longest list in json parameters, minimum 1.
|
20
20
|
queue_connection_params: ['redis://127.0.0.1:6379', DateTime.now.strftime('queue-%Y%m%d%H%M%S')],
|
21
21
|
worker_delay: 0,
|
22
|
-
test_command: 'cucumber'
|
22
|
+
test_command: 'cucumber',
|
23
|
+
backup_worker_count: 0
|
23
24
|
}.freeze
|
24
25
|
|
25
26
|
def initialize(argv)
|
@@ -61,6 +62,10 @@ module ParallelCucumber
|
|
61
62
|
options[:n] = n
|
62
63
|
end
|
63
64
|
|
65
|
+
opts.on('--backup-worker-count BACKUP_WORKERS', Integer, 'How many free workers to hold before all tasks are done. Default is none') do |n|
|
66
|
+
options[:backup_worker_count] = n
|
67
|
+
end
|
68
|
+
|
64
69
|
opts.on('-o', '--cucumber-options "OPTIONS"', 'Run cucumber with these options') do |cucumber_options|
|
65
70
|
options[:cucumber_options] = cucumber_options
|
66
71
|
end
|
@@ -149,17 +149,11 @@ module ParallelCucumber
|
|
149
149
|
Helper::Command.wrap_block(@options[:log_decoration],
|
150
150
|
@options[:log_decoration]['worker_block'] || 'workers',
|
151
151
|
@logger) do
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
) do |index|
|
158
|
-
ParallelCucumber::Worker
|
159
|
-
.new(@options, index, @logger)
|
160
|
-
.start(env_for_worker(@options[:env_variables], index))
|
161
|
-
end
|
162
|
-
map.inject(:merge) # Returns hash of file:line to statuses + :worker-index to summary.
|
152
|
+
|
153
|
+
worker_manager = ParallelCucumber::WorkerManager.new(@options, @logger)
|
154
|
+
worker_manager.start(number_of_workers)
|
155
|
+
ensure
|
156
|
+
worker_manager.kill
|
163
157
|
end
|
164
158
|
end
|
165
159
|
|
@@ -169,11 +163,11 @@ module ParallelCucumber
|
|
169
163
|
@logger.info("Inferred worker count #{@options[:n]} from env_variables option")
|
170
164
|
end
|
171
165
|
|
172
|
-
number_of_workers = [@options[:n], count].min
|
166
|
+
number_of_workers = [@options[:n], [@options[:backup_worker_count], count].max].min
|
173
167
|
unless number_of_workers == @options[:n]
|
174
168
|
@logger.info(<<-LOG)
|
175
169
|
Number of workers was overridden to #{number_of_workers}.
|
176
|
-
More workers (#{@options[:n]}) requested than tests (#{count})".
|
170
|
+
More workers (#{@options[:n]}) requested than tests (#{count}). BackupWorkerCount: #{@options[:backup_worker_count]}".
|
177
171
|
LOG
|
178
172
|
end
|
179
173
|
|
@@ -192,30 +186,5 @@ module ParallelCucumber
|
|
192
186
|
end
|
193
187
|
number_of_workers
|
194
188
|
end
|
195
|
-
|
196
|
-
private
|
197
|
-
|
198
|
-
def env_for_worker(env_variables, worker_number)
|
199
|
-
env = env_variables.map do |k, v|
|
200
|
-
case v
|
201
|
-
when String, Numeric, TrueClass, FalseClass
|
202
|
-
[k, v]
|
203
|
-
when Array
|
204
|
-
[k, v[worker_number]]
|
205
|
-
when Hash
|
206
|
-
value = v[worker_number.to_s]
|
207
|
-
[k, value] unless value.nil?
|
208
|
-
when NilClass
|
209
|
-
else
|
210
|
-
raise("Don't know how to set '#{v}'<#{v.class}> to the environment variable '#{k}'")
|
211
|
-
end
|
212
|
-
end.compact.to_h
|
213
|
-
|
214
|
-
# Defaults, if absent in env. Shame 'merge' isn't something non-commutative like 'adopts/defaults'.
|
215
|
-
env = { TEST: 1, TEST_PROCESS_NUMBER: worker_number, WORKER_INDEX: worker_number }.merge(env)
|
216
|
-
|
217
|
-
# Overwrite this if it exists in env.
|
218
|
-
env.merge(PARALLEL_CUCUMBER_EXPORTS: env.keys.join(',')).map { |k, v| [k.to_s, v.to_s] }.to_h
|
219
|
-
end
|
220
189
|
end
|
221
190
|
end
|
@@ -3,28 +3,10 @@ require 'timeout'
|
|
3
3
|
require 'tmpdir' # I loathe Ruby.
|
4
4
|
|
5
5
|
module ParallelCucumber
|
6
|
-
class Tracker
|
7
|
-
def initialize(queue)
|
8
|
-
@queue = queue
|
9
|
-
end
|
10
|
-
|
11
|
-
def status
|
12
|
-
queue_length = @queue.length
|
13
|
-
now = Time.now
|
14
|
-
@full ||= queue_length
|
15
|
-
@start ||= now
|
16
|
-
completed = @full - queue_length
|
17
|
-
elapsed = now - @start
|
18
|
-
estimate = (completed == 0) ? '' : " #{(elapsed * @full / completed).to_i}s est"
|
19
|
-
"#{queue_length}/#{@full} left #{elapsed.to_i}s worker#{estimate}"
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
6
|
class Worker
|
24
7
|
include ParallelCucumber::Helper::Utils
|
25
8
|
|
26
|
-
def initialize(options
|
27
|
-
@batch_size = options[:batch_size]
|
9
|
+
def initialize(options:, index:, stdout_logger:, manager:)
|
28
10
|
@group_by = options[:group_by]
|
29
11
|
@batch_timeout = options[:batch_timeout]
|
30
12
|
@batch_error_timeout = options[:batch_error_timeout]
|
@@ -34,7 +16,7 @@ module ParallelCucumber
|
|
34
16
|
@test_command = options[:test_command]
|
35
17
|
@pre_check = options[:pre_check]
|
36
18
|
@index = index
|
37
|
-
@
|
19
|
+
@name = "W#{@index}"
|
38
20
|
@setup_worker = options[:setup_worker]
|
39
21
|
@teardown_worker = options[:teardown_worker]
|
40
22
|
@worker_delay = options[:worker_delay]
|
@@ -43,6 +25,19 @@ module ParallelCucumber
|
|
43
25
|
@log_dir = options[:log_dir]
|
44
26
|
@log_file = "#{@log_dir}/worker_#{index}.log"
|
45
27
|
@stdout_logger = stdout_logger # .sync writes only.
|
28
|
+
@is_busy_running_test = false
|
29
|
+
@jobs_queue = Queue.new
|
30
|
+
@manager = manager
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_reader :index
|
34
|
+
|
35
|
+
def assign_job(instruction)
|
36
|
+
@jobs_queue.enq(instruction)
|
37
|
+
end
|
38
|
+
|
39
|
+
def busy_running_test?
|
40
|
+
@is_busy_running_test && @current_thread.alive?
|
46
41
|
end
|
47
42
|
|
48
43
|
def autoshutting_file
|
@@ -68,6 +63,9 @@ module ParallelCucumber
|
|
68
63
|
end
|
69
64
|
|
70
65
|
def start(env)
|
66
|
+
@current_thread = Thread.current
|
67
|
+
@manager.inform_idle(@name)
|
68
|
+
|
71
69
|
env = env.dup.merge!('WORKER_LOG' => @log_file)
|
72
70
|
|
73
71
|
File.delete(@log_file) if File.exist?(@log_file)
|
@@ -96,33 +94,31 @@ module ParallelCucumber
|
|
96
94
|
begin
|
97
95
|
setup(env)
|
98
96
|
|
99
|
-
queue = ParallelCucumber::Helper::Queue.new(@queue_connection_params)
|
100
|
-
directed_queue = ParallelCucumber::Helper::Queue.new(@queue_connection_params, "_#{@index}")
|
101
|
-
queue_tracker = Tracker.new(queue)
|
102
|
-
|
103
97
|
loop_mm, loop_ss = time_it do
|
104
98
|
loop do
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
99
|
+
job = @jobs_queue.pop(false)
|
100
|
+
case job.type
|
101
|
+
when Job::PRECHECK
|
102
|
+
precmd = precheck(env)
|
103
|
+
if (m = precmd.match(/precmd:retry-after-(\d+)-seconds/))
|
104
|
+
@manager.inform_idle(@name)
|
105
|
+
sleep(1 + m[1].to_i)
|
106
|
+
next
|
107
|
+
end
|
108
|
+
@manager.inform_healthy(@name)
|
109
|
+
when Job::RUN_TESTS
|
110
|
+
run_batch(env, results, running_total, job.details)
|
111
|
+
@manager.inform_idle(@name)
|
112
|
+
when Job::DIE
|
113
|
+
break
|
114
|
+
else
|
115
|
+
raise("Invalid job #{job.inspect}")
|
111
116
|
end
|
112
|
-
@batch_size.times do
|
113
|
-
# TODO: Handle recovery of possibly toxic dequeued undirected tests if a worker dies mid-processing.
|
114
|
-
batch << (directed_queue.empty? ? queue : directed_queue).dequeue
|
115
|
-
end
|
116
|
-
batch.compact!
|
117
|
-
batch.sort! # Workaround for https://github.com/cucumber/cucumber-ruby/issues/952
|
118
|
-
break if batch.empty?
|
119
|
-
|
120
|
-
run_batch(env, queue_tracker, results, running_total, batch)
|
121
117
|
end
|
122
118
|
end
|
123
119
|
@logger.debug("Loop took #{loop_mm} minutes #{loop_ss} seconds")
|
124
120
|
@logger.update_into(@stdout_logger)
|
125
|
-
rescue => e
|
121
|
+
rescue StandardError => e
|
126
122
|
trace = e.backtrace.join("\n\t").sub("\n\t", ": #{$ERROR_INFO}#{e.class ? " (#{e.class})" : ''}\n\t")
|
127
123
|
@logger.error("Threw: #{e.inspect} #{trace}")
|
128
124
|
ensure
|
@@ -135,10 +131,10 @@ module ParallelCucumber
|
|
135
131
|
results
|
136
132
|
end
|
137
133
|
|
138
|
-
def run_batch(env,
|
134
|
+
def run_batch(env, results, running_total, tests)
|
135
|
+
@is_busy_running_test = true
|
139
136
|
batch_id = "#{Time.now.to_i}-#{@index}"
|
140
137
|
@logger.debug("Batch ID is #{batch_id}")
|
141
|
-
@logger.info("Took #{tests.count} from the queue (#{queue_tracker.status}): #{tests.join(' ')}")
|
142
138
|
|
143
139
|
batch_mm, batch_ss = time_it do
|
144
140
|
begin
|
@@ -158,6 +154,7 @@ module ParallelCucumber
|
|
158
154
|
process_results(batch_results, tests)
|
159
155
|
running_totals(batch_results, running_total)
|
160
156
|
results.merge!(batch_results)
|
157
|
+
@is_busy_running_test = false
|
161
158
|
end
|
162
159
|
ensure
|
163
160
|
@logger.debug("Batch #{batch_id} took #{batch_mm} minutes #{batch_ss} seconds")
|
@@ -0,0 +1,152 @@
|
|
1
|
+
module ParallelCucumber
|
2
|
+
class WorkerManager
|
3
|
+
def initialize(options, logger)
|
4
|
+
@options = options
|
5
|
+
@batch_size = options[:batch_size]
|
6
|
+
@logger = logger
|
7
|
+
@queue_connection_params = options[:queue_connection_params]
|
8
|
+
@backlog = ParallelCucumber::Helper::Queue.new(@queue_connection_params)
|
9
|
+
@queue_tracker = Tracker.new(@backlog)
|
10
|
+
@back_up_worker_size = options[:backup_worker_count]
|
11
|
+
@directed_queues = Hash.new do |hash, key|
|
12
|
+
hash[key] = ParallelCucumber::Helper::Queue.new(@queue_connection_params, "_#{key}")
|
13
|
+
end
|
14
|
+
@workers = {}
|
15
|
+
@unchecked_workers = Queue.new
|
16
|
+
@healthy_workers = Queue.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def start(number_of_workers)
|
20
|
+
create_workers(number_of_workers)
|
21
|
+
start_managing
|
22
|
+
start_workers
|
23
|
+
end
|
24
|
+
|
25
|
+
def kill
|
26
|
+
@current_thread.kill
|
27
|
+
end
|
28
|
+
|
29
|
+
def inform_healthy(worker)
|
30
|
+
@healthy_workers.enq(worker)
|
31
|
+
end
|
32
|
+
|
33
|
+
def inform_idle(worker)
|
34
|
+
@unchecked_workers.enq(worker)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def create_workers(number_of_workers)
|
40
|
+
number_of_workers.times do |index|
|
41
|
+
@workers["W#{index}"] =
|
42
|
+
ParallelCucumber::Worker.new(options: @options, index: index, stdout_logger: @logger, manager: self)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def start_managing
|
47
|
+
@current_thread = Thread.start do
|
48
|
+
loop do
|
49
|
+
if !@backlog.empty?
|
50
|
+
pre_check_unchecked_workers
|
51
|
+
give_job_to_healthy_worker
|
52
|
+
elsif any_worker_busy?
|
53
|
+
kill_surplus_workers
|
54
|
+
sleep 0.5
|
55
|
+
else
|
56
|
+
kill_all_workers
|
57
|
+
break
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def start_workers
|
64
|
+
indices = (0...@workers.size).to_a
|
65
|
+
@results = Parallel.map(indices.dup, in_threads: @workers.size,
|
66
|
+
finish: ->(_, ix, _) { @logger.synch { |l| l.info("Finished: #{ix} remaining: #{indices -= [ix]}") } }) do |index|
|
67
|
+
puts "Starting W#{index}"
|
68
|
+
@workers["W#{index}"].start(env_for_worker(@options[:env_variables], index))
|
69
|
+
end
|
70
|
+
@results.inject(:merge) # Returns hash of file:line to statuses + :worker-index to summary.
|
71
|
+
end
|
72
|
+
|
73
|
+
def kill_all_workers
|
74
|
+
@workers.values.each { |w| w.assign_job(Job.new(Job::DIE)) }
|
75
|
+
end
|
76
|
+
|
77
|
+
def kill_surplus_workers
|
78
|
+
until (@unchecked_workers.size + @healthy_workers.size) <= @back_up_worker_size
|
79
|
+
queue = !@unchecked_workers.empty? ? @unchecked_workers : @healthy_workers
|
80
|
+
worker = queue.pop(true)
|
81
|
+
@logger.info("Backup workers more than #{@back_up_worker_size}, killing #{worker}")
|
82
|
+
@workers[worker].assign_job(Job.new(Job::DIE))
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def pre_check_unchecked_workers
|
87
|
+
while !@unchecked_workers.empty? && worker = @unchecked_workers.pop(false)
|
88
|
+
@logger.info("=== #{worker} was asked precheck")
|
89
|
+
@workers[worker].assign_job(Job.new(Job::PRECHECK))
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def give_job_to_healthy_worker
|
94
|
+
return if @healthy_workers.empty?
|
95
|
+
|
96
|
+
worker_name = @healthy_workers.pop(true)
|
97
|
+
worker = @workers[worker_name]
|
98
|
+
batch = []
|
99
|
+
directed_queue = @directed_queues[worker.index]
|
100
|
+
@batch_size.times do
|
101
|
+
batch << (directed_queue.empty? ? @backlog : directed_queue).dequeue
|
102
|
+
end
|
103
|
+
batch.compact!
|
104
|
+
@logger.info("=== #{worker_name} was assigned #{batch.count} from the queue (#{@queue_tracker.status}): #{batch.join(' ')}")
|
105
|
+
worker.assign_job(Job.new(Job::RUN_TESTS, batch))
|
106
|
+
end
|
107
|
+
|
108
|
+
def any_worker_busy?
|
109
|
+
@workers.values.any?(&:busy_running_test?)
|
110
|
+
end
|
111
|
+
|
112
|
+
def env_for_worker(env_variables, worker_number)
|
113
|
+
env = env_variables.map do |k, v|
|
114
|
+
case v
|
115
|
+
when String, Numeric, TrueClass, FalseClass
|
116
|
+
[k, v]
|
117
|
+
when Array
|
118
|
+
[k, v[worker_number]]
|
119
|
+
when Hash
|
120
|
+
value = v[worker_number.to_s]
|
121
|
+
[k, value] unless value.nil?
|
122
|
+
when NilClass
|
123
|
+
else
|
124
|
+
raise("Don't know how to set '#{v}'<#{v.class}> to the environment variable '#{k}'")
|
125
|
+
end
|
126
|
+
end.compact.to_h
|
127
|
+
|
128
|
+
# Defaults, if absent in env. Shame 'merge' isn't something non-commutative like 'adopts/defaults'.
|
129
|
+
env = { TEST: 1, TEST_PROCESS_NUMBER: worker_number, WORKER_INDEX: worker_number }.merge(env)
|
130
|
+
|
131
|
+
# Overwrite this if it exists in env.
|
132
|
+
env.merge(PARALLEL_CUCUMBER_EXPORTS: env.keys.join(',')).map { |k, v| [k.to_s, v.to_s] }.to_h
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
class Tracker
|
137
|
+
def initialize(queue)
|
138
|
+
@backlog = queue
|
139
|
+
end
|
140
|
+
|
141
|
+
def status
|
142
|
+
queue_length = @backlog.length
|
143
|
+
now = Time.now
|
144
|
+
@full ||= queue_length
|
145
|
+
@start ||= now
|
146
|
+
completed = @full - queue_length
|
147
|
+
elapsed = now - @start
|
148
|
+
estimate = (completed == 0) ? '' : " #{(elapsed * @full / completed).to_i}s est"
|
149
|
+
"#{queue_length}/#{@full} left #{elapsed.to_i}s worker#{estimate}"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
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.17
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexander Bayandin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-09-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: cucumber
|
@@ -101,11 +101,13 @@ files:
|
|
101
101
|
- lib/parallel_cucumber/helper/unittest/cucumber_test.rb
|
102
102
|
- lib/parallel_cucumber/helper/utils.rb
|
103
103
|
- lib/parallel_cucumber/hooks.rb
|
104
|
+
- lib/parallel_cucumber/job.rb
|
104
105
|
- lib/parallel_cucumber/logger.rb
|
105
106
|
- lib/parallel_cucumber/main.rb
|
106
107
|
- lib/parallel_cucumber/status.rb
|
107
108
|
- lib/parallel_cucumber/version.rb
|
108
109
|
- lib/parallel_cucumber/worker.rb
|
110
|
+
- lib/parallel_cucumber/worker_manager.rb
|
109
111
|
homepage: https://github.com/badoo/parallel_cucumber
|
110
112
|
licenses:
|
111
113
|
- MIT
|
@@ -125,8 +127,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
125
127
|
- !ruby/object:Gem::Version
|
126
128
|
version: '0'
|
127
129
|
requirements: []
|
128
|
-
|
129
|
-
rubygems_version: 2.7.8
|
130
|
+
rubygems_version: 3.0.4
|
130
131
|
signing_key:
|
131
132
|
specification_version: 4
|
132
133
|
summary: Run cucumber in parallel
|