tapping_device 0.4.8 → 0.5.1

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
@@ -2,9 +2,9 @@ require "active_record"
2
2
  require "tapping_device/version"
3
3
  require "tapping_device/manageable"
4
4
  require "tapping_device/payload"
5
+ require "tapping_device/output_payload"
5
6
  require "tapping_device/trackable"
6
7
  require "tapping_device/exceptions"
7
- require "tapping_device/sql_tapping_methods"
8
8
 
9
9
  class TappingDevice
10
10
 
@@ -16,7 +16,6 @@ class TappingDevice
16
16
  @devices = []
17
17
  @suspend_new = false
18
18
 
19
- include SqlTappingMethods
20
19
  extend Manageable
21
20
 
22
21
  def initialize(options = {}, &block)
@@ -25,12 +24,16 @@ class TappingDevice
25
24
  @options = process_options(options)
26
25
  @calls = []
27
26
  @disabled = false
27
+ @with_condition = nil
28
28
  self.class.devices << self
29
29
  end
30
30
 
31
31
  def tap_init!(klass)
32
32
  raise "argument should be a class, got #{klass}" unless klass.is_a?(Class)
33
- track(klass, condition: :tap_init?)
33
+ track(klass, condition: :tap_init?) do |payload|
34
+ payload[:return_value] = payload[:receiver]
35
+ payload[:receiver] = klass
36
+ end
34
37
  end
35
38
 
36
39
  def tap_on!(object)
@@ -46,8 +49,21 @@ class TappingDevice
46
49
  track(record, condition: :tap_associations?)
47
50
  end
48
51
 
49
- def and_print(payload_method)
50
- @output_block = -> (payload) { puts(payload.send(payload_method)) }
52
+ def and_print(payload_method = nil, &block)
53
+ @output_block =
54
+ if block
55
+ -> (output_payload) { puts(block.call(output_payload)) }
56
+ elsif payload_method
57
+ -> (output_payload) { puts(output_payload.send(payload_method)) }
58
+ else
59
+ raise "need to provide either a payload method name or a block"
60
+ end
61
+
62
+ self
63
+ end
64
+
65
+ def with(&block)
66
+ @with_condition = block
51
67
  end
52
68
 
53
69
  def set_block(&block)
@@ -81,7 +97,7 @@ class TappingDevice
81
97
 
82
98
  private
83
99
 
84
- def track(object, condition:)
100
+ def track(object, condition:, &payload_block)
85
101
  @target = object
86
102
  @trace_point = TracePoint.new(options[:event_type]) do |tp|
87
103
  if send(condition, object, tp)
@@ -89,7 +105,16 @@ class TappingDevice
89
105
 
90
106
  next if should_be_skipped_by_paths?(filepath)
91
107
 
92
- payload = build_payload(tp: tp, filepath: filepath, line_number: line_number)
108
+ payload = build_payload(tp: tp, filepath: filepath, line_number: line_number, &payload_block)
109
+
110
+ next unless with_condition_satisfied?(payload)
111
+
112
+ # skip TappingDevice related calls
113
+ if Module.respond_to?(:module_parents)
114
+ next if payload.defined_class.module_parents.include?(TappingDevice)
115
+ else
116
+ next if payload.defined_class.parents.include?(TappingDevice)
117
+ end
93
118
 
94
119
  record_call!(payload)
95
120
 
@@ -130,15 +155,12 @@ class TappingDevice
130
155
  end
131
156
 
132
157
  def build_payload(tp:, filepath:, line_number:)
