diff --git a/CHANGELOG.md b/CHANGELOG.md index ca632533b..98298db65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/cucumber/cli/main.rb b/lib/cucumber/cli/main.rb index a550ac984..2b14732a2 100644 --- a/lib/cucumber/cli/main.rb +++ b/lib/cucumber/cli/main.rb @@ -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) @@ -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 || ''}" + backtrace = thread.backtrace + + if backtrace&.any? + backtrace.each do |line| + @err.puts("#{prefix} #{line}") + end + else + @err.puts("#{prefix} ") + end + end + end + end + def runtime(existing_runtime) return Runtime.new(configuration) unless existing_runtime diff --git a/spec/cucumber/cli/main_spec.rb b/spec/cucumber/cli/main_spec.rb index e8bc4e030..cce413508 100644 --- a/spec/cucumber/cli/main_spec.rb +++ b/spec/cucumber/cli/main_spec.rb @@ -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} core\/thread.rb:/ + else + pattern = RUBY_VERSION >= '3.4' ? /'Process\.kill'/ : /`kill'/ + pattern = /Thread TID-#{tid} #{__FILE__}:#{kill_line}:in #{pattern}/ + end + + expect(stderr.string).to match(pattern) + end + end end [Cucumber::Cli::ProfilesNotDefinedError, Cucumber::Cli::YmlLoadError, Cucumber::Cli::ProfileNotFound].each do |exception_klass|