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.
@@ -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, :ivar_changes
5
+ :defined_class, :trace, :tag, :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 = {})
@@ -20,47 +20,80 @@ class TappingDevice
20
20
  TappingDevice::Trackers::MutationTracker.new(options, &block).track(object)
21
21
  end
22
22
 
23
- def print_traces(target, options = {})
24
- output_options = extract_output_options(options)
25
- options[:event_type] = :call
23
+ [:calls, :traces, :mutations].each do |subject|
24
+ [:print, :write].each do |output_action|
25
+ helper_method_name = "#{output_action}_#{subject}"
26
26
 
27
- device_1 = tap_on!(target, options).and_print do |output_payload|
28
- "Called #{output_payload.method_name_and_location(output_options)}"
29
- end
30
- device_2 = tap_passed!(target, options).and_print do |output_payload|
31
- output_payload.passed_at(output_options)
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
32
45
  end
33
- CollectionProxy.new([device_1, device_2])
34
46
  end
35
47
 
36
- def print_calls(target, options = {})
37
- output_options = extract_output_options(options)
48
+ private
49
+
50
+ def output_calls(target, options = {}, output_action:)
51
+ device_options, output_options = separate_options(options)
38
52
 
39
- tap_on!(target, options).and_print do |output_payload|
53
+ tap_on!(target, device_options).send(output_action, options: output_options) do |output_payload, output_options|
40
54
  output_payload.detail_call_info(output_options)
41
55
  end
42
56
  end
43
57
 
44
- def print_mutations(target, options = {})
45
- output_options = extract_output_options(options)
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"
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)
67
+ end
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)
46
73
 
47
- tap_mutation!(target, options).and_print do |output_payload|
74
+ tap_mutation!(target, device_options).send(output_action, options: output_options) do |output_payload, output_options|
48
75
  output_payload.call_info_with_ivar_changes(output_options)
49
76
  end
50
77
  end
51
78
 
52
- private
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
53
84
 
54
- def extract_output_options(options)
55
- {inspect: options.delete(:inspect), colorize: options.fetch(:colorize, true)}
85
+ [options, output_options]
56
86
  end
57
87
 
88
+ # CollectionProxy delegates chained actions to multiple devices
58
89
  class CollectionProxy
90
+ CHAINABLE_ACTIONS = [:stop!, :stop_when, :with]
91
+
59
92
  def initialize(devices)
60
93
  @devices = devices
61
94
  end
62
95
 
63
- [:stop!, :stop_when, :with].each do |method|
96
+ CHAINABLE_ACTIONS.each do |method|
64
97
  define_method method do |&block|
65
98
  @devices.each do |device|
66
99
  device.send(method, &block)
@@ -68,6 +101,32 @@ class TappingDevice
68
101
  end
69
102
  end
70
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
71
130
  end
72
131
  end
73
132
 
@@ -1,8 +1,25 @@
1
1
  class TappingDevice
2
2
  module Trackers
3
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
+
4
18
  def build_payload(tp:, filepath:, line_number:)
5
19
  payload = super
20
+
21
+ return payload if @is_active_record_model
22
+
6
23
  payload[:return_value] = payload[:receiver]
7
24
  payload[:receiver] = target
8
25
  payload
@@ -16,8 +33,17 @@ class TappingDevice
16
33
  receiver = tp.self
17
34
  method_name = tp.callee_id
18
35
 
19
- if target.ancestors.include?(ActiveRecord::Base)
20
- method_name == :new && receiver.ancestors.include?(target)
36
+ if @is_active_record_model
37
+ # ActiveRecord redefines model classes' .new method,
38
+ # so instead of calling Model#initialize, it'll actually call Model.new
39
+ # see https://github.com/rails/rails/blob/master/activerecord/lib/active_record/inheritance.rb#L50
40
+ method_name == :new &&
41
+ receiver.is_a?(Class) &&
42
+ # this checks if the model class is the target class or a subclass of it
43
+ receiver.ancestors.include?(target) &&
44
+ # Model.new triggers both c_return and return events. so we should only return in 1 type of the events
45
+ # otherwise the callback will be triggered twice
46
+ tp.event == :return
21
47
  else
22
48
  method_name == :initialize && receiver.is_a?(target)
23
49
  end
@@ -1,16 +1,14 @@
1
- require "pry" # for using Method#source
2
-
3
1
  class TappingDevice
4
2
  module Trackers
5
3
  class MutationTracker < TappingDevice
6
4
  def initialize(options, &block)
5
+ options[:hijack_attr_methods] = true
7
6
  super
8
7
  @snapshot_stack = []
9
8
  end
10
9
 
11
10
  def track(object)
12
11
  super
13
- hijack_attr_writers
14
12
  insert_snapshot_taking_trace_point
15
13
  self
16
14
  end
@@ -45,24 +43,6 @@ class TappingDevice
45
43
  end
46
44
  end
47
45
 