133
- arguments = {}
134
- tp.binding.local_variables.each { |name| arguments[name] = tp.binding.local_variable_get(name) }
135
-
136
- Payload.init({
158
+ payload = Payload.init({
137
159
  target: @target,
138
160
  receiver: tp.self,
139
161
  method_name: tp.callee_id,
140
162
  method_object: get_method_object_from(tp.self, tp.callee_id),
141
- arguments: arguments,
163
+ arguments: collect_arguments(tp),
142
164
  return_value: (tp.return_value rescue nil),
143
165
  filepath: filepath,
144
166
  line_number: line_number,
@@ -146,6 +168,10 @@ class TappingDevice
146
168
  trace: get_traces(tp),
147
169
  tp: tp
148
170
  })
171
+
172
+ yield(payload) if block_given?
173
+
174
+ payload
149
175
  end
150
176
 
151
177
  def tap_init?(klass, tp)
@@ -176,15 +202,7 @@ class TappingDevice
176
202
  return false if is_from_target?(self, tp)
177
203
  return false if tp.defined_class == TappingDevice::Trackable || tp.defined_class == TappingDevice
178
204
 
179
- method_object = get_method_object_from(tp.self, tp.callee_id)
180
- return false unless method_object.is_a?(Method)
181
- # if a no-arugment method is called, tp.binding.local_variables will be those local variables in the same scope
182
- # so we need to make sure the method takes arguments, then we can be sure that the locals are arguments
183
- return false unless method_object && method_object.arity.to_i > 0
184
-
185
- argument_values = tp.binding.local_variables.map { |name| tp.binding.local_variable_get(name) }
186
-
187
- argument_values.any? do |value|
205
+ collect_arguments(tp).values.any? do |value|
188
206
  # during comparison, Ruby might perform data type conversion like calling `to_sym` on the value
189
207
  # but not every value supports every conversion methods
190
208
  object == value rescue false
@@ -204,6 +222,19 @@ class TappingDevice
204
222
  nil
205
223
  end
206
224
 
225
+ def collect_arguments(tp)
226
+ parameters =
227
+ if RUBY_VERSION.to_f >= 2.6
228
+ tp.parameters
229
+ else
230
+ get_method_object_from(tp.self, tp.callee_id)&.parameters || []
231
+ end.map { |parameter| parameter[1] }
232
+
233
+ tp.binding.local_variables.each_with_object({}) do |name, args|
234
+ args[name] = tp.binding.local_variable_get(name) if parameters.include?(name)
235
+ end
236
+ end
237
+
207
238
  def process_options(options)
208
239
  options[:filter_by_paths] ||= []
209
240
  options[:exclude_by_paths] ||= []
@@ -238,7 +269,7 @@ class TappingDevice
238
269
  def record_call!(payload)
239
270
  return if @disabled
240
271
 
241
- @output_block.call(payload) if @output_block
272
+ @output_block.call(OutputPayload.init(payload)) if @output_block
242
273
 
243
274
  if @block
244
275
  root_device.calls << @block.call(payload)
@@ -246,4 +277,8 @@ class TappingDevice
246
277
  root_device.calls << payload
247
278
  end
248
279
  end
280
+
281
+ def with_condition_satisfied?(payload)
282
+ @with_condition.blank? || @with_condition.call(payload)
283
+ end
249
284
  end
@@ -0,0 +1,145 @@
1
+ class TappingDevice
2
+ class OutputPayload < Payload
3
+ alias :raw_arguments :arguments
4
+ alias :raw_return_value :return_value
5
+
6
+ def method_name(options = {})
7
+ ":#{super(options)}"
8
+ end
9
+
10
+ def arguments(options = {})
11
+ generate_string_result(raw_arguments, options[:inspect])
12
+ end
13
+
14
+ def return_value(options = {})
15
+ generate_string_result(raw_return_value, options[:inspect])
16
+ end
17
+
18
+ def self.full_color_code(code)
19
+ end
20
+
21
+ COLOR_CODES = {
22
+ green: 10,
23
+ yellow: 11,
24
+ blue: 12,
25
+ megenta: 13,
26
+ cyan: 14,
27
+ orange: 214
28
+ }
29
+
30
+ COLORS = COLOR_CODES.each_with_object({}) do |(name, code), hash|
31
+ hash[name] = "\u001b[38;5;#{code}m"
32
+ end.merge(
33
+ reset: "\u001b[0m",
34
+ nocolor: ""
35
+ )
36
+
37
+ PAYLOAD_ATTRIBUTES = {
38
+ method_name: {symbol: "", color: COLORS[:blue]},
39
+ location: {symbol: "from:", color: COLORS[:green]},
40
+ sql: {symbol: "QUERIES", color: COLORS[:nocolor]},
41
+ return_value: {symbol: "=>", color: COLORS[:megenta]},
42
+ arguments: {symbol: "<=", color: COLORS[:orange]},
43
+ defined_class: {symbol: "#", color: COLORS[:yellow]}
44
+ }
45
+
46
+ PAYLOAD_ATTRIBUTES.each do |attribute, attribute_options|
47
+ color = attribute_options[:color]
48
+
49
+ alias_method "original_#{attribute}".to_sym, attribute
50
+
51
+ # regenerate attributes with `colorize: true` support
52
+ define_method attribute do |options = {}|
53
+ call_result = send("original_#{attribute}", options)
54
+
55
+ if options[:colorize]
56
+ "#{color}#{call_result}#{COLORS[:reset]}"
57
+ else
58
+ call_result
59
+ end
60
+ end
61
+
62
+ define_method "#{attribute}_with_color" do |options = {}|
63
+ send(attribute, options.merge(colorize: true))
64
+ end
65
+
66
+ PAYLOAD_ATTRIBUTES.each do |and_attribute, and_attribute_options|
67
+ next if and_attribute == attribute
68
+
69
+ define_method "#{attribute}_and_#{and_attribute}" do |options = {}|
70
+ "#{send(attribute, options)} #{and_attribute_options[:symbol]} #{send(and_attribute, options)}"
71
+ end
72
+
73
+ define_method "#{attribute}_and_#{and_attribute}_with_color" do |options = {}|
74
+ send("#{attribute}_and_#{and_attribute}", options.merge(colorize: true))
75
+ end
76
+ end
77
+ end
78
+
79
+ def passed_at(options = {})
80
+ with_method_head = options.fetch(:with_method_head, false)
81
+ arg_name = raw_arguments.keys.detect { |k| raw_arguments[k] == target }
82
+
83
+ return unless arg_name
84
+
85
+ arg_name = ":#{arg_name}"
86
+ arg_name = value_with_color(arg_name, :orange) if options[:colorize]
87
+ msg = "Passed as #{arg_name} in '#{defined_class(options)}##{method_name(options)}' at #{location(options)}"
88
+ msg += "\n > #{method_head.strip}" if with_method_head
89
+ msg
90
+ end
91
+
92
+ def detail_call_info(options = {})
93
+ <<~MSG
94
+ #{method_name_and_defined_class(options)}
95
+ from: #{location(options)}
96
+ <= #{arguments(options)}
97
+ => #{return_value(options)}
98
+
99
+ MSG
100
+ end
101
+
102
+ private
103
+
104
+ def value_with_color(value, color)
105
+ "#{COLORS[color]}#{value}#{COLORS[:reset]}"
106
+ end
107
+
108
+ def generate_string_result(obj, inspect)
109
+ case obj
110
+ when Array
111
+ array_to_string(obj, inspect)
112
+ when Hash
113
+ hash_to_string(obj, inspect)
114
+ when String
115
+ "\"#{obj}\""
116
+ else
117
+ inspect ? obj.inspect : obj.to_s
118
+ end
119
+ end
120
+
121
+ def array_to_string(array, inspect)
122
+ elements_string = array.map do |elem|
123
+ generate_string_result(elem, inspect)
124
+ end.join(", ")
125
+ "[#{elements_string}]"
126
+ end
127
+
128
+ def hash_to_string(hash, inspect)
129
+ elements_string = hash.map do |key, value|
130
+ "#{key.to_s}: #{generate_string_result(value, inspect)}"
131
+ end.join(", ")
132
+ "{#{elements_string}}"
133
+ end
134
+
135
+ def obj_to_string(element, inspect)
136
+ to_string_method = inspect ? :inspect : :to_s
137
+
138
+ if !inspect && element.is_a?(String)
139
+ "\"#{element}\""
140
+ else
141
+ element.send(to_string_method)
142
+ end
143
+ end
144
+ end
145
+ end
@@ -6,7 +6,7 @@ class TappingDevice
6
6
  ]
7
7
 
8
8
  ATTRS.each do |attr|
9
- define_method attr do
9
+ define_method attr do |options = {}|
10
10
  self[attr]
11
11
  end
12
12
  end
@@ -19,45 +19,13 @@ class TappingDevice
19
19
  h
20
20
  end
21
21
 
22
- def passed_at(with_method_head: false)
23
- arg_name = arguments.keys.detect { |k| arguments[k] == target }
24
- return unless arg_name
25
- msg = "Passed as '#{arg_name}' in method ':#{method_name}'"
26
- msg += "\n > #{method_head.strip}" if with_method_head
27
- msg += "\n at #{location}"
28
- msg
29
- end
30
-
31
22
  def method_head
32
23
  source_file, source_line = method_object.source_location
33
24
  IO.readlines(source_file)[source_line-1]
34
25
  end
35
26
 
36
- def location
27
+ def location(options = {})
37
28
  "#{filepath}:#{line_number}"
38
29
  end
39
-
40
- SYMBOLS = {
41
- location: "FROM",
42
- sql: "QUERIES",
43
- return_value: "=>",
44
- arguments: "<=",
45
- defined_class: "#"
46
- }
47
-
48
- SYMBOLS.each do |name, symbol|
49
- define_method "method_name_and_#{name}" do
50
- ":#{method_name} #{symbol} #{send(name)}"
51
- end
52
- end
53
-
54
- def detail_call_info
55
- <<~MSG
56
- #{method_name_and_defined_class}
57
- <= #{arguments}
58
- => #{return_value || "nil"}
59
- FROM #{location}
60
- MSG
61
- end
62
30
  end
63
31
  end
@@ -1,6 +1,6 @@
1
1
  class TappingDevice
2
2
  module Trackable
3
- [:tap_on!, :tap_init!, :tap_assoc!, :tap_sql!, :tap_passed!].each do |method|
3
+ [:tap_on!, :tap_init!, :tap_assoc!, :tap_passed!].each do |method|
4
4
  define_method method do |object, options = {}, &block|
5
5
  new_device(options, &block).send(method, object)
6
6
  end
@@ -8,24 +8,45 @@ class TappingDevice
8
8
 
9
9
  def print_traces(target, options = {})
10
10
  options[:event_type] = :call
11
+ inspect = options.delete(:inspect)
12
+ colorize = options.fetch(:colorize, true)
11
13
 
12
- device_1 = tap_on!(target, options) do |payload|
13
- puts("Called #{payload.method_name_and_location}")
14
+ device_1 = tap_on!(target, options).and_print do |output_payload|
15
+ "Called #{output_payload.method_name_and_location(inspect: inspect, colorize: colorize)}"
14
16
  end
15
- device_2 = tap_passed!(target, options) do |payload|
16
- arg_name = payload.arguments.keys.detect { |k| payload.arguments[k] == target }
17
- next unless arg_name
18
- puts("Passed as '#{arg_name}' in '#{payload.defined_class}##{payload.method_name}' at #{payload.location}")
17
+ device_2 = tap_passed!(target, options).and_print do |output_payload|
18
+ output_payload.passed_at(inspect: inspect, colorize: colorize)
19
19
  end
20
- [device_1, device_2]
20
+ CollectionProxy.new([device_1, device_2])
21
21
  end
22
22
 
23
- def print_calls_in_detail(target, options = {})
24
- tap_on!(target, options).and_print(:detail_call_info)
23
+ def print_calls(target, options = {})
24
+ inspect = options.delete(:inspect)
25
+ colorize = options.fetch(:colorize, true)
26
+
27
+ tap_on!(target, options).and_print do |output_payload|
28
+ output_payload.detail_call_info(inspect: inspect, colorize: colorize)
29
+ end
25
30
  end
26
31
 
27
32
  def new_device(options, &block)
28
33
  TappingDevice.new(options, &block)
29
34
  end
35
+
36
+ class CollectionProxy
37
+ def initialize(devices)
38
+ @devices = devices
39
+ end
40
+
41
+ [:stop!, :stop_when, :with].each do |method|
42
+ define_method method do |&block|
43
+ @devices.each do |device|
44
+ device.send(method, &block)
45
+ end
46
+ end
47
+ end
48
+ end
30
49
  end
31
50
  end
51
+
52
+ include TappingDevice::Trackable
@@ -1,3 +1,3 @@
1
1
  class TappingDevice
2
- VERSION = "0.4.8"
2
+ VERSION = "0.5.1"
3
3
  end
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["st0012"]
9
9
  spec.email = ["stan001212@gmail.com"]
10
10
 
11
- spec.summary = %q{tapping_device provides useful helpers to intercept method calls}
12
- spec.description = %q{tapping_device provides useful helpers to intercept method calls}
11
+ spec.summary = %q{tapping_device lets you understand what your Ruby objects do without digging into the code}
12
+ spec.description = %q{tapping_device lets you understand what your Ruby objects do without digging into the code}
13
13
  spec.homepage = "https://github.com/st0012/tapping_device"
14
14
  spec.license = "MIT"
15
15
 
@@ -36,7 +36,7 @@ Gem::Specification.new do |spec|
36
36
  spec.add_development_dependency "database_cleaner"
37
37
  spec.add_development_dependency "bundler", "~> 2.0"
38
38
  spec.add_development_dependency "pry"
39
- spec.add_development_dependency "rake", "~> 10.0"
39
+ spec.add_development_dependency "rake", "~> 13.0"
40
40
  spec.add_development_dependency "rspec", "~> 3.0"
41
41
  spec.add_development_dependency "simplecov", "0.17.1"
42
42
  end