Skip to content

Commit 3a00c78

Browse files
committed
Show the backtrace of the subprocess and signal it on timeout if there is a subprocess
1 parent 34e127f commit 3a00c78

File tree

3 files changed

+87
-34
lines changed

3 files changed

+87
-34
lines changed

spec/mspec/lib/mspec/helpers/ruby_exe.rb

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -140,28 +140,44 @@ def ruby_exe(code = :not_given, opts = {})
140140
expected_status = opts.fetch(:exit_status, 0)
141141

142142
begin
143-
platform_is_not :opal do
144-
command = ruby_cmd(code, opts)
145-
output = `#{command}`
146-
status = Process.last_status
147-
148-
exit_status = if status.exited?
149-
status.exitstatus
150-
elsif status.signaled?
151-
signame = Signal.signame status.termsig
152-
raise "No signal name?" unless signame
153-
:"SIG#{signame}"
154-
else
155-
raise SpecExpectationNotMetError, "#{exit_status.inspect} is neither exited? nor signaled?"
156-
end
157-
if exit_status != expected_status
158-
formatted_output = output.lines.map { |line| " #{line}" }.join
159-
raise SpecExpectationNotMetError,
160-
"Expected exit status is #{expected_status.inspect} but actual is #{exit_status.inspect} for command ruby_exe(#{command.inspect})\nOutput:\n#{formatted_output}"
143+
command = ruby_cmd(code, opts)
144+
145+
# Try to avoid the extra shell for 2>&1
146+
# This is notably useful for TimeoutAction which can then signal the ruby subprocess and not the shell
147+
popen_options = []
148+
if command.end_with?(' 2>&1')
149+
command = command[0...-5]
150+
popen_options = [{ err: [:child, :out] }]
151+
end
152+
153+
output = IO.popen(command, *popen_options) do |io|
154+
pid = io.pid
155+
MSpec.subprocesses << pid
156+
begin
157+
io.read
158+
ensure
159+
MSpec.subprocesses.delete(pid)
161160
end
161+
end
162162

163-
output
163+
status = Process.last_status
164+
165+
exit_status = if status.exited?
166+
status.exitstatus
167+
elsif status.signaled?
168+
signame = Signal.signame status.termsig
169+
raise "No signal name?" unless signame
170+
:"SIG#{signame}"
171+
else
172+
raise SpecExpectationNotMetError, "#{exit_status.inspect} is neither exited? nor signaled?"
173+
end
174+
if exit_status != expected_status
175+
formatted_output = output.lines.map { |line| " #{line}" }.join
176+
raise SpecExpectationNotMetError,
177+
"Expected exit status is #{expected_status.inspect} but actual is #{exit_status.inspect} for command ruby_exe(#{command.inspect})\nOutput:\n#{formatted_output}"
164178
end
179+
180+
output
165181
ensure
166182
saved_env.each { |key, value| ENV[key] = value }
167183
env.keys.each do |key|

spec/mspec/lib/mspec/runner/actions/timeout.rb

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ def initialize(timeout)
33
@timeout = timeout
44
@queue = Queue.new
55
@started = now
6+
@fail = false
7+
@error_message = "took longer than the configured timeout of #{@timeout}s"
68
end
79

810
def register
@@ -37,15 +39,25 @@ def start
3739
elapsed = now - @started
3840
if elapsed > @timeout
3941
if @current_state
40-
STDERR.puts "\nExample took longer than the configured timeout of #{@timeout}s:"
42+
STDERR.puts "\nExample #{@error_message}:"
4143
STDERR.puts "#{@current_state.description}"
4244
else
43-
STDERR.puts "\nSome code outside an example took longer than the configured timeout of #{@timeout}s"
45+
STDERR.puts "\nSome code outside an example #{@error_message}"
4446
end
4547
STDERR.flush
4648

4749
show_backtraces
48-
exit 2
50+
if MSpec.subprocesses.empty?
51+
exit 2
52+
else
53+
# Do not exit but signal the subprocess so we can get their output
54+
MSpec.subprocesses.each do |pid|
55+
Process.kill :SIGTERM, pid
56+
end
57+
@fail = true
58+
@current_state = nil
59+
break # stop this thread, will fail in #after
60+
end
4961
end
5062
end
5163
end
@@ -65,6 +77,11 @@ def after(state = nil)
6577
@queue << -> do
6678
@current_state = nil
6779
end
80+
81+
if @fail
82+
STDERR.puts "\n\nThe last example #{@error_message}. See above for the subprocess stacktrace."
83+
exit 2
84+
end
6885
end
6986

7087
def finish
@@ -73,19 +90,38 @@ def finish
7390
end
7491

7592
private def show_backtraces
76-
if RUBY_ENGINE == 'truffleruby'
77-
STDERR.puts 'Java stacktraces:'
78-
Process.kill :SIGQUIT, Process.pid
79-
sleep 1
80-
end
93+
java_stacktraces = -> pid {
94+
if RUBY_ENGINE == 'truffleruby' || RUBY_ENGINE == 'jruby'
95+
STDERR.puts 'Java stacktraces:'
96+
Process.kill :SIGQUIT, pid
97+
sleep 1
98+
end
99+
}
81100

82-
STDERR.puts "\nRuby backtraces:"
83-
if defined?(Truffle::Debug.show_backtraces)
84-
Truffle::Debug.show_backtraces
101+
if MSpec.subprocesses.empty?
102+
java_stacktraces.call Process.pid
103+
104+
STDERR.puts "\nRuby backtraces:"
105+
if defined?(Truffle::Debug.show_backtraces)
106+
Truffle::Debug.show_backtraces
107+
else
108+
Thread.list.each do |thread|
109+
unless thread == Thread.current
110+
STDERR.puts thread.inspect, thread.backtrace, ''
111+
end
112+
end
113+
end
85114
else
86-
Thread.list.each do |thread|
87-
unless thread == Thread.current
88-
STDERR.puts thread.inspect, thread.backtrace, ''
115+
MSpec.subprocesses.each do |pid|
116+
STDERR.puts "\nFor subprocess #{pid}"
117+
java_stacktraces.call pid
118+
119+
if RUBY_ENGINE == 'truffleruby'
120+
STDERR.puts "\nRuby backtraces:"
121+
Process.kill :SIGALRM, pid
122+
sleep 1
123+
else
124+
STDERR.puts "Don't know how to print backtraces of a subprocess on #{RUBY_ENGINE}"
89125
end
90126
end
91127
end

spec/mspec/lib/mspec/runner/mspec.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,10 @@ module MSpec
3838
@expectation = nil
3939
@expectations = false
4040
@skips = []
41+
@subprocesses = []
4142

4243
class << self
43-
attr_reader :file, :include, :exclude, :skips
44+
attr_reader :file, :include, :exclude, :skips, :subprocesses
4445
attr_writer :repeat, :randomize
4546
attr_accessor :formatter
4647
end

0 commit comments

Comments
 (0)