tapping_device 0.4.9 → 0.5.2

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,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,53 +19,13 @@ 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
23
  source_file, source_line = method_object.source_location
34
24
  IO.readlines(source_file)[source_line-1]
35
25
  end
36
26
 
37
- def location
27
+ def location(options = {})
38
28
  "#{filepath}:#{line_number}"
39
29
  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
- MSG
70
- end
71
30
  end
72
31
  end
@@ -1,35 +1,74 @@
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 = {})
24
+ output_options = extract_output_options(options)
10
25
  options[:event_type] = :call
11
26
 
12
- device_1 = tap_on!(target, options) do |payload|
13
- puts("Called #{payload.method_name_and_location}")
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)
14
32
  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}")
33
+ CollectionProxy.new([device_1, device_2])
34
+ end
35
+
36
+ def print_calls(target, options = {})
37
+ output_options = extract_output_options(options)
38
+
39
+ tap_on!(target, options).and_print do |output_payload|
40
+ output_payload.detail_call_info(output_options)
19
41
  end
20
- [device_1, device_2]
21
42
  end
22
43
 
23
- def print_calls_in_detail(target, options = {})
24
- awesome_print = options.delete(:awesome_print)
44
+ def print_mutations(target, options = {})
45
+ output_options = extract_output_options(options)
25
46
 
26
- tap_on!(target, options) do |payload|
27
- puts(payload.detail_call_info(awesome_print: awesome_print))
47
+ tap_mutation!(target, options).and_print do |output_payload|
48
+ output_payload.call_info_with_ivar_changes(output_options)
28
49
  end
29
50
  end
30
51
 
31
- def new_device(options, &block)
32
- TappingDevice.new(options, &block)
52
+ private
53
+
54
+ def extract_output_options(options)
55
+ {inspect: options.delete(:inspect), colorize: options.fetch(:colorize, true)}
56
+ end
57
+
58
+ class CollectionProxy
59
+ def initialize(devices)
60
+ @devices = devices
61
+ end
62
+
63
+ [:stop!, :stop_when, :with].each do |method|
64
+ define_method method do |&block|
65
+ @devices.each do |device|
66
+ device.send(method, &block)
67
+ end
68
+ end
69
+ end
33
70
  end
34
71
  end
35
72
  end
