tapping_device 0.5.2 → 0.5.7

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.
@@ -1,10 +1,15 @@
1
1
  require "active_record"
2
+ require "active_support/core_ext/module/introspection"
3
+ require "pry" # for using Method#source
4
+
2
5
  require "tapping_device/version"
3
6
  require "tapping_device/manageable"
4
7
  require "tapping_device/payload"
5
- require "tapping_device/output_payload"
8
+ require "tapping_device/output"
6
9
  require "tapping_device/trackable"
10
+ require "tapping_device/configurable"
7
11
  require "tapping_device/exceptions"
12
+ require "tapping_device/method_hijacker"
8
13
  require "tapping_device/trackers/initialization_tracker"
9
14
  require "tapping_device/trackers/passed_tracker"
10
15
  require "tapping_device/trackers/association_call_tracker"
@@ -23,29 +28,19 @@ class TappingDevice
23
28
 
24
29
  extend Manageable
25
30
 
31
+ include Configurable
32
+ include Output::Helpers
33
+
26
34
  def initialize(options = {}, &block)
27
35
  @block = block
28
36
  @output_block = nil
29
- @options = process_options(options)
37
+ @options = process_options(options.dup)
30
38
  @calls = []
31
39
  @disabled = false
32
40
  @with_condition = nil
33
41
  TappingDevice.devices << self
34
42
  end
35
43
 
36
- def and_print(payload_method = nil, &block)
37
- @output_block =
38
- if block
39
- -> (output_payload) { puts(block.call(output_payload)) }
40
- elsif payload_method
41
- -> (output_payload) { puts(output_payload.send(payload_method)) }
42
- else
43
- raise "need to provide either a payload method name or a block"
44
- end
45
-
46
- self
47
- end
48
-
49
44
  def with(&block)
50
45
  @with_condition = block
51
46
  end
@@ -83,6 +78,8 @@ class TappingDevice
83
78
  @target = object
84
79
  validate_target!
85
80
 
81
+ MethodHijacker.new(@target).hijack_methods! if options[:hijack_attr_methods]
82
+
86
83
  @trace_point = build_minimum_trace_point(event_type: options[:event_type]) do |payload|
87
84
  record_call!(payload)
88
85
 
@@ -97,15 +94,19 @@ class TappingDevice
97
94
  private
98
95
 
99
96
  def build_minimum_trace_point(event_type:)
100
- TracePoint.new(event_type) do |tp|
97
+ TracePoint.new(*event_type) do |tp|
101
98
  next unless filter_condition_satisfied?(tp)
102
- next if is_tapping_device_call?(tp)
103
99
 
104
100
  filepath, line_number = get_call_location(tp)
105
101
  payload = build_payload(tp: tp, filepath: filepath, line_number: line_number)
106
102
 
107
- next if should_be_skipped_by_paths?(filepath)
108
- next unless with_condition_satisfied?(payload)
103
+ unless @options[:force_recording]
104
+ next if is_tapping_device_call?(tp)
105
+ next if should_be_skipped_by_paths?(filepath)
106
+ next unless with_condition_satisfied?(payload)
107
+ next if payload.is_private_call? && @options[:ignore_private]
108
+ next if !payload.is_private_call? && @options[:only_private]
109
+ end
109
110
 
110
111
  yield(payload)
111
112
  end
@@ -130,7 +131,7 @@ class TappingDevice
130
131
 
131
132
  if Module.respond_to?(:module_parents)
132
133
  tp.defined_class.module_parents.include?(TappingDevice)
133
- else
134
+ elsif Module.respond_to?(:parents)
134
135
  tp.defined_class.parents.include?(TappingDevice)
135
136
  end
136
137
  end
@@ -151,15 +152,14 @@ class TappingDevice
151
152
  line_number: line_number,
152
153
  defined_class: tp.defined_class,
153
154
  trace: get_traces(tp),
155
+ is_private_call?: tp.defined_class.private_method_defined?(tp.callee_id),
156
+ tag: options[:tag],
154
157
  tp: tp
155
158
  })
156
159
  end
