parallel_cucumber 0.2.19 → 0.2.24

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: 9ae598edd0afabdd7a9ff4ae2b117505e52a05028e79e8c01065548cf28d3298
4
- data.tar.gz: acb97b25354b64a8acdd9bfc87c631135042759a16b2aa3df4717a459c0e3946
3
+ metadata.gz: 3c5786a5361ff67156526199fce9a686256fa49e33dc6c5af590c5f2ea17adce
4
+ data.tar.gz: 49d1792f94978bbfe9ea954e13dc9e0ebfe0742dad6aef966395e2885a462bdf
5
5
  SHA512:
6
- metadata.gz: 731c24ee0e9d627768a97be407ab15ff2259e9ba14dae9cba0cc995100e737c64ae11fc29560a6c4eabd0faafb06a8fbdd15a880271efeba2fe08e52b2c14511
7
- data.tar.gz: f2ca8028be443de6b883a99358d5e2c32de9d916768b6a0f2be01945aefc8459cd83b3e982cdc89921e2a580e0a3789fff45011d9ede4a13a5fbe23e2b485bb3
6
+ metadata.gz: fc9332b43d59d80cedd19f8c2fd63cb231624e318a81a40fc378bd212e1dffb90aeb0ba7c359f6dae8cccec524d5f5828eb42d7f8e976a404ea5f7dba92ed283
7
+ data.tar.gz: 8f57b2ba36db0f161cfdf7db981411b365d8fc5d03b4369b388898c0e21b5e064331f4f91d154340a62075d16de4571e27ce406ee2eb84523a7156661dc45f77
@@ -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,
@@ -90,10 +89,6 @@ module ParallelCucumber
90
89
  options[:test_command] = test_command
91
90
  end
92
91
 
93
- opts.on('--pre-batch-check COMMAND', 'Command causing worker to quit on exit failure') do |pre_check|
94
- options[:pre_check] = pre_check
95
- end
96
-
97
92
  opts.on('--log-dir DIR', 'Directory for worker logfiles') do |log_dir|
98
93
  options[:log_dir] = log_dir
99
94
  end
@@ -173,13 +168,6 @@ module ParallelCucumber
173
168
  options[:batch_timeout] = batch_timeout
174
169
  end
175
170
 
176
- help_message = <<-TEXT.gsub(/\s+/, ' ').strip
177
- Timeout for each test precheck. Default is #{DEFAULTS[:batch_timeout]}
178
- TEXT
179
- opts.on('--precheck-timeout SECONDS', Float, help_message) do |timeout|
180
- options[:precheck_timeout] = timeout
181
- end
182
-
183
171
  help_message = <<-TEXT.gsub(/\s+/, ' ').strip
184
172
  Timeout for each batch_error script. Default is #{DEFAULTS[:batch_error_timeout]}
185
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
@@ -34,6 +38,10 @@ module ParallelCucumber
34
38
  def on_batch_error(&proc)
35
39
  Hooks.register_on_batch_error(proc)
36
40
  end
41
+
42
+ def on_dry_run_error(&proc)
43
+ Hooks.register_on_dry_run_error(proc)
44
+ end
37
45
  end
38
46
  end
39
47
  end
@@ -15,8 +15,13 @@ module ParallelCucumber
15
15
  end
16
16
 
17
17
  ONE_SECOND = 1
18
+ STACKTRACE_COLLECTION_TIMEOUT = 10
18
19
 
19
- def exec_command(env, desc, script, logger, log_decoration = {}, timeout: 30, capture: false, return_script_error: false) # rubocop:disable Metrics/ParameterLists, Metrics/LineLength
20
+ # rubocop:disable Metrics/ParameterLists, Metrics/LineLength
21
+ def exec_command(env, desc, script, logger, log_decoration = {},
22
+ timeout: 30, capture: false, return_script_error: false,
23
+ return_on_timeout: false, collect_stacktrace: false
24
+ )
20
25
  block_name = ''
21
26
  if log_decoration['worker_block']
22
27
  if log_decoration['start'] || log_decoration['end']
@@ -28,42 +33,48 @@ module ParallelCucumber
28
33
  full_script = "#{script} 2>&1"
29
34
  env_string = env.map { |k, v| "#{k}=#{v}" }.sort.join(' ')
30
35
  logger << "== Running command `#{full_script}` at #{Time.now}\n== with environment variables: #{env_string}\n"
