[ruby/irb] Support seamless integration with ruby/debug

(https://github.com/ruby/irb/pull/575)

* Support native integration with ruby/debug

* Prevent using multi-irb and activating debugger at the same time

Multi-irb makes a few assumptions:

- IRB will manage all threads that host sub-irb sessions
- All IRB sessions will be run on the threads created by IRB itself

However, when using the debugger these assumptions are broken:

- `debug` will freeze ALL threads when it suspends the session (e.g. when
  hitting a breakpoint, or performing step-debugging).
- Since the irb-debug integration runs IRB as the debugger's interface,
  it will be run on the debugger's thread, which is not managed by IRB.

So we should prevent the 2 features from being used at the same time.
To do that, we check if the other feature is already activated when
executing the commands that would activate the other feature.

https://github.com/ruby/irb/commit/d8fb3246be
This commit is contained in:
Stan Lo 2023-08-13 19:30:30 +01:00 committed by git
parent 9099d62ac7
commit 7f8f62c93b
11 changed files with 642 additions and 114 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

127
lib/irb/debug.rb Normal file
View File

@ -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

104
lib/irb/debug/ui.rb Normal file
View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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 ""