tapping_device 0.5.0 → 0.5.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|