Atul Bhosale

26 Feb 2018

RubySpec for Tracepoint

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 -

  • code
  • 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.

Tags

comments powered by Disqus