tapping_device 0.5.1 → 0.5.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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