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