Alexey said:
To me, the biggest problem with using irb in this mode is the "set up
some context" part.
Believe it or not, but it is quite easy to do with Eclipse/Java: one can
set a breakpoint, execute some test, wait till debugger stops on the
breakpoint, and then use Eclipse "Display" view to evaluate arbitrary
Java expressions in the current scope.
So far, I don't know any easy way to do the same with Ruby, so I end up
inserting things like "puts <expression>.inspect" and rerunning the
test. Which is not as nice as an interactive shell. Does anyone have a
better way?
I've done a quick implementation of breakpoints that will spawn an irb
session at the point of the break point in your application yesterday.
I've attached it.
Regards,
Florian Gross
require 'irb'
require 'binding_of_caller'
module IRB
def IRB.start(ap_path = nil, main_context = nil)
$0 = File::basename(ap_path, ".rb") if ap_path
# suppress some warnings about redefined constants
old_verbose, $VERBOSE = $VERBOSE, nil
IRB.setup(ap_path)
$VERBOSE = old_verbose
if @CONF[:SCRIPT]
irb = Irb.new(main_context, @CONF[:SCRIPT])
else
irb = Irb.new(main_context)
end
@CONF[:IRB_RC].call(irb.context) if @CONF[:IRB_RC]
@CONF[:MAIN_CONTEXT] = irb.context
trap("SIGINT") do
irb.signal_handle
end
catch
IRB_EXIT) do
irb.eval_input
end
end
end
# This will pop up an interactive ruby session at a
# pre-defined break point in a Ruby application. In
# this session you can examine the environment of
# the break point.
#
# You can get a list of variables in the context using
# local_variables via +local_variables+. You can then
# examine their values by typing their names.
#
# You can have a look at the call stack via +caller+.
#
# breakpoints can also return a value. They will execute
# a supplied block for getting a default return value.
# A custom value can be returned from the session by doing
# +throw
debug_return, value)+.
#
# You can also give names to break points which will be
# used in the message that is displayed upon execution
# of them.
#
# Here's a sample of how breakpoints should be placed:
#
# class Person
# def initialize(name, age)
# @name, @age = name, age
# breakpoint("Person#initialize")
# end
#
# attr_reader :age
# def name
# breakpoint("Person#name") { @name }
# end
# end
#
# person = Person.new("Random Person", 23)
# puts "Name: #{person.name}"
#
# And here is a sample debug session:
#
# Executing break point "Person#initialize" near file.rb:14
# irb(#<Person:0x292fbe8>):001:0> local_variables
# => ["name", "age", "_", "__"]
# irb(#<Person:0x292fbe8>):002:0> [name, age]
# => ["Random Person", 23]
# irb(#<Person:0x292fbe8>):003:0> [@name, @age]
# => ["Random Person", 23]
# irb(#<Person:0x292fbe8>):004:0> self
# => #<Person:0x292fbe8 @age=23, @name="Random Person">
# irb(#<Person:0x292fbe8>):005:0> @age += 1; self
# => #<Person:0x292fbe8 @age=24, @name="Random Person">
# irb(#<Person:0x292fbe8>):006:0> @age += 1; self
# Executing break point "Person#name" near file.rb:14
# irb(#<Person:0x292fbe8>):001:0> throw
debug_return, "Overriden name")
# Name: Overriden name
def breakpoint(id = nil, &block)
Binding.of_caller do |context, extra_data|
file, line = extra_data[1], extra_data[2]
puts "Executing break point #{"#{id.inspect} " if id}near #{file}:#{line}"
catch
debug_return) do |value|
IRB.start(nil, IRB::WorkSpace.new(context))
block.call if block
end
end
end
alias :break_point :breakpoint
begin
require 'simplecc'
rescue LoadError
def Continuation.create(*args, &block)
cc = nil; result = callcc {|c| cc = c; block.call(cc) if block and args.empty?}
result ||= args
return *[cc, *result]
end
end
# This method returns the binding of the method that called your
# method. It will raise an Exception when you're not inside a method.
#
# It's used like this:
# def inc_counter
# Binding.of_caller do |binding|
# eval("counter += 1", binding)
# end
# end
# counter = 0
# 2.times { inc_counter }
# counter # => 2
#
# You will have to put the whole rest of your method into the
# block that you pass into this method. If you don't do this
# an Exception will be raised. Because of the way that this is
# implemented it has to be done this way. If you don't do it
# like this it will raise an Exception.
def Binding.of_caller(&block)
old_critical = Thread.critical
Thread.critical = true
count = 0
cc, result, error, extra_data = Continuation.create(nil, nil)
error.call if error
tracer = lambda do |*args|
type, context, extra_data = args[0], args[4], args
if type == "return"
count += 1
# First this method and then calling one will return --
# the trace event of the second event gets the context
# of the method which called the method that called this
# method.
if count == 2
# It would be nice if we could restore the trace_func
# that was set before we swapped in our own one, but
# this is impossible without overloading set_trace_func
# in current Ruby.
set_trace_func(nil)
cc.call(eval("binding", context), nil, extra_data)
end
elsif type != "line"
set_trace_func(nil)
error_msg = "Binding.of_caller used in non-method context or " +
"trailing statements of method using it aren't in the block."
cc.call(nil, lambda { raise(ArgumentError, error_msg) }, nil)
end
end
unless result
set_trace_func(tracer)
return nil
else
Thread.critical = old_critical
case block.arity
when 1 then yield(result)
else yield(result, extra_data)
end
end
end