[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:
parent
9099d62ac7
commit
7f8f62c93b
83
lib/irb.rb
83
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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
127
lib/irb/debug.rb
Normal 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
104
lib/irb/debug/ui.rb
Normal 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
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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 ""
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user