tapping_device 0.4.3 → 0.4.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f69d455b71287ddab4b1568c0e09a60093571d262fab4a6213cfce66d9b6476
4
- data.tar.gz: 20f7523e65715636395c0250c231c93296f67be3ba5b191381c4bf54ed217299
3
+ metadata.gz: b224ae2b5f110b0b4f6cfce578eaf6d4170d646e8141aa087e0abbd7780938d1
4
+ data.tar.gz: 836334b532fad8983c63070ab322f88cce8578a61ff79b57fe76bd2f65f141a5
5
5
  SHA512:
6
- metadata.gz: f54d923ca9b34bb4e40d4c1522aa6ed10b0d0e8343c031e0449ec1f56f5410e6cb72b6f0fd493d292016644e7ec147eb2e0e570137d42215d36215ae2b4de74f
7
- data.tar.gz: 104a54fb9c29b02e095007f1d7991daa164379c0fe4790342901560fc6d339a1ba13a57def0c58fb4276e7ddee5e57c306ec738187701d7d538edb11fd6fd8b9
6
+ metadata.gz: 6c78d7c9adad4b705e8a8ec65316fe182764090cbfdfd3f0ab8a4931675f8527317031a1e0e1ac8726d613c87e777a9895b668c3376dbcc61ad9fc59501df34e
7
+ data.tar.gz: 0a6388a5c2096ce392d08951fd89f63c0f9967bbe21e99b2462893d75f454552288e49b231c2692b056b09567751c38756ff52bafd53fc7c09615534cee9c8a9
@@ -19,6 +19,8 @@ jobs:
19
19
  ruby-version: ${{ matrix.ruby_version }}
20
20
  - name: Install sqlite
21
21
  run: |
22
+ # See https://github.community/t5/GitHub-Actions/ubuntu-latest-Apt-repository-list-issues/td-p/41122/page/2
23
+ sudo rm -f /etc/apt/sources.list.d/dotnetdev.list /etc/apt/sources.list.d/microsoft-prod.list
22
24
  sudo apt-get update
23
25
  sudo apt-get install libsqlite3-dev
24
26
 
data/Gemfile.lock CHANGED
@@ -1,18 +1,18 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- tapping_device (0.4.3)
4
+ tapping_device (0.4.4)
5
5
  activerecord (>= 5.2)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- activemodel (6.0.1)
11
- activesupport (= 6.0.1)
12
- activerecord (6.0.1)
13
- activemodel (= 6.0.1)
14
- activesupport (= 6.0.1)
15
- activesupport (6.0.1)
10
+ activemodel (6.0.2)
11
+ activesupport (= 6.0.2)
12
+ activerecord (6.0.2)
13
+ activemodel (= 6.0.2)
14
+ activesupport (= 6.0.2)
15
+ activesupport (6.0.2)
16
16
  concurrent-ruby (~> 1.0, >= 1.0.2)
17
17
  i18n (>= 0.7, < 2)
18
18
  minitest (~> 5.1)
