tapping_device 0.5.0 → 0.5.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,112 @@
1
+ class TappingDevice
2
+ module Trackers
3
+ class MutationTracker < TappingDevice
4
+ def initialize(options, &block)
5
+ options[:hijack_attr_methods] = true
6
+ super
7
+ @snapshot_stack = []
8
+ end
9
+
10
+ def track(object)
11
+ super
12
+ insert_snapshot_taking_trace_point
13
+ self
14
+ end
15
+
16
+ def stop!
17
+ super
18
+ @ivar_snapshot_trace_point.disable
19
+ end
20
+
21
+ private
22
+
23
+ # we need to snapshot instance variables at the beginning of every method call
24
+ # so we can get a correct state for the later comparison
25
+ def insert_snapshot_taking_trace_point
26
+ @ivar_snapshot_trace_point = build_minimum_trace_point(event_type: :call) do
27
+ snapshot_instance_variables
28
+ end
29
+
30
+ @ivar_snapshot_trace_point.enable unless TappingDevice.suspend_new
31
+ end
32
+
33
+ def filter_condition_satisfied?(tp)
34
+ return false unless is_from_target?(tp)
35
+
36
+ if snapshot_capturing_event?(tp)
37
+ true
38
+ else
39
+ @latest_instance_variables = target_instance_variables
40
+ @instance_variables_snapshot = @snapshot_stack.pop
41
+
42
+ @latest_instance_variables != @instance_variables_snapshot
43
+ end
44
+ end
45
+
46
+ def build_payload(tp:, filepath:, line_number:)
47
+ payload = super
48
+
49
+ if change_capturing_event?(tp)
50
+ payload[:ivar_changes] = capture_ivar_changes
51
+ end
52
+
53
+ payload
54
+ end
55
+
56
+ def capture_ivar_changes
57
+ changes = {}
58
+
59
+ additional_keys = @latest_instance_variables.keys - @instance_variables_snapshot.keys
60
+ additional_keys.each do |key|
61
+ changes[key] = {before: Output::Payload::UNDEFINED, after: @latest_instance_variables[key]}
62
+ end
63
+
64
+ removed_keys = @instance_variables_snapshot.keys - @latest_instance_variables.keys
65
+ removed_keys.each do |key|
66
+ changes[key] = {before: @instance_variables_snapshot[key], after: Output::Payload::UNDEFINED}
67
+ end
68
+
69
+ remained_keys = @latest_instance_variables.keys - additional_keys
70
+ remained_keys.each do |key|
71
+ next if @latest_instance_variables[key] == @instance_variables_snapshot[key]
72
+ changes[key] = {before: @instance_variables_snapshot[key], after: @latest_instance_variables[key]}
73
+ end
74
+
75
+ changes
76
+ end
77
+
78
+ def snapshot_instance_variables
79
+ @snapshot_stack.push(target_instance_variables)
80
+ end
81
+
82
+ def target_instance_variables
83
+ target.instance_variables.each_with_object({}) do |ivar, hash|
84
+ hash[ivar] = target.instance_variable_get(ivar)
85
+ end
86
+ end
87
+
88
+ def snapshot_capturing_event?(tp)
89
+ tp.event == :call
90
+ end
91
+
92
+ def change_capturing_event?(tp)
93
+ !snapshot_capturing_event?(tp)
94
+ end
95
+
96
+ # belows are debugging helpers
97
+ # I'll leave them for a while in case there's a bug in the tracker
98
+ def print_snapshot_stack(tp)
99
+ puts("===== STACK - #{tp.callee_id} (#{tp.event}) =====")
100
+ puts(@snapshot_stack)
101
+ puts("================ END STACK =================")
102
+ end
103
+
104
+ def print_state_comparison
105
+ puts("###############")
106
+ puts(@latest_instance_variables)
107
+ puts(@instance_variables_snapshot)
108
+ puts("###############")
109
+ end
110
+ end
111
+ end
112
+ 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.5.0"
2
+ VERSION = "0.5.5"
3
3
  end
@@ -32,10 +32,12 @@ Gem::Specification.new do |spec|
32
32
  spec.add_dependency "activerecord", ">= 5.2"
33
33
  end
34
34
 
35
+ spec.add_dependency "pry" # for using Method#source in MutationTracker
36
+ spec.add_dependency "activesupport"
37
+
35
38
  spec.add_development_dependency "sqlite3", ">= 1.3.6"
