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.
@@ -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,175 @@
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
+ ":#{super(options)}"
11
+ end
12
+
13
+ def arguments(options = {})
14
+ generate_string_result(raw_arguments, options[:inspect])
15
+ end
16
+
17
+ def return_value(options = {})
18
+ generate_string_result(raw_return_value, options[:inspect])
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
+ return_value: {symbol: "=>", color: COLORS[:megenta]},
41
+ arguments: {symbol: "<=", color: COLORS[:orange]},
42
+ ivar_changes: {symbol: "changes:\n", color: COLORS[:blue]},
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)}\n"
88
+ msg += " > #{method_head}\n" 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
+ def ivar_changes(options = {})
103
+ super.map do |ivar, value_changes|
104
+ before = generate_string_result(value_changes[:before], options[:inspect])
105
+ after = generate_string_result(value_changes[:after], options[:inspect])
106
+
107
+ if options[:colorize]
108
+ ivar = "#{COLORS[:orange]}#{ivar}#{COLORS[:reset]}"
109
+ before = "#{COLORS[:blue]}#{before.to_s}#{COLORS[:reset]}"
110
+ after = "#{COLORS[:blue]}#{after.to_s}#{COLORS[:reset]}"
111
+ end
112
+
113
+ " #{ivar}: #{before.to_s} => #{after.to_s}"
114
+ end.join("\n")
115
+ end
116
+
117
+ def call_info_with_ivar_changes(options = {})
118
+ <<~MSG
119
+ #{method_name_and_defined_class(options)}
120
+ from: #{location(options)}
121
+ changes:
122
+ #{ivar_changes(options)}
123
+
124
+ MSG
125
+ end
126
+
127
+ private
128
+
129
+ def value_with_color(value, color)
130
+ "#{COLORS[color]}#{value}#{COLORS[:reset]}"
131
+ end
132
+
133
+ def generate_string_result(obj, inspect)
134
+ case obj
135
+ when Array
136
+ array_to_string(obj, inspect)
137
+ when Hash
138
+ hash_to_string(obj, inspect)
139
+ when UNDEFINED
140
+ UNDEFINED
141
+ when String
142
+ "\"#{obj}\""
143
+ when nil
144
+ "nil"
145
+ else
146
+ inspect ? obj.inspect : obj.to_s
147
+ end
148
+ end
149
+
150
+ def array_to_string(array, inspect)
151
+ elements_string = array.map do |elem|
152
+ generate_string_result(elem, inspect)
153
+ end.join(", ")
154
+ "[#{elements_string}]"
155
+ end
156
+
157
+ def hash_to_string(hash, inspect)
158
+ elements_string = hash.map do |key, value|
159
+ "#{key.to_s}: #{generate_string_result(value, inspect)}"
160
+ end.join(", ")
161
+ "{#{elements_string}}"
162
+ end
163
+
164
+ def obj_to_string(element, inspect)
165
+ to_string_method = inspect ? :inspect : :to_s
166
+
167
+ if !inspect && element.is_a?(String)
168
+ "\"#{element}\""
169
+ else
170
+ element.send(to_string_method)
171
+ end
172
+ end
173
+ end
174
+ end
175
+ 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
@@ -1,13 +1,12 @@
1
- require "awesome_print"
2
1
  class TappingDevice
3
2
  class Payload < Hash
4
3
  ATTRS = [
5
4
  :target, :receiver, :method_name, :method_object, :arguments, :return_value, :filepath, :line_number,
6
- :defined_class, :trace, :tp, :sql
5
+ :defined_class, :trace, :tp, :ivar_changes
7
6
  ]
8
7
 
9
8
  ATTRS.each do |attr|
10
- define_method attr do
9
+ define_method attr do |options = {}|
11
10
  self[attr]
12
11
  end
13
12
  end
@@ -20,54 +19,12 @@ class TappingDevice
20
19
  h
21
20
  end
22
21
 
23
- def passed_at(with_method_head: false)
24
- arg_name = arguments.keys.detect { |k| arguments[k] == target }
25
- return unless arg_name
26
- msg = "Passed as '#{arg_name}' in method ':#{method_name}'"
27
- msg += "\n > #{method_head.strip}" if with_method_head
28
- msg += "\n at #{location}"
29
- msg
30
- end
31
-
32
22
  def method_head
33
- source_file, source_line = method_object.source_location
34
- IO.readlines(source_file)[source_line-1]
23
+ method_object.source.strip if method_object.source_location
35
24
  end
36
25
 
37
- def location
26
+ def location(options = {})
38
27
  "#{filepath}:#{line_number}"
39
28
  end
