peeek 1.0.1

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,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