#ruby

RubySpec for Tracepoint

Atul Bhosale's avatar

Atul Bhosale

I like to contribute to opensource projects and learn from them. This post is about what I learned while working on my pull request to Ruby Spec which got merged recently. In Ruby github repository I found that it has a spec folder which has a Readme file and that's how I & learned about Ruby Spec Suite project. Ruby Spec Suite is a test suite which has specs for Ruby methods. It is used to check if any Ruby version passes the specs or not. While going through the issues in Ruby Spec Suite Github repository I came across an issue of TracePoint Specs i.e. to add specs for TracePoint class which were missing since it was introduced in Ruby 2.0. I decided to work on this issue myself and started learning about TracePoint.

TracePoint class

TracePoint is a Ruby class that lets you listen to events that happen at the Ruby virtual machine level and lets you register callbacks for these events. It provides methods for getting more information about the event. Let's take an example to trace a method call to find the class where the method is defined.

class A
  def bar; end
end
 
last_class_name = nil
 
trace = TracePoint.new(:call) do |tp|
  last_class_name = tp.defined_class
end
 
trace.enable do
  A.new.bar
  puts last_class_name # => A
end

We can provide event names to the new method as a parameter. After tracepoint object is enabled it starts listening to the events and hence we get the value of the last_class_name as A. The following are some other tracepoint events which you can try -

  • class
  • end
  • call
  • return
  • raise

TracePoint Examples

method_name = nil
def test; end
 
trace = TracePoint.new(:call) do |tp|
  method_name = tp.method_id
end
 
trace.enable do
  test
  puts method_name # => test
end
trace_value = nil
def test; 'test' end
 
TracePoint.new(:return) { |tp| trace_value = tp.return_value}.enable do
  test
  puts trace_value # => test
end

Running Ruby Specs using mspec tool

The mspec gem is used as RSpec-like test runner for the Ruby Spec Suite. The mspec gem can be installed using -

gem install mspec

If specs are missing for a Ruby class we can contribute by first running a generator to generate spec files for methods of the class in the Ruby Spec folder using the mkspec command -

mkspec -c TracePoint

After adding specs for a Ruby class we can run specs using mspec -

mspec core/tracepoint/

The documentation for mspec is available here.

Bugs in TracePoint class

  • For TracePoint#enable & TracePoint#disable I added a spec -
TracePoint.new(:line) do |tp|
   event_name = tp.event
 end.enable { event_name.should equal(:line) }

I thought of checking what arguments get passed to the block using *args. It contains nil as the value in the *args array. I added assertion for that --

TracePoint.new(:line) do |tp|
  event_name = tp.event
end.enable do |*args|
  event_name.should equal(:line)
  args.should == [nil]
end

I asked myself should args be nil? There is no reason for enable to yield nil here. In fact, it should not yield anything. I created an issue for this in the Ruby issue tracker and added a spec for the expected behavior as shown below. Notice how issues are tagged using ruby_bug:

ruby_bug "#14057", "2.0"..."2.5" do
  it 'can accept arguments within a block but it should not yield arguments' do
    event_name = nil
    trace = TracePoint.new(:line) { |tp| event_name = tp.event }
    trace.enable do |*args|
      event_name.should equal(:line)
      args.should == []
    end
    trace.enabled?.should be_false
  end
end
  • For TracePoint#new I initilized an object without a block and it raised a ThreadError -
>> TracePoint.new(:line)
ThreadError: must be called with a block
	from (irb):1:in `new'
	from (irb):1
	from /Users/atul/.rvm/rubies/ruby-2.4.0/bin/irb:11:in `<main>'

Why did ThreadError get raised? We are not dealing with threads in this code. This looks like a bug, there is no need for a ThreadError to be raised if block is not provided for TracePoint#new. We can write a spec for this bug --

ruby_bug "#140740", "2.0"..."2.5" do
  it 'expects to be called with a block' do
    -> { TracePoint.new(:line) }.should raise_error(ArgumentError)
  end
end

The bugs have been reported on Ruby issue tracker:

  1. TracePoint#enable and TracePoint#disable should not yield arguments.
  2. TracePoint#new without a block should not raise ThreadError.

Adding specs for bugs

We need to add specs for bugs so as to reflect the correct behavior of the method. mspec provides a guard ruby_bug that wraps the spec showing what is considered to be the correct behavior.

ruby_bug "#140740", "2.0"..."2.5"

The ruby_bug method takes 3 arguments i.e.

  1. bug id - from Ruby Issue Tracking website
  2. version - which version of Ruby is affected by this bug.
  3. block - the spec block

Contributing to Ruby Specs helps to learn and improve our Ruby skills. I hope you find this useful to learn more about Ruby & contributing to the Ruby Spec Suite.