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