157
160
 
158
161
  def get_method_object_from(target, method_name)
159
- target.method(method_name)
160
- rescue ArgumentError
161
- method_method = Object.method(:method).unbind
162
- method_method.bind(target).call(method_name)
162
+ Object.instance_method(:method).bind(target).call(method_name)
163
163
  rescue NameError
164
164
  # if any part of the program uses Refinement to extend its methods
165
165
  # we might still get NoMethodError when trying to get that method outside the scope
@@ -201,13 +201,19 @@ class TappingDevice
201
201
  end
202
202
 
203
203
  def process_options(options)
204
- options[:filter_by_paths] ||= []
205
- options[:exclude_by_paths] ||= []
206
- options[:with_trace_to] ||= 50
207
- options[:root_device] ||= self
208
- options[:event_type] ||= :return
204
+ options[:filter_by_paths] ||= config[:filter_by_paths]
205
+ options[:exclude_by_paths] ||= config[:exclude_by_paths]
206
+ options[:with_trace_to] ||= config[:with_trace_to]
207
+ options[:event_type] ||= config[:event_type]
208
+ options[:hijack_attr_methods] ||= config[:hijack_attr_methods]
209
+ options[:track_as_records] ||= config[:track_as_records]
210
+ options[:ignore_private] ||= config[:ignore_private]
211
+ options[:only_private] ||= config[:only_private]
212
+ # for debugging the gem more easily
213
+ options[:force_recording] ||= false
214
+
209
215
  options[:descendants] ||= []
210
- options[:track_as_records] ||= false
216
+ options[:root_device] ||= self
211
217
  options
212
218
  end
213
219
 
@@ -227,7 +233,7 @@ class TappingDevice
227
233
  def record_call!(payload)
228
234
  return if @disabled
229
235
 
230
- @output_block.call(OutputPayload.init(payload)) if @output_block
236
+ write_output!(payload) if @output_writer
231
237
 
232
238
  if @block
233
239
  root_device.calls << @block.call(payload)
@@ -236,10 +242,18 @@ class TappingDevice
236
242
  end
237
243
  end
238
244
 
245
+ def write_output!(payload)
246
+ @output_writer.write!(payload)
247
+ end
248
+
239
249
  def stop_if_condition_fulfilled!(payload)
240
250
  if @stop_when&.call(payload)
241
251
  stop!
242
252
  root_device.stop!
243
253
  end
244
254
  end
255
+
256
+ def config
257
+ TappingDevice.config
258
+ end
245
259
  end
