parallel_cucumber 0.2.16 → 0.2.21

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3754f455d081d50ef24488f466b9d15da6f40fb4a55b956cf2a2bbef0aea127b
4
- data.tar.gz: dfb977764ba2abc9a8839e9f15ac158adbe066ac23f1e1b60b896f2b8b59f9ad
3
+ metadata.gz: b2a5fcb6c187e21abd99851009a31c5015221e31118e5e16ccbb022467f64614
4
+ data.tar.gz: f45853fb7908faa80978b52c7e8208b2cec406a4dad87c80949623e615870dcc
5
5
  SHA512:
6
- metadata.gz: 0fd9d4d21114405cd8b166838b300455f29b6911ab8bc7c717c0d4b0d041b6e7753726e4a3a8350e73054a4c90b231348eff6f970fb31c1123c163fc74532c76
7
- data.tar.gz: 506ec27ac0b9ba080e1e904ad70335c78dc0be6c17143d873d61babcc30381c087773510f3a9f600b9110484a14c5cf9d805395c593a4f4153df01295f5e167e
6
+ metadata.gz: 21f7385b3c24476db0de9b9890d0e47166cd2ab3d4d5a5c52e8a73e39bc97e58502c50c0faaf4c82f0e250bda4ebe86a401a6b40c5f84b71959f058a9c6bfecb
7
+ data.tar.gz: eaeb5edcda79f778eb6b3bf1c0af1bfdabe69137422724a79523e9f0288c5e3e8177cfb30b920c227173ffcdf4644272772fb112ac46a214a3d3423a2709d0e9
@@ -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'
@@ -8,7 +8,6 @@ module ParallelCucumber
8
8
  batch_size: 1,
9
9
  batch_timeout: 600,
10
10
  setup_timeout: 30,
11
- precheck_timeout: 30,
12
11
  batch_error_timeout: 30,
13
12
  cucumber_options: '',
14
13
  debug: false,
@@ -19,7 +18,8 @@ module ParallelCucumber
19
18
  n: 0, # Default: computed from longest list in json parameters, minimum 1.
20
19
  queue_connection_params: ['redis://127.0.0.1:6379', DateTime.now.strftime('queue-%Y%m%d%H%M%S')],
21
20
  worker_delay: 0,
22
- test_command: 'cucumber'
21
+ test_command: 'cucumber',
22
+ backup_worker_count: 0
23
23
  }.freeze
24
24
 
25
25
  def initialize(argv)
@@ -61,6 +61,10 @@ module ParallelCucumber
61
61
  options[:n] = n
62
62
  end
63
63
 
64
+ opts.on('--backup-worker-count BACKUP_WORKERS', Integer, 'How many free workers to hold before all tasks are done. Default is none') do |n|
65
+ options[:backup_worker_count] = n
66
+ end
67
+
64
68
  opts.on('-o', '--cucumber-options "OPTIONS"', 'Run cucumber with these options') do |cucumber_options|
65
69
  options[:cucumber_options] = cucumber_options
66
70
  end
@@ -85,10 +89,6 @@ module ParallelCucumber
85
89
  options[:test_command] = test_command
86
90
  end
87
91
 
88
- opts.on('--pre-batch-check COMMAND', 'Command causing worker to quit on exit failure') do |pre_check|
89
- options[:pre_check] = pre_check
90
- end
91
-
92
92
  opts.on('--log-dir DIR', 'Directory for worker logfiles') do |log_dir|
93
93
  options[:log_dir] = log_dir
94
94
  end
@@ -168,13 +168,6 @@ module ParallelCucumber
168
168
  options[:batch_timeout] = batch_timeout
169
169
  end
170
170
 
171
- help_message = <<-TEXT.gsub(/\s+/, ' ').strip
172
- Timeout for each test precheck. Default is #{DEFAULTS[:batch_timeout]}
173
- TEXT
174
- opts.on('--precheck-timeout SECONDS', Float, help_message) do |timeout|
175
- options[:precheck_timeout] = timeout
176
- end
177
-
178
171
  help_message = <<-TEXT.gsub(/\s+/, ' ').strip
179
172
  Timeout for each batch_error script. Default is #{DEFAULTS[:batch_error_timeout]}
180
173
  TEXT
@@ -23,6 +23,10 @@ module ParallelCucumber
23
23
  Hooks.register_after_batch(proc)
24
24
  end
25
25
 
