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,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.4.10"
2
+ VERSION = "0.5.3"
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,13 @@ 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
+ spec.add_dependency "activesupport"
36
37
 
37
38
  spec.add_development_dependency "sqlite3", ">= 1.3.6"
38
39
  spec.add_development_dependency "database_cleaner"
39
40
  spec.add_development_dependency "bundler", "~> 2.0"
40
- spec.add_development_dependency "pry"
41
- spec.add_development_dependency "rake", "~> 10.0"
41
+ spec.add_development_dependency "rake", "~> 13.0"
42
42
  spec.add_development_dependency "rspec", "~> 3.0"
43
43
  spec.add_development_dependency "simplecov", "0.17.1"
44
44
  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.10
4
+ version: 0.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - st0012
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-02-05 00:00:00.000000000 Z
11
+ date: 2020-06-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -25,7 +25,21 @@ 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
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
45
  - - ">="
@@ -80,34 +94,20 @@ dependencies:
80
94
  - - "~>"
81
95
  - !ruby/object:Gem::Version
82
96
  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
97
  - !ruby/object:Gem::Dependency
98
98
  name: rake
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '10.0'
103
+ version: '13.0'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '10.0'
110
+ version: '13.0'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: rspec
113
113
  requirement: !ruby/object:Gem::Requirement
@@ -136,19 +136,22 @@ dependencies:
136
136
  - - '='
137
137
  - !ruby/object:Gem::Version
138
138
  version: 0.17.1
139
- description: tapping_device provides useful helpers to intercept method calls
139
+ description: tapping_device lets you understand what your Ruby objects do without
140
+ digging into the code
140
141
  email:
141
142
  - stan001212@gmail.com
142
143
  executables: []
143
144
  extensions: []
144
145
  extra_rdoc_files: []
145
146
  files:
147
+ - ".DS_Store"
146
148
  - ".github/workflows/gempush.yml"
147
149
  - ".github/workflows/ruby.yml"
148
150
  - ".gitignore"
149
151
  - ".rspec"
150
152
  - ".ruby-version"
151
153
  - ".travis.yml"
154
+ - CHANGELOG.md
152
155
  - CODE_OF_CONDUCT.md
153
156
  - Gemfile
154
157
  - Gemfile.lock
@@ -157,12 +160,27 @@ files:
157
160
  - Rakefile
158
161
  - bin/console
159
162
  - bin/setup
163
+ - images/print_calls - single entry.png
164
+ - images/print_calls.png
165
+ - images/print_mutations.png
166
+ - images/print_traces.png
160
167
  - lib/tapping_device.rb
168
+ - lib/tapping_device/configurable.rb
161
169
  - lib/tapping_device/exceptions.rb
162
170
  - lib/tapping_device/manageable.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
163
177
  - lib/tapping_device/payload.rb
164
- - lib/tapping_device/sql_tapping_methods.rb
165
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
166
184
  - lib/tapping_device/version.rb
167
185
  - tapping_device.gemspec
168
186
  homepage: https://github.com/st0012/tapping_device
@@ -190,5 +208,6 @@ requirements: []
190
208
  rubygems_version: 3.0.3
191
209
  signing_key:
192
210
  specification_version: 4
193
- summary: tapping_device provides useful helpers to intercept method calls
211
+ summary: tapping_device lets you understand what your Ruby objects do without digging
212
+ into the code
194
213
  test_files: []
@@ -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