data/README.md CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  ![](https://github.com/st0012/tapping_device/workflows/Ruby/badge.svg)
4
4
 
5
- **Please use 0.3.0+ versions, older versions have serious performance issues**
6
5
 
7
6
  `tapping_device` is a gem built on top of Ruby’s `TracePoint` class that allows you to tap method calls of specified objects. The purpose for this gem is to make debugging Rails applications easier. Here are some sample usages:
8
7
 
@@ -50,7 +49,7 @@ Method: :amends_order, line: /MY_PROJECT/app/models/order.rb:432
50
49
  ```
51
50
 
52
51
 
53
- ### Track calls that generates sql queries! (Beta)
52
+ ### Track calls that generates sql queries!
54
53
 
55
54
  `tap_sql!` method helps you track which method calls generate sql queries. This is particularly helpful when tracking calls created from a reused `ActiveRecord::Relation` object.
56
55
 
@@ -120,27 +119,13 @@ In order to use `tapping_device`, you need to include `TappingDevice::Trackable`
120
119
  - `tap_sql!(activerecord_relation_or_model)` - tracks sql queries generated from the target
121
120
 
122
121
  ### Payload of the call
123
- All tapping methods (start with `tap_`) takes a block and yield a `Payload` object as block argument. The `Payload` class inherits `Hash` so we can either use it as a hash, or you can call its keys as methods.
124
-
125
- ```ruby
126
- {
127
- :receiver=>#<Student:0x00007fabed02aeb8 @name="Stan", @age=18, @tapping_device=[#<TracePoint:return `age'@/PROJECT_PATH/tapping_device/spec/trackable_spec.rb:17>]>,
128
- :method_name=>:age,
129
- :arguments=>{},
130
- :return_value=>18,
131
- :filepath=>"/PROJECT_PATH/tapping_device/spec/trackable_spec.rb",
132
- :line_number=>"171",
133
- :defined_class=>Student,
134
- :trace=>[],
135
- :tp=>#<TracePoint:return `age'@/PROJECT_PATH/tapping_device/spec/trackable_spec.rb:17>
136
- }
137
- ```
138
-
139
- The hash contains
122
+ All tapping methods (start with `tap_`) takes a block and yield a `Payload` object as block argument. It responds to
140
123
 
124
+ - `target` - the target for `tap_x` call
141
125
  - `receiver` - the receiver object
142
126
  - `method_name` - method’s name (symbol)
143
127
  - e.g. `:name`
128
+ - `method_object` - the method object that’s being called. It might be `nil` in some edge cases.
144
129
  - `arguments` - arguments of the method call
145
130
  - e.g. `{name: “Stan”, age: 25}`
146
131
  - `return_value` - return value of the method call
@@ -153,6 +138,19 @@ The hash contains
153
138
  #### Some useful helpers
154
139
  - `method_name_and_location` - `"Method: :initialize, line: /PROJECT_PATH/tapping_device/spec/payload_spec.rb:7"`
155
140
  - `method_name_and_arguments` - `"Method: :initialize, argments: {:name=>\"Stan\", :age=>25}"`
141
+ - `passed_at` -
142
+ ```
143
+ Passed as 'object' in method ':initialize'
144
+ at /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/actionview-6.0.0/lib/action_view/helpers/tags/label.rb:60
145
+ ```
146
+
147
+ You can also set `passed_at(with_method_head: true)` to see the method’s head
148
+
149
+ ```
150
+ Passed as 'object' in method ':initialize'
151
+ > def initialize(template_object, object_name, method_name, object, tag_value)
152
+ at /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/actionview-6.0.0/lib/action_view/helpers/tags/label.rb:60
153
+ ```
156
154
 
157
155
 
158
156
  ### Options
@@ -221,6 +219,39 @@ Method: :user_id, line: /PROJECT_PATH/sample/app/views/posts/show.html.erb:10
221
219
  Method: :to_param, line: /RUBY_PATH/gems/2.6.0/gems/actionpack-5.2.0/lib/action_dispatch/routing/route_set.rb:236
222
220
  ```
223
221
 
222
+ ### `tap_passed!`
223
+
224
+ This is particularly useful when debugging libraries. It saves your time from jumping between files and check which path the object will go.
225
+
226
+ ```ruby
227
+ class PostsController < ApplicationController
228
+ include TappingDevice::Trackable
229
+ # GET /posts/new
230
+ def new
231
+ @post = Post.new
232
+
233
+ tap_passed!(@post) do |payload|
234
+ puts(payload.passed_at(with_method_head: true))
235
+ end
236
+ end
237
+ end
238
+ ```
239
+
240
+ ```
241
+ Passed as 'record' in method ':polymorphic_mapping'
242
+ > def polymorphic_mapping(record)
243
+ at /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/actionpack-6.0.0/lib/action_dispatch/routing/polymorphic_routes.rb:131
244
+ Passed as 'klass' in method ':get_method_for_class'
245
+ > def get_method_for_class(klass)
246
+ at /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/actionpack-6.0.0/lib/action_dispatch/routing/polymorphic_routes.rb:269
247
+ Passed as 'record' in method ':handle_model'
248
+ > def handle_model(record)
249
+ at /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/actionpack-6.0.0/lib/action_dispatch/routing/polymorphic_routes.rb:227
250
+ Passed as 'record_or_hash_or_array' in method ':polymorphic_method'
251
+ > def self.polymorphic_method(recipient, record_or_hash_or_array, action, type, options)
252
+ at /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/actionpack-6.0.0/lib/action_dispatch/routing/polymorphic_routes.rb:139
253
+ ```
254
+
224
255
  ### `tap_assoc!`
225
256
 
226
257
  ```ruby
@@ -237,7 +268,7 @@ Method: :amending_orders, line: /MY_PROJECT/app/models/order.rb:385
237
268
  Method: :amends_order, line: /MY_PROJECT/app/models/order.rb:432
238
269
  ```
239
270
 
240
- ### `tap_sql!` (beta)
271
+ ### `tap_sql!`
241
272
 
242
273
  ```ruby
243
274
  class PostsController < ApplicationController
@@ -1,5 +1,6 @@
1
1
  require "active_record"
2
2
  require "tapping_device/version"
3
+ require "tapping_device/manageable"
3
4
  require "tapping_device/payload"
4
5
  require "tapping_device/trackable"
5
6
  require "tapping_device/exceptions"
@@ -10,44 +11,13 @@ class TappingDevice
10
11
  CALLER_START_POINT = 3
11
12
  C_CALLER_START_POINT = 2
12
13
 
13
- attr_reader :options, :calls, :trace_point
14
+ attr_reader :options, :calls, :trace_point, :target
14
15
 
15
- @@devices = []
16
- @@suspend_new = false
16
+ @devices = []
17
+ @suspend_new = false
17
18
 
18
19
  include SqlTappingMethods
19
-
20
- def self.suspend_new
21
- @@suspend_new
22
- end
23
-
24
- # list all registered devices
25
- def self.devices
26
- @@devices
27
- end
28
-
29
- # disable given device and remove it from registered list
30
- def self.delete_device(device)
31
- device.trace_point&.disable
32
- @@devices -= [device]
33
- end
34
-
35
- # stops all registered devices and remove them from registered list
36
- def self.stop_all!
37
- @@devices.each(&:stop!)
38
- end
39
-
40
- # suspend enabling new trace points
41
- # user can still create new Device instances, but they won't be functional
42
- def self.suspend_new!
43
- @@suspend_new = true
44
- end
45
-
46
- # reset everything to clean state and disable all devices
47
- def self.reset!
48
- @@suspend_new = false
49
- stop_all!
50
- end
20
+ extend Manageable
51
21
 
52
22
  def initialize(options = {}, &block)
53
23
  @block = block
@@ -66,6 +36,10 @@ class TappingDevice
66
36
  track(object, condition: :tap_on?)
67
37
  end
68
38
 
39
+ def tap_passed!(object)
40
+ track(object, condition: :tap_passed?)
41
+ end
42
+
69
43
  def tap_assoc!(record)
70
44
  raise "argument should be an instance of ActiveRecord::Base" unless record.is_a?(ActiveRecord::Base)
71
45
  track(record, condition: :tap_associations?)
@@ -87,6 +61,7 @@ class TappingDevice
87
61
  def create_child_device
88
62
  new_device = self.class.new(@options.merge(root_device: root_device), &@block)
89
63
  new_device.stop_when(&@stop_when)
64
+ new_device.instance_variable_set(:@target, @target)
90
65
  self.descendants << new_device
91
66
  new_device
92
67
  end
@@ -99,26 +74,12 @@ class TappingDevice
99
74
  options[:descendants]
100
75
  end
101
76
 
102
- def record_call!(payload)
103
- return if @disabled
104
-
105
- if @block
106
- root_device.calls << @block.call(payload)
107
- else
108
- root_device.calls << payload
109
- end
110
- end
111
-
112
77
  private
113
78
 
114
79
  def track(object, condition:)
80
+ @target = object
115
81
  @trace_point = TracePoint.new(:return) do |tp|
116
- validation_params = {
117
- receiver: tp.self,
118
- method_name: tp.callee_id
119
- }
120
-
121
- if send(condition, object, validation_params)
82
+ if send(condition, object, tp)
122
83
  filepath, line_number = get_call_location(tp)
123
84
 
124
85
  next if should_be_skipped_by_paths?(filepath)
@@ -131,7 +92,7 @@ class TappingDevice
131
92
  end
132
93
  end
133
94
 
134
- @trace_point.enable unless @@suspend_new
95
+ @trace_point.enable unless self.class.suspend_new
135
96
 
136
97
  self
137
98
  end
@@ -155,8 +116,10 @@ class TappingDevice
155
116
  tp.binding.local_variables.each { |name| arguments[name] = tp.binding.local_variable_get(name) }
156
117
 
157
118
  Payload.init({
119
+ target: @target,
158
120
  receiver: tp.self,
159
121
  method_name: tp.callee_id,
122
+ method_object: get_method_object_from(tp.self, tp.callee_id),
160
123
  arguments: arguments,
161
124
  return_value: (tp.return_value rescue nil),
162
125
  filepath: filepath,
@@ -167,9 +130,9 @@ class TappingDevice
167
130
  })
168
131
  end
169
132
 
170
- def tap_init?(klass, parameters)
171
- receiver = parameters[:receiver]
172
- method_name = parameters[:method_name]
133
+ def tap_init?(klass, tp)
134
+ receiver = tp.self
135
+ method_name = tp.callee_id
173
136
 
174
137
  if klass.ancestors.include?(ActiveRecord::Base)
175
138
  method_name == :new && receiver.ancestors.include?(klass)
@@ -178,16 +141,49 @@ class TappingDevice
178
141
  end
179
142
  end
180
143
 
181
- def tap_on?(object, parameters)
182
- parameters[:receiver].__id__ == object.__id__
144
+ def tap_on?(object, tp)
145
+ is_from_target?(object, tp)
183
146
  end
184
147
 
185
- def tap_associations?(object, parameters)
186
- return false unless tap_on?(object, parameters)
148
+ def tap_associations?(object, tp)
149
+ return false unless tap_on?(object, tp)
187
150
 
188
151
  model_class = object.class
189
152
  associations = model_class.reflections
190
- associations.keys.include?(parameters[:method_name].to_s)
153
+ associations.keys.include?(tp.callee_id.to_s)
154
+ end
155
+
156
+ def tap_passed?(object, tp)
157
+ # we don't care about calls from the device instance
158
+ return false if is_from_target?(self, tp)
159
+
160
+ method_object = get_method_object_from(tp.self, tp.callee_id)
161
+ # if a no-arugment method is called, tp.binding.local_variables will be those local variables in the same scope
162
+ # so we need to make sure the method takes arguments, then we can be sure that the locals are arguments
163
+ return false unless method_object && method_object.arity.to_i > 0
164
+
165
+ argument_values = tp.binding.local_variables.map { |name| tp.binding.local_variable_get(name) }
166
+
167
+ argument_values.any? do |value|
168
+ # during comparison, Ruby might perform data type conversion like calling `to_sym` on the value
169
+ # but not every value supports every conversion methods
170
+ begin
171
+ object == value
172
+ rescue NoMethodError
173
+ false
174
+ end
175
+ end
176
+ end
177
+
178
+ def get_method_object_from(target, method_name)
179
+ target.method(method_name)
180
+ rescue ArgumentError
181
+ method_method = Object.method(:method).unbind
182
+ method_method.bind(target).call(method_name)
183
+ rescue NameError
184
+ # if any part of the program uses Refinement to extend its methods
185
+ # we might still get NoMethodError when trying to get that method outside the scope
186
+ nil
191
187
  end
192
188
 
193
189
  def process_options(options)
@@ -209,4 +205,14 @@ class TappingDevice
209
205
  def is_from_target?(object, tp)
210
206
  object.__id__ == tp.self.__id__
211
207
  end
208
+
209
+ def record_call!(payload)
210
+ return if @disabled
211
+
212
+ if @block
213
+ root_device.calls << @block.call(payload)
214
+ else
215
+ root_device.calls << payload
216
+ end
217
+ end
212
218
  end
@@ -0,0 +1,37 @@
1
+ class TappingDevice
2
+ module Manageable
3
+
4
+ def suspend_new
5
+ @suspend_new
6
+ end
7
+
8
+ # list all registered devices
9
+ def devices
10
+ @devices
11
+ end
12
+
13
+ # disable given device and remove it from registered list
14
+ def delete_device(device)
15
+ device.trace_point&.disable
16
+ @devices -= [device]
17
+ end
18
+
19
+ # stops all registered devices and remove them from registered list
20
+ def stop_all!
21
+ @devices.each(&:stop!)
22
+ end
23
+
24
+ # suspend enabling new trace points
25
+ # user can still create new Device instances, but they won't be functional
26
+ def suspend_new!
27
+ @suspend_new = true
28
+ end
29
+
30
+ # reset everything to clean state and disable all devices
31
+ def reset!
32
+ @suspend_new = false
33
+ stop_all!
34
+ end
35
+ end
36
+ end
37
+
@@ -1,6 +1,9 @@
1
1
  class TappingDevice
2
2
  class Payload < Hash
3
- ATTRS = [:receiver, :method_name, :arguments, :return_value, :filepath, :line_number, :defined_class, :trace, :tp, :sql]
3
+ ATTRS = [
4
+ :target, :receiver, :method_name, :method_object, :arguments, :return_value, :filepath, :line_number,
5
+ :defined_class, :trace, :tp, :sql
6
+ ]
4
7
 
5
8
  ATTRS.each do |attr|
6
9
  define_method attr do
@@ -16,8 +19,26 @@ class TappingDevice
16
19
  h
17
20
  end
18
21
 
22
+ def passed_at(with_method_head: false)
23
+ arg_name = arguments.keys.detect { |k| arguments[k] == target }
24
+ return unless arg_name
25
+ msg = "Passed as '#{arg_name}' in method ':#{method_name}'"
26
+ msg += "\n > #{method_head.strip}" if with_method_head
27
+ msg += "\n at #{location}"
28
+ msg
29
+ end
30
+
31
+ def method_head
32
+ source_file, source_line = method_object.source_location
33
+ IO.readlines(source_file)[source_line-1]
34
+ end
35
+
36
+ def location
37
+ "#{filepath}:#{line_number}"
38
+ end
39
+
19
40
  def method_name_and_location
20
- "Method: :#{method_name}, line: #{filepath}:#{line_number}"
41
+ "Method: :#{method_name}, line: #{location}"
21
42
  end
22
43
 
23
44
  def method_name_and_arguments
@@ -13,6 +13,7 @@ class TappingDevice
13
13
 
14
14
  def tap_sql!(object)
15
15
  @call_stack = []
16
+ @target ||= object
16
17
  @trace_point = with_trace_point_on_target(object, event: [:call, :c_call]) do |start_tp|
17
18
  ########## Check if the call is worth recording ##########
18
19
  filepath, line_number = get_call_location(start_tp, padding: 1) # we need extra padding because of `with_trace_point_on_target`
@@ -1,19 +1,9 @@
1
1
  class TappingDevice
2
2
  module Trackable
3
- def tap_init!(klass, options = {}, &block)
4
- new_device(options, &block).tap_init!(klass)
5
- end
6
-
7
- def tap_assoc!(record, options = {}, &block)
8
- new_device(options, &block).tap_assoc!(record)
9
- end
10
-
11
- def tap_on!(object, options = {}, &block)
12
- new_device(options, &block).tap_on!(object)
13
- end
14
-
15
- def tap_sql!(object, options = {}, &block)
16
- new_device(options, &block).tap_sql!(object)
3
+ [:tap_on!, :tap_init!, :tap_assoc!, :tap_sql!, :tap_passed!].each do |method|
4
+ define_method method do |object, options = {}, &block|
5
+ new_device(options, &block).send(method, object)
6
+ end
17
7
  end
18
8
 
19
9
  def new_device(options, &block)
@@ -1,3 +1,3 @@
1
1
  class TappingDevice
2
- VERSION = "0.4.3"
2
+ VERSION = "0.4.4"
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.3
4
+ version: 0.4.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - st0012
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-12-09 00:00:00.000000000 Z
11
+ date: 2019-12-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -130,6 +130,7 @@ files:
130
130
  - bin/setup
131
131
  - lib/tapping_device.rb
132
132
  - lib/tapping_device/exceptions.rb
133
+ - lib/tapping_device/manageable.rb
133
134
  - lib/tapping_device/payload.rb
134
135
  - lib/tapping_device/sql_tapping_methods.rb
135
136
  - lib/tapping_device/trackable.rb