26
+ def worker_health_check(&proc)
27
+ Hooks.register_worker_health_check(proc)
28
+ end
29
+
26
30
  def before_workers(&proc)
27
31
  Hooks.register_before_workers(proc)
28
32
  end
@@ -26,25 +26,32 @@ module ParallelCucumber
26
26
 
27
27
  def parse_json_report(json_report)
28
28
  report = JSON.parse(json_report, symbolize_names: true)
29
- report.map do |scenario, cucumber_status|
30
- status = case cucumber_status
31
- when 'failed'
32
- Status::FAILED
33
- when 'passed'
34
- Status::PASSED
35
- when 'pending'
36
- Status::PENDING
37
- when 'skipped'
38
- Status::SKIPPED
39
- when 'undefined'
40
- Status::UNDEFINED
41
- when 'unknown'
42
- Status::UNKNOWN
43
- else
44
- Status::UNKNOWN
45
- end
46
- [scenario, status]
47
- end.to_h
29
+ report.each do |scenario, details|
30
+ report[scenario][:status] = case details[:status]
31
+ when 'failed'
32
+ Status::FAILED
33
+ when 'passed'
34
+ Status::PASSED
35
+ when 'pending'
36
+ Status::PENDING
37
+ when 'skipped'
38
+ Status::SKIPPED
39
+ when 'undefined'
40
+ Status::UNDEFINED
41
+ when 'unknown'
42
+ Status::UNKNOWN
43
+ else
44
+ Status::UNKNOWN
45
+ end
46
+ end
47
+ report
48
+ end
49
+
50
+ def unknown_result(tests)
51
+ res = tests.map do |test|
52
+ [test.to_sym, {status: ::ParallelCucumber::Status::UNKNOWN}]
53
+ end
54
+ res.to_h
48
55
  end
49
56
 
50
57
  private
@@ -57,7 +64,7 @@ module ParallelCucumber
57
64
  options = remove_strict_flag(options)
58
65
  content = nil
59
66
 
60
- Tempfile.open(%w(dry-run .json)) do |f|
67
+ Tempfile.open(%w[dry-run .json]) do |f|
61
68
  dry_run_options = "--dry-run --format ParallelCucumber::Helper::Cucumber::JsonStatusFormatter --out #{f.path}"
62
69
 
63
70
  cmd = "cucumber #{options} #{dry_run_options} #{args_string}"
@@ -15,7 +15,13 @@ module ParallelCucumber
15
15
  end
16
16
 
17
17
  def on_after_test_case(event)
18
- @result[event.test_case.location.to_s] = event.result.to_sym
18
+ details = {status: event.result.to_sym}
19
+ if event.result.respond_to?(:exception)
20
+ details[:exception_classname] = event.result.exception.class.to_s
21
+ details[:exception_message] = event.result.exception.message
22
+ end
23
+ details[:finish_time] = Time.now.to_i
24
+ @result[event.test_case.location.to_s] = details
19
25
  end
20
26
 
21
27
  def on_finished_testing(*)
@@ -1,12 +1,18 @@
1
1
  module ParallelCucumber
2
2
  class Hooks
3
- @before_batch_hooks ||= []
4
- @after_batch_hooks ||= []
5
- @before_workers ||= []
6
- @after_workers ||= []
7
- @on_batch_error ||= []
3
+ @worker_health_check ||= []
4
+ @before_batch_hooks ||= []
5
+ @after_batch_hooks ||= []
6
+ @before_workers ||= []
7
+ @after_workers ||= []
8
+ @on_batch_error ||= []
8
9
 
9
10
  class << self
11
+ def register_worker_health_check(proc)
12
+ raise(ArgumentError, 'Please provide a valid callback') unless proc.respond_to?(:call)
13
+ @worker_health_check << proc
14
+ end
15
+
10
16
  def register_before_batch(proc)
11
17
  raise(ArgumentError, 'Please provide a valid callback') unless proc.respond_to?(:call)
12
18
  @before_batch_hooks << proc
@@ -32,6 +38,12 @@ module ParallelCucumber
32
38
  @on_batch_error << proc
33
39
  end
34
40
 
41
+ def fire_worker_health_check(*args)
42
+ @worker_health_check.each do |hook|
43
+ hook.call(*args)
44
+ end
45
+ end
46
+
35
47
  def fire_before_batch_hooks(*args)
36
48
  @before_batch_hooks.each do |hook|
37
49
  hook.call(*args)
