├── lib ├── debug │ ├── version.rb │ ├── start.rb │ ├── open.rb │ ├── open_nonstop.rb │ ├── irb_integration.rb │ ├── prelude.rb │ ├── abbrev_command.rb │ ├── local.rb │ ├── color.rb │ ├── source_repository.rb │ ├── frame_info.rb │ ├── tracer.rb │ ├── console.rb │ └── client.rb └── debug.rb ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── custom.md │ ├── feature_request.md │ └── bug_report.md ├── pull_request_template.md ├── workflows │ ├── truffleruby.yml │ ├── ruby-macos.yaml │ ├── ruby.yml │ ├── protocol.yml │ └── test_test.yml └── actions │ └── launchable │ └── setup │ └── action.yaml ├── bin ├── setup ├── console └── gentest ├── Gemfile ├── .gitignore ├── test ├── protocol │ ├── terminate_test.rb │ ├── finish_test.rb │ ├── disconnect_cdp_test.rb │ ├── watch_test.rb │ ├── threads_test.rb │ ├── delete_test.rb │ ├── step_back_test.rb │ ├── catch_test.rb │ ├── disconnect_dap_test.rb │ ├── next_test.rb │ ├── step_test.rb │ ├── binary_data_dap_test.rb │ ├── call_stack_with_skip_dap_test.rb │ ├── eval_test.rb │ ├── hover_test.rb │ ├── break_test.rb │ ├── variables_test.rb │ └── rdbgTraceInspctor_test.rb ├── console │ ├── load_test.rb │ ├── thread_test.rb │ ├── rescue_test.rb │ ├── quit_test.rb │ ├── kill_test.rb │ ├── edit_test.rb │ ├── help_test.rb │ ├── trap_test.rb │ ├── daemon_test.rb │ ├── client_test.rb │ ├── eval_test.rb │ ├── print_test.rb │ ├── whereami_test.rb │ ├── delete_test.rb │ ├── frame_block_identifier_test.rb │ ├── session_test.rb │ ├── display_test.rb │ ├── list_test.rb │ ├── nested_break_test.rb │ ├── outline_test.rb │ ├── config_postmortem_test.rb │ ├── irb_test.rb │ ├── frame_control_test.rb │ ├── color_test.rb │ ├── debugger_local_test.rb │ ├── rdbg_option_test.rb │ ├── watch_test.rb │ ├── config_fork_test.rb │ ├── debugger_method_test.rb │ ├── catch_test.rb │ └── backtrace_test.rb └── support │ ├── protocol_test_case_test.rb │ ├── test_case_test.rb │ ├── assertions_test.rb │ ├── assertions.rb │ ├── dap_utils.rb │ └── cdp_utils.rb ├── TODO.md ├── ext └── debug │ ├── extconf.rb │ ├── iseq_collector.c │ └── debug.c ├── LICENSE.txt ├── debug.gemspec ├── exe └── rdbg └── Rakefile /lib/debug/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DEBUGGER__ 4 | VERSION = "1.11.0" 5 | end 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/debug/start.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'session' 4 | return unless defined?(DEBUGGER__) 5 | DEBUGGER__.start no_sigint_hook: false 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Blank issue 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem "rake" 6 | gem "rake-compiler" 7 | gem "test-unit", "~> 3.0" 8 | gem "test-unit-rr" 9 | gem "json-schema" 10 | gem "test-unit-launchable" 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | *.bundle 10 | /Gemfile.lock 11 | /lib/debug/debug.so 12 | .ruby-version 13 | /debugAdapterProtocol.json 14 | /chromeDevToolsProtocol.json 15 | -------------------------------------------------------------------------------- /lib/debug.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if ENV['RUBY_DEBUG_LAZY'] 4 | require_relative 'debug/prelude' 5 | else 6 | require_relative 'debug/session' 7 | return unless defined?(DEBUGGER__) 8 | DEBUGGER__::start no_sigint_hook: true, nonstop: true 9 | end 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Your proposal** 11 | What is your idea? 12 | 13 | **Additional context** 14 | Add any other context or screenshots about the feature request here. 15 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "debug" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /lib/debug/open.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # Open the door for the debugger to connect. 4 | # Users can connect to debuggee program with "rdbg --attach" option. 5 | # 6 | # If RUBY_DEBUG_PORT envval is provided (digits), open TCP/IP port. 7 | # Otherwise, UNIX domain socket is used. 8 | # 9 | 10 | require_relative 'session' 11 | return unless defined?(DEBUGGER__) 12 | 13 | DEBUGGER__.open 14 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Thanks for your Pull Request 🎉 2 | 3 | **Please follow these instructions to help us review it more efficiently:** 4 | 5 | - Add references of related issues/PRs in the description if available. 6 | - If you're updating the readme file, make sure you followed [the instruction here](https://github.com/ruby/debug/blob/master/CONTRIBUTING.md#to-update-readme). 7 | 8 | ## Description 9 | Describe your changes: 10 | -------------------------------------------------------------------------------- /test/protocol/terminate_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/protocol_test_case' 4 | 5 | module DEBUGGER__ 6 | 7 | class TerminateTest < ProtocolTestCase 8 | PROGRAM = <<~RUBY 9 | 1| a = 1 10 | RUBY 11 | 12 | def test_terminate_request_terminates_the_debuggee 13 | run_protocol_scenario PROGRAM do 14 | req_terminate_debuggee 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | ## Basic functionality 4 | 5 | * Support Fibers and Ractors 6 | 7 | ## UI 8 | 9 | * Multi-line support 10 | * Completion for Ruby's code 11 | * Interactive breakpoint setting 12 | * Interactive record & play debugging 13 | * irb integration 14 | 15 | ## Debug command 16 | 17 | * Watch points 18 | * Lightweight watchpoints for instance variables with Ruby 3.3 features (TP:ivar_set) 19 | * Alias 20 | 21 | ## Debug port 22 | 23 | * Debug port for monitoring 24 | -------------------------------------------------------------------------------- /lib/debug/open_nonstop.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # Open the door for the debugger to connect. 4 | # Unlike debug/open, it does not stop at the beginning of the program. 5 | # Users can connect to debuggee program with "rdbg --attach" option or 6 | # VSCode attach type. 7 | # 8 | # If RUBY_DEBUG_PORT envval is provided (digits), open TCP/IP port. 9 | # Otherwise, UNIX domain socket is used. 10 | # 11 | 12 | require_relative 'session' 13 | return unless defined?(DEBUGGER__) 14 | 15 | DEBUGGER__.open(nonstop: true) 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Your environment** 11 | 12 | * `ruby -v`: 13 | * `rdbg -v`: 14 | 15 | **Describe the bug** 16 | A clear and concise description of what the bug is. 17 | 18 | **To Reproduce** 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /test/console/load_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | class LoadTest < ConsoleTestCase 7 | def program 8 | <<~RUBY 9 | 1| r = require 'debug' 10 | 2| binding.break 11 | RUBY 12 | end 13 | 14 | def test_require_debug_should_return_false 15 | debug_code(program) do 16 | type "c" 17 | type "p r" 18 | assert_line_text('false') 19 | type 'kill!' 20 | end 21 | end 22 | end 23 | end 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/console/thread_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../support/console_test_case' 2 | 3 | module DEBUGGER__ 4 | class ThreadControlTest < ConsoleTestCase 5 | def program 6 | <<~RUBY 7 | 1| def foo 8 | 2| Thread.new { sleep 5 } 9 | 3| end 10 | 4| 11 | 5| 5.times do 12 | 6| foo 13 | 7| binding.b(do: "1 == 2") # eval Ruby code in debugger 14 | 8| end 15 | RUBY 16 | end 17 | 18 | def test_debugger_isnt_hung_by_new_threads 19 | debug_code(program) do 20 | type "c" 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/console/rescue_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | class RescueTest < ConsoleTestCase 7 | def program 8 | <<~RUBY 9 | 1| 1.times do 10 | 2| begin 11 | 3| raise 12 | 4| rescue 13 | 5| p :ok 14 | 6| end 15 | 7| end 16 | RUBY 17 | end 18 | 19 | def test_rescue 20 | debug_code program, remote: false do 21 | type 's' 22 | type 's' 23 | type 'c' 24 | end 25 | end 26 | end if RUBY_VERSION.to_f >= 3.5 27 | end 28 | 29 | -------------------------------------------------------------------------------- /.github/workflows/truffleruby.yml: -------------------------------------------------------------------------------- 1 | name: TruffleRuby 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | truffleruby: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 30 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Ruby 16 | uses: ruby/setup-ruby@v1 17 | with: 18 | ruby-version: truffleruby-head 19 | bundler-cache: true 20 | - name: Test installing the gem on TruffleRuby 21 | run: | 22 | bundle exec rake compile 23 | bundle exec rake build 24 | gem install pkg/debug-*.gem 25 | -------------------------------------------------------------------------------- /test/protocol/finish_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/protocol_test_case' 4 | 5 | module DEBUGGER__ 6 | class FinishTest < ProtocolTestCase 7 | PROGRAM = <<~RUBY 8 | 1| module Foo 9 | 2| class Bar 10 | 3| def self.a 11 | 4| "hello" 12 | 5| end 13 | 6| end 14 | 7| Bar.a 15 | 8| bar = Bar.new 16 | 9| end 17 | RUBY 18 | 19 | def test_finish_leaves_from_the_method 20 | run_protocol_scenario PROGRAM do 21 | req_add_breakpoint 3 22 | req_continue 23 | assert_line_num 4 24 | req_finish 25 | assert_line_num 5 26 | req_terminate_debuggee 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/protocol/disconnect_cdp_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/protocol_test_case' 4 | 5 | module DEBUGGER__ 6 | class DisconnectCDPTest < ProtocolTestCase 7 | PROGRAM = <<~RUBY 8 | 1| module Foo 9 | 2| class Bar 10 | 3| def self.a 11 | 4| "hello" 12 | 5| end 13 | 6| end 14 | 7| loop do 15 | 8| b = 1 16 | 9| end 17 | 10| Bar.a 18 | 11| bar = Bar.new 19 | 12| end 20 | RUBY 21 | 22 | def test_closing_cdp_connection_doesnt_kill_the_debuggee 23 | run_protocol_scenario PROGRAM, dap: false do 24 | req_cdp_disconnect 25 | attach_to_cdp_server 26 | req_terminate_debuggee 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/protocol/watch_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/protocol_test_case' 4 | 5 | module DEBUGGER__ 6 | class WatchTest < ProtocolTestCase 7 | PROGRAM = <<~RUBY 8 | 1| a = 2 9 | 2| a += 1 10 | 3| a += 1 11 | 4| d = 4 12 | 5| a += 1 13 | 6| e = 5 14 | 7| f = 6 15 | RUBY 16 | 17 | def test_watch_matches_with_stopped_place 18 | run_protocol_scenario PROGRAM do 19 | req_next 20 | assert_watch_result({value: '2', type: 'Integer'}, 'a') 21 | req_next 22 | assert_watch_result({value: '3', type: 'Integer'}, 'a') 23 | req_next 24 | assert_watch_result({value: '4', type: 'Integer'}, 'a') 25 | req_terminate_debuggee 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/protocol/threads_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/protocol_test_case' 4 | 5 | module DEBUGGER__ 6 | class ThreadsTest < ProtocolTestCase 7 | PROGRAM = <<~RUBY 8 | 1| def foo 9 | 2| Thread.new { sleep 30 } 10 | 3| end 11 | 4| 12 | 5| foo 13 | 6| sleep 0.1 # make sure the thread stops 14 | 7| binding.b 15 | RUBY 16 | 17 | def test_reponse_returns_correct_threads_info 18 | run_protocol_scenario PROGRAM, cdp: false do 19 | req_continue 20 | 21 | assert_threads_result( 22 | [ 23 | /\.rb:\d:in [`']
'/, 24 | /\.rb:\d:in [`']block in (Object#)?foo'/ 25 | ] 26 | ) 27 | 28 | req_terminate_debuggee 29 | end 30 | end 31 | end 32 | end 33 | 34 | -------------------------------------------------------------------------------- /test/protocol/delete_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/protocol_test_case' 4 | 5 | module DEBUGGER__ 6 | class DeleteTest < ProtocolTestCase 7 | PROGRAM = <<~RUBY 8 | 1| module Foo 9 | 2| class Bar 10 | 3| def self.a 11 | 4| 'hello' 12 | 5| end 13 | 6| end 14 | 7| Bar.a 15 | 8| bar = Bar.new 16 | 9| end 17 | RUBY 18 | 19 | def test_delete_deletes_specific_breakpoints 20 | run_protocol_scenario PROGRAM do 21 | req_add_breakpoint 3 22 | req_add_breakpoint 5 23 | req_add_breakpoint 8 24 | req_delete_breakpoint 0 25 | req_delete_breakpoint 0 26 | req_continue 27 | assert_line_num 8 28 | req_terminate_debuggee 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/console/quit_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | class QuitTest < ConsoleTestCase 7 | def program 8 | <<~RUBY 9 | 1| a=1 10 | RUBY 11 | end 12 | 13 | def test_quit_quits_debugger_process_if_confirmed 14 | debug_code(program) do 15 | type 'q' 16 | assert_line_text(/Really quit\? \[Y\/n\]/) 17 | type 'y' 18 | end 19 | end 20 | 21 | def test_quit_does_not_quit_debugger_process_if_not_confirmed 22 | debug_code(program) do 23 | type 'q' 24 | assert_line_text(/Really quit\? \[Y\/n\]/) 25 | type 'n' 26 | type 'kill!' 27 | end 28 | end 29 | 30 | def test_quit_with_exclamation_mark_quits_immediately_debugger_process 31 | debug_code(program) do 32 | type 'q!' 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/console/kill_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | class KillTest < ConsoleTestCase 7 | def program 8 | <<~RUBY 9 | 1| a = 1 10 | RUBY 11 | end 12 | 13 | def test_kill_kills_the_debugger_process_if_confirmed 14 | debug_code(program) do 15 | type 'kill' 16 | assert_line_text(/Really kill\? \[Y\/n\]/) 17 | type 'y' 18 | end 19 | end 20 | 21 | def test_kill_does_not_kill_the_debugger_process_if_not_confirmed 22 | debug_code(program) do 23 | type 'kill' 24 | assert_line_text(/Really kill\? \[Y\/n\]/) 25 | type 'n' 26 | type 'kill!' 27 | end 28 | end 29 | 30 | def test_kill_with_exclamation_mark_kills_the_debugger_process_immediately 31 | debug_code(program) do 32 | type "kill!" 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/support/protocol_test_case_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'protocol_test_case' 4 | 5 | module DEBUGGER__ 6 | class TestProtocolTestCase < ProtocolTestCase 7 | def test_the_test_fails_when_debuggee_doesnt_exit 8 | omit "too slow now" 9 | 10 | program = <<~RUBY 11 | 1| a=1 12 | RUBY 13 | 14 | assert_fail_assertion do 15 | run_protocol_scenario program do 16 | end 17 | end 18 | end 19 | 20 | def test_the_assertion_failure_takes_presedence_over_debuggee_not_exiting 21 | program = <<~RUBY 22 | 1| a = 2 23 | 2| b = 3 24 | RUBY 25 | 26 | assert_raise_message(/<\"100\"> expected but was/) do 27 | run_protocol_scenario program do 28 | req_add_breakpoint 2 29 | req_continue 30 | assert_repl_result({value: '100', type: 'Integer'}, 'a') 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /ext/debug/extconf.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | require_relative '../../lib/debug/version' 3 | File.write("debug_version.h", "#define RUBY_DEBUG_VERSION \"#{DEBUGGER__::VERSION}\"\n") 4 | $distcleanfiles << "debug_version.h" 5 | 6 | if defined? RubyVM 7 | $defs << '-DHAVE_RB_ISEQ' 8 | $defs << '-DHAVE_RB_ISEQ_PARAMETERS' 9 | $defs << '-DHAVE_RB_ISEQ_CODE_LOCATION' 10 | 11 | if RUBY_VERSION >= '3.1.0' 12 | $defs << '-DHAVE_RB_ISEQ_TYPE' 13 | end 14 | else 15 | # not on MRI 16 | 17 | have_func "rb_iseq_parameters(NULL, 0)", 18 | [["VALUE rb_iseq_parameters(void *, int is_proc);"]] 19 | 20 | have_func "rb_iseq_code_location(NULL, NULL, NULL, NULL, NULL)", 21 | [["void rb_iseq_code_location(void *, int *first_lineno, int *first_column, int *last_lineno, int *last_column);"]] 22 | # from Ruby 3.1 23 | have_func "rb_iseq_type(NULL)", 24 | [["VALUE rb_iseq_type(void *);"]] 25 | end 26 | 27 | create_makefile 'debug/debug' 28 | -------------------------------------------------------------------------------- /test/console/edit_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | class EditTest < ConsoleTestCase 7 | def program 8 | <<~RUBY 9 | 1| a = 1 10 | RUBY 11 | end 12 | 13 | def test_edit_opens_the_editor 14 | ENV["EDITOR"] = "null_editor" 15 | 16 | debug_code(program, remote: false) do 17 | type "edit" 18 | assert_line_text(/command: null_editor/) 19 | type "continue" 20 | end 21 | end 22 | 23 | def test_edit_shows_warning_when_the_file_can_not_be_found 24 | ENV["EDITOR"] = "null_editor" 25 | 26 | debug_code(program, remote: false) do 27 | type "edit foo.rb" 28 | assert_line_text(/not found/) 29 | type "continue" 30 | end 31 | end 32 | 33 | def test_edit_shows_warning_when_editor_env_is_not_set 34 | ENV["EDITOR"] = nil 35 | 36 | debug_code(program, remote: false) do 37 | type "edit" 38 | assert_line_text(/can not find editor setting/) 39 | type "continue" 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/debug/irb_integration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'irb' 4 | 5 | module DEBUGGER__ 6 | module IrbPatch 7 | def evaluate(line, line_no) 8 | SESSION.send(:restart_all_threads) 9 | super 10 | # This is to communicate with the test framework so it can feed the next input 11 | puts "INTERNAL_INFO: {}" if ENV['RUBY_DEBUG_TEST_UI'] == 'terminal' 12 | ensure 13 | SESSION.send(:stop_all_threads) 14 | end 15 | end 16 | 17 | class ThreadClient 18 | def activate_irb_integration 19 | IRB.setup(location, argv: []) 20 | workspace = IRB::WorkSpace.new(current_frame&.binding || TOPLEVEL_BINDING) 21 | irb = IRB::Irb.new(workspace) 22 | IRB.conf[:MAIN_CONTEXT] = irb.context 23 | IRB::Debug.setup(irb) 24 | IRB::Context.prepend(IrbPatch) 25 | end 26 | end 27 | 28 | class Session 29 | def deactivate_irb_integration 30 | Reline.completion_proc = nil 31 | Reline.output_modifier_proc = nil 32 | Reline.autocompletion = false 33 | Reline.dig_perfect_match_proc = nil 34 | reset_ui UI_LocalConsole.new 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/console/help_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | class HelpTest < ConsoleTestCase 7 | def program 8 | <<~RUBY 9 | 1| a = 1 10 | RUBY 11 | end 12 | 13 | def test_help_prints_all_help_messages_by_default 14 | debug_code(program) do 15 | type "help" 16 | assert_line_text( 17 | [ 18 | /### Breakpoint/, 19 | /Show all breakpoints/, 20 | /### Frame control/ 21 | ] 22 | ) 23 | type "continue" 24 | end 25 | end 26 | 27 | def test_help_only_prints_given_command_when_specified 28 | debug_code(program) do 29 | type "help break" 30 | assert_line_text(/Show all breakpoints/) 31 | assert_no_line_text(/### Frame control/) 32 | type "continue" 33 | end 34 | end 35 | 36 | def test_help_with_undefined_command_shows_an_error 37 | debug_code(program) do 38 | type 'help foo' 39 | assert_line_text(/not found: foo/) 40 | type 'kill!' 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /bin/gentest: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'optparse' 4 | 5 | require_relative '../test/tool/test_builder' 6 | 7 | options = {} 8 | 9 | OptionParser.new do |opt| 10 | opt.banner = 'Usage: bin/gentest [file] [option]' 11 | opt.on('-m METHOD', 'Method name in the test file') do |m| 12 | options[:method] = m 13 | end 14 | opt.on('-c CLASS', 'Class name in the test file') do |c| 15 | options[:class] = c 16 | end 17 | opt.on('-f FILENAME', 'File name of the test file') do |name| 18 | options[:file_name] = name 19 | end 20 | opt.on('--open=FRONTEND', 'Start remote debugging with opening the network port.', 21 | 'Prepare for the given FRONTEND.') do |f| 22 | options[:open] = f.downcase 23 | end 24 | opt.parse!(ARGV) 25 | end 26 | 27 | exit if ARGV.empty? 28 | 29 | case options[:open] 30 | when 'vscode' 31 | DEBUGGER__::DAPTestBuilder.new(ARGV, options[:method], options[:class], options[:file_name]).start 32 | when 'chrome' 33 | DEBUGGER__::CDPTestBuilder.new(ARGV, options[:method], options[:class], options[:file_name]).start 34 | else 35 | DEBUGGER__::LocalTestBuilder.new(ARGV, options[:method], options[:class], options[:file_name]).start 36 | end 37 | -------------------------------------------------------------------------------- /.github/workflows/ruby-macos.yaml: -------------------------------------------------------------------------------- 1 | name: Ruby(macOS) 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | ruby-versions: 11 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 12 | with: 13 | engine: cruby 14 | min_version: 3.2 15 | 16 | test: 17 | needs: ruby-versions 18 | runs-on: macos-latest 19 | timeout-minutes: 15 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | ruby-version: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | # Set fetch-depth: 10 so that Launchable can receive commits information. 29 | fetch-depth: 10 30 | - name: Set up Launchable 31 | uses: ./.github/actions/launchable/setup 32 | with: 33 | os: macos-latest 34 | test-task: test_console 35 | - name: Set up Ruby 36 | uses: ruby/setup-ruby@v1 37 | with: 38 | ruby-version: ${{ matrix.ruby-version }} 39 | bundler-cache: true 40 | - name: Set up tests 41 | run: | 42 | bundle exec rake clobber 43 | bundle exec rake compile 44 | - name: Run tests 45 | run: bundle exec rake test_console 46 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | ruby-versions: 11 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 12 | with: 13 | engine: cruby 14 | min_version: 2.7 15 | versions: '["debug"]' 16 | 17 | test: 18 | needs: ruby-versions 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 30 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | ruby-version: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | with: 29 | # Set fetch-depth: 10 so that Launchable can receive commits information. 30 | fetch-depth: 10 31 | - name: Set up Launchable 32 | uses: ./.github/actions/launchable/setup 33 | with: 34 | os: ubuntu-latest 35 | test-task: test_console 36 | - name: Set up Ruby 37 | uses: ruby/setup-ruby@v1 38 | with: 39 | ruby-version: ${{ matrix.ruby-version }} 40 | bundler-cache: true 41 | - name: Set up tests 42 | run: | 43 | bundle exec rake clobber 44 | bundle exec rake compile 45 | - name: Run tests 46 | run: bundle exec rake test_console 47 | -------------------------------------------------------------------------------- /.github/workflows/protocol.yml: -------------------------------------------------------------------------------- 1 | name: Protocol 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | ruby-versions: 11 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 12 | with: 13 | engine: cruby 14 | min_version: 2.7 15 | versions: '["debug"]' 16 | 17 | test: 18 | needs: ruby-versions 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 10 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | ruby-version: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | with: 29 | # Set fetch-depth: 10 so that Launchable can receive commits information. 30 | fetch-depth: 10 31 | - name: Set up Launchable 32 | uses: ./.github/actions/launchable/setup 33 | with: 34 | os: ubuntu-latest 35 | test-task: test_protocol 36 | - name: Set up Ruby 37 | uses: ruby/setup-ruby@v1 38 | with: 39 | ruby-version: ${{ matrix.ruby-version }} 40 | bundler-cache: true 41 | - name: Set up tests 42 | run: | 43 | bundle exec rake clobber 44 | bundle exec rake compile 45 | - name: Run tests 46 | run: bundle exec rake test_protocol 47 | -------------------------------------------------------------------------------- /.github/workflows/test_test.yml: -------------------------------------------------------------------------------- 1 | name: TestTestFramework 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | ruby-versions: 11 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 12 | with: 13 | engine: cruby 14 | min_version: 3.0 15 | versions: '["debug"]' 16 | 17 | test: 18 | needs: ruby-versions 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 30 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | ruby-version: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | with: 29 | # Set fetch-depth: 10 so that Launchable can receive commits information. 30 | fetch-depth: 10 31 | - name: Set up Launchable 32 | uses: ./.github/actions/launchable/setup 33 | with: 34 | os: ubuntu-latest 35 | test-task: test_test 36 | - name: Set up Ruby 37 | uses: ruby/setup-ruby@v1 38 | with: 39 | ruby-version: ${{ matrix.ruby-version }} 40 | bundler-cache: true 41 | - name: Set up tests 42 | run: | 43 | bundle exec rake clobber 44 | bundle exec rake compile 45 | - name: Run tests 46 | run: bundle exec rake test_test 47 | -------------------------------------------------------------------------------- /test/console/trap_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | class TrapTest < ConsoleTestCase 7 | def program 8 | <<~RUBY 9 | 1| trap('SIGINT'){ puts "SIGINT" } 10 | 2| Process.kill('SIGINT', Process.pid) 11 | 3| p :ok 12 | RUBY 13 | end 14 | 15 | def test_sigint 16 | debug_code program, remote: false do 17 | type 'b 3' 18 | type 'c' 19 | assert_line_text(/is registered as SIGINT handler/) 20 | type 'sigint' 21 | assert_line_num 3 22 | assert_line_text(/SIGINT/) 23 | type 'c' 24 | end 25 | end 26 | 27 | def test_trap_with 28 | debug_code %q{ 29 | 1| trap(:INT){} # Symbol 30 | 2| _ = 1 31 | }, remote: false do 32 | type 'n' 33 | type 'n' 34 | end 35 | 36 | debug_code %q{ 37 | 1| trap('INT'){} # String 38 | 2| _ = 1 39 | }, remote: false do 40 | type 'n' 41 | type 'n' 42 | end 43 | 44 | debug_code %q{ 45 | 1| trap(Signal.list['INT']){} if Signal.list['INT'] # Integer 46 | 2| _ = 1 47 | }, remote: false do 48 | type 'n' 49 | type 'n' 50 | end 51 | end 52 | end 53 | end 54 | 55 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /test/console/daemon_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | class DaemonTest < ConsoleTestCase 7 | 8 | def test_daemon 9 | # Ignore SIGHUP since the test debuggee receives SIGHUP after Process.daemon. 10 | # When manualy debugging a daemon, it doesn't receive SIGHUP. 11 | # I don't know why. 12 | program = <<~'RUBY' 13 | 1| trap(:HUP, 'IGNORE') 14 | 2| puts 'Daemon starting' 15 | 3| Process.daemon 16 | 4| puts 'Daemon started' 17 | RUBY 18 | 19 | # The program can't be debugged locally since the parent process exits when Process.daemon is called. 20 | debug_code program, remote: :remote_only do 21 | type 'b 3' 22 | type 'c' 23 | assert_line_num 3 24 | type 'b 4' 25 | type 'c' 26 | assert_line_num 4 27 | type 'c' 28 | end 29 | end 30 | 31 | def test_daemon_patch 32 | program = <<~'RUBY' 33 | 1| trap(:HUP, 'IGNORE') 34 | 2| puts 'Daemon starting' 35 | 3| Process.daemon(false, false) 36 | 4| puts 'Daemon started' 37 | RUBY 38 | 39 | debug_code program, remote: :remote_only do 40 | type 'b 3' 41 | type 'c' 42 | assert_line_num 3 43 | type 'b 4' 44 | type 'c' 45 | assert_line_num 4 46 | type 'c' 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/protocol/step_back_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/protocol_test_case' 4 | 5 | module DEBUGGER__ 6 | class StepBackTest < ProtocolTestCase 7 | PROGRAM = <<~RUBY 8 | 1| binding.b do: 'record on' 9 | 2| 10 | 3| module Foo 11 | 4| class Bar 12 | 5| def self.a 13 | 6| "hello" 14 | 7| end 15 | 8| end 16 | 9| Bar.a 17 | 10| bar = Bar.new 18 | 11| end 19 | RUBY 20 | 21 | def test_step_back_goes_back_to_the_previous_statement 22 | run_protocol_scenario PROGRAM, cdp: false do 23 | req_add_breakpoint 9 24 | req_continue 25 | req_step_back 26 | assert_line_num 9 27 | assert_locals_result( 28 | [ 29 | { name: "%self", value: "Foo", type: "Module" }, 30 | { name: "bar", value: "nil", type: "NilClass" } 31 | ] 32 | ) 33 | req_step_back 34 | assert_line_num 5 35 | assert_locals_result([ 36 | { name: "%self", value: "Foo::Bar", type: "Class" } 37 | ]) 38 | req_step_back 39 | assert_line_num 4 40 | assert_locals_result( 41 | [ 42 | { name: "%self", value: "Foo", type: "Module" }, 43 | { name: "bar", value: "nil", type: "NilClass" } 44 | ] 45 | ) 46 | req_terminate_debuggee 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/protocol/catch_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/protocol_test_case' 4 | 5 | module DEBUGGER__ 6 | class CatchTest < ProtocolTestCase 7 | PROGRAM = <<~RUBY 8 | 1| def foo 9 | 2| a = 1 10 | 3| raise "foo" 11 | 4| end 12 | 5| 13 | 6| foo 14 | RUBY 15 | 16 | def test_set_exception_breakpoints_sets_exception_breakpoints 17 | run_protocol_scenario PROGRAM do 18 | req_set_exception_breakpoints([{ name: "RuntimeError" }]) 19 | req_continue 20 | assert_line_num 3 21 | req_terminate_debuggee 22 | end 23 | end 24 | 25 | def test_set_exception_breakpoints_unsets_exception_breakpoints 26 | run_protocol_scenario PROGRAM, cdp: false do 27 | req_set_exception_breakpoints([{ name: "RuntimeError" }]) 28 | req_set_exception_breakpoints([]) 29 | req_terminate_debuggee 30 | end 31 | end 32 | 33 | def test_set_exception_breakpoints_accepts_condition 34 | run_protocol_scenario PROGRAM, cdp: false do 35 | req_set_exception_breakpoints([{ name: "RuntimeError", condition: "a == 2" }]) 36 | req_terminate_debuggee 37 | end 38 | 39 | run_protocol_scenario PROGRAM, cdp: false do 40 | req_set_exception_breakpoints([{ name: "RuntimeError", condition: "a == 1" }]) 41 | req_continue 42 | assert_line_num 3 43 | req_terminate_debuggee 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/protocol/disconnect_dap_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/protocol_test_case' 4 | 5 | module DEBUGGER__ 6 | 7 | class DisconnectDAPTest < ProtocolTestCase 8 | PROGRAM = <<~RUBY 9 | 1| module Foo 10 | 2| class Bar 11 | 3| def self.a 12 | 4| "hello" 13 | 5| end 14 | 6| end 15 | 7| loop do 16 | 8| b = 1 17 | 9| end 18 | 10| Bar.a 19 | 11| bar = Bar.new 20 | 12| end 21 | RUBY 22 | 23 | def test_disconnect_without_terminateDebuggee_keeps_debuggee_alive 24 | run_protocol_scenario PROGRAM, cdp: false do 25 | req_dap_disconnect(terminate_debuggee: false) 26 | attach_to_dap_server 27 | assert_reattached 28 | # suspends the debuggee so it'll take the later requests (include terminate) 29 | suspend_debugee 30 | req_terminate_debuggee 31 | end 32 | end 33 | 34 | def test_disconnect_with_terminateDebuggee_kills_debuggee 35 | run_protocol_scenario PROGRAM, cdp: false do 36 | req_dap_disconnect(terminate_debuggee: true) 37 | end 38 | end 39 | 40 | private 41 | 42 | def suspend_debugee 43 | send_dap_request "pause", threadId: 1 44 | end 45 | 46 | def assert_reattached 47 | res = find_crt_dap_response 48 | result_cmd = res.dig(:command) 49 | assert_equal 'configurationDone', result_cmd 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/console/client_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | require 'stringio' 5 | 6 | module DEBUGGER__ 7 | class ClientTest < ConsoleTestCase 8 | def test_gen_sockpath 9 | output = with_captured_stdout do 10 | Client.util("gen-sockpath") 11 | end 12 | 13 | assert_match(/rdbg-/, output) 14 | end 15 | 16 | def test_list_socks 17 | output = with_captured_stdout do 18 | Client.util("list-socks") 19 | end 20 | 21 | unless output.empty? 22 | assert_match(/rdbg-/, output) 23 | end 24 | end 25 | 26 | def test_unknown_command 27 | stdout = with_captured_stdout do 28 | stderr = with_captured_stderr do 29 | begin 30 | Client.util("fix-my-code") 31 | rescue Exception => e 32 | assert_equal SystemExit, e.class 33 | end 34 | end 35 | 36 | assert_equal "Unknown utility: fix-my-code\n", stderr 37 | end 38 | 39 | assert_equal "", stdout 40 | end 41 | 42 | def with_captured_stdout 43 | original_stdout = $stdout 44 | $stdout = StringIO.new 45 | yield 46 | $stdout.string 47 | ensure 48 | $stdout = original_stdout 49 | end 50 | 51 | def with_captured_stderr 52 | original_stderr = $stderr 53 | $stderr = StringIO.new 54 | yield 55 | $stderr.string 56 | ensure 57 | $stderr = original_stderr 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/console/eval_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | class EvalTest < ConsoleTestCase 7 | def program 8 | <<~RUBY 9 | 1| a = "foo" 10 | 2| b = "bar" 11 | 3| c = 3 12 | 4| __END__ 13 | RUBY 14 | end 15 | 16 | def test_eval_evaluates_method_call 17 | debug_code(program) do 18 | type 'b 3' 19 | type 'continue' 20 | type 'e a.upcase!' 21 | type 'p a' 22 | assert_line_text(/"FOO"/) 23 | type 'kill!' 24 | end 25 | end 26 | 27 | def test_eval_evaluates_computation_and_assignment 28 | debug_code(program) do 29 | type 'b 3' 30 | type 'continue' 31 | type 'e b = a + b' 32 | type 'p b' 33 | assert_line_text(/"foobar"/) 34 | type 'kill!' 35 | end 36 | end 37 | end 38 | 39 | class EvalThreadTest < ConsoleTestCase 40 | def program 41 | <<~RUBY 42 | 1| th0 = Thread.new{sleep} 43 | 2| m = Mutex.new; q = Queue.new 44 | 3| th1 = Thread.new do 45 | 4| m.lock; q << true 46 | 5| sleep 1 47 | 6| m.unlock 48 | 7| end 49 | 8| q.pop # wait for locking 50 | 9| p :ok 51 | RUBY 52 | end 53 | 54 | def test_eval_with_threads 55 | debug_code program do 56 | type 'b 9' 57 | type 'c' 58 | type 'm.lock.nil?' 59 | assert_line_text 'false' 60 | type 'c' 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/console/print_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | class PrintTest < ConsoleTestCase 7 | def program 8 | <<~RUBY 9 | 1| h = { foo: "bar" } 10 | 2| binding.break 11 | RUBY 12 | end 13 | 14 | def test_p_prints_the_expression 15 | debug_code(program) do 16 | type "c" 17 | type "p h" 18 | assert_line_text({ foo: "bar" }.inspect) 19 | type "c" 20 | end 21 | end 22 | 23 | def test_pp_pretty_prints_the_expression 24 | debug_code(program) do 25 | type "c" 26 | type "pp h" 27 | assert_line_text({ foo: "bar" }.pretty_print_inspect) 28 | type "c" 29 | end 30 | end 31 | end 32 | 33 | class InspectionFailureTest < ConsoleTestCase 34 | def program 35 | <<~RUBY 36 | 1| f = Object.new 37 | 2| def f.inspect 38 | 3| raise "foo" 39 | 4| end 40 | 5| binding.b 41 | RUBY 42 | end 43 | 44 | def test_p_prints_the_expression 45 | debug_code(program) do 46 | type "c" 47 | type "p f" 48 | assert_line_text('# rescued during inspection') 49 | type "c" 50 | end 51 | end 52 | 53 | def test_pp_pretty_prints_the_expression 54 | debug_code(program) do 55 | type "c" 56 | type "pp f" 57 | assert_line_text('# rescued during inspection') 58 | type "c" 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/console/whereami_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | class WhereamiTest < ConsoleTestCase 7 | def program 8 | <<~RUBY 9 | 1| a = 1 10 | 2| a = 1 11 | 3| a = 1 12 | 4| a = 1 13 | 5| a = 1 14 | 6| a = 1 15 | 7| a = 1 16 | 8| a = 1 17 | 9| a = 1 18 | 10| a = 1 19 | 11| b = 1 20 | 12| b = 1 21 | 13| b = 1 22 | 14| b = 1 23 | 15| b = 1 24 | 16| b = 1 25 | 17| b = 1 26 | 18| b = 1 27 | 19| b = 1 28 | 20| b = 1 29 | 21| c = 1 30 | RUBY 31 | end 32 | 33 | def test_whereami_displays_current_frames_code 34 | debug_code(program) do 35 | type "list" 36 | type "list" 37 | 38 | # after 2 list commands, we should advance to the next 10 lines and not able to see the current frame's source 39 | assert_no_line_text(/=> 1\| a = 1/) 40 | assert_line_text(/b = 1/) 41 | 42 | type "whereami" 43 | 44 | # with whereami, we should see the current frame's source but have no visual outside the closest 10 lines 45 | assert_no_line_text(/b = 1/) 46 | assert_line_text(/=> 1\| a = 1/) 47 | 48 | type "list" 49 | 50 | # list command should work as normal after whereami is executed 51 | assert_no_line_text(/b = 1/) 52 | assert_line_text(/c = 1/) 53 | 54 | type "continue" 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/debug/prelude.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | return if ENV['RUBY_DEBUG_ENABLE'] == '0' 4 | return if defined?(::DEBUGGER__::Session) 5 | 6 | # Put the following line in your login script (e.g. ~/.bash_profile) with modified path: 7 | # 8 | # export RUBYOPT="-r /path/to/debug/prelude ${RUBYOPT}" 9 | # 10 | module Kernel 11 | def debugger(*a, up_level: 0, **kw) 12 | begin 13 | require_relative 'version' 14 | cur_version = ::DEBUGGER__::VERSION 15 | require_relative 'frame_info' 16 | 17 | if !defined?(::DEBUGGER__::SO_VERSION) || ::DEBUGGER__::VERSION != ::DEBUGGER__::SO_VERSION 18 | ::Object.send(:remove_const, :DEBUGGER__) 19 | raise LoadError 20 | end 21 | require_relative 'session' 22 | up_level += 1 23 | rescue LoadError 24 | $LOADED_FEATURES.delete_if{|e| 25 | e.start_with?(__dir__) || e.end_with?('debug/debug.so') 26 | } 27 | require 'debug/session' 28 | require 'debug/version' 29 | ::DEBUGGER__.info "Can not activate debug #{cur_version} specified by debug/prelude.rb. Activate debug #{DEBUGGER__::VERSION} instead." 30 | up_level += 1 31 | end 32 | 33 | ::DEBUGGER__::start no_sigint_hook: true, nonstop: true 34 | 35 | begin 36 | debugger(*a, up_level: up_level, **kw) 37 | self 38 | rescue ArgumentError # for 1.2.4 and earlier 39 | debugger(*a, **kw) 40 | self 41 | end 42 | end 43 | 44 | alias bb debugger if ENV['RUBY_DEBUG_BB'] 45 | end 46 | 47 | class Binding 48 | alias break debugger 49 | alias b debugger 50 | end 51 | -------------------------------------------------------------------------------- /debug.gemspec: -------------------------------------------------------------------------------- 1 | require_relative 'lib/debug/version' 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "debug" 5 | spec.version = DEBUGGER__::VERSION 6 | spec.authors = ["Koichi Sasada"] 7 | spec.email = ["ko1@atdot.net"] 8 | 9 | spec.summary = %q{Debugging functionality for Ruby} 10 | spec.description = %q{Debugging functionality for Ruby. This is completely rewritten debug.rb which was contained by the ancient Ruby versions.} 11 | spec.homepage = "https://github.com/ruby/debug" 12 | spec.licenses = ["Ruby", "BSD-2-Clause"] 13 | spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0") 14 | 15 | spec.metadata["homepage_uri"] = spec.homepage 16 | spec.metadata["source_code_uri"] = spec.homepage 17 | spec.metadata["changelog_uri"] = "#{spec.homepage}/releases/tag/v#{spec.version}" 18 | 19 | # Specify which files should be added to the gem when it is released. 20 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 21 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 22 | `git ls-files -z`.split("\x0").reject do |f| 23 | (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)}) 24 | end 25 | end 26 | spec.bindir = "exe" 27 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } 28 | spec.require_paths = ["lib"] 29 | spec.extensions = ['ext/debug/extconf.rb'] 30 | 31 | spec.add_dependency "irb", "~> 1.10" # for irb:debug integration 32 | spec.add_dependency "reline", ">= 0.3.8" 33 | end 34 | -------------------------------------------------------------------------------- /test/console/delete_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | class Deletetest < ConsoleTestCase 7 | def program 8 | <<~RUBY 9 | 1| a=1 10 | 1| b=2 11 | 2| c=3 12 | 3| d=4 13 | RUBY 14 | end 15 | 16 | def test_delete_deletes_all_breakpoints_by_default 17 | debug_code(program) do 18 | type "break 2" 19 | type "break 3" 20 | 21 | type "delete" 22 | type "y" # confirm deletion 23 | 24 | type "continue" 25 | end 26 | end 27 | 28 | def test_delete_deletes_a_specific_breakpoint 29 | debug_code(program) do 30 | type "break 2" 31 | type "break 3" 32 | 33 | type "delete 0" 34 | 35 | type "continue" 36 | assert_line_num(3) 37 | type 'kill!' 38 | end 39 | end 40 | 41 | def test_delete_keeps_current_breakpoints_if_not_confirmed 42 | debug_code(program) do 43 | type 'b 2' 44 | assert_line_text(/\#0 BP \- Line .*/) 45 | type 'b 3' 46 | assert_line_text(/\#1 BP \- Line .*/) 47 | type 'del' 48 | assert_line_text([ 49 | /\#0 BP \- Line .*/, 50 | /\#1 BP \- Line .*/, 51 | /Remove all breakpoints\? \[y\/N\]/ 52 | ]) 53 | type 'n' # confirmation 54 | type 'b' 55 | assert_line_text([ 56 | /\#0 BP \- Line .*/, 57 | /\#1 BP \- Line .*/ 58 | ]) 59 | type 'kill!' 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/protocol/next_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/protocol_test_case' 4 | 5 | module DEBUGGER__ 6 | class NextTest < ProtocolTestCase 7 | PROGRAM = <<~RUBY 8 | 1| module Foo 9 | 2| class Bar 10 | 3| def self.a 11 | 4| "hello" 12 | 5| end 13 | 6| end 14 | 7| Bar.a 15 | 8| bar = Bar.new 16 | 9| end 17 | RUBY 18 | 19 | def test_next_goes_to_the_next_statement 20 | run_protocol_scenario PROGRAM do 21 | req_next 22 | assert_line_num 2 23 | 24 | assert_locals_result( 25 | [ 26 | { name: "%self", value: "Foo", type: "Module" }, 27 | { name: "bar", value: "nil", type: "NilClass" } 28 | ] 29 | ) 30 | req_next 31 | assert_line_num 3 32 | 33 | assert_locals_result( 34 | [ 35 | { name: "%self", value: "Foo::Bar", type: "Class" }, 36 | ] 37 | ) 38 | req_next 39 | assert_line_num 7 40 | 41 | assert_locals_result( 42 | [ 43 | { name: "%self", value: "Foo", type: "Module" }, 44 | { name: "bar", value: "nil", type: "NilClass" } 45 | ] 46 | ) 47 | req_next 48 | assert_line_num 8 49 | 50 | assert_locals_result( 51 | [ 52 | { name: "%self", value: "Foo", type: "Module" }, 53 | { name: "bar", value: "nil", type: "NilClass" } 54 | ] 55 | ) 56 | req_terminate_debuggee 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/protocol/step_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/protocol_test_case' 4 | 5 | module DEBUGGER__ 6 | class StepTest < ProtocolTestCase 7 | PROGRAM = <<~RUBY 8 | 1| module Foo 9 | 2| class Bar 10 | 3| def self.a 11 | 4| "hello" 12 | 5| end 13 | 6| end 14 | 7| Bar.a 15 | 8| bar = Bar.new 16 | 9| end 17 | RUBY 18 | 19 | def test_step_goes_to_the_next_statement 20 | run_protocol_scenario PROGRAM do 21 | req_step 22 | assert_line_num 2 23 | req_step 24 | assert_line_num 3 25 | req_step 26 | assert_line_num 7 27 | req_step 28 | assert_line_num 4 29 | req_step 30 | assert_line_num 5 31 | req_step 32 | assert_line_num 8 33 | req_terminate_debuggee 34 | end 35 | end 36 | end 37 | 38 | class StepTest2 < ProtocolTestCase 39 | def program path 40 | <<~RUBY 41 | 1| require_relative "#{path}" 42 | 2| Foo.new.bar 43 | RUBY 44 | end 45 | 46 | def extra_file 47 | <<~RUBY 48 | class Foo 49 | def bar 50 | puts :hoge 51 | end 52 | end 53 | RUBY 54 | end 55 | 56 | def test_step_goes_to_the_next_file 57 | with_extra_tempfile do |extra_file| 58 | run_protocol_scenario(program(extra_file.path), cdp: false) do 59 | req_next 60 | assert_line_num 2 61 | req_step 62 | assert_line_num 3 63 | req_terminate_debuggee 64 | end 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/console/frame_block_identifier_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | 7 | class FrameBlockIdentifierTest < ConsoleTestCase 8 | def program 9 | <<~RUBY 10 | 1| 11 | 2| class Whatever 12 | 3| def some_method 13 | 4| will_exit = false 14 | 5| loop do 15 | 6| return if will_exit 16 | 7| will_exit = true 17 | 8| 18 | 9| begin 19 | 10| raise "foo" 20 | 11| rescue => e 21 | 12| puts "the end" 22 | 13| end 23 | 14| end 24 | 15| end 25 | 16| end 26 | 17| 27 | 18| Whatever.new.some_method 28 | RUBY 29 | end 30 | 31 | def test_frame_block_identifier 32 | debug_code(program) do 33 | type 'b 12' 34 | type 'c' 35 | assert_line_num 12 36 | assert_line_text([ 37 | /\[7, 16\] in .*/, 38 | / 7\| will_exit = true/, 39 | / 8\| /, 40 | / 9\| begin/, 41 | / 10\| raise "foo"/, 42 | / 11\| rescue => e/, 43 | /=> 12\| puts "the end"/, 44 | / 13\| end/, 45 | / 14\| end/, 46 | / 15\| end/, 47 | / 16\| end/, 48 | /=>\#0\tWhatever\#some_method at .*/, 49 | / \#1\t.*/, 50 | / \# and (?:2|3) frames \(use `bt' command for all frames\)/, 51 | //, 52 | /Stop by \#0 BP \- Line .*/ 53 | ]) 54 | type 'c' 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/debug/abbrev_command.rb: -------------------------------------------------------------------------------- 1 | 2 | module DEBUGGER__ 3 | class AbbrevCommand 4 | class TrieNode 5 | def initialize 6 | @children = {} 7 | @types = {} # set 8 | end 9 | 10 | def append c, type 11 | trie = (@children[c] ||= TrieNode.new) 12 | trie.add_type type 13 | end 14 | 15 | def [](c) 16 | @children[c] 17 | end 18 | 19 | def add_type type 20 | @types[type] = true 21 | self 22 | end 23 | 24 | def types 25 | @types.keys 26 | end 27 | 28 | def type 29 | if @types.size == 1 30 | @types.keys.first 31 | else 32 | nil 33 | end 34 | end 35 | 36 | def candidates 37 | @children.map{|c, n| 38 | ss = n.candidates 39 | ss.empty? ? c : 40 | ss.map{|s| 41 | c+s 42 | } 43 | }.flatten 44 | end 45 | end 46 | 47 | # config: { type: [commands...], ... } 48 | def initialize config 49 | @trie = TrieNode.new 50 | build config 51 | end 52 | 53 | private def build config 54 | config.each do |type, commands| 55 | commands.each do |command| 56 | trie = @trie 57 | command.each_char do |c| 58 | trie = trie.append(c, type) 59 | end 60 | end 61 | end 62 | end 63 | 64 | def search str, if_none = nil 65 | trie = @trie 66 | str.each_char do |c| 67 | if trie = trie[c] 68 | return trie.type if trie.type 69 | else 70 | return if_none 71 | end 72 | end 73 | yield trie.candidates.map{|s| str + s} if block_given? 74 | if_none 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /test/console/session_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | class ConsoleStartTest < ConsoleTestCase 7 | def program 8 | <<~RUBY 9 | 1| a = 1 10 | 2| b = 2 11 | 3| require "debug" 12 | 4| DEBUGGER__.start 13 | 5| c = 3 14 | 6| binding.break 15 | 7| "foo" 16 | RUBY 17 | end 18 | 19 | def test_debugger_session_starts_correctly 20 | run_ruby(program) do 21 | assert_line_num(5) 22 | type 'c' 23 | assert_line_num(6) 24 | type 'c' 25 | end 26 | end 27 | end 28 | 29 | class RequireStartTest 30 | class OptionRequireTest < ConsoleTestCase 31 | def program 32 | <<~RUBY 33 | 1| a = 1 34 | 2| b = 2 35 | 3| binding.break 36 | 4| "foo" 37 | RUBY 38 | end 39 | 40 | def test_debugger_session_starts_correctly 41 | run_ruby(program, options: "-r debug/start") do 42 | assert_line_num(1) 43 | type 'c' 44 | assert_line_num(3) 45 | type 'c' 46 | end 47 | end 48 | end 49 | 50 | class CodeRequireTest < ConsoleTestCase 51 | def program 52 | <<~RUBY 53 | 1| a = 1 54 | 2| b = 2 55 | 3| require "debug/start" 56 | 4| 57 | 5| c = 3 58 | 6| binding.break 59 | 7| "foo" 60 | RUBY 61 | end 62 | 63 | def test_debugger_session_starts_correctly 64 | run_ruby(program) do 65 | assert_line_num(5) 66 | type 'c' 67 | assert_line_num(6) 68 | type 'c' 69 | end 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /exe/rdbg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative '../lib/debug/config' 4 | config = DEBUGGER__::Config::parse_argv(ARGV) 5 | 6 | # mode is not an actual configuration option 7 | # it's only used to carry the result of parse_argv here 8 | case config.delete(:mode) 9 | when :start 10 | require 'rbconfig' 11 | 12 | libpath = File.join(File.expand_path(File.dirname(__dir__)), 'lib/debug') 13 | start_mode = config[:open] ? "open" : 'start' 14 | cmd = config[:command] ? ARGV.shift : (ENV['RUBY'] || RbConfig.ruby) 15 | 16 | if defined?($:.resolve_feature_path) 17 | begin 18 | _, sopath = $:.resolve_feature_path('debug/debug.so') 19 | rescue LoadError 20 | # raises LoadError before 3.1 (2.7 and 3.0) 21 | else 22 | sopath = File.dirname(File.dirname(sopath)) if sopath 23 | end 24 | else 25 | # `$:.resolve_feature_path` is not defined in 2.6 or earlier. 26 | so = "debug/debug.#{RbConfig::CONFIG['DLEXT']}" 27 | sopath = $:.find {|dir| File.exist?(File.join(dir, so))} 28 | end 29 | added = "-r #{libpath}/#{start_mode}" 30 | added = "-I #{sopath} #{added}" if sopath 31 | rubyopt = ENV['RUBYOPT'] 32 | env = ::DEBUGGER__::Config.config_to_env_hash(config) 33 | env['RUBY_DEBUG_ADDED_RUBYOPT'] = added 34 | env['RUBYOPT'] = "#{rubyopt} #{added}" 35 | 36 | exec(env, cmd, *ARGV) 37 | 38 | when :attach 39 | require_relative "../lib/debug/client" 40 | ::DEBUGGER__::CONFIG.set_config(**config) 41 | 42 | begin 43 | if ARGV.empty? && config[:port] 44 | DEBUGGER__::Client.new([config[:host], config[:port]].compact).connect 45 | else 46 | DEBUGGER__::Client.new(ARGV).connect 47 | end 48 | rescue DEBUGGER__::CommandLineOptionError 49 | puts opt.help 50 | end 51 | else 52 | raise # assert 53 | end 54 | -------------------------------------------------------------------------------- /test/console/display_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | class DisplayTest < ConsoleTestCase 7 | def program 8 | <<~RUBY 9 | 1| a = 1 10 | 2| b = 2 11 | 3| binding.break 12 | 4| __END__ 13 | RUBY 14 | end 15 | 16 | def test_display_displays_expressions_when_the_program_stopps 17 | debug_code(program) do 18 | type "display a" 19 | assert_line_text(/0: a =/) 20 | type "display b" 21 | assert_line_text(/0: a = /) 22 | assert_line_text(/1: b = /) 23 | type "continue" 24 | assert_line_text(/0: a = 1/) 25 | assert_line_text(/1: b = 2/) 26 | 27 | type 'kill!' 28 | end 29 | end 30 | 31 | def test_display_without_expression_lists_display_settings 32 | debug_code(program) do 33 | type "display a" 34 | type "display b" 35 | type "display" 36 | assert_line_text(/0: a = /) 37 | assert_line_text(/1: b = /) 38 | 39 | type 'kill!' 40 | end 41 | end 42 | 43 | def test_undisplay_deletes_a_given_display_setting 44 | debug_code(program) do 45 | type "display a" 46 | type "undisplay 0" 47 | type "y" 48 | type "continue" 49 | assert_no_line_text(/0: a =/) 50 | 51 | type 'kill!' 52 | end 53 | end 54 | 55 | def test_undisplay_without_expression_deletes_all_display_settings 56 | debug_code(program) do 57 | type "display a" 58 | type "display b" 59 | type "undisplay" 60 | type "y" 61 | assert_no_line_text(/0: a = /) 62 | assert_no_line_text(/1: b = /) 63 | 64 | type 'kill!' 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/console/list_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | class ListTest < ConsoleTestCase 7 | def program 8 | <<~RUBY 9 | 1| p 1 10 | 2| p 2 11 | 3| p 3 12 | 4| p 4 13 | 5| p 5 14 | 6| p 6 15 | 7| p 7 16 | 8| p 8 17 | 9| p 9 18 | 10| p 10 19 | 11| p 11 20 | 12| p 12 21 | 13| p 13 22 | 14| p 14 23 | 15| p 15 24 | 16| p 16 25 | 17| p 17 26 | 18| p 18 27 | 19| p 19 28 | 20| p 20 29 | 21| p 21 30 | 22| p 22 31 | 23| p 23 32 | 24| p 24 33 | 25| p 25 34 | 26| p 26 35 | 27| p 27 36 | 28| p 28 37 | 29| p 29 38 | 30| p 30 39 | RUBY 40 | end 41 | 42 | def test_list_only_lists_part_of_the_program 43 | debug_code(program) do 44 | type 'list' 45 | assert_line_text(/p 1/) 46 | assert_line_text(/p 10/) 47 | assert_no_line_text(/p 11/) 48 | 49 | type 'kill!' 50 | end 51 | end 52 | 53 | def test_list_only_lists_after_the_given_line 54 | debug_code(program) do 55 | type 'list 11' 56 | assert_no_line_text(/p 10/) 57 | assert_line_text(/p 11/) 58 | 59 | type 'kill!' 60 | end 61 | end 62 | 63 | def test_list_continues_automatically 64 | debug_code(program) do 65 | type 'list' 66 | assert_line_text(/p 10/) 67 | assert_no_line_text(/p 11/) 68 | 69 | type "" 70 | assert_line_text(/p 20/) 71 | assert_no_line_text(/p 10/) 72 | assert_no_line_text(/p 21/) 73 | 74 | type "" 75 | assert_line_text(/p 30/) 76 | assert_no_line_text(/p 20/) 77 | type 'kill!' 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | begin 5 | require "rake/extensiontask" 6 | task :build => :compile 7 | 8 | Rake::ExtensionTask.new("debug") do |ext| 9 | ext.lib_dir = "lib/debug" 10 | end 11 | rescue LoadError 12 | end 13 | 14 | task :default => [:clobber, :compile, 'README.md', :check_readme, :test_console] 15 | 16 | file 'README.md' => ['lib/debug/session.rb', 'lib/debug/config.rb', 17 | 'exe/rdbg', 'misc/README.md.erb'] do 18 | require_relative 'lib/debug/session' 19 | require 'erb' 20 | File.write 'README.md', ERB.new(File.read('misc/README.md.erb')).result 21 | puts 'README.md is updated.' 22 | end 23 | 24 | task :check_readme do 25 | require_relative 'lib/debug/session' 26 | require 'erb' 27 | current_readme = File.read("README.md") 28 | generated_readme = ERB.new(File.read('misc/README.md.erb')).result 29 | 30 | if current_readme != generated_readme 31 | fail <<~MSG 32 | The content of README.md doesn't match its template and/or source. 33 | Please apply the changes to info source (e.g. command comments) or the template and run 'rake README.md' to update README.md. 34 | MSG 35 | end 36 | end 37 | 38 | desc "Run debug.gem test-framework tests" 39 | Rake::TestTask.new(:test_test) do |t| 40 | t.test_files = FileList["test/support/*_test.rb"] 41 | end 42 | 43 | desc "Run all debugger console related tests" 44 | Rake::TestTask.new(:test_console) do |t| 45 | t.test_files = FileList["test/console/*_test.rb"] 46 | end 47 | 48 | desc "Run all debugger protocols (CAP & DAP) related tests" 49 | Rake::TestTask.new(:test_protocol) do |t| 50 | t.test_files = FileList["test/protocol/*_test.rb"] 51 | end 52 | 53 | task test: 'test_console' do 54 | warn '`rake test` doesn\'t run protocol tests. Use `rake test_all` to test all.' 55 | end 56 | 57 | task test_all: [:test_test, :test_console, :test_protocol] 58 | -------------------------------------------------------------------------------- /test/protocol/binary_data_dap_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/protocol_test_case' 4 | 5 | module DEBUGGER__ 6 | class BinaryDataDAPTest < ProtocolTestCase 7 | def test_binary_data_gets_encoded 8 | program = <<~RUBY 9 | 1| class PassthroughInspect 10 | 2| def initialize(data) 11 | 3| @data = data 12 | 4| end 13 | 5| 14 | 6| def inspect 15 | 7| @data 16 | 8| end 17 | 9| end 18 | 10| 19 | 11| with_binary_data = PassthroughInspect.new([8, 200, 1].pack('CCC')) 20 | 12| with_binary_data 21 | RUBY 22 | run_protocol_scenario(program, cdp: false) do 23 | req_add_breakpoint 12 24 | req_continue 25 | assert_locals_result( 26 | [ 27 | { name: '%self', value: 'main', type: 'Object' }, 28 | { name: 'with_binary_data', value: [8, 200, 1].pack('CCC').encode(Encoding::UTF_8, invalid: :replace, undef: :replace), type: 'PassthroughInspect' } 29 | ] 30 | ) 31 | req_terminate_debuggee 32 | end 33 | end 34 | 35 | def test_frozen_strings_are_supported 36 | # When `inspect` fails, `DEBUGGER__.safe_inspect` returns a frozen error message 37 | # Just returning a frozen string wouldn't work, as `DEBUGGER__.safe_inspect` constructs 38 | # the return value with a buffer. 39 | program = <<~RUBY 40 | 1| class Uninspectable 41 | 2| def inspect; raise 'error'; end 42 | 3| end 43 | 4| broken_inspect = Uninspectable.new 44 | 5| broken_inspect 45 | RUBY 46 | run_protocol_scenario(program, cdp: false) do 47 | req_add_breakpoint 5 48 | req_continue 49 | assert_locals_result( 50 | [ 51 | { name: '%self', value: 'main', type: 'Object' }, 52 | { name: 'broken_inspect', value: /#inspect raises/, type: 'Uninspectable' } 53 | ] 54 | ) 55 | req_terminate_debuggee 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /ext/debug/iseq_collector.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #ifdef HAVE_RB_ISEQ 4 | VALUE rb_iseqw_new(VALUE v); 5 | void rb_objspace_each_objects( 6 | int (*callback)(void *start, void *end, size_t stride, void *data), 7 | void *data); 8 | size_t rb_obj_memsize_of(VALUE); 9 | 10 | // implementation specific. 11 | enum imemo_type { 12 | imemo_iseq = 7, 13 | imemo_mask = 0xf 14 | }; 15 | 16 | static inline enum imemo_type 17 | imemo_type(VALUE imemo) 18 | { 19 | return (RBASIC(imemo)->flags >> FL_USHIFT) & imemo_mask; 20 | } 21 | 22 | static inline int 23 | rb_obj_is_iseq(VALUE iseq) 24 | { 25 | return RB_TYPE_P(iseq, T_IMEMO) && imemo_type(iseq) == imemo_iseq; 26 | } 27 | 28 | struct iseq_i_data { 29 | void (*func)(VALUE v, void *data); 30 | void *data; 31 | }; 32 | 33 | int 34 | iseq_i(void *vstart, void *vend, size_t stride, void *ptr) 35 | { 36 | VALUE v; 37 | struct iseq_i_data *data = (struct iseq_i_data *)ptr; 38 | 39 | for (v = (VALUE)vstart; v != (VALUE)vend; v += stride) { 40 | if (RBASIC(v)->flags) { 41 | switch (BUILTIN_TYPE(v)) { 42 | case T_IMEMO: 43 | if (rb_obj_is_iseq(v)) { 44 | data->func(v, data->data); 45 | } 46 | continue; 47 | default: 48 | continue; 49 | } 50 | } 51 | } 52 | 53 | return 0; 54 | } 55 | 56 | static void 57 | each_iseq_i(VALUE v, void *ptr) 58 | { 59 | rb_yield(rb_iseqw_new(v)); 60 | } 61 | 62 | static VALUE 63 | each_iseq(VALUE self) 64 | { 65 | struct iseq_i_data data = {each_iseq_i, NULL}; 66 | rb_objspace_each_objects(iseq_i, &data); 67 | return Qnil; 68 | } 69 | 70 | static void 71 | count_iseq_i(VALUE v, void *ptr) 72 | { 73 | size_t *sizep = (size_t *)ptr; 74 | *sizep += 1; 75 | } 76 | 77 | static VALUE 78 | count_iseq(VALUE self) 79 | { 80 | size_t size = 0; 81 | struct iseq_i_data data = {count_iseq_i, &size}; 82 | rb_objspace_each_objects(iseq_i, &data); 83 | return SIZET2NUM(size); 84 | } 85 | 86 | void 87 | Init_iseq_collector(void) 88 | { 89 | VALUE rb_mObjSpace = rb_const_get(rb_cObject, rb_intern("ObjectSpace")); 90 | rb_define_singleton_method(rb_mObjSpace, "each_iseq", each_iseq, 0); 91 | rb_define_singleton_method(rb_mObjSpace, "count_iseq", count_iseq, 0); 92 | } 93 | #endif 94 | -------------------------------------------------------------------------------- /test/support/test_case_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'console_test_case' 4 | 5 | module DEBUGGER__ 6 | class PseudoTerminalTest < ConsoleTestCase 7 | def program 8 | <<~RUBY 9 | a = 1 10 | RUBY 11 | end 12 | 13 | def test_the_test_fails_when_debugger_exits_early 14 | assert_raise_message(/Expected all commands\/assertions to be executed/) do 15 | debug_code(program, remote: false) do 16 | type 'continue' 17 | type 'foo' 18 | end 19 | end 20 | end 21 | 22 | def test_the_test_fails_when_the_script_doesnt_have_line_numbers 23 | assert_raise_message(/line numbers are required in test script. please update the script with:\n/) do 24 | debug_code(program, remote: false) do 25 | type 'continue' 26 | end 27 | end 28 | end 29 | 30 | def test_the_test_work_when_debuggee_outputs_many_lines 31 | debug_code ' 1| 300.times{|i| p i}' do 32 | type 'c' 33 | end 34 | end 35 | 36 | def test_the_test_fails_when_the_repl_prompt_does_not_finish_even_though_scenario_is_empty 37 | assert_raise_message(/Expected the REPL prompt to finish/) do 38 | debug_code(program, remote: false) do 39 | end 40 | end 41 | end 42 | end 43 | 44 | class PseudoTerminalTestForRemoteDebuggee < ConsoleTestCase 45 | def program 46 | <<~RUBY 47 | 1| def a 48 | 2| end 49 | 3| 50 | 4| loop{ 51 | 5| a() 52 | 6| } 53 | RUBY 54 | end 55 | 56 | def steps 57 | Proc.new{ 58 | type 'quit' 59 | type 'y' 60 | } 61 | end 62 | 63 | def test_the_test_fails_when_debuggee_on_unix_domain_socket_mode_doesnt_exist_after_scenarios 64 | omit "too slow now" 65 | 66 | assert_raise_message(/Expected the debuggee program to finish/) do 67 | prepare_test_environment(program, steps) do 68 | debug_code_on_unix_domain_socket() 69 | end 70 | end 71 | end 72 | 73 | def test_the_test_fails_when_debuggee_on_tcpip_mode_doesnt_exist_after_scenarios 74 | omit "too slow now" 75 | 76 | assert_raise_message(/Expected the debuggee program to finish/) do 77 | prepare_test_environment(program, steps) do 78 | debug_code_on_tcpip() 79 | end 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /test/protocol/call_stack_with_skip_dap_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/protocol_test_case' 4 | 5 | module DEBUGGER__ 6 | class CallStackWithSkipDAPTest < ProtocolTestCase 7 | def program(path) 8 | <<~RUBY 9 | 1| require_relative "#{path}" 10 | 2| with_foo do 11 | 3| "something" 12 | 4| end 13 | RUBY 14 | end 15 | 16 | def extra_file 17 | <<~RUBY 18 | def with_foo 19 | yield 20 | end 21 | RUBY 22 | end 23 | 24 | def req_stacktrace_file_names 25 | response = send_dap_request('stackTrace', threadId: 1) 26 | stack_frames = response.dig(:body, :stackFrames) 27 | stack_frames.map { |f| f.dig(:source, :name) } 28 | end 29 | 30 | def test_it_does_not_skip_a_path 31 | with_extra_tempfile do |extra_file| 32 | run_protocol_scenario(program(extra_file.path), cdp: false) do 33 | req_add_breakpoint 3 34 | req_continue 35 | 36 | assert_equal( 37 | [File.basename(temp_file_path), File.basename(extra_file.path), File.basename(temp_file_path)], 38 | req_stacktrace_file_names 39 | ) 40 | 41 | req_terminate_debuggee 42 | end 43 | end 44 | end 45 | 46 | def test_it_skips_a_path 47 | with_extra_tempfile do |extra_file| 48 | ENV['RUBY_DEBUG_SKIP_PATH'] = extra_file.path 49 | run_protocol_scenario(program(extra_file.path), cdp: false) do 50 | req_add_breakpoint 3 51 | req_continue 52 | 53 | assert_equal([File.basename(temp_file_path), File.basename(temp_file_path)], req_stacktrace_file_names) 54 | 55 | req_terminate_debuggee 56 | end 57 | end 58 | ensure 59 | ENV['RUBY_DEBUG_SKIP_PATH'] = nil 60 | end 61 | 62 | def test_it_does_not_skip_a_path_if_there_is_a_breakpoint 63 | with_extra_tempfile do |extra_file| 64 | ENV['RUBY_DEBUG_SKIP_PATH'] = extra_file.path 65 | run_protocol_scenario(program(extra_file.path), cdp: false) do 66 | req_add_breakpoint 2, path: extra_file.path 67 | req_continue 68 | 69 | assert_equal([File.basename(extra_file.path), File.basename(temp_file_path)], req_stacktrace_file_names) 70 | 71 | req_terminate_debuggee 72 | end 73 | end 74 | ensure 75 | ENV['RUBY_DEBUG_SKIP_PATH'] = nil 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/support/assertions_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'console_test_case' 4 | 5 | module DEBUGGER__ 6 | class AssertLineTextTest < ConsoleTestCase 7 | def program 8 | <<~RUBY 9 | a = 1 10 | RUBY 11 | end 12 | 13 | def test_the_helper_takes_a_string_expectation_and_escape_it 14 | assert_raise_message(/Expected to include `"foobar\\\\?/) do 15 | debug_code(program, remote: false) do 16 | assert_line_text("foobar?") 17 | end 18 | end 19 | end 20 | 21 | def test_the_helper_takes_an_array_of_string_expectations_and_combine_them 22 | assert_raise_message(/Expected to include `"foobar\\\\?/) do 23 | debug_code(program, remote: false) do 24 | assert_line_text(["foo", "bar?"]) 25 | end 26 | end 27 | end 28 | 29 | def test_the_helper_takes_a_regexp_expectation 30 | assert_raise_message(/Expected to include `\/foobar\/`/) do 31 | debug_code(program, remote: false) do 32 | assert_line_text(/foobar/) 33 | end 34 | end 35 | end 36 | 37 | def test_the_helper_takes_an_array_of_regexp_expectations_and_combine_them 38 | assert_raise_message(/Expected to include `\/foo\.\*bar\/m`/) do 39 | debug_code(program, remote: false) do 40 | assert_line_text([/foo/, /bar/]) 41 | end 42 | end 43 | end 44 | 45 | def test_the_helper_raises_an_error_with_invalid_expectation 46 | assert_raise_message(/Unknown expectation value: 123/) do 47 | debug_code(program, remote: false) do 48 | assert_line_text(123) 49 | end 50 | end 51 | end 52 | 53 | def test_the_test_fails_when_debuggee_on_unix_domain_socket_mode_doesnt_exist_after_scenarios 54 | assert_raise_message(/Expected to include `"foobar\\\\?/) do 55 | prepare_test_environment(program, steps) do 56 | debug_code_on_unix_domain_socket() 57 | end 58 | end 59 | end 60 | 61 | def test_the_test_fails_when_debuggee_on_tcpip_mode_doesnt_exist_after_scenarios 62 | assert_raise_message(/Expected to include `"foobar\\\\?/) do 63 | prepare_test_environment(program, steps) do 64 | debug_code_on_tcpip() 65 | end 66 | end 67 | end 68 | 69 | private 70 | 71 | def steps 72 | Proc.new{ 73 | assert_line_text("foobar?") 74 | } 75 | end 76 | end 77 | end 78 | 79 | -------------------------------------------------------------------------------- /test/protocol/eval_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/protocol_test_case' 4 | 5 | module DEBUGGER__ 6 | class EvalTest < ProtocolTestCase 7 | PROGRAM = <<~RUBY 8 | 1| a = 2 9 | 2| b = 3 10 | 3| c = 1 11 | 4| d = 4 12 | 5| e = 5 13 | 6| f = 6 14 | RUBY 15 | 16 | def test_eval_evaluates_expressions 17 | run_protocol_scenario PROGRAM do 18 | req_add_breakpoint 5 19 | req_continue 20 | assert_repl_result({value: '2', type: 'Integer'}, 'a') 21 | assert_repl_result({value: '4', type: 'Integer'}, 'd') 22 | assert_repl_result({value: '3', type: 'Integer'}, '1+2') 23 | assert_repl_result({value: 'false', type: 'FalseClass'}, 'a == 1') 24 | req_terminate_debuggee 25 | end 26 | end 27 | 28 | def test_eval_executes_commands 29 | run_protocol_scenario PROGRAM, cdp: false do 30 | req_add_breakpoint 3 31 | req_continue 32 | assert_repl_result({value: '(rdbg:command) b 5 ;; b 6', type: nil}, ",b 5 ;; b 6") 33 | req_continue 34 | assert_line_num 5 35 | req_continue 36 | assert_line_num 6 37 | req_terminate_debuggee 38 | end 39 | end 40 | end 41 | 42 | class EvaluateOnSomeFramesTest < ProtocolTestCase 43 | PROGRAM = <<~RUBY 44 | 1| a = 2 45 | 2| def foo 46 | 3| a = 4 47 | 4| end 48 | 5| foo 49 | RUBY 50 | 51 | def test_eval_evaluates_arithmetic_expressions 52 | run_protocol_scenario PROGRAM do 53 | req_add_breakpoint 4 54 | req_continue 55 | assert_repl_result({value: '4', type: 'Integer'}, 'a', frame_idx: 0) 56 | assert_repl_result({value: '2', type: 'Integer'}, 'a', frame_idx: 1) 57 | req_terminate_debuggee 58 | end 59 | end 60 | end 61 | 62 | class EvaluateThreadTest < ProtocolTestCase 63 | PROGRAM = <<~RUBY 64 | 1| th0 = Thread.new{sleep} 65 | 2| m = Mutex.new; q = Queue.new 66 | 3| th1 = Thread.new do 67 | 4| m.lock; q << true 68 | 5| sleep 1 69 | 6| m.unlock 70 | 7| end 71 | 8| q.pop # wait for locking 72 | 9| p :ok 73 | RUBY 74 | 75 | def test_eval_with_threads 76 | run_protocol_scenario PROGRAM, cdp: false do 77 | req_add_breakpoint 9 78 | req_continue 79 | assert_repl_result({value: 'false', type: 'FalseClass'}, 'm.lock.nil?', frame_idx: 0) 80 | req_terminate_debuggee 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/console/nested_break_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | class NestedBreakAtMethodsTest < ConsoleTestCase 7 | def program 8 | <<~RUBY 9 | 1| def foo a 10 | 2| b = a + 1 # break 11 | 3| end 12 | 4| def bar 13 | 5| x = 1 # break 14 | 6| end 15 | 7| bar 16 | 8| x = 2 17 | RUBY 18 | end 19 | 20 | def test_nested_break 21 | debug_code program do 22 | type 'break 2' 23 | type 'break 5' 24 | type 'c' 25 | assert_line_num 5 26 | 27 | type 'up' 28 | assert_line_text(/=>\#1/) 29 | 30 | type 'p foo(42)' 31 | 32 | if TracePoint.respond_to? :allow_reentry 33 | # nested break 34 | assert_line_num 2 35 | type 'p a' 36 | assert_line_text(/42/) 37 | type 'c' 38 | assert_line_num 7 # because restored `up` line 39 | end 40 | 41 | # pop nested break 42 | assert_line_text(/43/) 43 | 44 | type 'bt' 45 | assert_line_text(/=>\#1/) 46 | 47 | type 'c' 48 | end 49 | end 50 | 51 | def test_nested_break_bt 52 | debug_code program do 53 | type 'break 2' 54 | type 'break 5' 55 | type 'c' 56 | 57 | assert_line_num 5 58 | type 'p foo(42)' 59 | 60 | if TracePoint.respond_to? :allow_reentry 61 | # nested break 62 | assert_line_num 2 63 | type 'bt' 64 | assert_no_line_text 'thread_client.rb' 65 | type 'c' 66 | end 67 | 68 | type 'c' 69 | end 70 | end 71 | 72 | def test_multiple_nested_break 73 | debug_code program do 74 | type 'break 2' 75 | type 'break 5' 76 | type 'c' 77 | assert_line_num 5 78 | 79 | type 'p foo(42)' 80 | 81 | if TracePoint.respond_to? :allow_reentry 82 | # nested break 83 | assert_line_num 2 84 | type 'p foo(142)' 85 | type 'bt' 86 | assert_line_text(/\#\d+\s+
/) 87 | 88 | type 'c' 89 | assert_line_text(/143/) 90 | 91 | type 'bt' 92 | assert_no_line_text(/\#9/) 93 | 94 | type 'c' 95 | end 96 | 97 | assert_line_text(/43/) 98 | type 'c' 99 | end 100 | end 101 | end 102 | end 103 | 104 | -------------------------------------------------------------------------------- /test/console/outline_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | class OutlineTest < ConsoleTestCase 7 | def program 8 | <<~RUBY 9 | 1| class Foo 10 | 2| def initialize 11 | 3| @var = "foobar" 12 | 4| end 13 | 5| 14 | 6| def bar; end 15 | 7| def self.baz; end 16 | 8| end 17 | 9| 18 | 10| foo = Foo.new 19 | 11| 20 | 12| binding.b 21 | RUBY 22 | end 23 | 24 | def test_outline_lists_local_variables 25 | debug_code(program) do 26 | type 'c' 27 | type 'outline' 28 | assert_line_text(/locals: foo/) 29 | type 'c' 30 | end 31 | end 32 | 33 | def test_outline_lists_object_info 34 | debug_code(program) do 35 | type 'c' 36 | type 'outline foo' 37 | assert_line_text([ 38 | /Foo#methods: bar/, 39 | /instance variables: @var/ 40 | ]) 41 | type 'c' 42 | end 43 | end 44 | 45 | def test_outline_lists_class_info 46 | debug_code(program) do 47 | type 'c' 48 | type 'outline Foo' 49 | assert_line_text( 50 | [ 51 | /Class#methods:\s+allocate/, 52 | /Foo\.methods: baz/, 53 | ] 54 | ) 55 | type 'c' 56 | end 57 | end 58 | 59 | def test_outline_aliases 60 | debug_code(program) do 61 | type 'c' 62 | type 'outline' 63 | assert_line_text(/locals: foo/) 64 | type 'ls' 65 | assert_line_text(/locals: foo/) 66 | type 'c' 67 | end 68 | end 69 | end 70 | 71 | class OutlineThreadLockingTest < ConsoleTestCase 72 | def program 73 | <<~RUBY 74 | 1| th0 = Thread.new{sleep} 75 | 2| $m = Mutex.new 76 | 3| th1 = Thread.new do 77 | 4| $m.lock 78 | 5| sleep 1 79 | 6| $m.unlock 80 | 7| end 81 | 8| 82 | 9| def self.constants # overriding constants is only one of the ways to cause deadlock with outline 83 | 10| $m.lock 84 | 11| [] 85 | 12| end 86 | 13| 87 | 14| sleep 0.5 88 | 15| debugger 89 | RUBY 90 | end 91 | 92 | def test_outline_doesnt_cause_deadlock 93 | debug_code(program) do 94 | type 'c' 95 | type 'ls' 96 | assert_line_text(/locals: th0/) 97 | type 'c' 98 | end 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /test/console/config_postmortem_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | class PostmortemTest < ConsoleTestCase 7 | def program 8 | <<~RUBY 9 | 1| def foo y = __LINE__ 10 | 2| bar 11 | 3| end 12 | 4| def bar x = __LINE__ 13 | 5| raise 14 | 6| end 15 | 7| foo 16 | RUBY 17 | end 18 | 19 | def test_config_postmortem 20 | debug_code(program) do 21 | type 'config postmortem = true' 22 | type 'c' 23 | assert_line_text(/Enter postmortem mode with RuntimeError/) 24 | type 'p x' 25 | assert_line_text(/=> 4/) 26 | type 'up' 27 | type 'p y' 28 | assert_line_text(/=> 1/) 29 | type 'step' 30 | assert_line_text(/Can not use this command on postmortem mode/) 31 | type 'c' 32 | end 33 | end 34 | 35 | def test_env_var_postmortem 36 | ENV["RUBY_DEBUG_POSTMORTEM"] = "true" 37 | debug_code(program) do 38 | type 'c' 39 | assert_line_text(/Enter postmortem mode with RuntimeError/) 40 | type 'p x' 41 | assert_line_text(/=> 4/) 42 | type 'up' 43 | type 'p y' 44 | assert_line_text(/=> 1/) 45 | type 'step' 46 | assert_line_text(/Can not use this command on postmortem mode/) 47 | type 'c' 48 | end 49 | ensure 50 | ENV["RUBY_DEBUG_POSTMORTEM"] = nil 51 | end 52 | end 53 | 54 | class CustomPostmortemTest < ConsoleTestCase 55 | def program 56 | <<~RUBY 57 | 1| DEBUGGER__::CONFIG[:postmortem] = true 58 | 2| def foo y = __LINE__ 59 | 3| bar 60 | 4| end 61 | 5| def bar x = __LINE__ 62 | 6| raise 63 | 7| end 64 | 8| begin 65 | 9| foo 66 | 10| rescue => e 67 | 11| DEBUGGER__::SESSION.enter_postmortem_session e 68 | 12| end 69 | 13| binding.b 70 | 14| v = :ok1 71 | 15| DEBUGGER__::CONFIG[:postmortem] = false 72 | RUBY 73 | end 74 | 75 | def test_config_postmortem 76 | debug_code(program) do 77 | type 'c' 78 | assert_line_num 6 79 | type 'bt' 80 | assert_line_text([/bar/, /foo/]) 81 | type 'c' 82 | assert_line_num 13 83 | type 'p v' 84 | assert_line_text(/=> nil/) 85 | type 'step' 86 | type 'step' 87 | type 'p v' 88 | assert_line_text(/=> :ok1/) 89 | type 'c' 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /test/protocol/hover_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/protocol_test_case' 4 | 5 | module DEBUGGER__ 6 | 7 | class HoverTest < ProtocolTestCase 8 | PROGRAM = <<~RUBY 9 | 1| a = 1 10 | 2| b = 2 11 | 3| c = 3 12 | 4| d = 4 13 | 5| e = 5 14 | RUBY 15 | 16 | def test_hover_matches_with_stopped_place 17 | run_protocol_scenario PROGRAM do 18 | req_add_breakpoint 4 19 | req_continue 20 | assert_hover_result({value: '2', type: 'Integer'}, 'b') 21 | assert_hover_result({value: '3', type: 'Integer'}, 'c') 22 | assert_hover_result({value: '1', type: 'Integer'}, 'a') 23 | req_terminate_debuggee 24 | end 25 | end 26 | end 27 | 28 | class HoverTest2 < ProtocolTestCase 29 | PROGRAM = <<~RUBY 30 | 1| p 1 31 | RUBY 32 | 33 | def test_hover_returns_method_info 34 | run_protocol_scenario PROGRAM do 35 | assert_hover_result({value: /\# 123' 51 | type 'irb_info' 52 | assert_line_text('IRB version:') 53 | type 'next' 54 | type 'info' 55 | assert_line_text([/a = 1/, /b = nil/]) 56 | 57 | # disable irb console 58 | type 'config set irb_console false' 59 | type '456' 60 | assert_raw_line_text '(rdbg) 456' 61 | type 'q!' 62 | end 63 | end 64 | 65 | def test_irb_console_config_activates_irb 66 | ENV["RUBY_DEBUG_IRB_CONSOLE"] = "true" 67 | 68 | debug_code(program, remote: false) do 69 | type '123' 70 | assert_raw_line_text 'irb:rdbg(main):002> 123' 71 | type 'irb_info' 72 | assert_line_text('IRB version:') 73 | type 'next' 74 | type 'info' 75 | assert_line_text([/a = 1/, /b = nil/]) 76 | 77 | # disable irb console 78 | type 'config set irb_console false' 79 | type '456' 80 | assert_raw_line_text '(rdbg) 456' 81 | type 'q!' 82 | end 83 | ensure 84 | ENV["RUBY_DEBUG_IRB_CONSOLE"] = nil 85 | end 86 | 87 | private 88 | 89 | # assert_line_text ignores the prompt line, so we can't use it to assert the prompt transition 90 | # assert_raw_line_text is a workaround for that 91 | def assert_raw_line_text(expectation) 92 | @scenario.push(Proc.new do |test_info| 93 | assert_include(test_info.last_backlog.join, expectation) 94 | end) 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/debug/color.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | begin 4 | require 'irb/color' 5 | 6 | module IRB 7 | module Color 8 | DIM = 2 unless defined? DIM 9 | end 10 | end 11 | 12 | require "irb/color_printer" 13 | rescue LoadError 14 | warn "DEBUGGER: can not load newer irb for coloring. Write 'gem \"debug\" in your Gemfile." 15 | end 16 | 17 | module DEBUGGER__ 18 | module Color 19 | if defined? IRB::Color.colorize 20 | begin 21 | IRB::Color.colorize('', [:DIM], colorable: true) 22 | SUPPORT_COLORABLE_OPTION = true 23 | rescue ArgumentError 24 | end 25 | 26 | if defined? SUPPORT_COLORABLE_OPTION 27 | def irb_colorize str, color 28 | IRB::Color.colorize str, color, colorable: true 29 | end 30 | else 31 | def irb_colorize str, color 32 | IRB::Color.colorize str, color 33 | end 34 | end 35 | 36 | def colorize str, color 37 | if !CONFIG[:no_color] 38 | irb_colorize str, color 39 | else 40 | str 41 | end 42 | end 43 | else 44 | def colorize str, color 45 | str 46 | end 47 | end 48 | 49 | if defined? IRB::ColorPrinter.pp 50 | def color_pp obj, width 51 | with_inspection_error_guard do 52 | if !CONFIG[:no_color] 53 | IRB::ColorPrinter.pp(obj, "".dup, width) 54 | else 55 | obj.pretty_inspect 56 | end 57 | end 58 | end 59 | else 60 | def color_pp obj, width 61 | with_inspection_error_guard do 62 | obj.pretty_inspect 63 | end 64 | end 65 | end 66 | 67 | def colored_inspect obj, width: SESSION.width, no_color: false 68 | with_inspection_error_guard do 69 | if !no_color 70 | color_pp obj, width 71 | else 72 | obj.pretty_inspect 73 | end 74 | end 75 | end 76 | 77 | if defined? IRB::Color.colorize_code 78 | if defined? SUPPORT_COLORABLE_OPTION 79 | def colorize_code code 80 | IRB::Color.colorize_code(code, colorable: true) 81 | end 82 | else 83 | def colorize_code code 84 | IRB::Color.colorize_code(code) 85 | end 86 | end 87 | else 88 | def colorize_code code 89 | code 90 | end 91 | end 92 | 93 | def colorize_cyan(str) 94 | colorize(str, [:CYAN, :BOLD]) 95 | end 96 | 97 | def colorize_blue(str) 98 | colorize(str, [:BLUE, :BOLD]) 99 | end 100 | 101 | def colorize_magenta(str) 102 | colorize(str, [:MAGENTA, :BOLD]) 103 | end 104 | 105 | def colorize_dim(str) 106 | colorize(str, [:DIM]) 107 | end 108 | 109 | def with_inspection_error_guard 110 | yield 111 | rescue Exception => ex 112 | err_msg = "#{ex.inspect} rescued during inspection" 113 | string_result = obj.to_s rescue nil 114 | 115 | # don't colorize the string here because it's not from user's application 116 | if string_result 117 | %Q{"#{string_result}" from #to_s because #{err_msg}} 118 | else 119 | err_msg 120 | end 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /test/console/frame_control_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | class FrameControlTest < ConsoleTestCase 7 | def extra_file 8 | <<~RUBY 9 | class Foo 10 | def bar 11 | baz 12 | end 13 | 14 | def baz 15 | 10 16 | end 17 | end 18 | RUBY 19 | end 20 | 21 | def program(extra_file_path) 22 | <<~RUBY 23 | 1| load "#{extra_file_path}" 24 | 2| Foo.new.bar 25 | 3| 26 | 4| p 1 27 | 5| p 2 28 | 6| p 3 29 | RUBY 30 | end 31 | 32 | def test_frame_prints_the_current_frame 33 | with_extra_tempfile do |extra_file| 34 | debug_code(program(extra_file.path)) do 35 | type 'b Foo#baz' 36 | type 'continue' 37 | 38 | type 'frame' 39 | assert_line_text(/Foo#baz at/) 40 | type 'kill!' 41 | end 42 | end 43 | end 44 | 45 | def test_up_moves_up_one_frame 46 | with_extra_tempfile do |extra_file| 47 | debug_code(program(extra_file.path)) do 48 | type 'b Foo#baz' 49 | type 'continue' 50 | 51 | type 'frame' 52 | assert_line_text(/Foo#baz at/) 53 | type 'up' 54 | assert_line_text(/Foo#bar at/) 55 | type 'up' 56 | assert_line_text(/
at/) 57 | type 'frame' 58 | assert_line_text(/
at/) 59 | type 'kill!' 60 | end 61 | end 62 | end 63 | 64 | def test_up_sets_correct_thread_client_location 65 | with_extra_tempfile do |extra_file| 66 | debug_code(program(extra_file.path)) do 67 | type 'b Foo#bar' 68 | type 'continue' 69 | 70 | type 'up' 71 | type 'b 5' 72 | type 'c' 73 | assert_line_text(/
at/) 74 | assert_line_num(5) 75 | type 'kill!' 76 | end 77 | end 78 | end 79 | 80 | def test_down_moves_down_one_frame 81 | with_extra_tempfile do |extra_file| 82 | debug_code(program(extra_file.path)) do 83 | type 'b Foo#baz' 84 | type 'continue' 85 | 86 | type 'up' 87 | assert_line_text(/Foo#bar at/) 88 | type 'up' 89 | assert_line_text(/
at/) 90 | type 'down' 91 | assert_line_text(/Foo#bar at/) 92 | type 'down' 93 | assert_line_text(/Foo#baz at/) 94 | type 'kill!' 95 | end 96 | end 97 | end 98 | 99 | def test_down_sets_correct_thread_client_location 100 | with_extra_tempfile do |extra_file| 101 | debug_code(program(extra_file.path)) do 102 | type 'b Foo#bar' 103 | type 'continue' 104 | 105 | type 'up' 106 | type 'down' 107 | type 'b 7' 108 | type 'c' 109 | assert_line_num(7) 110 | assert_line_text(/Foo#baz at/) 111 | type 'kill!' 112 | end 113 | end 114 | end 115 | 116 | def test_frame_sets_correct_thread_client_location 117 | with_extra_tempfile do |extra_file| 118 | debug_code(program(extra_file.path)) do 119 | type 'b Foo#bar' 120 | type 'continue' 121 | 122 | type 'frame 1' 123 | type 'b 5' 124 | type 'c' 125 | assert_line_text(/
at/) 126 | assert_line_num(5) 127 | type 'kill!' 128 | end 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /.github/actions/launchable/setup/action.yaml: -------------------------------------------------------------------------------- 1 | name: Set up Launchable 2 | description: >- 3 | Install the required dependencies and execute the necessary Launchable commands for test recording 4 | 5 | inputs: 6 | report-path: 7 | default: launchable_reports.json 8 | required: true 9 | description: The file path of the test report for uploading to Launchable 10 | 11 | test-task: 12 | default: none 13 | required: true 14 | description: >- 15 | Test task that determine how tests are run. 16 | This value is used in the Launchable flavor. 17 | 18 | runs: 19 | using: composite 20 | 21 | steps: 22 | - name: Enable Launchable conditionally 23 | id: enable-launchable 24 | run: echo "enable-launchable=true" >> $GITHUB_OUTPUT 25 | shell: bash 26 | if: ${{ github.repository == 'ruby/debug' }} 27 | 28 | # Launchable CLI requires Python and Java. 29 | # https://www.launchableinc.com/docs/resources/cli-reference/ 30 | - name: Set up Python 31 | uses: actions/setup-python@871daa956ca9ea99f3c3e30acb424b7960676734 # v5.0.0 32 | with: 33 | python-version: "3.x" 34 | if: ${{ steps.enable-launchable.outputs.enable-launchable }} 35 | 36 | - name: Set up Java 37 | uses: actions/setup-java@7a445ee88d4e23b52c33fdc7601e40278616c7f8 # v4.0.0 38 | with: 39 | distribution: 'temurin' 40 | java-version: '17' 41 | if: ${{ steps.enable-launchable.outputs.enable-launchable }} 42 | 43 | - name: Set environment variables for Launchable 44 | shell: bash 45 | run: | 46 | : # GITHUB_PULL_REQUEST_URL are used for commenting test reports in Launchable Github App. 47 | : # https://github.com/launchableinc/cli/blob/v1.80.1/launchable/utils/link.py#L42 48 | echo "GITHUB_PULL_REQUEST_URL=${{ github.event.pull_request.html_url }}" >> $GITHUB_ENV 49 | : # The following envs are necessary in Launchable tokenless authentication. 50 | : # https://github.com/launchableinc/cli/blob/v1.80.1/launchable/utils/authentication.py#L20 51 | echo "LAUNCHABLE_ORGANIZATION=${{ github.repository_owner }}" >> $GITHUB_ENV 52 | echo "LAUNCHABLE_WORKSPACE=${{ github.event.repository.name }}" >> $GITHUB_ENV 53 | : # https://github.com/launchableinc/cli/blob/v1.80.1/launchable/utils/authentication.py#L71 54 | echo "GITHUB_PR_HEAD_SHA=${{ github.event.pull_request.head.sha || github.sha }}" >> $GITHUB_ENV 55 | if: ${{ steps.enable-launchable.outputs.enable-launchable }} 56 | 57 | - name: Set up Launchable 58 | shell: bash 59 | run: | 60 | set -x 61 | PATH=$PATH:$(python -msite --user-base)/bin 62 | echo "PATH=$PATH" >> $GITHUB_ENV 63 | pip install --user launchable 64 | launchable verify 65 | : # The build name cannot include a slash, so we replace the string here. 66 | github_ref="${{ github.ref }}" 67 | github_ref="${github_ref//\//_}" 68 | launchable record build --name ${github_ref}_${GITHUB_PR_HEAD_SHA} 69 | echo "TESTOPTS=${TESTOPTS} --runner=launchable --launchable-test-report-json=${{ inputs.report-path }}" >> $GITHUB_ENV 70 | if: ${{ steps.enable-launchable.outputs.enable-launchable }} 71 | 72 | - name: Record test results in Launchable 73 | uses: gacts/run-and-post-run@674528335da98a7afc80915ff2b4b860a0b3553a # v1.4.0 74 | with: 75 | shell: bash 76 | post: | 77 | launchable record tests --flavor os=${{ inputs.os }} --flavor test_task=${{ inputs.test-task }} raw ${{ inputs.report-path }} 78 | rm -f ${{ inputs.report-path }} 79 | if: ${{ always() && steps.enable-launchable.outputs.enable-launchable }} 80 | -------------------------------------------------------------------------------- /test/console/color_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../../lib/debug/color' 4 | require_relative '../../lib/debug/config' 5 | require 'test/unit' 6 | require 'test/unit/rr' 7 | 8 | module DEBUGGER__ 9 | class ColorTest < Test::Unit::TestCase 10 | include Color 11 | 12 | # These constant variable are copied from https://github.com/ruby/irb/blob/master/test/irb/test_color.rb#L9-L18 13 | CLEAR = "\e[0m" 14 | BOLD = "\e[1m" 15 | UNDERLINE = "\e[4m" 16 | REVERSE = "\e[7m" 17 | RED = "\e[31m" 18 | GREEN = "\e[32m" 19 | YELLOW = "\e[33m" 20 | BLUE = "\e[34m" 21 | MAGENTA = "\e[35m" 22 | CYAN = "\e[36m" 23 | 24 | ENV['TERM'] = 'xterm-256color' # This environment variable is not defined in some platforms such as Ubuntu. 25 | 26 | def test_colored_inspect_color_objects_if_use_colorize 27 | stub_width_method 28 | enable_colorable 29 | 30 | dummy_class = Struct.new(:foo) do 31 | def bar 32 | @a = foo 33 | end 34 | end 35 | 36 | { "#{GREEN}##{CLEAR}\n": dummy_class.new('b'), 37 | "#{RED}#{BOLD}\"#{CLEAR}#{RED}hoge#{CLEAR}#{RED}#{BOLD}\"#{CLEAR}\n": 'hoge'}.each do |k, v| 38 | expected = k.to_s 39 | obj = v 40 | assert_equal(expected, colored_inspect(obj)) 41 | end 42 | ensure 43 | remove_const_SESSION 44 | end 45 | 46 | def test_colored_inspect_does_not_color_objects_if_do_not_use_colorize 47 | CONFIG[:no_color] = true 48 | stub_width_method 49 | 50 | dummy_class = Struct.new(:foo) do 51 | def bar 52 | @a = foo 53 | end 54 | end 55 | 56 | { "#\n": dummy_class.new('b'), 57 | "\"hoge\"\n": 'hoge'}.each do |k, v| 58 | expected = k.to_s 59 | obj = v 60 | assert_equal(expected, colored_inspect(obj)) 61 | end 62 | ensure 63 | CONFIG[:no_color] = nil 64 | remove_const_SESSION 65 | end 66 | 67 | def test_colorize_color_string_if_use_colorize 68 | enable_colorable 69 | 70 | { 71 | "#{YELLOW}#{BOLD}#{REVERSE}foo#{CLEAR}": [:YELLOW, :BOLD, :REVERSE], 72 | "#{MAGENTA}#{BOLD}foo#{CLEAR}": [:MAGENTA, :BOLD], 73 | "#{GREEN}foo#{CLEAR}": [:GREEN], 74 | "#{CYAN}#{BOLD}foo#{CLEAR}": [:CYAN, :BOLD], 75 | "#{BLUE}#{BOLD}foo#{CLEAR}": [:BLUE, :BOLD] 76 | }.each do |k, v| 77 | assert_equal(k.to_s, colorize('foo', v)) 78 | end 79 | end 80 | 81 | def test_colorize_does_not_color_string_if_do_not_use_colorize 82 | CONFIG[:no_color] = true 83 | 84 | [ 85 | [:YELLOW, :BOLD, :REVERSE], 86 | [:MAGENTA, :BOLD], 87 | [:GREEN], 88 | [:CYAN, :BOLD], 89 | [:BLUE, :BOLD] 90 | ].each do |color| 91 | assert_equal('foo', colorize('foo', color)) 92 | end 93 | ensure 94 | CONFIG[:no_color] = nil 95 | end 96 | 97 | SESSION_class = Struct.new('SESSION', :a) 98 | 99 | private 100 | 101 | def stub_width_method 102 | DEBUGGER__.const_set('SESSION', SESSION_class) 103 | stub(::DEBUGGER__::SESSION).width { IO.console_size[1] } 104 | end 105 | 106 | def remove_const_SESSION 107 | DEBUGGER__.public_class_method(:remove_const) 108 | DEBUGGER__.remove_const(:SESSION) 109 | DEBUGGER__.private_class_method(:remove_const) 110 | end 111 | 112 | def enable_colorable 113 | stub($stdout).tty? { true } 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /test/protocol/break_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/protocol_test_case' 4 | 5 | module DEBUGGER__ 6 | class BreakTest1 < ProtocolTestCase 7 | PROGRAM = <<~RUBY 8 | 1| module Foo 9 | 2| class Bar 10 | 3| def self.a 11 | 4| "hello" 12 | 5| end 13 | 6| end 14 | 7| Bar.a 15 | 8| bar = Bar.new 16 | 9| end 17 | RUBY 18 | 19 | def test_break_stops_at_correct_place 20 | run_protocol_scenario PROGRAM do 21 | req_add_breakpoint 5 22 | req_continue 23 | assert_line_num 5 24 | 25 | assert_locals_result( 26 | [ 27 | { name: "%self", value: "Foo::Bar", type: "Class" }, 28 | { name: "_return", value: "hello", type: "String" } 29 | ] 30 | ) 31 | 32 | req_add_breakpoint 9 33 | req_continue 34 | assert_line_num 9 35 | 36 | assert_locals_result( 37 | [ 38 | { name: "%self", value: "Foo", type: "Module" }, 39 | { name: "bar", value: /# nil 34 | end 35 | 36 | def add iseq, src 37 | # only manage loaded file names 38 | if (path = (iseq.absolute_path || iseq.path)) && File.exist?(path) 39 | if @loaded_file_map.has_key? path 40 | return path, true # reloaded 41 | else 42 | @loaded_file_map[path] = path 43 | return path, false 44 | end 45 | end 46 | end 47 | 48 | def orig_src iseq 49 | lines = iseq.script_lines&.map(&:chomp) 50 | line = iseq.first_line 51 | if line > 1 52 | [*([''] * (line - 1)), *lines] 53 | else 54 | lines 55 | end 56 | end 57 | 58 | def get_colored iseq 59 | if lines = @cmap[iseq] 60 | lines 61 | else 62 | if src_lines = get(iseq) 63 | @cmap[iseq] = colorize_code(src_lines.join("\n")).lines 64 | else 65 | nil 66 | end 67 | end 68 | end 69 | else 70 | # ruby 3.0 or earlier 71 | SrcInfo = Struct.new(:src, :colored) 72 | 73 | def initialize 74 | @files = {} # filename => SrcInfo 75 | end 76 | 77 | def add iseq, src 78 | path = (iseq.absolute_path || iseq.path) 79 | 80 | if path && File.exist?(path) 81 | reloaded = @files.has_key? path 82 | 83 | if src 84 | add_iseq iseq, src 85 | return path, reloaded 86 | else 87 | add_path path 88 | return path, reloaded 89 | end 90 | else 91 | add_iseq iseq, src 92 | nil 93 | end 94 | end 95 | 96 | private def all_iseq iseq, rs = [] 97 | rs << iseq 98 | iseq.each_child{|ci| 99 | all_iseq(ci, rs) 100 | } 101 | rs 102 | end 103 | 104 | private def add_iseq iseq, src 105 | line = iseq.first_line 106 | if line > 1 107 | src = ("\n" * (line - 1)) + src 108 | end 109 | 110 | si = SrcInfo.new(src.each_line.map{|l| l.chomp}) 111 | all_iseq(iseq).each{|e| 112 | e.instance_variable_set(:@debugger_si, si) 113 | e.freeze 114 | } 115 | end 116 | 117 | private def add_path path 118 | src_lines = File.readlines(path, chomp: true) 119 | @files[path] = SrcInfo.new(src_lines) 120 | rescue SystemCallError 121 | end 122 | 123 | private def get_si iseq 124 | return unless iseq 125 | 126 | if iseq.instance_variable_defined?(:@debugger_si) 127 | iseq.instance_variable_get(:@debugger_si) 128 | elsif @files.has_key?(path = (iseq.absolute_path || iseq.path)) 129 | @files[path] 130 | elsif path 131 | add_path(path) 132 | end 133 | end 134 | 135 | def orig_src iseq 136 | if si = get_si(iseq) 137 | si.src 138 | end 139 | end 140 | 141 | def get_colored iseq 142 | if si = get_si(iseq) 143 | si.colored || begin 144 | si.colored = colorize_code(si.src.join("\n")).lines 145 | end 146 | end 147 | end 148 | end 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /test/console/debugger_local_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | class DebuggerLocalsTest < ConsoleTestCase 7 | class REPLLocalsTest < ConsoleTestCase 8 | def program 9 | <<~RUBY 10 | 1| a = 1 11 | RUBY 12 | end 13 | 14 | def test_locals_added_in_locals_are_accessible_between_evaluations 15 | debug_code(program) do 16 | type "y = 50" 17 | type "y" 18 | assert_line_text(/50/) 19 | type "c" 20 | end 21 | end 22 | end 23 | 24 | class RaisedTest < ConsoleTestCase 25 | class RubyMethodTest < ConsoleTestCase 26 | def program 27 | <<~RUBY 28 | 1| foo rescue nil 29 | RUBY 30 | end 31 | 32 | def test_raised_is_accessible_from_repl 33 | debug_code(program) do 34 | type "catch Exception" 35 | type "c" 36 | type "_raised" 37 | assert_line_text(/undefined local variable or method [`']foo' for main/) 38 | type "c" 39 | end 40 | end 41 | 42 | def test_raised_is_accessible_from_command 43 | debug_code(program) do 44 | type "catch Exception pre: p _raised" 45 | type "c" 46 | assert_line_text(/undefined local variable or method [`']foo' for main/) 47 | type "c" 48 | end 49 | end 50 | end 51 | 52 | class CMethodTest < ConsoleTestCase 53 | def program 54 | <<~RUBY 55 | 1| 1/0 rescue nil 56 | RUBY 57 | end 58 | 59 | def test_raised_is_accessible_from_repl 60 | debug_code(program) do 61 | type "catch Exception" 62 | type "c" 63 | type "_raised" 64 | assert_line_text(/ZeroDivisionError/) 65 | type "c" 66 | end 67 | end 68 | 69 | def test_raised_is_accessible_from_command 70 | debug_code(program) do 71 | type "catch Exception pre: p _raised" 72 | type "c" 73 | assert_line_text(/ZeroDivisionError/) 74 | type "c" 75 | end 76 | end 77 | end 78 | 79 | class EncapsulationTest < ConsoleTestCase 80 | def program 81 | <<~RUBY 82 | 1| 1/0 rescue nil 83 | 2| a = _raised 84 | RUBY 85 | end 86 | 87 | def test_raised_doesnt_leak_to_program_binding 88 | debug_code(program) do 89 | type "catch StandardError" 90 | type "c" 91 | # stops for ZeroDivisionError 92 | type "info" 93 | type "_raised" 94 | assert_line_text(/ZeroDivisionError/) 95 | type "c" 96 | 97 | # stops for NoMethodError because _raised is not defined in the program 98 | type "_raised" 99 | assert_line_text(/undefined local variable or method [`']_raised' for main/) 100 | type "c" 101 | end 102 | end 103 | end 104 | end 105 | 106 | class ReturnedTest < ConsoleTestCase 107 | def program 108 | <<~RUBY 109 | 1| def foo 110 | 2| "foo" 111 | 3| end 112 | 4| 113 | 5| foo 114 | RUBY 115 | end 116 | 117 | def test_returned_is_accessible_from_repl 118 | debug_code(program) do 119 | type "b 3" 120 | type "c" 121 | type "_return + 'bar'" 122 | assert_line_text(/"foobar"/) 123 | type "c" 124 | end 125 | end 126 | 127 | def test_returned_is_accessible_from_command 128 | debug_code(program) do 129 | type "b 3 pre: p _return + 'bar'" 130 | type "c" 131 | assert_line_text(/"foobar"/) 132 | type "c" 133 | end 134 | end 135 | 136 | class EncapsulationTest < ConsoleTestCase 137 | def program 138 | <<~RUBY 139 | 1| def foo 140 | 2| "foo" 141 | 3| end 142 | 4| 143 | 5| foo 144 | 6| puts _return 145 | RUBY 146 | end 147 | 148 | def test_raised_doesnt_leak_to_program_binding 149 | debug_code(program) do 150 | type "catch StandardError" 151 | type "b 3" 152 | type "c" 153 | type "_return + 'bar'" 154 | assert_line_text(/"foobar"/) 155 | type "c" 156 | # stops for NoMethodError because _return is not defined in the program 157 | type "_raised" 158 | assert_line_text(/undefined local variable or method [`']_return' for main/) 159 | type "c" 160 | end 161 | end 162 | end 163 | end 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /test/protocol/variables_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../support/protocol_test_case" 4 | 5 | module DEBUGGER__ 6 | class DAPGlobalVariablesTest < ProtocolTestCase 7 | PROGRAM = <<~RUBY 8 | 1| $a = 1 9 | 2| $b = 2 10 | 3| $c = 3 11 | RUBY 12 | 13 | def test_eval_evaluates_global_variables 14 | run_protocol_scenario PROGRAM, cdp: false do 15 | req_add_breakpoint 3 16 | req_continue 17 | 18 | globals = gather_variables(type: "globals") 19 | 20 | # User defined globals 21 | assert_includes(globals, { name: "$a", value: "1", type: "Integer", variablesReference: 0 }) 22 | assert_includes(globals, { name: "$b", value: "2", type: "Integer", variablesReference: 0 }) 23 | 24 | # Ruby defined globals 25 | assert_includes(globals, { name: "$VERBOSE", value: "false", type: "FalseClass", variablesReference: 0 }) 26 | assert_includes(globals, { name: "$stdout", value: "#>", type: "IO", variablesReference: 0 }) 27 | 28 | req_terminate_debuggee 29 | end 30 | end 31 | end 32 | 33 | class CDPGlobalVariablesTest < ProtocolTestCase 34 | PROGRAM = <<~RUBY 35 | 1| $a = 1 36 | 2| $b = 2 37 | 3| $c = 3 38 | RUBY 39 | 40 | def test_eval_evaluates_global_variables 41 | run_protocol_scenario PROGRAM, dap: false do 42 | req_add_breakpoint 3 43 | req_continue 44 | 45 | globals = gather_variables(type: "global") 46 | 47 | # User defined globals 48 | assert_includes(globals, { name: "$a", value: "1", type: "Number" }) 49 | assert_includes(globals, { name: "$b", value: "2", type: "Number" }) 50 | 51 | # Ruby defined globals 52 | assert_includes(globals, { name: "$VERBOSE", value: "false", type: "Boolean" }) 53 | assert_includes(globals, { name: "$stdout", value: "#>", type: "Object" }) 54 | 55 | req_terminate_debuggee 56 | end 57 | end 58 | end 59 | 60 | class DAPInstanceVariableTest < ProtocolTestCase 61 | PROGRAM = <<~RUBY 62 | 1| @a = 1 63 | 2| @c = 3 64 | 3| @b = 2 65 | 4| __LINE__ 66 | RUBY 67 | 68 | def test_ordering_instance_variables 69 | run_protocol_scenario PROGRAM, cdp: false do 70 | req_add_breakpoint 4 71 | req_continue 72 | 73 | locals = gather_variables 74 | 75 | variables_reference = locals.find { |local| local[:name] == "%self" }[:variablesReference] 76 | res = send_dap_request 'variables', variablesReference: variables_reference 77 | 78 | instance_vars = res.dig(:body, :variables) 79 | assert_equal instance_vars.map { |var| var[:name] }, ["#class", "@a", "@b", "@c"] 80 | 81 | req_terminate_debuggee 82 | end 83 | end 84 | end 85 | 86 | class DAPOverwrittenNameMethod < ProtocolTestCase 87 | PROGRAM = <<~RUBY 88 | 1| class Foo 89 | 2| def self.name(value) end 90 | 3| end 91 | 4| f = Foo.new 92 | 5| __LINE__ 93 | RUBY 94 | 95 | def test_overwritten_name_method 96 | run_protocol_scenario PROGRAM, cdp: false do 97 | req_add_breakpoint 5 98 | req_continue 99 | 100 | locals = gather_variables 101 | 102 | variable_info = locals.find { |local| local[:name] == "f" } 103 | 104 | assert_match(/\#/, variable_info[:value]) 105 | assert_equal "Foo", variable_info[:type] 106 | 107 | req_terminate_debuggee 108 | end 109 | end 110 | end 111 | 112 | class DAPOverwrittenClassMethod < ProtocolTestCase 113 | PROGRAM = <<~RUBY 114 | 1| class Foo 115 | 2| def self.class(value) end 116 | 3| end 117 | 4| f = Foo.new 118 | 5| __LINE__ 119 | RUBY 120 | 121 | def test_overwritten_class_method 122 | run_protocol_scenario PROGRAM, cdp: false do 123 | req_add_breakpoint 5 124 | req_continue 125 | 126 | locals = gather_variables 127 | 128 | variable_info = locals.find { |local| local[:name] == "f" } 129 | assert_match(/\#/, variable_info[:value]) 130 | assert_equal "Foo", variable_info[:type] 131 | 132 | req_terminate_debuggee 133 | end 134 | end 135 | end 136 | 137 | class DAPAnonymousClassInstance < ProtocolTestCase 138 | PROGRAM = <<~RUBY 139 | 1| f = Class.new.new 140 | 2| __LINE__ 141 | RUBY 142 | 143 | def test_anonymous_class_instance 144 | run_protocol_scenario PROGRAM, cdp: false do 145 | req_add_breakpoint 2 146 | req_continue 147 | 148 | locals = gather_variables 149 | 150 | variable_info = locals.find { |local| local[:name] == "f" } 151 | assert_match(/\#/, variable_info[:value]) 152 | assert_match(/\#/, variable_info[:type]) 153 | 154 | req_terminate_debuggee 155 | end 156 | end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /test/console/rdbg_option_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | class RUBYOPTHandlingTest < ConsoleTestCase 7 | def program 8 | <<~RUBY 9 | 1| a = 1 10 | RUBY 11 | end 12 | 13 | def test_debugger_removes_rubyopt_added_by_rdbg 14 | run_rdbg(program) do 15 | type "ENV['RUBYOPT']" 16 | assert_no_line_text(/debug\/start/) 17 | type "c" 18 | end 19 | end 20 | 21 | def test_debugger_respects_other_rubyopt 22 | run_rdbg(program, rubyopt: "-rreline") do 23 | type "ENV['RUBYOPT']" 24 | assert_no_line_text(/debug\/start/) 25 | assert_line_text(/-rreline/) 26 | type "c" 27 | end 28 | end 29 | end 30 | 31 | class DebugCommandOptionTest < ConsoleTestCase 32 | def program 33 | <<~RUBY 34 | 1| raise "foo" 35 | RUBY 36 | end 37 | 38 | def test_debug_command_is_executed 39 | run_rdbg(program, options: "-e 'catch RuntimeError'") do 40 | type "c" 41 | assert_line_text(/Stop by #0 BP - Catch "RuntimeError"/) 42 | type 'kill!' 43 | end 44 | end 45 | end 46 | 47 | class NonstopOptionTest < ConsoleTestCase 48 | def program 49 | <<~RUBY 50 | 1| a = "foo" 51 | 2| binding.b 52 | RUBY 53 | end 54 | 55 | def test_debugger_doesnt_stop 56 | run_rdbg(program, options: "--nonstop") do 57 | type "a + 'bar'" 58 | assert_line_text(/foobar/) 59 | type "c" 60 | end 61 | end 62 | end 63 | 64 | class StopAtLoadOptionTest < ConsoleTestCase 65 | def program 66 | <<~RUBY 67 | 1| a = "foo" 68 | 2| binding.b 69 | RUBY 70 | end 71 | 72 | def test_debugger_stops_immediately 73 | run_rdbg(program, options: "--stop-at-load") do 74 | # stops at the earliest possible location 75 | assert_line_text(/\[C\] Kernel[#\.]require/) 76 | type "c" 77 | type "a + 'bar'" 78 | assert_line_text(/foobar/) 79 | type "c" 80 | end 81 | end 82 | end 83 | 84 | class RCFileTest < ConsoleTestCase 85 | def rc_filename 86 | File.join(pty_home_dir, ".rdbgrc") 87 | end 88 | 89 | def rc_script 90 | "config set skip_path /foo/bar/" 91 | end 92 | 93 | def program 94 | <<~RUBY 95 | 1| a = 1 96 | RUBY 97 | end 98 | 99 | def with_rc_script 100 | begin 101 | File.open(rc_filename, "w") { |f| f.write(rc_script) } 102 | rescue Errno::EPERM, Errno::EACCES 103 | omit "Skip test with rc files. Cannot create rcfiles in HOME directory." 104 | end 105 | 106 | yield 107 | ensure 108 | if File.exist?(rc_filename) 109 | File.delete(rc_filename) 110 | end 111 | end 112 | 113 | def test_debugger_loads_the_rc_file_by_default 114 | with_rc_script do 115 | run_rdbg(program) do 116 | type "config skip_path" 117 | assert_line_text(/foo\\\/bar/) 118 | type "c" 119 | end 120 | end 121 | end 122 | 123 | def test_debugger_doesnt_load_the_rc_file_with_no_rc 124 | with_rc_script do 125 | run_rdbg(program, options: "--no-rc") do 126 | type "config skip_path" 127 | assert_no_line_text(/foo\\\/bar/) 128 | type "c" 129 | end 130 | end 131 | end 132 | end 133 | 134 | class InitScriptTest < ConsoleTestCase 135 | TEMPFILE_BASENAME = __FILE__.hash.abs.to_s(16) 136 | 137 | def with_init_script(filename) 138 | t = Tempfile.create(filename).tap do |f| 139 | f.write(init_script) 140 | f.close 141 | end 142 | yield t 143 | ensure 144 | File.unlink t if t 145 | end 146 | 147 | class CommandScriptTest < InitScriptTest 148 | def init_script 149 | <<~CMD 150 | catch RuntimeError 151 | CMD 152 | end 153 | 154 | def program 155 | <<~RUBY 156 | 1| raise "foo" 157 | RUBY 158 | end 159 | 160 | def test_debugger_executes_the_file_as_commands 161 | with_init_script([TEMPFILE_BASENAME]) do |init_script| 162 | run_rdbg(program, options: "-x #{init_script.path}") do 163 | type "c" 164 | assert_line_text(/Stop by #0 BP - Catch "RuntimeError"/) 165 | type 'kill!' 166 | end 167 | end 168 | end 169 | end 170 | 171 | class RubyScriptTest < InitScriptTest 172 | def init_script 173 | <<~CMD 174 | def foo 175 | "foo" 176 | end 177 | CMD 178 | end 179 | 180 | def program 181 | <<~RUBY 182 | 1| 123 183 | RUBY 184 | end 185 | 186 | def test_debugger_executes_the_file_as_ruby 187 | with_init_script([TEMPFILE_BASENAME, ".rb"]) do |init_script| 188 | run_rdbg(program, options: "-x #{init_script.path}") do 189 | type "foo + 'bar'" 190 | assert_line_text(/foobar/) 191 | type 'kill!' 192 | end 193 | end 194 | end 195 | end 196 | end 197 | end 198 | -------------------------------------------------------------------------------- /lib/debug/frame_info.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DEBUGGER__ 4 | FrameInfo = Struct.new(:location, :self, :binding, :iseq, :class, :frame_depth, 5 | :has_return_value, :return_value, 6 | :has_raised_exception, :raised_exception, 7 | :show_line, 8 | :_local_variables, :_callee, # for recorder 9 | :dupped_binding, 10 | ) 11 | 12 | # extend FrameInfo with debug.so 13 | begin 14 | require_relative 'debug.so' 15 | rescue LoadError 16 | require 'debug/debug.so' 17 | end 18 | 19 | class FrameInfo 20 | HOME = ENV['HOME'] ? (ENV['HOME'] + '/') : nil 21 | 22 | def path 23 | location.path 24 | end 25 | 26 | def realpath 27 | location.absolute_path 28 | end 29 | 30 | def self.pretty_path path 31 | return '#' unless path 32 | use_short_path = CONFIG[:use_short_path] 33 | 34 | case 35 | when use_short_path && path.start_with?(dir = RbConfig::CONFIG["rubylibdir"] + '/') 36 | path.sub(dir, '$(rubylibdir)/') 37 | when use_short_path && Gem.path.any? do |gp| 38 | path.start_with?(dir = gp + '/gems/') 39 | end 40 | path.sub(dir, '$(Gem)/') 41 | when HOME && path.start_with?(HOME) 42 | path.sub(HOME, '~/') 43 | else 44 | path 45 | end 46 | end 47 | 48 | def pretty_path 49 | FrameInfo.pretty_path path 50 | end 51 | 52 | def name 53 | # p frame_type: frame_type, self: self 54 | case frame_type 55 | when :block 56 | level, block_loc = block_identifier 57 | "block in #{block_loc}#{level}" 58 | when :method 59 | method_identifier 60 | when :c 61 | c_identifier 62 | when :other 63 | other_identifier 64 | end 65 | end 66 | 67 | def file_lines 68 | SESSION.source(self.iseq) 69 | end 70 | 71 | def frame_type 72 | if self.local_variables && iseq 73 | if iseq.type == :block 74 | :block 75 | elsif callee 76 | :method 77 | else 78 | :other 79 | end 80 | else 81 | :c 82 | end 83 | end 84 | 85 | BLOCK_LABL_REGEXP = /\Ablock( \(\d+ levels\))* in (.+)\z/ 86 | 87 | def block_identifier 88 | return unless frame_type == :block 89 | re_match = location.label.match(BLOCK_LABL_REGEXP) 90 | _, level, block_loc = re_match ? re_match.to_a : [nil, nil, location.label] 91 | 92 | [level || "", block_loc] 93 | end 94 | 95 | def method_identifier 96 | return unless frame_type == :method 97 | "#{klass_sig}#{callee}" 98 | end 99 | 100 | def c_identifier 101 | return unless frame_type == :c 102 | "[C] #{klass_sig}#{location.base_label}" 103 | end 104 | 105 | def other_identifier 106 | return unless frame_type == :other 107 | location.label 108 | end 109 | 110 | def callee 111 | self._callee ||= begin 112 | self.binding&.eval('__callee__') 113 | rescue NameError # BasicObject 114 | nil 115 | end 116 | end 117 | 118 | def return_str 119 | if self.binding && iseq && has_return_value 120 | DEBUGGER__.safe_inspect(return_value, short: true) 121 | end 122 | end 123 | 124 | def matchable_location 125 | # realpath can sometimes be nil so we can't use it here 126 | "#{path}:#{location.lineno}" 127 | end 128 | 129 | def location_str 130 | "#{pretty_path}:#{location.lineno}" 131 | end 132 | 133 | def eval_binding 134 | if b = self.dupped_binding 135 | b 136 | else 137 | b = self.binding || TOPLEVEL_BINDING 138 | self.dupped_binding = b.dup 139 | end 140 | end 141 | 142 | def local_variables 143 | if lvars = self._local_variables 144 | lvars 145 | elsif b = self.binding 146 | b.local_variables.map{|var| 147 | [var, b.local_variable_get(var)] 148 | }.to_h 149 | end 150 | end 151 | 152 | def iseq_parameters_info 153 | case frame_type 154 | when :block, :method 155 | parameters_info 156 | else 157 | nil 158 | end 159 | end 160 | 161 | def parameters_info 162 | vars = iseq.parameters_symbols 163 | vars.map{|var| 164 | begin 165 | { name: var, value: DEBUGGER__.safe_inspect(local_variable_get(var), short: true) } 166 | rescue NameError, TypeError 167 | nil 168 | end 169 | }.compact 170 | end 171 | 172 | private def get_singleton_class obj 173 | obj.singleton_class # TODO: don't use it 174 | rescue Exception 175 | nil 176 | end 177 | 178 | private def local_variable_get var 179 | local_variables[var] 180 | end 181 | 182 | private def klass_sig 183 | if self.class == get_singleton_class(self.self) 184 | "#{self.self}." 185 | else 186 | "#{self.class}#" 187 | end 188 | end 189 | end 190 | end 191 | -------------------------------------------------------------------------------- /test/console/watch_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | class ObjectInstanceVariableWatchingTest < ConsoleTestCase 7 | def program 8 | <<~RUBY 9 | 1| class Student 10 | 2| attr_accessor :name 11 | 3| 12 | 4| def initialize(name) 13 | 5| @name = name 14 | 6| binding.b 15 | 7| end 16 | 8| end 17 | 9| 18 | 10| class Teacher 19 | 11| attr_accessor :name 20 | 12| 21 | 13| def initialize(name) 22 | 14| @name = name 23 | 15| end 24 | 16| end 25 | 17| 26 | 18| s1 = Student.new("John") 27 | 19| t1 = Teacher.new("Jane") # it shouldn't stop for this 28 | 20| s1.name = "Josh" 29 | 21| "foo" 30 | RUBY 31 | end 32 | 33 | def test_debugger_only_stops_when_the_ivar_of_instance_changes 34 | debug_code(program) do 35 | type 'continue' 36 | type 'watch @name' 37 | assert_line_text(/#0 BP - Watch # @name = John/) 38 | type 'continue' 39 | assert_line_text(/Stop by #0 BP - Watch # @name = John -> Josh/) 40 | type 'continue' 41 | end 42 | end 43 | 44 | def test_watch_command_isnt_repeatable 45 | debug_code(program) do 46 | type 'continue' 47 | type 'watch @name' 48 | type '' 49 | assert_no_line_text(/duplicated breakpoint/) 50 | type 'kill!' 51 | end 52 | end 53 | 54 | def test_watch_works_with_command 55 | debug_code(program) do 56 | type 'continue' 57 | type 'watch @name pre: p "1234"' 58 | assert_line_text(/#0 BP - Watch # @name = John/) 59 | type 'continue' 60 | assert_line_text(/1234/) 61 | type 'continue' 62 | end 63 | 64 | debug_code(program) do 65 | type 'continue' 66 | type 'watch @name do: p "1234"' 67 | assert_line_text(/#0 BP - Watch # @name = John/) 68 | type 'b 21' 69 | type 'continue' 70 | assert_line_text(/1234/) 71 | type 'continue' 72 | end 73 | end 74 | 75 | class ConditionTest < ConsoleTestCase 76 | def program 77 | <<~RUBY 78 | 1| class Student 79 | 2| attr_accessor :name, :age 80 | 3| 81 | 4| def initialize(name, age) 82 | 5| @name = name 83 | 6| @age = age 84 | 7| binding.b(do: "watch @age if: name == 'Sean'") 85 | 8| end 86 | 9| end 87 | 10| 88 | 11| stan = Student.new("Stan", 30) 89 | 12| stan.age += 1 90 | 13| # only stops for Sean's age change 91 | 14| sean = Student.new("Sean", 25) 92 | 15| sean.age += 1 93 | 16| 94 | 17| a = 1 # additional line for line tp 95 | RUBY 96 | end 97 | 98 | def test_condition_is_evaluated_in_the_watched_object 99 | debug_code(program) do 100 | type 'continue' 101 | assert_line_text(/Stop by #\d BP - Watch # @age = 25 -> 26/) 102 | type 'continue' 103 | end 104 | end 105 | end 106 | 107 | class PathOptionTest < ConsoleTestCase 108 | def extra_file 109 | <<~RUBY 110 | STUDENT.age = 25 111 | _ = 1 # for the debugger to stop 112 | RUBY 113 | end 114 | 115 | def program(extra_file_path) 116 | <<~RUBY 117 | 1| class Student 118 | 2| attr_accessor :age 119 | 3| 120 | 4| def initialize 121 | 5| 122 | 6| end 123 | 7| end 124 | 8| 125 | 9| STUDENT = Student.new 126 | 10| 127 | 11| load "#{extra_file_path}" 128 | 12| 129 | 13| STUDENT.age = 30 130 | 14| _ = 1 131 | RUBY 132 | end 133 | 134 | def test_watch_only_stops_when_path_matches 135 | with_extra_tempfile do |extra_file| 136 | debug_code(program(extra_file.path)) do 137 | type 'b 5' 138 | type 'c' 139 | type "watch @age path: #{extra_file.path}" 140 | type 'c' 141 | assert_line_text(/@age = -> 25/) 142 | type 'c' 143 | end 144 | end 145 | end 146 | 147 | def test_the_path_option_supersede_skip_path_config 148 | # skips the extra_file's breakpoint 149 | with_extra_tempfile do |extra_file| 150 | debug_code(program(extra_file.path)) do 151 | type "config set skip_path /#{extra_file.path}/" 152 | type 'b 5' 153 | type 'c' 154 | type "watch @age" 155 | type 'c' 156 | assert_line_text(/@age = 25 -> 30/) 157 | type 'c' 158 | end 159 | 160 | # ignores skip_path and stops at designated path 161 | debug_code(program(extra_file.path)) do 162 | type "config set skip_path /#{extra_file.path}/" 163 | type 'b 5' 164 | type 'c' 165 | type "watch @age path: #{extra_file.path}" 166 | type 'c' 167 | assert_line_text(/@age = -> 25/) 168 | type 'c' 169 | end 170 | end 171 | end 172 | end 173 | end 174 | end 175 | -------------------------------------------------------------------------------- /test/console/config_fork_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | module ForkWithBlock 7 | def program 8 | <<~RUBY 9 | 1| pid = #{fork_method} do 10 | 2| binding.b do: 'p :child_enter' 11 | 3| a = 1 12 | 4| b = 2 13 | 5| c = 3 14 | 6| binding.b do: 'p :child_leave' 15 | 7| end 16 | 8| sleep 0.5 17 | 9| binding.b do: 'p :parent_enter' 18 | 10| a = 1 19 | 11| b = 2 20 | 12| c = 3 21 | 13| binding.b do: 'p :parent_leave' 22 | 14| Process.waitpid pid 23 | RUBY 24 | end 25 | end 26 | 27 | module ForkWithoutBlock 28 | def program 29 | <<~RUBY 30 | 1| if !(pid = #{fork_method}) 31 | 2| binding.b do: 'p :child_enter' 32 | 3| a = 1 33 | 4| b = 2 34 | 5| c = 3 35 | 6| binding.b do: 'p :child_leave' 36 | 7| else # parent 37 | 8| sleep 0.5 38 | 9| binding.b do: 'p :parent_enter' 39 | 10| a = 1 40 | 11| b = 2 41 | 12| c = 3 42 | 13| binding.b do: 'p :parent_leave' 43 | 14| Process.waitpid pid 44 | 15| end 45 | RUBY 46 | end 47 | end 48 | 49 | module ForkTestTemplate 50 | def test_default_case 51 | debug_code(program) do 52 | # type 'config fork_mode = both' # default 53 | type 'b 5' 54 | type 'b 10' 55 | type 'c' 56 | assert_line_num 5 57 | # assert_line_text([/DEBUGGER: Detaching after fork from parent process \d+/,]) # TODO 58 | assert_line_text([ 59 | # /DEBUGGER: Attaching after process \d+ fork to child process \d+/, # TODO 60 | /:child_enter/, 61 | ]) 62 | type 'c' # continue 5 63 | assert_line_num 10 64 | type 'c' # continue 10 65 | end 66 | end 67 | 68 | def test_both_case 69 | debug_code(program) do 70 | type 'config fork_mode = both' # default 71 | type 'b 5' 72 | type 'b 10' 73 | type 'c' 74 | assert_line_num 5 75 | # assert_line_text([/DEBUGGER: Detaching after fork from parent process \d+/,]) # TODO 76 | assert_line_text([ 77 | # /DEBUGGER: Attaching after process \d+ fork to child process \d+/, # TODO 78 | /:child_enter/, 79 | ]) 80 | type 'c' # continue 5 81 | assert_line_num 10 82 | type 'c' # continue 10 83 | end 84 | end 85 | 86 | PN = 2 87 | LN = 2 88 | 89 | def test_both_stress 90 | code = <<~RUBY 91 | 1| $0 = 'P '; start_tm = Time.now; ps = #{PN}.times.map do |i| 92 | 2| fork do 93 | 3| $0 = "c\#{i} " 94 | 4| #{LN}.times{ 95 | 5| binding.break 96 | 6| } 97 | 7| end 98 | 8| end 99 | 9| begin 100 | 10| ps.each{|pid| Process.waitpid pid; p finished_pid: pid} 101 | 11| rescue Errno::ECHILD 102 | 12| end 103 | 13| p(waited: ps, time: (Time.now - start_tm)); binding.break 104 | RUBY 105 | 106 | debug_code code do 107 | type 'c' # first 108 | PN.times{ 109 | LN.times{ 110 | assert_line_num 5 111 | type 'c' 112 | } 113 | } 114 | assert_line_num 13 115 | type 'c' 116 | end 117 | end 118 | 119 | def test_child_case 120 | debug_code(program) do 121 | type 'config fork_mode = child' 122 | type 'b 5' 123 | type 'b 10' 124 | type 'c' 125 | assert_line_num 5 126 | # assert_line_text([/DEBUGGER: Detaching after fork from parent process \d+/,]) # TODO 127 | assert_line_text([ 128 | # /DEBUGGER: Attaching after process \d+ fork to child process \d+/, # TODO 129 | /:child_enter/, 130 | ]) 131 | type 'c' 132 | end 133 | end 134 | 135 | def test_parent_case 136 | debug_code(program) do 137 | type 'config parent_on_fork = true' 138 | type 'b 5' 139 | type 'b 10' 140 | type 'c' 141 | assert_line_num 10 142 | assert_line_text([ 143 | # /DEBUGGER: Detaching after fork from child process \d+/, # TODO: puts on debug console 144 | /:parent_leave/ 145 | ]) 146 | type 'c' 147 | end 148 | end 149 | end 150 | 151 | # matrix 152 | [ForkWithBlock, ForkWithoutBlock].each.with_index{|program, i| 153 | ['fork', 'Process.fork', 'Kernel.fork'].each{|fork_method| 154 | c = Class.new ConsoleTestCase do 155 | include ForkTestTemplate 156 | include program 157 | define_method :fork_method do 158 | fork_method 159 | end 160 | end 161 | 162 | const_set "Fork_#{fork_method.tr('.', '_')}_#{i}", c 163 | } 164 | } 165 | 166 | class NestedForkTest < ConsoleTestCase 167 | def program 168 | <<~RUBY 169 | 1| DEBUGGER__::CONFIG[:fork_mode] = :parent 170 | 2| pid1 = fork do 171 | 3| puts 'parent forked.' 172 | 4| pid2 = fork do 173 | 5| puts 'child forked.' 174 | 6| end 175 | 7| Process.waitpid(pid2) 176 | 8| end 177 | 9| Process.waitpid(pid1) 178 | RUBY 179 | end 180 | 181 | def test_nested_fork 182 | debug_code program do 183 | type 'b 9' 184 | type 'c' 185 | assert_line_num 9 186 | type 'c' 187 | end 188 | end 189 | end 190 | end 191 | -------------------------------------------------------------------------------- /test/console/debugger_method_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | class DebuggerMethodTest < ConsoleTestCase 7 | METHOD_PLACE_HOLDER = "__BREAK_METHOD__" 8 | SUPPORTED_DEBUG_METHODS = %w(debugger binding.break binding.b).freeze 9 | 10 | def debug_code(program) 11 | SUPPORTED_DEBUG_METHODS.each do |mid| 12 | super(program.gsub(METHOD_PLACE_HOLDER, mid)) 13 | end 14 | end 15 | end 16 | 17 | class DebuggerMethodBasicTest < DebuggerMethodTest 18 | def program 19 | <<~RUBY 20 | 1| class Foo 21 | 2| def bar 22 | 3| #{METHOD_PLACE_HOLDER} 23 | 4| end 24 | 5| end 25 | 6| 26 | 7| Foo.new.bar 27 | RUBY 28 | end 29 | 30 | def test_breakpoint_fires_correctly 31 | debug_code(program) do 32 | type 'continue' 33 | assert_line_text('Foo#bar') 34 | type 'kill!' 35 | end 36 | end 37 | 38 | def test_debugger_method_in_subsession 39 | debug_code program do 40 | type 'c' 41 | assert_line_num 3 42 | type 'eval debugger do: "p 2 ** 32"' 43 | assert_line_text('4294967296') 44 | type 'eval debugger do: "p 2 ** 32;; n;; p 2 ** 33;;"' 45 | assert_line_num 4 46 | assert_line_text('4294967296') 47 | assert_line_text('8589934592') 48 | type 'c' 49 | end 50 | end 51 | end 52 | 53 | class DebuggerMethodWithPreCommandTest < DebuggerMethodTest 54 | def program 55 | <<~RUBY 56 | 1| class Foo 57 | 2| def bar 58 | 3| #{METHOD_PLACE_HOLDER}(pre: "p 'aaaaa'") 59 | 4| baz 60 | 5| end 61 | 6| 62 | 7| def baz 63 | 8| #{METHOD_PLACE_HOLDER} 64 | 9| end 65 | 10| end 66 | 11| 67 | 12| Foo.new.bar 68 | RUBY 69 | end 70 | 71 | def test_breakpoint_executes_command_argument_correctly 72 | debug_code(program) do 73 | type 'continue' 74 | assert_line_text('Foo#bar') 75 | assert_line_text(/aaaaa/) 76 | # should stay at Foo#bar 77 | assert_no_line_text(/Foo#baz/) 78 | 79 | type 'continue' 80 | assert_line_text('Foo#baz') 81 | type 'continue' 82 | end 83 | end 84 | 85 | def test_debugger_doesnt_complain_about_duplicated_breakpoint 86 | debug_code(program) do 87 | type 'continue' 88 | assert_no_line_text(/duplicated breakpoint:/) 89 | type 'kill!' 90 | end 91 | end 92 | end 93 | 94 | class DebuggerMethodWithDoCommandTest < DebuggerMethodTest 95 | def program 96 | <<~RUBY 97 | 1| class Foo 98 | 2| def bar 99 | 3| #{METHOD_PLACE_HOLDER}(do: "p 'aaaaa'") 100 | 4| baz 101 | 5| end 102 | 6| 103 | 7| def baz 104 | 8| #{METHOD_PLACE_HOLDER} 105 | 9| end 106 | 10| end 107 | 11| 108 | 12| Foo.new.bar 109 | RUBY 110 | end 111 | 112 | def test_breakpoint_execute_command_argument_correctly 113 | debug_code(program) do 114 | type 'continue' 115 | assert_line_text(/aaaaa/) 116 | # should move on to the next bp 117 | assert_line_text('Foo#baz') 118 | type 'continue' 119 | end 120 | end 121 | 122 | def test_debugger_doesnt_complain_about_duplicated_breakpoint 123 | debug_code(program) do 124 | type 'continue' 125 | assert_no_line_text(/duplicated breakpoint:/) 126 | type 'kill!' 127 | end 128 | end 129 | 130 | class ThreadManagementTest < DebuggerMethodTest 131 | def program 132 | <<~RUBY 133 | 1| Thread.new do 134 | 2| #{METHOD_PLACE_HOLDER}(do: "p 'foo' + 'bar'") 135 | 3| end.join 136 | 4| 137 | 5| Thread.new do 138 | 6| #{METHOD_PLACE_HOLDER}(do: "p 'bar' + 'baz'") 139 | 7| end.join 140 | 8| 141 | 9| #{METHOD_PLACE_HOLDER} 142 | RUBY 143 | end 144 | 145 | def test_debugger_auto_continues_across_threads 146 | debug_code(program) do 147 | type 'continue' 148 | assert_line_text(/foobar/) 149 | assert_line_text(/barbaz/) 150 | type 'continue' 151 | end 152 | end 153 | end 154 | end 155 | 156 | class StepInTest < ConsoleTestCase 157 | def program 158 | <<~RUBY 159 | 1| def foo(num) 160 | 2| num 161 | 3| end 162 | 4| 163 | 5| DEBUGGER__.step_in do 164 | 6| foo(10) 165 | 7| end 166 | RUBY 167 | end 168 | 169 | def test_step_in_stops_the_program 170 | run_ruby(program, options: "-Ilib -rdebug") do 171 | assert_line_num(6) 172 | type "s" 173 | assert_line_num(2) 174 | type "num" 175 | assert_line_text(/10/) 176 | type "c" 177 | end 178 | end 179 | end 180 | 181 | class PreludeTest < ConsoleTestCase 182 | def program 183 | <<~RUBY 184 | 1| require "debug/prelude" 185 | 2| debugger_source = Kernel.method(:debugger).source_location 186 | 3| a = 100 187 | 4| b = 20 188 | 5| debugger 189 | 6| 190 | 7| __END__ 191 | RUBY 192 | end 193 | 194 | def test_prelude_defines_debugger_statements 195 | run_ruby(program, options: "-Ilib") do 196 | assert_line_num(5) 197 | type "a + b" 198 | assert_line_text(/120/) 199 | type "c" 200 | end 201 | end 202 | 203 | def test_prelude_doesnt_override_debugger 204 | run_ruby(program, options: "-Ilib -rdebug") do 205 | assert_line_num(5) 206 | type "debugger_source" 207 | assert_line_text(/debug\/session\.rb/) 208 | type "c" 209 | end 210 | end 211 | 212 | def test_require_config_doesnt_cancel_prelude 213 | run_ruby(program, options: "-Ilib -rdebug/config") do 214 | assert_line_num(5) 215 | type "a + b" 216 | assert_line_text(/120/) 217 | type "c" 218 | end 219 | end 220 | end 221 | end 222 | -------------------------------------------------------------------------------- /test/console/catch_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | class BasicCatchTest < ConsoleTestCase 7 | def program 8 | <<~RUBY 9 | 1| a = 1 10 | 2| b = 2 11 | 3| 12 | 4| 1/0 rescue nil 13 | 5| binding.b 14 | RUBY 15 | end 16 | 17 | def test_debugger_stops_when_the_exception_raised 18 | debug_code(program) do 19 | type 'catch ZeroDivisionError' 20 | assert_line_text(/#0 BP - Catch "ZeroDivisionError"/) 21 | type 'continue' 22 | assert_line_text('Integer#/') 23 | type 'kill!' 24 | end 25 | end 26 | 27 | def test_debugger_stops_when_child_exception_raised 28 | debug_code(program) do 29 | type 'catch StandardError' 30 | type 'continue' 31 | assert_line_text('Integer#/') 32 | type 'kill!' 33 | end 34 | end 35 | 36 | def test_catch_command_isnt_repeatable 37 | debug_code(program) do 38 | type 'catch StandardError' 39 | type '' 40 | assert_no_line_text(/duplicated breakpoint/) 41 | type 'kill!' 42 | end 43 | end 44 | 45 | def test_catch_works_with_command 46 | debug_code(program) do 47 | type 'catch ZeroDivisionError pre: p "catching zero division"' 48 | assert_line_text(/#0 BP - Catch "ZeroDivisionError"/) 49 | type 'continue' 50 | assert_line_text(/catching zero division/) 51 | type 'continue' 52 | type 'continue' 53 | end 54 | 55 | debug_code(program) do 56 | type 'catch ZeroDivisionError do: p "catching zero division"' 57 | assert_line_text(/#0 BP - Catch "ZeroDivisionError"/) 58 | type 'continue' 59 | assert_line_text(/catching zero division/) 60 | type 'continue' 61 | end 62 | end 63 | 64 | def test_catch_works_with_condition 65 | debug_code(program) do 66 | type 'catch ZeroDivisionError if: a == 2 do: p "catching zero division"' 67 | assert_line_text(/#0 BP - Catch "ZeroDivisionError"/) 68 | type 'continue' 69 | assert_no_line_text(/catching zero division/) 70 | type 'continue' 71 | end 72 | end 73 | 74 | def test_debugger_rejects_duplicated_catch_bp 75 | debug_code(program) do 76 | type 'catch ZeroDivisionError' 77 | type 'catch ZeroDivisionError' 78 | assert_line_text(/duplicated breakpoint:/) 79 | type 'continue' 80 | 81 | assert_line_text('Integer#/') # stopped by catch 82 | type 'continue' 83 | 84 | type 'continue' # exit the final binding.b 85 | end 86 | end 87 | end 88 | 89 | class ReraisedExceptionCatchTest < ConsoleTestCase 90 | def program 91 | <<~RUBY 92 | 1| def foo 93 | 2| bar 94 | 3| rescue ZeroDivisionError 95 | 4| raise 96 | 5| end 97 | 6| 98 | 7| def bar 99 | 8| 1/0 100 | 9| end 101 | 10| 102 | 11| foo 103 | RUBY 104 | end 105 | 106 | def test_debugger_stops_when_the_exception_raised 107 | debug_code(program) do 108 | type 'catch ZeroDivisionError' 109 | type 'continue' 110 | assert_line_text('Integer#/') 111 | type 's' 112 | assert_line_text('Object#bar') 113 | type 'kill!' 114 | end 115 | end 116 | end 117 | 118 | class NamespacedExceptionCatchTest < ConsoleTestCase 119 | def program 120 | <<~RUBY 121 | 1| class TestException < StandardError; end 122 | 2| 123 | 3| module Foo 124 | 4| class TestException < StandardError; end 125 | 5| 126 | 6| def self.raised_exception 127 | 7| raise TestException 128 | 8| end 129 | 9| end 130 | 10| 131 | 11| Foo.raised_exception rescue nil 132 | RUBY 133 | end 134 | 135 | def test_catch_without_namespace_does_not_stop_at_exception 136 | debug_code(program) do 137 | type 'catch TestException' 138 | type 'continue' 139 | end 140 | end 141 | 142 | def test_catch_with_namespace_stops_at_exception 143 | debug_code(program) do 144 | type 'catch Foo::TestException' 145 | type 'continue' 146 | assert_line_num(7) 147 | type 'continue' 148 | end 149 | end 150 | end 151 | 152 | class PathOptionTest < ConsoleTestCase 153 | def extra_file 154 | <<~RUBY 155 | def bar 156 | raise "bar" 157 | rescue 158 | end 159 | RUBY 160 | end 161 | 162 | def program(extra_file_path) 163 | <<~RUBY 164 | 1| load "#{extra_file_path}" 165 | 2| 166 | 3| def foo 167 | 4| raise "foo" 168 | 5| rescue 169 | 6| end 170 | 7| 171 | 8| foo 172 | 9| bar 173 | RUBY 174 | end 175 | 176 | def test_catch_only_stops_when_path_matches 177 | with_extra_tempfile do |extra_file| 178 | debug_code(program(extra_file.path)) do 179 | type "catch RuntimeError path: #{extra_file.path}" 180 | type 'c' 181 | assert_line_text(/bar/) 182 | type 'c' 183 | end 184 | end 185 | end 186 | 187 | def test_the_path_option_supersede_skip_path_config 188 | # skips the extra_file's breakpoint 189 | with_extra_tempfile do |extra_file| 190 | debug_code(program(extra_file.path)) do 191 | type "config set skip_path /#{extra_file.path}/" 192 | type "catch RuntimeError" 193 | type 'c' 194 | assert_line_text(/foo/) 195 | type 'c' 196 | end 197 | 198 | # ignores skip_path and stops at designated path 199 | debug_code(program(extra_file.path)) do 200 | type "config set skip_path /#{extra_file.path}/" 201 | type "catch RuntimeError path: #{extra_file.path}" 202 | type 'c' 203 | assert_line_text(/bar/) 204 | type 'c' 205 | end 206 | end 207 | end 208 | end 209 | end 210 | -------------------------------------------------------------------------------- /test/support/dap_utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DEBUGGER__ 4 | module DAP_TestUtils 5 | class RetryBecauseCantRead < Exception 6 | end 7 | 8 | def recv_request sock, backlog 9 | case sock.gets 10 | when /Content-Length: (\d+)/ 11 | b = sock.read(2) 12 | raise b.inspect unless b == "\r\n" 13 | 14 | l = sock.read $1.to_i 15 | backlog << "V)(D|V)\s/, '') 41 | JSON.pretty_generate(JSON.parse h) 42 | }.reverse.join("\n") 43 | 44 | last_protocol_msg = <<~DEBUGGER_MSG.chomp 45 | -------------------------- 46 | | Last Protocol Messages | 47 | -------------------------- 48 | 49 | #{last_msg} 50 | DEBUGGER_MSG 51 | 52 | debuggee_msg = 53 | <<~DEBUGGEE_MSG.chomp 54 | -------------------- 55 | | Debuggee Session | 56 | -------------------- 57 | 58 | > #{remote_info.debuggee_backlog.join('> ')} 59 | DEBUGGEE_MSG 60 | 61 | failure_msg = <<~FAILURE_MSG.chomp 62 | ------------------- 63 | | Failure Message | 64 | ------------------- 65 | 66 | #{fail_msg} 67 | FAILURE_MSG 68 | 69 | <<~MSG.chomp 70 | #{all_protocol_msg} 71 | 72 | #{last_protocol_msg} 73 | 74 | #{debuggee_msg} 75 | 76 | #{failure_msg} 77 | MSG 78 | end 79 | 80 | DAP_TestInfo = Struct.new(:res_backlog, :backlog, :failed_process, :reader_thread, :remote_info) 81 | 82 | class Detach < StandardError 83 | end 84 | 85 | def connect_to_dap_server test_info 86 | remote_info = test_info.remote_info 87 | sock = Socket.unix remote_info.sock_path 88 | test_info.reader_thread = Thread.new(sock, test_info) do |s, info| 89 | while res = recv_request(s, info.backlog) 90 | info.res_backlog << res 91 | end 92 | rescue Detach 93 | end 94 | sleep 0.001 while test_info.reader_thread.status != 'sleep' 95 | test_info.reader_thread.run 96 | sock 97 | end 98 | 99 | TIMEOUT_SEC = (ENV['RUBY_DEBUG_TIMEOUT_SEC'] || 10).to_i 100 | 101 | def run_dap_scenario program, &msgs 102 | begin 103 | write_temp_file(strip_line_num(program)) 104 | 105 | test_info = DAP_TestInfo.new([], []) 106 | remote_info = test_info.remote_info = setup_unix_domain_socket_remote_debuggee 107 | Timeout.timeout(TIMEOUT_SEC) do 108 | sleep 0.001 until remote_info.debuggee_backlog.join.include? 'connection...' 109 | end 110 | 111 | res_log = test_info.res_backlog 112 | sock = nil 113 | target_msg = nil 114 | 115 | msgs.call.each{|msg| 116 | case msg[:type] 117 | when 'request' 118 | if msg[:command] == 'initialize' 119 | sock = connect_to_dap_server test_info 120 | end 121 | str = JSON.dump(msg) 122 | sock.write "Content-Length: #{str.bytesize}\r\n\r\n#{str}" 123 | test_info.backlog << "V>D #{str}" 124 | when 'response' 125 | target_msg = msg 126 | 127 | result = collect_result_from_res_log(res_log, :request_seq, msg) 128 | 129 | msg.delete :seq 130 | verify_result(result, msg, test_info) 131 | 132 | if msg[:command] == 'disconnect' 133 | res_log.clear 134 | test_info.reader_thread.raise Detach 135 | sock.close 136 | end 137 | when 'event' 138 | target_msg = msg 139 | 140 | result = collect_result_from_res_log(res_log, :event, msg) 141 | 142 | msg.delete :seq 143 | verify_result(result, msg, test_info) 144 | 145 | res_log.delete result 146 | end 147 | } 148 | flunk create_protocol_msg test_info, "Expected the debuggee program to finish" unless wait_pid remote_info.pid, 3 149 | rescue Timeout::Error 150 | flunk create_protocol_msg test_info, "TIMEOUT ERROR (#{TIMEOUT_SEC} sec) while waiting for the following response.\n#{JSON.pretty_generate target_msg}" 151 | ensure 152 | test_info.reader_thread.kill 153 | sock.close 154 | remote_info.reader_thread.kill 155 | remote_info.r.close 156 | remote_info.w.close 157 | end 158 | end 159 | 160 | def collect_result_from_res_log(res_log, identifier, msg) 161 | result = nil 162 | 163 | Timeout.timeout(TIMEOUT_SEC) do 164 | loop do 165 | res_log.each{|r| 166 | if r[identifier] == msg[identifier] 167 | result = r 168 | break 169 | end 170 | } 171 | break unless result.nil? 172 | 173 | sleep 0.01 174 | end 175 | end 176 | 177 | result 178 | end 179 | 180 | def verify_result(result, msg, test_info) 181 | expected = ResponsePattern.new.parse msg 182 | expected.each do |key, expected_value| 183 | failure_msg = FailureMessage.new{create_protocol_msg test_info, "expected:\n#{JSON.pretty_generate msg}\n\nresult:\n#{JSON.pretty_generate result}"} 184 | 185 | case expected_value 186 | when Regexp 187 | assert_match expected_value, result.dig(*key).to_s, failure_msg 188 | else 189 | assert_equal expected_value, result.dig(*key), failure_msg 190 | end 191 | end 192 | end 193 | end 194 | end 195 | -------------------------------------------------------------------------------- /lib/debug/tracer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DEBUGGER__ 4 | class Tracer 5 | include SkipPathHelper 6 | include Color 7 | 8 | def colorize(str, color) 9 | # don't colorize trace sent into a file 10 | if @into 11 | str 12 | else 13 | super 14 | end 15 | end 16 | 17 | attr_reader :type, :key 18 | 19 | def initialize ui, pattern: nil, into: nil 20 | if /\ADEBUGGER__::(([A-Z][a-z]+?)[A-Z][a-z]+)/ =~ self.class.name 21 | @name = $1 22 | @type = $2.downcase 23 | end 24 | 25 | setup 26 | 27 | if pattern 28 | @pattern = Regexp.compile(pattern) 29 | else 30 | @pattern = nil 31 | end 32 | 33 | if @into = into 34 | @output = File.open(into, 'w') 35 | @output.puts "PID:#{Process.pid} #{self}" 36 | else 37 | @output = ui 38 | end 39 | 40 | @key = [@type, @pattern, @into].freeze 41 | 42 | enable 43 | end 44 | 45 | def header depth 46 | "DEBUGGER (trace/#{@type}) \#th:#{Thread.current.instance_variable_get(:@__thread_client_id)} \#depth:#{'%-2d'%depth}" 47 | end 48 | 49 | def enable 50 | @tracer.enable 51 | end 52 | 53 | def disable 54 | @tracer.disable 55 | end 56 | 57 | def enabled? 58 | @tracer.enabled? 59 | end 60 | 61 | def description 62 | nil 63 | end 64 | 65 | def to_s 66 | s = "#{@name}#{description} (#{@tracer.enabled? ? 'enabled' : 'disabled'})" 67 | s += " with pattern #{@pattern.inspect}" if @pattern 68 | s += " into: #{@into}" if @into 69 | s 70 | end 71 | 72 | def skip? tp 73 | ThreadClient.current.management? || skip_path?(tp.path) || skip_with_pattern?(tp) 74 | end 75 | 76 | def skip_with_pattern?(tp) 77 | @pattern && !tp.path.match?(@pattern) 78 | end 79 | 80 | def out tp, msg = nil, depth = caller.size - 1 81 | location_str = colorize("#{FrameInfo.pretty_path(tp.path)}:#{tp.lineno}", [:GREEN]) 82 | buff = "#{header(depth)}#{msg} at #{location_str}" 83 | 84 | if false # TODO: Ractor.main? 85 | ThreadClient.current.on_trace self.object_id, buff 86 | else 87 | @output.puts buff 88 | @output.flush 89 | end 90 | end 91 | 92 | def minfo tp 93 | return "block{}" if tp.event == :b_call 94 | 95 | klass = tp.defined_class 96 | 97 | if klass.singleton_class? 98 | "#{tp.self}.#{tp.method_id}" 99 | else 100 | "#{klass}\##{tp.method_id}" 101 | end 102 | end 103 | end 104 | 105 | class LineTracer < Tracer 106 | def setup 107 | @tracer = TracePoint.new(:line){|tp| 108 | next if skip?(tp) 109 | # pp tp.object_id, caller(0) 110 | out tp 111 | } 112 | end 113 | end 114 | 115 | class CallTracer < Tracer 116 | def setup 117 | @tracer = TracePoint.new(:a_call, :a_return){|tp| 118 | next if skip?(tp) 119 | 120 | depth = caller.size 121 | 122 | call_identifier_str = 123 | if tp.defined_class 124 | minfo(tp) 125 | else 126 | "block" 127 | end 128 | 129 | call_identifier_str = colorize_blue(call_identifier_str) 130 | 131 | case tp.event 132 | when :call, :c_call, :b_call 133 | depth += 1 if tp.event == :c_call 134 | sp = ' ' * depth 135 | out tp, ">#{sp}#{call_identifier_str}", depth 136 | when :return, :c_return, :b_return 137 | depth += 1 if tp.event == :c_return 138 | sp = ' ' * depth 139 | return_str = colorize_magenta(DEBUGGER__.safe_inspect(tp.return_value, short: true)) 140 | out tp, "<#{sp}#{call_identifier_str} #=> #{return_str}", depth 141 | end 142 | } 143 | end 144 | 145 | def skip_with_pattern?(tp) 146 | super && !tp.method_id&.match?(@pattern) 147 | end 148 | end 149 | 150 | class ExceptionTracer < Tracer 151 | def setup 152 | @tracer = TracePoint.new(:raise) do |tp| 153 | next if skip?(tp) 154 | 155 | exc = tp.raised_exception 156 | 157 | out tp, " #{colorize_magenta(exc.inspect)}" 158 | rescue Exception => e 159 | p e 160 | end 161 | end 162 | 163 | def skip_with_pattern?(tp) 164 | super && !tp.raised_exception.inspect.match?(@pattern) 165 | end 166 | end 167 | 168 | class ObjectTracer < Tracer 169 | def initialize ui, obj_id, obj_inspect, **kw 170 | @obj_id = obj_id 171 | @obj_inspect = obj_inspect 172 | super(ui, **kw) 173 | @key = [@type, @obj_id, @pattern, @into].freeze 174 | end 175 | 176 | def description 177 | " for #{@obj_inspect}" 178 | end 179 | 180 | def colorized_obj_inspect 181 | colorize_magenta(@obj_inspect) 182 | end 183 | 184 | def setup 185 | @tracer = TracePoint.new(:a_call){|tp| 186 | next if skip?(tp) 187 | 188 | if M_OBJECT_ID.bind_call(tp.self) == @obj_id 189 | klass = tp.defined_class 190 | method = tp.method_id 191 | method_info = 192 | if klass.singleton_class? 193 | if tp.self.is_a?(Class) 194 | ".#{method} (#{klass}.#{method})" 195 | else 196 | ".#{method}" 197 | end 198 | else 199 | "##{method} (#{klass}##{method})" 200 | end 201 | 202 | out tp, " #{colorized_obj_inspect} receives #{colorize_blue(method_info)}" 203 | elsif !tp.parameters.empty? 204 | b = tp.binding 205 | method_info = colorize_blue(minfo(tp)) 206 | 207 | tp.parameters.each{|type, name| 208 | next unless name 209 | 210 | colorized_name = colorize_cyan(name) 211 | 212 | case type 213 | when :req, :opt, :key, :keyreq 214 | if b.local_variable_get(name).object_id == @obj_id 215 | out tp, " #{colorized_obj_inspect} is used as a parameter #{colorized_name} of #{method_info}" 216 | end 217 | when :rest 218 | next if name == :"*" 219 | 220 | ary = b.local_variable_get(name) 221 | ary.each{|e| 222 | if e.object_id == @obj_id 223 | out tp, " #{colorized_obj_inspect} is used as a parameter in #{colorized_name} of #{method_info}" 224 | end 225 | } 226 | when :keyrest 227 | next if name == :'**' 228 | h = b.local_variable_get(name) 229 | h.each{|k, e| 230 | if e.object_id == @obj_id 231 | out tp, " #{colorized_obj_inspect} is used as a parameter in #{colorized_name} of #{method_info}" 232 | end 233 | } 234 | end 235 | } 236 | end 237 | } 238 | end 239 | end 240 | end 241 | 242 | -------------------------------------------------------------------------------- /lib/debug/console.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module DEBUGGER__ 3 | class Console 4 | begin 5 | raise LoadError if CONFIG[:no_reline] 6 | require 'reline' 7 | 8 | require_relative 'color' 9 | 10 | include Color 11 | 12 | def parse_input buff, commands 13 | c, rest = get_command buff 14 | case 15 | when commands.keys.include?(c) 16 | :command 17 | when !rest && /\A\s*[a-z]*\z/ =~ c 18 | nil 19 | else 20 | :ruby 21 | end 22 | end 23 | 24 | def readline_setup prompt 25 | load_history_if_not_loaded 26 | commands = DEBUGGER__.commands 27 | 28 | prev_completion_proc = Reline.completion_proc 29 | prev_output_modifier_proc = Reline.output_modifier_proc 30 | prev_prompt_proc = Reline.prompt_proc 31 | 32 | # prompt state 33 | state = nil # :command, :ruby, nil (unknown) 34 | 35 | Reline.prompt_proc = -> args, *kw do 36 | case state = parse_input(args.first, commands) 37 | when nil, :command 38 | [prompt] 39 | when :ruby 40 | [prompt.sub('rdbg'){colorize('ruby', [:RED])}] 41 | end * args.size 42 | end 43 | 44 | Reline.completion_proc = -> given do 45 | buff = Reline.line_buffer 46 | Reline.completion_append_character= ' ' 47 | 48 | if /\s/ =~ buff # second parameters 49 | given = File.expand_path(given + 'a').sub(/a\z/, '') 50 | files = Dir.glob(given + '*') 51 | if files.size == 1 && File.directory?(files.first) 52 | Reline.completion_append_character= '/' 53 | end 54 | files 55 | else 56 | commands.keys.grep(/\A#{Regexp.escape(given)}/) 57 | end 58 | end 59 | 60 | Reline.output_modifier_proc = -> buff, **kw do 61 | c, rest = get_command buff 62 | 63 | case state 64 | when :command 65 | cmd = colorize(c, [:CYAN, :UNDERLINE]) 66 | 67 | if commands[c] == c 68 | rprompt = colorize(" # command", [:DIM]) 69 | else 70 | rprompt = colorize(" # #{commands[c]} command", [:DIM]) 71 | end 72 | 73 | rest = rest ? colorize_code(rest) : '' 74 | cmd + rest + rprompt 75 | when nil 76 | buff 77 | when :ruby 78 | colorize_code(buff) 79 | end 80 | end unless CONFIG[:no_hint] 81 | 82 | yield 83 | 84 | ensure 85 | Reline.completion_proc = prev_completion_proc 86 | Reline.output_modifier_proc = prev_output_modifier_proc 87 | Reline.prompt_proc = prev_prompt_proc 88 | end 89 | 90 | private def get_command line 91 | case line.chomp 92 | when /\A(\s*[a-z]+)(\s.*)?\z$/ 93 | return $1.strip, $2 94 | else 95 | line.strip 96 | end 97 | end 98 | 99 | def readline prompt 100 | readline_setup prompt do 101 | Reline.readmultiline(prompt, true){ true } 102 | end 103 | end 104 | 105 | def history 106 | Reline::HISTORY 107 | end 108 | 109 | rescue LoadError 110 | begin 111 | require 'readline.so' 112 | 113 | def readline_setup 114 | load_history_if_not_loaded 115 | commands = DEBUGGER__.commands 116 | 117 | Readline.completion_proc = proc{|given| 118 | buff = Readline.line_buffer 119 | Readline.completion_append_character= ' ' 120 | 121 | if /\s/ =~ buff # second parameters 122 | given = File.expand_path(given + 'a').sub(/a\z/, '') 123 | files = Dir.glob(given + '*') 124 | if files.size == 1 && File.directory?(files.first) 125 | Readline.completion_append_character= '/' 126 | end 127 | files 128 | else 129 | commands.keys.grep(/\A#{given}/) 130 | end 131 | } 132 | end 133 | 134 | def readline prompt 135 | readline_setup 136 | Readline.readline(prompt, true) 137 | end 138 | 139 | def history 140 | Readline::HISTORY 141 | end 142 | 143 | rescue LoadError 144 | def readline prompt 145 | print prompt 146 | $stdin.gets 147 | end 148 | 149 | def history 150 | nil 151 | end 152 | end 153 | end 154 | 155 | def history_file 156 | case 157 | when (path = CONFIG[:history_file]) && !path.empty? 158 | path = File.expand_path(path) 159 | when (path = File.expand_path("~/.rdbg_history")) && File.exist?(path) # for compatibility 160 | # path 161 | else 162 | state_dir = ENV['XDG_STATE_HOME'] || File.join(Dir.home, '.local', 'state') 163 | path = File.join(File.expand_path(state_dir), 'rdbg', 'history') 164 | end 165 | 166 | FileUtils.mkdir_p(File.dirname(path)) unless File.exist?(path) 167 | path 168 | end 169 | 170 | FH = "# Today's OMIKUJI: " 171 | 172 | def read_history_file 173 | if history && File.exist?(path = history_file()) 174 | f = (['', 'DAI-', 'CHU-', 'SHO-'].map{|e| e+'KICHI'}+['KYO']).sample 175 | # Read history file and scrub invalid characters to prevent encoding errors 176 | lines = File.readlines(path).map(&:scrub) 177 | ["#{FH}#{f}".dup] + lines 178 | else 179 | [] 180 | end 181 | end 182 | 183 | def initialize 184 | @init_history_lines = nil 185 | end 186 | 187 | def load_history_if_not_loaded 188 | return if @init_history_lines 189 | 190 | @init_history_lines = load_history 191 | end 192 | 193 | def deactivate 194 | if history && @init_history_lines 195 | added_records = history.to_a[@init_history_lines .. -1] 196 | path = history_file() 197 | max = CONFIG[:save_history] 198 | 199 | if !added_records.empty? && !path.empty? 200 | orig_records = read_history_file 201 | open(history_file(), 'w'){|f| 202 | (orig_records + added_records).last(max).each{|line| 203 | # Use scrub to handle encoding issues gracefully 204 | scrubbed_line = line.scrub.strip 205 | if !line.start_with?(FH) && !scrubbed_line.empty? 206 | f.puts scrubbed_line 207 | end 208 | } 209 | } 210 | end 211 | end 212 | end 213 | 214 | def load_history 215 | read_history_file.each{|line| 216 | # Use scrub to handle encoding issues gracefully, then strip 217 | line.scrub! 218 | line.strip! 219 | history << line unless line.empty? 220 | } if history.empty? 221 | history.count 222 | end 223 | end # class Console 224 | end 225 | -------------------------------------------------------------------------------- /test/protocol/rdbgTraceInspctor_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/protocol_test_case' 4 | 5 | module DEBUGGER__ 6 | class RdbgTraceInspectorTraceTest < ProtocolTestCase 7 | PROGRAM = <<~RUBY 8 | 1| def foo 9 | 2| 'bar' 10 | 3| end 11 | 4| foo 12 | 5| foo 13 | RUBY 14 | 15 | def test_defaut_setting 16 | DEBUGGER__::INITIALIZE_DAP_MSGS[1][:arguments][:rdbgExtensions] = ["traceInspector"] 17 | run_protocol_scenario(PROGRAM, cdp: false) do 18 | req_rdbgTraceInspector_trace_enable 19 | req_add_breakpoint 5 20 | req_continue 21 | assert_rdbgTraceInspector_trace_collect_result( 22 | [ 23 | { 24 | returnValue: "\"bar\"", 25 | name: 'Object#foo', 26 | location: { 27 | line: 3, 28 | } 29 | }, 30 | { 31 | name: 'Object#foo', 32 | location: { 33 | line: 1, 34 | } 35 | }, 36 | { 37 | location: { 38 | line: 4, 39 | } 40 | }, 41 | ] 42 | ) 43 | req_terminate_debuggee 44 | end 45 | ensure 46 | DEBUGGER__::INITIALIZE_DAP_MSGS[1][:arguments].delete :rdbgExtensions 47 | end 48 | 49 | def test_call_event 50 | DEBUGGER__::INITIALIZE_DAP_MSGS[1][:arguments][:rdbgExtensions] = ["traceInspector"] 51 | run_protocol_scenario(PROGRAM, cdp: false) do 52 | req_rdbgTraceInspector_trace_enable(events: ['traceCall']) 53 | req_add_breakpoint 5 54 | req_continue 55 | assert_rdbgTraceInspector_trace_collect_result( 56 | [ 57 | { 58 | name: 'Object#foo', 59 | location: { 60 | line: 1, 61 | } 62 | }, 63 | ] 64 | ) 65 | req_terminate_debuggee 66 | end 67 | ensure 68 | DEBUGGER__::INITIALIZE_DAP_MSGS[1][:arguments].delete :rdbgExtensions 69 | end 70 | 71 | def test_return_event 72 | DEBUGGER__::INITIALIZE_DAP_MSGS[1][:arguments][:rdbgExtensions] = ["traceInspector"] 73 | run_protocol_scenario(PROGRAM, cdp: false) do 74 | req_rdbgTraceInspector_trace_enable(events: ['traceReturn']) 75 | req_add_breakpoint 5 76 | req_continue 77 | assert_rdbgTraceInspector_trace_collect_result( 78 | [ 79 | { 80 | returnValue: "\"bar\"", 81 | name: 'Object#foo', 82 | location: { 83 | line: 3, 84 | } 85 | }, 86 | ] 87 | ) 88 | req_terminate_debuggee 89 | end 90 | ensure 91 | DEBUGGER__::INITIALIZE_DAP_MSGS[1][:arguments].delete :rdbgExtensions 92 | end 93 | 94 | def test_line_event 95 | DEBUGGER__::INITIALIZE_DAP_MSGS[1][:arguments][:rdbgExtensions] = ["traceInspector"] 96 | run_protocol_scenario(PROGRAM, cdp: false) do 97 | req_rdbgTraceInspector_trace_enable(events: ['traceLine']) 98 | req_add_breakpoint 5 99 | req_continue 100 | assert_rdbgTraceInspector_trace_collect_result( 101 | [ 102 | { 103 | location: { 104 | line: 4, 105 | } 106 | }, 107 | ] 108 | ) 109 | req_terminate_debuggee 110 | end 111 | ensure 112 | DEBUGGER__::INITIALIZE_DAP_MSGS[1][:arguments].delete :rdbgExtensions 113 | end 114 | 115 | def test_restart_trace 116 | DEBUGGER__::INITIALIZE_DAP_MSGS[1][:arguments][:rdbgExtensions] = ["traceInspector"] 117 | run_protocol_scenario(PROGRAM, cdp: false) do 118 | req_rdbgTraceInspector_trace_enable 119 | req_rdbgTraceInspector_trace_disable 120 | req_rdbgTraceInspector_trace_enable(events: ['traceLine']) 121 | req_add_breakpoint 5 122 | req_continue 123 | assert_rdbgTraceInspector_trace_collect_result( 124 | [ 125 | { 126 | location: { 127 | line: 4, 128 | } 129 | }, 130 | ] 131 | ) 132 | req_terminate_debuggee 133 | end 134 | ensure 135 | DEBUGGER__::INITIALIZE_DAP_MSGS[1][:arguments].delete :rdbgExtensions 136 | end 137 | end 138 | 139 | class RdbgTraceInspectorRecordTest < ProtocolTestCase 140 | PROGRAM = <<~RUBY 141 | 1| module Foo 142 | 2| class Bar 143 | 3| def self.a 144 | 4| "hello" 145 | 5| end 146 | 6| end 147 | 7| Bar.a 148 | 8| bar = Bar.new 149 | 9| end 150 | RUBY 151 | 152 | def test_defaut_setting 153 | DEBUGGER__::INITIALIZE_DAP_MSGS[1][:arguments][:rdbgExtensions] = ["traceInspector"] 154 | run_protocol_scenario(PROGRAM, cdp: false) do 155 | req_rdbgTraceInspector_record_enable 156 | req_add_breakpoint 5 157 | req_continue 158 | assert_rdbgTraceInspector_record_collect_result( 159 | [ 160 | { 161 | name: "", 162 | location: { 163 | line: 2, 164 | } 165 | }, 166 | { 167 | name: "", 168 | location: { 169 | line: 3, 170 | } 171 | } 172 | ] 173 | ) 174 | req_rdbgTraceInspector_record_step_back 4 175 | assert_line_num 2 176 | req_rdbgTraceInspector_record_step 1 177 | assert_line_num 3 178 | req_terminate_debuggee 179 | end 180 | ensure 181 | DEBUGGER__::INITIALIZE_DAP_MSGS[1][:arguments].delete :rdbgExtensions 182 | end 183 | 184 | def test_restart_trace 185 | DEBUGGER__::INITIALIZE_DAP_MSGS[1][:arguments][:rdbgExtensions] = ["traceInspector"] 186 | run_protocol_scenario(PROGRAM, cdp: false) do 187 | req_rdbgTraceInspector_record_enable 188 | req_rdbgTraceInspector_record_disable 189 | req_rdbgTraceInspector_record_enable 190 | req_add_breakpoint 5 191 | req_continue 192 | assert_rdbgTraceInspector_record_collect_result( 193 | [ 194 | { 195 | name: "", 196 | location: { 197 | line: 2, 198 | } 199 | }, 200 | { 201 | name: "", 202 | location: { 203 | line: 3, 204 | } 205 | } 206 | ] 207 | ) 208 | req_rdbgTraceInspector_record_step_back 4 209 | assert_line_num 2 210 | req_rdbgTraceInspector_record_step 1 211 | assert_line_num 3 212 | req_terminate_debuggee 213 | end 214 | ensure 215 | DEBUGGER__::INITIALIZE_DAP_MSGS[1][:arguments].delete :rdbgExtensions 216 | end 217 | end 218 | end 219 | -------------------------------------------------------------------------------- /test/console/backtrace_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../support/console_test_case' 4 | 5 | module DEBUGGER__ 6 | class BasicBacktraceTest < ConsoleTestCase 7 | def program 8 | <<~RUBY 9 | 1| class Foo 10 | 2| def first_call 11 | 3| second_call(20) 12 | 4| end 13 | 5| 14 | 6| def second_call(num) 15 | 7| third_call_with_block do |ten| 16 | 8| num + ten 17 | 9| end 18 | 10| end 19 | 11| 20 | 12| def third_call_with_block(&block) 21 | 13| yield(10) 22 | 14| end 23 | 15| end 24 | 16| 25 | 17| [1, 2, 3].reverse_each do 26 | 18| Foo.new.first_call 27 | 19| end 28 | RUBY 29 | end 30 | 31 | def test_backtrace_prints_c_method_frame 32 | debug_code(program) do 33 | type 'b 18' 34 | type 'c' 35 | type 'bt' 36 | assert_line_text(/\[C\] Array#reverse_each/) 37 | type 'kill!' 38 | end 39 | end 40 | 41 | def test_backtrace_prints_the_return_value 42 | debug_code(program) do 43 | type 'b 4' 44 | type 'c' 45 | type 'bt' 46 | assert_line_text(/Foo#first_call .* #=> 30/) 47 | type 'kill!' 48 | end 49 | end 50 | 51 | def test_backtrace_prints_method_arguments 52 | debug_code(program) do 53 | type 'b 7' 54 | type 'c' 55 | type 'bt' 56 | assert_line_text(/Foo#second_call\(num=20\)/) 57 | type 'kill!' 58 | end 59 | end 60 | 61 | def test_backtrace_prints_block_arguments 62 | debug_code(program) do 63 | type 'b 9' 64 | type 'c' 65 | type 'bt' 66 | assert_line_text(/block {\|ten=10\|}/) 67 | type 'kill!' 68 | end 69 | end 70 | 71 | def test_backtrace_prints_a_given_number_of_traces 72 | debug_code(program) do 73 | type 'b 13' 74 | type 'c' 75 | type 'bt 2' 76 | assert_line_text(/Foo#third_call_with_block/) 77 | assert_line_text(/Foo#second_call/) 78 | assert_no_line_text(/Foo#first_call/) 79 | type 'kill!' 80 | end 81 | end 82 | 83 | def test_backtrace_filters_traces_with_location 84 | debug_code(program) do 85 | type 'b 13' 86 | type 'c' 87 | type 'bt /rb:\d\z/' 88 | assert_line_text(/Foo#second_call/) 89 | assert_line_text(/Foo#first_call/) 90 | assert_no_line_text(/Foo#third_call_with_block/) 91 | type 'kill!' 92 | end 93 | end 94 | 95 | def test_backtrace_filters_traces_with_method_name 96 | debug_code(program) do 97 | type 'b 13' 98 | type 'c' 99 | type 'bt /second/' 100 | assert_line_text(/Foo#second_call/) 101 | assert_no_line_text(/Foo#first_call/) 102 | assert_no_line_text(/Foo#third_call_with_block/) 103 | type 'kill!' 104 | end 105 | end 106 | 107 | def test_backtrace_takes_both_number_and_pattern 108 | debug_code(program) do 109 | type 'b 13' 110 | type 'c' 111 | type 'bt 1 /rb:\d\z/' 112 | assert_line_text(/Foo#second_call/) 113 | assert_no_line_text(/Foo#first_call/) 114 | type 'kill!' 115 | end 116 | end 117 | 118 | def test_frame_filtering_works_with_unexpanded_path_and_expanded_skip_path 119 | foo_path = "#{pty_home_dir}/foo_#{Time.now.to_i}.rb" 120 | foo_file = <<~RUBY 121 | class Foo 122 | def bar 123 | debugger 124 | end 125 | end 126 | RUBY 127 | 128 | program = <<~RUBY 129 | 1| load "~/#{File.basename(foo_path)}" 130 | 2| Foo.new.bar 131 | RUBY 132 | 133 | begin 134 | File.open(foo_path, 'w+').close 135 | rescue Errno::EACCES, Errno::EPERM 136 | omit "Skip test with load files. Cannot create files in HOME directory." 137 | end 138 | 139 | File.open(foo_path, 'w+') { |f| f.write(foo_file) } 140 | debug_code(program) do 141 | type 'c' 142 | type 'bt' 143 | assert_line_text(/Foo#bar/) 144 | assert_line_text(/~\/foo_\d+.rb/) 145 | type "eval DEBUGGER__::CONFIG[:skip_path] = '#{foo_path}'" 146 | type 'bt' 147 | assert_no_line_text(/Foo#bar/) # ~/foo....rb should match foo.rb's absolute path and be skipped 148 | assert_no_line_text(/~\/foo\.rb/) 149 | type 'c' 150 | end 151 | ensure 152 | if File.exist? foo_path 153 | File.unlink foo_path 154 | end 155 | end 156 | end 157 | 158 | class BlockTraceTest < ConsoleTestCase 159 | def program 160 | <<~RUBY 161 | 1| tap do 162 | 2| tap do 163 | 3| p 1 164 | 4| end 165 | 5| end 166 | 6| 167 | 7| __END__ 168 | RUBY 169 | end 170 | 171 | def test_backtrace_prints_block_label_correctly 172 | debug_code(program) do 173 | type 'b 2' 174 | type 'c' 175 | type 'bt' 176 | assert_line_text(/block in
at/) 177 | type 'kill!' 178 | end 179 | end 180 | 181 | def test_backtrace_prints_nested_block_label_correctly 182 | debug_code(program) do 183 | type 'b 3' 184 | type 'c' 185 | type 'bt' 186 | assert_line_text(/block in
\(2 levels\) at/) 187 | type 'kill!' 188 | end 189 | end 190 | end 191 | 192 | class ThreadLockingTraceTest < ConsoleTestCase 193 | def program 194 | <<~RUBY 195 | 1| th0 = Thread.new{sleep} 196 | 2| $m = Mutex.new 197 | 3| th1 = Thread.new do 198 | 4| $m.lock 199 | 5| sleep 1 200 | 6| $m.unlock 201 | 7| end 202 | 8| 203 | 9| o = Object.new 204 | 10| def o.inspect 205 | 11| $m.lock 206 | 12| "foo".tap { $m.unlock } 207 | 13| end 208 | 14| 209 | 15| def foo(o) 210 | 16| debugger 211 | 17| end 212 | 18| sleep 0.5 213 | 19| foo(o) 214 | RUBY 215 | end 216 | 217 | def test_backtrace_prints_without_hanging 218 | debug_code(program) do 219 | type "c" 220 | 221 | type "bt" 222 | assert_line_text(/Object#foo\(o=foo\)/) 223 | type "bt" 224 | assert_line_text(/Object#foo\(o=foo\)/) 225 | type "bt" 226 | assert_line_text(/Object#foo\(o=foo\)/) 227 | 228 | type "kill!" 229 | end 230 | end 231 | end 232 | 233 | class BrokenSingletonMethodBacktraceTest < ConsoleTestCase 234 | def program 235 | <<~RUBY 236 | 1| class C 237 | 2| def self.foo 238 | 3| debugger 239 | 4| end 240 | 5| def singleton_class 241 | 6| raise 242 | 7| end 243 | 8| def self.singleton_class 244 | 9| eval(")") # SyntaxError 245 | 10| end 246 | 11| end 247 | 12| C.foo 248 | RUBY 249 | end 250 | 251 | def test_raise_exception 252 | debug_code program do 253 | type 'c' 254 | assert_line_text(/foo/) 255 | type 'c' 256 | end 257 | end 258 | end 259 | end 260 | -------------------------------------------------------------------------------- /ext/debug/debug.c: -------------------------------------------------------------------------------- 1 | 2 | #include "ruby/ruby.h" 3 | #include "ruby/debug.h" 4 | #include "ruby/encoding.h" 5 | #include "debug_version.h" 6 | // 7 | static VALUE rb_mDebugger; 8 | 9 | // iseq 10 | typedef struct rb_iseq_struct rb_iseq_t; 11 | const rb_iseq_t *rb_iseqw_to_iseq(VALUE iseqw); 12 | VALUE rb_iseq_realpath(const rb_iseq_t *iseq); 13 | 14 | static VALUE 15 | iseq_realpath(VALUE iseqw) 16 | { 17 | return rb_iseq_realpath(rb_iseqw_to_iseq(iseqw)); 18 | } 19 | 20 | static VALUE rb_cFrameInfo; 21 | 22 | static VALUE 23 | di_entry(VALUE loc, VALUE self, VALUE binding, VALUE iseq, VALUE klass, VALUE depth) 24 | { 25 | return rb_struct_new(rb_cFrameInfo, 26 | // :location, :self, :binding, :iseq, :class, :frame_depth, 27 | loc, self, binding, iseq, klass, depth, 28 | // :has_return_value, :return_value, 29 | Qnil, Qnil, 30 | // :has_raised_exception, :raised_exception, 31 | Qnil, Qnil, 32 | // :show_line, :local_variables 33 | Qnil, 34 | // :_local_variables, :_callee # for recorder 35 | Qnil, Qnil, 36 | // :dupped_binding 37 | Qnil 38 | ); 39 | } 40 | 41 | static int 42 | str_start_with(VALUE str, VALUE prefix) 43 | { 44 | StringValue(prefix); 45 | rb_enc_check(str, prefix); 46 | if (RSTRING_LEN(str) >= RSTRING_LEN(prefix) && 47 | memcmp(RSTRING_PTR(str), RSTRING_PTR(prefix), RSTRING_LEN(prefix)) == 0) { 48 | return 1; 49 | } 50 | else { 51 | return 0; 52 | } 53 | } 54 | 55 | static VALUE 56 | di_body(const rb_debug_inspector_t *dc, void *ptr) 57 | { 58 | VALUE skip_path_prefix = (VALUE)ptr; 59 | VALUE locs = rb_debug_inspector_backtrace_locations(dc); 60 | VALUE ary = rb_ary_new(); 61 | long len = RARRAY_LEN(locs); 62 | long i; 63 | 64 | for (i=1; i e 253 | STDERR.puts "disconnected (#{e})" 254 | exit 255 | ensure 256 | deactivate 257 | end 258 | end 259 | end 260 | 261 | if __FILE__ == $0 262 | DEBUGGER__::Client.new(argv).connect 263 | end 264 | -------------------------------------------------------------------------------- /test/support/cdp_utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DEBUGGER__ 4 | module CDP_TestUtils 5 | 6 | class Detach < StandardError 7 | end 8 | 9 | # search free port by opening server socket with port 0 10 | Socket.tcp_server_sockets(0).tap do |ss| 11 | TCPIP_PORT = ss.first.local_address.ip_port 12 | end.each{|s| s.close} 13 | 14 | RUBY = ENV['RUBY'] || RbConfig.ruby 15 | RDBG_EXECUTABLE = "#{RUBY} #{__dir__}/../../exe/rdbg" 16 | 17 | def setup_chrome_debuggee 18 | @remote_info = setup_remote_debuggee("#{RDBG_EXECUTABLE} -O --port=#{TCPIP_PORT} -- #{temp_file_path}") 19 | @remote_info.port = TCPIP_PORT 20 | 21 | Timeout.timeout(TIMEOUT_SEC) do 22 | sleep 0.001 until @remote_info.debuggee_backlog.join.include? 'connection' 23 | end 24 | rescue Timeout::Error 25 | flunk <<~MSG 26 | -------------------- 27 | | Debuggee Session | 28 | -------------------- 29 | > #{@remote_info.debuggee_backlog.join('> ')} 30 | TIMEOUT ERROR (#{TIMEOUT_SEC} sec) 31 | MSG 32 | end 33 | 34 | def connect_to_cdp_server 35 | ENV['RUBY_DEBUG_TEST_MODE'] = 'true' 36 | 37 | body = get_request HOST, @remote_info.port, '/json' 38 | sock = Socket.tcp HOST, @remote_info.port 39 | uuid = body[0][:id] 40 | 41 | Timeout.timeout(TIMEOUT_SEC) do 42 | sleep 0.001 until @remote_info.debuggee_backlog.join.match?(/Disconnected\.\R.*Connected/) 43 | end 44 | @web_sock = WebSocketClient.new sock 45 | @web_sock.handshake @remote_info.port, uuid 46 | @reader_thread = Thread.new do 47 | Thread.current.abort_on_exception = true 48 | while res = @web_sock.extract_data 49 | str = JSON.dump res 50 | @backlog << "C #{remote_info.debuggee_backlog.join('> ')} 62 | TIMEOUT ERROR (#{TIMEOUT_SEC} sec) 63 | MSG 64 | end 65 | 66 | TIMEOUT_SEC = (ENV['RUBY_DEBUG_TIMEOUT_SEC'] || 10).to_i 67 | HOST = '127.0.0.1' 68 | 69 | def run_cdp_scenario program, &msgs 70 | ENV['RUBY_DEBUG_TEST_UI'] = 'chrome' 71 | 72 | program = program.delete_suffix "\n" 73 | write_temp_file(strip_line_num(program)) 74 | 75 | setup_chrome_debuggee 76 | connect_to_cdp_server 77 | exchange_cdp_message msgs 78 | rescue Detach 79 | end 80 | 81 | def exchange_cdp_message msgs 82 | @res_backlog = [] 83 | @backlog = [] 84 | target_msg = nil 85 | obj_map = {} 86 | current_frame = nil 87 | evaluateOnCallFrameId = nil 88 | expression = nil 89 | getProperties_id = nil 90 | msgs.call.each{|msg| 91 | case 92 | # request 93 | when msg.key?(:method) && msg.key?(:id) 94 | case msg[:method] 95 | when 'Runtime.getProperties' 96 | o_id = msg.dig(:params, :objectId) 97 | msg[:params][:objectId] = obj_map[o_id] 98 | getProperties_id = msg[:id] 99 | when 'Debugger.evaluateOnCallFrame' 100 | callFrameId = current_frame[:callFrameId] 101 | msg[:params][:callFrameId] = callFrameId 102 | expression = msg.dig(:params, :expression) 103 | evaluateOnCallFrameId = msg[:id] 104 | end 105 | 106 | @web_sock.send(msg) 107 | str = JSON.dump msg 108 | @backlog << "C>D #{str}" 109 | # response 110 | when msg.key?(:id) && (msg.key?(:result) || msg.key?(:error)) 111 | target_msg = msg 112 | 113 | result, _ = find_result(:id, msg) 114 | case result[:id] 115 | when evaluateOnCallFrameId 116 | o_id = result.dig(:result, :result, :objectId) 117 | obj_map[expression] = o_id 118 | when getProperties_id 119 | rs = result.dig(:result, :result) 120 | rs.each{|r| 121 | o_id = r.dig(:value, :objectId) 122 | v = r.dig(:value, :value) 123 | obj_map[v] = o_id 124 | } 125 | internalProperties = result.dig(:result, :internalProperties) 126 | internalProperties.each{|p| 127 | o_id = p.dig(:value, :objectId) 128 | description = p.dig(:value, :description) 129 | obj_map[description] = o_id 130 | } unless internalProperties.nil? 131 | end 132 | 133 | assert_result msg, result 134 | # event 135 | when msg.key?(:method) 136 | target_msg = msg 137 | 138 | result, result_idx = find_result(:method, msg) 139 | case result[:method] 140 | when 'Debugger.paused' 141 | frames = result.dig(:params, :callFrames) 142 | current_frame = frames.first 143 | frames.each_with_index{|frame, idx| 144 | frame[:scopeChain].each{|scope| 145 | o_id = scope.dig(:object, :objectId) 146 | key = "#{idx}:#{scope[:type]}" 147 | obj_map[key] = o_id 148 | } 149 | } 150 | end 151 | 152 | assert_result msg, result 153 | @res_backlog.delete_at result_idx 154 | else 155 | raise "Unknown message #{msg}" 156 | end 157 | } 158 | flunk create_protocol_message "Expected the debuggee program to finish" unless wait_pid @remote_info.pid, TIMEOUT_SEC 159 | rescue Timeout::Error 160 | flunk create_protocol_message"TIMEOUT ERROR (#{TIMEOUT_SEC} sec) while waiting for the following response.\n#{JSON.pretty_generate target_msg}" 161 | ensure 162 | @reader_thread.kill 163 | @web_sock.close 164 | @remote_info.reader_thread.kill 165 | @remote_info.r.close 166 | @remote_info.w.close 167 | end 168 | 169 | # FIXME: Commonalize this method. 170 | def find_result(identifier, msg) 171 | result = nil 172 | result_idx = nil 173 | 174 | Timeout.timeout(TIMEOUT_SEC) do 175 | loop do 176 | @res_backlog.each_with_index{|r, i| 177 | if r[identifier] == msg[identifier] 178 | result = r 179 | result_idx = i 180 | break 181 | end 182 | } 183 | break unless result.nil? 184 | 185 | sleep 0.01 186 | end 187 | end 188 | 189 | [result, result_idx] 190 | end 191 | 192 | # FIXME: Commonalize this method. 193 | def assert_result(expected, actual) 194 | pattern = ResponsePattern.new.parse expected 195 | pattern.each do |key, expected_value| 196 | msg = <<~MSG 197 | expected: 198 | #{JSON.pretty_generate expected} 199 | 200 | actual: 201 | #{JSON.pretty_generate actual} 202 | MSG 203 | failure_msg = FailureMessage.new{create_protocol_message msg} 204 | 205 | case expected_value 206 | when Regexp 207 | assert_match expected_value, actual.dig(*key).to_s, failure_msg 208 | else 209 | assert_equal expected_value, actual.dig(*key), failure_msg 210 | end 211 | end 212 | end 213 | end 214 | end 215 | --------------------------------------------------------------------------------