pry-stack 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,85 @@
1
+ module PryStack
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<Binding>] 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
@@ -0,0 +1,94 @@
1
+ module PryStack
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 !PryStack.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
+ PryStack.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
+ start_frame_index = start_frames.first.last
51
+
52
+ if start_frames.size >= 2
53
+ # god knows what's going on in here
54
+ idx1, idx2 = start_frames.take(2).map(&:last)
55
+ start_frame_index = idx2 if !nested_session?(bindings[idx1..idx2])
56
+ end
57
+
58
+ bindings.drop(start_frame_index + 1)
59
+ end
60
+
61
+ # remove pry-nav / pry-debugger frames
62
+ def remove_debugger_frames(bindings)
63
+ bindings.drop_while { |b| b.eval("__FILE__") =~ /pry-(?:nav|debugger)/ }
64
+ end
65
+
66
+ # binding.pry frame
67
+ # @return [Boolean]
68
+ def pry_method_frame?(binding)
69
+ safe_send(binding.eval("__method__"), :==, :pry)
70
+ end
71
+
72
+ # When a pry session is started within a pry session
73
+ # @return [Boolean]
74
+ def nested_session?(bindings)
75
+ bindings.detect do |b|
76
+ safe_send(b.eval("__method__"), :==, :re) &&
77
+ safe_send(b.eval("self.class"), :equal?, Pry)
78
+ end
79
+ end
80
+
81
+ # @return [Array<Array<Binding, Fixnum>>]
82
+ def internal_frames_with_indices(bindings)
83
+ bindings.each_with_index.select do |b, i|
84
+ b.frame_type == :method &&
85
+ safe_send(b.eval("self"), :equal?, Pry) &&
86
+ safe_send(b.eval("__method__"), :==, :start)
87
+ end
88
+ end
89
+
90
+ def valid_call_stack?(bindings)
91
+ bindings.any? && bindings.all? { |v| v.is_a?(Binding) }
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,92 @@
1
+ require 'rubygems'
2
+ require 'ostruct'
3
+ require 'pry'
4
+
5
+ PRY_STACK_VERSION = File.read(File.expand_path '../../VERSION', __FILE__)
6
+
7
+ unless Object.const_defined? 'PryStack'
8
+ $:.unshift File.expand_path '../../lib', __FILE__
9
+ require 'pry-stack'
10
+ end
11
+
12
+ require 'bacon'
13
+
14
+ puts "---------------------------------------------------------"
15
+ puts "Testing"
16
+ puts "---------------------------------------------------------"
17
+ puts "pry-stack version: #{PRY_STACK_VERSION}"
18
+ puts "Ruby version: #{RUBY_VERSION}"
19
+
20
+ PE = PryStack
21
+
22
+ class << Pry
23
+ alias_method :orig_reset_defaults, :reset_defaults
24
+ def reset_defaults
25
+ orig_reset_defaults
26
+
27
+ Pry.color = false
28
+ Pry.pager = false
29
+ Pry.config.should_load_rc = false
30
+ Pry.config.should_load_plugins = false
31
+ Pry.config.history.should_load = false
32
+ Pry.config.history.should_save = false
33
+ Pry.config.auto_indent = false
34
+ Pry.config.hooks = Pry::Hooks.new
35
+ Pry.config.collision_warning = false
36
+ end
37
+ end
38
+
39
+ AfterSessionHook = Pry.config.hooks.get_hook(:after_session, :delete_frame_manager)
40
+ WhenStartedHook = Pry.config.hooks.get_hook(:when_started, :save_caller_bindings)
41
+
42
+ Pry.reset_defaults
43
+
44
+ class InputTester
45
+ def initialize(*actions)
46
+ if actions.last.is_a?(Hash) && actions.last.keys == [:history]
47
+ @hist = actions.pop[:history]
48
+ end
49
+ @orig_actions = actions.dup
50
+ @actions = actions
51
+ end
52
+
53
+ def readline(*)
54
+ @actions.shift.tap{ |line| @hist << line if @hist }
55
+ end
56
+
57
+ def rewind
58
+ @actions = @orig_actions.dup
59
+ end
60
+ end
61
+
62
+ # Set I/O streams.
63
+ #
64
+ # Out defaults to an anonymous StringIO.
65
+ #
66
+ def redirect_pry_io(new_in, new_out = StringIO.new)
67
+ old_in = Pry.input
68
+ old_out = Pry.output
69
+
70
+ Pry.input = new_in
71
+ Pry.output = new_out
72
+ begin
73
+ yield
74
+ ensure
75
+ Pry.input = old_in
76
+ Pry.output = old_out
77
+ end
78
+ end
79
+
80
+ def mock_pry(*args)
81
+
82
+ binding = args.first.is_a?(Binding) ? args.shift : binding()
83
+
84
+ input = InputTester.new(*args)
85
+ output = StringIO.new
86
+
87
+ redirect_pry_io(input, output) do
88
+ binding.pry
89
+ end
90
+
91
+ output.string
92
+ end
@@ -0,0 +1,358 @@
1
+ require 'helper'
2
+
3
+
4
+ class Top
5
+ attr_accessor :method_list, :middle
6
+ def initialize method_list
7
+ @method_list = method_list
8
+ end
9
+ def bing
10
+ @middle = Middle.new method_list
11
+ @middle.bong
12
+ end
13
+ end
14
+
15
+ class Middle
16
+ attr_accessor :method_list, :bottom
17
+ def initialize method_list
18
+ @method_list = method_list
19
+ end
20
+ def bong
21
+ @bottom = Bottom.new method_list
22
+ @bottom.bang
23
+ end
24
+ end
25
+
26
+ class Bottom
27
+ attr_accessor :method_list
28
+ def initialize method_list
29
+ @method_list = method_list
30
+ end
31
+ def bang
32
+ Pry.start(binding)
33
+ end
34
+ end
35
+
36
+
37
+ describe PryStack::Commands do
38
+
39
+ before do
40
+ Pry.config.hooks.add_hook(:when_started, :save_caller_bindings, WhenStartedHook)
41
+ Pry.config.hooks.add_hook(:after_session, :delete_frame_manager, AfterSessionHook)
42
+
43
+ @o = Object.new
44
+ class << @o; attr_accessor :first_method, :second_method, :third_method; end
45
+ def @o.bing() bong end
46
+ def @o.bong() bang end
47
+ def @o.bang() Pry.start(binding) end
48
+
49
+ method_list = []
50
+ @top = Top.new method_list
51
+ end
52
+
53
+ after do
54
+ Pry.config.hooks.delete_hook(:when_started, :save_caller_bindings)
55
+ Pry.config.hooks.delete_hook(:after_session, :delete_frame_manager)
56
+ end
57
+
58
+ describe "up" do
59
+ it 'should move up the call stack one frame at a time' do
60
+ redirect_pry_io(InputTester.new("@first_method = __method__",
61
+ "up",
62
+ "@second_method = __method__",
63
+ "up",
64
+ "@third_method = __method__",
65
+ "exit-all"), out=StringIO.new) do
66
+ @o.bing
67
+ end
68
+
69
+ @o.first_method.should == :bang
70
+ @o.second_method.should == :bong
71
+ @o.third_method.should == :bing
72
+ end
73
+
74
+ it 'should move up the call stack two frames at a time' do
75
+ redirect_pry_io(InputTester.new("@first_method = __method__",
76
+ "up 2",
77
+ "@second_method = __method__",
78
+ "exit-all"), out=StringIO.new) do
79
+ @o.bing
80
+ end
81
+
82
+ @o.first_method.should == :bang
83
+ @o.second_method.should == :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("@first_method = __method__",
89
+ "up bi",
90
+ "@second_method = __method__",
91
+ "exit-all"), out=StringIO.new) do
92
+ @o.bing
93
+ end
94
+
95
+ @o.first_method.should == :bang
96
+ @o.second_method.should == :bing
97
+ end
98
+
99
+ it 'should move through all methods that match regex in order' do
100
+ redirect_pry_io(InputTester.new("@first_method = __method__",
101
+ "up b",
102
+ "@second_method = __method__",
103
+ "up b",
104
+ "@third_method = __method__",
105
+ "exit-all"), out=StringIO.new) do
106
+ @o.bing
107
+ end
108
+
109
+ @o.first_method.should == :bang
110
+ @o.second_method.should == :bong
111
+ @o.third_method.should == :bing
112
+ end
113
+
114
+ it 'should error if it cant find frame to match regex' do
115
+ redirect_pry_io(InputTester.new("up conrad_irwin",
116
+ "exit-all"), out=StringIO.new) do
117
+ @o.bing
118
+ end
119
+
120
+ out.string.should =~ /Error: No frame that matches/
121
+ end
122
+ end
123
+
124
+
125
+ describe 'by Class#method name regex' do
126
+ it 'should move to the method and class that matches the regex' do
127
+ redirect_pry_io(InputTester.new("@method_list << self.class.to_s + '#' + __method__.to_s",
128
+ 'up Middle#bong',
129
+ "@method_list << self.class.to_s + '#' + __method__.to_s",
130
+ "exit-all"), out=StringIO.new) do
131
+ @top.bing
132
+ end
133
+
134
+ @top.method_list.should == ['Bottom#bang', 'Middle#bong']
135
+ end
136
+
137
+ ### ????? ###
138
+ # it 'should be case sensitive' do
139
+ # end
140
+ ### ????? ###
141
+
142
+ it 'should allow partial class names' do
143
+ redirect_pry_io(InputTester.new("@method_list << self.class.to_s + '#' + __method__.to_s",
144
+ 'up Mid#bong',
145
+ "@method_list << self.class.to_s + '#' + __method__.to_s",
146
+ "exit-all"), out=StringIO.new) do
147
+ @top.bing
148
+ end
149
+
150
+ @top.method_list.should == ['Bottom#bang', 'Middle#bong']
151
+
152
+ end
153
+
154
+ it 'should allow partial method names' do
155
+ redirect_pry_io(InputTester.new("@method_list << self.class.to_s + '#' + __method__.to_s",
156
+ 'up Middle#bo',
157
+ "@method_list << self.class.to_s + '#' + __method__.to_s",
158
+ "exit-all"), out=StringIO.new) do
159
+ @top.bing
160
+ end
161
+
162
+ @top.method_list.should == ['Bottom#bang', 'Middle#bong']
163
+
164
+ end
165
+
166
+ it 'should error if it cant find frame to match regex' do
167
+ redirect_pry_io(InputTester.new('up Conrad#irwin',
168
+ "exit-all"), out=StringIO.new) do
169
+ @top.bing
170
+ end
171
+
172
+ out.string.should =~ /Error: No frame that matches/
173
+ end
174
+ end
175
+ end
176
+
177
+ describe "down" do
178
+ it 'should move down the call stack one frame at a time' do
179
+ def @o.bang() Pry.start(binding, :initial_frame => 1) end
180
+
181
+ redirect_pry_io(InputTester.new("@first_method = __method__",
182
+ "down",
183
+ "@second_method = __method__",
184
+ "exit-all"), out=StringIO.new) do
185
+ @o.bing
186
+ end
187
+
188
+ @o.first_method.should == :bong
189
+ @o.second_method.should == :bang
190
+ end
191
+
192
+ it 'should move down the call stack two frames at a time' do
193
+ def @o.bang() Pry.start(binding, :initial_frame => 2) end
194
+
195
+ redirect_pry_io(InputTester.new("@first_method = __method__",
196
+ "down 2",
197
+ "@second_method = __method__",
198
+ "exit-all"), out=StringIO.new) do
199
+ @o.bing
200
+ end
201
+
202
+ @o.first_method.should == :bing
203
+ @o.second_method.should == :bang
204
+ end
205
+
206
+ describe "by method name regex" do
207
+ it 'should move to the method name that matches the regex' do
208
+ redirect_pry_io(InputTester.new("frame -1",
209
+ "down bo",
210
+ "@first_method = __method__",
211
+ "exit-all"), out=StringIO.new) do
212
+ @o.bing
213
+ end
214
+
215
+ @o.first_method.should == :bong
216
+ end
217
+
218
+ it 'should move through all methods that match regex in order' do
219
+ redirect_pry_io(InputTester.new("frame bing",
220
+ "@first_method = __method__",
221
+ "down b",
222
+ "@second_method = __method__",
223
+ "down b",
224
+ "@third_method = __method__",
225
+ "exit-all"), out=StringIO.new) do
226
+ @o.bing
227
+ end
228
+
229
+ @o.first_method.should == :bing
230
+ @o.second_method.should == :bong
231
+ @o.third_method.should == :bang
232
+ end
233
+
234
+ it 'should error if it cant find frame to match regex' do
235
+ redirect_pry_io(InputTester.new("frame -1",
236
+ "down conrad_irwin",
237
+ "exit-all"), out=StringIO.new) do
238
+ @o.bing
239
+ end
240
+
241
+ out.string.should =~ /Error: No frame that matches/
242
+ end
243
+ end
244
+
245
+ describe 'by Class#method name regex' do
246
+ it 'should move to the method and class that matches the regex' do
247
+ redirect_pry_io(InputTester.new('frame Top#bing',
248
+ "@method_list << self.class.to_s + '#' + __method__.to_s",
249
+ 'down Middle#bong',
250
+ "@method_list << self.class.to_s + '#' + __method__.to_s",
251
+ "exit-all"), out=StringIO.new) do
252
+ @top.bing
253
+ end
254
+
255
+ @top.method_list.should == ['Top#bing', 'Middle#bong']
256
+ end
257
+
258
+ ### ????? ###
259
+ # it 'should be case sensitive' do
260
+ # end
261
+ ### ????? ###
262
+
263
+ it 'should error if it cant find frame to match regex' do
264
+ redirect_pry_io(InputTester.new('down Conrad#irwin',
265
+ "exit-all"), out=StringIO.new) do
266
+ @top.bing
267
+ end
268
+
269
+ out.string.should =~ /Error: No frame that matches/
270
+ end
271
+ end
272
+
273
+ end
274
+
275
+ describe "frame" do
276
+ describe "by method name regex" do
277
+ it 'should jump to correct stack frame when given method name' do
278
+ redirect_pry_io(InputTester.new("frame bi",
279
+ "@first_method = __method__",
280
+ "exit-all"), out=StringIO.new) do
281
+ @o.bing
282
+ end
283
+
284
+ @o.first_method.should == :bing
285
+ end
286
+
287
+ it 'should NOT jump to frames lower down stack when given method name' do
288
+ redirect_pry_io(InputTester.new("frame -1",
289
+ "frame bang",
290
+ "exit-all"), out=StringIO.new) do
291
+ @o.bing
292
+ end
293
+
294
+ out.string.should =~ /Error: No frame that matches/
295
+ end
296
+
297
+ end
298
+
299
+ it 'should move to the given frame in the call stack' do
300
+ redirect_pry_io(InputTester.new("frame 2",
301
+ "@first_method = __method__",
302
+ "exit-all"), out=StringIO.new) do
303
+ @o.bing
304
+ end
305
+
306
+ @o.first_method.should == :bing
307
+ end
308
+
309
+ it 'should return info on current frame when given no parameters' do
310
+ redirect_pry_io(InputTester.new("frame",
311
+ "exit-all"), out=StringIO.new) do
312
+ @o.bing
313
+ end
314
+
315
+ out.string.should =~ /\#0.*?bang/
316
+ out.string.should.not =~ /\#1/
317
+ end
318
+
319
+ describe "negative indices" do
320
+ it 'should work with negative frame numbers' do
321
+ o = Object.new
322
+ class << o; attr_accessor :frame; end
323
+ def o.alpha() binding end
324
+ def o.beta() binding end
325
+ def o.gamma() binding end
326
+
327
+ call_stack = [o.alpha, o.beta, o.gamma]
328
+ method_names = call_stack.map { |v| v.eval('__method__') }.reverse
329
+ (1..3).each_with_index do |v, idx|
330
+ redirect_pry_io(InputTester.new("frame -#{v}",
331
+ "@frame = __method__",
332
+ "exit-all"), out=StringIO.new) do
333
+ Pry.start(o, :call_stack => call_stack)
334
+ end
335
+ o.frame.should == method_names[idx]
336
+ end
337
+ end
338
+
339
+ it 'should convert negative indices to their positive counterparts' do
340
+ o = Object.new
341
+ class << o; attr_accessor :frame_number; end
342
+ def o.alpha() binding end
343
+ def o.beta() binding end
344
+ def o.gamma() binding end
345
+
346
+ call_stack = [o.alpha, o.beta, o.gamma]
347
+ (1..3).each_with_index do |v, idx|
348
+ redirect_pry_io(InputTester.new("frame -#{v}",
349
+ "@frame_number = PryStack.frame_manager(_pry_).binding_index",
350
+ "exit-all"), out=StringIO.new) do
351
+ Pry.start(o, :call_stack => call_stack)
352
+ end
353
+ o.frame_number.should == call_stack.size - v
354
+ end
355
+ end
356
+ end
357
+ end
358
+ end