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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1d9163af655c3a81a95497b5b85dbba4806d38a8
|
4
|
+
data.tar.gz: 94d66998484a454134c8c41041e593f4b458b458
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e4593100bd8419742be0174883536e40f7628211411f124259eef776481968486a387a2916dc958510239a7be27b266ce004f094a8b5fc87d059095d435f51cf
|
7
|
+
data.tar.gz: 2263f4beddadacbc497b4cc1c47a076b34c755c8ef9e8cda003b7e8ad55d7eef423fdbd427bfe014a5314bbf456feb905fd52e5fb49e76d894162221e2fc7ae6
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Takahiro Kondo
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
Peeek
|
2
|
+
=====
|
3
|
+
|
4
|
+
Peeek peeks at calls of a method
|
5
|
+
|
6
|
+
Installation
|
7
|
+
------------
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'peeek'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install peeek
|
20
|
+
|
21
|
+
Usage
|
22
|
+
-----
|
23
|
+
|
24
|
+
You can peek at calls of a method by `Object#peeek` (or `Peeek#hook`).
|
25
|
+
|
26
|
+
require 'peeek'
|
27
|
+
|
28
|
+
String.peeek(:%) # or Peeek.current.hook(String, :%)
|
29
|
+
|
30
|
+
format = '%s (%d)'
|
31
|
+
puts format % ['Koyomi', 18]
|
32
|
+
puts format % ['Karen', 14]
|
33
|
+
puts format % ['Tsukihi', 14]
|
34
|
+
|
35
|
+
puts Peeek.current.calls # => String#% from "%s (%d)" with ["Koyomi", 18] returned "Koyomi (18)" in peeek-example.rb at 6
|
36
|
+
# => String#% from "%s (%d)" with ["Karen", 14] returned "Karen (14)" in peeek-example.rb at 7
|
37
|
+
# => String#% from "%s (%d)" with ["Tsukihi", 14] returned "Tsukihi (14)" in peeek-example.rb at 8
|
38
|
+
|
39
|
+
### How to hook to a method
|
40
|
+
|
41
|
+
Call `Object#peeek` to any module or any class, hook to their instance methods.
|
42
|
+
|
43
|
+
String.peeek(:%)
|
44
|
+
|
45
|
+
Also for an instance of any class, hook to its singleton methods.
|
46
|
+
|
47
|
+
$stdout.peeek(:write)
|
48
|
+
|
49
|
+
If want to choose whether to hook to either instance method or singleton method,
|
50
|
+
add "#" before in the method name in instance method, add "." in singleton
|
51
|
+
method.
|
52
|
+
|
53
|
+
String.peeek('#%')
|
54
|
+
$stdout.peeek('.write')
|
55
|
+
Regexp.peeek('.quote')
|
56
|
+
|
57
|
+
Even if the method isn't defined at the time you call `Object#peeek`, enable the
|
58
|
+
hook when the method is defined.
|
59
|
+
|
60
|
+
Kernel.peeek(:pp)
|
61
|
+
require 'pp' # enable the hook
|
62
|
+
pp {'Koyomi' => 18, 'Karen' => 14, 'Tsukihi' => 14}
|
63
|
+
|
64
|
+
### Localize a Peeek object
|
65
|
+
|
66
|
+
`Peeek.local` enables hooks only in a block.
|
67
|
+
|
68
|
+
require 'peeek'
|
69
|
+
|
70
|
+
format = '%s (%d)'
|
71
|
+
|
72
|
+
calls = Peeek.local do
|
73
|
+
String.peeek(:%)
|
74
|
+
puts format % ['Koyomi', 18]
|
75
|
+
Peeek.current.calls
|
76
|
+
end
|
77
|
+
|
78
|
+
puts calls # => String#% from "%s (%d)" with ["Koyomi", 18] returned "Koyomi (18)" in peeek-local.rb at 7
|
79
|
+
|
80
|
+
puts format % ['Karen', 14] # not captured
|
81
|
+
|
82
|
+
Hook to methods at head of the block, and return the calls, then use
|
83
|
+
`Peeek.capture`.
|
84
|
+
|
85
|
+
require 'peeek'
|
86
|
+
|
87
|
+
format = '%s (%d)'
|
88
|
+
|
89
|
+
calls = Peeek.capture(String => :%) do
|
90
|
+
puts format % ['Koyomi', 18]
|
91
|
+
end
|
92
|
+
|
93
|
+
puts calls # => String#% from "%s (%d)" with ["Koyomi", 18] returned "Koyomi (18)" in peeek-capture.rb at 6
|
94
|
+
|
95
|
+
puts format % ['Karen', 14] # not captured
|
96
|
+
|
97
|
+
### Filter calls
|
98
|
+
|
99
|
+
`Peeek#calls` or `Peeek.local` return an instance of `Peeek::Calls`. It has
|
100
|
+
implemented methods to filter by attributes of the call.
|
101
|
+
|
102
|
+
require 'peeek'
|
103
|
+
|
104
|
+
format = '%s (%d)'
|
105
|
+
|
106
|
+
calls = Peeek.capture(String => :%) do
|
107
|
+
puts format % ['Koyomi', 18]
|
108
|
+
puts format % ['Karen'] rescue $!
|
109
|
+
end
|
110
|
+
|
111
|
+
puts calls.in('peeek-calls.rb') # filter by file name
|
112
|
+
puts calls.in(/\.rb/) # can also use regular expressions
|
113
|
+
puts calls.at(6) # filter by line number
|
114
|
+
puts calls.at(1..10) # can also use range
|
115
|
+
puts calls.from('%s (%d)') # filter by receiver
|
116
|
+
puts calls.return_values # filter only calls that returned a value
|
117
|
+
puts calls.exceptions # filter only calls that raised an exception
|
118
|
+
|
119
|
+
|
120
|
+
Contributing
|
121
|
+
------------
|
122
|
+
|
123
|
+
1. Fork it
|
124
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
125
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
126
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
127
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/peeek
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'stringio'
|
3
|
+
require 'peeek'
|
4
|
+
|
5
|
+
path = ARGV.first
|
6
|
+
|
7
|
+
original_stdout = $stdout
|
8
|
+
$stdout = StringIO.new
|
9
|
+
|
10
|
+
calls = begin
|
11
|
+
Peeek.capture(String => :%) { load path } # want to be you can specify
|
12
|
+
ensure
|
13
|
+
$stdout = original_stdout
|
14
|
+
end
|
15
|
+
|
16
|
+
puts calls
|
data/lib/peeek.rb
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'peeek/version'
|
2
|
+
require 'peeek/hook'
|
3
|
+
require 'peeek/hooks'
|
4
|
+
require 'peeek/supervisor'
|
5
|
+
require 'peeek/calls'
|
6
|
+
|
7
|
+
class Peeek
|
8
|
+
|
9
|
+
# @attribute [r] global
|
10
|
+
# @return [Peeek] the global Peeek object
|
11
|
+
def self.global
|
12
|
+
@global ||= new
|
13
|
+
end
|
14
|
+
|
15
|
+
# @attribute [r] current
|
16
|
+
# @return [Peeek] the current Peeek object
|
17
|
+
#
|
18
|
+
# @see Peeek.local
|
19
|
+
def self.current
|
20
|
+
@current ||= global
|
21
|
+
end
|
22
|
+
|
23
|
+
# Run process to switch to a local Peeek object from the current Peeek
|
24
|
+
# object. The local Peeek object doesn't inherit the registered hooks and
|
25
|
+
# supervision from the current Peeek object. The current Peeek object reverts
|
26
|
+
# after ran the process.
|
27
|
+
#
|
28
|
+
# @yield any process that want to run to switch
|
29
|
+
def self.local
|
30
|
+
raise ArgumentError, 'block not supplied' unless block_given?
|
31
|
+
|
32
|
+
old = current
|
33
|
+
@current = new
|
34
|
+
|
35
|
+
old.circumvent do
|
36
|
+
begin
|
37
|
+
yield
|
38
|
+
ensure
|
39
|
+
current.release
|
40
|
+
@current = old
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Capture all calls to hook targets.
|
46
|
+
#
|
47
|
+
# @param [Hash{Module, Class, Object => String, Array<String>, Symbol, Array<Symbol>}]
|
48
|
+
# object_and_method_specs an object and method specification(s) that be
|
49
|
+
# target of hook
|
50
|
+
# @yield any process that want to run to capture
|
51
|
+
# @return [Peeek::Calls] calls that were captured in the block
|
52
|
+
def self.capture(object_and_method_specs)
|
53
|
+
raise ArgumentError, 'block not supplied' unless block_given?
|
54
|
+
|
55
|
+
local do
|
56
|
+
object_and_method_specs.each { |object, method_specs| current.hook(object, *method_specs) }
|
57
|
+
yield
|
58
|
+
current.calls
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Initialize the Peeek object.
|
63
|
+
def initialize
|
64
|
+
@hooks = Hooks.new
|
65
|
+
@instance_supervisor = Supervisor.create_for_instance
|
66
|
+
@singleton_supervisor = Supervisor.create_for_singleton
|
67
|
+
end
|
68
|
+
|
69
|
+
# @attribute [r] hooks
|
70
|
+
# @return [Peeek::Hooks] the registered hooks
|
71
|
+
attr_reader :hooks
|
72
|
+
|
73
|
+
# @attribute [r] calls
|
74
|
+
# @return [Peeek::Calls] calls to the methods that the registered hooks
|
75
|
+
# captured
|
76
|
+
def calls
|
77
|
+
Calls.new(@hooks.map(&:calls).inject([], &:+))
|
78
|
+
end
|
79
|
+
|
80
|
+
# Register a hook to methods of an object.
|
81
|
+
#
|
82
|
+
# @param [Module, Class, Object] object a target object that hook
|
83
|
+
# @param [Array<String>, Array<Symbol>] method_specs method specifications of
|
84
|
+
# the object. see also examples of {Peeek::Hook.create}
|
85
|
+
# @yield [call] process a call to the methods. give optionally
|
86
|
+
# @yieldparam [Peeek::Call] call a call to the methods
|
87
|
+
# @return [Peeek::Hooks] registered hooks at calling
|
88
|
+
#
|
89
|
+
# @see Peeek::Hook.create
|
90
|
+
def hook(object, *method_specs, &process)
|
91
|
+
hooks = method_specs.map do |method_spec|
|
92
|
+
Hook.create(object, method_spec, &process).tap do |hook|
|
93
|
+
if hook.defined?
|
94
|
+
hook.link
|
95
|
+
elsif hook.instance?
|
96
|
+
@instance_supervisor << hook
|
97
|
+
elsif hook.singleton?
|
98
|
+
@singleton_supervisor << hook
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
@hooks.push(*hooks)
|
104
|
+
Hooks.new(hooks)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Release the registered hooks and supervision.
|
108
|
+
def release
|
109
|
+
@hooks.clear
|
110
|
+
@instance_supervisor.clear
|
111
|
+
@singleton_supervisor.clear
|
112
|
+
self
|
113
|
+
end
|
114
|
+
|
115
|
+
# Run process while circumvent the registered hooks and supervision.
|
116
|
+
#
|
117
|
+
# @yield any process that want to run while circumvent the registered hooks
|
118
|
+
# and supervision
|
119
|
+
def circumvent(&process)
|
120
|
+
raise ArgumentError, 'block not supplied' unless block_given?
|
121
|
+
|
122
|
+
@singleton_supervisor.circumvent do
|
123
|
+
@instance_supervisor.circumvent do
|
124
|
+
@hooks.circumvent(&process)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
module Readily
|
130
|
+
|
131
|
+
# Register a hook to methods of self to the current Peeek object.
|
132
|
+
#
|
133
|
+
# @param [Array<String>, Array<Symbol>] method_specs method specifications
|
134
|
+
# of the object. see also examples of {Peeek::Hook.create}
|
135
|
+
# @yield [call] process a call to the methods. give optionally
|
136
|
+
# @yieldparam [Peeek::Call] call a call to the methods
|
137
|
+
#
|
138
|
+
# @see Peeek#hook
|
139
|
+
def peeek(*method_specs, &process)
|
140
|
+
Peeek.current.hook(self, *method_specs, &process)
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
Object.__send__(:include, Readily)
|
146
|
+
|
147
|
+
end
|
data/lib/peeek/call.rb
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
class Peeek
|
2
|
+
class Call
|
3
|
+
|
4
|
+
# Initialize the call.
|
5
|
+
#
|
6
|
+
# @param [Peeek::Hook] hook hook the call occurred
|
7
|
+
# @param [Array<String>] backtrace backtrace the call occurred
|
8
|
+
# @param [Module, Class, Object] receiver object that received the call
|
9
|
+
# @param [Array] arguments arguments at the call
|
10
|
+
# @param [Peeek::Call::Result] result result of the call
|
11
|
+
def initialize(hook, backtrace, receiver, arguments, result)
|
12
|
+
raise ArgumentError, 'invalid as result' unless result.is_a?(Result)
|
13
|
+
@hook = hook
|
14
|
+
@backtrace = backtrace
|
15
|
+
@file, @line = extract_file_and_line(backtrace.first)
|
16
|
+
@receiver = receiver
|
17
|
+
@arguments = arguments
|
18
|
+
@result = result
|
19
|
+
end
|
20
|
+
|
21
|
+
# @attribute [r] hook
|
22
|
+
# @return [Peeek::Hook] hook the call occurred
|
23
|
+
attr_reader :hook
|
24
|
+
|
25
|
+
# @attribute [r] backtrace
|
26
|
+
# @return [Array<String>] backtrace the call occurred
|
27
|
+
attr_reader :backtrace
|
28
|
+
|
29
|
+
# @attribute [r] file
|
30
|
+
# @return [String] name of file the call occurred
|
31
|
+
attr_reader :file
|
32
|
+
|
33
|
+
# @attribute [r] line
|
34
|
+
# @return [Integer] line number the call occurred
|
35
|
+
attr_reader :line
|
36
|
+
|
37
|
+
# @attribute [r] receiver
|
38
|
+
# @return [Module, Class, Object] object that received the call
|
39
|
+
attr_reader :receiver
|
40
|
+
|
41
|
+
# @attribute [r] arguments
|
42
|
+
# @return [Array] arguments at the call
|
43
|
+
attr_reader :arguments
|
44
|
+
|
45
|
+
# @attribute [r] result
|
46
|
+
# @return [Peeek::Call::Result] result of the call
|
47
|
+
attr_reader :result
|
48
|
+
|
49
|
+
# @attribute [r] return_value
|
50
|
+
# @return [Object] value that the call returned
|
51
|
+
def return_value
|
52
|
+
raise TypeError, "the call didn't return a value" unless returned?
|
53
|
+
@result.value
|
54
|
+
end
|
55
|
+
|
56
|
+
# @attribute [r] exception
|
57
|
+
# @return [StandardError] exception that raised from the call
|
58
|
+
def exception
|
59
|
+
raise TypeError, "the call didn't raised an exception" unless raised?
|
60
|
+
@result.value
|
61
|
+
end
|
62
|
+
|
63
|
+
# Determine if the result is a return value.
|
64
|
+
#
|
65
|
+
# @return whether the result is a return value
|
66
|
+
def returned?
|
67
|
+
@result.is_a?(ReturnValue)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Determine if the result is an exception.
|
71
|
+
#
|
72
|
+
# @return whether the result is an exception
|
73
|
+
def raised?
|
74
|
+
@result.is_a?(Exception)
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_s
|
78
|
+
parts = [@hook.to_s]
|
79
|
+
parts << "from #{@receiver.inspect}"
|
80
|
+
|
81
|
+
if @arguments.size == 1
|
82
|
+
parts << "with #{@arguments.first.inspect}"
|
83
|
+
elsif @arguments.size > 1
|
84
|
+
parts << "with (#{@arguments.map(&:inspect) * ', '})"
|
85
|
+
end
|
86
|
+
|
87
|
+
if returned?
|
88
|
+
parts << "returned #{return_value.inspect}"
|
89
|
+
elsif raised?
|
90
|
+
parts << "raised #{exception.inspect}"
|
91
|
+
end
|
92
|
+
|
93
|
+
parts << "in #{@file}"
|
94
|
+
parts << "at #{@line}"
|
95
|
+
parts * ' '
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def extract_file_and_line(string)
|
101
|
+
_, file, line = /^(.+):(\d+)(?::in\s+|$)/.match(string).to_a
|
102
|
+
raise ArgumentError, 'invalid as string of backtrace' unless file and line
|
103
|
+
[file, line.to_i]
|
104
|
+
end
|
105
|
+
|
106
|
+
class Result
|
107
|
+
|
108
|
+
# Initialize the result.
|
109
|
+
#
|
110
|
+
# @param [Object] value value of the result
|
111
|
+
def initialize(value)
|
112
|
+
@value = value
|
113
|
+
end
|
114
|
+
|
115
|
+
# @attribute [r] value
|
116
|
+
# @return [Object] value of the result
|
117
|
+
attr_reader :value
|
118
|
+
|
119
|
+
def ==(other)
|
120
|
+
self.class == other.class && @value == other.value
|
121
|
+
end
|
122
|
+
alias eql? ==
|
123
|
+
|
124
|
+
def hash
|
125
|
+
(self.class.hash << 32) + @value.hash
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
class ReturnValue < Result; end
|
131
|
+
class Exception < Result; end
|
132
|
+
|
133
|
+
end
|
134
|
+
end
|