peeek 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,43 @@
1
+ class Peeek
2
+ class Calls < Array
3
+
4
+ # Filter the calls by name of a file.
5
+ #
6
+ # @param [String, Regexp] file name or pattern of a file
7
+ # @return [Peeek::Calls] filtered calls
8
+ def in(file)
9
+ Calls.new(select { |call| file === call.file })
10
+ end
11
+
12
+ # Filter the calls by line number.
13
+ #
14
+ # @param [Number, Range<Number>] line line number or range of lines
15
+ # @return [Peeek::Calls] filtered calls
16
+ def at(line)
17
+ Calls.new(select { |call| line === call.line })
18
+ end
19
+
20
+ # Filter the calls by a receiver.
21
+ #
22
+ # @param [Module, Class, Object] receiver
23
+ # @return [Peeek::Calls] filtered calls
24
+ def from(receiver)
25
+ Calls.new(select { |call| call.receiver == receiver })
26
+ end
27
+
28
+ # Filter only the calls that a value returned.
29
+ #
30
+ # @return [Peeek::Calls] filtered calls
31
+ def return_values
32
+ Calls.new(select(&:returned?))
33
+ end
34
+
35
+ # Filter only the calls that an exception raised.
36
+ #
37
+ # @return [Peeek::Calls] filtered calls
38
+ def exceptions
39
+ Calls.new(select(&:raised?))
40
+ end
41
+
42
+ end
43
+ end
data/lib/peeek/hook.rb ADDED
@@ -0,0 +1,188 @@
1
+ require 'peeek/hook/instance'
2
+ require 'peeek/hook/singleton'
3
+ require 'peeek/call'
4
+ require 'peeek/calls'
5
+
6
+ class Peeek
7
+ class Hook
8
+
9
+ # Create a hook to method of an object. The hook can apply to a instance
10
+ # method or a singleton method.
11
+ #
12
+ # @example Hook to an instance method
13
+ # Peeek::Hook.create(IO, '#puts')
14
+ # # => #<Peeek::Hook IO#puts>
15
+ #
16
+ # # Hook implicitly to the instance method if the object is a module or
17
+ # # a class.
18
+ # Peeek::Hook.create(IO, :puts)
19
+ # # => #<Peeek::Hook IO#puts>
20
+ #
21
+ # # Can't hook to the instance method if the object is an instance of any
22
+ # # class.
23
+ # Peeek::Hook.create($stdout, '#puts')
24
+ # # => raise #<ArgumentError: can't create a hook of instance method to an instance of any class>
25
+ #
26
+ # @example Hook to an singleton method
27
+ # Peeek::Hook.create($stdout, '.puts')
28
+ # # => #<Peeek::Hook #<IO:<STDOUT>>.puts>
29
+ #
30
+ # # hook implicitly to the singleton method if the object is an instance
31
+ # # of any class.
32
+ # Peeek::Hook.create($stdout, :puts)
33
+ # # => #<Peeek::Hook #<IO:<STDOUT>>.puts>
34
+ #
35
+ # @param [Module, Class, Object] object a target object that hook
36
+ # @param [String, Symbol] method_spec method specification of the object
37
+ # @yield [call] process a call to the method. give optionally
38
+ # @yieldparam [Peeek::Call] call a call to the method
39
+ # @return [Peeek::Hook] a hook to the method of the object
40
+ def self.create(object, method_spec, &process)
41
+ linker_class, method_name = parse(method_spec)
42
+ linker_class = any_module?(object) ? Instance : Singleton unless linker_class
43
+ new(object, method_name, linker_class, &process)
44
+ end
45
+
46
+ # Determine if an object is a module or a class.
47
+ #
48
+ # @param [Module, Class, Object] object an object
49
+ # @return whether an object is a module or a class
50
+ def self.any_module?(object)
51
+ object.class == Module || object.class == Class
52
+ end
53
+
54
+ # Determine if an object is an instance of any class.
55
+ #
56
+ # @param [Module, Class, Object] object an object
57
+ # @return whether an object is an instance of any class
58
+ def self.any_instance?(object)
59
+ !any_module?(object)
60
+ end
61
+
62
+ # Initialize the hook.
63
+ #
64
+ # @param [Module, Class, Object] object a target object that hook
65
+ # @param [Symbol] method_name method name of the object
66
+ # @param [Class] linker_class class of an object to link the hook
67
+ # @yield [call] process a call to the method. give optionally
68
+ # @yieldparam [Peeek::Call] call a call to the method
69
+ def initialize(object, method_name, linker_class, &process)
70
+ raise ArgumentError, "invalid as linker class, #{Linker.classes.join(' or ')} are valid" unless Linker.classes.include?(linker_class)
71
+ @object = object
72
+ @method_name = method_name
73
+ @linker = linker_class.new(object, method_name)
74
+ @process = process
75
+ @calls = Calls.new
76
+ raise ArgumentError, "can't create a hook of instance method to an instance of any class" if self.class.any_instance?(object) and instance?
77
+ end
78
+
79
+ # @attribute [r] object
80
+ # @return [Module, Class, Object] a target object that hook
81
+ attr_reader :object
82
+
83
+ # @attribute [r] method_name
84
+ # @return [Symbol] method name of the object
85
+ attr_reader :method_name
86
+
87
+ # @attribute [r] calls
88
+ # @return [Peeek::Calls] calls to the method that the hook captured
89
+ attr_reader :calls
90
+
91
+ # Determine if the hook to an instance method.
92
+ #
93
+ # @return whether the hook to an instance method
94
+ def instance?
95
+ @linker.is_a?(Instance)
96
+ end
97
+
98
+ # Determine if the hook to a singleton method.
99
+ #
100
+ # @return whether the hook to a singleton method
101
+ def singleton?
102
+ @linker.is_a?(Singleton)
103
+ end
104
+
105
+ # Determine if the method is defined in the object
106
+ #
107
+ # @return whether the method is defined in the object
108
+ def defined?
109
+ @linker.defined?
110
+ end
111
+
112
+ # Determine if the hook is linked to the method
113
+ #
114
+ # @return whether the hook is linked to the method
115
+ def linked?
116
+ instance_variable_defined?(:@original_method)
117
+ end
118
+
119
+ # Link the hook to the method.
120
+ def link
121
+ @original_method = @linker.link(&method(:call)) unless linked?
122
+ self
123
+ end
124
+
125
+ # Unlink the hook from the method.
126
+ def unlink
127
+ if linked?
128
+ @linker.unlink(@original_method)
129
+ remove_instance_variable(:@original_method)
130
+ end
131
+
132
+ self
133
+ end
134
+
135
+ def to_s
136
+ @object.inspect + @linker.method_prefix + @method_name.to_s
137
+ end
138
+
139
+ def inspect
140
+ state = []
141
+ state << 'linked' if linked?
142
+ state_string = state.empty? ? '' : " (#{state * ', '})"
143
+ "#<#{self.class} #{self}#{state_string}>"
144
+ end
145
+
146
+ private
147
+
148
+ INSTANCE_METHOD_PREFIXES = %w(#)
149
+ SINGLETON_METHOD_PREFIXES = %w(. .# ::)
150
+ METHOD_PREFIXES = INSTANCE_METHOD_PREFIXES + SINGLETON_METHOD_PREFIXES
151
+
152
+ def self.parse(method_spec)
153
+ method_spec = method_spec.to_s
154
+
155
+ method_prefix = METHOD_PREFIXES.sort_by(&:length).reverse.find do |method_prefix|
156
+ method_spec.start_with?(method_prefix)
157
+ end
158
+
159
+ return nil, method_spec.to_sym unless method_prefix
160
+
161
+ linker_class = INSTANCE_METHOD_PREFIXES.include?(method_prefix) ? Instance : Singleton
162
+ method_name = method_spec.to_s.sub(/^#{Regexp.quote(method_prefix || '')}/, '').to_sym
163
+ [linker_class, method_name]
164
+ end
165
+
166
+ class << self
167
+ private :parse
168
+ end
169
+
170
+ def call(backtrace, receiver, args)
171
+ method = @original_method.is_a?(UnboundMethod) ? @original_method.bind(receiver) : @original_method
172
+
173
+ result = begin
174
+ Call::ReturnValue.new(method[*args])
175
+ rescue => e
176
+ e.set_backtrace(backtrace)
177
+ Call::Exception.new(e)
178
+ end
179
+
180
+ call = Call.new(self, backtrace, receiver, args, result)
181
+ @calls << call
182
+ @process[call] if @process
183
+ raise call.exception if call.raised?
184
+ call.return_value
185
+ end
186
+
187
+ end
188
+ end
@@ -0,0 +1,51 @@
1
+ require 'peeek/hook/linker'
2
+
3
+ class Peeek
4
+ class Hook
5
+ class Instance < Linker
6
+ METHOD_PREFIX = '#'.freeze
7
+
8
+ # @attribute [r] method_prefix
9
+ # @return [String] method prefix for instance method. return always "#"
10
+ def method_prefix
11
+ METHOD_PREFIX
12
+ end
13
+
14
+ # Determine if the instance method is defined in the object.
15
+ #
16
+ # @return whether the instance method is defined in the object
17
+ def defined?
18
+ @object.method_defined?(@method_name) or @object.private_method_defined?(@method_name)
19
+ end
20
+
21
+ # Link the hook to the instance method.
22
+ #
23
+ # @yield [backtrace, receiver, args] callback for hook
24
+ # @yieldparam [Array<String>] backtrace backtrace the call occurred
25
+ # @yieldparam [Object] receiver object that received the call
26
+ # @yieldparam [Array] args arguments at the call
27
+ # @yieldreturn [Object] return value of the original method
28
+ # @return [UnboundMethod] the original method
29
+ def link
30
+ raise ArgumentError, 'block not supplied' unless block_given?
31
+ original_method = @object.instance_method(@method_name)
32
+ define_method { |*args| yield caller, self, args }
33
+ original_method
34
+ end
35
+
36
+ # Unlink the hook from the instance method.
37
+ #
38
+ # @param [UnboundMethod] original_method original method
39
+ def unlink(original_method)
40
+ define_method(original_method)
41
+ end
42
+
43
+ private
44
+
45
+ def define_method(*args, &block)
46
+ @object.__send__(:define_method, @method_name, *args, &block)
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,29 @@
1
+ class Peeek
2
+ class Hook
3
+ class Linker
4
+ @classes = []
5
+
6
+ class << self
7
+
8
+ # @attribute [r] classes
9
+ # @return [Array<Class>] classes valid as linker
10
+ attr_reader :classes
11
+
12
+ end
13
+
14
+ def self.inherited(klass)
15
+ @classes << klass
16
+ end
17
+
18
+ # Initialize the linker.
19
+ #
20
+ # @param [Module, Class, Object] object a target object that hook
21
+ # @param [Symbol] method_name method name of the object
22
+ def initialize(object, method_name)
23
+ @object = object
24
+ @method_name = method_name
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,52 @@
1
+ require 'peeek/hook/linker'
2
+
3
+ class Peeek
4
+ class Hook
5
+ class Singleton < Linker
6
+ METHOD_PREFIX = '.'.freeze
7
+
8
+ # @attribute [r] method_prefix
9
+ # @return [String] method prefix for singleton method. return always "."
10
+ def method_prefix
11
+ METHOD_PREFIX
12
+ end
13
+
14
+ # Determine if the method is defined in the object.
15
+ #
16
+ # @return whether the method is defined in the object
17
+ def defined?
18
+ @object.respond_to?(@method_name, true)
19
+ end
20
+
21
+ # Link the hook to the method.
22
+ #
23
+ # @yield [backtrace, receiver, args] callback for hook
24
+ # @yieldparam [Array<String>] backtrace backtrace the call occurred
25
+ # @yieldparam [Object] receiver object that received the call
26
+ # @yieldparam [Array] args arguments at the call
27
+ # @yieldreturn [Object] return value of the original method
28
+ # @return [Method] original method
29
+ def link
30
+ raise ArgumentError, 'block not supplied' unless block_given?
31
+ original_method = @object.method(@method_name)
32
+ define_method { |*args| yield caller, self, args }
33
+ original_method
34
+ end
35
+
36
+ # Unlink the hook from the method.
37
+ #
38
+ # @param [Method] original_method original method
39
+ def unlink(original_method)
40
+ define_method(&original_method)
41
+ end
42
+
43
+ private
44
+
45
+ def define_method(&block)
46
+ singleton_class = class << @object; self end
47
+ singleton_class.__send__(:define_method, @method_name, &block)
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,46 @@
1
+ class Peeek
2
+ class Hooks < Array
3
+
4
+ # Get a hook by an object and a method name.
5
+ #
6
+ # @param [Module, Class, Object] object object of a hook to get
7
+ # @param [Symbol] method_name method name of a hook to get. get only the
8
+ # hook by the object if omitted
9
+ # @return [Peeek::Hook] a hook to be got
10
+ # @return [nil] if a hook that corresponds to the object and the method name
11
+ # doesn't exist
12
+ def get(object, method_name = nil)
13
+ if method_name.nil?
14
+ find { |hook| hook.object == object }
15
+ else
16
+ find { |hook| hook.object == object && hook.method_name == method_name }
17
+ end
18
+ end
19
+
20
+ # Clear the hooks.
21
+ def clear
22
+ each do |hook|
23
+ hook.unlink
24
+ hook.calls.clear
25
+ end
26
+
27
+ super
28
+ end
29
+
30
+ # Run process while circumvent the hooks.
31
+ #
32
+ # @yield any process that wants to run while circumvent the hooks
33
+ def circumvent
34
+ raise ArgumentError, 'block not supplied' unless block_given?
35
+
36
+ linked_hooks = select(&:linked?).each(&:unlink)
37
+
38
+ begin
39
+ yield
40
+ ensure
41
+ linked_hooks.each(&:link)
42
+ end
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,120 @@
1
+ require 'peeek/hooks'
2
+
3
+ class Peeek
4
+ class Supervisor
5
+
6
+ # Create a supervisor for instance methods.
7
+ #
8
+ # @return [Peeek::Supervisor] a supervisor for instance methods
9
+ def self.create_for_instance
10
+ new(:method_added)
11
+ end
12
+
13
+ # Create a supervisor for singleton methods.
14
+ #
15
+ # @return [Peeek::Supervisor] a supervisor for singleton methods
16
+ def self.create_for_singleton
17
+ new(:singleton_method_added)
18
+ end
19
+
20
+ # Initialize the supervisor.
21
+ #
22
+ # @param [Symbol] callback_name name of the method that is called when
23
+ # methods was added to object of a hook
24
+ def initialize(callback_name)
25
+ @callback_name = callback_name
26
+ @hooks = Hooks.new
27
+ @original_callbacks = {}
28
+ end
29
+
30
+ # @attribute [r] hooks
31
+ # @return [Peeek::Hooks] hooks that is registered to the supervisor
32
+ attr_reader :hooks
33
+
34
+ # @attribute [r] original_callbacks
35
+ # @return [Hash<Object, Method>] original callbacks of objects that is
36
+ # supervising
37
+ attr_reader :original_callbacks
38
+
39
+ # Add hooks to target that is supervised.
40
+ #
41
+ # @param [Array<Peeek::Hook>] hooks hooks that is supervised
42
+ def add(*hooks)
43
+ @hooks.push(*hooks)
44
+
45
+ hooks.map(&:object).uniq.each do |object|
46
+ @original_callbacks[object] = proceed(object) unless proceeded?(object)
47
+ end
48
+
49
+ self
50
+ end
51
+ alias << add
52
+
53
+ # Clear the hooks and the objects that is supervising.
54
+ def clear
55
+ @hooks.clear
56
+ define_callbacks(@original_callbacks).clear
57
+ self
58
+ end
59
+
60
+ # Run process while circumvent supervision.
61
+ #
62
+ # @yield any process that wants to run while circumvent supervision
63
+ def circumvent(&process)
64
+ current_callbacks = @original_callbacks.keys.map do |object|
65
+ [object, object.method(@callback_name)]
66
+ end
67
+
68
+ define_callbacks(@original_callbacks)
69
+
70
+ begin
71
+ @hooks.circumvent(&process)
72
+ ensure
73
+ define_callbacks(Hash[current_callbacks])
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def proceeded?(object)
80
+ !!@original_callbacks[object]
81
+ end
82
+
83
+ def proceed(object)
84
+ supervisor = self
85
+ hooks = @hooks
86
+ original_callbacks = @original_callbacks
87
+
88
+ object.method(@callback_name).tap do |original_callback|
89
+ define_callback(object) do |method_name|
90
+ hook = hooks.get(self, method_name)
91
+
92
+ if hook
93
+ hooks.delete(hook)
94
+
95
+ unless hooks.get(self)
96
+ original_callback = original_callbacks.delete(self)
97
+ supervisor.__send__(:define_callback, self, &original_callback)
98
+ end
99
+
100
+ hook.link
101
+ end
102
+
103
+ original_callback[method_name]
104
+ end
105
+ end
106
+ end
107
+
108
+ def define_callback(object, &proc)
109
+ singleton_class = class << object; self end
110
+ singleton_class.__send__(:define_method, @callback_name, &proc)
111
+ end
112
+
113
+ def define_callbacks(callbacks)
114
+ callbacks.each do |object, callback|
115
+ define_callback(object, &callback)
116
+ end
117
+ end
118
+
119
+ end
120
+ end