tapping_device 0.4.8 → 0.5.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 +4 -4
- data/.DS_Store +0 -0
- data/.github/workflows/ruby.yml +12 -5
- data/.ruby-version +1 -0
- data/Gemfile.lock +15 -15
- data/README.md +90 -371
- data/images/print_calls - single entry.png +0 -0
- data/images/print_calls.png +0 -0
- data/images/print_traces.png +0 -0
- data/lib/tapping_device.rb +57 -22
- data/lib/tapping_device/output_payload.rb +145 -0
- data/lib/tapping_device/payload.rb +2 -34
- data/lib/tapping_device/trackable.rb +31 -10
- data/lib/tapping_device/version.rb +1 -1
- data/tapping_device.gemspec +3 -3
- metadata +14 -7
- data/lib/tapping_device/sql_tapping_methods.rb +0 -89
Binary file
|
Binary file
|
Binary file
|
data/lib/tapping_device.rb
CHANGED
@@ -2,9 +2,9 @@ require "active_record"
|
|
2
2
|
require "tapping_device/version"
|
3
3
|
require "tapping_device/manageable"
|
4
4
|
require "tapping_device/payload"
|
5
|
+
require "tapping_device/output_payload"
|
5
6
|
require "tapping_device/trackable"
|
6
7
|
require "tapping_device/exceptions"
|
7
|
-
require "tapping_device/sql_tapping_methods"
|
8
8
|
|
9
9
|
class TappingDevice
|
10
10
|
|
@@ -16,7 +16,6 @@ class TappingDevice
|
|
16
16
|
@devices = []
|
17
17
|
@suspend_new = false
|
18
18
|
|
19
|
-
include SqlTappingMethods
|
20
19
|
extend Manageable
|
21
20
|
|
22
21
|
def initialize(options = {}, &block)
|
@@ -25,12 +24,16 @@ class TappingDevice
|
|
25
24
|
@options = process_options(options)
|
26
25
|
@calls = []
|
27
26
|
@disabled = false
|
27
|
+
@with_condition = nil
|
28
28
|
self.class.devices << self
|
29
29
|
end
|
30
30
|
|
31
31
|
def tap_init!(klass)
|
32
32
|
raise "argument should be a class, got #{klass}" unless klass.is_a?(Class)
|
33
|
-
track(klass, condition: :tap_init?)
|
33
|
+
track(klass, condition: :tap_init?) do |payload|
|
34
|
+
payload[:return_value] = payload[:receiver]
|
35
|
+
payload[:receiver] = klass
|
36
|
+
end
|
34
37
|
end
|
35
38
|
|
36
39
|
def tap_on!(object)
|
@@ -46,8 +49,21 @@ class TappingDevice
|
|
46
49
|
track(record, condition: :tap_associations?)
|
47
50
|
end
|
48
51
|
|
49
|
-
def and_print(payload_method)
|
50
|
-
@output_block =
|
52
|
+
def and_print(payload_method = nil, &block)
|
53
|
+
@output_block =
|
54
|
+
if block
|
55
|
+
-> (output_payload) { puts(block.call(output_payload)) }
|
56
|
+
elsif payload_method
|
57
|
+
-> (output_payload) { puts(output_payload.send(payload_method)) }
|
58
|
+
else
|
59
|
+
raise "need to provide either a payload method name or a block"
|
60
|
+
end
|
61
|
+
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
def with(&block)
|
66
|
+
@with_condition = block
|
51
67
|
end
|
52
68
|
|
53
69
|
def set_block(&block)
|
@@ -81,7 +97,7 @@ class TappingDevice
|
|
81
97
|
|
82
98
|
private
|
83
99
|
|
84
|
-
def track(object, condition
|
100
|
+
def track(object, condition:, &payload_block)
|
85
101
|
@target = object
|
86
102
|
@trace_point = TracePoint.new(options[:event_type]) do |tp|
|
87
103
|
if send(condition, object, tp)
|
@@ -89,7 +105,16 @@ class TappingDevice
|
|
89
105
|
|
90
106
|
next if should_be_skipped_by_paths?(filepath)
|
91
107
|
|
92
|
-
payload = build_payload(tp: tp, filepath: filepath, line_number: line_number)
|
108
|
+
payload = build_payload(tp: tp, filepath: filepath, line_number: line_number, &payload_block)
|
109
|
+
|
110
|
+
next unless with_condition_satisfied?(payload)
|
111
|
+
|
112
|
+
# skip TappingDevice related calls
|
113
|
+
if Module.respond_to?(:module_parents)
|
114
|
+
next if payload.defined_class.module_parents.include?(TappingDevice)
|
115
|
+
else
|
116
|
+
next if payload.defined_class.parents.include?(TappingDevice)
|
117
|
+
end
|
93
118
|
|
94
119
|
record_call!(payload)
|
95
120
|
|
@@ -130,15 +155,12 @@ class TappingDevice
|
|
130
155
|
end
|
131
156
|
|
132
157
|
def build_payload(tp:, filepath:, line_number:)
|
133
|
-
|
134
|
-
tp.binding.local_variables.each { |name| arguments[name] = tp.binding.local_variable_get(name) }
|
135
|
-
|
136
|
-
Payload.init({
|
158
|
+
payload = Payload.init({
|
137
159
|
target: @target,
|
138
160
|
receiver: tp.self,
|
139
161
|
method_name: tp.callee_id,
|
140
162
|
method_object: get_method_object_from(tp.self, tp.callee_id),
|
141
|
-
arguments:
|
163
|
+
arguments: collect_arguments(tp),
|
142
164
|
return_value: (tp.return_value rescue nil),
|
143
165
|
filepath: filepath,
|
144
166
|
line_number: line_number,
|
@@ -146,6 +168,10 @@ class TappingDevice
|
|
146
168
|
trace: get_traces(tp),
|
147
169
|
tp: tp
|
148
170
|
})
|
171
|
+
|
172
|
+
yield(payload) if block_given?
|
173
|
+
|
174
|
+
payload
|
149
175
|
end
|
150
176
|
|
151
177
|
def tap_init?(klass, tp)
|
@@ -176,15 +202,7 @@ class TappingDevice
|
|
176
202
|
return false if is_from_target?(self, tp)
|
177
203
|
return false if tp.defined_class == TappingDevice::Trackable || tp.defined_class == TappingDevice
|
178
204
|
|
179
|
-
|
180
|
-
return false unless method_object.is_a?(Method)
|
181
|
-
# if a no-arugment method is called, tp.binding.local_variables will be those local variables in the same scope
|
182
|
-
# so we need to make sure the method takes arguments, then we can be sure that the locals are arguments
|
183
|
-
return false unless method_object && method_object.arity.to_i > 0
|
184
|
-
|
185
|
-
argument_values = tp.binding.local_variables.map { |name| tp.binding.local_variable_get(name) }
|
186
|
-
|
187
|
-
argument_values.any? do |value|
|
205
|
+
collect_arguments(tp).values.any? do |value|
|
188
206
|
# during comparison, Ruby might perform data type conversion like calling `to_sym` on the value
|
189
207
|
# but not every value supports every conversion methods
|
190
208
|
object == value rescue false
|
@@ -204,6 +222,19 @@ class TappingDevice
|
|
204
222
|
nil
|
205
223
|
end
|
206
224
|
|
225
|
+
def collect_arguments(tp)
|
226
|
+
parameters =
|
227
|
+
if RUBY_VERSION.to_f >= 2.6
|
228
|
+
tp.parameters
|
229
|
+
else
|
230
|
+
get_method_object_from(tp.self, tp.callee_id)&.parameters || []
|
231
|
+
end.map { |parameter| parameter[1] }
|
232
|
+
|
233
|
+
tp.binding.local_variables.each_with_object({}) do |name, args|
|
234
|
+
args[name] = tp.binding.local_variable_get(name) if parameters.include?(name)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
207
238
|
def process_options(options)
|
208
239
|
options[:filter_by_paths] ||= []
|
209
240
|
options[:exclude_by_paths] ||= []
|
@@ -238,7 +269,7 @@ class TappingDevice
|
|
238
269
|
def record_call!(payload)
|
239
270
|
return if @disabled
|
240
271
|
|
241
|
-
@output_block.call(payload) if @output_block
|
272
|
+
@output_block.call(OutputPayload.init(payload)) if @output_block
|
242
273
|
|
243
274
|
if @block
|
244
275
|
root_device.calls << @block.call(payload)
|
@@ -246,4 +277,8 @@ class TappingDevice
|
|
246
277
|
root_device.calls << payload
|
247
278
|
end
|
248
279
|
end
|
280
|
+
|
281
|
+
def with_condition_satisfied?(payload)
|
282
|
+
@with_condition.blank? || @with_condition.call(payload)
|
283
|
+
end
|
249
284
|
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
class TappingDevice
|
2
|
+
class OutputPayload < Payload
|
3
|
+
alias :raw_arguments :arguments
|
4
|
+
alias :raw_return_value :return_value
|
5
|
+
|
6
|
+
def method_name(options = {})
|
7
|
+
":#{super(options)}"
|
8
|
+
end
|
9
|
+
|
10
|
+
def arguments(options = {})
|
11
|
+
generate_string_result(raw_arguments, options[:inspect])
|
12
|
+
end
|
13
|
+
|
14
|
+
def return_value(options = {})
|
15
|
+
generate_string_result(raw_return_value, options[:inspect])
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.full_color_code(code)
|
19
|
+
end
|
20
|
+
|
21
|
+
COLOR_CODES = {
|
22
|
+
green: 10,
|
23
|
+
yellow: 11,
|
24
|
+
blue: 12,
|
25
|
+
megenta: 13,
|
26
|
+
cyan: 14,
|
27
|
+
orange: 214
|
28
|
+
}
|
29
|
+
|
30
|
+
COLORS = COLOR_CODES.each_with_object({}) do |(name, code), hash|
|
31
|
+
hash[name] = "\u001b[38;5;#{code}m"
|
32
|
+
end.merge(
|
33
|
+
reset: "\u001b[0m",
|
34
|
+
nocolor: ""
|
35
|
+
)
|
36
|
+
|
37
|
+
PAYLOAD_ATTRIBUTES = {
|
38
|
+
method_name: {symbol: "", color: COLORS[:blue]},
|
39
|
+
location: {symbol: "from:", color: COLORS[:green]},
|
40
|
+
sql: {symbol: "QUERIES", color: COLORS[:nocolor]},
|
41
|
+
return_value: {symbol: "=>", color: COLORS[:megenta]},
|
42
|
+
arguments: {symbol: "<=", color: COLORS[:orange]},
|
43
|
+
defined_class: {symbol: "#", color: COLORS[:yellow]}
|
44
|
+
}
|
45
|
+
|
46
|
+
PAYLOAD_ATTRIBUTES.each do |attribute, attribute_options|
|
47
|
+
color = attribute_options[:color]
|
48
|
+
|
49
|
+
alias_method "original_#{attribute}".to_sym, attribute
|
50
|
+
|
51
|
+
# regenerate attributes with `colorize: true` support
|
52
|
+
define_method attribute do |options = {}|
|
53
|
+
call_result = send("original_#{attribute}", options)
|
54
|
+
|
55
|
+
if options[:colorize]
|
56
|
+
"#{color}#{call_result}#{COLORS[:reset]}"
|
57
|
+
else
|
58
|
+
call_result
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
define_method "#{attribute}_with_color" do |options = {}|
|
63
|
+
send(attribute, options.merge(colorize: true))
|
64
|
+
end
|
65
|
+
|
66
|
+
PAYLOAD_ATTRIBUTES.each do |and_attribute, and_attribute_options|
|
67
|
+
next if and_attribute == attribute
|
68
|
+
|
69
|
+
define_method "#{attribute}_and_#{and_attribute}" do |options = {}|
|
70
|
+
"#{send(attribute, options)} #{and_attribute_options[:symbol]} #{send(and_attribute, options)}"
|
71
|
+
end
|
72
|
+
|
73
|
+
define_method "#{attribute}_and_#{and_attribute}_with_color" do |options = {}|
|
74
|
+
send("#{attribute}_and_#{and_attribute}", options.merge(colorize: true))
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def passed_at(options = {})
|
80
|
+
with_method_head = options.fetch(:with_method_head, false)
|
81
|
+
arg_name = raw_arguments.keys.detect { |k| raw_arguments[k] == target }
|
82
|
+
|
83
|
+
return unless arg_name
|
84
|
+
|
85
|
+
arg_name = ":#{arg_name}"
|
86
|
+
arg_name = value_with_color(arg_name, :orange) if options[:colorize]
|
87
|
+
msg = "Passed as #{arg_name} in '#{defined_class(options)}##{method_name(options)}' at #{location(options)}"
|
88
|
+
msg += "\n > #{method_head.strip}" if with_method_head
|
89
|
+
msg
|
90
|
+
end
|
91
|
+
|
92
|
+
def detail_call_info(options = {})
|
93
|
+
<<~MSG
|
94
|
+
#{method_name_and_defined_class(options)}
|
95
|
+
from: #{location(options)}
|
96
|
+
<= #{arguments(options)}
|
97
|
+
=> #{return_value(options)}
|
98
|
+
|
99
|
+
MSG
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def value_with_color(value, color)
|
105
|
+
"#{COLORS[color]}#{value}#{COLORS[:reset]}"
|
106
|
+
end
|
107
|
+
|
108
|
+
def generate_string_result(obj, inspect)
|
109
|
+
case obj
|
110
|
+
when Array
|
111
|
+
array_to_string(obj, inspect)
|
112
|
+
when Hash
|
113
|
+
hash_to_string(obj, inspect)
|
114
|
+
when String
|
115
|
+
"\"#{obj}\""
|
116
|
+
else
|
117
|
+
inspect ? obj.inspect : obj.to_s
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def array_to_string(array, inspect)
|
122
|
+
elements_string = array.map do |elem|
|
123
|
+
generate_string_result(elem, inspect)
|
124
|
+
end.join(", ")
|
125
|
+
"[#{elements_string}]"
|
126
|
+
end
|
127
|
+
|
128
|
+
def hash_to_string(hash, inspect)
|
129
|
+
elements_string = hash.map do |key, value|
|
130
|
+
"#{key.to_s}: #{generate_string_result(value, inspect)}"
|
131
|
+
end.join(", ")
|
132
|
+
"{#{elements_string}}"
|
133
|
+
end
|
134
|
+
|
135
|
+
def obj_to_string(element, inspect)
|
136
|
+
to_string_method = inspect ? :inspect : :to_s
|
137
|
+
|
138
|
+
if !inspect && element.is_a?(String)
|
139
|
+
"\"#{element}\""
|
140
|
+
else
|
141
|
+
element.send(to_string_method)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -6,7 +6,7 @@ class TappingDevice
|
|
6
6
|
]
|
7
7
|
|
8
8
|
ATTRS.each do |attr|
|
9
|
-
define_method attr do
|
9
|
+
define_method attr do |options = {}|
|
10
10
|
self[attr]
|
11
11
|
end
|
12
12
|
end
|
@@ -19,45 +19,13 @@ class TappingDevice
|
|
19
19
|
h
|
20
20
|
end
|
21
21
|
|
22
|
-
def passed_at(with_method_head: false)
|
23
|
-
arg_name = arguments.keys.detect { |k| arguments[k] == target }
|
24
|
-
return unless arg_name
|
25
|
-
msg = "Passed as '#{arg_name}' in method ':#{method_name}'"
|
26
|
-
msg += "\n > #{method_head.strip}" if with_method_head
|
27
|
-
msg += "\n at #{location}"
|
28
|
-
msg
|
29
|
-
end
|
30
|
-
|
31
22
|
def method_head
|
32
23
|
source_file, source_line = method_object.source_location
|
33
24
|
IO.readlines(source_file)[source_line-1]
|
34
25
|
end
|
35
26
|
|
36
|
-
def location
|
27
|
+
def location(options = {})
|
37
28
|
"#{filepath}:#{line_number}"
|
38
29
|
end
|
39
|
-
|
40
|
-
SYMBOLS = {
|
41
|
-
location: "FROM",
|
42
|
-
sql: "QUERIES",
|
43
|
-
return_value: "=>",
|
44
|
-
arguments: "<=",
|
45
|
-
defined_class: "#"
|
46
|
-
}
|
47
|
-
|
48
|
-
SYMBOLS.each do |name, symbol|
|
49
|
-
define_method "method_name_and_#{name}" do
|
50
|
-
":#{method_name} #{symbol} #{send(name)}"
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def detail_call_info
|
55
|
-
<<~MSG
|
56
|
-
#{method_name_and_defined_class}
|
57
|
-
<= #{arguments}
|
58
|
-
=> #{return_value || "nil"}
|
59
|
-
FROM #{location}
|
60
|
-
MSG
|
61
|
-
end
|
62
30
|
end
|
63
31
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
class TappingDevice
|
2
2
|
module Trackable
|
3
|
-
[:tap_on!, :tap_init!, :tap_assoc!, :
|
3
|
+
[:tap_on!, :tap_init!, :tap_assoc!, :tap_passed!].each do |method|
|
4
4
|
define_method method do |object, options = {}, &block|
|
5
5
|
new_device(options, &block).send(method, object)
|
6
6
|
end
|
@@ -8,24 +8,45 @@ class TappingDevice
|
|
8
8
|
|
9
9
|
def print_traces(target, options = {})
|
10
10
|
options[:event_type] = :call
|
11
|
+
inspect = options.delete(:inspect)
|
12
|
+
colorize = options.fetch(:colorize, true)
|
11
13
|
|
12
|
-
device_1 = tap_on!(target, options) do |
|
13
|
-
|
14
|
+
device_1 = tap_on!(target, options).and_print do |output_payload|
|
15
|
+
"Called #{output_payload.method_name_and_location(inspect: inspect, colorize: colorize)}"
|
14
16
|
end
|
15
|
-
device_2 = tap_passed!(target, options) do |
|
16
|
-
|
17
|
-
next unless arg_name
|
18
|
-
puts("Passed as '#{arg_name}' in '#{payload.defined_class}##{payload.method_name}' at #{payload.location}")
|
17
|
+
device_2 = tap_passed!(target, options).and_print do |output_payload|
|
18
|
+
output_payload.passed_at(inspect: inspect, colorize: colorize)
|
19
19
|
end
|
20
|
-
[device_1, device_2]
|
20
|
+
CollectionProxy.new([device_1, device_2])
|
21
21
|
end
|
22
22
|
|
23
|
-
def
|
24
|
-
|
23
|
+
def print_calls(target, options = {})
|
24
|
+
inspect = options.delete(:inspect)
|
25
|
+
colorize = options.fetch(:colorize, true)
|
26
|
+
|
27
|
+
tap_on!(target, options).and_print do |output_payload|
|
28
|
+
output_payload.detail_call_info(inspect: inspect, colorize: colorize)
|
29
|
+
end
|
25
30
|
end
|
26
31
|
|
27
32
|
def new_device(options, &block)
|
28
33
|
TappingDevice.new(options, &block)
|
29
34
|
end
|
35
|
+
|
36
|
+
class CollectionProxy
|
37
|
+
def initialize(devices)
|
38
|
+
@devices = devices
|
39
|
+
end
|
40
|
+
|
41
|
+
[:stop!, :stop_when, :with].each do |method|
|
42
|
+
define_method method do |&block|
|
43
|
+
@devices.each do |device|
|
44
|
+
device.send(method, &block)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
30
49
|
end
|
31
50
|
end
|
51
|
+
|
52
|
+
include TappingDevice::Trackable
|
data/tapping_device.gemspec
CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.authors = ["st0012"]
|
9
9
|
spec.email = ["stan001212@gmail.com"]
|
10
10
|
|
11
|
-
spec.summary = %q{tapping_device
|
12
|
-
spec.description = %q{tapping_device
|
11
|
+
spec.summary = %q{tapping_device lets you understand what your Ruby objects do without digging into the code}
|
12
|
+
spec.description = %q{tapping_device lets you understand what your Ruby objects do without digging into the code}
|
13
13
|
spec.homepage = "https://github.com/st0012/tapping_device"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
@@ -36,7 +36,7 @@ Gem::Specification.new do |spec|
|
|
36
36
|
spec.add_development_dependency "database_cleaner"
|
37
37
|
spec.add_development_dependency "bundler", "~> 2.0"
|
38
38
|
spec.add_development_dependency "pry"
|
39
|
-
spec.add_development_dependency "rake", "~>
|
39
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
40
40
|
spec.add_development_dependency "rspec", "~> 3.0"
|
41
41
|
spec.add_development_dependency "simplecov", "0.17.1"
|
42
42
|
end
|