@@ -0,0 +1,13 @@
1
+ module ParallelCucumber
2
+ class Job
3
+ RUN_TESTS = :run_tests
4
+ PRECHECK = :precheck
5
+ DIE = :die
6
+
7
+ attr_accessor :type, :details
8
+ def initialize(type, details = nil)
9
+ @type = type
10
+ @details = details
11
+ end
12
+ end
13
+ end
@@ -98,7 +98,7 @@ module ParallelCucumber
98
98
 
99
99
  status_totals = Status.constants.map do |status|
100
100
  status = Status.const_get(status)
101
- tests_with_status = results.select { |_t, s| s == status }.keys
101
+ tests_with_status = results.select { |_t, s| s[:status] == status }.keys
102
102
  [status, tests_with_status]
103
103
  end.to_h
104
104
 
@@ -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
- remaining = (0...number_of_workers).to_a
153
- map = Parallel.map(
154
- remaining.dup,
155
- in_threads: number_of_workers,
156
- finish: -> (_, ix, _) { @logger.synch { |l| l.info("Finished: #{ix} remaining: #{remaining -= [ix]}") } }
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
@@ -1,3 +1,3 @@
1
1
  module ParallelCucumber
2
- VERSION = '0.2.16'.freeze
2
+ VERSION = '0.2.21'.freeze
3
3
  end
@@ -3,38 +3,18 @@ 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, index, stdout_logger)
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]
31
- @precheck_timeout = options[:precheck_timeout]
32
13
  @setup_timeout = options[:setup_timeout]
33
14
  @cucumber_options = options[:cucumber_options]
34
15
  @test_command = options[:test_command]
35
- @pre_check = options[:pre_check]
36
16
  @index = index
37
- @queue_connection_params = options[:queue_connection_params]
17
+ @name = "W#{@index}"
38
18
  @setup_worker = options[:setup_worker]
39
19
  @teardown_worker = options[:teardown_worker]
40
20
  @worker_delay = options[:worker_delay]
@@ -43,6 +23,19 @@ module ParallelCucumber
43
23
  @log_dir = options[:log_dir]
44
24
  @log_file = "#{@log_dir}/worker_#{index}.log"
45
25
  @stdout_logger = stdout_logger # .sync writes only.
26
+ @is_busy_running_test = false
27
+ @jobs_queue = Queue.new
28
+ @manager = manager
29
+ end
30
+
31
+ attr_reader :index
32
+
33
+ def assign_job(instruction)
34
+ @jobs_queue.enq(instruction)
35
+ end
36
+
37
+ def busy_running_test?
38
+ @is_busy_running_test && @current_thread.alive?
46
39
  end
47
40
 
48
41
  def autoshutting_file
@@ -68,6 +61,9 @@ module ParallelCucumber
68
61
  end
69
62
 
70
63
  def start(env)
64
+ @current_thread = Thread.current
65
+ @manager.inform_idle(@name)
66
+
71
67
  env = env.dup.merge!('WORKER_LOG' => @log_file)
72
68
 
73
69
  File.delete(@log_file) if File.exist?(@log_file)
@@ -96,33 +92,26 @@ module ParallelCucumber
96
92
  begin
97
93
  setup(env)
98
94
 
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
95
  loop_mm, loop_ss = time_it do
104
96
  loop do
105
- break if queue.empty? && directed_queue.empty?
106
- batch = []
107
- precmd = precheck(env)
108
- if (m = precmd.match(/precmd:retry-after-(\d+)-seconds/))
109
- sleep(1 + m[1].to_i)
110
- next
111
- 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
97
+ job = @jobs_queue.pop(false)
98
+ case job.type
99
+ when Job::PRECHECK
100
+ Hooks.fire_worker_health_check(env)
101
+ @manager.inform_healthy(@name)
102
+ when Job::RUN_TESTS
103
+ run_batch(env, results, running_total, job.details)
104
+ @manager.inform_idle(@name)
105
+ when Job::DIE
106
+ break
107
+ else
108
+ raise("Invalid job #{job.inspect}")
115
109
  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
110
  end
122
111
  end
123
112
  @logger.debug("Loop took #{loop_mm} minutes #{loop_ss} seconds")
124
113
  @logger.update_into(@stdout_logger)
