Search code examples
ruby-on-railsrubyrspecrspec-rails

It's possible to start a binding.pry session passing a command?


I have added an alias on RSpec using pry-byebug, just like this:

# frozen_string_literal: true

RSpec.configure do |rspec|
  rspec.alias_example_group_to :pcontext, pry: true
  rspec.alias_example_group_to :pdescribe, pry: true
  rspec.alias_example_to :pit, pry: true

  rspec.before(:example, pry: true) do |_|
    # rubocop:disable Lint/Debugger
    binding.pry
    # rubocop:enable Lint/Debugger
  end
end

I would like that when using this alias, for example in this line of code:

pit { expect(published_post.published?).to be_truthy }

For the session to start on the test line like this:

...
=> 16:     pit { expect(published_post.published?).to be_truthy }

But at moment it start inside the alias:

From: /opt/hobbyonrails/spec/support/alias.rb:12 :

       7: 
       8:   rspec.before(:example, pry: true) do |_|
       9:     # rubocop:disable Lint/Debugger
      10:     binding.pry
      11:     # rubocop:enable Lint/Debugger
 => 12:   end
    13: end

To go to where I want I need to run this command: step 25. So, I was wondering if there is a way to have this command run automatically right after the session starts, but I can't find the answer.

Can you give me some tips to achieve this, please?


Solution

  • Use a Pry before_session hook as described in the Pry wiki article on hooks.

    A before_session hook is executed before dropping you into the REPL when binding.pry is called. Pry uses a before_session hook every time you invoke binding.pry to call whereami --quiet, like you have in your post:

    From: /opt/hobbyonrails/spec/support/alias.rb:12 :
    
           7: 
           8:   rspec.before(:example, pry: true) do |_|
           9:     # rubocop:disable Lint/Debugger
          10:     binding.pry
          11:     # rubocop:enable Lint/Debugger
     => 12:   end
        13: end
    

    The idea of using a before_session hook to solve your problem is to tell Pry that when it is invoked it should automatically call step 25. (or however many steps you need)

    It gets a little bit tricky though because the step command from pry-byebug actually invokes a new Pry session. If you add a before_session hook that calls step, and step then invokes a new session, you'll get caught in a loop that steps through until the program finishes executing.

    Another issue is that any invocation of binding.pry in your code that is executed after the hook is added will invoke step again, and you probably don't want it to step every time, just in one particular spot.

    The fix for this is to add a hook that removes itself when executed:

    Pry.hooks.add_hook(:before_session, 'step') do |output, binding, pry|
      Pry.hooks.delete_hook(:before_session, 'step')
      pry.run_command('step 1')
    end
    

    Now the flow should be:

    1. The hook above is added
    2. binding.pry is invoked
    3. The hook above is executed, removing the hook and running step 1
    4. step invokes a new Pry session
    5. The hook above is no longer present and is not executed
    6. The REPL opens
    7. Any further binding.pry invocations do not execute the hook

    Here is an example app to show how this would work:

    require 'pry'
    require 'pry-byebug'
    
    Pry.hooks.add_hook(:before_session, 'step') do |output, binding, pry|
      Pry.hooks.delete_hook(:before_session, 'step')
      pry.run_command('step 1')
    end
    
    binding.pry
    puts 'Pry will run `whereami` and point at this line with => first'
    puts 'But when the hook runs it will step to and point at this line with =>'
    puts 'And it will stop before it reaches this line'
    
    binding.pry
    puts 'And after this second binding, there will be no hook and thus no step'
    puts 'And it will stop before it reaches this line'
    

    And here's its output demonstrating that it works as described:

    $ ruby foo.rb
    
    From: /Users/foo/foo.rb:10 :
    
         5:   Pry.hooks.delete_hook(:before_session, 'step')
         6:   pry.run_command('step 1')
         7: end
         8:
         9: binding.pry
     => 10: puts 'Pry will run `whereami` and point at this line with => first'
        11: puts 'But when the hook runs it will step to and point at this line with =>'
        12: puts 'And it will stop before it reaches this line'
        13:
        14: binding.pry
        15: puts 'And after this second binding, there will be no hook and thus no step'
    
    Pry will run `whereami` and point at this line with => first
    
    From: /Users/foo/foo.rb:11 :
    
         6:   pry.run_command('step 1')
         7: end
         8:
         9: binding.pry
        10: puts 'Pry will run `whereami` and point at this line with => first'
     => 11: puts 'But when the hook runs it will step to and point at this line with =>'
        12: puts 'And it will stop before it reaches this line'
        13:
        14: binding.pry
        15: puts 'And after this second binding, there will be no hook and thus no step'
        16: puts 'And it will stop before it reaches this line'
    
    foo|(main):1 ⇒ exit
    But when the hook runs it will step to and point at this line with =>
    And it will stop before it reaches this line
    
    From: /Users/foo/foo.rb:15 :
    
        10: puts 'Pry will run `whereami` and point at this line with => first'
        11: puts 'But when the hook runs it will step to and point at this line with =>'
        12: puts 'And it will stop before it reaches this line'
        13:
        14: binding.pry
     => 15: puts 'And after this second binding, there will be no hook and thus no step'
        16: puts 'And it will stop before it reaches this line'
    
    foo|(main):1 ⇒ 
    

    Integrating this into your app should be pretty straightforward:

    rspec.before(:example, pry: true) do |_|
      # rubocop:disable Lint/Debugger
      # You should find a good way to set this value as it seems likely to change
      steps = 25
    
      Pry.hooks.add_hook(:before_session, 'step') do |output, binding, pry|
        Pry.hooks.delete_hook(:before_session, 'step')
        pry.run_command("step #{steps}")
      end
    
      binding.pry
      # rubocop:enable Lint/Debugger
    end