├── .gemtest ├── .yardopts ├── Gemfile ├── lib ├── pry-stack_explorer │ ├── version.rb │ ├── frame_manager.rb │ ├── when_started_hook.rb │ └── commands.rb └── pry-stack_explorer.rb ├── .gitignore ├── .rspec ├── test ├── support │ ├── bingbong.rb │ ├── input_tester.rb │ ├── io_utils.rb │ └── reset_helper.rb ├── test_helper.rb ├── frame_manager_test.rb ├── commands_test.rb └── stack_explorer_test.rb ├── examples ├── example2.rb ├── example.rb └── example3.rb ├── .travis.yml ├── CHANGELOG ├── bin └── rspec ├── pry-stack_explorer.gemspec ├── LICENSE ├── README.md └── Rakefile /.gemtest: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --markup markdown 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /lib/pry-stack_explorer/version.rb: -------------------------------------------------------------------------------- 1 | module PryStackExplorer 2 | VERSION = '0.6.1' 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Makefile 2 | *.so 3 | *.o 4 | *.def 5 | doc/ 6 | pkg/ 7 | .yardoc/ 8 | Gemfile.lock 9 | *.gem 10 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require ./test/test_helper.rb 4 | --pattern "**{,/*/**}/*_{spec,test}.rb" 5 | --default-path test 6 | -------------------------------------------------------------------------------- /test/support/bingbong.rb: -------------------------------------------------------------------------------- 1 | class BingBong 2 | attr_reader :frames, :frame, :methods 3 | 4 | def initialize 5 | @methods = [] 6 | end 7 | 8 | def bing; bong; end 9 | def bong; bang; end 10 | def bang; Pry.start(binding); end 11 | end 12 | -------------------------------------------------------------------------------- /examples/example2.rb: -------------------------------------------------------------------------------- 1 | unless Object.const_defined? :PryStackExplorer 2 | $:.unshift File.expand_path '../../lib', __FILE__ 3 | require 'pry' 4 | end 5 | 6 | require 'pry-stack_explorer' 7 | 8 | def alpha 9 | x = "hello" 10 | beta 11 | puts x 12 | end 13 | 14 | def beta 15 | binding.pry 16 | end 17 | 18 | alpha 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | script: 4 | rspec --backtrace 5 | 6 | before_install: 7 | - gem install bundler 8 | 9 | rvm: 10 | - 2.6.6 11 | - 2.7.2 12 | - 3.0.0 13 | - ruby-head 14 | 15 | matrix: 16 | allow_failures: 17 | - rvm: ruby-head 18 | fast_finish: true 19 | 20 | notifications: 21 | irc: "irc.freenode.org#pry" 22 | 23 | branches: 24 | only: 25 | - master 26 | -------------------------------------------------------------------------------- /test/support/input_tester.rb: -------------------------------------------------------------------------------- 1 | class InputTester 2 | def initialize(*actions) 3 | if actions.last.is_a?(Hash) && actions.last.keys == [:history] 4 | @hist = actions.pop[:history] 5 | end 6 | @orig_actions = actions.dup 7 | @actions = actions 8 | end 9 | 10 | def readline(*) 11 | @actions.shift.tap{ |line| @hist << line if @hist } 12 | end 13 | 14 | def rewind 15 | @actions = @orig_actions.dup 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /examples/example.rb: -------------------------------------------------------------------------------- 1 | unless Object.const_defined? :PryStackExplorer 2 | $:.unshift File.expand_path '../../lib', __FILE__ 3 | require 'pry' 4 | end 5 | 6 | require 'pry-stack_explorer' 7 | 8 | def alphabet(y) 9 | x = 20 10 | b 11 | end 12 | 13 | def b 14 | x = 30 15 | proc { 16 | c 17 | }.call 18 | end 19 | 20 | def c 21 | u = 50 22 | binding.pry 23 | end 24 | 25 | # hello 26 | def beta 27 | gamma 28 | end 29 | 30 | def gamma 31 | zeta 32 | end 33 | 34 | def zeta 35 | vitamin = 100 36 | binding.pry 37 | end 38 | # 39 | 40 | proc { 41 | class J 42 | alphabet(22) 43 | end 44 | }.call 45 | 46 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | ## v0.6.1 (8 February 2021) 2 | * Fix 'Source Code' link in gem metadata 3 | * Stack now filters for application frames (#53) 4 | 5 | ## v0.6.0 (31 December 2020) 6 | * Use binding_of_caller version for Ruby 3.0 (#55) 7 | * Rename 'show-stack' to 'stack' (#50, #8) 8 | 9 | ## v0.5.1 (21 May 2020) 10 | * Fix exception in started hook 11 | 12 | ## v0.5.0 (21 May 2020) 13 | * Should fix most deprecation warnings as of release 14 | * Require Pry 0.13 15 | * Fix Pry#_pry_ => #pry_instance deprecation 16 | 17 | ## v0.4.11 (21 May 2020) 18 | * Special branch for Ruby 2.5 19 | * Fix Pry 0.13+ deprecations and broken `show-stack` 20 | -------------------------------------------------------------------------------- /examples/example3.rb: -------------------------------------------------------------------------------- 1 | unless Object.const_defined? :PryStackExplorer 2 | $:.unshift File.expand_path '../../lib', __FILE__ 3 | require 'pry' 4 | end 5 | 6 | require 'pry-stack_explorer' 7 | 8 | def b 9 | x = 30 10 | proc { 11 | c 12 | }.call 13 | end 14 | 15 | def c 16 | u = 50 17 | V.new.beta 18 | end 19 | 20 | # hello 21 | class V 22 | def beta 23 | gamma 24 | end 25 | 26 | def gamma 27 | zeta 28 | end 29 | end 30 | 31 | def zeta 32 | vitamin = 100 33 | binding.pry 34 | end 35 | # 36 | 37 | proc { 38 | class J 39 | def alphabet(y) 40 | x = 20 41 | b 42 | end 43 | end 44 | }.call 45 | 46 | J.new.alphabet(122) 47 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | require 'pry' 3 | 4 | Dir[File.expand_path("../support/**/*.rb", __FILE__)].each do |file| 5 | require file 6 | end 7 | 8 | if RUBY_VERSION >= '2.7.2' 9 | # NOTE: https://bugs.ruby-lang.org/issues/17000 10 | Warning[:deprecated] = true 11 | end 12 | 13 | # unless Object.const_defined? 'PryStackExplorer' 14 | $:.unshift File.expand_path '../../lib', __FILE__ 15 | require 'pry-stack_explorer' 16 | # end 17 | 18 | puts "Testing pry-stack_explorer version #{PryStackExplorer::VERSION}..." 19 | puts "Ruby version: #{RUBY_VERSION}" 20 | 21 | PE = PryStackExplorer 22 | 23 | ResetHelper::Hooks.memoize! 24 | 25 | RSpec.configure do |config| 26 | config.include IOUtils 27 | end 28 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rspec' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("rspec-core", "rspec") 30 | -------------------------------------------------------------------------------- /pry-stack_explorer.gemspec: -------------------------------------------------------------------------------- 1 | require File.expand_path('../lib/pry-stack_explorer/version', __FILE__) 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "pry-stack_explorer" 5 | s.version = PryStackExplorer::VERSION 6 | 7 | s.required_ruby_version = ">= 2.6.0" 8 | 9 | s.authors = ["John Mair (banisterfiend)"] 10 | s.email = ["jrmair@gmail.com"] 11 | 12 | s.license = "MIT" 13 | 14 | s.summary = "Walk the stack in a Pry session" 15 | 16 | s.require_paths = ["lib"] 17 | s.files = `git ls-files lib *.md LICENSE`.split("\n") 18 | 19 | s.homepage = "https://github.com/pry/pry-stack_explorer" 20 | s.metadata = { 21 | "bug_tracker_uri" => "https://github.com/pry/pry-stack_explorer/issues", 22 | "source_code_uri" => "https://github.com/pry/pry-stack_explorer", 23 | "changelog_uri" => "https://github.com/pry/pry-stack_explorer/blob/master/CHANGELOG", 24 | } 25 | 26 | s.specification_version = 4 27 | 28 | s.add_runtime_dependency 'binding_of_caller', '~> 1.0' 29 | s.add_runtime_dependency 'pry', '~> 0.13' 30 | 31 | s.add_development_dependency 'rspec', '~> 3.9' 32 | s.add_development_dependency 'rake', '~> 0.9' 33 | end 34 | -------------------------------------------------------------------------------- /test/support/io_utils.rb: -------------------------------------------------------------------------------- 1 | module IOUtils 2 | def redirect_pry_output! 3 | @pry_output = StringIO.new 4 | Pry.config.output = @pry_output 5 | end 6 | 7 | attr_accessor :pry_output 8 | 9 | # Set I/O streams. 10 | # 11 | # Out defaults to an anonymous StringIO. 12 | def with_pry_output_captured(new_in, new_out = StringIO.new) 13 | old_in = Pry.input 14 | old_out = Pry.output 15 | 16 | Pry.input = new_in 17 | Pry.output = new_out 18 | 19 | begin 20 | yield 21 | ensure 22 | Pry.input = old_in 23 | Pry.output = old_out 24 | end 25 | 26 | new_out 27 | end 28 | 29 | alias :redirect_pry_io :with_pry_output_captured 30 | 31 | 32 | def mock_pry(*args) 33 | binding = args.first.is_a?(Binding) ? args.shift : binding() 34 | 35 | input = InputTester.new(*args) 36 | output = StringIO.new 37 | 38 | redirect_pry_io(input, output) do 39 | binding.pry 40 | end 41 | 42 | output.string 43 | end 44 | 45 | def issue_pry_commands(*commands, &block) 46 | input_tester = InputTester.new(*commands) 47 | redirect_pry_io(input_tester, &block).string 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | License 2 | ------- 3 | 4 | (The MIT License) 5 | 6 | Copyright (c) 2011 John Mair (banisterfiend) 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining 9 | a copy of this software and associated documentation files (the 10 | 'Software'), to deal in the Software without restriction, including 11 | without limitation the rights to use, copy, modify, merge, publish, 12 | distribute, sublicense, and/or sell copies of the Software, and to 13 | permit persons to whom the Software is furnished to do so, subject to 14 | the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be 17 | included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 22 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 23 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 24 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 25 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /test/support/reset_helper.rb: -------------------------------------------------------------------------------- 1 | module ResetHelper 2 | 3 | def self.reset_pry_defaults! 4 | # Pry.reset_defaults 5 | 6 | # Pry.color = false 7 | Pry.pager = false 8 | Pry.config.hooks = Pry::Hooks.new 9 | # Pry.config.should_load_rc = false 10 | # Pry.config.should_load_plugins = false 11 | # Pry.config.auto_indent = false 12 | # Pry.config.collision_warning = false 13 | end 14 | 15 | def hooks 16 | Hooks 17 | end 18 | 19 | def self.included(base) 20 | base.class_exec do 21 | before :all do 22 | ResetHelper.reset_pry_defaults! 23 | end 24 | 25 | around do |example| 26 | hooks.with_setup{ example.run } 27 | end 28 | end 29 | end 30 | 31 | module Hooks; end 32 | 33 | class << Hooks 34 | def memoize! 35 | @@hooks = { 36 | when_started: Pry.config.hooks.get_hook(:when_started, :save_caller_bindings), 37 | after_session: Pry.config.hooks.get_hook(:after_session, :delete_frame_manager) 38 | } 39 | end 40 | 41 | def setup! 42 | Pry.config.hooks.add_hook(:when_started, :save_caller_bindings, @@hooks[:when_started]) 43 | Pry.config.hooks.add_hook(:after_session, :delete_frame_manager, @@hooks[:after_session]) 44 | end 45 | 46 | def teardown! 47 | Pry.config.hooks.delete_hook(:when_started, :save_caller_bindings) 48 | Pry.config.hooks.delete_hook(:after_session, :delete_frame_manager) 49 | end 50 | 51 | def with_setup(&block) 52 | setup! 53 | yield 54 | teardown! 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pry-stack_explorer 2 | =========== 3 | 4 | _Walk the stack in a Pry session_ 5 | 6 | --- 7 | 8 | Pry::StackExplorer is a plugin for [Pry](http://pry.github.com) 9 | that allows navigating the call stack. 10 | 11 | From the point a Pry session is started, the user can move up the stack 12 | through parent frames, examine state, and even evaluate code. 13 | 14 | Unlike `ruby-debug`, pry-stack_explorer incurs no runtime cost and 15 | enables navigation right up the call-stack to the birth of the 16 | program. 17 | 18 | The `up`, `down`, `frame` and `stack` commands are provided. See 19 | Pry's in-session help for more information on any of these commands. 20 | 21 | ## Usage 22 | Provides commands available in Pry sessions. 23 | 24 | Commands: 25 | * `up`/`down` - Move up or down the call stack 26 | * `frame [n]` - Go to frame *n* 27 | * `stack` - Show call stack 28 | 29 | 30 | ## Install 31 | 32 | In Gemfile: 33 | ```rb 34 | gem 'pry-stack_explorer', '~> 0.6.0' 35 | ``` 36 | 37 | ``` 38 | gem install pry-stack_explorer 39 | ``` 40 | 41 | * Read the [documentation](http://rdoc.info/github/banister/pry-stack_explorer/master/file/README.md) 42 | * See the [wiki](https://github.com/pry/pry-stack_explorer/wiki) for in-depth usage information. 43 | 44 | 45 | ### Branches and compatible Ruby versions 46 | * v0.5, v0.6: Ruby 2.6+, Pry 0.13+ 47 | * v0.4.11+: Ruby 2.5, Pry 0.12+ (branch `0-4` – end-of-life in March 2021) 48 | * v0.4.9.3: Older versions (unsupported) 49 | 50 | Example: 51 | -------- 52 | Here we run the following ruby script: 53 | ```Ruby 54 | require 'pry-stack_explorer' 55 | 56 | def alpha 57 | x = "hello" 58 | beta 59 | puts x 60 | end 61 | 62 | def beta 63 | binding.pry 64 | end 65 | 66 | alpha 67 | ``` 68 | 69 | We wander around the stack a little bit, and modify the state of a frame above the one we `binding.pry`'d at. 70 | 71 | [![asciicast](https://asciinema.org/a/257713.svg)](https://asciinema.org/a/257713) 72 | 73 | Output from above is `Goodbye` as we changed the `x` local inside the `alpha` (caller) stack frame. 74 | 75 | 76 | License 77 | ------- 78 | Released under the [MIT License](https://github.com/pry/pry-stack_explorer/blob/master/LICENSE) by John Mair (banisterfiend) and contributors 79 | 80 | Contributions to this gem are released under the same license. 81 | -------------------------------------------------------------------------------- /test/frame_manager_test.rb: -------------------------------------------------------------------------------- 1 | require_relative 'test_helper' 2 | 3 | describe PryStackExplorer::FrameManager do 4 | before :all do 5 | redirect_pry_output! 6 | end 7 | 8 | before do 9 | @pry_instance = Pry.new 10 | @bindings = [binding, binding, binding, binding] 11 | @bindings.each_with_index { |v, i| v.eval("x = #{i}") } 12 | @pry_instance.binding_stack.push @bindings.last 13 | @frame_manager = PE::FrameManager.new(@bindings, @pry_instance) 14 | end 15 | 16 | describe "creation" do 17 | it "should make bindings accessible via 'bindings' method" do 18 | expect(@frame_manager.bindings).to eq(@bindings) 19 | end 20 | 21 | it "should set binding_index to 0" do 22 | expect(@frame_manager.binding_index).to eq(0) 23 | end 24 | 25 | it "should set current_frame to first frame" do 26 | expect(@frame_manager.current_frame).to eq(@bindings.first) 27 | end 28 | end 29 | 30 | describe "FrameManager#change_frame_to" do 31 | it 'should change the frame to the given one' do 32 | @frame_manager.change_frame_to(1) 33 | 34 | expect(@frame_manager.binding_index).to eq(1) 35 | expect(@frame_manager.current_frame).to eq(@bindings[1]) 36 | expect(@pry_instance.binding_stack.last).to eq(@frame_manager.current_frame) 37 | end 38 | 39 | it 'should accept negative indices when specifying frame' do 40 | @frame_manager.change_frame_to(-1) 41 | 42 | # negative index is converted to a positive one inside change_frame_to 43 | expect(@frame_manager.binding_index).to eq(@bindings.size - 1) 44 | expect(@frame_manager.current_frame).to eq(@bindings[-1]) 45 | expect(@pry_instance.binding_stack.last).to eq(@frame_manager.current_frame) 46 | end 47 | end 48 | 49 | describe "FrameManager#refresh_frame" do 50 | it 'should change the Pry frame to the active one in the FrameManager' do 51 | @frame_manager.binding_index = 2 52 | @frame_manager.refresh_frame 53 | 54 | expect(@pry_instance.binding_stack.last).to eq(@frame_manager.current_frame) 55 | end 56 | end 57 | 58 | describe "FrameManager is Enumerable" do 59 | it 'should perform an Enumerable#map on the frames' do 60 | result = @frame_manager.map { |v| v.eval("x") } 61 | 62 | expect(result).to eq( 63 | (0..(@bindings.size - 1)).to_a 64 | ) 65 | end 66 | end 67 | 68 | end 69 | -------------------------------------------------------------------------------- /lib/pry-stack_explorer/frame_manager.rb: -------------------------------------------------------------------------------- 1 | module PryStackExplorer 2 | 3 | # This class represents a call-stack. It stores the 4 | # frames that make up the stack and is responsible for updating the 5 | # associated Pry instance to reflect the active frame. It is fully Enumerable. 6 | class FrameManager 7 | include Enumerable 8 | 9 | # @return [Array] The array of bindings that constitute 10 | # the call-stack. 11 | attr_accessor :bindings 12 | 13 | # @return [Fixnum] The index of the active frame (binding) in the call-stack. 14 | attr_accessor :binding_index 15 | 16 | # @return [Hash] A hash for user defined data 17 | attr_reader :user 18 | 19 | # @return [Binding] The binding of the Pry instance before the 20 | # FrameManager took over. 21 | attr_reader :prior_binding 22 | 23 | # @return [Array] The backtrace of the Pry instance before the 24 | # FrameManager took over. 25 | attr_reader :prior_backtrace 26 | 27 | def initialize(bindings, _pry_) 28 | self.bindings = bindings 29 | self.binding_index = 0 30 | @pry = _pry_ 31 | @user = {} 32 | @prior_binding = _pry_.binding_stack.last 33 | @prior_backtrace = _pry_.backtrace 34 | end 35 | 36 | # Iterate over all frames 37 | def each(&block) 38 | bindings.each(&block) 39 | end 40 | 41 | # Ensure the Pry instance's active binding is the frame manager's 42 | # active binding. 43 | def refresh_frame(run_whereami=true) 44 | change_frame_to binding_index, run_whereami 45 | end 46 | 47 | # @return [Binding] The currently active frame 48 | def current_frame 49 | bindings[binding_index] 50 | end 51 | 52 | # Set the binding index (aka frame index), but raising an Exception when invalid 53 | # index received. Also converts negative indices to their positive counterparts. 54 | # @param [Fixnum] index The index. 55 | def set_binding_index_safely(index) 56 | if index > bindings.size - 1 57 | raise Pry::CommandError, "At top of stack, cannot go further!" 58 | elsif index < -bindings.size 59 | raise Pry::CommandError, "At bottom of stack, cannot go further!" 60 | else 61 | # wrap around negative indices 62 | index = (bindings.size - 1) + index + 1 if index < 0 63 | 64 | self.binding_index = index 65 | end 66 | end 67 | 68 | # Change active frame to the one indexed by `index`. 69 | # Note that indexing base is `0` 70 | # @param [Fixnum] index The index of the frame. 71 | def change_frame_to(index, run_whereami=true) 72 | 73 | set_binding_index_safely(index) 74 | 75 | if @pry.binding_stack.empty? 76 | @pry.binding_stack.replace [bindings[binding_index]] 77 | else 78 | @pry.binding_stack[-1] = bindings[binding_index] 79 | end 80 | 81 | @pry.run_command "whereami" if run_whereami 82 | end 83 | 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/pry-stack_explorer/when_started_hook.rb: -------------------------------------------------------------------------------- 1 | module PryStackExplorer 2 | class WhenStartedHook 3 | include Pry::Helpers::BaseHelpers 4 | 5 | def caller_bindings(target) 6 | bindings = binding.callers 7 | 8 | bindings = remove_internal_frames(bindings) 9 | bindings = remove_debugger_frames(bindings) 10 | bindings = bindings.drop(1) if pry_method_frame?(bindings.first) 11 | 12 | # Use the binding returned by #of_caller if possible (as we get 13 | # access to frame_type). 14 | # Otherwise stick to the given binding (target). 15 | if !PryStackExplorer.bindings_equal?(target, bindings.first) 16 | bindings.shift 17 | bindings.unshift(target) 18 | end 19 | 20 | bindings 21 | end 22 | 23 | def call(target, options, _pry_) 24 | target ||= _pry_.binding_stack.first if _pry_ 25 | options = { 26 | :call_stack => true, 27 | :initial_frame => 0 28 | }.merge!(options) 29 | 30 | return if !options[:call_stack] 31 | 32 | if options[:call_stack].is_a?(Array) 33 | bindings = options[:call_stack] 34 | 35 | if !valid_call_stack?(bindings) 36 | raise ArgumentError, ":call_stack must be an array of bindings" 37 | end 38 | else 39 | bindings = caller_bindings(target) 40 | end 41 | 42 | PryStackExplorer.create_and_push_frame_manager bindings, _pry_, :initial_frame => options[:initial_frame] 43 | end 44 | 45 | private 46 | 47 | # remove internal frames related to setting up the session 48 | def remove_internal_frames(bindings) 49 | start_frames = internal_frames_with_indices(bindings) 50 | return bindings if start_frames.empty? 51 | 52 | start_frame_index = start_frames.first.last 53 | 54 | if start_frames.size >= 2 55 | # god knows what's going on in here 56 | idx1, idx2 = start_frames.take(2).map(&:last) 57 | start_frame_index = idx2 if !nested_session?(bindings[idx1..idx2]) 58 | end 59 | 60 | bindings.drop(start_frame_index + 1) 61 | end 62 | 63 | # remove pry-nav / pry-debugger / pry-byebug frames 64 | def remove_debugger_frames(bindings) 65 | bindings.drop_while { |b| b.source_location[0] =~ /pry-(?:nav|debugger|byebug)/ } 66 | end 67 | 68 | # binding.pry frame 69 | # @return [Boolean] 70 | def pry_method_frame?(binding) 71 | safe_send(binding.eval("__method__"), :==, :pry) 72 | end 73 | 74 | # When a pry session is started within a pry session 75 | # @return [Boolean] 76 | def nested_session?(bindings) 77 | bindings.detect do |b| 78 | safe_send(b.eval("__method__"), :==, :re) && 79 | safe_send(b.eval("self.class"), :equal?, Pry) 80 | end 81 | end 82 | 83 | # @return [Array>] 84 | def internal_frames_with_indices(bindings) 85 | bindings.each_with_index.select do |b, i| 86 | b.frame_type == :method && 87 | safe_send(b.eval("self"), :equal?, Pry) && 88 | safe_send(b.eval("__method__"), :==, :start) 89 | end 90 | end 91 | 92 | def valid_call_stack?(bindings) 93 | bindings.any? && bindings.all? { |v| v.is_a?(Binding) } 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | $:.unshift 'lib' 4 | 5 | dlext = RbConfig::CONFIG['DLEXT'] 6 | direc = File.dirname(__FILE__) 7 | 8 | PROJECT_NAME = "pry-stack_explorer" 9 | 10 | require 'rake/clean' 11 | require 'rubygems/package_task' 12 | require "#{PROJECT_NAME}/version" 13 | 14 | CLOBBER.include("**/*~", "**/*#*", "**/*.log") 15 | CLEAN.include("**/*#*", "**/*#*.*", "**/*_flymake*.*", "**/*_flymake", 16 | "**/*.rbc", "**/.#*.*") 17 | 18 | def apply_spec_defaults(s) 19 | s.name = PROJECT_NAME 20 | s.summary = "Walk the stack in a Pry session" 21 | s.version = PryStackExplorer::VERSION 22 | s.date = Time.now.strftime '%Y-%m-%d' 23 | s.author = "John Mair (banisterfiend)" 24 | s.email = 'jrmair@gmail.com' 25 | s.description = s.summary 26 | s.require_path = 'lib' 27 | s.add_dependency("binding_of_caller",">= 0.7") 28 | s.add_dependency("pry",">=0.9.11") 29 | s.add_development_dependency("bacon","~>1.1.0") 30 | s.add_development_dependency('rake', '~> 0.9') 31 | s.homepage = "https://github.com/pry/pry-stack_explorer" 32 | s.files = `git ls-files`.split("\n") 33 | s.test_files = `git ls-files -- test/*`.split("\n") 34 | end 35 | 36 | desc "run pry with plugin enabled" 37 | task :pry do 38 | exec("pry -I#{direc}/lib/ -r #{direc}/lib/#{PROJECT_NAME}") 39 | end 40 | 41 | desc "Run example" 42 | task :example do 43 | sh "ruby -I#{direc}/lib/ #{direc}/examples/example.rb " 44 | end 45 | 46 | desc "Run example2" 47 | task :example2 do 48 | sh "ruby -I#{direc}/lib/ #{direc}/examples/example2.rb " 49 | end 50 | 51 | desc "Run example3" 52 | task :example3 do 53 | sh "ruby -I#{direc}/lib/ #{direc}/examples/example3.rb " 54 | end 55 | 56 | desc "Show version" 57 | task :version do 58 | puts "PryStackExplorer version: #{PryStackExplorer::VERSION}" 59 | end 60 | 61 | desc "run tests" 62 | task :default => :test 63 | 64 | desc "run tests" 65 | task :test do 66 | sh "rspec" 67 | end 68 | 69 | desc "generate gemspec" 70 | task :gemspec => "ruby:gemspec" 71 | 72 | namespace :ruby do 73 | spec = Gem::Specification.new do |s| 74 | apply_spec_defaults(s) 75 | s.platform = Gem::Platform::RUBY 76 | end 77 | 78 | Gem::PackageTask.new(spec) do |pkg| 79 | pkg.need_zip = false 80 | pkg.need_tar = false 81 | end 82 | 83 | desc "Generate gemspec file" 84 | task :gemspec do 85 | File.open("#{spec.name}.gemspec", "w") do |f| 86 | f << spec.to_ruby 87 | end 88 | end 89 | end 90 | 91 | desc "build all platform gems at once" 92 | task :gems => [:clean, :rmgems, :gemspec, "ruby:gem"] 93 | 94 | desc "remove all platform gems" 95 | task :rmgems => ["ruby:clobber_package"] 96 | 97 | desc "reinstall gem" 98 | task :reinstall => :gems do 99 | sh "gem uninstall pry-stack_explorer" rescue nil 100 | sh "gem install -l #{direc}/pkg/#{PROJECT_NAME}-#{PryStackExplorer::VERSION}.gem" 101 | end 102 | 103 | task :install => :reinstall 104 | 105 | desc "build and push latest gems" 106 | task :pushgems => :gems do 107 | chdir("#{File.dirname(__FILE__)}/pkg") do 108 | Dir["*.gem"].each do |gemfile| 109 | sh "gem push #{gemfile}" 110 | end 111 | end 112 | end 113 | 114 | task :pushgem => :pushgems 115 | -------------------------------------------------------------------------------- /lib/pry-stack_explorer.rb: -------------------------------------------------------------------------------- 1 | # pry-stack_explorer.rb 2 | # (C) John Mair (banisterfiend); MIT license 3 | 4 | require "pry" unless defined?(::Pry) 5 | require "pry-stack_explorer/version" 6 | require "pry-stack_explorer/commands" 7 | require "pry-stack_explorer/frame_manager" 8 | require "pry-stack_explorer/when_started_hook" 9 | require "binding_of_caller" 10 | 11 | module PryStackExplorer 12 | 13 | # short-hand for `PryStackExplorer` 14 | ::SE = self 15 | 16 | class << self 17 | # @return [Hash] The hash storing all frames for all Pry instances for 18 | # the current thread. 19 | def frame_hash 20 | Thread.current[:__pry_frame_managers__] ||= Hash.new { |h, k| h[k] = [] } 21 | end 22 | 23 | # Return the complete frame manager stack for the Pry instance 24 | # @param [Pry] _pry_ The Pry instance associated with the frame 25 | # managers 26 | # @return [Array] The stack of Pry::FrameManager objections 27 | def frame_managers(_pry_) 28 | frame_hash[_pry_] 29 | end 30 | 31 | # Create a `Pry::FrameManager` object and push it onto the frame 32 | # manager stack for the relevant `_pry_` instance. 33 | # @param [Array] bindings The array of bindings (frames) 34 | # @param [Pry] _pry_ The Pry instance associated with the frame manager 35 | def create_and_push_frame_manager(bindings, _pry_, options={}) 36 | fm = FrameManager.new(bindings, _pry_) 37 | frame_hash[_pry_].push fm 38 | push_helper(fm, options) 39 | fm 40 | end 41 | 42 | # Update the Pry instance to operate on the specified frame for the 43 | # current frame manager. 44 | # @param [PryStackExplorer::FrameManager] fm The active frame manager. 45 | # @param [Hash] options The options hash. 46 | def push_helper(fm, options={}) 47 | options = { 48 | :initial_frame => 0 49 | }.merge!(options) 50 | 51 | fm.change_frame_to(options[:initial_frame], false) 52 | end 53 | 54 | private :push_helper 55 | 56 | # Delete the currently active frame manager 57 | # @param [Pry] _pry_ The Pry instance associated with the frame 58 | # managers. 59 | # @return [Pry::FrameManager] The popped frame manager. 60 | def pop_frame_manager(_pry_) 61 | return if frame_managers(_pry_).empty? 62 | 63 | popped_fm = frame_managers(_pry_).pop 64 | pop_helper(popped_fm, _pry_) 65 | popped_fm 66 | end 67 | 68 | # Restore the Pry instance to operate on the previous 69 | # binding. Also responsible for restoring Pry instance's backtrace. 70 | # @param [Pry::FrameManager] popped_fm The recently popped frame manager. 71 | # @param [Pry] _pry_ The Pry instance associated with the frame managers. 72 | def pop_helper(popped_fm, _pry_) 73 | if frame_managers(_pry_).empty? 74 | if _pry_.binding_stack.empty? 75 | _pry_.binding_stack.push popped_fm.prior_binding 76 | else 77 | _pry_.binding_stack[-1] = popped_fm.prior_binding 78 | end 79 | 80 | frame_hash.delete(_pry_) 81 | else 82 | frame_manager(_pry_).refresh_frame(false) 83 | end 84 | 85 | # restore backtrace 86 | _pry_.backtrace = popped_fm.prior_backtrace 87 | end 88 | 89 | private :pop_helper 90 | 91 | # Clear the stack of frame managers for the Pry instance 92 | # @param [Pry] _pry_ The Pry instance associated with the frame managers 93 | def clear_frame_managers(_pry_) 94 | pop_frame_manager(_pry_) until frame_managers(_pry_).empty? 95 | frame_hash.delete(_pry_) # this line should be unnecessary! 96 | end 97 | 98 | alias_method :delete_frame_managers, :clear_frame_managers 99 | 100 | # @return [PryStackExplorer::FrameManager] The currently active frame manager 101 | def frame_manager(_pry_) 102 | frame_hash[_pry_].last 103 | end 104 | 105 | # Simple test to check whether two `Binding` objects are equal. 106 | # @param [Binding] b1 First binding. 107 | # @param [Binding] b2 Second binding. 108 | # @return [Boolean] Whether the `Binding`s are equal. 109 | def bindings_equal?(b1, b2) 110 | (b1.eval('self').equal?(b2.eval('self'))) && 111 | (b1.eval('__method__') == b2.eval('__method__')) && 112 | (b1.eval('local_variables').map { |v| b1.eval("#{v}") }.equal?( 113 | b2.eval('local_variables').map { |v| b2.eval("#{v}") })) 114 | end 115 | end 116 | end 117 | 118 | Pry.config.hooks.add_hook(:after_session, :delete_frame_manager) do |_, _, _pry_| 119 | PryStackExplorer.clear_frame_managers(_pry_) 120 | end 121 | 122 | Pry.config.hooks.add_hook(:when_started, :save_caller_bindings, PryStackExplorer::WhenStartedHook.new) 123 | 124 | # Import the StackExplorer commands 125 | Pry.config.commands.import PryStackExplorer::Commands 126 | 127 | # monkey-patch the whereami command to show some frame information, 128 | # useful for navigating stack. 129 | Pry.config.hooks.add_hook(:before_whereami, :stack_explorer) do 130 | if PryStackExplorer.frame_manager(pry_instance) && !internal_binding?(target) 131 | bindings = PryStackExplorer.frame_manager(pry_instance).bindings 132 | binding_index = PryStackExplorer.frame_manager(pry_instance).binding_index 133 | 134 | output.puts "\n" 135 | output.puts "#{Pry::Helpers::Text.bold('Frame number:')} #{binding_index}/#{bindings.size - 1}" 136 | output.puts "#{Pry::Helpers::Text.bold('Frame type:')} #{bindings[binding_index].frame_type}" if bindings[binding_index].frame_type 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /test/commands_test.rb: -------------------------------------------------------------------------------- 1 | require_relative 'test_helper' 2 | 3 | class Top 4 | attr_accessor :method_list, :middle 5 | def initialize method_list 6 | @method_list = method_list 7 | end 8 | def bing 9 | @middle = Middle.new method_list 10 | @middle.bong 11 | end 12 | end 13 | 14 | class Middle 15 | attr_accessor :method_list, :bottom 16 | def initialize method_list 17 | @method_list = method_list 18 | end 19 | def bong 20 | @bottom = Bottom.new method_list 21 | @bottom.bang 22 | end 23 | end 24 | 25 | class Bottom 26 | attr_accessor :method_list 27 | def initialize method_list 28 | @method_list = method_list 29 | end 30 | def bang 31 | Pry.start(binding) 32 | end 33 | end 34 | 35 | 36 | describe "Commands" do 37 | let(:bingbong){ BingBong.new } 38 | 39 | include ResetHelper 40 | 41 | before do 42 | method_list = [] 43 | @top = Top.new method_list 44 | end 45 | 46 | 47 | describe "stack" do 48 | it "outputs the call stack" do 49 | output = issue_pry_commands("stack"){ bingbong.bing } 50 | 51 | expect(output).to match(/bang.*?bong.*?bing/m) 52 | end 53 | 54 | it "supports 'show-stack' as an alias" do 55 | output = issue_pry_commands("show-stack"){ bingbong.bing } 56 | 57 | expect(output).to match(/bang.*?bong.*?bing/m) 58 | end 59 | end 60 | 61 | describe "up" do 62 | it 'should move up the call stack one frame at a time' do 63 | redirect_pry_io(InputTester.new("@methods << __method__", 64 | "up", 65 | "@methods << __method__", 66 | "up", 67 | "@methods << __method__", 68 | "exit-all"), out=StringIO.new) do 69 | bingbong.bing 70 | end 71 | 72 | expect(bingbong.methods).to eq [:bang, :bong, :bing] 73 | end 74 | 75 | it 'should move up the call stack two frames at a time' do 76 | redirect_pry_io(InputTester.new("@methods << __method__", 77 | "up 2", 78 | "@methods << __method__", 79 | "exit-all"), out=StringIO.new) do 80 | bingbong.bing 81 | end 82 | 83 | expect(bingbong.methods).to eq [:bang, :bing] 84 | end 85 | 86 | describe "by method name regex" do 87 | it 'should move to the method name that matches the regex' do 88 | redirect_pry_io(InputTester.new("@methods << __method__", 89 | "up bi", 90 | "@methods << __method__", 91 | "exit-all"), out=StringIO.new) do 92 | bingbong.bing 93 | end 94 | 95 | expect(bingbong.methods).to eq [:bang, :bing] 96 | end 97 | 98 | it 'should move through all methods that match regex in order' do 99 | redirect_pry_io(InputTester.new("@methods << __method__", 100 | "up b", 101 | "@methods << __method__", 102 | "up b", 103 | "@methods << __method__", 104 | "exit-all"), out=StringIO.new) do 105 | bingbong.bing 106 | end 107 | 108 | expect(bingbong.methods).to eq [:bang, :bong, :bing] 109 | end 110 | 111 | it 'should error if it cant find frame to match regex' do 112 | redirect_pry_io(InputTester.new("up conrad_irwin", 113 | "exit-all"), out=StringIO.new) do 114 | bingbong.bing 115 | end 116 | 117 | expect(out.string).to match(/Error: No frame that matches/) 118 | end 119 | end 120 | 121 | 122 | describe 'by Class#method name regex' do 123 | it 'should move to the method and class that matches the regex' do 124 | redirect_pry_io(InputTester.new("@method_list << self.class.to_s + '#' + __method__.to_s", 125 | 'up Middle#bong', 126 | "@method_list << self.class.to_s + '#' + __method__.to_s", 127 | "exit-all"), out=StringIO.new) do 128 | @top.bing 129 | end 130 | 131 | expect(@top.method_list).to eq(['Bottom#bang', 'Middle#bong']) 132 | end 133 | 134 | ### ????? ### 135 | # it 'should be case sensitive' do 136 | # end 137 | ### ????? ### 138 | 139 | it 'should allow partial class names' do 140 | redirect_pry_io(InputTester.new("@method_list << self.class.to_s + '#' + __method__.to_s", 141 | 'up Mid#bong', 142 | "@method_list << self.class.to_s + '#' + __method__.to_s", 143 | "exit-all"), out=StringIO.new) do 144 | @top.bing 145 | end 146 | 147 | expect(@top.method_list).to eq(['Bottom#bang', 'Middle#bong']) 148 | end 149 | 150 | it 'should allow partial method names' do 151 | redirect_pry_io(InputTester.new("@method_list << self.class.to_s + '#' + __method__.to_s", 152 | 'up Middle#bo', 153 | "@method_list << self.class.to_s + '#' + __method__.to_s", 154 | "exit-all"), out=StringIO.new) do 155 | @top.bing 156 | end 157 | 158 | expect(@top.method_list).to eq(['Bottom#bang', 'Middle#bong']) 159 | end 160 | 161 | it 'should error if it cant find frame to match regex' do 162 | redirect_pry_io(InputTester.new('up Conrad#irwin', 163 | "exit-all"), out=StringIO.new) do 164 | @top.bing 165 | end 166 | 167 | expect(out.string).to match(/Error: No frame that matches/) 168 | end 169 | end 170 | end 171 | 172 | describe "down" do 173 | it 'should move down the call stack one frame at a time' do 174 | def bingbong.bang() Pry.start(binding, :initial_frame => 1) end 175 | 176 | redirect_pry_io(InputTester.new("@methods << __method__", 177 | "down", 178 | "@methods << __method__", 179 | "exit-all"), out=StringIO.new) do 180 | bingbong.bing 181 | end 182 | 183 | expect(bingbong.methods).to eq [:bong, :bang] 184 | end 185 | 186 | it 'should move down the call stack two frames at a time' do 187 | def bingbong.bang() Pry.start(binding, :initial_frame => 2) end 188 | 189 | redirect_pry_io(InputTester.new("@methods << __method__", 190 | "down 2", 191 | "@methods << __method__", 192 | "exit-all"), out=StringIO.new) do 193 | bingbong.bing 194 | end 195 | 196 | expect(bingbong.methods).to eq [:bing, :bang] 197 | end 198 | 199 | describe "by method name regex" do 200 | it 'should move to the method name that matches the regex' do 201 | redirect_pry_io(InputTester.new("frame -1", 202 | "down bo", 203 | "@methods << __method__", 204 | "exit-all"), out=StringIO.new) do 205 | bingbong.bing 206 | end 207 | 208 | expect(bingbong.methods[0]).to eq(:bong) 209 | end 210 | 211 | it 'should move through all methods that match regex in order' do 212 | redirect_pry_io(InputTester.new("frame bing", 213 | "@methods << __method__", 214 | "down b", 215 | "@methods << __method__", 216 | "down b", 217 | "@methods << __method__", 218 | "exit-all"), out=StringIO.new) do 219 | bingbong.bing 220 | end 221 | 222 | expect(bingbong.methods).to eq [:bing, :bong, :bang] 223 | end 224 | 225 | it 'should error if it cant find frame to match regex' do 226 | redirect_pry_io(InputTester.new("frame -1", 227 | "down conrad_irwin", 228 | "exit-all"), out=StringIO.new) do 229 | bingbong.bing 230 | end 231 | 232 | expect(out.string).to match(/Error: No frame that matches/) 233 | end 234 | end 235 | 236 | describe 'by Class#method name regex' do 237 | it 'should move to the method and class that matches the regex' do 238 | redirect_pry_io(InputTester.new('frame Top#bing', 239 | "@method_list << self.class.to_s + '#' + __method__.to_s", 240 | 'down Middle#bong', 241 | "@method_list << self.class.to_s + '#' + __method__.to_s", 242 | "exit-all"), out=StringIO.new) do 243 | @top.bing 244 | end 245 | 246 | expect(@top.method_list).to eq(['Top#bing', 'Middle#bong']) 247 | end 248 | 249 | ### ????? ### 250 | # it 'should be case sensitive' do 251 | # end 252 | ### ????? ### 253 | 254 | it 'should error if it cant find frame to match regex' do 255 | redirect_pry_io(InputTester.new('down Conrad#irwin', 256 | "exit-all"), out=StringIO.new) do 257 | @top.bing 258 | end 259 | 260 | expect(out.string).to match(/Error: No frame that matches/) 261 | end 262 | end 263 | 264 | end 265 | 266 | describe "frame" do 267 | describe "by method name regex" do 268 | it 'should jump to correct stack frame when given method name' do 269 | redirect_pry_io(InputTester.new("frame bi", 270 | "@methods << __method__", 271 | "exit-all"), out=StringIO.new) do 272 | bingbong.bing 273 | end 274 | 275 | expect(bingbong.methods[0]).to eq(:bing) 276 | end 277 | 278 | it 'should NOT jump to frames lower down stack when given method name' do 279 | redirect_pry_io(InputTester.new("frame -1", 280 | "frame bang", 281 | "exit-all"), out=StringIO.new) do 282 | bingbong.bing 283 | end 284 | 285 | expect(out.string).to match(/Error: No frame that matches/) 286 | end 287 | 288 | end 289 | 290 | it 'should move to the given frame in the call stack' do 291 | redirect_pry_io(InputTester.new("frame 2", 292 | "@methods << __method__", 293 | "exit-all"), out=StringIO.new) do 294 | bingbong.bing 295 | end 296 | 297 | expect(bingbong.methods[0]).to eq(:bing) 298 | end 299 | 300 | it 'should return info on current frame when given no parameters' do 301 | redirect_pry_io(InputTester.new("frame", 302 | "exit-all"), out=StringIO.new) do 303 | bingbong.bing 304 | end 305 | 306 | expect(out.string).to match(/\#0.*?bang/) 307 | expect(out.string).not_to match(/\#1/) 308 | end 309 | 310 | describe "negative indices" do 311 | class AlphaBetaGamma 312 | attr_accessor :frame, :frame_number 313 | 314 | def alpha; binding; end 315 | def beta; binding; end 316 | def gamma; binding; end 317 | end 318 | 319 | let(:alphabetagamma){ AlphaBetaGamma.new } 320 | 321 | it 'should work with negative frame numbers' do 322 | o = AlphaBetaGamma.new 323 | 324 | call_stack = [o.alpha, o.beta, o.gamma] 325 | method_names = call_stack.map { |v| v.eval('__method__') }.reverse 326 | (1..3).each_with_index do |v, idx| 327 | redirect_pry_io(InputTester.new("frame -#{v}", 328 | "@frame = __method__", 329 | "exit-all"), out=StringIO.new) do 330 | Pry.start(o, :call_stack => call_stack) 331 | end 332 | expect(o.frame).to eq(method_names[idx]) 333 | end 334 | end 335 | 336 | it 'should convert negative indices to their positive counterparts' do 337 | o = AlphaBetaGamma.new 338 | 339 | call_stack = [o.alpha, o.beta, o.gamma] 340 | 341 | (1..3).each_with_index do |v, idx| 342 | issue_pry_commands( 343 | "frame -#{v}", 344 | "@frame_number = PryStackExplorer.frame_manager(pry_instance).binding_index", 345 | "exit-all" 346 | ){ Pry.start(o, call_stack: call_stack) } 347 | 348 | expect(o.frame_number).to eq(call_stack.size - v) 349 | end 350 | end 351 | end 352 | end 353 | end 354 | -------------------------------------------------------------------------------- /lib/pry-stack_explorer/commands.rb: -------------------------------------------------------------------------------- 1 | module PryStackExplorer 2 | module FrameHelpers 3 | private 4 | 5 | # @return [PryStackExplorer::FrameManager] The active frame manager for 6 | # the current `Pry` instance. 7 | def frame_manager 8 | PryStackExplorer.frame_manager(pry_instance) 9 | end 10 | 11 | # @return [Array] All the frame 12 | # managers for the current `Pry` instance. 13 | def frame_managers 14 | PryStackExplorer.frame_managers(pry_instance) 15 | end 16 | 17 | # @return [Boolean] Whether there is a context to return to once 18 | # the current `frame_manager` is popped. 19 | def prior_context_exists? 20 | frame_managers.count > 1 || frame_manager.prior_binding 21 | end 22 | 23 | # Return a description of the frame (binding). 24 | # This is only useful for regular old bindings that have not been 25 | # enhanced by `#of_caller`. 26 | # @param [Binding] b The binding. 27 | # @return [String] A description of the frame (binding). 28 | def frame_description(b) 29 | b_self = b.eval('self') 30 | b_method = b.eval('__method__') 31 | 32 | if b_method && b_method != :__binding__ && b_method != :__binding_impl__ 33 | b_method.to_s 34 | elsif b_self.instance_of?(Module) 35 | "" 36 | elsif b_self.instance_of?(Class) 37 | "" 38 | else 39 | "
" 40 | end 41 | end 42 | 43 | # Return a description of the passed binding object. Accepts an 44 | # optional `verbose` parameter. 45 | # @param [Binding] b The binding. 46 | # @param [Boolean] verbose Whether to generate a verbose description. 47 | # @return [String] The description of the binding. 48 | def frame_info(b, verbose = false) 49 | meth = b.eval('__method__') 50 | b_self = b.eval('self') 51 | meth_obj = Pry::Method.from_binding(b) if meth 52 | 53 | type = b.frame_type ? "[#{b.frame_type}]".ljust(9) : "" 54 | desc = b.frame_description ? "#{b.frame_description}" : "#{frame_description(b)}" 55 | sig = meth_obj ? "<#{signature_with_owner(meth_obj)}>" : "" 56 | 57 | self_clipped = "#{Pry.view_clip(b_self)}" 58 | path = '@ ' + b.source_location.join(':') 59 | 60 | if !verbose 61 | "#{type} #{desc} #{sig}" 62 | else 63 | "#{type} #{desc} #{sig}\n in #{self_clipped} #{path}" 64 | end 65 | end 66 | 67 | # @param [Pry::Method] meth_obj The method object. 68 | # @return [String] Signature for the method object in Class#method format. 69 | def signature_with_owner(meth_obj) 70 | if !meth_obj.undefined? 71 | args = meth_obj.parameters.inject([]) do |arr, (type, name)| 72 | name ||= (type == :block ? 'block' : "arg#{arr.size + 1}") 73 | arr << case type 74 | when :req then name.to_s 75 | when :opt then "#{name}=?" 76 | when :rest then "*#{name}" 77 | when :block then "&#{name}" 78 | else '?' 79 | end 80 | end 81 | "#{meth_obj.name_with_owner}(#{args.join(', ')})" 82 | else 83 | "#{meth_obj.name_with_owner}(UNKNOWN) (undefined method)" 84 | end 85 | end 86 | 87 | # Regexp.new(args[0]) 88 | def find_frame_by_regex(regex, up_or_down) 89 | frame_index = find_frame_by_block(up_or_down) do |b| 90 | b.eval("__method__").to_s =~ regex 91 | end 92 | 93 | if frame_index 94 | frame_index 95 | else 96 | raise Pry::CommandError, "No frame that matches #{regex.source} found!" 97 | end 98 | end 99 | 100 | def find_frame_by_object_regex(class_regex, method_regex, up_or_down) 101 | frame_index = find_frame_by_block(up_or_down) do |b| 102 | class_match = b.eval("self.class").to_s =~ class_regex 103 | meth_match = b.eval("__method__").to_s =~ method_regex 104 | 105 | class_match && meth_match 106 | end 107 | 108 | if frame_index 109 | frame_index 110 | else 111 | raise Pry::CommandError, "No frame that matches #{class_regex.source}" + '#' + "#{method_regex.source} found!" 112 | end 113 | end 114 | 115 | def find_frame_by_block(up_or_down) 116 | start_index = frame_manager.binding_index 117 | 118 | if up_or_down == :down 119 | enum = frame_manager.bindings[0..start_index - 1].reverse_each 120 | else 121 | enum = frame_manager.bindings[start_index + 1..-1] 122 | end 123 | 124 | new_frame = enum.find do |b| 125 | yield(b) 126 | end 127 | 128 | frame_manager.bindings.index(new_frame) 129 | end 130 | end 131 | 132 | 133 | Commands = Pry::CommandSet.new do 134 | create_command "up", "Go up to the caller's context." do 135 | include FrameHelpers 136 | 137 | banner <<-BANNER 138 | Usage: up [OPTIONS] 139 | Go up to the caller's context. Accepts optional numeric parameter for how many frames to move up. 140 | Also accepts a string (regex) instead of numeric; for jumping to nearest parent method frame which matches the regex. 141 | e.g: up #=> Move up 1 stack frame. 142 | e.g: up 3 #=> Move up 2 stack frames. 143 | e.g: up meth #=> Jump to nearest parent stack frame whose method matches /meth/ regex, i.e `my_method`. 144 | BANNER 145 | 146 | def process 147 | inc = args.first.nil? ? "1" : args.first 148 | 149 | if !frame_manager 150 | raise Pry::CommandError, "Nowhere to go!" 151 | else 152 | if inc =~ /\d+/ 153 | frame_manager.change_frame_to frame_manager.binding_index + inc.to_i 154 | elsif match = /^([A-Z]+[^#.]*)(#|\.)(.+)$/.match(inc) 155 | new_frame_index = find_frame_by_object_regex(Regexp.new(match[1]), Regexp.new(match[3]), :up) 156 | frame_manager.change_frame_to new_frame_index 157 | elsif inc =~ /^[^-].*$/ 158 | new_frame_index = find_frame_by_regex(Regexp.new(inc), :up) 159 | frame_manager.change_frame_to new_frame_index 160 | end 161 | end 162 | end 163 | end 164 | 165 | create_command "down", "Go down to the callee's context." do 166 | include FrameHelpers 167 | 168 | banner <<-BANNER 169 | Usage: down [OPTIONS] 170 | Go down to the callee's context. Accepts optional numeric parameter for how many frames to move down. 171 | Also accepts a string (regex) instead of numeric; for jumping to nearest child method frame which matches the regex. 172 | e.g: down #=> Move down 1 stack frame. 173 | e.g: down 3 #=> Move down 2 stack frames. 174 | e.g: down meth #=> Jump to nearest child stack frame whose method matches /meth/ regex, i.e `my_method`. 175 | BANNER 176 | 177 | def process 178 | inc = args.first.nil? ? "1" : args.first 179 | 180 | if !frame_manager 181 | raise Pry::CommandError, "Nowhere to go!" 182 | else 183 | if inc =~ /\d+/ 184 | if frame_manager.binding_index - inc.to_i < 0 185 | raise Pry::CommandError, "At bottom of stack, cannot go further!" 186 | else 187 | frame_manager.change_frame_to frame_manager.binding_index - inc.to_i 188 | end 189 | elsif match = /^([A-Z]+[^#.]*)(#|\.)(.+)$/.match(inc) 190 | new_frame_index = find_frame_by_object_regex(Regexp.new(match[1]), Regexp.new(match[3]), :down) 191 | frame_manager.change_frame_to new_frame_index 192 | elsif inc =~ /^[^-].*$/ 193 | new_frame_index = find_frame_by_regex(Regexp.new(inc), :down) 194 | frame_manager.change_frame_to new_frame_index 195 | end 196 | end 197 | end 198 | end 199 | 200 | create_command "frame", "Switch to a particular frame." do 201 | include FrameHelpers 202 | 203 | banner <<-BANNER 204 | Usage: frame [OPTIONS] 205 | Switch to a particular frame. Accepts numeric parameter (or regex for method name) for the target frame to switch to (use with show-stack). 206 | Negative frame numbers allowed. When given no parameter show information about the current frame. 207 | 208 | e.g: frame 4 #=> jump to the 4th frame 209 | e.g: frame meth #=> jump to nearest parent stack frame whose method matches /meth/ regex, i.e `my_method` 210 | e.g: frame -2 #=> jump to the second-to-last frame 211 | e.g: frame #=> show information info about current frame 212 | BANNER 213 | 214 | def process 215 | if !frame_manager 216 | raise Pry::CommandError, "nowhere to go!" 217 | else 218 | 219 | if args[0] =~ /\d+/ 220 | frame_manager.change_frame_to args[0].to_i 221 | elsif match = /^([A-Z]+[^#.]*)(#|\.)(.+)$/.match(args[0]) 222 | new_frame_index = find_frame_by_object_regex(Regexp.new(match[1]), Regexp.new(match[3]), :up) 223 | frame_manager.change_frame_to new_frame_index 224 | elsif args[0] =~ /^[^-].*$/ 225 | new_frame_index = find_frame_by_regex(Regexp.new(args[0]), :up) 226 | frame_manager.change_frame_to new_frame_index 227 | else 228 | output.puts "##{frame_manager.binding_index} #{frame_info(target, true)}" 229 | end 230 | end 231 | end 232 | end 233 | 234 | create_command "stack", "Show all frames" do 235 | include FrameHelpers 236 | 237 | banner <<-BANNER 238 | Usage: stack [OPTIONS] 239 | Show all accessible stack frames. 240 | e.g: stack -v 241 | 242 | alias: show-stack 243 | BANNER 244 | 245 | def options(opt) 246 | opt.on :v, :verbose, "Include extra information." 247 | opt.on :H, :head, "Display the first N stack frames (defaults to 10).", :optional_argument => true, :as => Integer, :default => 10 248 | opt.on :T, :tail, "Display the last N stack frames (defaults to 10).", :optional_argument => true, :as => Integer, :default => 10 249 | opt.on :c, :current, "Display N frames either side of current frame (default to 5).", :optional_argument => true, :as => Integer, :default => 5 250 | opt.on :a, :app, "Display application frames only", optional_argument: true 251 | end 252 | 253 | def memoized_info(index, b, verbose) 254 | frame_manager.user[:frame_info] ||= Hash.new { |h, k| h[k] = [] } 255 | 256 | if verbose 257 | frame_manager.user[:frame_info][:v][index] ||= frame_info(b, verbose) 258 | else 259 | frame_manager.user[:frame_info][:normal][index] ||= frame_info(b, verbose) 260 | end 261 | end 262 | 263 | private :memoized_info 264 | 265 | # @return [Array>] Return tuple of 266 | # base_frame_index and the array of frames. 267 | def selected_stack_frames 268 | if opts.present?(:head) 269 | [0, frame_manager.bindings[0..(opts[:head] - 1)]] 270 | 271 | elsif opts.present?(:tail) 272 | tail = opts[:tail] 273 | if tail > frame_manager.bindings.size 274 | tail = frame_manager.bindings.size 275 | end 276 | 277 | base_frame_index = frame_manager.bindings.size - tail 278 | [base_frame_index, frame_manager.bindings[base_frame_index..-1]] 279 | 280 | elsif opts.present?(:current) 281 | first_frame_index = frame_manager.binding_index - (opts[:current]) 282 | first_frame_index = 0 if first_frame_index < 0 283 | last_frame_index = frame_manager.binding_index + (opts[:current]) 284 | [first_frame_index, frame_manager.bindings[first_frame_index..last_frame_index]] 285 | 286 | else 287 | [0, frame_manager.bindings] 288 | end 289 | end 290 | 291 | private :selected_stack_frames 292 | 293 | def process 294 | return no_stack_available! unless frame_manager 295 | 296 | title = "Showing all accessible frames in stack (#{frame_manager.bindings.size} in total):" 297 | 298 | content = [ 299 | bold(title), 300 | "---", 301 | make_stack_lines 302 | ].join("\n") 303 | 304 | stagger_output content 305 | end 306 | 307 | private 308 | 309 | def make_stack_lines 310 | frames_with_indices.map do |b, i| 311 | make_stack_line(b, i, (i == frame_manager.binding_index)) 312 | end.join("\n") 313 | end 314 | 315 | def frames_with_indices 316 | if opts.present?(:app) && defined?(ActiveSupport::BacktraceCleaner) 317 | app_frames 318 | else 319 | offset_frames 320 | end 321 | end 322 | 323 | # "=> #0 method_name " 324 | def make_stack_line(b, i, active) 325 | arw = active ? "=>" : " " 326 | 327 | "#{arw} ##{i} #{memoized_info(i, b, opts[:v])}" 328 | end 329 | 330 | def offset_frames 331 | base_frame_index, frames = selected_stack_frames 332 | 333 | frames.each_with_index.map do |frame, index| 334 | [frame, index + base_frame_index] 335 | end 336 | end 337 | 338 | def no_stack_available! 339 | output.puts "No caller stack available!" 340 | end 341 | 342 | LOCATION_LAMBDA = ->(_binding){ _binding.source_location[0] } 343 | 344 | def app_frames 345 | locations = frame_manager.bindings.map(&LOCATION_LAMBDA) 346 | filtered = backtrace_cleaner.clean(locations) 347 | 348 | frame_manager.bindings 349 | .each_with_index 350 | .map 351 | .select do |_binding, _index| 352 | LOCATION_LAMBDA.call(_binding).in?(filtered) 353 | end 354 | end 355 | 356 | # also see Rails::BacktraceCleaner 357 | def backtrace_cleaner 358 | @backtrace_cleaner ||= ActiveSupport::BacktraceCleaner.new 359 | end 360 | end 361 | 362 | alias_command "show-stack", "stack" 363 | 364 | end 365 | end 366 | -------------------------------------------------------------------------------- /test/stack_explorer_test.rb: -------------------------------------------------------------------------------- 1 | require_relative 'test_helper' 2 | 3 | describe PryStackExplorer do 4 | 5 | describe "Pry.start" do 6 | include ResetHelper 7 | 8 | let(:bingbong){ BingBong.new } 9 | 10 | describe ":initial_frame option" do 11 | it 'should default to first frame when no option provided' do 12 | redirect_pry_io(StringIO.new("@frame = __method__\nexit\n"), out=StringIO.new) do 13 | bingbong.bing 14 | end 15 | 16 | expect(bingbong.frame).to eq(:bang) 17 | end 18 | 19 | it 'should begin at correct frame even if Pry.start is monkey-patched (only works with one monkey-patch currently)' do 20 | class << Pry 21 | alias_method :old_start, :start 22 | 23 | def start(*args, &block) 24 | old_start(*args, &block) 25 | end 26 | end 27 | 28 | o = BingBong.new 29 | 30 | redirect_pry_io( 31 | InputTester.new( 32 | "@frames = SE.frame_manager(pry_instance).bindings.take(3)", 33 | "exit-all" 34 | ) 35 | ){ o.bing } 36 | 37 | expect( 38 | o.frames.map { |f| f.eval("__method__") } 39 | ).to eq([:bang, :bong, :bing]) 40 | 41 | class << Pry 42 | alias_method :start, :old_start 43 | end 44 | end 45 | 46 | it 'should begin session at specified frame' do 47 | o = bingbong 48 | def o.bang; Pry.start(binding, :initial_frame => 1); end 49 | 50 | redirect_pry_io(StringIO.new("@frame = __method__\nexit-all\n"), out=StringIO.new) do 51 | o.bing 52 | end 53 | 54 | expect(o.frame).to eq(:bong) 55 | end 56 | 57 | it 'should begin session at specified frame when using :call_stack' do 58 | o = Object.new 59 | class << o; attr_accessor :frame; end 60 | def o.alpha() binding end 61 | def o.beta() binding end 62 | def o.gamma() binding end 63 | 64 | redirect_pry_io(StringIO.new("@frame = __method__\nexit\n"), out=StringIO.new) do 65 | Pry.start(binding, :call_stack => [o.gamma, o.beta, o.alpha], :initial_frame => 1) 66 | end 67 | 68 | expect(o.frame).to eq(:beta) 69 | end 70 | 71 | # regression test for #12 72 | it 'does not infinite loop when pry is started in MyObject#==' do 73 | o = Object.new 74 | def o.==(other) 75 | binding.pry 76 | end 77 | 78 | redirect_pry_io(InputTester.new(":hello", "exit-all"), out=StringIO.new) do 79 | o.==(1) 80 | end 81 | 82 | expect(out.string).to match(/hello/) 83 | end 84 | end 85 | 86 | describe ":call_stack option" do 87 | it 'should invoke a session with the call stack set' do 88 | redirect_pry_io(StringIO.new("stack\nexit\n"), out=StringIO.new) do 89 | bingbong.bing 90 | end 91 | 92 | expect(out.string).to match(/bang.*?bong.*?bing/m) 93 | end 94 | 95 | it 'should set no call stack when :call_stack => false' do 96 | def bingbong.bang; Pry.start(binding, :call_stack => false); end 97 | 98 | redirect_pry_io(StringIO.new("stack\nexit\n"), out=StringIO.new) do 99 | bingbong.bing 100 | end 101 | 102 | expect(out.string).to match(/No caller stack/) 103 | end 104 | 105 | it 'should set custom call stack when :call_stack => [b1, b2]' do 106 | o = Object.new 107 | def o.alpha() binding end 108 | def o.beta() binding end 109 | def o.gamma() binding end 110 | 111 | redirect_pry_io(StringIO.new("stack\nexit\n"), out=StringIO.new) do 112 | Pry.start(binding, :call_stack => [o.beta, o.gamma, o.alpha]) 113 | end 114 | 115 | expect(out.string).to match(/beta.*?gamma.*?alpha/m) 116 | end 117 | 118 | it 'should raise if custom call stack does not contain bindings' do 119 | o = OpenStruct.new 120 | redirect_pry_io(StringIO.new("self.errors = pry_instance.hooks.errors\nexit\n")) do 121 | Pry.start(o, :call_stack => [1, 2, 3]) 122 | end 123 | expect(o.errors.first.is_a?(ArgumentError)).to eq(true) 124 | end 125 | 126 | it 'should raise if custom call stack is empty' do 127 | o = OpenStruct.new 128 | redirect_pry_io(StringIO.new("self.errors = pry_instance.hooks.errors\nexit\n")) do 129 | Pry.start o, :call_stack => [] 130 | end 131 | expect(o.errors.first.is_a?(ArgumentError)).to eq(true) 132 | end 133 | end 134 | end 135 | 136 | describe "class methods" do 137 | before do 138 | @pry_instance = Pry.new 139 | @bindings = [binding, binding] 140 | end 141 | 142 | after do 143 | PE.clear_frame_managers(@pry_instance) 144 | end 145 | 146 | describe "PryStackExplorer.create_and_push_frame_manager" do 147 | 148 | it "should create and push one new FrameManager" do 149 | PE.create_and_push_frame_manager(@bindings, @pry_instance) 150 | expect(PE.frame_manager(@pry_instance).is_a?(PE::FrameManager)).to eq(true) 151 | expect(PE.frame_managers(@pry_instance).count).to eq(1) 152 | end 153 | 154 | it "should refresh Pry instance to use FrameManager's active binding" do 155 | PE.create_and_push_frame_manager(@bindings, @pry_instance) 156 | expect(@pry_instance.binding_stack.size).to eq(1) 157 | expect(@pry_instance.binding_stack.first).to eq(@bindings.first) 158 | end 159 | 160 | it 'should save prior binding in FrameManager instance' do 161 | _pry_ = Pry.new 162 | _pry_.binding_stack.push(b=binding) 163 | PryStackExplorer.create_and_push_frame_manager(@bindings, _pry_) 164 | expect(PryStackExplorer.frame_manager(_pry_).prior_binding).to eq(b) 165 | end 166 | 167 | describe ":initial_frame option" do 168 | it 'should start on specified frame' do 169 | PE.create_and_push_frame_manager(@bindings, @pry_instance, :initial_frame => 1) 170 | expect(@pry_instance.binding_stack.size).to eq(1) 171 | expect(@pry_instance.binding_stack.first).to eq(@bindings.last) 172 | end 173 | 174 | describe "negative numbers" do 175 | it 'should work with negative frame number (-1)' do 176 | PE.create_and_push_frame_manager(@bindings, @pry_instance, :initial_frame => -1) 177 | expect(@pry_instance.binding_stack.size).to eq(1) 178 | expect(@pry_instance.binding_stack.first).to eq(@bindings.last) 179 | end 180 | 181 | it 'should work with negative frame number (-2)' do 182 | PE.create_and_push_frame_manager(@bindings, @pry_instance, :initial_frame => -2) 183 | expect(@pry_instance.binding_stack.size).to eq(1) 184 | expect(@pry_instance.binding_stack.first).to eq(@bindings.first) 185 | end 186 | end 187 | end 188 | 189 | it 'should save prior backtrace in FrameManager instance' do 190 | _pry_ = Pry.new 191 | _pry_.backtrace = ["my backtrace"] 192 | PryStackExplorer.create_and_push_frame_manager(@bindings, _pry_) 193 | expect(PryStackExplorer.frame_manager(_pry_).prior_backtrace).to eq(_pry_.backtrace) 194 | end 195 | 196 | it "should create and push multiple FrameManagers" do 197 | PE.create_and_push_frame_manager(@bindings, @pry_instance) 198 | PE.create_and_push_frame_manager(@bindings, @pry_instance) 199 | expect(PE.frame_managers(@pry_instance).count).to eq(2) 200 | end 201 | 202 | it 'should push FrameManagers to stacks based on Pry instance' do 203 | p2 = Pry.new 204 | bindings = [binding, binding] 205 | PE.create_and_push_frame_manager(@bindings, @pry_instance) 206 | PE.create_and_push_frame_manager(bindings, p2) 207 | expect(PE.frame_managers(@pry_instance).count).to eq(1) 208 | expect(PE.frame_managers(p2).count).to eq(1) 209 | end 210 | end 211 | 212 | describe "PryStackExplorer.frame_manager" do 213 | it "should have the correct bindings" do 214 | PE.create_and_push_frame_manager(@bindings, @pry_instance) 215 | expect(PE.frame_manager(@pry_instance).bindings).to eq(@bindings) 216 | end 217 | 218 | it "should return the last pushed FrameManager" do 219 | bindings = [binding, binding] 220 | PE.create_and_push_frame_manager(@bindings, @pry_instance) 221 | PE.create_and_push_frame_manager(bindings, @pry_instance) 222 | expect(PE.frame_manager(@pry_instance).bindings).to eq(bindings) 223 | end 224 | 225 | it "should return the correct FrameManager for the given Pry instance" do 226 | bindings = [binding, binding] 227 | p2 = Pry.new 228 | PE.create_and_push_frame_manager(@bindings, @pry_instance) 229 | PE.create_and_push_frame_manager(bindings, p2) 230 | expect(PE.frame_manager(@pry_instance).bindings).to eq(@bindings) 231 | expect(PE.frame_manager(p2).bindings).to eq(bindings) 232 | end 233 | end 234 | 235 | describe "PryStackExplorer.pop_frame_manager" do 236 | it "should remove FrameManager from stack" do 237 | PE.create_and_push_frame_manager(@bindings, @pry_instance) 238 | PE.create_and_push_frame_manager(@bindings, @pry_instance) 239 | PE.pop_frame_manager(@pry_instance) 240 | expect(PE.frame_managers(@pry_instance).count).to eq(1) 241 | end 242 | 243 | it "should return the most recently added FrameManager" do 244 | bindings = [binding, binding] 245 | PE.create_and_push_frame_manager(@bindings, @pry_instance) 246 | PE.create_and_push_frame_manager(bindings, @pry_instance) 247 | expect(PE.pop_frame_manager(@pry_instance).bindings).to eq(bindings) 248 | end 249 | 250 | it "should remove FrameManager from the appropriate stack based on Pry instance" do 251 | p2 = Pry.new 252 | bindings = [binding, binding] 253 | PE.create_and_push_frame_manager(@bindings, @pry_instance) 254 | PE.create_and_push_frame_manager(bindings, p2) 255 | PE.pop_frame_manager(@pry_instance) 256 | expect(PE.frame_managers(@pry_instance).count).to eq(0) 257 | expect(PE.frame_managers(p2).count).to eq(1) 258 | end 259 | 260 | it "should remove key when no frames remaining for Pry instance" do 261 | PE.create_and_push_frame_manager(@bindings, @pry_instance) 262 | PE.create_and_push_frame_manager(@bindings, @pry_instance) 263 | PE.pop_frame_manager(@pry_instance) 264 | PE.pop_frame_manager(@pry_instance) 265 | expect(PE.frame_hash.has_key?(@pry_instance)).to eq(false) 266 | end 267 | 268 | it 'should not change size of binding_stack when popping' do 269 | bindings = [bindings, bindings] 270 | PE.create_and_push_frame_manager(bindings, @pry_instance) 271 | PE.create_and_push_frame_manager(@bindings, @pry_instance) 272 | PE.pop_frame_manager(@pry_instance) 273 | expect(@pry_instance.binding_stack.size).to eq(1) 274 | end 275 | 276 | it 'should return nil when popping non-existent frame manager' do 277 | expect(PE.pop_frame_manager(@pry_instance)).to eq(nil) 278 | end 279 | 280 | describe "restoring previous binding" do 281 | it 'should restore previous binding for Pry instance on pop, where previous binding is not first frame' do 282 | bindings = [binding, binding] 283 | PE.create_and_push_frame_manager(bindings, @pry_instance).binding_index = 1 284 | PE.create_and_push_frame_manager(@bindings, @pry_instance) 285 | PE.pop_frame_manager(@pry_instance) 286 | expect(@pry_instance.binding_stack.first).to eq(bindings[1]) 287 | end 288 | 289 | it 'should restore previous binding for Pry instance on pop (previous frame frame manager)' do 290 | bindings = [binding, binding] 291 | PE.create_and_push_frame_manager(bindings, @pry_instance) 292 | PE.create_and_push_frame_manager(@bindings, @pry_instance) 293 | PE.pop_frame_manager(@pry_instance) 294 | expect(@pry_instance.binding_stack.first).to eq(bindings.first) 295 | end 296 | 297 | it 'should restore previous binding for Pry instance on pop (no previous frame manager)' do 298 | b = binding 299 | @pry_instance.binding_stack = [b] 300 | PE.create_and_push_frame_manager(@bindings, @pry_instance) 301 | PE.pop_frame_manager(@pry_instance) 302 | expect(@pry_instance.binding_stack.first).to eq(b) 303 | end 304 | 305 | it 'should restore previous binding for Pry instance on pop (no previous frame manager AND no empty binding_stack)' do 306 | b = binding 307 | @pry_instance.binding_stack = [b] 308 | PE.create_and_push_frame_manager(@bindings, @pry_instance) 309 | @pry_instance.binding_stack.clear 310 | PE.pop_frame_manager(@pry_instance) 311 | expect(@pry_instance.binding_stack.first).to eq(b) 312 | end 313 | end 314 | 315 | describe "_pry_.backtrace" do 316 | it "should restore backtrace when frame is popped" do 317 | p1 = Pry.new 318 | bindings = [binding, binding] 319 | p1.backtrace = "my backtrace1" 320 | PE.create_and_push_frame_manager(bindings, p1) 321 | p1.backtrace = "my backtrace2" 322 | PE.create_and_push_frame_manager(bindings, p1) 323 | p1.backtrace = "my backtrace3" 324 | 325 | PE.pop_frame_manager(p1) 326 | expect(p1.backtrace).to eq("my backtrace2") 327 | PE.pop_frame_manager(p1) 328 | expect(p1.backtrace).to eq("my backtrace1") 329 | end 330 | end 331 | end 332 | 333 | describe "PryStackExplorer.clear_frame_managers" do 334 | it "should clear all FrameManagers for a Pry instance" do 335 | PE.create_and_push_frame_manager(@bindings, @pry_instance) 336 | PE.create_and_push_frame_manager(@bindings, @pry_instance) 337 | PE.clear_frame_managers(@pry_instance) 338 | expect(PE.frame_hash.has_key?(@pry_instance)).to eq(false) 339 | end 340 | 341 | it "should clear all FrameManagers for a Pry instance but leave others untouched" do 342 | p2 = Pry.new 343 | bindings = [binding, binding] 344 | PE.create_and_push_frame_manager(@bindings, @pry_instance) 345 | PE.create_and_push_frame_manager(bindings, p2) 346 | PE.clear_frame_managers(@pry_instance) 347 | expect(PE.frame_managers(p2).count).to eq(1) 348 | expect(PE.frame_hash.has_key?(@pry_instance)).to eq(false) 349 | end 350 | 351 | it "should remove key" do 352 | PE.create_and_push_frame_manager(@bindings, @pry_instance) 353 | PE.create_and_push_frame_manager(@bindings, @pry_instance) 354 | PE.clear_frame_managers(@pry_instance) 355 | expect(PE.frame_hash.has_key?(@pry_instance)).to eq(false) 356 | end 357 | 358 | describe "_pry_.backtrace" do 359 | it "should restore backtrace to initial one when frame managers are cleared" do 360 | p1 = Pry.new 361 | bindings = [binding, binding] 362 | p1.backtrace = "my backtrace1" 363 | PE.create_and_push_frame_manager(bindings, p1) 364 | p1.backtrace = "my backtrace2" 365 | PE.create_and_push_frame_manager(bindings, p1) 366 | p1.backtrace = "my backtrace3" 367 | 368 | PE.clear_frame_managers(p1) 369 | expect(p1.backtrace).to eq("my backtrace1") 370 | end 371 | end 372 | end 373 | end 374 | end 375 | --------------------------------------------------------------------------------