40
-
41
- SYMBOLS = {
42
- location: "from:",
43
- sql: "QUERIES",
44
- return_value: "=>",
45
- arguments: "<=",
46
- defined_class: "#"
47
- }
48
-
49
- SYMBOLS.each do |name, symbol|
50
- define_method "method_name_and_#{name}" do
51
- ":#{method_name} #{symbol} #{send(name)}"
52
- end
53
- end
54
-
55
- def detail_call_info(awesome_print: false)
56
- arguments_output = arguments.inspect
57
- return_value_output = return_value.inspect
58
-
59
- if awesome_print
60
- arguments_output = arguments.ai(ruby19_syntax: true, multiline: false)
61
- return_value_output = return_value.ai(ruby19_syntax: true, multiline: false)
62
- end
63
-
64
- <<~MSG
65
- #{method_name_and_defined_class}
66
- from: #{location}
67
- <= #{arguments_output}
68
- => #{return_value_output}
69
-
70
- MSG
71
- end
72
29
  end
73
30
  end
@@ -1,35 +1,101 @@
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)
6
- end
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)
7
21
  end
8
22
 
9
23
  def print_traces(target, options = {})
10
- options[:event_type] = :call
24
+ output_traces(target, options, output_action: :and_print)
25
+ end
26
+
27
+ def write_traces(target, options = {})
28
+ output_traces(target, options, output_action: :and_write)
29
+ end
30
+
31
+ def print_calls(target, options = {})
32
+ output_calls(target, options, output_action: :and_print)
33
+ end
34
+
35
+ def write_calls(target, options = {})
36
+ output_calls(target, options, output_action: :and_write)
37
+ end
11
38
 
12
- device_1 = tap_on!(target, options) do |payload|
13
- puts("Called #{payload.method_name_and_location}")
39
+ def print_mutations(target, options = {})
40
+ output_mutations(target, options, output_action: :and_print)
41
+ end
42
+
43
+ def write_mutations(target, options = {})
44
+ output_mutations(target, options, output_action: :and_write)
45
+ end
46
+
47
+ private
48
+
49
+ def output_calls(target, options = {}, output_action:)
50
+ device_options, output_options = separate_options(options)
51
+
52
+ tap_on!(target, device_options).send(output_action, options: output_options) do |output_payload, output_options|
53
+ output_payload.detail_call_info(output_options)
54
+ end
55
+ end
56
+
57
+ def output_traces(target, options = {}, output_action:)
58
+ device_options, output_options = separate_options(options)
59
+ device_options[:event_type] = :call
60
+
61
+ device_1 = tap_on!(target, device_options).send(output_action, options: output_options) do |output_payload, output_options|
62
+ "Called #{output_payload.method_name_and_location(output_options)}\n"
14
63
  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}")
64
+ device_2 = tap_passed!(target, device_options).send(output_action, options: output_options) do |output_payload, output_options|
65
+ output_payload.passed_at(output_options)
19
66
  end
20
- [device_1, device_2]
67
+ CollectionProxy.new([device_1, device_2])
21
68
  end
22
69
 
23
- def print_calls_in_detail(target, options = {})
24
- awesome_print = options.delete(:awesome_print)
70
+ def output_mutations(target, options = {}, output_action:)
71
+ device_options, output_options = separate_options(options)
72
+
73
+ tap_mutation!(target, device_options).send(output_action, options: output_options) do |output_payload, output_options|
74
+ output_payload.call_info_with_ivar_changes(output_options)
75
+ end
76
+ end
25
77
 
26
- tap_on!(target, options) do |payload|
27
- puts(payload.detail_call_info(awesome_print: awesome_print))
78
+ def separate_options(options)
79
+ output_options = Output::DEFAULT_OPTIONS.keys.each_with_object({}) do |key, hash|
80
+ hash[key] = options.fetch(key, TappingDevice.config[key])
81
+ options.delete(key)
28
82
  end
83
+
84
+ [options, output_options]
29
85
  end
30
86
 
31
- def new_device(options, &block)
32
- TappingDevice.new(options, &block)
87
+ class CollectionProxy
88
+ def initialize(devices)
89
+ @devices = devices
90
+ end
91
+
92
+ [:stop!, :stop_when, :with].each do |method|
93
+ define_method method do |&block|
94
+ @devices.each do |device|
95
+ device.send(method, &block)
96
+ end
97
+ end
98
+ end
33
99
  end
34
100
  end
35
101
  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,27 @@
1
+ class TappingDevice
2
+ module Trackers
3
+ class InitializationTracker < TappingDevice
4
+ def build_payload(tp:, filepath:, line_number:)
5
+ payload = super
6
+ payload[:return_value] = payload[:receiver]
7
+ payload[:receiver] = target
8
+ payload
9
+ end
10
+
11
+ def validate_target!
12
+ raise NotAClassError.new(target) unless target.is_a?(Class)
13
+ end
14
+
15
+ def filter_condition_satisfied?(tp)
16
+ receiver = tp.self
17
+ method_name = tp.callee_id
18
+
19
+ if target.ancestors.include?(ActiveRecord::Base)
20
+ method_name == :new && receiver.ancestors.include?(target)
21
+ else
22
+ method_name == :initialize && receiver.is_a?(target)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end