73
+
74
+ include TappingDevice::Trackable
@@ -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
@@ -0,0 +1,9 @@
1
+ class TappingDevice
2
+ module Trackers
3
+ class MethodCallTracker < TappingDevice
4
+ def filter_condition_satisfied?(tp)
5
+ is_from_target?(tp)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,132 @@
1
+ require "pry" # for using Method#source
2
+
3
+ class TappingDevice
4
+ module Trackers
5
+ class MutationTracker < TappingDevice
6
+ def initialize(options, &block)
7
+ super
8
+ @snapshot_stack = []
9
+ end
10
+
11
+ def track(object)
12
+ super
13
+ hijack_attr_writers
14
+ insert_snapshot_taking_trace_point
15
+ self
16
+ end
17
+
18
+ def stop!
19
+ super
20
+ @ivar_snapshot_trace_point.disable
21
+ end
22
+
23
+ private
24
+
25
+ # we need to snapshot instance variables at the beginning of every method call
26
+ # so we can get a correct state for the later comparison
27
+ def insert_snapshot_taking_trace_point
28
+ @ivar_snapshot_trace_point = build_minimum_trace_point(event_type: :call) do
29
+ snapshot_instance_variables
30
+ end
31
+
32
+ @ivar_snapshot_trace_point.enable unless TappingDevice.suspend_new
33
+ end
34
+
35
+ def filter_condition_satisfied?(tp)
36
+ return false unless is_from_target?(tp)
37
+
38
+ if snapshot_capturing_event?(tp)
39
+ true
40
+ else
41
+ @latest_instance_variables = target_instance_variables
42
+ @instance_variables_snapshot = @snapshot_stack.pop
43
+
44
+ @latest_instance_variables != @instance_variables_snapshot
45
+ end
46
+ end
47
+
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
+ def build_payload(tp:, filepath:, line_number:)
67
+ payload = super
68
+
69
+ if change_capturing_event?(tp)
70
+ payload[:ivar_changes] = capture_ivar_changes
71
+ end
72
+
73
+ payload
74
+ end
75
+
76
+ def capture_ivar_changes
77
+ changes = {}
78
+
79
+ additional_keys = @latest_instance_variables.keys - @instance_variables_snapshot.keys
80
+ additional_keys.each do |key|
81
+ changes[key] = {before: OutputPayload::UNDEFINED, after: @latest_instance_variables[key]}
82
+ end
83
+
84
+ removed_keys = @instance_variables_snapshot.keys - @latest_instance_variables.keys
85
+ removed_keys.each do |key|
86
+ changes[key] = {before: @instance_variables_snapshot[key], after: OutputPayload::UNDEFINED}
87
+ end
88
+
89
+ remained_keys = @latest_instance_variables.keys - additional_keys
90
+ remained_keys.each do |key|
91
+ next if @latest_instance_variables[key] == @instance_variables_snapshot[key]
92
+ changes[key] = {before: @instance_variables_snapshot[key], after: @latest_instance_variables[key]}
93
+ end
94
+
95
+ changes
96
+ end
97
+
98
+ def snapshot_instance_variables
99
+ @snapshot_stack.push(target_instance_variables)
100
+ end
101
+
102
+ def target_instance_variables
103
+ target.instance_variables.each_with_object({}) do |ivar, hash|
104
+ hash[ivar] = target.instance_variable_get(ivar)
105
+ end
106
+ end
107
+
108
+ def snapshot_capturing_event?(tp)
109
+ tp.event == :call
110
+ end
111
+
112
+ def change_capturing_event?(tp)
113
+ !snapshot_capturing_event?(tp)
114
+ end
115
+
116
+ # belows are debugging helpers
117
+ # I'll leave them for a while in case there's a bug in the tracker
118
+ def print_snapshot_stack(tp)
119
+ puts("===== STACK - #{tp.callee_id} (#{tp.event}) =====")
120
+ puts(@snapshot_stack)
121
+ puts("================ END STACK =================")
122
+ end
123
+
124
+ def print_state_comparison
125
+ puts("###############")
126
+ puts(@latest_instance_variables)
127
+ puts(@instance_variables_snapshot)
128
+ puts("###############")
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,16 @@
1
+ class TappingDevice
2
+ module Trackers
3
+ # PassedTracker tracks calls that use the target object as an argument
4
+ class PassedTracker < TappingDevice
5
+ def filter_condition_satisfied?(tp)
6
+ collect_arguments(tp).values.any? do |value|
7
+ # during comparison, Ruby might perform data type conversion like calling `to_sym` on the value
8
+ # but not every value supports every conversion methods
9
+ target == value rescue false
10
+ end
11
+ rescue
12
+ false
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,3 +1,3 @@
1
1
  class TappingDevice
2
- VERSION = "0.4.9"
2
+ VERSION = "0.5.2"
3
3
  end
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["st0012"]
9
9
  spec.email = ["stan001212@gmail.com"]
10
10
 
11
- spec.summary = %q{tapping_device provides useful helpers to intercept method calls}
12
- spec.description = %q{tapping_device provides useful helpers to intercept method calls}
11
+ spec.summary = %q{tapping_device lets you understand what your Ruby objects do without digging into the code}
12
+ spec.description = %q{tapping_device lets you understand what your Ruby objects do without digging into the code}
13
13
  spec.homepage = "https://github.com/st0012/tapping_device"
14
14
  spec.license = "MIT"
15
15
 
@@ -32,13 +32,12 @@ Gem::Specification.new do |spec|
32
32
  spec.add_dependency "activerecord", ">= 5.2"
33
33
  end
34
34
 
35
- spec.add_dependency "awesome_print"
35
+ spec.add_dependency "pry" # for using Method#source in MutationTracker
36
36
 
37
37
  spec.add_development_dependency "sqlite3", ">= 1.3.6"
