tapping_device 0.4.3 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
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