parallel_cucumber 0.2.16 → 0.2.17
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/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
|