Module: DevUtils::Debug

Defined in:
lib/dev-utils/debug.rb,
lib/dev-utils/debug/irb.rb,
lib/dev-utils/debug/log.rb

Overview

DevUtils::Debug contains methods to aid debugging Ruby programs, although when using these methods, you don’t care about the module; it is included into the top-level when you require 'dev-utils/debug'.

The methods are:

  • #breakpoint, for escaping to IRB from a running program, with local environment intact;

  • #debug, for logging debugging messages to a zero-conf logfile; and

  • #trace, for tracing expressions to that same file.

Planned features include a method for determining the difference between two complex objects.

Constant Summary collapse

DEBUGLOG =

The target of logging messages from debug and trace.

Logger.new(File.new('debug.log', 'w'))
TRACE_STYLES =

:nodoc:

{}

Instance Method Summary collapse

Instance Method Details

#breakpoint(name = nil, context = nil, &block) ⇒ Object Also known as: break_point, goirb

Method

breakpoint

Aliases

break_point, goirb

Description

This will pop up an interactive ruby session from whereever it is called in a Ruby application. In IRB you can examine the environment of the break point, peeking and poking local variables, calling methods, viewing the stack (with caller), etc.

This is like setting breakpoints in a debugger, except that you are running the program normally (debuggers tend to run programs more slowly). Debuggers are generally more flexible, but this is a good lightweight solution for many cases. You can not step through the code with this technique. But you can, of course, set multiple breakpoints. And you can make breakpoints conditional.

You can force a breakpoint to return a certain value. This is typically only useful if the breakpoint is the last value in a method, as this will cause the method to return a different value than normal. This is demonstrated in the example below.

You can also give names to break points which will be used in the message that is displayed upon execution of them. This helps to differentiate them at runtime if you have set several breakpoints in the code.

Parameters

name

A String to identify the breakpoint, giving a more informative message.

context

Any object; IRB will use this as its context. The default is the current scope’s binding, which is nearly always what you will want.

block

Will be executed when the breakpoint returns normally. Bypassed if you throw :debug_return, value from IRB. Unless you are planning to use the debug_return feature for a given breakpoint, you don’t need to worry about the block.

Typical Invocation

breakpoint                          # Plain message.
breakpoint "Person#name"            # More informative message.
breakpoint { normal_return_value }

Example

Here’s a sample of how breakpoints should be placed:

require 'dev-utils/debug'

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" at file.rb:4 in `initialize'
irb(#<Person:0x292fbe8>):001:0> <b>local_variables</b>
=> ["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> exit
Executing break point "Person#name" at file.rb:9 in `name'
irb(#<Person:0x292fbe8>):001:0> throw(:debug_return, "Overriden name")
Name: Overriden name

This example is explored more thoroughly at dev-utils.rubyforge.org/DebuggingAids.html.

Credits

Joel VanderWerf and Florian Gross have contributed the code and documentation for this method.



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/dev-utils/debug/irb.rb', line 99

def breakpoint(name = nil, context = nil, &block)
  file, line, method = *caller.first.match(/^(.+?):(\d+)(?::in `(.*?)')?/).captures
  message = "Executing breakpoint #{name.inspect if name} at #{file}:#{line}"
  message << " in '#{method}'" if method

  body = lambda do |_context|
    puts message
    catch(:debug_return) do |value|
      IRB.start_session(IRB::WorkSpace.new(_context))
      block.call if block        
    end
  end

    # Run IRB with the given context, if it _is_ given.
  return body.call(context) if context
    # Otherwise, run IRB with the parent scope's binding, giving access to
    # the local variables of the method that called method.
  Binding.of_caller do |binding_context|
    body.call(binding_context)
  end
end

#debug(message) ⇒ Object

Write message to the debugging log.

The debugging log is a zero-conf logfile. Here is an example usage:

$ cat example.rb

require 'dev-utils/debug'

debug "Setting variables x and y"
x = 5; y = 17
trace 'x + y'
puts "Finished"

$ ruby example.rb
Finished

$ cat debug.log
D, [#244] DEBUG : Setting variables x and y
D, [#244] DEBUG : x + y = 22

Simply with require 'dev-utils/debug', you have availed yourself of a handy debugging log file which you don’t have to create.

See also the trace method.



44
45
46
# File 'lib/dev-utils/debug/log.rb', line 44

def debug(message)
  DEBUGLOG.debug message
end

#logfile=(x) ⇒ Object

Planned feature; not yet implemented.



103
104
105
# File 'lib/dev-utils/debug/log.rb', line 103

def logfile=(x)
  warn 'logfile= is not implemented; ignoring'
end

#logger=(x) ⇒ Object

Planned feature; not yet implemented.



98
99
100
# File 'lib/dev-utils/debug/log.rb', line 98

def logger=(x)
  warn 'logger= is not implemented; ignoring'
end

#trace(expr, style = :p) ⇒ Object

Prints a trace message to DEBUGLOG (at debug level). Useful for emitting the value of variables, etc. Use like this:

x = y = 5
trace 'x'        # -> 'x = 5'
trace 'x ** y'   # -> 'x ** y = 3125'

If you have a more complicated value, like an array of hashes, then you’ll probably want to use an alternative output format. For instance:

trace 'value', :yaml

Valid output format values (the style parameter) are:

:p :inspect
:pp                     (pretty-print, using 'pp' library)
:s :to_s
:y :yaml :to_yaml       (using the 'yaml' library')

The default is :p.



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/dev-utils/debug/log.rb', line 70

def trace(expr, style=:p)
  unless expr.respond_to? :to_str
    message = "trace: Can't evaluate the given value: #{caller.first}"
    DEBUGLOG.warn message
  else
    Binding.of_caller do |b|
      value = b.eval(expr.to_str)
      formatter = TRACE_STYLES[style] || :inspect
      case formatter
      when :pp then require 'pp'
      when :y, :yaml, :to_yaml then require 'yaml'
      end
      value_s = value.send(formatter)
      message = "#{expr} = #{value_s}"
      lines = message.split(/\n/)
      indent = "   "
      DEBUGLOG.debug lines.shift            # First line unindented.
      lines.each do |line|
        DEBUGLOG.debug(indent + line)       # Other lines indented.
      end
    end
  end
end