tapping_device 0.1.1 → 0.2.0
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/Gemfile.lock +13 -13
- data/README.md +63 -38
- data/lib/tapping_device.rb +166 -3
- data/lib/tapping_device/exceptions.rb +4 -0
- data/lib/tapping_device/trackable.rb +7 -90
- data/lib/tapping_device/version.rb +2 -2
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4000e62db2d9672d87d133a5c6adeeaf5eed9d134b4bd8b9a948887ee2d06f48
|
|
4
|
+
data.tar.gz: 477c9bf04c5a218aa8a23034929bcae7d0d52e663954380bfeb7b57f1290152c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 629ce1239a852d1bc1e9b7585ad7746273a5722139a08b24a8712448af9fc3201915eb3d882d7eed5300afaba590ee880e3db103a9fa9825d5304613e278ad23
|
|
7
|
+
data.tar.gz: f03e06164e74ceab8bbef4f0dec75795fab1866ba427dbcc570a4dac6bdf5a7ac3c166e0d4a2af7a6b042d1fd74cc9ad7465c960254fe6536258bd7fa1b81031
|
data/Gemfile.lock
CHANGED
|
@@ -1,30 +1,31 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
tapping_device (0.1.
|
|
5
|
-
activerecord (~>
|
|
4
|
+
tapping_device (0.1.1)
|
|
5
|
+
activerecord (~> 5.2)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
8
8
|
remote: https://rubygems.org/
|
|
9
9
|
specs:
|
|
10
|
-
activemodel (
|
|
11
|
-
activesupport (=
|
|
12
|
-
activerecord (
|
|
13
|
-
activemodel (=
|
|
14
|
-
activesupport (=
|
|
15
|
-
|
|
10
|
+
activemodel (5.2.3)
|
|
11
|
+
activesupport (= 5.2.3)
|
|
12
|
+
activerecord (5.2.3)
|
|
13
|
+
activemodel (= 5.2.3)
|
|
14
|
+
activesupport (= 5.2.3)
|
|
15
|
+
arel (>= 9.0)
|
|
16
|
+
activesupport (5.2.3)
|
|
16
17
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
17
18
|
i18n (>= 0.7, < 2)
|
|
18
19
|
minitest (~> 5.1)
|
|
19
20
|
tzinfo (~> 1.1)
|
|
20
|
-
|
|
21
|
+
arel (9.0.0)
|
|
21
22
|
coderay (1.1.2)
|
|
22
23
|
concurrent-ruby (1.1.5)
|
|
23
24
|
diff-lcs (1.3)
|
|
24
25
|
i18n (1.7.0)
|
|
25
26
|
concurrent-ruby (~> 1.0)
|
|
26
27
|
method_source (0.9.2)
|
|
27
|
-
minitest (5.
|
|
28
|
+
minitest (5.13.0)
|
|
28
29
|
pry (0.12.2)
|
|
29
30
|
coderay (~> 1.1.0)
|
|
30
31
|
method_source (~> 0.9.0)
|
|
@@ -42,11 +43,10 @@ GEM
|
|
|
42
43
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
43
44
|
rspec-support (~> 3.8.0)
|
|
44
45
|
rspec-support (3.8.2)
|
|
45
|
-
sqlite3 (1.
|
|
46
|
+
sqlite3 (1.3.13)
|
|
46
47
|
thread_safe (0.3.6)
|
|
47
48
|
tzinfo (1.2.5)
|
|
48
49
|
thread_safe (~> 0.1)
|
|
49
|
-
zeitwerk (2.2.0)
|
|
50
50
|
|
|
51
51
|
PLATFORMS
|
|
52
52
|
ruby
|
|
@@ -56,7 +56,7 @@ DEPENDENCIES
|
|
|
56
56
|
pry
|
|
57
57
|
rake (~> 10.0)
|
|
58
58
|
rspec (~> 3.0)
|
|
59
|
-
sqlite3 (~> 1.
|
|
59
|
+
sqlite3 (~> 1.3.6)
|
|
60
60
|
tapping_device!
|
|
61
61
|
|
|
62
62
|
BUNDLED WITH
|
data/README.md
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
```ruby
|
|
8
8
|
class PostsController < ApplicationController
|
|
9
|
-
include TappingDevice::Trackable
|
|
10
|
-
|
|
11
9
|
def show
|
|
12
10
|
@post = Post.find(params[:id])
|
|
13
|
-
|
|
11
|
+
|
|
12
|
+
device = TappingDevice.new do |payload|
|
|
14
13
|
puts "Method: #{payload[:method_name]} line: #{payload[:filepath]}:#{payload[:line_number]}"
|
|
15
14
|
end
|
|
15
|
+
device.tap_on!(@post)
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
```
|
|
@@ -28,9 +28,10 @@ Method: to_param line: /RUBY_PATH/gems/2.6.0/gems/actionpack-5.2.0/lib/action_di
|
|
|
28
28
|
Or you can use `tap_assoc!`. This is very useful for tracking potential n+1 query calls, here’s a sample from my work
|
|
29
29
|
|
|
30
30
|
```ruby
|
|
31
|
-
|
|
31
|
+
device = TappingDevice.new do |payload|
|
|
32
32
|
puts "Assoc: #{payload[:method_name]} line: #{payload[:filepath]}:#{payload[:line_number]}"
|
|
33
33
|
end
|
|
34
|
+
device.tap_assoc!(order)
|
|
34
35
|
```
|
|
35
36
|
|
|
36
37
|
```
|
|
@@ -65,15 +66,44 @@ $ gem install tapping_device
|
|
|
65
66
|
```
|
|
66
67
|
|
|
67
68
|
## Usage
|
|
68
|
-
In order to use `tapping_device`, you need to include `TappingDevice::Trackable` module in where you want to track your code.
|
|
69
69
|
|
|
70
|
-
###
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
### Create a device object
|
|
71
|
+
In order to tap on something, you need to first initialize a tapping device with a block that process the call info.
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
device = TappingDevice.new do |payload|
|
|
75
|
+
if payload[:method_name].to_s.match?(/foo/)
|
|
76
|
+
puts "Method: #{payload[:method_name]} line: #{payload[:filepath]}:#{payload[:line_number]}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Performance issue and setup stop condition
|
|
82
|
+
|
|
83
|
+
Because `tapping_device` is built upon `TracePoint`, which literally scans **every method call** on **every object**. Even if we filter out the calls we’re not interested in, just filtering out through those method calls takes time if your application isn’t a small one. So it’s very important to stop the tapping device at a certain point. You can do this in 2 ways:
|
|
84
|
+
|
|
85
|
+
#### Use `device.stop_when(&block)` to set a stop condition
|
|
86
|
+
To define a stop condition, you can use `stop_when` method.
|
|
75
87
|
|
|
76
|
-
|
|
88
|
+
```ruby
|
|
89
|
+
device.stop_when do |payload|
|
|
90
|
+
device.calls.count >= 10 # stop after gathering 10 calls' data
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**If you don’t set a stop condition, you need to use tapping methods that has exclamation mark**, like `device.tap_on!(post)`.
|
|
95
|
+
|
|
96
|
+
#### `device.stop!`
|
|
97
|
+
If you don’t define a stop condition, you can also use `device.stop!` to stop it manually.
|
|
98
|
+
|
|
99
|
+
### Start tapping
|
|
100
|
+
|
|
101
|
+
#### Methods
|
|
102
|
+
- `TappingDevice#tap_init(class)` - tracks a class’ instance initialization
|
|
103
|
+
- `TappingDevice#tap_on(object)` - tracks any calls received by the object
|
|
104
|
+
- `TappingDevice#tap_assoc(activerecord_object)` - tracks association calls on a record, like `post.comments`
|
|
105
|
+
|
|
106
|
+
#### Info of the call
|
|
77
107
|
All tapping methods (start with `tap_`) takes a block and yield a hash as block argument.
|
|
78
108
|
|
|
79
109
|
```ruby
|
|
@@ -110,34 +140,14 @@ The hash contains
|
|
|
110
140
|
- `exclude_by_paths: [/path/]` - an array of call path patterns that we want to skip. This could be very helpful when working on large project like Rails applications.
|
|
111
141
|
- `filter_by_paths: [/path/]` - only contain calls from the specified paths
|
|
112
142
|
|
|
113
|
-
|
|
114
|
-
tap_on!(@post, exclude_by_paths: [/active_record/]) do |payload|
|
|
115
|
-
puts "Method: #{payload[:method_name]} line: #{payload[:filepath]}:#{payload[:line_number]}"
|
|
116
|
-
end
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
```
|
|
120
|
-
Method: _read_attribute line: /RUBY_PATH/gems/2.6.0/gems/activerecord-5.2.0/lib/active_record/attribute_methods/read.rb:40
|
|
121
|
-
Method: name line: /PROJECT_PATH/sample/app/views/posts/show.html.erb:5
|
|
122
|
-
Method: _read_attribute line: /RUBY_PATH/gems/2.6.0/gems/activerecord-5.2.0/lib/active_record/attribute_methods/read.rb:40
|
|
123
|
-
Method: user_id line: /PROJECT_PATH/sample/app/views/posts/show.html.erb:10
|
|
124
|
-
.......
|
|
125
|
-
|
|
126
|
-
# versus
|
|
127
|
-
|
|
128
|
-
Method: name line: /PROJECT_PATH/sample/app/views/posts/show.html.erb:5
|
|
129
|
-
Method: user_id line: /PROJECT_PATH/sample/app/views/posts/show.html.erb:10
|
|
130
|
-
Method: to_param line: /RUBY_PATH/gems/2.6.0/gems/actionpack-5.2.0/lib/action_dispatch/routing/route_set.rb:236
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
### `#tap_init!`
|
|
143
|
+
### `#tap_init`
|
|
135
144
|
|
|
136
145
|
```ruby
|
|
137
146
|
calls = []
|
|
138
|
-
|
|
147
|
+
device = TappingDevice.new do |payload|
|
|
139
148
|
calls << [payload[:method_name], payload[:arguments]]
|
|
140
149
|
end
|
|
150
|
+
device.tap_init!(Student)
|
|
141
151
|
|
|
142
152
|
Student.new("Stan", 18)
|
|
143
153
|
Student.new("Jane", 23)
|
|
@@ -149,16 +159,16 @@ puts(calls.to_s) #=> [[:initialize, [[:name, "Stan"], [:age, 18]]], [:initialize
|
|
|
149
159
|
|
|
150
160
|
```ruby
|
|
151
161
|
class PostsController < ApplicationController
|
|
152
|
-
include TappingDevice::Trackable
|
|
153
|
-
|
|
154
162
|
before_action :set_post, only: [:show, :edit, :update, :destroy]
|
|
155
163
|
|
|
156
164
|
# GET /posts/1
|
|
157
165
|
# GET /posts/1.json
|
|
158
166
|
def show
|
|
159
|
-
|
|
167
|
+
device = TappingDevice.new do |payload|
|
|
160
168
|
puts "Method: #{payload[:method_name]} line: #{payload[:filepath]}:#{payload[:line_number]}"
|
|
161
169
|
end
|
|
170
|
+
|
|
171
|
+
device.tap_on!(@post)
|
|
162
172
|
end
|
|
163
173
|
end
|
|
164
174
|
```
|
|
@@ -174,9 +184,10 @@ Method: to_param line: /RUBY_PATH/gems/2.6.0/gems/actionpack-5.2.0/lib/action_di
|
|
|
174
184
|
### `tap_assoc!`
|
|
175
185
|
|
|
176
186
|
```ruby
|
|
177
|
-
|
|
187
|
+
device = TappingDevice.new do |payload|
|
|
178
188
|
puts "Assoc: #{payload[:method_name]} line: #{payload[:filepath]}:#{payload[:line_number]}"
|
|
179
189
|
end
|
|
190
|
+
device.tap_assoc!(order)
|
|
180
191
|
```
|
|
181
192
|
|
|
182
193
|
```
|
|
@@ -187,6 +198,20 @@ Assoc: amending_orders line: /MY_PROJECT/app/models/order.rb:385
|
|
|
187
198
|
Assoc: amends_order line: /MY_PROJECT/app/models/order.rb:432
|
|
188
199
|
```
|
|
189
200
|
|
|
201
|
+
### Device states & Managing Devices
|
|
202
|
+
|
|
203
|
+
Every `TappingDevice` instance can have 3 states:
|
|
204
|
+
|
|
205
|
+
- `Initial` - means the instance is initialized but hasn’t been used to tap on anything.
|
|
206
|
+
- `Enabled` - means the instance has started to tap on something (has called `tap_*` methods).
|
|
207
|
+
- `Disabled` - means the instance has been disabled. It will no longer receive any call info.
|
|
208
|
+
|
|
209
|
+
When debugging, we may create many device instances and tap objects in several places. Then it’ll be quite annoying to manage their states. So `TappingDevice` has several class methods that allows you to manage all `TappingDevice` instances:
|
|
210
|
+
|
|
211
|
+
- `TappingDevice.devices` - Lists all registered devices with `initial` or `enabled` state. Note that any instance that’s been stopped will be removed from the list.
|
|
212
|
+
- `TappingDevice.stop_all!` - Stops all registered devices and remove them from the `devices` list.
|
|
213
|
+
- `TappingDevice.suspend_new!` - Suspends any device instance from changing their state from `initatial` to `enabled`. Which means any `tap_*` calls after it will no longer work.
|
|
214
|
+
- `TappingDevice.reset!` - Cancels `suspend_new` (if called) and stops/removes all created devices. Useful to reset environment between test cases.
|
|
190
215
|
|
|
191
216
|
## Development
|
|
192
217
|
|
data/lib/tapping_device.rb
CHANGED
|
@@ -1,7 +1,170 @@
|
|
|
1
|
+
require "active_record"
|
|
1
2
|
require "tapping_device/version"
|
|
2
3
|
require "tapping_device/trackable"
|
|
4
|
+
require "tapping_device/exceptions"
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
class TappingDevice
|
|
7
|
+
CALLER_START_POINT = 2
|
|
8
|
+
FORCE_STOP_WHEN_MESSAGE = "You must set stop_when condition before start tapping"
|
|
9
|
+
|
|
10
|
+
attr_reader :options, :calls, :trace_point
|
|
11
|
+
|
|
12
|
+
@@devices = []
|
|
13
|
+
@@suspend_new = false
|
|
14
|
+
|
|
15
|
+
# list all registered devices
|
|
16
|
+
def self.devices
|
|
17
|
+
@@devices
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# disable given device and remove it from registered list
|
|
21
|
+
def self.delete_device(device)
|
|
22
|
+
device.trace_point&.disable
|
|
23
|
+
@@devices -= [device]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# stops all registered devices and remove them from registered list
|
|
27
|
+
def self.stop_all!
|
|
28
|
+
@@devices.each(&:stop!)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# suspend enabling new trace points
|
|
32
|
+
# user can still create new Device instances, but they won't be functional
|
|
33
|
+
def self.suspend_new!
|
|
34
|
+
@@suspend_new = true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# reset everything to clean state and disable all devices
|
|
38
|
+
def self.reset!
|
|
39
|
+
@@suspend_new = false
|
|
40
|
+
stop_all!
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def initialize(options = {}, &block)
|
|
44
|
+
@block = block
|
|
45
|
+
@options = options
|
|
46
|
+
@calls = []
|
|
47
|
+
self.class.devices << self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def tap_init!(klass)
|
|
51
|
+
raise "argument should be a class, got #{klass}" unless klass.is_a?(Class)
|
|
52
|
+
track(klass, condition: :tap_init?, block: @block, **@options)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def tap_on!(object)
|
|
56
|
+
track(object, condition: :tap_on?, block: @block, **@options)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def tap_assoc!(record)
|
|
60
|
+
raise "argument should be an instance of ActiveRecord::Base" unless record.is_a?(ActiveRecord::Base)
|
|
61
|
+
track(record, condition: :tap_associations?, block: @block, **@options)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def tap_init(klass)
|
|
65
|
+
validate_tapping(__method__)
|
|
66
|
+
tap_init!(klass)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def tap_on(object)
|
|
70
|
+
validate_tapping(__method__)
|
|
71
|
+
tap_on!(object)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def tap_assoc(record)
|
|
75
|
+
validate_tapping(__method__)
|
|
76
|
+
tap_assoc!(record)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def set_block(&block)
|
|
80
|
+
@block = block
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def stop!
|
|
84
|
+
self.class.delete_device(self)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def stop_when(&block)
|
|
88
|
+
@stop_when = block
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def validate_tapping(method_name)
|
|
94
|
+
unless @stop_when
|
|
95
|
+
raise TappingDevice::Exception.new <<~ERROR
|
|
96
|
+
You must set stop_when condition before calling #{method_name}. Or you can use #{method_name}! to force tapping.
|
|
97
|
+
Tapping without stop condition can largely slow down or even halt your application, because it'll need to
|
|
98
|
+
screen literally every call happened.
|
|
99
|
+
ERROR
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def track(object, condition:, block:, with_trace_to: nil, exclude_by_paths: [], filter_by_paths: nil)
|
|
104
|
+
@trace_point = TracePoint.new(:return) do |tp|
|
|
105
|
+
filepath, line_number = caller(CALLER_START_POINT).first.split(":")[0..1]
|
|
106
|
+
|
|
107
|
+
# this needs to be placed upfront so we can exclude noise before doing more work
|
|
108
|
+
next if exclude_by_paths.any? { |pattern| pattern.match?(filepath) }
|
|
109
|
+
|
|
110
|
+
if filter_by_paths
|
|
111
|
+
next unless filter_by_paths.any? { |pattern| pattern.match?(filepath) }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
arguments = tp.binding.local_variables.map { |n| [n, tp.binding.local_variable_get(n)] }
|
|
115
|
+
|
|
116
|
+
yield_parameters = {
|
|
117
|
+
receiver: tp.self,
|
|
118
|
+
method_name: tp.callee_id,
|
|
119
|
+
arguments: arguments,
|
|
120
|
+
return_value: (tp.return_value rescue nil),
|
|
121
|
+
filepath: filepath,
|
|
122
|
+
line_number: line_number,
|
|
123
|
+
defined_class: tp.defined_class,
|
|
124
|
+
trace: [],
|
|
125
|
+
tp: tp
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
yield_parameters[:trace] = caller[CALLER_START_POINT..(CALLER_START_POINT + with_trace_to)] if with_trace_to
|
|
129
|
+
|
|
130
|
+
if send(condition, object, yield_parameters)
|
|
131
|
+
if @block
|
|
132
|
+
@calls << block.call(yield_parameters)
|
|
133
|
+
else
|
|
134
|
+
@calls << yield_parameters
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
stop! if @stop_when&.call(yield_parameters)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
@trace_point.enable unless @@suspend_new
|
|
142
|
+
|
|
143
|
+
self
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
def tap_init?(klass, parameters)
|
|
149
|
+
receiver = parameters[:receiver]
|
|
150
|
+
method_name = parameters[:method_name]
|
|
151
|
+
|
|
152
|
+
if klass.ancestors.include?(ActiveRecord::Base)
|
|
153
|
+
method_name == :new && receiver.ancestors.include?(klass)
|
|
154
|
+
else
|
|
155
|
+
method_name == :initialize && receiver.is_a?(klass)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def tap_on?(object, parameters)
|
|
160
|
+
parameters[:receiver].object_id == object.object_id
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def tap_associations?(object, parameters)
|
|
164
|
+
return false unless tap_on?(object, parameters)
|
|
165
|
+
|
|
166
|
+
model_class = object.class
|
|
167
|
+
associations = model_class.reflections
|
|
168
|
+
associations.keys.include?(parameters[:method_name].to_s)
|
|
169
|
+
end
|
|
7
170
|
end
|
|
@@ -1,98 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
module TappingDevice
|
|
1
|
+
class TappingDevice
|
|
4
2
|
module Trackable
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def tap_initialization_of!(klass, options = {}, &block)
|
|
9
|
-
raise "argument should be a class, got #{klass}" unless klass.is_a?(Class)
|
|
10
|
-
track(klass, condition: :tap_init?, block: block, **options)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def tap_association_calls!(record, options = {}, &block)
|
|
14
|
-
raise "argument should be an instance of ActiveRecord::Base" unless record.is_a?(ActiveRecord::Base)
|
|
15
|
-
track(record, condition: :tap_associations?, block: block, **options)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def tap_calls_on!(object, options = {}, &block)
|
|
19
|
-
track(object, condition: :tap_on?, block: block, **options)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def stop_tapping!(object)
|
|
23
|
-
get_tapping_device(object)&.each { |tp| tp.disable }
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
alias :tap_init! :tap_initialization_of!
|
|
27
|
-
alias :tap_assoc! :tap_association_calls!
|
|
28
|
-
alias :tap_on! :tap_calls_on!
|
|
29
|
-
alias :untap! :stop_tapping!
|
|
30
|
-
|
|
31
|
-
private
|
|
32
|
-
|
|
33
|
-
def track(object, condition:, block:, with_trace_to: nil, exclude_by_paths: [], filter_by_paths: nil)
|
|
34
|
-
trace_point = TracePoint.trace(:return) do |tp|
|
|
35
|
-
filepath, line_number = caller(CALLER_START_POINT).first.split(":")[0..1]
|
|
36
|
-
|
|
37
|
-
# this needs to be placed upfront so we can exclude noise before doing more work
|
|
38
|
-
next if exclude_by_paths.any? { |pattern| pattern.match?(filepath) }
|
|
39
|
-
|
|
40
|
-
if filter_by_paths
|
|
41
|
-
next unless filter_by_paths.any? { |pattern| pattern.match?(filepath) }
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
arguments = tp.binding.local_variables.map { |n| [n, tp.binding.local_variable_get(n)] }
|
|
45
|
-
|
|
46
|
-
yield_parameters = {
|
|
47
|
-
receiver: tp.self,
|
|
48
|
-
method_name: tp.callee_id,
|
|
49
|
-
arguments: arguments,
|
|
50
|
-
return_value: (tp.return_value rescue nil),
|
|
51
|
-
filepath: filepath,
|
|
52
|
-
line_number: line_number,
|
|
53
|
-
defined_class: tp.defined_class,
|
|
54
|
-
trace: [],
|
|
55
|
-
tp: tp
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
yield_parameters[:trace] = caller[CALLER_START_POINT..(CALLER_START_POINT + with_trace_to)] if with_trace_to
|
|
59
|
-
|
|
60
|
-
block.call(yield_parameters) if send(condition, object, yield_parameters)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
add_tapping_device(object, trace_point)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def tap_init?(klass, parameters)
|
|
67
|
-
receiver = parameters[:receiver]
|
|
68
|
-
method_name = parameters[:method_name]
|
|
69
|
-
|
|
70
|
-
if klass.ancestors.include?(ActiveRecord::Base)
|
|
71
|
-
method_name == :new && receiver.ancestors.include?(klass)
|
|
72
|
-
else
|
|
73
|
-
method_name == :initialize && receiver.is_a?(klass)
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def tap_on?(object, parameters)
|
|
78
|
-
parameters[:receiver].object_id == object.object_id
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def tap_associations?(object, parameters)
|
|
82
|
-
return false unless tap_on?(object, parameters)
|
|
83
|
-
|
|
84
|
-
model_class = object.class
|
|
85
|
-
associations = model_class.reflections
|
|
86
|
-
associations.keys.include?(parameters[:method_name].to_s)
|
|
3
|
+
def tap_init!(klass, options = {}, &block)
|
|
4
|
+
TappingDevice.new(options, &block).tap_init!(klass)
|
|
87
5
|
end
|
|
88
6
|
|
|
89
|
-
def
|
|
90
|
-
|
|
7
|
+
def tap_assoc!(record, options = {}, &block)
|
|
8
|
+
TappingDevice.new(options, &block).tap_assoc!(record)
|
|
91
9
|
end
|
|
92
10
|
|
|
93
|
-
def
|
|
94
|
-
|
|
95
|
-
object.instance_variable_get(TAPPING_DEVICE) << trace_point
|
|
11
|
+
def tap_on!(object, options = {}, &block)
|
|
12
|
+
TappingDevice.new(options, &block).tap_on!(object)
|
|
96
13
|
end
|
|
97
14
|
end
|
|
98
15
|
end
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
VERSION = "0.
|
|
1
|
+
class TappingDevice
|
|
2
|
+
VERSION = "0.2.0"
|
|
3
3
|
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.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- st0012
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2019-
|
|
11
|
+
date: 2019-11-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -115,6 +115,7 @@ files:
|
|
|
115
115
|
- bin/console
|
|
116
116
|
- bin/setup
|
|
117
117
|
- lib/tapping_device.rb
|
|
118
|
+
- lib/tapping_device/exceptions.rb
|
|
118
119
|
- lib/tapping_device/trackable.rb
|
|
119
120
|
- lib/tapping_device/version.rb
|
|
120
121
|
- tapping_device.gemspec
|