48
- def hijack_attr_writers
49
- writer_methods = target.methods.grep(/\w+=/)
50
- writer_methods.each do |method_name|
51
- if target.method(method_name).source.match?(/attr_writer|attr_accessor/)
52
- ivar_name = "@#{method_name.to_s.sub("=", "")}"
53
-
54
- # need to use instance_eval to make the call site location consistent with normal methods
55
- target.instance_eval(
56
- <<~CODE
57
- def #{method_name}(val)
58
- #{ivar_name} = val
59
- end
60
- CODE
61
- )
62
- end
63
- end
64
- end
65
-
66
46
  def build_payload(tp:, filepath:, line_number:)
67
47
  payload = super
68
48
 
@@ -78,12 +58,12 @@ class TappingDevice
78
58
 
79
59
  additional_keys = @latest_instance_variables.keys - @instance_variables_snapshot.keys
80
60
  additional_keys.each do |key|
81
- changes[key] = {before: OutputPayload::UNDEFINED, after: @latest_instance_variables[key]}
61
+ changes[key] = {before: Output::Payload::UNDEFINED, after: @latest_instance_variables[key]}
82
62
  end
83
63
 
84
64
  removed_keys = @instance_variables_snapshot.keys - @latest_instance_variables.keys
85
65
  removed_keys.each do |key|
86
- changes[key] = {before: @instance_variables_snapshot[key], after: OutputPayload::UNDEFINED}
66
+ changes[key] = {before: @instance_variables_snapshot[key], after: Output::Payload::UNDEFINED}
87
67
  end
88
68
 
89
69
  remained_keys = @latest_instance_variables.keys - additional_keys
@@ -1,3 +1,3 @@
1
1
  class TappingDevice
2
- VERSION = "0.5.2"
2
+ VERSION = "0.5.7"
3
3
  end
@@ -33,6 +33,8 @@ Gem::Specification.new do |spec|
33
33
  end
34
34
 
35
35
  spec.add_dependency "pry" # for using Method#source in MutationTracker
36
+ spec.add_dependency "activesupport"
37
+ spec.add_dependency "pastel"
36
38
 
37
39
  spec.add_development_dependency "sqlite3", ">= 1.3.6"
38
40
  spec.add_development_dependency "database_cleaner"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tapping_device
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.2
4
+ version: 0.5.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - st0012
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-06-10 00:00:00.000000000 Z
11
+ date: 2020-09-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -38,6 +38,34 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pastel
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
41
69
  - !ruby/object:Gem::Dependency
42
70
  name: sqlite3
43
71
  requirement: !ruby/object:Gem::Requirement
@@ -137,6 +165,7 @@ files:
137
165
  - ".rspec"
138
166
  - ".ruby-version"
139
167
  - ".travis.yml"
168
+ - CHANGELOG.md
140
169
  - CODE_OF_CONDUCT.md
141
170
  - Gemfile
142
171
  - Gemfile.lock
@@ -150,9 +179,15 @@ files:
150
179
  - images/print_mutations.png
151
180
  - images/print_traces.png
152
181
  - lib/tapping_device.rb
182
+ - lib/tapping_device/configurable.rb
153
183
  - lib/tapping_device/exceptions.rb
154
184
  - lib/tapping_device/manageable.rb
155
- - lib/tapping_device/output_payload.rb
185
+ - lib/tapping_device/method_hijacker.rb
186
+ - lib/tapping_device/output.rb
187
+ - lib/tapping_device/output/file_writer.rb
188
+ - lib/tapping_device/output/payload.rb
189
+ - lib/tapping_device/output/stdout_writer.rb
190
+ - lib/tapping_device/output/writer.rb
156
191
  - lib/tapping_device/payload.rb
157
192
  - lib/tapping_device/trackable.rb
158
193
  - lib/tapping_device/trackers/association_call_tracker.rb
