tapping_device 0.4.11 → 0.5.4
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 +18 -17
- data/README.md +182 -306
- 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 +118 -115
- 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 +4 -78
- data/lib/tapping_device/trackable.rb +114 -18
- data/lib/tapping_device/trackers/association_call_tracker.rb +17 -0
- data/lib/tapping_device/trackers/initialization_tracker.rb +35 -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 -3
- metadata +50 -17
- 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
|
-
@options = process_options(options)
|
37
|
+
@options = process_options(options.dup)
|
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,48 @@ 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
|
-
|
94
|
-
|
95
|
-
payload = build_payload(tp: tp, filepath: filepath, line_number: line_number, &payload_block)
|
81
|
+
MethodHijacker.new(@target).hijack_methods! if options[:hijack_attr_methods]
|
96
82
|
|
97
|
-
|
83
|
+
@trace_point = build_minimum_trace_point(event_type: options[:event_type]) do |payload|
|
84
|
+
record_call!(payload)
|
98
85
|
|
99
|
-
|
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
|
+
|
100
|
+
filepath, line_number = get_call_location(tp)
|
101
|
+
payload = build_payload(tp: tp, filepath: filepath, line_number: line_number)
|
102
|
+
|
103
|
+
unless @options[:force_recording]
|
104
|
+
next if is_tapping_device_call?(tp)
|
105
|
+
next if should_be_skipped_by_paths?(filepath)
|
106
|
+
next unless with_condition_satisfied?(payload)
|
107
|
+
next if payload.is_private_call? && @options[:ignore_private]
|
108
|
+
next if !payload.is_private_call? && @options[:only_private]
|
109
|
+
end
|
110
|
+
|
111
|
+
yield(payload)
|
117
112
|
end
|
118
113
|
end
|
119
114
|
|
120
|
-
def
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
else
|
125
|
-
[]
|
126
|
-
end
|
115
|
+
def validate_target!; end
|
116
|
+
|
117
|
+
def filter_condition_satisfied?(tp)
|
118
|
+
false
|
127
119
|
end
|
128
120
|
|
129
121
|
# this needs to be placed upfront so we can exclude noise before doing more work
|
@@ -132,8 +124,24 @@ class TappingDevice
|
|
132
124
|
(options[:filter_by_paths].present? && !options[:filter_by_paths].any? { |pattern| pattern.match?(filepath) })
|
133
125
|
end
|
134
126
|
|
127
|
+
def is_tapping_device_call?(tp)
|
128
|
+
if tp.defined_class == TappingDevice::Trackable || tp.defined_class == TappingDevice
|
129
|
+
return true
|
130
|
+
end
|
131
|
+
|
132
|
+
if Module.respond_to?(:module_parents)
|
133
|
+
tp.defined_class.module_parents.include?(TappingDevice)
|
134
|
+
elsif Module.respond_to?(:parents)
|
135
|
+
tp.defined_class.parents.include?(TappingDevice)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def with_condition_satisfied?(payload)
|
140
|
+
@with_condition.blank? || @with_condition.call(payload)
|
141
|
+
end
|
142
|
+
|
135
143
|
def build_payload(tp:, filepath:, line_number:)
|
136
|
-
|
144
|
+
Payload.init({
|
137
145
|
target: @target,
|
138
146
|
receiver: tp.self,
|
139
147
|
method_name: tp.callee_id,
|
@@ -144,49 +152,9 @@ class TappingDevice
|
|
144
152
|
line_number: line_number,
|
145
153
|
defined_class: tp.defined_class,
|
146
154
|
trace: get_traces(tp),
|
155
|
+
is_private_call?: tp.defined_class.private_method_defined?(tp.callee_id),
|
147
156
|
tp: tp
|
148
157
|
})
|
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
158
|
end
|
191
159
|
|
192
160
|
def get_method_object_from(target, method_name)
|
@@ -200,6 +168,27 @@ class TappingDevice
|
|
200
168
|
nil
|
201
169
|
end
|
202
170
|
|
171
|
+
def get_call_location(tp, padding: 0)
|
172
|
+
caller(get_trace_index(tp) + padding).first.split(":")[0..1]
|
173
|
+
end
|
174
|
+
|
175
|
+
def get_trace_index(tp)
|
176
|
+
if tp.event == :c_call
|
177
|
+
C_CALLER_START_POINT
|
178
|
+
else
|
179
|
+
CALLER_START_POINT
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def get_traces(tp)
|
184
|
+
if with_trace_to = options[:with_trace_to]
|
185
|
+
trace_index = get_trace_index(tp)
|
186
|
+
caller[trace_index..(trace_index + with_trace_to)]
|
187
|
+
else
|
188
|
+
[]
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
203
192
|
def collect_arguments(tp)
|
204
193
|
parameters =
|
205
194
|
if RUBY_VERSION.to_f >= 2.6
|
@@ -214,29 +203,28 @@ class TappingDevice
|
|
214
203
|
end
|
215
204
|
|
216
205
|
def process_options(options)
|
217
|
-
options[:filter_by_paths] ||= []
|
218
|
-
options[:exclude_by_paths] ||= []
|
219
|
-
options[:with_trace_to] ||=
|
220
|
-
options[:
|
221
|
-
options[:
|
206
|
+
options[:filter_by_paths] ||= config[:filter_by_paths]
|
207
|
+
options[:exclude_by_paths] ||= config[:exclude_by_paths]
|
208
|
+
options[:with_trace_to] ||= config[:with_trace_to]
|
209
|
+
options[:event_type] ||= config[:event_type]
|
210
|
+
options[:hijack_attr_methods] ||= config[:hijack_attr_methods]
|
211
|
+
options[:track_as_records] ||= config[:track_as_records]
|
212
|
+
options[:ignore_private] ||= config[:ignore_private]
|
213
|
+
options[:only_private] ||= config[:only_private]
|
214
|
+
# for debugging the gem more easily
|
215
|
+
options[:force_recording] ||= false
|
216
|
+
|
222
217
|
options[:descendants] ||= []
|
223
|
-
options[:
|
218
|
+
options[:root_device] ||= self
|
224
219
|
options
|
225
220
|
end
|
226
221
|
|
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)
|
222
|
+
def is_from_target?(tp)
|
235
223
|
comparsion = tp.self
|
236
|
-
is_the_same_record?(
|
224
|
+
is_the_same_record?(comparsion) || target.__id__ == comparsion.__id__
|
237
225
|
end
|
238
226
|
|
239
|
-
def is_the_same_record?(
|
227
|
+
def is_the_same_record?(comparsion)
|
240
228
|
return false unless options[:track_as_records]
|
241
229
|
if target.is_a?(ActiveRecord::Base) && comparsion.is_a?(target.class)
|
242
230
|
primary_key = target.class.primary_key
|
@@ -247,7 +235,7 @@ class TappingDevice
|
|
247
235
|
def record_call!(payload)
|
248
236
|
return if @disabled
|
249
237
|
|
250
|
-
|
238
|
+
write_output!(payload) if @output_writer
|
251
239
|
|
252
240
|
if @block
|
253
241
|
root_device.calls << @block.call(payload)
|
@@ -255,4 +243,19 @@ class TappingDevice
|
|
255
243
|
root_device.calls << payload
|
256
244
|
end
|
257
245
|
end
|
246
|
+
|
247
|
+
def write_output!(payload)
|
248
|
+
@output_writer.write!(payload)
|
249
|
+
end
|
250
|
+
|
251
|
+
def stop_if_condition_fulfilled!(payload)
|
252
|
+
if @stop_when&.call(payload)
|
253
|
+
stop!
|
254
|
+
root_device.stop!
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def config
|
259
|
+
TappingDevice.config
|
260
|
+
end
|
258
261
|
end
|
@@ -0,0 +1,27 @@
|
|
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
|
+
ignore_private: false,
|
16
|
+
only_private: false
|
17
|
+
}.merge(TappingDevice::Output::DEFAULT_OPTIONS)
|
18
|
+
|
19
|
+
included do
|
20
|
+
include ActiveSupport::Configurable
|
21
|
+
|
22
|
+
DEFAULTS.each do |key, value|
|
23
|
+
config[key] = value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
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
|