summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/irb.rb83
-rw-r--r--lib/irb/cmd/debug.rb128
-rw-r--r--lib/irb/cmd/subirb.rb36
-rw-r--r--lib/irb/context.rb2
-rw-r--r--lib/irb/debug.rb127
-rw-r--r--lib/irb/debug/ui.rb104
-rw-r--r--lib/irb/history.rb4
-rw-r--r--lib/irb/ruby-lex.rb6
-rw-r--r--lib/irb/workspace.rb4
-rw-r--r--test/irb/test_debug_cmd.rb217
-rw-r--r--test/irb/test_history.rb45
11 files changed, 642 insertions, 114 deletions
diff --git a/lib/irb.rb b/lib/irb.rb
index c3631715da..c884d70a67 100644
--- a/lib/irb.rb
+++ b/lib/irb.rb
@@ -18,6 +18,7 @@ require_relative "irb/color"
require_relative "irb/version"
require_relative "irb/easter-egg"
+require_relative "irb/debug"
# IRB stands for "interactive Ruby" and is a tool to interactively execute Ruby
# expressions read from the standard input.
@@ -373,8 +374,6 @@ module IRB
class Abort < Exception;end
@CONF = {}
-
-
# Displays current configuration.
#
# Modifying the configuration is achieved by sending a message to IRB.conf.
@@ -441,7 +440,7 @@ module IRB
# Creates a new irb session
def initialize(workspace = nil, input_method = nil)
@context = Context.new(self, workspace, input_method)
- @context.main.extend ExtendCommandBundle
+ @context.workspace.load_commands_to_main
@signal_status = :IN_IRB
@scanner = RubyLex.new(@context)
end
@@ -457,6 +456,38 @@ module IRB
end
end
+ def debug_readline(binding)
+ workspace = IRB::WorkSpace.new(binding)
+ context.workspace = workspace
+ context.workspace.load_commands_to_main
+ scanner.increase_line_no(1)
+
+ # When users run:
+ # 1. Debugging commands, like `step 2`
+ # 2. Any input that's not irb-command, like `foo = 123`
+ #
+ # Irb#eval_input will simply return the input, and we need to pass it to the debugger.
+ input = if IRB.conf[:SAVE_HISTORY] && context.io.support_history_saving?
+ # Previous IRB session's history has been saved when `Irb#run` is exited
+ # We need to make sure the saved history is not saved again by reseting the counter
+ context.io.reset_history_counter
+
+ begin
+ eval_input
+ ensure
+ context.io.save_history
+ end
+ else
+ eval_input
+ end
+
+ if input&.include?("\n")
+ scanner.increase_line_no(input.count("\n") - 1)
+ end
+
+ input
+ end
+
def run(conf = IRB.conf)
in_nested_session = !!conf[:MAIN_CONTEXT]
conf[:IRB_RC].call(context) if conf[:IRB_RC]
@@ -542,6 +573,18 @@ module IRB
@scanner.each_top_level_statement do |line, line_no, is_assignment|
signal_status(:IN_EVAL) do
begin
+ # If the integration with debugger is activated, we need to handle certain input differently
+ if @context.with_debugger
+ command_class = load_command_class(line)
+ # First, let's pass debugging command's input to debugger
+ # Secondly, we need to let debugger evaluate non-command input
+ # Otherwise, the expression will be evaluated in the debugger's main session thread
+ # This is the only way to run the user's program in the expected thread
+ if !command_class || ExtendCommand::DebugCommand > command_class
+ return line
+ end
+ end
+
evaluate_line(line, line_no)
# Don't echo if the line ends with a semicolon
@@ -633,6 +676,12 @@ module IRB
@context.evaluate(line, line_no)
end
+ def load_command_class(line)
+ command, _ = line.split(/\s/, 2)
+ command_name = @context.command_aliases[command.to_sym]
+ ExtendCommandBundle.load_command(command_name || command)
+ end
+
def convert_invalid_byte_sequence(str, enc)
str.force_encoding(enc)
str.scrub { |c|
@@ -986,12 +1035,32 @@ class Binding
#
# See IRB@Usage for more information.
def irb(show_code: true)
+ # Setup IRB with the current file's path and no command line arguments
IRB.setup(source_location[0], argv: [])
+ # Create a new workspace using the current binding
workspace = IRB::WorkSpace.new(self)
+ # Print the code around the binding if show_code is true
STDOUT.print(workspace.code_around_binding) if show_code
- binding_irb = IRB::Irb.new(workspace)
- binding_irb.context.irb_path = File.expand_path(source_location[0])
- binding_irb.run(IRB.conf)
- binding_irb.debug_break
+ # Get the original IRB instance
+ debugger_irb = IRB.instance_variable_get(:@debugger_irb)
+
+ irb_path = File.expand_path(source_location[0])
+
+ if debugger_irb
+ # If we're already in a debugger session, set the workspace and irb_path for the original IRB instance
+ debugger_irb.context.workspace = workspace
+ debugger_irb.context.irb_path = irb_path
+ # If we've started a debugger session and hit another binding.irb, we don't want to start an IRB session
+ # instead, we want to resume the irb:rdbg session.
+ IRB::Debug.setup(debugger_irb)
+ IRB::Debug.insert_debug_break
+ debugger_irb.debug_break
+ else
+ # If we're not in a debugger session, create a new IRB instance with the current workspace
+ binding_irb = IRB::Irb.new(workspace)
+ binding_irb.context.irb_path = irb_path
+ binding_irb.run(IRB.conf)
+ binding_irb.debug_break
+ end
end
end
diff --git a/lib/irb/cmd/debug.rb b/lib/irb/cmd/debug.rb
index 7d39b9fa27..9eca964218 100644
--- a/lib/irb/cmd/debug.rb
+++ b/lib/irb/cmd/debug.rb
@@ -1,4 +1,5 @@
require_relative "nop"
+require_relative "../debug"
module IRB
# :stopdoc:
@@ -12,37 +13,46 @@ module IRB
'<internal:prelude>',
binding.method(:irb).source_location.first,
].map { |file| /\A#{Regexp.escape(file)}:\d+:in `irb'\z/ }
- IRB_DIR = File.expand_path('..', __dir__)
def execute(pre_cmds: nil, do_cmds: nil)
- unless binding_irb?
- puts "`debug` command is only available when IRB is started with binding.irb"
- return
- end
+ if irb_context.with_debugger
+ # If IRB is already running with a debug session, throw the command and IRB.debug_readline will pass it to the debugger.
+ if cmd = pre_cmds || do_cmds
+ throw :IRB_EXIT, cmd
+ else
+ puts "IRB is already running with a debug session."
+ return
+ end
+ else
+ # If IRB is not running with a debug session yet, then:
+ # 1. Check if the debugging command is run from a `binding.irb` call.
+ # 2. If so, try setting up the debug gem.
+ # 3. Insert a debug breakpoint at `Irb#debug_break` with the intended command.
+ # 4. Exit the current Irb#run call via `throw :IRB_EXIT`.
+ # 5. `Irb#debug_break` will be called and trigger the breakpoint, which will run the intended command.
+ unless binding_irb?
+ puts "`debug` command is only available when IRB is started with binding.irb"
+ return
+ end
- unless setup_debugger
- puts <<~MSG
- You need to install the debug gem before using this command.
- If you use `bundle exec`, please add `gem "debug"` into your Gemfile.
- MSG
- return
- end
+ if IRB.respond_to?(:JobManager)
+ warn "Can't start the debugger when IRB is running in a multi-IRB session."
+ return
+ end
- options = { oneshot: true, hook_call: false }
- if pre_cmds || do_cmds
- options[:command] = ['irb', pre_cmds, do_cmds]
- end
- if DEBUGGER__::LineBreakpoint.instance_method(:initialize).parameters.include?([:key, :skip_src])
- options[:skip_src] = true
- end
+ unless IRB::Debug.setup(irb_context.irb)
+ puts <<~MSG
+ You need to install the debug gem before using this command.
+ If you use `bundle exec`, please add `gem "debug"` into your Gemfile.
+ MSG
+ return
+ end
- # To make debugger commands like `next` or `continue` work without asking
- # the user to quit IRB after that, we need to exit IRB first and then hit
- # a TracePoint on #debug_break.
- file, lineno = IRB::Irb.instance_method(:debug_break).source_location
- DEBUGGER__::SESSION.add_line_breakpoint(file, lineno + 1, **options)
- # exit current Irb#run call
- throw :IRB_EXIT
+ IRB::Debug.insert_debug_break(pre_cmds: pre_cmds, do_cmds: do_cmds)
+
+ # exit current Irb#run call
+ throw :IRB_EXIT
+ end
end
private
@@ -54,72 +64,6 @@ module IRB
end
end
end
-
- module SkipPathHelperForIRB
- def skip_internal_path?(path)
- # The latter can be removed once https://github.com/ruby/debug/issues/866 is resolved
- super || path.match?(IRB_DIR) || path.match?('<internal:prelude>')
- end
- end
-
- def setup_debugger
- unless defined?(DEBUGGER__::SESSION)
- begin
- require "debug/session"
- rescue LoadError # debug.gem is not written in Gemfile
- return false unless load_bundled_debug_gem
- end
- DEBUGGER__.start(nonstop: true)
- end
-
- unless DEBUGGER__.respond_to?(:capture_frames_without_irb)
- DEBUGGER__.singleton_class.send(:alias_method, :capture_frames_without_irb, :capture_frames)
-
- def DEBUGGER__.capture_frames(*args)
- frames = capture_frames_without_irb(*args)
- frames.reject! do |frame|
- frame.realpath&.start_with?(IRB_DIR) || frame.path == "<internal:prelude>"
- end
- frames
- end
-
- DEBUGGER__::ThreadClient.prepend(SkipPathHelperForIRB)
- end
-
- true
- end
-
- # This is used when debug.gem is not written in Gemfile. Even if it's not
- # installed by `bundle install`, debug.gem is installed by default because
- # it's a bundled gem. This method tries to activate and load that.
- def load_bundled_debug_gem
- # Discover latest debug.gem under GEM_PATH
- debug_gem = Gem.paths.path.flat_map { |path| Dir.glob("#{path}/gems/debug-*") }.select do |path|
- File.basename(path).match?(/\Adebug-\d+\.\d+\.\d+(\w+)?\z/)
- end.sort_by do |path|
- Gem::Version.new(File.basename(path).delete_prefix('debug-'))
- end.last
- return false unless debug_gem
-
- # Discover debug/debug.so under extensions for Ruby 3.2+
- ext_name = "/debug/debug.#{RbConfig::CONFIG['DLEXT']}"
- ext_path = Gem.paths.path.flat_map do |path|
- Dir.glob("#{path}/extensions/**/#{File.basename(debug_gem)}#{ext_name}")
- end.first
-
- # Attempt to forcibly load the bundled gem
- if ext_path
- $LOAD_PATH << ext_path.delete_suffix(ext_name)
- end
- $LOAD_PATH << "#{debug_gem}/lib"
- begin
- require "debug/session"
- puts "Loaded #{File.basename(debug_gem)}"
- true
- rescue LoadError
- false
- end
- end
end
class DebugCommand < Debug
diff --git a/lib/irb/cmd/subirb.rb b/lib/irb/cmd/subirb.rb
index 3018ab277b..5ffd646416 100644
--- a/lib/irb/cmd/subirb.rb
+++ b/lib/irb/cmd/subirb.rb
@@ -11,8 +11,7 @@ module IRB
module ExtendCommand
class MultiIRBCommand < Nop
- def initialize(conf)
- super
+ def execute(*args)
extend_irb_context
end
@@ -29,6 +28,10 @@ module IRB
# this extension patches IRB context like IRB.CurrentContext
require_relative "../ext/multi-irb"
end
+
+ def print_debugger_warning
+ warn "Multi-IRB commands are not available when the debugger is enabled."
+ end
end
class IrbCommand < MultiIRBCommand
@@ -37,6 +40,13 @@ module IRB
def execute(*obj)
print_deprecated_warning
+
+ if irb_context.with_debugger
+ print_debugger_warning
+ return
+ end
+
+ super
IRB.irb(nil, *obj)
end
end
@@ -47,6 +57,13 @@ module IRB
def execute
print_deprecated_warning
+
+ if irb_context.with_debugger
+ print_debugger_warning
+ return
+ end
+
+ super
IRB.JobManager
end
end
@@ -57,6 +74,14 @@ module IRB
def execute(key = nil)
print_deprecated_warning
+
+ if irb_context.with_debugger
+ print_debugger_warning
+ return
+ end
+
+ super
+
raise CommandArgumentError.new("Please specify the id of target IRB job (listed in the `jobs` command).") unless key
IRB.JobManager.switch(key)
end
@@ -68,6 +93,13 @@ module IRB
def execute(*keys)
print_deprecated_warning
+
+ if irb_context.with_debugger
+ print_debugger_warning
+ return
+ end
+
+ super
IRB.JobManager.kill(*keys)
end
end
diff --git a/lib/irb/context.rb b/lib/irb/context.rb
index 18125ff6fb..43d9b53435 100644
--- a/lib/irb/context.rb
+++ b/lib/irb/context.rb
@@ -345,6 +345,8 @@ module IRB
# User-defined IRB command aliases
attr_accessor :command_aliases
+ attr_accessor :with_debugger
+
# Alias for #use_multiline
alias use_multiline? use_multiline
# Alias for #use_singleline
diff --git a/lib/irb/debug.rb b/lib/irb/debug.rb
new file mode 100644
index 0000000000..dab9d1846a
--- /dev/null
+++ b/lib/irb/debug.rb
@@ -0,0 +1,127 @@
+# frozen_string_literal: true
+
+module IRB
+ module Debug
+ BINDING_IRB_FRAME_REGEXPS = [
+ '<internal:prelude>',
+ binding.method(:irb).source_location.first,
+ ].map { |file| /\A#{Regexp.escape(file)}:\d+:in `irb'\z/ }
+ IRB_DIR = File.expand_path('..', __dir__)
+
+ class << self
+ def insert_debug_break(pre_cmds: nil, do_cmds: nil)
+ options = { oneshot: true, hook_call: false }
+
+ if pre_cmds || do_cmds
+ options[:command] = ['irb', pre_cmds, do_cmds]
+ end
+ if DEBUGGER__::LineBreakpoint.instance_method(:initialize).parameters.include?([:key, :skip_src])
+ options[:skip_src] = true
+ end
+
+ # To make debugger commands like `next` or `continue` work without asking
+ # the user to quit IRB after that, we need to exit IRB first and then hit
+ # a TracePoint on #debug_break.
+ file, lineno = IRB::Irb.instance_method(:debug_break).source_location
+ DEBUGGER__::SESSION.add_line_breakpoint(file, lineno + 1, **options)
+ end
+
+ def setup(irb)
+ # When debug session is not started at all
+ unless defined?(DEBUGGER__::SESSION)
+ begin
+ require "debug/session"
+ rescue LoadError # debug.gem is not written in Gemfile
+ return false unless load_bundled_debug_gem
+ end
+ DEBUGGER__::CONFIG.set_config
+ configure_irb_for_debugger(irb)
+ thread = Thread.current
+
+ DEBUGGER__.initialize_session{ IRB::Debug::UI.new(thread, irb) }
+ end
+
+ # When debug session was previously started but not by IRB
+ if defined?(DEBUGGER__::SESSION) && !irb.context.with_debugger
+ configure_irb_for_debugger(irb)
+ thread = Thread.current
+
+ DEBUGGER__::SESSION.reset_ui(IRB::Debug::UI.new(thread, irb))
+ end
+
+ # Apply patches to debug gem so it skips IRB frames
+ unless DEBUGGER__.respond_to?(:capture_frames_without_irb)
+ DEBUGGER__.singleton_class.send(:alias_method, :capture_frames_without_irb, :capture_frames)
+
+ def DEBUGGER__.capture_frames(*args)
+ frames = capture_frames_without_irb(*args)
+ frames.reject! do |frame|
+ frame.realpath&.start_with?(IRB_DIR) || frame.path == "<internal:prelude>"
+ end
+ frames
+ end
+
+ DEBUGGER__::ThreadClient.prepend(SkipPathHelperForIRB)
+ end
+
+ true
+ end
+
+ private
+
+ def configure_irb_for_debugger(irb)
+ require 'irb/debug/ui'
+ IRB.instance_variable_set(:@debugger_irb, irb)
+ irb.context.with_debugger = true
+ irb.context.irb_name = "irb:rdbg"
+ end
+
+ def binding_irb?
+ caller.any? do |frame|
+ BINDING_IRB_FRAME_REGEXPS.any? do |regexp|
+ frame.match?(regexp)
+ end
+ end
+ end
+
+ module SkipPathHelperForIRB
+ def skip_internal_path?(path)
+ # The latter can be removed once https://github.com/ruby/debug/issues/866 is resolved
+ super || path.match?(IRB_DIR) || path.match?('<internal:prelude>')
+ end
+ end
+
+ # This is used when debug.gem is not written in Gemfile. Even if it's not
+ # installed by `bundle install`, debug.gem is installed by default because
+ # it's a bundled gem. This method tries to activate and load that.
+ def load_bundled_debug_gem
+ # Discover latest debug.gem under GEM_PATH
+ debug_gem = Gem.paths.path.flat_map { |path| Dir.glob("#{path}/gems/debug-*") }.select do |path|
+ File.basename(path).match?(/\Adebug-\d+\.\d+\.\d+(\w+)?\z/)
+ end.sort_by do |path|
+ Gem::Version.new(File.basename(path).delete_prefix('debug-'))
+ end.last
+ return false unless debug_gem
+
+ # Discover debug/debug.so under extensions for Ruby 3.2+
+ ext_name = "/debug/debug.#{RbConfig::CONFIG['DLEXT']}"
+ ext_path = Gem.paths.path.flat_map do |path|
+ Dir.glob("#{path}/extensions/**/#{File.basename(debug_gem)}#{ext_name}")
+ end.first
+
+ # Attempt to forcibly load the bundled gem
+ if ext_path
+ $LOAD_PATH << ext_path.delete_suffix(ext_name)
+ end
+ $LOAD_PATH << "#{debug_gem}/lib"
+ begin
+ require "debug/session"
+ puts "Loaded #{File.basename(debug_gem)}"
+ true
+ rescue LoadError
+ false
+ end
+ end
+ end
+ end
+end
diff --git a/lib/irb/debug/ui.rb b/lib/irb/debug/ui.rb
new file mode 100644
index 0000000000..a4ca4fdf0f
--- /dev/null
+++ b/lib/irb/debug/ui.rb
@@ -0,0 +1,104 @@
+require 'io/console/size'
+require 'debug/console'
+
+module IRB
+ module Debug
+ class UI < DEBUGGER__::UI_Base
+ def initialize(thread, irb)
+ @thread = thread
+ @irb = irb
+ end
+
+ def remote?
+ false
+ end
+
+ def activate session, on_fork: false
+ end
+
+ def deactivate
+ end
+
+ def width
+ if (w = IO.console_size[1]) == 0 # for tests PTY
+ 80
+ else
+ w
+ end
+ end
+
+ def quit n
+ yield
+ exit n
+ end
+
+ def ask prompt
+ setup_interrupt do
+ print prompt
+ ($stdin.gets || '').strip
+ end
+ end
+
+ def puts str = nil
+ case str
+ when Array
+ str.each{|line|
+ $stdout.puts line.chomp
+ }
+ when String
+ str.each_line{|line|
+ $stdout.puts line.chomp
+ }
+ when nil
+ $stdout.puts
+ end
+ end
+
+ def readline _
+ setup_interrupt do
+ tc = DEBUGGER__::SESSION.get_thread_client(@thread)
+ cmd = @irb.debug_readline(tc.current_frame.binding || TOPLEVEL_BINDING)
+
+ case cmd
+ when nil # when user types C-d
+ "continue"
+ else
+ cmd
+ end
+ end
+ end
+
+ def setup_interrupt
+ DEBUGGER__::SESSION.intercept_trap_sigint false do
+ current_thread = Thread.current # should be session_server thread
+
+ prev_handler = trap(:INT){
+ current_thread.raise Interrupt
+ }
+
+ yield
+ ensure
+ trap(:INT, prev_handler)
+ end
+ end
+
+ def after_fork_parent
+ parent_pid = Process.pid
+
+ at_exit{
+ DEBUGGER__::SESSION.intercept_trap_sigint_end
+ trap(:SIGINT, :IGNORE)
+
+ if Process.pid == parent_pid
+ # only check child process from its parent
+ begin
+ # wait for all child processes to keep terminal
+ Process.waitpid
+ rescue Errno::ESRCH, Errno::ECHILD
+ end
+ end
+ }
+ end
+ end
+ end
+end
diff --git a/lib/irb/history.rb b/lib/irb/history.rb
index 516890ac05..ae924d152b 100644
--- a/lib/irb/history.rb
+++ b/lib/irb/history.rb
@@ -4,6 +4,10 @@ module IRB
true
end
+ def reset_history_counter
+ @loaded_history_lines = self.class::HISTORY.size if defined? @loaded_history_lines
+ end
+
def load_history
history = self.class::HISTORY
if history_file = IRB.conf[:HISTORY_FILE]
diff --git a/lib/irb/ruby-lex.rb b/lib/irb/ruby-lex.rb
index 7d4f8a514c..282e6ef05f 100644
--- a/lib/irb/ruby-lex.rb
+++ b/lib/irb/ruby-lex.rb
@@ -183,6 +183,10 @@ class RubyLex
prompt(opens, continue, line_num_offset)
end
+ def increase_line_no(addition)
+ @line_no += addition
+ end
+
def readmultiline
save_prompt_to_context_io([], false, 0)
@@ -220,7 +224,7 @@ class RubyLex
code.force_encoding(@context.io.encoding)
yield code, @line_no, assignment_expression?(code)
end
- @line_no += code.count("\n")
+ increase_line_no(code.count("\n"))
rescue TerminateLineInput
end
end
diff --git a/lib/irb/workspace.rb b/lib/irb/workspace.rb
index d6fa67053d..2bf3d5e0f1 100644
--- a/lib/irb/workspace.rb
+++ b/lib/irb/workspace.rb
@@ -108,6 +108,10 @@ EOF
# <code>IRB.conf[:__MAIN__]</code>
attr_reader :main
+ def load_commands_to_main
+ main.extend ExtendCommandBundle
+ end
+
# Evaluate the given +statements+ within the context of this workspace.
def evaluate(statements, file = __FILE__, line = __LINE__)
eval(statements, @binding, file, line)
diff --git a/test/irb/test_debug_cmd.rb b/test/irb/test_debug_cmd.rb
index 35239364ba..c4e4a04fdd 100644
--- a/test/irb/test_debug_cmd.rb
+++ b/test/irb/test_debug_cmd.rb
@@ -27,10 +27,10 @@ module TestIRB
output = run_ruby_file do
type "backtrace"
- type "q!"
+ type "exit!"
end
- assert_match(/\(rdbg:irb\) backtrace/, output)
+ assert_match(/irb\(main\):001> backtrace/, output)
assert_match(/Object#foo at #{@ruby_file.to_path}/, output)
end
@@ -46,10 +46,27 @@ module TestIRB
type "continue"
end
- assert_match(/\(rdbg\) next/, output)
+ assert_match(/irb\(main\):001> debug/, output)
+ assert_match(/irb:rdbg\(main\):002> next/, output)
assert_match(/=> 2\| puts "hello"/, output)
end
+ def test_debug_command_only_runs_once
+ write_ruby <<~'ruby'
+ binding.irb
+ ruby
+
+ output = run_ruby_file do
+ type "debug"
+ type "debug"
+ type "continue"
+ end
+
+ assert_match(/irb\(main\):001> debug/, output)
+ assert_match(/irb:rdbg\(main\):002> debug/, output)
+ assert_match(/IRB is already running with a debug session/, output)
+ end
+
def test_next
write_ruby <<~'ruby'
binding.irb
@@ -61,7 +78,7 @@ module TestIRB
type "continue"
end
- assert_match(/\(rdbg:irb\) next/, output)
+ assert_match(/irb\(main\):001> next/, output)
assert_match(/=> 2\| puts "hello"/, output)
end
@@ -77,7 +94,7 @@ module TestIRB
type "continue"
end
- assert_match(/\(rdbg:irb\) break/, output)
+ assert_match(/irb\(main\):001> break/, output)
assert_match(/=> 2\| puts "Hello"/, output)
end
@@ -96,7 +113,7 @@ module TestIRB
type "continue"
end
- assert_match(/\(rdbg:irb\) delete/, output)
+ assert_match(/irb:rdbg\(main\):003> delete/, output)
assert_match(/deleted: #0 BP - Line/, output)
end
@@ -115,11 +132,44 @@ module TestIRB
type "continue"
end
- assert_match(/\(rdbg:irb\) step/, output)
+ assert_match(/irb\(main\):001> step/, output)
assert_match(/=> 5\| foo/, output)
assert_match(/=> 2\| puts "Hello"/, output)
end
+ def test_long_stepping
+ write_ruby <<~'RUBY'
+ class Foo
+ def foo(num)
+ bar(num + 10)
+ end
+
+ def bar(num)
+ num
+ end
+ end
+
+ binding.irb
+ Foo.new.foo(100)
+ RUBY
+
+ output = run_ruby_file do
+ type "step"
+ type "step"
+ type "step"
+ type "step"
+ type "num"
+ type "continue"
+ end
+
+ assert_match(/irb\(main\):001> step/, output)
+ assert_match(/irb:rdbg\(main\):002> step/, output)
+ assert_match(/irb:rdbg\(#<Foo:.*>\):003> step/, output)
+ assert_match(/irb:rdbg\(#<Foo:.*>\):004> step/, output)
+ assert_match(/irb:rdbg\(#<Foo:.*>\):005> num/, output)
+ assert_match(/=> 110/, output)
+ end
+
def test_continue
write_ruby <<~'RUBY'
binding.irb
@@ -133,8 +183,9 @@ module TestIRB
type "continue"
end
- assert_match(/\(rdbg:irb\) continue/, output)
+ assert_match(/irb\(main\):001> continue/, output)
assert_match(/=> 3: binding.irb/, output)
+ assert_match(/irb:rdbg\(main\):002> continue/, output)
end
def test_finish
@@ -151,7 +202,7 @@ module TestIRB
type "continue"
end
- assert_match(/\(rdbg:irb\) finish/, output)
+ assert_match(/irb\(main\):001> finish/, output)
assert_match(/=> 4\| end/, output)
end
@@ -169,7 +220,7 @@ module TestIRB
type "continue"
end
- assert_match(/\(rdbg:irb\) info/, output)
+ assert_match(/irb\(main\):001> info/, output)
assert_match(/%self = main/, output)
assert_match(/a = "Hello"/, output)
end
@@ -186,8 +237,152 @@ module TestIRB
type "continue"
end
- assert_match(/\(rdbg:irb\) catch/, output)
+ assert_match(/irb\(main\):001> catch/, output)
assert_match(/Stop by #0 BP - Catch "ZeroDivisionError"/, output)
end
+
+ def test_exit
+ write_ruby <<~'RUBY'
+ binding.irb
+ puts "hello"
+ RUBY
+
+ output = run_ruby_file do
+ type "next"
+ type "exit"
+ end
+
+ assert_match(/irb\(main\):001> next/, output)
+ end
+
+ def test_quit
+ write_ruby <<~'RUBY'
+ binding.irb
+ RUBY
+
+ output = run_ruby_file do
+ type "next"
+ type "quit!"
+ end
+
+ assert_match(/irb\(main\):001> next/, output)
+ end
+
+ def test_prompt_line_number_continues
+ write_ruby <<~'ruby'
+ binding.irb
+ puts "Hello"
+ puts "World"
+ ruby
+
+ output = run_ruby_file do
+ type "123"
+ type "456"
+ type "next"
+ type "info"
+ type "next"
+ type "continue"
+ end
+
+ assert_match(/irb\(main\):003> next/, output)
+ assert_match(/irb:rdbg\(main\):004> info/, output)
+ assert_match(/irb:rdbg\(main\):005> next/, output)
+ end
+
+ def test_irb_commands_are_available_after_moving_around_with_the_debugger
+ write_ruby <<~'ruby'
+ class Foo
+ def bar
+ puts "bar"
+ end
+ end
+
+ binding.irb
+ Foo.new.bar
+ ruby
+
+ output = run_ruby_file do
+ # Due to the way IRB defines its commands, moving into the Foo instance from main is necessary for proper testing.
+ type "next"
+ type "step"
+ type "irb_info"
+ type "continue"
+ end
+
+ assert_include(output, "InputMethod: RelineInputMethod")
+ end
+
+ def test_input_is_evaluated_in_the_context_of_the_current_thread
+ write_ruby <<~'ruby'
+ current_thread = Thread.current
+ binding.irb
+ ruby
+
+ output = run_ruby_file do
+ type "debug"
+ type '"Threads match: #{current_thread == Thread.current}"'
+ type "continue"
+ end
+
+ assert_match(/irb\(main\):001> debug/, output)
+ assert_match(/Threads match: true/, output)
+ end
+
+ def test_irb_switches_debugger_interface_if_debug_was_already_activated
+ write_ruby <<~'ruby'
+ require 'debug'
+ class Foo
+ def bar
+ puts "bar"
+ end
+ end
+
+ binding.irb
+ Foo.new.bar
+ ruby
+
+ output = run_ruby_file do
+ # Due to the way IRB defines its commands, moving into the Foo instance from main is necessary for proper testing.
+ type "next"
+ type "step"
+ type 'irb_info'
+ type "continue"
+ end
+
+ assert_match(/irb\(main\):001> next/, output)
+ assert_include(output, "InputMethod: RelineInputMethod")
+ end
+
+ def test_debugger_cant_be_activated_while_multi_irb_is_active
+ write_ruby <<~'ruby'
+ binding.irb
+ a = 1
+ ruby
+
+ output = run_ruby_file do
+ type "jobs"
+ type "next"
+ type "exit"
+ end
+
+ assert_match(/irb\(main\):001> jobs/, output)
+ assert_include(output, "Can't start the debugger when IRB is running in a multi-IRB session.")
+ end
+
+ def test_multi_irb_commands_are_not_available_after_activating_the_debugger
+ write_ruby <<~'ruby'
+ binding.irb
+ a = 1
+ ruby
+
+ output = run_ruby_file do
+ type "next"
+ type "jobs"
+ type "continue"
+ end
+
+ assert_match(/irb\(main\):001> next/, output)
+ assert_include(output, "Multi-IRB commands are not available when the debugger is enabled.")
+ end
end
end
diff --git a/test/irb/test_history.rb b/test/irb/test_history.rb
index 39f9e82750..9bf146609c 100644
--- a/test/irb/test_history.rb
+++ b/test/irb/test_history.rb
@@ -209,7 +209,50 @@ module TestIRB
end
end
- class NestedIRBHistoryTest < IntegrationTestCase
+ class IRBHistoryIntegrationTest < IntegrationTestCase
+ def test_history_saving_with_debug
+ if ruby_core?
+ omit "This test works only under ruby/irb"
+ end
+
+ write_history ""
+
+ write_ruby <<~'RUBY'
+ def foo
+ end
+
+ binding.irb
+
+ foo
+ RUBY
+
+ output = run_ruby_file do
+ type "'irb session'"
+ type "next"
+ type "'irb:debug session'"
+ type "step"
+ type "irb_info"
+ type "puts Reline::HISTORY.to_a.to_s"
+ type "q!"
+ end
+
+ assert_include(output, "InputMethod: RelineInputMethod")
+ # check that in-memory history is preserved across sessions
+ assert_include output, %q(
+ ["'irb session'", "next", "'irb:debug session'", "step", "irb_info", "puts Reline::HISTORY.to_a.to_s"]
+ ).strip
+
+ assert_equal <<~HISTORY, @history_file.open.read
+ 'irb session'
+ next
+ 'irb:debug session'
+ step
+ irb_info
+ puts Reline::HISTORY.to_a.to_s
+ q!
+ HISTORY
+ end
+
def test_history_saving_with_nested_sessions
write_history ""