@@ -0,0 +1,27 @@
1
+ require "active_support/configurable"
2
+ require "active_support/concern"
3
+
4
+ class TappingDevice
5
+ module Configurable
6
+ extend ActiveSupport::Concern
7
+
8
+ DEFAULTS = {
9
+ filter_by_paths: [],
10
+ exclude_by_paths: [],
11
+ with_trace_to: 50,
12
+ event_type: :return,
13
+ hijack_attr_methods: false,
14
+ track_as_records: false,
15
+ ignore_private: false,
16
+ only_private: false
17
+ }.merge(TappingDevice::Output::DEFAULT_OPTIONS)
18
+
19
+ included do
20
+ include ActiveSupport::Configurable
21
+
22
+ DEFAULTS.each do |key, value|
23
+ config[key] = value
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,51 @@
1
+ class TappingDevice
2
+ class MethodHijacker
3
+ attr_reader :target
4
+
5
+ def initialize(target)
6
+ @target = target
7
+ end
8
+
9
+ def hijack_methods!
10
+ target.methods.each do |method_name|
11
+ if is_writer_method?(method_name)
12
+ redefine_writer_method!(method_name)
13
+ elsif is_reader_method?(method_name)
14
+ redefine_reader_method!(method_name)
15
+ end
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def is_writer_method?(method_name)
22
+ has_definition_source?(method_name) && method_name.match?(/\w+=/) && target.method(method_name).source.match?(/attr_writer|attr_accessor/)
23
+ end
24
+
25
+ def is_reader_method?(method_name)
26
+ has_definition_source?(method_name) && target.method(method_name).source.match?(/attr_reader|attr_accessor/)
27
+ end
28
+
29
+ def has_definition_source?(method_name)
30
+ target.method(method_name).source_location
31
+ end
32
+
33
+ def redefine_writer_method!(method_name)
34
+ ivar_name = "@#{method_name.to_s.sub("=", "")}"
35
+
36
+ target.instance_eval <<-RUBY, __FILE__, __LINE__ + 1
37
+ def #{method_name}(val)
38
+ #{ivar_name} = val
39
+ end
40
+ RUBY
41
+ end
42
+
43
+ def redefine_reader_method!(method_name)
44
+ target.instance_eval <<-RUBY, __FILE__, __LINE__ + 1
45
+ def #{method_name}
46
+ @#{method_name}
47
+ end
48
+ RUBY
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,42 @@
1
+ require "tapping_device/output/payload"
2
+ require "tapping_device/output/writer"
3
+ require "tapping_device/output/stdout_writer"
4
+ require "tapping_device/output/file_writer"
5
+
6
+ class TappingDevice
7
+ module Output
8
+ DEFAULT_OPTIONS = {
9
+ inspect: false,
10
+ colorize: true,
11
+ log_file: "/tmp/tapping_device.log"
12
+ }
13
+
14
+ module Helpers
15
+ def and_write(payload_method = nil, options: {}, &block)
16
+ and_output(payload_method, options: options, writer_klass: FileWriter, &block)
17
+ end
18
+
19
+ def and_print(payload_method = nil, options: {}, &block)
20
+ and_output(payload_method, options: options, writer_klass: StdoutWriter, &block)
21
+ end
22
+
23
+ def and_output(payload_method = nil, options: {}, writer_klass:, &block)
24
+ output_block = generate_output_block(payload_method, block)
25
+ @output_writer = writer_klass.new(options, output_block)
26
+ self
27
+ end
28
+
29
+ private
30
+
31
+ def generate_output_block(payload_method, block)
32
+ if block
33
+ block
34
+ elsif payload_method
35
+ -> (output_payload, output_options) { output_payload.send(payload_method, output_options) }
36
+ else
37
+ raise "need to provide either a payload method name or a block"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -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,166 @@
1
+ require "pastel"
2
+
3
+ class TappingDevice
4
+ module Output
5
+ class Payload < Payload
6
+ UNDEFINED = "[undefined]"
7
+ PRIVATE_MARK = " (private)"
8
+
9
+ PASTEL = Pastel.new
10
+ PASTEL.alias_color(:orange, :bright_red, :bright_yellow)
11
+
12
+ alias :raw_arguments :arguments
13
+ alias :raw_return_value :return_value
14
+
15
+ def method_name(options = {})
16
+ name = ":#{super(options)}"
17
+
18
+ name += " [#{tag}]" if tag
19
+ name += PRIVATE_MARK if is_private_call?
20
+
21
+ name
22
+ end
23
+
24
+ def arguments(options = {})
25
+ generate_string_result(raw_arguments, options[:inspect])
26
+ end
27
+
28
+ def return_value(options = {})
29
+ generate_string_result(raw_return_value, options[:inspect])
30
+ end
31
+
32
+ PAYLOAD_ATTRIBUTES = {
33
+ method_name: {symbol: "", color: :bright_blue},
34
+ location: {symbol: "from:", color: :green},
35
+ return_value: {symbol: "=>", color: :magenta},
36
+ arguments: {symbol: "<=", color: :orange},
37
+ ivar_changes: {symbol: "changes:\n", color: :blue},
38
+ defined_class: {symbol: "#", color: :yellow}
39
+ }
40
+
41
+ PAYLOAD_ATTRIBUTES.each do |attribute, attribute_options|
42
+ color = attribute_options[:color]
43
+
44
+ alias_method "original_#{attribute}".to_sym, attribute
45
+
46
+ # regenerate attributes with `colorize: true` support
47
+ define_method attribute do |options = {}|
48
+ call_result = send("original_#{attribute}", options)
49
+
50
+ if options[:colorize]
51
+ PASTEL.send(color, call_result)
52
+ else
53
+ call_result
54
+ end
55
+ end
56
+
57
+ define_method "#{attribute}_with_color" do |options = {}|
58
+ send(attribute, options.merge(colorize: true))
59
+ end
60
+
61
+ PAYLOAD_ATTRIBUTES.each do |and_attribute, and_attribute_options|
62
+ next if and_attribute == attribute
63
+
64
+ define_method "#{attribute}_and_#{and_attribute}" do |options = {}|
65
+ "#{send(attribute, options)} #{and_attribute_options[:symbol]} #{send(and_attribute, options)}"
66
+ end
67
+
68
+ define_method "#{attribute}_and_#{and_attribute}_with_color" do |options = {}|
69
+ send("#{attribute}_and_#{and_attribute}", options.merge(colorize: true))
70
+ end
71
+ end
72
+ end
73
+
74
+ def passed_at(options = {})
75
+ with_method_head = options.fetch(:with_method_head, false)
76
+ arg_name = raw_arguments.keys.detect { |k| raw_arguments[k] == target }
77
+
78
+ return unless arg_name
79
+
80
+ arg_name = ":#{arg_name}"
81
+ arg_name = PASTEL.orange(arg_name) if options[:colorize]
82
+ msg = "Passed as #{arg_name} in '#{defined_class(options)}##{method_name(options)}' at #{location(options)}\n"
83
+ msg += " > #{method_head}\n" if with_method_head
84
+ msg
85
+ end
86
+
87
+ def detail_call_info(options = {})
88
+ <<~MSG
89
+ #{method_name_and_defined_class(options)}
90
+ from: #{location(options)}
91
+ <= #{arguments(options)}
92
+ => #{return_value(options)}
93
+
94
+ MSG
95
+ end
96
+
97
+ def ivar_changes(options = {})
98
+ super.map do |ivar, value_changes|
99
+ before = generate_string_result(value_changes[:before], options[:inspect])
100
+ after = generate_string_result(value_changes[:after], options[:inspect])
101
+
102
+ if options[:colorize]
103
+ ivar = PASTEL.orange(ivar)
104
+ before = PASTEL.bright_blue(before.to_s)
105
+ after = PASTEL.bright_blue(after.to_s)
106
+ end
107
+
108
+ " #{ivar}: #{before} => #{after}"
109
+ end.join("\n")
110
+ end
111
+
112
+ def call_info_with_ivar_changes(options = {})
113
+ <<~MSG
114
+ #{method_name_and_defined_class(options)}
115
+ from: #{location(options)}
116
+ changes:
117
+ #{ivar_changes(options)}
118
+
119
+ MSG
120
+ end
121
+
122
+ private
123
+
124
+ def generate_string_result(obj, inspect)
125
+ case obj
126
+ when Array
127
+ array_to_string(obj, inspect)
128
+ when Hash
129
+ hash_to_string(obj, inspect)
130
+ when UNDEFINED
131
+ UNDEFINED
132
+ when String
133
+ "\"#{obj}\""
134
+ when nil
135
+ "nil"
136
+ else
137
+ inspect ? obj.inspect : obj.to_s
138
+ end
139
+ end
140
+
141
+ def array_to_string(array, inspect)
142
+ elements_string = array.map do |elem|
143
+ generate_string_result(elem, inspect)
144
+ end.join(", ")
145
+ "[#{elements_string}]"
146
+ end
147
+
148
+ def hash_to_string(hash, inspect)
149
+ elements_string = hash.map do |key, value|
150
+ "#{key.to_s}: #{generate_string_result(value, inspect)}"
151
+ end.join(", ")
152
+ "{#{elements_string}}"
153
+ end
154
+
155
+ def obj_to_string(element, inspect)
156
+ to_string_method = inspect ? :inspect : :to_s
157
+
158
+ if !inspect && element.is_a?(String)
159
+ "\"#{element}\""
160
+ else
161
+ element.send(to_string_method)
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end