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
Binary file
|
Binary file
|
Binary file
|
Binary file
|
data/lib/tapping_device.rb
CHANGED
@@ -1,10 +1,20 @@
|
|
1
1
|
require "active_record"
|
2
|
+
require "active_support/core_ext/module/introspection"
|
3
|
+
require "pry" # for using Method#source
|
4
|
+
|
2
5
|
require "tapping_device/version"
|
3
6
|
require "tapping_device/manageable"
|
4
7
|
require "tapping_device/payload"
|
8
|
+
require "tapping_device/output"
|
5
9
|
require "tapping_device/trackable"
|
10
|
+
require "tapping_device/configurable"
|
6
11
|
require "tapping_device/exceptions"
|
7
|
-
require "tapping_device/
|
12
|
+
require "tapping_device/method_hijacker"
|
13
|
+
require "tapping_device/trackers/initialization_tracker"
|
14
|
+
require "tapping_device/trackers/passed_tracker"
|
15
|
+
require "tapping_device/trackers/association_call_tracker"
|
16
|
+
require "tapping_device/trackers/method_call_tracker"
|
17
|
+
require "tapping_device/trackers/mutation_tracker"
|
8
18
|
|
9
19
|
class TappingDevice
|
10
20
|
|
@@ -16,41 +26,23 @@ class TappingDevice
|
|
16
26
|
@devices = []
|
17
27
|
@suspend_new = false
|
18
28
|
|
19
|
-
include SqlTappingMethods
|
20
29
|
extend Manageable
|
21
30
|
|
31
|
+
include Configurable
|
32
|
+
include Output::Helpers
|
33
|
+
|
22
34
|
def initialize(options = {}, &block)
|
23
35
|
@block = block
|
24
36
|
@output_block = nil
|
25
37
|
@options = process_options(options)
|
26
38
|
@calls = []
|
27
39
|
@disabled = false
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
def tap_init!(klass)
|
32
|
-
raise "argument should be a class, got #{klass}" unless klass.is_a?(Class)
|
33
|
-
track(klass, condition: :tap_init?) do |payload|
|
34
|
-
payload[:return_value] = payload[:receiver]
|
35
|
-
payload[:receiver] = klass
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
def tap_on!(object)
|
40
|
-
track(object, condition: :tap_on?)
|
40
|
+
@with_condition = nil
|
41
|
+
TappingDevice.devices << self
|
41
42
|
end
|
42
43
|
|
43
|
-
def
|
44
|
-
|
45
|
-
end
|
46
|
-
|
47
|
-
def tap_assoc!(record)
|
48
|
-
raise "argument should be an instance of ActiveRecord::Base" unless record.is_a?(ActiveRecord::Base)
|
49
|
-
track(record, condition: :tap_associations?)
|
50
|
-
end
|
51
|
-
|
52
|
-
def and_print(payload_method)
|
53
|
-
@output_block = -> (payload) { puts(payload.send(payload_method)) }
|
44
|
+
def with(&block)
|
45
|
+
@with_condition = block
|
54
46
|
end
|
55
47
|
|
56
48
|
def set_block(&block)
|
@@ -59,7 +51,7 @@ class TappingDevice
|
|
59
51
|
|
60
52
|
def stop!
|
61
53
|
@disabled = true
|
62
|
-
|
54
|
+
TappingDevice.delete_device(self)
|
63
55
|
end
|
64
56
|
|
65
57
|
def stop_when(&block)
|
@@ -82,48 +74,44 @@ class TappingDevice
|
|
82
74
|
options[:descendants]
|
83
75
|
end
|
84
76
|
|
85
|
-
|
86
|
-
|
87
|
-
def track(object, condition:, &payload_block)
|
77
|
+
def track(object)
|
88
78
|
@target = object
|
89
|
-
|
90
|
-
if send(condition, object, tp)
|
91
|
-
filepath, line_number = get_call_location(tp)
|
79
|
+
validate_target!
|
92
80
|
|
93
|
-
|
81
|
+
MethodHijacker.new(@target).hijack_methods! if options[:hijack_attr_methods]
|
94
82
|
|
95
|
-
|
83
|
+
@trace_point = build_minimum_trace_point(event_type: options[:event_type]) do |payload|
|
84
|
+
record_call!(payload)
|
96
85
|
|
97
|
-
|
98
|
-
|
99
|
-
stop_if_condition_fulfilled(payload)
|
100
|
-
end
|
86
|
+
stop_if_condition_fulfilled!(payload)
|
101
87
|
end
|
102
88
|
|
103
|
-
@trace_point.enable unless
|
89
|
+
@trace_point.enable unless TappingDevice.suspend_new
|
104
90
|
|
105
91
|
self
|
106
92
|
end
|
107
93
|
|
108
|
-
|
109
|
-
caller(get_trace_index(tp) + padding).first.split(":")[0..1]
|
110
|
-
end
|
94
|
+
private
|
111
95
|
|
112
|
-
def
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
96
|
+
def build_minimum_trace_point(event_type:)
|
97
|
+
TracePoint.new(event_type) do |tp|
|
98
|
+
next unless filter_condition_satisfied?(tp)
|
99
|
+
next if is_tapping_device_call?(tp)
|
100
|
+
|
101
|
+
filepath, line_number = get_call_location(tp)
|
102
|
+
payload = build_payload(tp: tp, filepath: filepath, line_number: line_number)
|
103
|
+
|
104
|
+
next if should_be_skipped_by_paths?(filepath)
|
105
|
+
next unless with_condition_satisfied?(payload)
|
106
|
+
|
107
|
+
yield(payload)
|
117
108
|
end
|
118
109
|
end
|
119
110
|
|
120
|
-
def
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
else
|
125
|
-
[]
|
126
|
-
end
|
111
|
+
def validate_target!; end
|
112
|
+
|
113
|
+
def filter_condition_satisfied?(tp)
|
114
|
+
false
|
127
115
|
end
|
128
116
|
|
129
117
|
# this needs to be placed upfront so we can exclude noise before doing more work
|
@@ -132,8 +120,24 @@ class TappingDevice
|
|
132
120
|
(options[:filter_by_paths].present? && !options[:filter_by_paths].any? { |pattern| pattern.match?(filepath) })
|
133
121
|
end
|
134
122
|
|
123
|
+
def is_tapping_device_call?(tp)
|
124
|
+
if tp.defined_class == TappingDevice::Trackable || tp.defined_class == TappingDevice
|
125
|
+
return true
|
126
|
+
end
|
127
|
+
|
128
|
+
if Module.respond_to?(:module_parents)
|
129
|
+
tp.defined_class.module_parents.include?(TappingDevice)
|
130
|
+
elsif Module.respond_to?(:parents)
|
131
|
+
tp.defined_class.parents.include?(TappingDevice)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def with_condition_satisfied?(payload)
|
136
|
+
@with_condition.blank? || @with_condition.call(payload)
|
137
|
+
end
|
138
|
+
|
135
139
|
def build_payload(tp:, filepath:, line_number:)
|
136
|
-
|
140
|
+
Payload.init({
|
137
141
|
target: @target,
|
138
142
|
receiver: tp.self,
|
139
143
|
method_name: tp.callee_id,
|
@@ -146,47 +150,6 @@ class TappingDevice
|
|
146
150
|
trace: get_traces(tp),
|
147
151
|
tp: tp
|
148
152
|
})
|
149
|
-
|
150
|
-
yield(payload) if block_given?
|
151
|
-
|
152
|
-
payload
|
153
|
-
end
|
154
|
-
|
155
|
-
def tap_init?(klass, tp)
|
156
|
-
receiver = tp.self
|
157
|
-
method_name = tp.callee_id
|
158
|
-
|
159
|
-
if klass.ancestors.include?(ActiveRecord::Base)
|
160
|
-
method_name == :new && receiver.ancestors.include?(klass)
|
161
|
-
else
|
162
|
-
method_name == :initialize && receiver.is_a?(klass)
|
163
|
-
end
|
164
|
-
end
|
165
|
-
|
166
|
-
def tap_on?(object, tp)
|
167
|
-
is_from_target?(object, tp)
|
168
|
-
end
|
169
|
-
|
170
|
-
def tap_associations?(object, tp)
|
171
|
-
return false unless tap_on?(object, tp)
|
172
|
-
|
173
|
-
model_class = object.class
|
174
|
-
associations = model_class.reflections
|
175
|
-
associations.keys.include?(tp.callee_id.to_s)
|
176
|
-
end
|
177
|
-
|
178
|
-
def tap_passed?(object, tp)
|
179
|
-
# we don't care about calls from the device instance or helper methods
|
180
|
-
return false if is_from_target?(self, tp)
|
181
|
-
return false if tp.defined_class == TappingDevice::Trackable || tp.defined_class == TappingDevice
|
182
|
-
|
183
|
-
collect_arguments(tp).values.any? do |value|
|
184
|
-
# during comparison, Ruby might perform data type conversion like calling `to_sym` on the value
|
185
|
-
# but not every value supports every conversion methods
|
186
|
-
object == value rescue false
|
187
|
-
end
|
188
|
-
rescue
|
189
|
-
false
|
190
153
|
end
|
191
154
|
|
192
155
|
def get_method_object_from(target, method_name)
|
@@ -200,6 +163,27 @@ class TappingDevice
|
|
200
163
|
nil
|
201
164
|
end
|
202
165
|
|
166
|
+
def get_call_location(tp, padding: 0)
|
167
|
+
caller(get_trace_index(tp) + padding).first.split(":")[0..1]
|
168
|
+
end
|
169
|
+
|
170
|
+
def get_trace_index(tp)
|
171
|
+
if tp.event == :c_call
|
172
|
+
C_CALLER_START_POINT
|
173
|
+
else
|
174
|
+
CALLER_START_POINT
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def get_traces(tp)
|
179
|
+
if with_trace_to = options[:with_trace_to]
|
180
|
+
trace_index = get_trace_index(tp)
|
181
|
+
caller[trace_index..(trace_index + with_trace_to)]
|
182
|
+
else
|
183
|
+
[]
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
203
187
|
def collect_arguments(tp)
|
204
188
|
parameters =
|
205
189
|
if RUBY_VERSION.to_f >= 2.6
|
@@ -214,29 +198,24 @@ class TappingDevice
|
|
214
198
|
end
|
215
199
|
|
216
200
|
def process_options(options)
|
217
|
-
options[:filter_by_paths] ||= []
|
218
|
-
options[:exclude_by_paths] ||= []
|
219
|
-
options[:with_trace_to] ||=
|
220
|
-
options[:
|
221
|
-
options[:
|
201
|
+
options[:filter_by_paths] ||= config[:filter_by_paths]
|
202
|
+
options[:exclude_by_paths] ||= config[:exclude_by_paths]
|
203
|
+
options[:with_trace_to] ||= config[:with_trace_to]
|
204
|
+
options[:event_type] ||= config[:event_type]
|
205
|
+
options[:hijack_attr_methods] ||= config[:hijack_attr_methods]
|
206
|
+
options[:track_as_records] ||= config[:track_as_records]
|
207
|
+
|
222
208
|
options[:descendants] ||= []
|
223
|
-
options[:
|
209
|
+
options[:root_device] ||= self
|
224
210
|
options
|
225
211
|
end
|
226
212
|
|
227
|
-
def
|
228
|
-
if @stop_when&.call(payload)
|
229
|
-
stop!
|
230
|
-
root_device.stop!
|
231
|
-
end
|
232
|
-
end
|
233
|
-
|
234
|
-
def is_from_target?(object, tp)
|
213
|
+
def is_from_target?(tp)
|
235
214
|
comparsion = tp.self
|
236
|
-
is_the_same_record?(
|
215
|
+
is_the_same_record?(comparsion) || target.__id__ == comparsion.__id__
|
237
216
|
end
|
238
217
|
|
239
|
-
def is_the_same_record?(
|
218
|
+
def is_the_same_record?(comparsion)
|
240
219
|
return false unless options[:track_as_records]
|
241
220
|
if target.is_a?(ActiveRecord::Base) && comparsion.is_a?(target.class)
|
242
221
|
primary_key = target.class.primary_key
|
@@ -247,7 +226,7 @@ class TappingDevice
|
|
247
226
|
def record_call!(payload)
|
248
227
|
return if @disabled
|
249
228
|
|
250
|
-
|
229
|
+
write_output!(payload) if @output_writer
|
251
230
|
|
252
231
|
if @block
|
253
232
|
root_device.calls << @block.call(payload)
|
@@ -255,4 +234,19 @@ class TappingDevice
|
|
255
234
|
root_device.calls << payload
|
256
235
|
end
|
257
236
|
end
|
237
|
+
|
238
|
+
def write_output!(payload)
|
239
|
+
@output_writer.write!(payload)
|
240
|
+
end
|
241
|
+
|
242
|
+
def stop_if_condition_fulfilled!(payload)
|
243
|
+
if @stop_when&.call(payload)
|
244
|
+
stop!
|
245
|
+
root_device.stop!
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def config
|
250
|
+
TappingDevice.config
|
251
|
+
end
|
258
252
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require "active_support/configurable"
|
2
|
+
require "active_support/concern"
|
3
|
+
|
4
|
+
class TappingDevice
|
5
|
+
module Configurable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
DEFAULTS = {
|
9
|
+
filter_by_paths: [],
|
10
|
+
exclude_by_paths: [],
|
11
|
+
with_trace_to: 50,
|
12
|
+
event_type: :return,
|
13
|
+
hijack_attr_methods: false,
|
14
|
+
track_as_records: false,
|
15
|
+
}.merge(TappingDevice::Output::DEFAULT_OPTIONS)
|
16
|
+
|
17
|
+
included do
|
18
|
+
include ActiveSupport::Configurable
|
19
|
+
|
20
|
+
DEFAULTS.each do |key, value|
|
21
|
+
config[key] = value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -1,4 +1,16 @@
|
|
1
1
|
class TappingDevice
|
2
2
|
class Exception < StandardError
|
3
3
|
end
|
4
|
+
|
5
|
+
class NotAnActiveRecordInstanceError < Exception
|
6
|
+
def initialize(object)
|
7
|
+
super("target object should be an instance of ActiveRecord::Base, got #{object}")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class NotAClassError < Exception
|
12
|
+
def initialize(object)
|
13
|
+
super("target object should be a class, got #{object}")
|
14
|
+
end
|
15
|
+
end
|
4
16
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
class TappingDevice
|
2
|
+
class MethodHijacker
|
3
|
+
attr_reader :target
|
4
|
+
|
5
|
+
def initialize(target)
|
6
|
+
@target = target
|
7
|
+
end
|
8
|
+
|
9
|
+
def hijack_methods!
|
10
|
+
target.methods.each do |method_name|
|
11
|
+
if is_writer_method?(method_name)
|
12
|
+
redefine_writer_method!(method_name)
|
13
|
+
elsif is_reader_method?(method_name)
|
14
|
+
redefine_reader_method!(method_name)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def is_writer_method?(method_name)
|
22
|
+
has_definition_source?(method_name) && method_name.match?(/\w+=/) && target.method(method_name).source.match?(/attr_writer|attr_accessor/)
|
23
|
+
end
|
24
|
+
|
25
|
+
def is_reader_method?(method_name)
|
26
|
+
has_definition_source?(method_name) && target.method(method_name).source.match?(/attr_reader|attr_accessor/)
|
27
|
+
end
|
28
|
+
|
29
|
+
def has_definition_source?(method_name)
|
30
|
+
target.method(method_name).source_location
|
31
|
+
end
|
32
|
+
|
33
|
+
def redefine_writer_method!(method_name)
|
34
|
+
ivar_name = "@#{method_name.to_s.sub("=", "")}"
|
35
|
+
|
36
|
+
target.instance_eval <<-RUBY, __FILE__, __LINE__ + 1
|
37
|
+
def #{method_name}(val)
|
38
|
+
#{ivar_name} = val
|
39
|
+
end
|
40
|
+
RUBY
|
41
|
+
end
|
42
|
+
|
43
|
+
def redefine_reader_method!(method_name)
|
44
|
+
target.instance_eval <<-RUBY, __FILE__, __LINE__ + 1
|
45
|
+
def #{method_name}
|
46
|
+
@#{method_name}
|
47
|
+
end
|
48
|
+
RUBY
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require "tapping_device/output/payload"
|
2
|
+
require "tapping_device/output/writer"
|
3
|
+
require "tapping_device/output/stdout_writer"
|
4
|
+
require "tapping_device/output/file_writer"
|
5
|
+
|
6
|
+
class TappingDevice
|
7
|
+
module Output
|
8
|
+
DEFAULT_OPTIONS = {
|
9
|
+
inspect: false,
|
10
|
+
colorize: true,
|
11
|
+
log_file: "/tmp/tapping_device.log"
|
12
|
+
}
|
13
|
+
|
14
|
+
module Helpers
|
15
|
+
def and_write(payload_method = nil, options: {}, &block)
|
16
|
+
and_output(payload_method, options: options, writer_klass: FileWriter, &block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def and_print(payload_method = nil, options: {}, &block)
|
20
|
+
and_output(payload_method, options: options, writer_klass: StdoutWriter, &block)
|
21
|
+
end
|
22
|
+
|
23
|
+
def and_output(payload_method = nil, options: {}, writer_klass:, &block)
|
24
|
+
output_block = generate_output_block(payload_method, block)
|
25
|
+
@output_writer = writer_klass.new(options, output_block)
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def generate_output_block(payload_method, block)
|
32
|
+
if block
|
33
|
+
block
|
34
|
+
elsif payload_method
|
35
|
+
-> (output_payload, output_options) { output_payload.send(payload_method, output_options) }
|
36
|
+
else
|
37
|
+
raise "need to provide either a payload method name or a block"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|