Skip to content
Open
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Please visit [cucumber/CONTRIBUTING.md](https://github.com/cucumber/cucumber/blo
- A first initial iteration of the new `cucumber-query` structure ([#1801](https://github.com/cucumber/cucumber-ruby/pull/1801) [luke-hill](https://github.com/luke-hill))
> This will be used for the migration of all existing formatters - becoming the building blocks for the future of cucumber formatters
> which will begin being migrated in the start of 2026
- Print thread backtraces on SIGINFO/SIGPWR ([#1830](https://github.com/cucumber/cucumber-ruby/pull/1830)) [sobrinho](https://github.com/sobrinho)

### Changed
- Use the test result type 'ambiguous' added to cucumber-ruby-core when steps are ambiguous
Expand Down
22 changes: 22 additions & 0 deletions lib/cucumber/cli/main.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def initialize(args, out = $stdout, err = $stderr, kernel = Kernel)

def execute!(existing_runtime = nil)
trap_interrupt
trap_thread_dump_signal

runtime = runtime(existing_runtime)

Expand Down Expand Up @@ -90,6 +91,27 @@ def trap_interrupt
end
end

def trap_thread_dump_signal
signal = %w[INFO PWR].find { |s| Signal.list.key?(s) }

return if signal.nil?

trap(signal) do
Thread.list.each do |thread|
prefix = "Thread TID-#{(thread.object_id ^ Process.pid).to_s(36)} #{thread.name || '<no name>'}"
backtrace = thread.backtrace

if backtrace&.any?
backtrace.each do |line|
@err.puts("#{prefix} #{line}")
end
else
@err.puts("#{prefix} <no backtrace available>")
end
end
end
end

def runtime(existing_runtime)
return Runtime.new(configuration) unless existing_runtime

Expand Down
39 changes: 39 additions & 0 deletions spec/cucumber/cli/main_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,45 @@ def do_execute
subject.execute!
end
end

thread_dump_signal = %w[INFO PWR].find { |s| Signal.list.key?(s) }

context 'when interrupted with thread dump signal', skip: thread_dump_signal.nil? do
let(:runtime) { double('runtime').as_null_object }

it 'dumps the thread backtrace to the error stream' do
kill_line = 0

allow(runtime).to receive(:run!) do
Process.kill(thread_dump_signal, Process.pid)
kill_line = __LINE__ - 1
end

allow(runtime).to receive(:failure?).and_return(false)

expect(kernel).to receive(:exit).with(0)

subject.execute!(runtime)

tid = (Thread.current.object_id ^ Process.pid).to_s(36)

if defined?(JRUBY_VERSION)
pattern = /Thread TID-[0-9a-z]+ SIGPWR handler /

# JRuby runs signal handlers asynchronously on a dedicated thread
deadline = Time.now + 2
sleep 0.01 while stderr.string !~ pattern && Time.now < deadline
elsif defined?(TruffleRuby)
# TruffleRuby does not dump the actual Process.kill call, but rather the internal core/thread.rb call
pattern = /Thread TID-#{tid} <no name> <internal:core> core\/thread.rb:/
else
pattern = RUBY_VERSION >= '3.4' ? /'Process\.kill'/ : /`kill'/
pattern = /Thread TID-#{tid} <no name> #{__FILE__}:#{kill_line}:in #{pattern}/
end

expect(stderr.string).to match(pattern)
end
Comment thread
sobrinho marked this conversation as resolved.
end
end

[Cucumber::Cli::ProfilesNotDefinedError, Cucumber::Cli::YmlLoadError, Cucumber::Cli::ProfileNotFound].each do |exception_klass|
Expand Down
Loading