38
38
  spec.add_development_dependency "database_cleaner"
39
39
  spec.add_development_dependency "bundler", "~> 2.0"
40
- spec.add_development_dependency "pry"
41
- spec.add_development_dependency "rake", "~> 10.0"
40
+ spec.add_development_dependency "rake", "~> 13.0"
42
41
  spec.add_development_dependency "rspec", "~> 3.0"
43
42
  spec.add_development_dependency "simplecov", "0.17.1"
44
43
  end
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.4.9
4
+ version: 0.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - st0012
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-01-20 00:00:00.000000000 Z
11
+ date: 2020-06-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -25,7 +25,7 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '5.2'
27
27
  - !ruby/object:Gem::Dependency
28
- name: awesome_print
28
+ name: pry
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
@@ -80,34 +80,20 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '2.0'
83
- - !ruby/object:Gem::Dependency
84
- name: pry
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
83
  - !ruby/object:Gem::Dependency
98
84
  name: rake
99
85
  requirement: !ruby/object:Gem::Requirement
100
86
  requirements:
101
87
  - - "~>"
102
88
  - !ruby/object:Gem::Version
103
- version: '10.0'
89
+ version: '13.0'
104
90
  type: :development
105
91
  prerelease: false
106
92
  version_requirements: !ruby/object:Gem::Requirement
107
93
  requirements:
108
94
  - - "~>"
109
95
  - !ruby/object:Gem::Version
110
- version: '10.0'
96
+ version: '13.0'
111
97
  - !ruby/object:Gem::Dependency
112
98
  name: rspec
113
99
  requirement: !ruby/object:Gem::Requirement
@@ -136,17 +122,20 @@ dependencies:
136
122
  - - '='
137
123
  - !ruby/object:Gem::Version
138
124
  version: 0.17.1
139
- description: tapping_device provides useful helpers to intercept method calls
125
+ description: tapping_device lets you understand what your Ruby objects do without
126
+ digging into the code
140
127
  email:
141
128
  - stan001212@gmail.com
142
129
  executables: []
143
130
  extensions: []
144
131
  extra_rdoc_files: []
145
132
  files:
133
+ - ".DS_Store"
146
134
  - ".github/workflows/gempush.yml"
147
135
  - ".github/workflows/ruby.yml"
148
136
  - ".gitignore"
149
137
  - ".rspec"
138
+ - ".ruby-version"
150
139
  - ".travis.yml"
151
140
  - CODE_OF_CONDUCT.md
152
141
  - Gemfile
@@ -156,12 +145,21 @@ files:
156
145
  - Rakefile
157
146
  - bin/console
158
147
  - bin/setup
148
+ - images/print_calls - single entry.png
149
+ - images/print_calls.png
150
+ - images/print_mutations.png
151
+ - images/print_traces.png
159
152
  - lib/tapping_device.rb
160
153
  - lib/tapping_device/exceptions.rb
161
154
  - lib/tapping_device/manageable.rb
155
+ - lib/tapping_device/output_payload.rb
162
156
  - lib/tapping_device/payload.rb
163
- - lib/tapping_device/sql_tapping_methods.rb
164
157
  - lib/tapping_device/trackable.rb
158
+ - lib/tapping_device/trackers/association_call_tracker.rb
159
+ - lib/tapping_device/trackers/initialization_tracker.rb
160
+ - lib/tapping_device/trackers/method_call_tracker.rb
161
+ - lib/tapping_device/trackers/mutation_tracker.rb
162
+ - lib/tapping_device/trackers/passed_tracker.rb
165
163
  - lib/tapping_device/version.rb
166
164
  - tapping_device.gemspec
167
165
  homepage: https://github.com/st0012/tapping_device
@@ -189,5 +187,6 @@ requirements: []
189
187
  rubygems_version: 3.0.3
190
188
  signing_key:
191
189
  specification_version: 4
192
- summary: tapping_device provides useful helpers to intercept method calls
190
+ summary: tapping_device lets you understand what your Ruby objects do without digging
191
+ into the code
193
192
  test_files: []