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.
Binary file
Binary file
@@ -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/sql_tapping_methods"
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
- self.class.devices << self
29
- end
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 tap_passed!(object)
44
- track(object, condition: :tap_passed?)
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
- self.class.delete_device(self)
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
- private
86
-
87
- def track(object, condition:, &payload_block)
77
+ def track(object)
88
78
  @target = object
89
- @trace_point = TracePoint.new(options[:event_type]) do |tp|
90
- if send(condition, object, tp)
91
- filepath, line_number = get_call_location(tp)
79
+ validate_target!
92
80
 
93
- next if should_be_skipped_by_paths?(filepath)
81
+ MethodHijacker.new(@target).hijack_methods! if options[:hijack_attr_methods]
94
82
 
95
- payload = build_payload(tp: tp, filepath: filepath, line_number: line_number, &payload_block)
83
+ @trace_point = build_minimum_trace_point(event_type: options[:event_type]) do |payload|
84
+ record_call!(payload)
96
85
 
97
- record_call!(payload)
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 self.class.suspend_new
89
+ @trace_point.enable unless TappingDevice.suspend_new
104
90
 
105
91
  self
106
92
  end
107
93
 
108
- def get_call_location(tp, padding: 0)
109
- caller(get_trace_index(tp) + padding).first.split(":")[0..1]
110
- end
94
+ private
111
95
 
112
- def get_trace_index(tp)
113
- if tp.event == :c_call
114
- C_CALLER_START_POINT
115
- else
116
- CALLER_START_POINT
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 get_traces(tp)
121
- if with_trace_to = options[:with_trace_to]
122
- trace_index = get_trace_index(tp)
123
- caller[trace_index..(trace_index + with_trace_to)]
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
- payload = Payload.init({
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] ||= 50
220
- options[:root_device] ||= self
221
- options[:event_type] ||= :return
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[:track_as_records] ||= false
209
+ options[:root_device] ||= self
224
210
  options
225
211
  end
226
212
 
227
- def stop_if_condition_fulfilled(payload)
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?(object, comparsion) || object.__id__ == comparsion.__id__
215
+ is_the_same_record?(comparsion) || target.__id__ == comparsion.__id__
237
216
  end
238
217
 
239
- def is_the_same_record?(target, comparsion)
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
- @output_block.call(payload) if @output_block
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