tapping_device 0.5.1 → 0.5.6

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.
@@ -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,9 @@
1
+ class TappingDevice
2
+ module Output
3
+ class StdoutWriter < Writer
4
+ def write!(payload)
5
+ puts(generate_output(payload))
6
+ end
7
+ end
8
+ end
9
+ 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, :sql
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
- source_file, source_line = method_object.source_location
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,44 +1,99 @@
1
1
  class TappingDevice
2
2
  module Trackable
3
- [:tap_on!, :tap_init!, :tap_assoc!, :tap_passed!].each do |method|
4
- define_method method do |object, options = {}, &block|
5
- new_device(options, &block).send(method, object)
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
6
45
  end
7
46
  end
8
47
 
9
- def print_traces(target, options = {})
10
- options[:event_type] = :call
11
- inspect = options.delete(:inspect)
12
- colorize = options.fetch(:colorize, true)
48
+ private
13
49
 
14
- device_1 = tap_on!(target, options).and_print do |output_payload|
15
- "Called #{output_payload.method_name_and_location(inspect: inspect, colorize: colorize)}"
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)
55
+ end
56
+ end
57
+
58
+ def output_traces(target, options = {}, output_action:)
59
+ device_options, output_options = separate_options(options)
60
+ device_options[:event_type] = :call
61
+
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"
16
64
  end
17
- device_2 = tap_passed!(target, options).and_print do |output_payload|
18
- output_payload.passed_at(inspect: inspect, colorize: colorize)
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)
19
67
  end
20
68
  CollectionProxy.new([device_1, device_2])
21
69
  end
22
70
 
23
- def print_calls(target, options = {})
24
- inspect = options.delete(:inspect)
25
- colorize = options.fetch(:colorize, true)
71
+ def output_mutations(target, options = {}, output_action:)
72
+ device_options, output_options = separate_options(options)
26
73
 
27
- tap_on!(target, options).and_print do |output_payload|
28
- output_payload.detail_call_info(inspect: inspect, colorize: colorize)
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)
29
76
  end
30
77
  end
31
78
 
32
- def new_device(options, &block)
33
- TappingDevice.new(options, &block)
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)
83
+ end
84
+
85
+ [options, output_options]
34
86
  end
35
87
 
88
+ # CollectionProxy delegates chained actions to multiple devices
36
89
  class CollectionProxy
90
+ CHAINABLE_ACTIONS = [:stop!, :stop_when, :with]
91
+
37
92
  def initialize(devices)
38
93
  @devices = devices
39
94
  end
40
95
 
41
- [:stop!, :stop_when, :with].each do |method|
96
+ CHAINABLE_ACTIONS.each do |method|
42
97
  define_method method do |&block|
43
98
  @devices.each do |device|
44
99
  device.send(method, &block)
@@ -46,6 +101,32 @@ class TappingDevice
46
101
  end
47
102
  end
48
103
  end
104
+
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
129
+ end
49
130
  end
50
131
  end
51
132
 
@@ -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,44 @@
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
+ @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
+
21
+ return payload if @is_active_record_model
22
+
23
+ payload[:return_value] = payload[:receiver]
24
+ payload[:receiver] = target
25
+ payload
26
+ end
27
+
28
+ def validate_target!
29
+ raise NotAClassError.new(target) unless target.is_a?(Class)
30
+ end
31
+
32
+ def filter_condition_satisfied?(tp)
33
+ receiver = tp.self
34
+ method_name = tp.callee_id
35
+
36
+ if @is_active_record_model
37
+ method_name == :new && receiver.is_a?(Class) && receiver.ancestors.include?(target)
38
+ else
39
+ method_name == :initialize && receiver.is_a?(target)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end