31
- pstat = nil
36
+ wait_thread = nil
32
37
  pout = nil
33
38
  capture &&= [''] # Pass by reference
34
39
  exception = nil
40
+ command_pid = nil
35
41
 
36
42
  begin
37
43
  completed = begin
38
- pin, pout, pstat = Open3.popen2e(env, full_script)
39
- logger << "Command has pid #{pstat[:pid]}\n"
44
+ pin, pout, wait_thread = Open3.popen2e(env, full_script)
45
+ command_pid = wait_thread[:pid].to_s
46
+ logger << "Command has pid #{command_pid}\n"
40
47
  pin.close
41
48
  out_reader = Thread.new do
42
- output_reader(pout, pstat, logger, capture)
49
+ output_reader(pout, wait_thread, logger, capture)
43
50
  end
44
51
 
45
52
  unless out_reader.join(timeout)
46
53
  raise TimedOutError
47
54
  end
48
55
 
49
- graceful_process_shutdown(out_reader, pstat, pout, logger)
56
+ graceful_process_shutdown(out_reader, wait_thread, pout, logger)
50
57
 
51
- pstat.value # reap already-terminated child.
52
- "Command completed #{pstat.value} at #{Time.now}"
58
+ wait_thread.value # reap already-terminated child.
59
+ "Command completed #{wait_thread.value} at #{Time.now}"
53
60
  end
54
61
 
55
62
  logger << "#{completed}\n"
56
63
 
57
- raise "Script returned #{pstat.value.exitstatus}" unless pstat.value.success? || return_script_error
64
+ raise "Script returned #{wait_thread.value.exitstatus}" unless wait_thread.value.success? || return_script_error
58
65
 
59
66
  capture_or_empty = capture ? capture.first : '' # Even '' is truthy
60
- return pstat.value.success? ? capture_or_empty : nil
67
+ return wait_thread.value.success? ? capture_or_empty : nil
61
68
  rescue TimedOutError => e
62
- force_kill_process_with_tree(out_reader, pstat, pout, full_script, logger, timeout)
69
+ process_tree = Helper::Processes.ps_tree
70
+ send_usr1_to_process_with_tree(command_pid, full_script, logger, process_tree) if collect_stacktrace
71
+ force_kill_process_with_tree(out_reader, wait_thread, pout, full_script, logger, timeout, process_tree, command_pid)
72
+
73
+ return capture.first if return_on_timeout
63
74
 
64
75
  exception = e
65
76
  rescue => e
