parallel_cucumber 0.2.21 → 0.2.25
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/dsl.rb +4 -0
- data/lib/parallel_cucumber/helper/command.rb +50 -37
- data/lib/parallel_cucumber/helper/cucumber/json_status_formatter.rb +1 -0
- data/lib/parallel_cucumber/helper/processes.rb +2 -2
- data/lib/parallel_cucumber/hooks.rb +12 -0
- data/lib/parallel_cucumber/main.rb +6 -2
- data/lib/parallel_cucumber/version.rb +1 -1
- data/lib/parallel_cucumber/worker.rb +2 -1
- data/lib/parallel_cucumber/worker_manager.rb +5 -1
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c26b5025007a16790939a4b3d276cfd552f36f611b80554d7a597d477775b168
|
4
|
+
data.tar.gz: 9e3016e687ece3ff4e3ba714a598f489e3f8b8381f01a40004e59cecf7160985
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 77722cf57a707fe7c650f7656b680418ce4e8b8a5411142afb1be201d09d426e07e9ce81eb3331e472c492c8993d02e349dc62385d495498b70d568c3e668f42
|
7
|
+
data.tar.gz: 9a8c49cbc3f35e05642aff922f4c6490035e334ff4e521d651a2938b5715ccd787db8a26c560ca24e564cc10f083fdad2beec3d6a66ddf177a7977b4e24e59b1
|
@@ -15,8 +15,13 @@ module ParallelCucumber
|
|
15
15
|
end
|
16
16
|
|
17
17
|
ONE_SECOND = 1
|
18
|
+
STACKTRACE_COLLECTION_TIMEOUT = 10
|
18
19
|
|
19
|
-
|
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
|
-
|
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,
|
39
|
-
|
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,
|
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,
|
56
|
+
graceful_process_shutdown(out_reader, wait_thread, pout, logger)
|
50
57
|
|
51
|
-
|
52
|
-
"Command completed #{
|
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 #{
|
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
|
67
|
+
return wait_thread.value.success? ? capture_or_empty : nil
|
61
68
|
rescue TimedOutError => e
|
62
|
-
|
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 #{
|
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,
|
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 ||
|
96
|
-
logger << "\n== Terminating because io_select=#{io_select} when
|
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, #{
|
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=#{
|
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,
|
116
|
-
out_reader.value # Should terminate with
|
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
|
119
|
-
logger << "== Thread #{
|
130
|
+
if wait_thread.status
|
131
|
+
logger << "== Thread #{wait_thread.inspect} is not dead"
|
120
132
|
|
121
|
-
if
|
122
|
-
logger << "== Thread #{
|
133
|
+
if wait_thread.join(3)
|
134
|
+
logger << "== Thread #{wait_thread.inspect} joined late"
|
123
135
|
else
|
124
|
-
|
125
|
-
logger << "== Thread #{
|
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
|
-
|
130
|
-
"Command completed #{
|
141
|
+
wait_thread.value # reap already-terminated child.
|
142
|
+
"Command completed #{wait_thread.value} at #{Time.now}"
|
131
143
|
end
|
132
144
|
|
133
|
-
def
|
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 #{
|
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
|
-
|
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
|
@@ -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 "
|
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 "
|
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!
|
@@ -6,6 +6,7 @@ module ParallelCucumber
|
|
6
6
|
@before_workers ||= []
|
7
7
|
@after_workers ||= []
|
8
8
|
@on_batch_error ||= []
|
9
|
+
@on_dry_run_error ||= []
|
9
10
|
|
10
11
|
class << self
|
11
12
|
def register_worker_health_check(proc)
|
@@ -38,6 +39,11 @@ module ParallelCucumber
|
|
38
39
|
@on_batch_error << proc
|
39
40
|
end
|
40
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
|
+
|
41
47
|
def fire_worker_health_check(*args)
|
42
48
|
@worker_health_check.each do |hook|
|
43
49
|
hook.call(*args)
|
@@ -73,6 +79,12 @@ module ParallelCucumber
|
|
73
79
|
hook.call(*args)
|
74
80
|
end
|
75
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
|
76
88
|
end
|
77
89
|
end
|
78
90
|
end
|
@@ -30,8 +30,12 @@ module ParallelCucumber
|
|
30
30
|
exit(1)
|
31
31
|
end
|
32
32
|
|
33
|
-
|
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)
|
@@ -201,7 +201,8 @@ module ParallelCucumber
|
|
201
201
|
begin
|
202
202
|
ParallelCucumber::Helper::Command.exec_command(
|
203
203
|
batch_env, 'batch', mapped_batch_cmd, @logger, @log_decoration,
|
204
|
-
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
|
205
206
|
)
|
206
207
|
rescue => e
|
207
208
|
@logger << "ERROR #{e} #{e.backtrace.first(5)}"
|
@@ -52,11 +52,15 @@ module ParallelCucumber
|
|
52
52
|
elsif any_worker_busy?
|
53
53
|
kill_surplus_workers
|
54
54
|
else
|
55
|
-
kill_all_workers
|
56
55
|
break
|
57
56
|
end
|
58
57
|
sleep 0.5
|
59
58
|
end
|
59
|
+
rescue StandardError => e
|
60
|
+
puts "There was a FATAL ERROR with worker manager. #{e}"
|
61
|
+
raise e
|
62
|
+
ensure
|
63
|
+
kill_all_workers
|
60
64
|
end
|
61
65
|
end
|
62
66
|
|
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.25
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexander Bayandin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-08-17 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.
|
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.
|
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.
|
130
|
+
rubygems_version: 3.0.3
|
131
131
|
signing_key:
|
132
132
|
specification_version: 4
|
133
133
|
summary: Run cucumber in parallel
|