@@ -1,173 +0,0 @@
1
- class TappingDevice
2
- class OutputPayload < Payload
3
- UNDEFINED = "[undefined]"
4
-
5
- alias :raw_arguments :arguments
6
- alias :raw_return_value :return_value
7
-
8
- def method_name(options = {})
9
- ":#{super(options)}"
10
- end
11
-
12
- def arguments(options = {})
13
- generate_string_result(raw_arguments, options[:inspect])
14
- end
15
-
16
- def return_value(options = {})
17
- generate_string_result(raw_return_value, options[:inspect])
18
- end
19
-
20
- COLOR_CODES = {
21
- green: 10,
22
- yellow: 11,
23
- blue: 12,
24
- megenta: 13,
25
- cyan: 14,
26
- orange: 214
27
- }
28
-
29
- COLORS = COLOR_CODES.each_with_object({}) do |(name, code), hash|
30
- hash[name] = "\u001b[38;5;#{code}m"
31
- end.merge(
32
- reset: "\u001b[0m",
33
- nocolor: ""
34
- )
35
-
36
- PAYLOAD_ATTRIBUTES = {
37
- method_name: {symbol: "", color: COLORS[:blue]},
38
- location: {symbol: "from:", color: COLORS[:green]},
39
- return_value: {symbol: "=>", color: COLORS[:megenta]},
40
- arguments: {symbol: "<=", color: COLORS[:orange]},
41
- ivar_changes: {symbol: "changes:\n", color: COLORS[:blue]},
42
- defined_class: {symbol: "#", color: COLORS[:yellow]}
43
- }
44
-
45
- PAYLOAD_ATTRIBUTES.each do |attribute, attribute_options|
46
- color = attribute_options[:color]
47
-
48
- alias_method "original_#{attribute}".to_sym, attribute
49
-
50
- # regenerate attributes with `colorize: true` support
51
- define_method attribute do |options = {}|
52
- call_result = send("original_#{attribute}", options)
53
-
54
- if options[:colorize]
55
- "#{color}#{call_result}#{COLORS[:reset]}"
56
- else
57
- call_result
58
- end
59
- end
60
-
61
- define_method "#{attribute}_with_color" do |options = {}|
62
- send(attribute, options.merge(colorize: true))
63
- end
64
-
65
- PAYLOAD_ATTRIBUTES.each do |and_attribute, and_attribute_options|
66
- next if and_attribute == attribute
67
-
68
- define_method "#{attribute}_and_#{and_attribute}" do |options = {}|
69
- "#{send(attribute, options)} #{and_attribute_options[:symbol]} #{send(and_attribute, options)}"
70
- end
71
-
72
- define_method "#{attribute}_and_#{and_attribute}_with_color" do |options = {}|
73
- send("#{attribute}_and_#{and_attribute}", options.merge(colorize: true))
74
- end
75
- end
76
- end
77
-
78
- def passed_at(options = {})
79
- with_method_head = options.fetch(:with_method_head, false)
80
- arg_name = raw_arguments.keys.detect { |k| raw_arguments[k] == target }
81
-
82
- return unless arg_name
83
-
84
- arg_name = ":#{arg_name}"
85
- arg_name = value_with_color(arg_name, :orange) if options[:colorize]
86
- msg = "Passed as #{arg_name} in '#{defined_class(options)}##{method_name(options)}' at #{location(options)}"
87
- msg += "\n > #{method_head.strip}" if with_method_head
88
- msg
89
- end
90
-
91
- def detail_call_info(options = {})
92
- <<~MSG
93
- #{method_name_and_defined_class(options)}
94
- from: #{location(options)}
95
- <= #{arguments(options)}
96
- => #{return_value(options)}
97
-
98
- MSG
99
- end
100
-
101
- def ivar_changes(options = {})
102
- super.map do |ivar, value_changes|
103
- before = generate_string_result(value_changes[:before], options[:inspect])
104
- after = generate_string_result(value_changes[:after], options[:inspect])
105
-
106
- if options[:colorize]
107
- ivar = "#{COLORS[:orange]}#{ivar}#{COLORS[:reset]}"
108
- before = "#{COLORS[:blue]}#{before.to_s}#{COLORS[:reset]}"
109
- after = "#{COLORS[:blue]}#{after.to_s}#{COLORS[:reset]}"
110
- end
111
-
112
- " #{ivar}: #{before.to_s} => #{after.to_s}"
113
- end.join("\n")
114
- end
115
-
116
- def call_info_with_ivar_changes(options = {})
117
- <<~MSG
118
- #{method_name_and_defined_class(options)}
119
- from: #{location(options)}
120
- changes:
121
- #{ivar_changes(options)}
122
-
123
- MSG
124
- end
125
-
126
- private
127
-
128
- def value_with_color(value, color)
129
- "#{COLORS[color]}#{value}#{COLORS[:reset]}"
130
- end
131
-
132
- def generate_string_result(obj, inspect)
133
- case obj
134
- when Array
135
- array_to_string(obj, inspect)
136
- when Hash
137
- hash_to_string(obj, inspect)
138
- when UNDEFINED
139
- UNDEFINED
140
- when String
141
- "\"#{obj}\""
142
- when nil
143
- "nil"
144
- else
145
- inspect ? obj.inspect : obj.to_s
146
- end
147
- end
148
-
149
- def array_to_string(array, inspect)
150
- elements_string = array.map do |elem|
151
- generate_string_result(elem, inspect)
152
- end.join(", ")
153
- "[#{elements_string}]"
154
- end
155
-
156
- def hash_to_string(hash, inspect)
157
- elements_string = hash.map do |key, value|
158
- "#{key.to_s}: #{generate_string_result(value, inspect)}"
159
- end.join(", ")
160
- "{#{elements_string}}"
161
- end
162
-
163
- def obj_to_string(element, inspect)
164
- to_string_method = inspect ? :inspect : :to_s
165
-
166
- if !inspect && element.is_a?(String)
167
- "\"#{element}\""
168
- else
169
- element.send(to_string_method)
170
- end
171
- end
172
- end
173
- end