tapping_device 0.4.10 → 0.5.3
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 +11 -4
- data/CHANGELOG.md +208 -0
- data/Gemfile.lock +22 -23
- data/README.md +206 -297
- data/images/print_calls - single entry.png +0 -0
- data/images/print_calls.png +0 -0
- data/images/print_mutations.png +0 -0
- data/images/print_traces.png +0 -0
- data/lib/tapping_device.rb +108 -114
- data/lib/tapping_device/configurable.rb +25 -0
- data/lib/tapping_device/exceptions.rb +12 -0
- data/lib/tapping_device/method_hijacker.rb +51 -0
- data/lib/tapping_device/output.rb +42 -0
- data/lib/tapping_device/output/file_writer.rb +21 -0
- data/lib/tapping_device/output/payload.rb +175 -0
- data/lib/tapping_device/output/stdout_writer.rb +9 -0
- data/lib/tapping_device/output/writer.rb +20 -0
- data/lib/tapping_device/payload.rb +4 -47
- data/lib/tapping_device/trackable.rb +84 -18
- data/lib/tapping_device/trackers/association_call_tracker.rb +17 -0
- data/lib/tapping_device/trackers/initialization_tracker.rb +27 -0
- data/lib/tapping_device/trackers/method_call_tracker.rb +9 -0
- data/lib/tapping_device/trackers/mutation_tracker.rb +112 -0
- data/lib/tapping_device/trackers/passed_tracker.rb +16 -0
- data/lib/tapping_device/version.rb +1 -1
- data/tapping_device.gemspec +5 -5
- metadata +41 -22
- data/lib/tapping_device/sql_tapping_methods.rb +0 -89
@@ -0,0 +1,21 @@
|
|
1
|
+
class TappingDevice
|
2
|
+
module Output
|
3
|
+
class FileWriter < Writer
|
4
|
+
def initialize(options, output_block)
|
5
|
+
@path = options[:log_file]
|
6
|
+
|
7
|
+
File.write(@path, "") # clean file
|
8
|
+
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
def write!(payload)
|
13
|
+
output = generate_output(payload)
|
14
|
+
|
15
|
+
File.open(@path, "a") do |f|
|
16
|
+
f << output
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
class TappingDevice
|
2
|
+
module Output
|
3
|
+
class Payload < Payload
|
4
|
+
UNDEFINED = "[undefined]"
|
5
|
+
|
6
|
+
alias :raw_arguments :arguments
|
7
|
+
alias :raw_return_value :return_value
|
8
|
+
|
9
|
+
def method_name(options = {})
|
10
|
+
":#{super(options)}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def arguments(options = {})
|
14
|
+
generate_string_result(raw_arguments, options[:inspect])
|
15
|
+
end
|
16
|
+
|
17
|
+
def return_value(options = {})
|
18
|
+
generate_string_result(raw_return_value, options[:inspect])
|
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
|
+
return_value: {symbol: "=>", color: COLORS[:megenta]},
|
41
|
+
arguments: {symbol: "<=", color: COLORS[:orange]},
|
42
|
+
ivar_changes: {symbol: "changes:\n", color: COLORS[:blue]},
|
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)}\n"
|
88
|
+
msg += " > #{method_head}\n" 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
|
+
def ivar_changes(options = {})
|
103
|
+
super.map do |ivar, value_changes|
|
104
|
+
before = generate_string_result(value_changes[:before], options[:inspect])
|
105
|
+
after = generate_string_result(value_changes[:after], options[:inspect])
|
106
|
+
|
107
|
+
if options[:colorize]
|
108
|
+
ivar = "#{COLORS[:orange]}#{ivar}#{COLORS[:reset]}"
|
109
|
+
before = "#{COLORS[:blue]}#{before.to_s}#{COLORS[:reset]}"
|
110
|
+
after = "#{COLORS[:blue]}#{after.to_s}#{COLORS[:reset]}"
|
111
|
+
end
|
112
|
+
|
113
|
+
" #{ivar}: #{before.to_s} => #{after.to_s}"
|
114
|
+
end.join("\n")
|
115
|
+
end
|
116
|
+
|
117
|
+
def call_info_with_ivar_changes(options = {})
|
118
|
+
<<~MSG
|
119
|
+
#{method_name_and_defined_class(options)}
|
120
|
+
from: #{location(options)}
|
121
|
+
changes:
|
122
|
+
#{ivar_changes(options)}
|
123
|
+
|
124
|
+
MSG
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
def value_with_color(value, color)
|
130
|
+
"#{COLORS[color]}#{value}#{COLORS[:reset]}"
|
131
|
+
end
|
132
|
+
|
133
|
+
def generate_string_result(obj, inspect)
|
134
|
+
case obj
|
135
|
+
when Array
|
136
|
+
array_to_string(obj, inspect)
|
137
|
+
when Hash
|
138
|
+
hash_to_string(obj, inspect)
|
139
|
+
when UNDEFINED
|
140
|
+
UNDEFINED
|
141
|
+
when String
|
142
|
+
"\"#{obj}\""
|
143
|
+
when nil
|
144
|
+
"nil"
|
145
|
+
else
|
146
|
+
inspect ? obj.inspect : obj.to_s
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def array_to_string(array, inspect)
|
151
|
+
elements_string = array.map do |elem|
|
152
|
+
generate_string_result(elem, inspect)
|
153
|
+
end.join(", ")
|
154
|
+
"[#{elements_string}]"
|
155
|
+
end
|
156
|
+
|
157
|
+
def hash_to_string(hash, inspect)
|
158
|
+
elements_string = hash.map do |key, value|
|
159
|
+
"#{key.to_s}: #{generate_string_result(value, inspect)}"
|
160
|
+
end.join(", ")
|
161
|
+
"{#{elements_string}}"
|
162
|
+
end
|
163
|
+
|
164
|
+
def obj_to_string(element, inspect)
|
165
|
+
to_string_method = inspect ? :inspect : :to_s
|
166
|
+
|
167
|
+
if !inspect && element.is_a?(String)
|
168
|
+
"\"#{element}\""
|
169
|
+
else
|
170
|
+
element.send(to_string_method)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class TappingDevice
|
2
|
+
module Output
|
3
|
+
class Writer
|
4
|
+
def initialize(options, output_block)
|
5
|
+
@options = options
|
6
|
+
@output_block = output_block
|
7
|
+
end
|
8
|
+
|
9
|
+
def write!(payload)
|
10
|
+
raise NotImplementedError
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def generate_output(payload)
|
16
|
+
@output_block.call(Output::Payload.init(payload), @options)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -1,13 +1,12 @@
|
|
1
|
-
require "awesome_print"
|
2
1
|
class TappingDevice
|
3
2
|
class Payload < Hash
|
4
3
|
ATTRS = [
|
5
4
|
:target, :receiver, :method_name, :method_object, :arguments, :return_value, :filepath, :line_number,
|
6
|
-
:defined_class, :trace, :tp, :
|
5
|
+
:defined_class, :trace, :tp, :ivar_changes
|
7
6
|
]
|
8
7
|
|
9
8
|
ATTRS.each do |attr|
|
10
|
-
define_method attr do
|
9
|
+
define_method attr do |options = {}|
|
11
10
|
self[attr]
|
12
11
|
end
|
13
12
|
end
|
@@ -20,54 +19,12 @@ class TappingDevice
|
|
20
19
|
h
|
21
20
|
end
|
22
21
|
|
23
|
-
def passed_at(with_method_head: false)
|
24
|
-
arg_name = arguments.keys.detect { |k| arguments[k] == target }
|
25
|
-
return unless arg_name
|
26
|
-
msg = "Passed as '#{arg_name}' in method ':#{method_name}'"
|
27
|
-
msg += "\n > #{method_head.strip}" if with_method_head
|
28
|
-
msg += "\n at #{location}"
|
29
|
-
msg
|
30
|
-
end
|
31
|
-
|
32
22
|
def method_head
|
33
|
-
|
34
|
-
IO.readlines(source_file)[source_line-1]
|
23
|
+
method_object.source.strip if method_object.source_location
|
35
24
|
end
|
36
25
|
|
37
|
-
def location
|
26
|
+
def location(options = {})
|
38
27
|
"#{filepath}:#{line_number}"
|
39
28
|
end
|
40
|
-
|
41
|
-
SYMBOLS = {
|
42
|
-
location: "from:",
|
43
|
-
sql: "QUERIES",
|
44
|
-
return_value: "=>",
|
45
|
-
arguments: "<=",
|
46
|
-
defined_class: "#"
|
47
|
-
}
|
48
|
-
|
49
|
-
SYMBOLS.each do |name, symbol|
|
50
|
-
define_method "method_name_and_#{name}" do
|
51
|
-
":#{method_name} #{symbol} #{send(name)}"
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
def detail_call_info(awesome_print: false)
|
56
|
-
arguments_output = arguments.inspect
|
57
|
-
return_value_output = return_value.inspect
|
58
|
-
|
59
|
-
if awesome_print
|
60
|
-
arguments_output = arguments.ai(ruby19_syntax: true, multiline: false)
|
61
|
-
return_value_output = return_value.ai(ruby19_syntax: true, multiline: false)
|
62
|
-
end
|
63
|
-
|
64
|
-
<<~MSG
|
65
|
-
#{method_name_and_defined_class}
|
66
|
-
from: #{location}
|
67
|
-
<= #{arguments_output}
|
68
|
-
=> #{return_value_output}
|
69
|
-
|
70
|
-
MSG
|
71
|
-
end
|
72
29
|
end
|
73
30
|
end
|
@@ -1,35 +1,101 @@
|
|
1
1
|
class TappingDevice
|
2
2
|
module Trackable
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
def tap_init!(object, options = {}, &block)
|
4
|
+
TappingDevice::Trackers::InitializationTracker.new(options, &block).track(object)
|
5
|
+
end
|
6
|
+
|
7
|
+
def tap_passed!(object, options = {}, &block)
|
8
|
+
TappingDevice::Trackers::PassedTracker.new(options, &block).track(object)
|
9
|
+
end
|
10
|
+
|
11
|
+
def tap_assoc!(object, options = {}, &block)
|
12
|
+
TappingDevice::Trackers::AssociactionCallTracker.new(options, &block).track(object)
|
13
|
+
end
|
14
|
+
|
15
|
+
def tap_on!(object, options = {}, &block)
|
16
|
+
TappingDevice::Trackers::MethodCallTracker.new(options, &block).track(object)
|
17
|
+
end
|
18
|
+
|
19
|
+
def tap_mutation!(object, options = {}, &block)
|
20
|
+
TappingDevice::Trackers::MutationTracker.new(options, &block).track(object)
|
7
21
|
end
|
8
22
|
|
9
23
|
def print_traces(target, options = {})
|
10
|
-
options
|
24
|
+
output_traces(target, options, output_action: :and_print)
|
25
|
+
end
|
26
|
+
|
27
|
+
def write_traces(target, options = {})
|
28
|
+
output_traces(target, options, output_action: :and_write)
|
29
|
+
end
|
30
|
+
|
31
|
+
def print_calls(target, options = {})
|
32
|
+
output_calls(target, options, output_action: :and_print)
|
33
|
+
end
|
34
|
+
|
35
|
+
def write_calls(target, options = {})
|
36
|
+
output_calls(target, options, output_action: :and_write)
|
37
|
+
end
|
11
38
|
|
12
|
-
|
13
|
-
|
39
|
+
def print_mutations(target, options = {})
|
40
|
+
output_mutations(target, options, output_action: :and_print)
|
41
|
+
end
|
42
|
+
|
43
|
+
def write_mutations(target, options = {})
|
44
|
+
output_mutations(target, options, output_action: :and_write)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def output_calls(target, options = {}, output_action:)
|
50
|
+
device_options, output_options = separate_options(options)
|
51
|
+
|
52
|
+
tap_on!(target, device_options).send(output_action, options: output_options) do |output_payload, output_options|
|
53
|
+
output_payload.detail_call_info(output_options)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def output_traces(target, options = {}, output_action:)
|
58
|
+
device_options, output_options = separate_options(options)
|
59
|
+
device_options[:event_type] = :call
|
60
|
+
|
61
|
+
device_1 = tap_on!(target, device_options).send(output_action, options: output_options) do |output_payload, output_options|
|
62
|
+
"Called #{output_payload.method_name_and_location(output_options)}\n"
|
14
63
|
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}")
|
64
|
+
device_2 = tap_passed!(target, device_options).send(output_action, options: output_options) do |output_payload, output_options|
|
65
|
+
output_payload.passed_at(output_options)
|
19
66
|
end
|
20
|
-
[device_1, device_2]
|
67
|
+
CollectionProxy.new([device_1, device_2])
|
21
68
|
end
|
22
69
|
|
23
|
-
def
|
24
|
-
|
70
|
+
def output_mutations(target, options = {}, output_action:)
|
71
|
+
device_options, output_options = separate_options(options)
|
72
|
+
|
73
|
+
tap_mutation!(target, device_options).send(output_action, options: output_options) do |output_payload, output_options|
|
74
|
+
output_payload.call_info_with_ivar_changes(output_options)
|
75
|
+
end
|
76
|
+
end
|
25
77
|
|
26
|
-
|
27
|
-
|
78
|
+
def separate_options(options)
|
79
|
+
output_options = Output::DEFAULT_OPTIONS.keys.each_with_object({}) do |key, hash|
|
80
|
+
hash[key] = options.fetch(key, TappingDevice.config[key])
|
81
|
+
options.delete(key)
|
28
82
|
end
|
83
|
+
|
84
|
+
[options, output_options]
|
29
85
|
end
|
30
86
|
|
31
|
-
|
32
|
-
|
87
|
+
class CollectionProxy
|
88
|
+
def initialize(devices)
|
89
|
+
@devices = devices
|
90
|
+
end
|
91
|
+
|
92
|
+
[:stop!, :stop_when, :with].each do |method|
|
93
|
+
define_method method do |&block|
|
94
|
+
@devices.each do |device|
|
95
|
+
device.send(method, &block)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
33
99
|
end
|
34
100
|
end
|
35
101
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class TappingDevice
|
2
|
+
module Trackers
|
3
|
+
class AssociactionCallTracker < TappingDevice
|
4
|
+
def validate_target!
|
5
|
+
raise NotAnActiveRecordInstanceError.new(target) unless target.is_a?(ActiveRecord::Base)
|
6
|
+
end
|
7
|
+
|
8
|
+
def filter_condition_satisfied?(tp)
|
9
|
+
return false unless is_from_target?(tp)
|
10
|
+
|
11
|
+
model_class = target.class
|
12
|
+
associations = model_class.reflections
|
13
|
+
associations.keys.include?(tp.callee_id.to_s)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class TappingDevice
|
2
|
+
module Trackers
|
3
|
+
class InitializationTracker < TappingDevice
|
4
|
+
def build_payload(tp:, filepath:, line_number:)
|
5
|
+
payload = super
|
6
|
+
payload[:return_value] = payload[:receiver]
|
7
|
+
payload[:receiver] = target
|
8
|
+
payload
|
9
|
+
end
|
10
|
+
|
11
|
+
def validate_target!
|
12
|
+
raise NotAClassError.new(target) unless target.is_a?(Class)
|
13
|
+
end
|
14
|
+
|
15
|
+
def filter_condition_satisfied?(tp)
|
16
|
+
receiver = tp.self
|
17
|
+
method_name = tp.callee_id
|
18
|
+
|
19
|
+
if target.ancestors.include?(ActiveRecord::Base)
|
20
|
+
method_name == :new && receiver.ancestors.include?(target)
|
21
|
+
else
|
22
|
+
method_name == :initialize && receiver.is_a?(target)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|