36
39
  spec.add_development_dependency "database_cleaner"
37
40
  spec.add_development_dependency "bundler", "~> 2.0"
38
- spec.add_development_dependency "pry"
39
41
  spec.add_development_dependency "rake", "~> 13.0"
40
42
  spec.add_development_dependency "rspec", "~> 3.0"
41
43
  spec.add_development_dependency "simplecov", "0.17.1"
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.0
4
+ version: 0.5.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - st0012
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-05-25 00:00:00.000000000 Z
11
+ date: 2020-07-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -25,27 +25,27 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '5.2'
27
27
  - !ruby/object:Gem::Dependency
28
- name: sqlite3
28
+ name: pry
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 1.3.6
34
- type: :development
33
+ version: '0'
34
+ type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 1.3.6
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: database_cleaner
42
+ name: activesupport
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
47
  version: '0'
48
- type: :development
48
+ type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
@@ -53,21 +53,21 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: bundler
56
+ name: sqlite3
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - "~>"
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: '2.0'
61
+ version: 1.3.6
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - "~>"
66
+ - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: '2.0'
68
+ version: 1.3.6
69
69
  - !ruby/object:Gem::Dependency
70
- name: pry
70
+ name: database_cleaner
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bundler
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.0'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: rake
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -137,6 +151,7 @@ files:
137
151
  - ".rspec"
138
152
  - ".ruby-version"
139
153
  - ".travis.yml"
154
+ - CHANGELOG.md
140
155
  - CODE_OF_CONDUCT.md
141
156
  - Gemfile
142
157
  - Gemfile.lock
@@ -147,14 +162,25 @@ files:
147
162
  - bin/setup
148
163
  - images/print_calls - single entry.png
149
164
  - images/print_calls.png
165
+ - images/print_mutations.png
150
166
  - images/print_traces.png
151
167
  - lib/tapping_device.rb
168
+ - lib/tapping_device/configurable.rb
152
169
  - lib/tapping_device/exceptions.rb
153
170
  - lib/tapping_device/manageable.rb
154
- - lib/tapping_device/output_payload.rb
171
+ - lib/tapping_device/method_hijacker.rb
172
+ - lib/tapping_device/output.rb
173
+ - lib/tapping_device/output/file_writer.rb
174
+ - lib/tapping_device/output/payload.rb
175
+ - lib/tapping_device/output/stdout_writer.rb
176
+ - lib/tapping_device/output/writer.rb
155
177
  - lib/tapping_device/payload.rb
156
- - lib/tapping_device/sql_tapping_methods.rb
157
178
  - lib/tapping_device/trackable.rb
179
+ - lib/tapping_device/trackers/association_call_tracker.rb
180
+ - lib/tapping_device/trackers/initialization_tracker.rb
181
+ - lib/tapping_device/trackers/method_call_tracker.rb
182
+ - lib/tapping_device/trackers/mutation_tracker.rb
183
+ - lib/tapping_device/trackers/passed_tracker.rb
158
184
  - lib/tapping_device/version.rb
159
185
  - tapping_device.gemspec
160
186
  homepage: https://github.com/st0012/tapping_device
