tapping_device 0.5.0 → 0.5.5

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