125
- rescue => e
114
+ rescue StandardError => e
126
115
  trace = e.backtrace.join("\n\t").sub("\n\t", ": #{$ERROR_INFO}#{e.class ? " (#{e.class})" : ''}\n\t")
127
116
  @logger.error("Threw: #{e.inspect} #{trace}")
128
117
  ensure
@@ -135,10 +124,10 @@ module ParallelCucumber
135
124
  results
136
125
  end
137
126
 
138
- def run_batch(env, queue_tracker, results, running_total, tests)
127
+ def run_batch(env, results, running_total, tests)
128
+ @is_busy_running_test = true
139
129
  batch_id = "#{Time.now.to_i}-#{@index}"
140
130
  @logger.debug("Batch ID is #{batch_id}")
141
- @logger.info("Took #{tests.count} from the queue (#{queue_tracker.status}): #{tests.join(' ')}")
142
131
 
143
132
  batch_mm, batch_ss = time_it do
144
133
  begin
@@ -158,28 +147,17 @@ module ParallelCucumber
158
147
  process_results(batch_results, tests)
159
148
  running_totals(batch_results, running_total)
160
149
  results.merge!(batch_results)
150
+ @is_busy_running_test = false
161
151
  end
162
152
  ensure
163
153
  @logger.debug("Batch #{batch_id} took #{batch_mm} minutes #{batch_ss} seconds")
164
154
  @logger.update_into(@stdout_logger)
165
155
  end
166
156
 
167
- def precheck(env)
168
- return 'default no-op pre_check' unless @pre_check
169
- begin
170
- return Helper::Command.exec_command(
171
- env, 'precheck', @pre_check, @logger, @log_decoration, timeout: @precheck_timeout, capture: true
172
- )
173
- rescue
174
- @logger.error('Pre-check failed: quitting immediately')
175
- raise 'Pre-check failed: quitting immediately'
176
- end
177
- end
178
-
179
157
  def running_totals(batch_results, running_total)
180
158
  batch_info = Status.constants.map do |status|
181
159
  status = Status.const_get(status)
182
- [status, batch_results.select { |_t, s| s == status }.keys]
160
+ [status, batch_results.select { |_t, s| s[:status] == status }.keys]
183
161
  end.to_h
184
162
  batch_info.each do |s, tt|
185
163
  @logger.info("#{s.to_s.upcase} #{tt.count} tests: #{tt.join(' ')}") unless tt.empty?
@@ -194,7 +172,7 @@ module ParallelCucumber
194
172
  test_syms = tests.map(&:to_sym)
195
173
  unrun = test_syms - batch_keys
196
174
  surfeit = batch_keys - test_syms
197
- unrun.each { |test| batch_results[test] = Status::UNKNOWN }
175
+ unrun.each { |test| batch_results[test][:status] = Status::UNKNOWN }
198
176
  surfeit.each { |test| batch_results.delete(test) }
199
177
  @logger.error("Did not run #{unrun.count}/#{tests.count}: #{unrun.join(' ')}") unless unrun.empty?
200
178
  @logger.error("Extraneous runs (#{surfeit.count}): #{surfeit.join(' ')}") unless surfeit.empty?
@@ -235,7 +213,7 @@ module ParallelCucumber
235
213
  @logger.warn("There was exception in on_batch_error hook #{exc.message} \n #{trace}")
236
214
  end
237
215
 
238
- return tests.map { |t| [t, ::ParallelCucumber::Status::UNKNOWN] }.to_h
216
+ return Helper::Cucumber.unknown_result(tests)
239
217
  end
240
218
  parse_results(test_state, tests)
241
219
  ensure
@@ -294,18 +272,18 @@ module ParallelCucumber
294
272
  def parse_results(f, tests)
295
273
  unless File.file?(f)
296
274
  @logger.error("Results file does not exist: #{f}")
297
- return tests.map { |t| [t, ::ParallelCucumber::Status::UNKNOWN] }.to_h
275
+ return Helper::Cucumber.unknown_result(tests)
298
276
  end
299
277
  json_report = File.read(f)
300
278
  if json_report.empty?
301
279
  @logger.error("Results file is empty: #{f}")
302
- return tests.map { |t| [t, ::ParallelCucumber::Status::UNKNOWN] }.to_h
280
+ return Helper::Cucumber.unknown_result(tests)
303
281
  end
304
282
  Helper::Cucumber.parse_json_report(json_report)
305
283
  rescue => e
306
284
  trace = e.backtrace.join("\n\t").sub("\n\t", ": #{$ERROR_INFO}#{e.class ? " (#{e.class})" : ''}\n\t")
307
285
  @logger.error("Threw: JSON parse of results caused #{trace}")
308
- tests.map { |t| [t, ::ParallelCucumber::Status::UNKNOWN] }.to_h
286
+ Helper::Cucumber.unknown_result(tests)
309
287
  end
310
288
  end
311
289
  end
@@ -0,0 +1,157 @@
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
+ else
55
+ kill_all_workers
56
+ break
57
+ end
58
+ sleep 0.5
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 do |seed, result|
71
+ seed.merge(result) do |_key, oldval, newval|
72
+ (newval[:finish_time] > oldval[:finish_time]) ? newval : oldval
73
+ end
74
+ end
75
+ end
76
+
77
+ def kill_all_workers
78
+ @logger.info('=== Killing All Workers')
79
+ @workers.values.each { |w| w.assign_job(Job.new(Job::DIE)) }
80
+ end
81
+
82
+ def kill_surplus_workers
83
+ until (@unchecked_workers.size + @healthy_workers.size) <= @back_up_worker_size
84
+ queue = !@unchecked_workers.empty? ? @unchecked_workers : @healthy_workers
85
+ worker = queue.pop(true)
86
+ @logger.info("Backup workers more than #{@back_up_worker_size}, killing #{worker}")
87
+ @workers[worker].assign_job(Job.new(Job::DIE))
88
+ end
89
+ end
90
+
91
+ def pre_check_unchecked_workers
92
+ while !@unchecked_workers.empty? && worker = @unchecked_workers.pop(false)
93
+ @logger.info("=== #{worker} was asked precheck")
94
+ @workers[worker].assign_job(Job.new(Job::PRECHECK))
95
+ end
96
+ end
97
+
98
+ def give_job_to_healthy_worker
99
+ return if @healthy_workers.empty?
100
+
101
+ worker_name = @healthy_workers.pop(true)
102
+ worker = @workers[worker_name]
103
+ batch = []
104
+ directed_queue = @directed_queues[worker.index]
105
+ @batch_size.times do
106
+ batch << (directed_queue.empty? ? @backlog : directed_queue).dequeue
107
+ end
108
+ batch.compact!
109
+ @logger.info("=== #{worker_name} was assigned #{batch.count} from the queue (#{@queue_tracker.status}): #{batch.join(' ')}")
110
+ worker.assign_job(Job.new(Job::RUN_TESTS, batch))
111
+ end
112
+
113
+ def any_worker_busy?
114
+ @workers.values.any?(&:busy_running_test?)
115
+ end
116
+
117
+ def env_for_worker(env_variables, worker_number)
118
+ env = env_variables.map do |k, v|
119
+ case v
120
+ when String, Numeric, TrueClass, FalseClass
121
+ [k, v]
122
+ when Array
123
+ [k, v[worker_number]]
124
+ when Hash
125
+ value = v[worker_number.to_s]
126
+ [k, value] unless value.nil?
127
+ when NilClass
128
+ else
129
+ raise("Don't know how to set '#{v}'<#{v.class}> to the environment variable '#{k}'")
130
+ end
131
+ end.compact.to_h
132
+
133
+ # Defaults, if absent in env. Shame 'merge' isn't something non-commutative like 'adopts/defaults'.
134
+ env = { TEST: 1, TEST_PROCESS_NUMBER: worker_number, WORKER_INDEX: worker_number }.merge(env)
135
+
136
+ # Overwrite this if it exists in env.
137
+ env.merge(PARALLEL_CUCUMBER_EXPORTS: env.keys.join(',')).map { |k, v| [k.to_s, v.to_s] }.to_h
138
+ end
139
+ end
140
+
141
+ class Tracker
142
+ def initialize(queue)
143
+ @backlog = queue
144
+ end
145
+
146
+ def status
147
+ queue_length = @backlog.length
148
+ now = Time.now
149
+ @full ||= queue_length
150
+ @start ||= now
151
+ completed = @full - queue_length
152
+ elapsed = now - @start
153
+ estimate = (completed == 0) ? '' : " #{(elapsed * @full / completed).to_i}s est"
154
+ "#{queue_length}/#{@full} left #{elapsed.to_i}s worker#{estimate}"
155
+ end
156
+ end
157
+ 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.16
4
+ version: 0.2.21
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-07-19 00:00:00.000000000 Z
11
+ date: 2020-09-17 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
- rubyforge_project:
129
- rubygems_version: 2.7.8
130
+ rubygems_version: 3.0.8
130
131
  signing_key:
131
132
  specification_version: 4
132
133
  summary: Run cucumber in parallel