tapping_device 0.4.7 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.DS_Store +0 -0
- data/.github/workflows/ruby.yml +1 -1
- data/.ruby-version +1 -0
- data/Gemfile.lock +16 -16
- data/README.md +96 -322
- data/images/print_calls - single entry.png +0 -0
- data/images/print_calls.png +0 -0
- data/images/print_traces.png +0 -0
- data/lib/tapping_device.rb +52 -21
- data/lib/tapping_device/output_payload.rb +145 -0
- data/lib/tapping_device/payload.rb +2 -34
- data/lib/tapping_device/trackable.rb +25 -0
- data/lib/tapping_device/version.rb +1 -1
- data/tapping_device.gemspec +3 -3
- metadata +14 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9806974025c4e1ba9042bf2df5a4b4211a7e4a9630b291e05892d472e54263cc
|
4
|
+
data.tar.gz: fc609ed5be23cac4943e90c34d7126aaffd53816910320f7300d2a33a9d40585
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 15ee39e171f665492e47c378bcffedd3d7bc1c8204269607e54c1ac174f44fe5ee4cc33a0ffcd82d4b732abc63def63eefb8b1fda386d4e40503a4bd959e71e2
|
7
|
+
data.tar.gz: 3b4bb1b05949fd4735fde568995e8c92c64634ad39f196f76649e2f401834d8fdc605080546655f90920e8c2cfc1cebb4bf686b7da34494217c677e058f6c73b
|
data/.DS_Store
ADDED
Binary file
|
data/.github/workflows/ruby.yml
CHANGED
@@ -22,7 +22,7 @@ jobs:
|
|
22
22
|
- name: Install sqlite
|
23
23
|
run: |
|
24
24
|
# See https://github.community/t5/GitHub-Actions/ubuntu-latest-Apt-repository-list-issues/td-p/41122/page/2
|
25
|
-
|
25
|
+
for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do sudo rm $apt_file; done
|
26
26
|
sudo apt-get update
|
27
27
|
sudo apt-get install libsqlite3-dev
|
28
28
|
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.6.5
|
data/Gemfile.lock
CHANGED
@@ -1,37 +1,37 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
tapping_device (0.
|
4
|
+
tapping_device (0.5.0)
|
5
5
|
activerecord (>= 5.2)
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: https://rubygems.org/
|
9
9
|
specs:
|
10
|
-
activemodel (6.0.
|
11
|
-
activesupport (= 6.0.
|
12
|
-
activerecord (6.0.
|
13
|
-
activemodel (= 6.0.
|
14
|
-
activesupport (= 6.0.
|
15
|
-
activesupport (6.0.
|
10
|
+
activemodel (6.0.3.1)
|
11
|
+
activesupport (= 6.0.3.1)
|
12
|
+
activerecord (6.0.3.1)
|
13
|
+
activemodel (= 6.0.3.1)
|
14
|
+
activesupport (= 6.0.3.1)
|
15
|
+
activesupport (6.0.3.1)
|
16
16
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
17
17
|
i18n (>= 0.7, < 2)
|
18
18
|
minitest (~> 5.1)
|
19
19
|
tzinfo (~> 1.1)
|
20
|
-
zeitwerk (~> 2.2)
|
20
|
+
zeitwerk (~> 2.2, >= 2.2.2)
|
21
21
|
coderay (1.1.2)
|
22
|
-
concurrent-ruby (1.1.
|
22
|
+
concurrent-ruby (1.1.6)
|
23
23
|
database_cleaner (1.7.0)
|
24
24
|
diff-lcs (1.3)
|
25
25
|
docile (1.3.2)
|
26
|
-
i18n (1.
|
26
|
+
i18n (1.8.2)
|
27
27
|
concurrent-ruby (~> 1.0)
|
28
28
|
json (2.3.0)
|
29
29
|
method_source (0.9.2)
|
30
|
-
minitest (5.
|
30
|
+
minitest (5.14.1)
|
31
31
|
pry (0.12.2)
|
32
32
|
coderay (~> 1.1.0)
|
33
33
|
method_source (~> 0.9.0)
|
34
|
-
rake (
|
34
|
+
rake (13.0.1)
|
35
35
|
rspec (3.8.0)
|
36
36
|
rspec-core (~> 3.8.0)
|
37
37
|
rspec-expectations (~> 3.8.0)
|
@@ -52,9 +52,9 @@ GEM
|
|
52
52
|
simplecov-html (0.10.2)
|
53
53
|
sqlite3 (1.4.1)
|
54
54
|
thread_safe (0.3.6)
|
55
|
-
tzinfo (1.2.
|
55
|
+
tzinfo (1.2.7)
|
56
56
|
thread_safe (~> 0.1)
|
57
|
-
zeitwerk (2.
|
57
|
+
zeitwerk (2.3.0)
|
58
58
|
|
59
59
|
PLATFORMS
|
60
60
|
ruby
|
@@ -63,11 +63,11 @@ DEPENDENCIES
|
|
63
63
|
bundler (~> 2.0)
|
64
64
|
database_cleaner
|
65
65
|
pry
|
66
|
-
rake (~>
|
66
|
+
rake (~> 13.0)
|
67
67
|
rspec (~> 3.0)
|
68
68
|
simplecov (= 0.17.1)
|
69
69
|
sqlite3 (>= 1.3.6)
|
70
70
|
tapping_device!
|
71
71
|
|
72
72
|
BUNDLED WITH
|
73
|
-
2.
|
73
|
+
2.1.1
|
data/README.md
CHANGED
@@ -1,406 +1,180 @@
|
|
1
1
|
# TappingDevice
|
2
2
|
|
3
|
-
![](https://github.com/st0012/tapping_device/workflows/Ruby/badge.svg)
|
3
|
+
![GitHub Action](https://github.com/st0012/tapping_device/workflows/Ruby/badge.svg)
|
4
|
+
[![Gem Version](https://badge.fury.io/rb/tapping_device.svg)](https://badge.fury.io/rb/tapping_device)
|
4
5
|
[![Maintainability](https://api.codeclimate.com/v1/badges/3e3732a6983785bccdbd/maintainability)](https://codeclimate.com/github/st0012/tapping_device/maintainability)
|
5
6
|
[![Test Coverage](https://api.codeclimate.com/v1/badges/3e3732a6983785bccdbd/test_coverage)](https://codeclimate.com/github/st0012/tapping_device/test_coverage)
|
6
7
|
[![Open Source Helpers](https://www.codetriage.com/st0012/tapping_device/badges/users.svg)](https://www.codetriage.com/st0012/tapping_device)
|
7
8
|
|
8
|
-
## Related Posts
|
9
|
-
- [Debug Rails issues effectively with tapping_device](https://dev.to/st0012/debug-rails-issues-effectively-with-tappingdevice-c7c)
|
10
|
-
- [Want to know more about your Rails app? Tap on your objects!](https://dev.to/st0012/want-to-know-more-about-your-rails-app-tap-on-your-objects-bd3)
|
11
|
-
|
12
|
-
## Table of Content
|
13
|
-
- [Introduction](#introduction)
|
14
|
-
- [Track Method Calls](#track-method-calls)
|
15
|
-
- [Track Association Calls](#track-association-calls)
|
16
|
-
- [Track Calls that Generates SQL Queries](#track-calls-that-generates-sql-queries)
|
17
|
-
- [Installation](#installation)
|
18
|
-
- [Usages](#usages)
|
19
|
-
- Tapping Methods
|
20
|
-
- [tap_init!](#tap_init)
|
21
|
-
- [tap_on!](#tap_on)
|
22
|
-
- [tap_passed!](#tap_passed)
|
23
|
-
- [tap_assoc!](#tap_assoc)
|
24
|
-
- [tap_sql!](#tap_sql)
|
25
|
-
- [Options](#options)
|
26
|
-
- [Payload](#payload-of-the-call)
|
27
|
-
- [Advance Usages](#advance-usages)
|
28
9
|
|
29
10
|
## Introduction
|
30
|
-
`
|
11
|
+
`TappingDevice` makes the objects tell you what they do, so you don't need to track them yourself.
|
31
12
|
|
32
|
-
|
13
|
+
#### Contract Tracing For Objects
|
33
14
|
|
34
|
-
|
35
|
-
class PostsController < ApplicationController
|
36
|
-
include TappingDevice::Trackable
|
37
|
-
|
38
|
-
def show
|
39
|
-
@post = Post.find(params[:id])
|
40
|
-
tap_on!(@post).and_print(:method_name_and_location)
|
41
|
-
end
|
42
|
-
end
|
43
|
-
```
|
15
|
+
The concept is very simple. It's basically like [contact tracing](https://en.wikipedia.org/wiki/Contact_tracing) for your Ruby objects. You can use
|
44
16
|
|
45
|
-
|
17
|
+
- `print_calls(object)` to see what the object does
|
18
|
+
- `print_traces(object)` to see how the object interacts with other objects (like used as an argument)
|
46
19
|
|
47
|
-
|
48
|
-
name FROM /PROJECT_PATH/sample/app/views/posts/show.html.erb:5
|
49
|
-
user_id FROM /PROJECT_PATH/sample/app/views/posts/show.html.erb:10
|
50
|
-
to_param FROM /RUBY_PATH/gems/2.6.0/gems/actionpack-5.2.0/lib/action_dispatch/routing/route_set.rb:236
|
51
|
-
```
|
20
|
+
Still sounds vague? Let's see some examples:
|
52
21
|
|
53
|
-
### Track
|
22
|
+
### `print_calls` To Track Method Calls
|
54
23
|
|
55
|
-
|
24
|
+
In [Discourse](https://github.com/discourse/discourse), it uses the `Guardian` class for authorization (like policy objects). It's barely visible in controller actions, but it does many checks under the hood. Now, let's say we want to know what the `Guadian` would do when a user creates a post; here's the controller action:
|
56
25
|
|
57
26
|
```ruby
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
# simulate current_user
|
77
|
-
@current_user = User.last
|
78
|
-
# reusable ActiveRecord::Relation
|
79
|
-
@posts = Post.all
|
80
|
-
|
81
|
-
tap_sql!(@posts) do |payload|
|
82
|
-
puts("Method: #{payload[:method_name]} generated sql: #{payload[:sql]} from #{payload[:filepath]}:#{payload[:line_number]}")
|
27
|
+
def create
|
28
|
+
@manager_params = create_params
|
29
|
+
@manager_params[:first_post_checks] = !is_api?
|
30
|
+
|
31
|
+
manager = NewPostManager.new(current_user, @manager_params)
|
32
|
+
|
33
|
+
if is_api?
|
34
|
+
memoized_payload = DistributedMemoizer.memoize(signature_for(@manager_params), 120) do
|
35
|
+
result = manager.perform
|
36
|
+
MultiJson.dump(serialize_data(result, NewPostResultSerializer, root: false))
|
37
|
+
end
|
38
|
+
|
39
|
+
parsed_payload = JSON.parse(memoized_payload)
|
40
|
+
backwards_compatible_json(parsed_payload, parsed_payload['success'])
|
41
|
+
else
|
42
|
+
result = manager.perform
|
43
|
+
json = serialize_data(result, NewPostResultSerializer, root: false)
|
44
|
+
backwards_compatible_json(json, result.success?)
|
83
45
|
end
|
84
46
|
end
|
85
|
-
end
|
86
|
-
```
|
87
|
-
|
88
|
-
```erb
|
89
|
-
<h1>Posts (<%= @posts.count %>)</h1>
|
90
|
-
......
|
91
|
-
<% @posts.each do |post| %>
|
92
|
-
......
|
93
|
-
<% end %>
|
94
|
-
......
|
95
|
-
<p>Posts created by you: <%= @posts.where(user: @current_user).count %></p>
|
96
|
-
```
|
97
|
-
|
98
|
-
And the output would be
|
99
|
-
|
100
|
-
```
|
101
|
-
Method: count generated sql: SELECT COUNT(*) FROM "posts" from /PROJECT_PATH/rails-6-sample/app/views/posts/index.html.erb:3
|
102
|
-
Method: each generated sql: SELECT "posts".* FROM "posts" from /PROJECT_PATH/rails-6-sample/app/views/posts/index.html.erb:16
|
103
|
-
Method: count generated sql: SELECT COUNT(*) FROM "posts" WHERE "posts"."user_id" = ? from /PROJECT_PATH/rails-6-sample/app/views/posts/index.html.erb:31
|
104
47
|
```
|
105
48
|
|
106
|
-
|
49
|
+
As you can see, it doesn't even exist in the controller action, which makes tracking it by reading code very hard to do.
|
107
50
|
|
108
|
-
|
109
|
-
Add this line to your application’s Gemfile:
|
51
|
+
But with `TappingDevice`. You can use `print_calls` to show what method calls the object performs
|
110
52
|
|
111
53
|
```ruby
|
112
|
-
|
54
|
+
def create
|
55
|
+
# you can retrieve the current guardian object by calling guardian in the controller
|
56
|
+
print_calls(guardian)
|
57
|
+
@manager_params = create_params
|
58
|
+
|
59
|
+
# .....
|
113
60
|
```
|
114
61
|
|
115
|
-
|
62
|
+
Now, if you execute the code, like via tests:
|
116
63
|
|
117
|
-
```
|
118
|
-
$
|
64
|
+
```shell
|
65
|
+
$ rspec spec/requests/posts_controller_spec.rb:603
|
119
66
|
```
|
120
67
|
|
121
|
-
|
68
|
+
You can get all the method calls it performs with basically everything you need to know
|
122
69
|
|
123
|
-
|
124
|
-
$ gem install tapping_device
|
125
|
-
```
|
70
|
+
<img src="https://github.com/st0012/tapping_device/blob/master/images/print_calls.png" alt="image of print_calls output" width="50%">
|
126
71
|
|
127
|
-
|
128
|
-
|
72
|
+
Let's take a closer look at each entry. Everyone of them contains the method call's
|
73
|
+
- method name
|
74
|
+
- method source class/module
|
75
|
+
- call site
|
76
|
+
- arguments
|
77
|
+
- return value
|
129
78
|
|
130
|
-
|
79
|
+
![explanation of individual entry](https://github.com/st0012/tapping_device/blob/master/images/print_calls%20-%20single%20entry.png)
|
131
80
|
|
132
|
-
|
81
|
+
These are the information you'd have to look up one by one manually (probably with many debug code writing). Now you can get all of them in just one line of code.
|
133
82
|
|
134
|
-
```ruby
|
135
|
-
calls = []
|
136
|
-
tap_init!(Student) do |payload|
|
137
|
-
calls << [payload[:method_name], payload[:arguments]]
|
138
|
-
end
|
139
83
|
|
140
|
-
|
141
|
-
Student.new("Jane", 23)
|
84
|
+
### `print_traces` To See The Object's Traces
|
142
85
|
|
143
|
-
|
144
|
-
```
|
145
|
-
|
146
|
-
### tap_on!
|
147
|
-
|
148
|
-
`tap_on!(object)` - tracks any calls received by the object
|
86
|
+
If you're not interested in what an object does, but what it interacts with other parts of the program, e.g., used as arguments. You can use the `print_traces` helper. Let's see how `Discourse` uses the `manager` object when creating a post
|
149
87
|
|
150
88
|
```ruby
|
151
|
-
|
152
|
-
|
89
|
+
def create
|
90
|
+
@manager_params = create_params
|
91
|
+
@manager_params[:first_post_checks] = !is_api?
|
92
|
+
|
93
|
+
manager = NewPostManager.new(current_user, @manager_params)
|
153
94
|
|
154
|
-
|
155
|
-
|
156
|
-
def show
|
157
|
-
tap_on!(@post).and_print(:method_name_and_location)
|
158
|
-
end
|
159
|
-
end
|
95
|
+
print_traces(manager)
|
96
|
+
# .....
|
160
97
|
```
|
161
98
|
|
162
|
-
And
|
163
|
-
|
164
|
-
```
|
165
|
-
name FROM /PROJECT_PATH/sample/app/views/posts/show.html.erb:5
|
166
|
-
user_id FROM /PROJECT_PATH/sample/app/views/posts/show.html.erb:10
|
167
|
-
to_param FROM /RUBY_PATH/gems/2.6.0/gems/actionpack-5.2.0/lib/action_dispatch/routing/route_set.rb:236
|
168
|
-
```
|
169
|
-
|
170
|
-
Also check the `track_as_records` option if you want to track `ActiveRecord` records.
|
171
|
-
|
172
|
-
### tap_passed!
|
99
|
+
And after running the test case
|
173
100
|
|
174
|
-
|
175
|
-
|
176
|
-
```ruby
|
177
|
-
class PostsController < ApplicationController
|
178
|
-
include TappingDevice::Trackable
|
179
|
-
# GET /posts/new
|
180
|
-
def new
|
181
|
-
@post = Post.new
|
182
|
-
|
183
|
-
tap_passed!(@post) do |payload|
|
184
|
-
puts(payload.passed_at(with_method_head: true))
|
185
|
-
end
|
186
|
-
end
|
187
|
-
end
|
188
|
-
```
|
189
|
-
|
190
|
-
```
|
191
|
-
Passed as 'record' in method ':polymorphic_mapping'
|
192
|
-
> def polymorphic_mapping(record)
|
193
|
-
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
|
194
|
-
Passed as 'klass' in method ':get_method_for_class'
|
195
|
-
> def get_method_for_class(klass)
|
196
|
-
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
|
197
|
-
Passed as 'record' in method ':handle_model'
|
198
|
-
> def handle_model(record)
|
199
|
-
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
|
200
|
-
Passed as 'record_or_hash_or_array' in method ':polymorphic_method'
|
201
|
-
> def self.polymorphic_method(recipient, record_or_hash_or_array, action, type, options)
|
202
|
-
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
|
101
|
+
```shell
|
102
|
+
$ rspec spec/requests/posts_controller_spec.rb:603
|
203
103
|
```
|
204
104
|
|
205
|
-
|
105
|
+
You will see that it performs 2 calls: `perform` and `perform_create_post`. And it's also used as `manager` argument in various of calls of the `NewPostManager` class.
|
206
106
|
|
207
|
-
|
107
|
+
![image of print_traces output](https://github.com/st0012/tapping_device/blob/master/images/print_traces.png)
|
208
108
|
|
209
|
-
|
210
|
-
tap_assoc!(order).and_print(:method_name_and_location)
|
211
|
-
```
|
109
|
+
**You can try these examples on [my fork of discourse](https://github.com/st0012/discourse/tree/demo-for-tapping-device)**
|
212
110
|
|
213
|
-
```
|
214
|
-
payments FROM /RUBY_PATH/gems/2.6.0/gems/jsonapi-resources-0.9.10/lib/jsonapi/resource.rb:124
|
215
|
-
line_items FROM /MY_PROJECT/app/models/line_item_container_helpers.rb:44
|
216
|
-
effective_line_items FROM /MY_PROJECT/app/models/line_item_container_helpers.rb:110
|
217
|
-
amending_orders FROM /MY_PROJECT/app/models/order.rb:385
|
218
|
-
amends_order FROM /MY_PROJECT/app/models/order.rb:432
|
219
|
-
```
|
220
111
|
|
221
|
-
|
222
|
-
|
223
|
-
`tap_sql!(anything_that_generates_sql_queries)` tracks sql queries generated from the target
|
112
|
+
## Installation
|
113
|
+
Add this line to your application's Gemfile:
|
224
114
|
|
225
115
|
```ruby
|
226
|
-
|
227
|
-
def index
|
228
|
-
# simulate current_user
|
229
|
-
@current_user = User.last
|
230
|
-
# reusable ActiveRecord::Relation
|
231
|
-
@posts = Post.all
|
232
|
-
|
233
|
-
tap_sql!(@posts) do |payload|
|
234
|
-
puts("Method: #{payload[:method_name]} generated sql: #{payload[:sql]} from #{payload[:filepath]}:#{payload[:line_number]}")
|
235
|
-
end
|
236
|
-
end
|
237
|
-
end
|
116
|
+
gem 'tapping_device', group: :development
|
238
117
|
```
|
239
118
|
|
240
|
-
|
241
|
-
<h1>Posts (<%= @posts.count %>)</h1>
|
242
|
-
......
|
243
|
-
<% @posts.each do |post| %>
|
244
|
-
......
|
245
|
-
<% end %>
|
246
|
-
......
|
247
|
-
<p>Posts created by you: <%= @posts.where(user: @current_user).count %></p>
|
248
|
-
```
|
119
|
+
And then execute:
|
249
120
|
|
250
121
|
```
|
251
|
-
|
252
|
-
Method: each generated sql: SELECT "posts".* FROM "posts" from /PROJECT_PATH/rails-6-sample/app/views/posts/index.html.erb:16
|
253
|
-
Method: count generated sql: SELECT COUNT(*) FROM "posts" WHERE "posts"."user_id" = ? from /PROJECT_PATH/rails-6-sample/app/views/posts/index.html.erb:31
|
254
|
-
```
|
255
|
-
|
256
|
-
|
257
|
-
### Options
|
258
|
-
#### with_trace_to
|
259
|
-
It takes an integer as the number of traces we want to put into `trace`. Default is `nil`, so `trace` would be empty.
|
260
|
-
|
261
|
-
```ruby
|
262
|
-
stan = Student.new("Stan", 18)
|
263
|
-
tap_on!(stan, with_trace_to: 5)
|
264
|
-
|
265
|
-
stan.name
|
266
|
-
|
267
|
-
puts(device.calls.first.trace) #=>
|
268
|
-
/Users/st0012/projects/tapping_device/spec/tapping_device_spec.rb:287:in `block (4 levels) in <top (required)>'
|
269
|
-
/Users/st0012/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/rspec-core-3.8.2/lib/rspec/core/example.rb:257:in `instance_exec'
|
270
|
-
/Users/st0012/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/rspec-core-3.8.2/lib/rspec/core/example.rb:257:in `block in run'
|
271
|
-
/Users/st0012/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/rspec-core-3.8.2/lib/rspec/core/example.rb:503:in `block in with_around_and_singleton_context_hooks'
|
272
|
-
/Users/st0012/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/rspec-core-3.8.2/lib/rspec/core/example.rb:460:in `block in with_around_example_hooks'
|
273
|
-
/Users/st0012/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/rspec-core-3.8.2/lib/rspec/core/hooks.rb:464:in `block in run'
|
122
|
+
$ bundle
|
274
123
|
```
|
275
124
|
|
276
|
-
|
277
|
-
It makes the device to track objects as they are ActiveRecord instances. For example:
|
125
|
+
Or install it directly:
|
278
126
|
|
279
|
-
```ruby
|
280
|
-
tap_on!(@post, track_as_records: true)
|
281
|
-
post = Post.find(@post.id) # same record but a different object
|
282
|
-
post.title #=> this call will be recorded as well
|
283
127
|
```
|
284
|
-
|
285
|
-
#### exclude_by_paths
|
286
|
-
It takes an array of call path patterns that we want to skip. This could be very helpful when working on large project like Rails applications.
|
287
|
-
|
288
|
-
```ruby
|
289
|
-
tap_on!(@post, exclude_by_paths: [/active_record/]).and_print(:method_name_and_location)
|
128
|
+
$ gem install tapping_device
|
290
129
|
```
|
291
130
|
|
292
|
-
|
293
|
-
_read_attribute FROM /RUBY_PATH/gems/2.6.0/gems/activerecord-5.2.0/lib/active_record/attribute_methods/read.rb:40
|
294
|
-
name FROM /PROJECT_PATH/sample/app/views/posts/show.html.erb:5
|
295
|
-
_read_attribute FROM /RUBY_PATH/gems/2.6.0/gems/activerecord-5.2.0/lib/active_record/attribute_methods/read.rb:40
|
296
|
-
user_id FROM /PROJECT_PATH/sample/app/views/posts/show.html.erb:10
|
297
|
-
.......
|
131
|
+
**Depending on the size of your application, `TappingDevice` could harm the performance significantly. So make sure you don't put it inside the production group**
|
298
132
|
|
299
|
-
# versus
|
300
133
|
|
301
|
-
|
302
|
-
user_id FROM /PROJECT_PATH/sample/app/views/posts/show.html.erb:10
|
303
|
-
to_param FROM /RUBY_PATH/gems/2.6.0/gems/actionpack-5.2.0/lib/action_dispatch/routing/route_set.rb:236
|
304
|
-
```
|
134
|
+
### Advance Usages & Options
|
305
135
|
|
306
|
-
####
|
307
|
-
|
308
|
-
Like `exclude_by_paths`, but work in an opposite way.
|
309
|
-
|
310
|
-
|
311
|
-
### Payload of The Call
|
312
|
-
All tapping methods (start with `tap_`) takes a block and yield a `Payload` object as block argument. It responds to
|
313
|
-
|
314
|
-
- `target` - the target for `tap_x` call
|
315
|
-
- `receiver` - the receiver object
|
316
|
-
- `method_name` - method’s name (symbol)
|
317
|
-
- e.g. `:name`
|
318
|
-
- `method_object` - the method object that’s being called. It might be `nil` in some edge cases.
|
319
|
-
- `arguments` - arguments of the method call
|
320
|
-
- e.g. `{name: “Stan”, age: 25}`
|
321
|
-
- `return_value` - return value of the method call
|
322
|
-
- `filepath` - path to the file that performs the method call
|
323
|
-
- `line_number`
|
324
|
-
- `defined_class` - in which class that defines the method being called
|
325
|
-
- `trace` - stack trace of the call. Default is an empty array unless `with_trace_to` option is set
|
326
|
-
- `sql` - sql that generated from the call (only present in `tap_sql!` payloads)
|
327
|
-
- `tp` - trace point object of this call
|
328
|
-
|
329
|
-
|
330
|
-
#### Symbols for Payload Helpers
|
331
|
-
- `FROM` for method call’s location
|
332
|
-
- `<=` for arguments
|
333
|
-
- `=>` for return value
|
334
|
-
- `@` for defined class
|
335
|
-
|
336
|
-
#### Payload Helpers
|
337
|
-
- `method_name_and_location` - `initialize FROM /PROJECT_PATH/tapping_device/spec/payload_spec.rb:7`
|
338
|
-
- `method_name_and_arguments` - `initialize <= {:name=>\"Stan\", :age=>25}`
|
339
|
-
- `method_name_and_return_value` - `ten => 10`
|
340
|
-
- `method_name_and_defined_class` - `initialize @ Student`
|
341
|
-
- `passed_at` -
|
342
|
-
```
|
343
|
-
Passed as 'object' in method ':initialize'
|
344
|
-
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
|
345
|
-
```
|
136
|
+
#### Add Conditions With `.with`
|
346
137
|
|
347
|
-
|
138
|
+
Sometimes we don't need to know all the calls or traces of an object; we just want some of them. In those cases, we can chain the helpers with `.with` to filter the calls/traces.
|
348
139
|
|
349
|
-
```
|
350
|
-
|
351
|
-
|
352
|
-
|
140
|
+
```ruby
|
141
|
+
# only prints calls with name matches /foo/
|
142
|
+
print_calls(object).with do |payload|
|
143
|
+
payload.method_name.to_s.match?(/foo/)
|
144
|
+
end
|
353
145
|
```
|
354
146
|
|
355
|
-
|
356
|
-
|
357
|
-
```
|
358
|
-
initialize @ Student
|
359
|
-
<= {:name=>"Stan", :age=>25}
|
360
|
-
=> 25
|
361
|
-
FROM /Users/st0012/projects/tapping_device/spec/payload_spec.rb:7
|
362
|
-
```
|
147
|
+
#### `colorize: false`
|
363
148
|
|
364
|
-
|
149
|
+
By default `print_calls` and `print_traces` colorize their output. If you don't want the colors, you can use `colorize: false` to disable it.
|
365
150
|
|
366
|
-
Tapping methods introduced above like `tap_on!` are designed for simple use cases. They’re actually short for
|
367
151
|
|
368
152
|
```ruby
|
369
|
-
|
370
|
-
device.tap_on!(object)
|
153
|
+
print_calls(object, colorize: false)
|
371
154
|
```
|
372
155
|
|
373
|
-
And if you want to do some more configurations like stopping them manually or setting stop condition, you must have a `TappingDevie` instance. You can either get them like the above code, or save the return value of `tap_*!` method calls.
|
374
156
|
|
375
|
-
####
|
157
|
+
#### `inspect: true`
|
376
158
|
|
377
|
-
|
378
|
-
1. Manually calling `device.stop!`
|
379
|
-
2. Setting stop condition with `device.stop_when`, like
|
159
|
+
As you might have noticed, all the objects are converted into strings with `#to_s` instead of `#inspect`. This is because when used on some Rails objects, `#inspect` can generate a significantly larger string than `#to_s`. For example:
|
380
160
|
|
381
|
-
```ruby
|
382
|
-
|
383
|
-
|
384
|
-
end
|
161
|
+
``` ruby
|
162
|
+
post.to_s #=> #<Post:0x00007f89a55201d0>
|
163
|
+
post.inspect #=> #<Post id: 649, user_id: 3, topic_id: 600, post_number: 1, raw: "Hello world", cooked: "<p>Hello world</p>", created_at: "2020-05-24 08:07:29", updated_at: "2020-05-24 08:07:29", reply_to_post_number: nil, reply_count: 0, quote_count: 0, deleted_at: nil, off_topic_count: 0, like_count: 0, incoming_link_count: 0, bookmark_count: 0, score: nil, reads: 0, post_type: 1, sort_order: 1, last_editor_id: 3, hidden: false, hidden_reason_id: nil, notify_moderators_count: 0, spam_count: 0, illegal_count: 0, inappropriate_count: 0, last_version_at: "2020-05-24 08:07:29", user_deleted: false, reply_to_user_id: nil, percent_rank: 1.0, notify_user_count: 0, like_score: 0, deleted_by_id: nil, edit_reason: nil, word_count: 2, version: 1, cook_method: 1, wiki: false, baked_at: "2020-05-24 08:07:29", baked_version: 2, hidden_at: nil, self_edits: 0, reply_quoted: false, via_email: false, raw_email: nil, public_version: 1, action_code: nil, image_url: nil, locked_by_id: nil, image_upload_id: nil>
|
385
164
|
```
|
386
165
|
|
387
|
-
#### Device states & Managing Devices
|
388
166
|
|
389
|
-
|
167
|
+
### Lower-Level Helpers
|
168
|
+
`print_calls` and `print_traces` aren't the only helpers you can get from `TappingDevice`. They are actually built on top of other helpers, which you can use as well. To know more about them, please check [this page](https://github.com/st0012/tapping_device/wiki/Advance-Usages)
|
390
169
|
|
391
|
-
- `Initial` - means the instance is initialized but hasn’t tapped on anything.
|
392
|
-
- `Enabled` - means the instance are tapping on something (has called `tap_*` methods).
|
393
|
-
- `Disabled` - means the instance has been disabled. It will no longer receive any call info.
|
394
170
|
|
395
|
-
|
171
|
+
### Related Blog Posts
|
172
|
+
- [Optimize Your Debugging Process With Object-Oriented Tracing and tapping_device](http://bit.ly/object-oriented-tracing)
|
173
|
+
- [Debug Rails issues effectively with tapping_device](https://dev.to/st0012/debug-rails-issues-effectively-with-tappingdevice-c7c)
|
174
|
+
- [Want to know more about your Rails app? Tap on your objects!](https://dev.to/st0012/want-to-know-more-about-your-rails-app-tap-on-your-objects-bd3)
|
396
175
|
|
397
|
-
- `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.
|
398
|
-
- `TappingDevice.stop_all!` - Stops all registered devices and remove them from the `devices` list.
|
399
|
-
- `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.
|
400
|
-
- `TappingDevice.reset!` - Cancels `suspend_new` (if called) and stops/removes all created devices. Useful to reset environment between test cases.
|
401
176
|
|
402
177
|
## Development
|
403
|
-
|
404
178
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
405
179
|
|
406
180
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
@@ -411,8 +185,8 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/st0012
|
|
411
185
|
|
412
186
|
## License
|
413
187
|
|
414
|
-
The gem is available as open
|
188
|
+
The gem is available as open-source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
415
189
|
|
416
190
|
## Code of Conduct
|
417
191
|
|
418
|
-
Everyone interacting in the TappingDevice project
|
192
|
+
Everyone interacting in the TappingDevice project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/tapping_device/blob/master/CODE_OF_CONDUCT.md).
|
Binary file
|
Binary file
|
Binary file
|
data/lib/tapping_device.rb
CHANGED
@@ -2,6 +2,7 @@ require "active_record"
|
|
2
2
|
require "tapping_device/version"
|
3
3
|
require "tapping_device/manageable"
|
4
4
|
require "tapping_device/payload"
|
5
|
+
require "tapping_device/output_payload"
|
5
6
|
require "tapping_device/trackable"
|
6
7
|
require "tapping_device/exceptions"
|
7
8
|
require "tapping_device/sql_tapping_methods"
|
@@ -25,12 +26,16 @@ class TappingDevice
|
|
25
26
|
@options = process_options(options)
|
26
27
|
@calls = []
|
27
28
|
@disabled = false
|
29
|
+
@with_condition = nil
|
28
30
|
self.class.devices << self
|
29
31
|
end
|
30
32
|
|
31
33
|
def tap_init!(klass)
|
32
34
|
raise "argument should be a class, got #{klass}" unless klass.is_a?(Class)
|
33
|
-
track(klass, condition: :tap_init?)
|
35
|
+
track(klass, condition: :tap_init?) do |payload|
|
36
|
+
payload[:return_value] = payload[:receiver]
|
37
|
+
payload[:receiver] = klass
|
38
|
+
end
|
34
39
|
end
|
35
40
|
|
36
41
|
def tap_on!(object)
|
@@ -46,8 +51,21 @@ class TappingDevice
|
|
46
51
|
track(record, condition: :tap_associations?)
|
47
52
|
end
|
48
53
|
|
49
|
-
def and_print(payload_method)
|
50
|
-
@output_block =
|
54
|
+
def and_print(payload_method = nil, &block)
|
55
|
+
@output_block =
|
56
|
+
if block
|
57
|
+
-> (output_payload) { puts(block.call(output_payload)) }
|
58
|
+
elsif payload_method
|
59
|
+
-> (output_payload) { puts(output_payload.send(payload_method)) }
|
60
|
+
else
|
61
|
+
raise "need to provide either a payload method name or a block"
|
62
|
+
end
|
63
|
+
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
def with(&block)
|
68
|
+
@with_condition = block
|
51
69
|
end
|
52
70
|
|
53
71
|
def set_block(&block)
|
@@ -81,15 +99,17 @@ class TappingDevice
|
|
81
99
|
|
82
100
|
private
|
83
101
|
|
84
|
-
def track(object, condition
|
102
|
+
def track(object, condition:, &payload_block)
|
85
103
|
@target = object
|
86
|
-
@trace_point = TracePoint.new(:
|
104
|
+
@trace_point = TracePoint.new(options[:event_type]) do |tp|
|
87
105
|
if send(condition, object, tp)
|
88
106
|
filepath, line_number = get_call_location(tp)
|
89
107
|
|
90
108
|
next if should_be_skipped_by_paths?(filepath)
|
91
109
|
|
92
|
-
payload = build_payload(tp: tp, filepath: filepath, line_number: line_number)
|
110
|
+
payload = build_payload(tp: tp, filepath: filepath, line_number: line_number, &payload_block)
|
111
|
+
|
112
|
+
next unless with_condition_satisfied?(payload)
|
93
113
|
|
94
114
|
record_call!(payload)
|
95
115
|
|
@@ -130,15 +150,12 @@ class TappingDevice
|
|
130
150
|
end
|
131
151
|
|
132
152
|
def build_payload(tp:, filepath:, line_number:)
|
133
|
-
|
134
|
-
tp.binding.local_variables.each { |name| arguments[name] = tp.binding.local_variable_get(name) }
|
135
|
-
|
136
|
-
Payload.init({
|
153
|
+
payload = Payload.init({
|
137
154
|
target: @target,
|
138
155
|
receiver: tp.self,
|
139
156
|
method_name: tp.callee_id,
|
140
157
|
method_object: get_method_object_from(tp.self, tp.callee_id),
|
141
|
-
arguments:
|
158
|
+
arguments: collect_arguments(tp),
|
142
159
|
return_value: (tp.return_value rescue nil),
|
143
160
|
filepath: filepath,
|
144
161
|
line_number: line_number,
|
@@ -146,6 +163,10 @@ class TappingDevice
|
|
146
163
|
trace: get_traces(tp),
|
147
164
|
tp: tp
|
148
165
|
})
|
166
|
+
|
167
|
+
yield(payload) if block_given?
|
168
|
+
|
169
|
+
payload
|
149
170
|
end
|
150
171
|
|
151
172
|
def tap_init?(klass, tp)
|
@@ -176,15 +197,7 @@ class TappingDevice
|
|
176
197
|
return false if is_from_target?(self, tp)
|
177
198
|
return false if tp.defined_class == TappingDevice::Trackable || tp.defined_class == TappingDevice
|
178
199
|
|
179
|
-
|
180
|
-
return false unless method_object.is_a?(Method)
|
181
|
-
# if a no-arugment method is called, tp.binding.local_variables will be those local variables in the same scope
|
182
|
-
# so we need to make sure the method takes arguments, then we can be sure that the locals are arguments
|
183
|
-
return false unless method_object && method_object.arity.to_i > 0
|
184
|
-
|
185
|
-
argument_values = tp.binding.local_variables.map { |name| tp.binding.local_variable_get(name) }
|
186
|
-
|
187
|
-
argument_values.any? do |value|
|
200
|
+
collect_arguments(tp).values.any? do |value|
|
188
201
|
# during comparison, Ruby might perform data type conversion like calling `to_sym` on the value
|
189
202
|
# but not every value supports every conversion methods
|
190
203
|
object == value rescue false
|
@@ -204,11 +217,25 @@ class TappingDevice
|
|
204
217
|
nil
|
205
218
|
end
|
206
219
|
|
220
|
+
def collect_arguments(tp)
|
221
|
+
parameters =
|
222
|
+
if RUBY_VERSION.to_f >= 2.6
|
223
|
+
tp.parameters
|
224
|
+
else
|
225
|
+
get_method_object_from(tp.self, tp.callee_id)&.parameters || []
|
226
|
+
end.map { |parameter| parameter[1] }
|
227
|
+
|
228
|
+
tp.binding.local_variables.each_with_object({}) do |name, args|
|
229
|
+
args[name] = tp.binding.local_variable_get(name) if parameters.include?(name)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
207
233
|
def process_options(options)
|
208
234
|
options[:filter_by_paths] ||= []
|
209
235
|
options[:exclude_by_paths] ||= []
|
210
236
|
options[:with_trace_to] ||= 50
|
211
237
|
options[:root_device] ||= self
|
238
|
+
options[:event_type] ||= :return
|
212
239
|
options[:descendants] ||= []
|
213
240
|
options[:track_as_records] ||= false
|
214
241
|
options
|
@@ -237,7 +264,7 @@ class TappingDevice
|
|
237
264
|
def record_call!(payload)
|
238
265
|
return if @disabled
|
239
266
|
|
240
|
-
@output_block.call(payload) if @output_block
|
267
|
+
@output_block.call(OutputPayload.init(payload)) if @output_block
|
241
268
|
|
242
269
|
if @block
|
243
270
|
root_device.calls << @block.call(payload)
|
@@ -245,4 +272,8 @@ class TappingDevice
|
|
245
272
|
root_device.calls << payload
|
246
273
|
end
|
247
274
|
end
|
275
|
+
|
276
|
+
def with_condition_satisfied?(payload)
|
277
|
+
@with_condition.blank? || @with_condition.call(payload)
|
278
|
+
end
|
248
279
|
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
class TappingDevice
|
2
|
+
class OutputPayload < Payload
|
3
|
+
alias :raw_arguments :arguments
|
4
|
+
alias :raw_return_value :return_value
|
5
|
+
|
6
|
+
def method_name(options = {})
|
7
|
+
":#{super(options)}"
|
8
|
+
end
|
9
|
+
|
10
|
+
def arguments(options = {})
|
11
|
+
generate_string_result(raw_arguments, options[:inspect])
|
12
|
+
end
|
13
|
+
|
14
|
+
def return_value(options = {})
|
15
|
+
generate_string_result(raw_return_value, options[:inspect])
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.full_color_code(code)
|
19
|
+
end
|
20
|
+
|
21
|
+
COLOR_CODES = {
|
22
|
+
green: 10,
|
23
|
+
yellow: 11,
|
24
|
+
blue: 12,
|
25
|
+
megenta: 13,
|
26
|
+
cyan: 14,
|
27
|
+
orange: 214
|
28
|
+
}
|
29
|
+
|
30
|
+
COLORS = COLOR_CODES.each_with_object({}) do |(name, code), hash|
|
31
|
+
hash[name] = "\u001b[38;5;#{code}m"
|
32
|
+
end.merge(
|
33
|
+
reset: "\u001b[0m",
|
34
|
+
nocolor: ""
|
35
|
+
)
|
36
|
+
|
37
|
+
PAYLOAD_ATTRIBUTES = {
|
38
|
+
method_name: {symbol: "", color: COLORS[:blue]},
|
39
|
+
location: {symbol: "from:", color: COLORS[:green]},
|
40
|
+
sql: {symbol: "QUERIES", color: COLORS[:nocolor]},
|
41
|
+
return_value: {symbol: "=>", color: COLORS[:megenta]},
|
42
|
+
arguments: {symbol: "<=", color: COLORS[:orange]},
|
43
|
+
defined_class: {symbol: "#", color: COLORS[:yellow]}
|
44
|
+
}
|
45
|
+
|
46
|
+
PAYLOAD_ATTRIBUTES.each do |attribute, attribute_options|
|
47
|
+
color = attribute_options[:color]
|
48
|
+
|
49
|
+
alias_method "original_#{attribute}".to_sym, attribute
|
50
|
+
|
51
|
+
# regenerate attributes with `colorize: true` support
|
52
|
+
define_method attribute do |options = {}|
|
53
|
+
call_result = send("original_#{attribute}", options)
|
54
|
+
|
55
|
+
if options[:colorize]
|
56
|
+
"#{color}#{call_result}#{COLORS[:reset]}"
|
57
|
+
else
|
58
|
+
call_result
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
define_method "#{attribute}_with_color" do |options = {}|
|
63
|
+
send(attribute, options.merge(colorize: true))
|
64
|
+
end
|
65
|
+
|
66
|
+
PAYLOAD_ATTRIBUTES.each do |and_attribute, and_attribute_options|
|
67
|
+
next if and_attribute == attribute
|
68
|
+
|
69
|
+
define_method "#{attribute}_and_#{and_attribute}" do |options = {}|
|
70
|
+
"#{send(attribute, options)} #{and_attribute_options[:symbol]} #{send(and_attribute, options)}"
|
71
|
+
end
|
72
|
+
|
73
|
+
define_method "#{attribute}_and_#{and_attribute}_with_color" do |options = {}|
|
74
|
+
send("#{attribute}_and_#{and_attribute}", options.merge(colorize: true))
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def passed_at(options = {})
|
80
|
+
with_method_head = options.fetch(:with_method_head, false)
|
81
|
+
arg_name = raw_arguments.keys.detect { |k| raw_arguments[k] == target }
|
82
|
+
|
83
|
+
return unless arg_name
|
84
|
+
|
85
|
+
arg_name = ":#{arg_name}"
|
86
|
+
arg_name = value_with_color(arg_name, :orange) if options[:colorize]
|
87
|
+
msg = "Passed as #{arg_name} in '#{defined_class(options)}##{method_name(options)}' at #{location(options)}"
|
88
|
+
msg += "\n > #{method_head.strip}" if with_method_head
|
89
|
+
msg
|
90
|
+
end
|
91
|
+
|
92
|
+
def detail_call_info(options = {})
|
93
|
+
<<~MSG
|
94
|
+
#{method_name_and_defined_class(options)}
|
95
|
+
from: #{location(options)}
|
96
|
+
<= #{arguments(options)}
|
97
|
+
=> #{return_value(options)}
|
98
|
+
|
99
|
+
MSG
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def value_with_color(value, color)
|
105
|
+
"#{COLORS[color]}#{value}#{COLORS[:reset]}"
|
106
|
+
end
|
107
|
+
|
108
|
+
def generate_string_result(obj, inspect)
|
109
|
+
case obj
|
110
|
+
when Array
|
111
|
+
array_to_string(obj, inspect)
|
112
|
+
when Hash
|
113
|
+
hash_to_string(obj, inspect)
|
114
|
+
when String
|
115
|
+
"\"#{obj}\""
|
116
|
+
else
|
117
|
+
inspect ? obj.inspect : obj.to_s
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def array_to_string(array, inspect)
|
122
|
+
elements_string = array.map do |elem|
|
123
|
+
generate_string_result(elem, inspect)
|
124
|
+
end.join(", ")
|
125
|
+
"[#{elements_string}]"
|
126
|
+
end
|
127
|
+
|
128
|
+
def hash_to_string(hash, inspect)
|
129
|
+
elements_string = hash.map do |key, value|
|
130
|
+
"#{key.to_s}: #{generate_string_result(value, inspect)}"
|
131
|
+
end.join(", ")
|
132
|
+
"{#{elements_string}}"
|
133
|
+
end
|
134
|
+
|
135
|
+
def obj_to_string(element, inspect)
|
136
|
+
to_string_method = inspect ? :inspect : :to_s
|
137
|
+
|
138
|
+
if !inspect && element.is_a?(String)
|
139
|
+
"\"#{element}\""
|
140
|
+
else
|
141
|
+
element.send(to_string_method)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -6,7 +6,7 @@ class TappingDevice
|
|
6
6
|
]
|
7
7
|
|
8
8
|
ATTRS.each do |attr|
|
9
|
-
define_method attr do
|
9
|
+
define_method attr do |options = {}|
|
10
10
|
self[attr]
|
11
11
|
end
|
12
12
|
end
|
@@ -19,45 +19,13 @@ class TappingDevice
|
|
19
19
|
h
|
20
20
|
end
|
21
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
22
|
def method_head
|
32
23
|
source_file, source_line = method_object.source_location
|
33
24
|
IO.readlines(source_file)[source_line-1]
|
34
25
|
end
|
35
26
|
|
36
|
-
def location
|
27
|
+
def location(options = {})
|
37
28
|
"#{filepath}:#{line_number}"
|
38
29
|
end
|
39
|
-
|
40
|
-
SYMBOLS = {
|
41
|
-
location: "FROM",
|
42
|
-
sql: "QUERIES",
|
43
|
-
return_value: "=>",
|
44
|
-
arguments: "<=",
|
45
|
-
defined_class: "@"
|
46
|
-
}
|
47
|
-
|
48
|
-
SYMBOLS.each do |name, symbol|
|
49
|
-
define_method "method_name_and_#{name}" do
|
50
|
-
"#{method_name} #{symbol} #{send(name)}"
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def detail_call_info
|
55
|
-
<<~MSG
|
56
|
-
#{method_name_and_defined_class}
|
57
|
-
<= #{arguments}
|
58
|
-
=> #{return_value || "nil"}
|
59
|
-
FROM #{location}
|
60
|
-
MSG
|
61
|
-
end
|
62
30
|
end
|
63
31
|
end
|
@@ -6,8 +6,33 @@ class TappingDevice
|
|
6
6
|
end
|
7
7
|
end
|
8
8
|
|
9
|
+
def print_traces(target, options = {})
|
10
|
+
options[:event_type] = :call
|
11
|
+
inspect = options.delete(:inspect)
|
12
|
+
colorize = options.fetch(:colorize, true)
|
13
|
+
|
14
|
+
device_1 = tap_on!(target, options).and_print do |output_payload|
|
15
|
+
"Called #{output_payload.method_name_and_location(inspect: inspect, colorize: colorize)}"
|
16
|
+
end
|
17
|
+
device_2 = tap_passed!(target, options).and_print do |output_payload|
|
18
|
+
output_payload.passed_at(inspect: inspect, colorize: colorize)
|
19
|
+
end
|
20
|
+
[device_1, device_2]
|
21
|
+
end
|
22
|
+
|
23
|
+
def print_calls(target, options = {})
|
24
|
+
inspect = options.delete(:inspect)
|
25
|
+
colorize = options.fetch(:colorize, true)
|
26
|
+
|
27
|
+
tap_on!(target, options).and_print do |output_payload|
|
28
|
+
output_payload.detail_call_info(inspect: inspect, colorize: colorize)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
9
32
|
def new_device(options, &block)
|
10
33
|
TappingDevice.new(options, &block)
|
11
34
|
end
|
12
35
|
end
|
13
36
|
end
|
37
|
+
|
38
|
+
include TappingDevice::Trackable
|
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
|
|
@@ -36,7 +36,7 @@ Gem::Specification.new do |spec|
|
|
36
36
|
spec.add_development_dependency "database_cleaner"
|
37
37
|
spec.add_development_dependency "bundler", "~> 2.0"
|
38
38
|
spec.add_development_dependency "pry"
|
39
|
-
spec.add_development_dependency "rake", "~>
|
39
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
40
40
|
spec.add_development_dependency "rspec", "~> 3.0"
|
41
41
|
spec.add_development_dependency "simplecov", "0.17.1"
|
42
42
|
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.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- st0012
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-05-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -86,14 +86,14 @@ dependencies:
|
|
86
86
|
requirements:
|
87
87
|
- - "~>"
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: '
|
89
|
+
version: '13.0'
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
94
|
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version: '
|
96
|
+
version: '13.0'
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
98
|
name: rspec
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -122,17 +122,20 @@ dependencies:
|
|
122
122
|
- - '='
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: 0.17.1
|
125
|
-
description: tapping_device
|
125
|
+
description: tapping_device lets you understand what your Ruby objects do without
|
126
|
+
digging into the code
|
126
127
|
email:
|
127
128
|
- stan001212@gmail.com
|
128
129
|
executables: []
|
129
130
|
extensions: []
|
130
131
|
extra_rdoc_files: []
|
131
132
|
files:
|
133
|
+
- ".DS_Store"
|
132
134
|
- ".github/workflows/gempush.yml"
|
133
135
|
- ".github/workflows/ruby.yml"
|
134
136
|
- ".gitignore"
|
135
137
|
- ".rspec"
|
138
|
+
- ".ruby-version"
|
136
139
|
- ".travis.yml"
|
137
140
|
- CODE_OF_CONDUCT.md
|
138
141
|
- Gemfile
|
@@ -142,9 +145,13 @@ files:
|
|
142
145
|
- Rakefile
|
143
146
|
- bin/console
|
144
147
|
- bin/setup
|
148
|
+
- images/print_calls - single entry.png
|
149
|
+
- images/print_calls.png
|
150
|
+
- images/print_traces.png
|
145
151
|
- lib/tapping_device.rb
|
146
152
|
- lib/tapping_device/exceptions.rb
|
147
153
|
- lib/tapping_device/manageable.rb
|
154
|
+
- lib/tapping_device/output_payload.rb
|
148
155
|
- lib/tapping_device/payload.rb
|
149
156
|
- lib/tapping_device/sql_tapping_methods.rb
|
150
157
|
- lib/tapping_device/trackable.rb
|
@@ -175,5 +182,6 @@ requirements: []
|
|
175
182
|
rubygems_version: 3.0.3
|
176
183
|
signing_key:
|
177
184
|
specification_version: 4
|
178
|
-
summary: tapping_device
|
185
|
+
summary: tapping_device lets you understand what your Ruby objects do without digging
|
186
|
+
into the code
|
179
187
|
test_files: []
|