@@ -1,145 +0,0 @@
1
- class TappingDevice
2
- class OutputPayload < Payload
3
- alias :raw_arguments :arguments
4
- alias :raw_return_value :return_value
5
-
6
- def method_name(options = {})
7
- ":#{super(options)}"
8
- end
9
-
10
- def arguments(options = {})
11
- generate_string_result(raw_arguments, options[:inspect])
12
- end
13
-
14
- def return_value(options = {})
15
- generate_string_result(raw_return_value, options[:inspect])
16
- end
17
-
18
- def self.full_color_code(code)
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
- sql: {symbol: "QUERIES", color: COLORS[:nocolor]},
41
- return_value: {symbol: "=>", color: COLORS[:megenta]},
42
- arguments: {symbol: "<=", color: COLORS[:orange]},
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)}"
88
- msg += "\n > #{method_head.strip}" 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
- private
103
-
104
- def value_with_color(value, color)
105
- "#{COLORS[color]}#{value}#{COLORS[:reset]}"
106
- end
107
-
108
- def generate_string_result(obj, inspect)
109
- case obj
110
- when Array
111
- array_to_string(obj, inspect)
112
- when Hash
113
- hash_to_string(obj, inspect)
114
- when String
115
- "\"#{obj}\""
116
- else
117
- inspect ? obj.inspect : obj.to_s
118
- end
119
- end
120
-
121
- def array_to_string(array, inspect)
122
- elements_string = array.map do |elem|
123
- generate_string_result(elem, inspect)
124
- end.join(", ")
125
- "[#{elements_string}]"
126
- end
127
-
128
- def hash_to_string(hash, inspect)
129
- elements_string = hash.map do |key, value|
130
- "#{key.to_s}: #{generate_string_result(value, inspect)}"
131
- end.join(", ")
132
- "{#{elements_string}}"
133
- end
134
-
135
- def obj_to_string(element, inspect)
136
- to_string_method = inspect ? :inspect : :to_s
137
-
138
- if !inspect && element.is_a?(String)
139
- "\"#{element}\""
140
- else
141
- element.send(to_string_method)
142
- end
143
- end
144
- end
145
- end
@@ -1,89 +0,0 @@
1
- class TappingDevice
2
- module SqlTappingMethods
3
- CALL_STACK_SKIPPABLE_METHODS = [:transaction, :tap]
4
-
5
- # SQLListener acts like an interface for us to intercept activerecord query instrumentations
6
- # this means we only need to register one subscriber no matter how many objects we want to tap on
7
- class SQLListener
8
- def call(name, start, finish, message_id, values);end
9
- end
10
- @@sql_listener = SQLListener.new
11
-
12
- ActiveSupport::Notifications.subscribe("sql.active_record", @@sql_listener)
13
-
14
- def tap_sql!(object)
15
- @call_stack = []
16
- @target ||= object
17
- @trace_point = with_trace_point_on_target(object, event: [:call, :c_call]) do |start_tp|
18
- ########## Check if the call is worth recording ##########
19
- filepath, line_number = get_call_location(start_tp, padding: 1) # we need extra padding because of `with_trace_point_on_target`
20
- method = start_tp.callee_id
21
- next if already_recording?(method)
22
-
23
- ########## Start the recording ##########
24
- # 1. Mark recording state by pushing method into @call_stack
25
- # 2. Subscribe sql instrumentations generated by activerecord
26
- # 3. Record those sqls and run device callbacks
27
- # 4. Start waiting for current call's return callback
28
- @call_stack.push(method)
29
- payload = build_payload(tp: start_tp, filepath: filepath, line_number: line_number)
30
- device = tap_on_sql_instrumentation!(payload)
31
-
32
- with_trace_point_on_target(object, event: :return) do |return_tp|
33
- next unless return_tp.callee_id == method
34
-
35
- ########## End recording ##########
36
- # 1. Close itself
37
- # 2. Stop our subscription on SQLListener
38
- # 3. Remove current method from @call_stack
39
- # 4. Stop the device if stop condition is fulfilled
40
- return_tp.disable
41
- device.stop!
42
- @call_stack.pop
43
- stop_if_condition_fulfilled(payload)
44
-
45
- ########## Track descendant objects ##########
46
- # if the method creates another Relation object
47
- if return_tp.defined_class == ActiveRecord::QueryMethods
48
- create_child_device.tap_sql!(return_tp.return_value)
49
- end
50
- end.enable
51
- end
52
-
53
- @trace_point.enable unless self.class.suspend_new
54
-
55
- self
56
- end
57
- end
58
-
59
- private
60
-
61
- def tap_on_sql_instrumentation!(payload)
62
- device = TappingDevice.new do |sql_listener_payload|
63
- values = sql_listener_payload.arguments[:values]
64
-
65
- next if should_be_skipped_by_paths?(payload.filepath) ||
66
- ["SCHEMA", "TRANSACTION"].include?(values[:name]) ||
67
- values[:sql].match?(/SAVEPOINT/)
68
-
69
- payload[:sql] = values[:sql]
70
- record_call!(payload)
71
- end
72
- device.tap_on!(@@sql_listener)
73
- end
74
-
75
- # usually, AR's query methods (like `first`) will end up calling `find_by_sql`
76
- # then to TappingDevice, both `first` and `find_by_sql` generates the sql
77
- # but the results are duplicated, we should only consider the `first` call
78
- def already_recording?(method)
79
- !@call_stack.empty? || CALL_STACK_SKIPPABLE_METHODS.include?(method)
80
- end
81
-
82
- def with_trace_point_on_target(object, event:)
83
- TracePoint.new(*event) do |tp|
84
- if is_from_target?(object, tp)
85
- yield(tp)
86
- end
87
- end
88
- end
89
- end