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.
- checksums.yaml +4 -4
- data/.DS_Store +0 -0
- data/.github/workflows/ruby.yml +11 -4
- data/CHANGELOG.md +208 -0
- data/Gemfile.lock +22 -23
- data/README.md +206 -297
- data/images/print_calls - single entry.png +0 -0
- data/images/print_calls.png +0 -0
- data/images/print_mutations.png +0 -0
- data/images/print_traces.png +0 -0
- data/lib/tapping_device.rb +108 -114
- data/lib/tapping_device/configurable.rb +25 -0
- data/lib/tapping_device/exceptions.rb +12 -0
- data/lib/tapping_device/method_hijacker.rb +51 -0
- data/lib/tapping_device/output.rb +42 -0
- data/lib/tapping_device/output/file_writer.rb +21 -0
- data/lib/tapping_device/output/payload.rb +175 -0
- data/lib/tapping_device/output/stdout_writer.rb +9 -0
- data/lib/tapping_device/output/writer.rb +20 -0
- data/lib/tapping_device/payload.rb +4 -47
- data/lib/tapping_device/trackable.rb +84 -18
- data/lib/tapping_device/trackers/association_call_tracker.rb +17 -0
- data/lib/tapping_device/trackers/initialization_tracker.rb +27 -0
- data/lib/tapping_device/trackers/method_call_tracker.rb +9 -0
- data/lib/tapping_device/trackers/mutation_tracker.rb +112 -0
- data/lib/tapping_device/trackers/passed_tracker.rb +16 -0
- data/lib/tapping_device/version.rb +1 -1
- data/tapping_device.gemspec +5 -5
- metadata +41 -22
- data/lib/tapping_device/sql_tapping_methods.rb +0 -89
@@ -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
|
data/tapping_device.gemspec
CHANGED
@@ -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
|
12
|
-
spec.description = %q{tapping_device
|
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 "
|
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 "
|
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
|
+
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-
|
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:
|
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: '
|
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: '
|
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
|
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
|
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
|