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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3754f455d081d50ef24488f466b9d15da6f40fb4a55b956cf2a2bbef0aea127b
4
- data.tar.gz: dfb977764ba2abc9a8839e9f15ac158adbe066ac23f1e1b60b896f2b8b59f9ad
3
+ metadata.gz: 5664f82c63144d99bec9dc1880905601d0de45b2431b929eee77c0c4abb4521f
4
+ data.tar.gz: 9c5fce5710b7a36270c5f84431bc32195bf54a37779380ea584e04f892b8e616
5
5
  SHA512:
6
- metadata.gz: 0fd9d4d21114405cd8b166838b300455f29b6911ab8bc7c717c0d4b0d041b6e7753726e4a3a8350e73054a4c90b231348eff6f970fb31c1123c163fc74532c76
7
- data.tar.gz: 506ec27ac0b9ba080e1e904ad70335c78dc0be6c17143d873d61babcc30381c087773510f3a9f600b9110484a14c5cf9d805395c593a4f4153df01295f5e167e
6
+ metadata.gz: c51f9ebe8c466e9e31936ddf59bc1944f5ddab128e317d4185b4047d7d7c2f63f091c242556ac2185bd67bbd3de7145fe31fbb3fc51239e56b1b046b3412087e
7
+ data.tar.gz: bac0d600b9e52aacc815a7ad2e3138470c5efafa2aad96d4c94ba82bae7d82df2bdd48ce21b2b78b09c98045f1f40e3e13b760ef371a8f16babd7bfdeef180c4
@@ -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
@@ -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
@@ -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.17'.freeze
3
3
  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, 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]
@@ -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
- @queue_connection_params = options[:queue_connection_params]
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
- 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
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, queue_tracker, results, running_total, tests)
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.16
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-07-19 00:00:00.000000000 Z
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
- rubyforge_project:
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