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.
- checksums.yaml +4 -4
- data/.DS_Store +0 -0
- data/.github/workflows/ruby.yml +11 -4
- data/.ruby-version +1 -0
- data/Gemfile.lock +21 -23
- data/README.md +136 -353
- 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 -112
- data/lib/tapping_device/exceptions.rb +12 -0
- data/lib/tapping_device/output_payload.rb +173 -0
- data/lib/tapping_device/payload.rb +3 -44
- data/lib/tapping_device/trackable.rb +56 -17
- 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 +132 -0
- data/lib/tapping_device/trackers/passed_tracker.rb +16 -0
- data/lib/tapping_device/version.rb +1 -1
- data/tapping_device.gemspec +4 -5
- metadata +21 -22
- data/lib/tapping_device/sql_tapping_methods.rb +0 -89
@@ -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, :
|
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
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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 |
|
13
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
24
|
-
|
44
|
+
def print_mutations(target, options = {})
|
45
|
+
output_options = extract_output_options(options)
|
25
46
|
|
26
|
-
|
27
|
-
|
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
|
-
|
32
|
-
|
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,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
|
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,12 @@ 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
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 "
|
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
|
+
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-
|
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:
|
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: '
|
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: '
|
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
|
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
|
190
|
+
summary: tapping_device lets you understand what your Ruby objects do without digging
|
191
|
+
into the code
|
193
192
|
test_files: []
|