tapping_device 0.5.0 → 0.5.5
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 +210 -0
- data/Gemfile.lock +16 -15
- data/README.md +192 -74
- data/images/print_mutations.png +0 -0
- data/lib/tapping_device.rb +112 -133
- data/lib/tapping_device/configurable.rb +27 -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 +179 -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 +2 -3
- data/lib/tapping_device/trackable.rb +114 -19
- data/lib/tapping_device/trackers/association_call_tracker.rb +17 -0
- data/lib/tapping_device/trackers/initialization_tracker.rb +41 -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 +3 -1
- metadata +42 -16
- data/lib/tapping_device/output_payload.rb +0 -145
- 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,179 @@
|
|
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
|
+
if is_private_call?
|
11
|
+
":#{super(options)} (private)"
|
12
|
+
else
|
13
|
+
":#{super(options)}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def arguments(options = {})
|
18
|
+
generate_string_result(raw_arguments, options[:inspect])
|
19
|
+
end
|
20
|
+
|
21
|
+
def return_value(options = {})
|
22
|
+
generate_string_result(raw_return_value, options[:inspect])
|
23
|
+
end
|
24
|
+
|
25
|
+
COLOR_CODES = {
|
26
|
+
green: 10,
|
27
|
+
yellow: 11,
|
28
|
+
blue: 12,
|
29
|
+
megenta: 13,
|
30
|
+
cyan: 14,
|
31
|
+
orange: 214
|
32
|
+
}
|
33
|
+
|
34
|
+
COLORS = COLOR_CODES.each_with_object({}) do |(name, code), hash|
|
35
|
+
hash[name] = "\u001b[38;5;#{code}m"
|
36
|
+
end.merge(
|
37
|
+
reset: "\u001b[0m",
|
38
|
+
nocolor: ""
|
39
|
+
)
|
40
|
+
|
41
|
+
PAYLOAD_ATTRIBUTES = {
|
42
|
+
method_name: {symbol: "", color: COLORS[:blue]},
|
43
|
+
location: {symbol: "from:", color: COLORS[:green]},
|
44
|
+
return_value: {symbol: "=>", color: COLORS[:megenta]},
|
45
|
+
arguments: {symbol: "<=", color: COLORS[:orange]},
|
46
|
+
ivar_changes: {symbol: "changes:\n", color: COLORS[:blue]},
|
47
|
+
defined_class: {symbol: "#", color: COLORS[:yellow]}
|
48
|
+
}
|
49
|
+
|
50
|
+
PAYLOAD_ATTRIBUTES.each do |attribute, attribute_options|
|
51
|
+
color = attribute_options[:color]
|
52
|
+
|
53
|
+
alias_method "original_#{attribute}".to_sym, attribute
|
54
|
+
|
55
|
+
# regenerate attributes with `colorize: true` support
|
56
|
+
define_method attribute do |options = {}|
|
57
|
+
call_result = send("original_#{attribute}", options)
|
58
|
+
|
59
|
+
if options[:colorize]
|
60
|
+
"#{color}#{call_result}#{COLORS[:reset]}"
|
61
|
+
else
|
62
|
+
call_result
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
define_method "#{attribute}_with_color" do |options = {}|
|
67
|
+
send(attribute, options.merge(colorize: true))
|
68
|
+
end
|
69
|
+
|
70
|
+
PAYLOAD_ATTRIBUTES.each do |and_attribute, and_attribute_options|
|
71
|
+
next if and_attribute == attribute
|
72
|
+
|
73
|
+
define_method "#{attribute}_and_#{and_attribute}" do |options = {}|
|
74
|
+
"#{send(attribute, options)} #{and_attribute_options[:symbol]} #{send(and_attribute, options)}"
|
75
|
+
end
|
76
|
+
|
77
|
+
define_method "#{attribute}_and_#{and_attribute}_with_color" do |options = {}|
|
78
|
+
send("#{attribute}_and_#{and_attribute}", options.merge(colorize: true))
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def passed_at(options = {})
|
84
|
+
with_method_head = options.fetch(:with_method_head, false)
|
85
|
+
arg_name = raw_arguments.keys.detect { |k| raw_arguments[k] == target }
|
86
|
+
|
87
|
+
return unless arg_name
|
88
|
+
|
89
|
+
arg_name = ":#{arg_name}"
|
90
|
+
arg_name = value_with_color(arg_name, :orange) if options[:colorize]
|
91
|
+
msg = "Passed as #{arg_name} in '#{defined_class(options)}##{method_name(options)}' at #{location(options)}\n"
|
92
|
+
msg += " > #{method_head}\n" if with_method_head
|
93
|
+
msg
|
94
|
+
end
|
95
|
+
|
96
|
+
def detail_call_info(options = {})
|
97
|
+
<<~MSG
|
98
|
+
#{method_name_and_defined_class(options)}
|
99
|
+
from: #{location(options)}
|
100
|
+
<= #{arguments(options)}
|
101
|
+
=> #{return_value(options)}
|
102
|
+
|
103
|
+
MSG
|
104
|
+
end
|
105
|
+
|
106
|
+
def ivar_changes(options = {})
|
107
|
+
super.map do |ivar, value_changes|
|
108
|
+
before = generate_string_result(value_changes[:before], options[:inspect])
|
109
|
+
after = generate_string_result(value_changes[:after], options[:inspect])
|
110
|
+
|
111
|
+
if options[:colorize]
|
112
|
+
ivar = "#{COLORS[:orange]}#{ivar}#{COLORS[:reset]}"
|
113
|
+
before = "#{COLORS[:blue]}#{before.to_s}#{COLORS[:reset]}"
|
114
|
+
after = "#{COLORS[:blue]}#{after.to_s}#{COLORS[:reset]}"
|
115
|
+
end
|
116
|
+
|
117
|
+
" #{ivar}: #{before.to_s} => #{after.to_s}"
|
118
|
+
end.join("\n")
|
119
|
+
end
|
120
|
+
|
121
|
+
def call_info_with_ivar_changes(options = {})
|
122
|
+
<<~MSG
|
123
|
+
#{method_name_and_defined_class(options)}
|
124
|
+
from: #{location(options)}
|
125
|
+
changes:
|
126
|
+
#{ivar_changes(options)}
|
127
|
+
|
128
|
+
MSG
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def value_with_color(value, color)
|
134
|
+
"#{COLORS[color]}#{value}#{COLORS[:reset]}"
|
135
|
+
end
|
136
|
+
|
137
|
+
def generate_string_result(obj, inspect)
|
138
|
+
case obj
|
139
|
+
when Array
|
140
|
+
array_to_string(obj, inspect)
|
141
|
+
when Hash
|
142
|
+
hash_to_string(obj, inspect)
|
143
|
+
when UNDEFINED
|
144
|
+
UNDEFINED
|
145
|
+
when String
|
146
|
+
"\"#{obj}\""
|
147
|
+
when nil
|
148
|
+
"nil"
|
149
|
+
else
|
150
|
+
inspect ? obj.inspect : obj.to_s
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def array_to_string(array, inspect)
|
155
|
+
elements_string = array.map do |elem|
|
156
|
+
generate_string_result(elem, inspect)
|
157
|
+
end.join(", ")
|
158
|
+
"[#{elements_string}]"
|
159
|
+
end
|
160
|
+
|
161
|
+
def hash_to_string(hash, inspect)
|
162
|
+
elements_string = hash.map do |key, value|
|
163
|
+
"#{key.to_s}: #{generate_string_result(value, inspect)}"
|
164
|
+
end.join(", ")
|
165
|
+
"{#{elements_string}}"
|
166
|
+
end
|
167
|
+
|
168
|
+
def obj_to_string(element, inspect)
|
169
|
+
to_string_method = inspect ? :inspect : :to_s
|
170
|
+
|
171
|
+
if !inspect && element.is_a?(String)
|
172
|
+
"\"#{element}\""
|
173
|
+
else
|
174
|
+
element.send(to_string_method)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
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
|
@@ -2,7 +2,7 @@ class TappingDevice
|
|
2
2
|
class Payload < Hash
|
3
3
|
ATTRS = [
|
4
4
|
:target, :receiver, :method_name, :method_object, :arguments, :return_value, :filepath, :line_number,
|
5
|
-
:defined_class, :trace, :tp, :
|
5
|
+
:defined_class, :trace, :tp, :ivar_changes, :is_private_call?
|
6
6
|
]
|
7
7
|
|
8
8
|
ATTRS.each do |attr|
|
@@ -20,8 +20,7 @@ class TappingDevice
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def method_head
|
23
|
-
|
24
|
-
IO.readlines(source_file)[source_line-1]
|
23
|
+
method_object.source.strip if method_object.source_location
|
25
24
|
end
|
26
25
|
|
27
26
|
def location(options = {})
|
@@ -1,36 +1,131 @@
|
|
1
1
|
class TappingDevice
|
2
2
|
module Trackable
|
3
|
-
|
4
|
-
|
5
|
-
|
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)
|
21
|
+
end
|
22
|
+
|
23
|
+
[:calls, :traces, :mutations].each do |subject|
|
24
|
+
[:print, :write].each do |output_action|
|
25
|
+
helper_method_name = "#{output_action}_#{subject}"
|
26
|
+
|
27
|
+
define_method helper_method_name do |target, options = {}|
|
28
|
+
send("output_#{subject}", target, options, output_action: "and_#{output_action}")
|
29
|
+
end
|
30
|
+
|
31
|
+
define_method "with_#{helper_method_name}" do |options = {}|
|
32
|
+
send(helper_method_name, self, options)
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
define_method "#{output_action}_instance_#{subject}" do |target_klass, options = {}|
|
37
|
+
collection_proxy = AsyncCollectionProxy.new
|
38
|
+
|
39
|
+
tap_init!(target_klass, options.merge(force_recording: true)) do |payload|
|
40
|
+
collection_proxy << send(helper_method_name, payload.return_value, options)
|
41
|
+
end
|
42
|
+
|
43
|
+
collection_proxy
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def output_calls(target, options = {}, output_action:)
|
51
|
+
device_options, output_options = separate_options(options)
|
52
|
+
|
53
|
+
tap_on!(target, device_options).send(output_action, options: output_options) do |output_payload, output_options|
|
54
|
+
output_payload.detail_call_info(output_options)
|
6
55
|
end
|
7
56
|
end
|
8
57
|
|
9
|
-
def
|
10
|
-
|
11
|
-
|
12
|
-
colorize = options.fetch(:colorize, true)
|
58
|
+
def output_traces(target, options = {}, output_action:)
|
59
|
+
device_options, output_options = separate_options(options)
|
60
|
+
device_options[:event_type] = :call
|
13
61
|
|
14
|
-
device_1 = tap_on!(target,
|
15
|
-
"Called #{output_payload.method_name_and_location(
|
62
|
+
device_1 = tap_on!(target, device_options).send(output_action, options: output_options) do |output_payload, output_options|
|
63
|
+
"Called #{output_payload.method_name_and_location(output_options)}\n"
|
64
|
+
end
|
65
|
+
device_2 = tap_passed!(target, device_options).send(output_action, options: output_options) do |output_payload, output_options|
|
66
|
+
output_payload.passed_at(output_options)
|
16
67
|
end
|
17
|
-
|
18
|
-
|
68
|
+
CollectionProxy.new([device_1, device_2])
|
69
|
+
end
|
70
|
+
|
71
|
+
def output_mutations(target, options = {}, output_action:)
|
72
|
+
device_options, output_options = separate_options(options)
|
73
|
+
|
74
|
+
tap_mutation!(target, device_options).send(output_action, options: output_options) do |output_payload, output_options|
|
75
|
+
output_payload.call_info_with_ivar_changes(output_options)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def separate_options(options)
|
80
|
+
output_options = Output::DEFAULT_OPTIONS.keys.each_with_object({}) do |key, hash|
|
81
|
+
hash[key] = options.fetch(key, TappingDevice.config[key])
|
82
|
+
options.delete(key)
|
19
83
|
end
|
20
|
-
|
84
|
+
|
85
|
+
[options, output_options]
|
21
86
|
end
|
22
87
|
|
23
|
-
|
24
|
-
|
25
|
-
|
88
|
+
# CollectionProxy delegates chained actions to multiple devices
|
89
|
+
class CollectionProxy
|
90
|
+
CHAINABLE_ACTIONS = [:stop!, :stop_when, :with]
|
91
|
+
|
92
|
+
def initialize(devices)
|
93
|
+
@devices = devices
|
94
|
+
end
|
26
95
|
|
27
|
-
|
28
|
-
|
96
|
+
CHAINABLE_ACTIONS.each do |method|
|
97
|
+
define_method method do |&block|
|
98
|
+
@devices.each do |device|
|
99
|
+
device.send(method, &block)
|
100
|
+
end
|
101
|
+
end
|
29
102
|
end
|
30
103
|
end
|
31
104
|
|
32
|
-
|
33
|
-
|
105
|
+
# AsyncCollectionProxy delegates chained actions to multiple device "asyncronously"
|
106
|
+
# when we use tapping methods like `tap_init!` to create sub-devices
|
107
|
+
# we need to find a way to pass the chained actions to every sub-device that's created
|
108
|
+
# and this can only happen asyncronously as we won't know when'll that happen
|
109
|
+
class AsyncCollectionProxy < CollectionProxy
|
110
|
+
def initialize(devices = [])
|
111
|
+
super
|
112
|
+
@blocks = {}
|
113
|
+
end
|
114
|
+
|
115
|
+
CHAINABLE_ACTIONS.each do |method|
|
116
|
+
define_method method do |&block|
|
117
|
+
super(&block)
|
118
|
+
@blocks[method] = block
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def <<(device)
|
123
|
+
@devices << device
|
124
|
+
|
125
|
+
@blocks.each do |method, block|
|
126
|
+
device.send(method, &block)
|
127
|
+
end
|
128
|
+
end
|
34
129
|
end
|
35
130
|
end
|
36
131
|
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,41 @@
|
|
1
|
+
class TappingDevice
|
2
|
+
module Trackers
|
3
|
+
class InitializationTracker < TappingDevice
|
4
|
+
def initialize(options = {}, &block)
|
5
|
+
super
|
6
|
+
event_type = @options[:event_type]
|
7
|
+
# if a class doesn't override the 'initialize' method
|
8
|
+
# Class.new will only trigger c_return or c_call
|
9
|
+
@options[:event_type] = [event_type, "c_#{event_type}"]
|
10
|
+
end
|
11
|
+
|
12
|
+
def track(object)
|
13
|
+
super
|
14
|
+
@options[:is_active_record_model] = target.ancestors.include?(ActiveRecord::Base)
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def build_payload(tp:, filepath:, line_number:)
|
19
|
+
payload = super
|
20
|
+
payload[:return_value] = payload[:receiver]
|
21
|
+
payload[:receiver] = target
|
22
|
+
payload
|
23
|
+
end
|
24
|
+
|
25
|
+
def validate_target!
|
26
|
+
raise NotAClassError.new(target) unless target.is_a?(Class)
|
27
|
+
end
|
28
|
+
|
29
|
+
def filter_condition_satisfied?(tp)
|
30
|
+
receiver = tp.self
|
31
|
+
method_name = tp.callee_id
|
32
|
+
|
33
|
+
if @options[:is_active_record_model]
|
34
|
+
method_name == :new && receiver.is_a?(Class) && receiver.ancestors.include?(target)
|
35
|
+
else
|
36
|
+
method_name == :initialize && receiver.is_a?(target)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|