66
- logger.debug("Exception #{pstat ? pstat[:pid] : "pstat=#{pstat}=nil"}")
77
+ logger.debug("Exception #{wait_thread ? wait_thread[:pid] : "wait_thread=#{wait_thread}=nil"}")
67
78
  trace = e.backtrace.join("\n\t").sub("\n\t", ": #{$ERROR_INFO}#{e.class ? " (#{e.class})" : ''}\n\t")
68
79
  logger.error("Threw for #{full_script}, caused #{trace}")
69
80
 
@@ -75,6 +86,7 @@ module ParallelCucumber
75
86
 
76
87
  raise exception
77
88
  end
89
+ # rubocop:enable Metrics/ParameterLists, Metrics/LineLength
78
90
 
79
91
  def log_until_incomplete_line(logger, out_string)
80
92
  loop do
@@ -87,13 +99,13 @@ module ParallelCucumber
87
99
 
88
100
  private
89
101
 
90
- def output_reader(pout, pstat, logger, capture)
102
+ def output_reader(pout, wait_thread, logger, capture)
91
103
  out_string = ''
92
104
 
93
105
  loop do
94
106
  io_select = IO.select([pout], [], [], ONE_SECOND)
95
- unless io_select || pstat.alive?
96
- logger << "\n== Terminating because io_select=#{io_select} when pstat.alive?=#{pstat.alive?}\n"
107
+ unless io_select || wait_thread.alive?
108
+ logger << "\n== Terminating because io_select=#{io_select} when wait_thread.alive?=#{wait_thread.alive?}\n"
97
109
  break
98
110
  end
99
111
  next unless io_select
@@ -103,44 +115,45 @@ module ParallelCucumber
103
115
  out_string = log_until_incomplete_line(logger, out_string + partial)
104
116
  end
105
117
  rescue EOFError
106
- logger << "\n== EOF is normal exit, #{pstat.inspect}\n"
118
+ logger << "\n== EOF is normal exit, #{wait_thread.inspect}\n"
107
119
  rescue => e
108
120
  logger << "\n== Exception in out_reader due to #{e.inspect} #{e.backtrace}\n"
109
121
  ensure
110
122
  logger << out_string
111
123
  logger << ["\n== Left out_reader at #{Time.now}; ",
112
- "pipe=#{pstat.status}+#{pstat.status ? '≤no value≥' : pstat.value}\n"].join
124
+ "pipe=#{wait_thread.status}+#{wait_thread.status ? '≤no value≥' : wait_thread.value}\n"].join
113
125
  end
114
126
 
115
- def graceful_process_shutdown(out_reader, pstat, pout, logger)
116
- out_reader.value # Should terminate with pstat
127
+ def graceful_process_shutdown(out_reader, wait_thread, pout, logger)
128
+ out_reader.value # Should terminate with wait_thread
117
129
  pout.close
118
- if pstat.status
119
- logger << "== Thread #{pstat.inspect} is not dead"
130
+ if wait_thread.status
131
+ logger << "== Thread #{wait_thread.inspect} is not dead"
120
132
 
121
- if pstat.join(3)
122
- logger << "== Thread #{pstat.inspect} joined late"
133
+ if wait_thread.join(3)
134
+ logger << "== Thread #{wait_thread.inspect} joined late"
123
135
  else
124
- pstat.terminate # Just in case
125
- logger << "== Thread #{pstat.inspect} terminated"
136
+ wait_thread.terminate # Just in case
137
+ logger << "== Thread #{wait_thread.inspect} terminated"
126
138
  end # Make an effort to reap
127
139
  end
128
140
 
129
- pstat.value # reap already-terminated child.
130
- "Command completed #{pstat.value} at #{Time.now}"
141
+ wait_thread.value # reap already-terminated child.
142
+ "Command completed #{wait_thread.value} at #{Time.now}"
131
143
  end
132
144
 
133
- def force_kill_process_with_tree(out_reader, pstat, pout, full_script, logger, timeout) # rubocop:disable Metrics/ParameterLists, Metrics/LineLength
145
+ def send_usr1_to_process_with_tree(command_pid, full_script, logger, tree)
146
+ return if Helper::Processes.ms_windows?
147
+
148
+ logger << "Timeout, so trying SIGUSR1 to trigger watchdog stacktrace #{command_pid}=#{full_script}"
149
+ Helper::Processes.kill_tree('SIGUSR1', command_pid, logger, tree)
150
+ sleep(STACKTRACE_COLLECTION_TIMEOUT) # Wait enough time for child processes to act on SIGUSR1
151
+ end
152
+
153
+ def force_kill_process_with_tree(out_reader, wait_thread, pout, full_script, logger, timeout, tree, pid) # rubocop:disable Metrics/ParameterLists, Metrics/LineLength
134
154
  out_reader.exit
135
- tree = Helper::Processes.ps_tree
136
- pid = pstat[:pid].to_s
137
- unless Helper::Processes.ms_windows?
138
- logger << "Timeout, so trying SIGUSR1 to trigger watchdog stacktrace #{pstat[:pid]}=#{full_script}"
139
- Helper::Processes.kill_tree('SIGUSR1', pid, logger, tree)
140
- sleep 2
141
- end
142
155
 
143
- logger << "Timeout, so trying SIGINT at #{pstat[:pid]}=#{full_script}"
156
+ logger << "Timeout, so trying SIGINT at #{wait_thread[:pid]}=#{full_script}"
144
157
 
145
158
  log_copy = Thread.new do
146
159
  pout.each_line { |l| logger << l }
@@ -167,7 +180,7 @@ module ParallelCucumber
167
180
  end
168
181
 
169
182
  logger << "About to reap root #{pid}"
170
- pstat.value # reap root - everything else should be reaped by init.
183
+ wait_thread.value # reap root - everything else should be reaped by init.
171
184
  logger << "Reaped root #{pid}"
172
185
  end
173
186
  end
@@ -47,6 +47,13 @@ module ParallelCucumber
47
47
  report
48
48
  end
49
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
55
+ end
56
+
50
57
  private
51
58
 
52
59
  def dry_run_report(options, args_string)
@@ -20,6 +20,7 @@ module ParallelCucumber
20
20
  details[:exception_classname] = event.result.exception.class.to_s
21
21
  details[:exception_message] = event.result.exception.message
22
22
  end
23
+ details[:name] = "#{event.test_case.feature}: #{event.test_case.name}"
23
24
  details[:finish_time] = Time.now.to_i
24
25
  @result[event.test_case.location.to_s] = details
25
26
  end
@@ -35,7 +35,7 @@ module ParallelCucumber
35
35
  else
36
36
  descendants(root, logger, tree, old_tree, 'kill') do |pid, node|
37
37
  begin
38
- logger.warn "Killing #{node}"
38
+ logger.warn "Sending signal #{sig} to #{node}"
39
39
  Process.kill(sig, pid.to_i)
40
40
  rescue Errno::ESRCH
41
41
  nil # It's gone already? Hurrah!
@@ -44,7 +44,7 @@ module ParallelCucumber
44
44
  end
45
45
  # Let's kill pid unconditionally: descendants will go astray once reparented.
46
46
  begin
47
- logger.warn "Killing #{root} just in case"
47
+ logger.warn "Sending signal #{sig} to root process #{root} just in case"
48
48
  Process.kill(sig, root.to_i)
49
49
  rescue Errno::ESRCH
50
50
  nil # It's gone already? Hurrah!
@@ -1,12 +1,19 @@
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 ||= []
9
+ @on_dry_run_error ||= []
8
10
 
9
11
  class << self
12
+ def register_worker_health_check(proc)
13
+ raise(ArgumentError, 'Please provide a valid callback') unless proc.respond_to?(:call)
14
+ @worker_health_check << proc
15
+ end
16
+
10
17
  def register_before_batch(proc)
11
18
  raise(ArgumentError, 'Please provide a valid callback') unless proc.respond_to?(:call)
12
19
  @before_batch_hooks << proc
@@ -32,6 +39,17 @@ module ParallelCucumber
32
39
  @on_batch_error << proc
33
40
  end
34
41
 
42
+ def register_on_dry_run_error(proc)
43
+ raise(ArgumentError, 'Please provide a valid callback') unless proc.respond_to?(:call)
44
+ @on_dry_run_error << proc
45
+ end
46
+
47
+ def fire_worker_health_check(*args)
48
+ @worker_health_check.each do |hook|
49
+ hook.call(*args)
50
+ end
51
+ end
52
+
35
53
  def fire_before_batch_hooks(*args)
36
54
  @before_batch_hooks.each do |hook|
37
55
  hook.call(*args)
@@ -61,6 +79,12 @@ module ParallelCucumber
61
79
  hook.call(*args)
62
80
  end
63
81
  end
82
+
83
+ def fire_on_dry_run_error(error)
84
+ @on_dry_run_error.each do |hook|
85
+ hook.call(error)
86
+ end
87
+ end
64
88
  end
65
89
  end
66
90
  end
@@ -30,8 +30,12 @@ module ParallelCucumber
30
30
  exit(1)
31
31
  end
32
32
 
33
- all_tests = Helper::Cucumber.selected_tests(@options[:cucumber_options], @options[:cucumber_args])
34
-
33
+ begin
34
+ all_tests = Helper::Cucumber.selected_tests(@options[:cucumber_options], @options[:cucumber_args])
35
+ rescue StandardError => error
36
+ Hooks.fire_on_dry_run_error(error)
37
+ raise error
38
+ end
35
39
  if all_tests.empty?
36
40
  @logger.info('There is no tests to run, exiting...')
37
41
  exit(0)
@@ -1,3 +1,3 @@
1
1
  module ParallelCucumber
2
- VERSION = '0.2.19'.freeze
2
+ VERSION = '0.2.24'.freeze
3
3
  end
@@ -10,11 +10,9 @@ module ParallelCucumber
10
10
  @group_by = options[:group_by]
11
11
  @batch_timeout = options[:batch_timeout]
12
12
  @batch_error_timeout = options[:batch_error_timeout]
13
- @precheck_timeout = options[:precheck_timeout]
14
13
  @setup_timeout = options[:setup_timeout]
15
14
  @cucumber_options = options[:cucumber_options]
16
15
  @test_command = options[:test_command]
17
- @pre_check = options[:pre_check]
18
16
  @index = index
19
17
  @name = "W#{@index}"
20
18
  @setup_worker = options[:setup_worker]
@@ -99,12 +97,7 @@ module ParallelCucumber
99
97
  job = @jobs_queue.pop(false)
100
98
  case job.type
101
99
  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
100
+ Hooks.fire_worker_health_check(env)
108
101
  @manager.inform_healthy(@name)
109
102
  when Job::RUN_TESTS
110
103
  run_batch(env, results, running_total, job.details)
@@ -161,18 +154,6 @@ module ParallelCucumber
161
154
  @logger.update_into(@stdout_logger)
162
155
  end
163
156
 
164
- def precheck(env)
165
- return 'default no-op pre_check' unless @pre_check
166
- begin
167
- return Helper::Command.exec_command(
168
- env, 'precheck', @pre_check, @logger, @log_decoration, timeout: @precheck_timeout, capture: true
169
- )
170
- rescue
171
- @logger.error('Pre-check failed: quitting immediately')
172
- raise 'Pre-check failed: quitting immediately'
173
- end
174
- end
175
-
176
157
  def running_totals(batch_results, running_total)
177
158
  batch_info = Status.constants.map do |status|
178
159
  status = Status.const_get(status)
@@ -220,7 +201,8 @@ module ParallelCucumber
220
201
  begin
221
202
  ParallelCucumber::Helper::Command.exec_command(
222
203
  batch_env, 'batch', mapped_batch_cmd, @logger, @log_decoration,
223
- timeout: @batch_timeout, return_script_error: true
204
+ timeout: @batch_timeout, capture: true, return_script_error: true,
205
+ return_on_timeout: true, collect_stacktrace: true
224
206
  )
225
207
  rescue => e
226
208
  @logger << "ERROR #{e} #{e.backtrace.first(5)}"
@@ -232,7 +214,7 @@ module ParallelCucumber
232
214
  @logger.warn("There was exception in on_batch_error hook #{exc.message} \n #{trace}")
233
215
  end
234
216
 
235
- return tests.map { |t| [t, ::ParallelCucumber::Status::UNKNOWN] }.to_h
217
+ return Helper::Cucumber.unknown_result(tests)
236
218
  end
237
219
  parse_results(test_state, tests)
238
220
  ensure
@@ -291,18 +273,18 @@ module ParallelCucumber
291
273
  def parse_results(f, tests)
292
274
  unless File.file?(f)
293
275
  @logger.error("Results file does not exist: #{f}")
294
- return tests.map { |t| [t, ::ParallelCucumber::Status::UNKNOWN] }.to_h
276
+ return Helper::Cucumber.unknown_result(tests)
295
277
  end
296
278
  json_report = File.read(f)
297
279
  if json_report.empty?
298
280
  @logger.error("Results file is empty: #{f}")
299
- return tests.map { |t| [t, ::ParallelCucumber::Status::UNKNOWN] }.to_h
281
+ return Helper::Cucumber.unknown_result(tests)
300
282
  end
301
283
  Helper::Cucumber.parse_json_report(json_report)
302
284
  rescue => e
303
285
  trace = e.backtrace.join("\n\t").sub("\n\t", ": #{$ERROR_INFO}#{e.class ? " (#{e.class})" : ''}\n\t")
304
286
  @logger.error("Threw: JSON parse of results caused #{trace}")
305
- tests.map { |t| [t, ::ParallelCucumber::Status::UNKNOWN] }.to_h
287
+ Helper::Cucumber.unknown_result(tests)
306
288
  end
307
289
  end
308
290
  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.19
4
+ version: 0.2.24
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Bayandin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-02-24 00:00:00.000000000 Z
11
+ date: 2021-07-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cucumber
@@ -72,14 +72,14 @@ dependencies:
72
72
  requirements:
73
73
  - - '='
74
74
  - !ruby/object:Gem::Version
75
- version: 0.59.2
75
+ version: 0.73.0
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - '='
81
81
  - !ruby/object:Gem::Version
82
- version: 0.59.2
82
+ version: 0.73.0
83
83
  description: Our own parallel cucumber with queue and workers
84
84
  email: a.bayandin@gmail.com
85
85
  executables:
@@ -127,7 +127,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
127
127
  - !ruby/object:Gem::Version
128
128
  version: '0'
129
129
  requirements: []
130
- rubygems_version: 3.0.4
130
+ rubygems_version: 3.0.3
131
131
  signing_key:
132
132
  specification_version: 4
133
133
  summary: Run cucumber in parallel