peeek 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +127 -0
- data/Rakefile +1 -0
- data/bin/peeek +16 -0
- data/lib/peeek.rb +147 -0
- data/lib/peeek/call.rb +134 -0
- data/lib/peeek/calls.rb +43 -0
- data/lib/peeek/hook.rb +188 -0
- data/lib/peeek/hook/instance.rb +51 -0
- data/lib/peeek/hook/linker.rb +29 -0
- data/lib/peeek/hook/singleton.rb +52 -0
- data/lib/peeek/hooks.rb +46 -0
- data/lib/peeek/supervisor.rb +120 -0
- data/lib/peeek/version.rb +3 -0
- data/peeek.gemspec +25 -0
- data/spec/peeek/call_spec.rb +226 -0
- data/spec/peeek/calls_spec.rb +86 -0
- data/spec/peeek/hook/instance_spec.rb +87 -0
- data/spec/peeek/hook/linker_spec.rb +15 -0
- data/spec/peeek/hook/singleton_spec.rb +82 -0
- data/spec/peeek/hook_spec.rb +380 -0
- data/spec/peeek/hooks_spec.rb +92 -0
- data/spec/peeek/supervisor_spec.rb +129 -0
- data/spec/peeek_spec.rb +335 -0
- data/spec/spec_helper.rb +78 -0
- metadata +125 -0
data/lib/peeek/calls.rb
ADDED
@@ -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
|
data/lib/peeek/hooks.rb
ADDED
@@ -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
|