tapping_device 0.5.3 → 0.5.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: 9311ddb749d3c7bf09306161da25191c0de47734dea77f645f5d5af62a55d802
4
- data.tar.gz: 4df7c9fef5c3a3a2b3852b5b4f4db31ec071149dd19b3fceb1df6f6b1ac191e8
3
+ metadata.gz: 231efa59a44f630df303db52d0d3bb460698c02a116374838e4bad4d71d7f69e
4
+ data.tar.gz: ca80df8a0ea00350c1e074628425848b5cba4d7f53a7aa4176947d55a3164929
5
5
  SHA512:
6
- metadata.gz: deaece3e6096551df6fe3c7eb492f6049e72b32f7e19b7a8d4067f8d5a6dc9b6fe56a41d3bd9855220ae11c8adcc377cd188f51c043835845be5c2d9250bc6d1
7
- data.tar.gz: 057766f277605fb3e80bd2797ee1256f0f6d81fbca56c95226199a72c7e14565dc353292d06711847e257d2f552e9eb3ddcd076ec35f1abd3aaf30cc9f686f5c
6
+ metadata.gz: 77131aab9e85a2661694ee57470b9067ee44d479d574cbe395eefabd7f88350eaf31071c4ed0d117b1608856e9e6beff9d7ccae592a665c8969231425dafdc43
7
+ data.tar.gz: c06dedd14da7798cf93d80ae0d4866928c9e2522184f453588691fdf1a3220b21cfdf953f04575f1015cc1cb58c5bcb2cb5673fd9384cf5515855d048d0bbdb3
@@ -1,16 +1,18 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased](https://github.com/st0012/tapping_device/tree/HEAD)
3
+ ## [v0.5.3](https://github.com/st0012/tapping_device/tree/v0.5.3) (2020-06-21)
4
4
 
5
- [Full Changelog](https://github.com/st0012/tapping_device/compare/v0.5.2...HEAD)
5
+ [Full Changelog](https://github.com/st0012/tapping_device/compare/v0.5.2...v0.5.3)
6
6
 
7
7
  **Closed issues:**
8
8
 
9
+ - Global Configuration [\#46](https://github.com/st0012/tapping_device/issues/46)
9
10
  - Support write\_\* helpers [\#44](https://github.com/st0012/tapping_device/issues/44)
10
11
  - Use Method\#source to replace Payload\#method\_head’s implementation [\#19](https://github.com/st0012/tapping_device/issues/19)
11
12
 
12
13
  **Merged pull requests:**
13
14
 
15
+ - Support Global Configuration [\#48](https://github.com/st0012/tapping_device/pull/48) ([st0012](https://github.com/st0012))
14
16
  - Support write\_\* helpers [\#47](https://github.com/st0012/tapping_device/pull/47) ([st0012](https://github.com/st0012))
15
17
  - Hijack attr methods with `hijack\_attr\_methods` option [\#45](https://github.com/st0012/tapping_device/pull/45) ([st0012](https://github.com/st0012))
16
18
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- tapping_device (0.5.3)
4
+ tapping_device (0.5.4)
5
5
  activerecord (>= 5.2)
6
6
  activesupport
7
7
  pry
@@ -56,7 +56,7 @@ GEM
56
56
  thread_safe (0.3.6)
57
57
  tzinfo (1.2.7)
58
58
  thread_safe (~> 0.1)
59
- zeitwerk (2.3.0)
59
+ zeitwerk (2.3.1)
60
60
 
61
61
  PLATFORMS
62
62
  ruby
data/README.md CHANGED
@@ -8,187 +8,119 @@
8
8
 
9
9
 
10
10
  ## Introduction
11
- `TappingDevice` makes the objects tell you what they do, so you don't need to track them yourself.
11
+ As the name states, `TappingDevice` allows you to secretly listen to different events of an object:
12
12
 
13
- #### Contact Tracing For Objects
13
+ - `Method Calls` - what does the object do
14
+ - `Traces` - how is the object used by the application
15
+ - `State Mutations` - what happens inside the object
14
16
 
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
17
+ After collecting the events, `TappingDevice` will output them in a nice, readable format to either stdout or a file.
16
18
 
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)
19
- - `print_mutations(object)` to see what actions changed the object's state (instance variables)
19
+ **Ultimately, its goal is to let you know all the information you need for debugging with just 1 line of code.**
20
20
 
21
- Still sounds vague? Let's see some examples:
21
+ ## Usages
22
22
 
23
- ### `print_calls` - Track Method Calls
23
+ ### Track Method Calls
24
24
 
25
- 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 `Guardian` would do when a user creates a post; here's the controller action:
26
-
27
- ```ruby
28
- def create
29
- @manager_params = create_params
30
- @manager_params[:first_post_checks] = !is_api?
31
-
32
- manager = NewPostManager.new(current_user, @manager_params)
33
-
34
- if is_api?
35
- memoized_payload = DistributedMemoizer.memoize(signature_for(@manager_params), 120) do
36
- result = manager.perform
37
- MultiJson.dump(serialize_data(result, NewPostResultSerializer, root: false))
38
- end
39
-
40
- parsed_payload = JSON.parse(memoized_payload)
41
- backwards_compatible_json(parsed_payload, parsed_payload['success'])
42
- else
43
- result = manager.perform
44
- json = serialize_data(result, NewPostResultSerializer, root: false)
45
- backwards_compatible_json(json, result.success?)
46
- end
47
- end
48
- ```
49
-
50
- As you can see, it doesn't even exist in the controller action, which makes tracking it by reading code very hard to do.
51
-
52
- But with `TappingDevice`. You can use `print_calls` to show what method calls the object performs
53
-
54
- ```ruby
55
- def create
56
- # you can retrieve the current guardian object by calling guardian in the controller
57
- print_calls(guardian)
58
- @manager_params = create_params
59
-
60
- # .....
61
- ```
62
-
63
- Now, if you execute the code, like via tests:
64
-
65
- ```shell
66
- $ rspec spec/requests/posts_controller_spec.rb:603
67
- ```
68
-
69
- You can get all the method calls it performs with basically everything you need to know
25
+ By tracking an object's method calls, you'll be able to observe the object's behavior very easily
70
26
 
71
27
  <img src="https://github.com/st0012/tapping_device/blob/master/images/print_calls.png" alt="image of print_calls output" width="50%">
72
28
 
73
- Let's take a closer look at each entry. Everyone of them contains the method call's
74
- - method name
75
- - method source class/module
29
+ Each entry consists of 5 pieces of information:
30
+ - method name
31
+ - source of the method
76
32
  - call site
77
33
  - arguments
78
34
  - return value
79
35
 
80
36
  ![explanation of individual entry](https://github.com/st0012/tapping_device/blob/master/images/print_calls%20-%20single%20entry.png)
81
37
 
82
- 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.
38
+ #### Helpers
83
39
 
40
+ - `print_calls(object)` - prints the result to stdout
41
+ - `write_calls(object, log_file: "file_name")` - writes the result to a file
42
+ - the default file is `/tmp/tapping_device.log`, but you can change it with `log_file: "new_path"` option
84
43
 
85
- ### `print_traces` - See The Object's Traces
44
+ #### Use Cases
45
+ - Understand a service object/form object's behavior
46
+ - Debug a messy controller
86
47
 
87
- 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
48
+ ### Track Traces
88
49
 
89
- ```ruby
90
- def create
91
- @manager_params = create_params
92
- @manager_params[:first_post_checks] = !is_api?
93
-
94
- manager = NewPostManager.new(current_user, @manager_params)
95
-
96
- print_traces(manager)
97
- # .....
98
- ```
50
+ By tracking an object's traces, you'll be able to observe the object's journey in your application
99
51
 
100
- And after running the test case
52
+ ![image of print_traces output](https://github.com/st0012/tapping_device/blob/master/images/print_traces.png)
101
53
 
102
- ```shell
103
- $ rspec spec/requests/posts_controller_spec.rb:603
104
- ```
54
+ #### Helpers
105
55
 
106
- 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.
56
+ - `print_traces(object)` - prints the result to stdout
57
+ - `write_traces(object, log_file: "file_name")` - writes the result to a file
58
+ - the default file is `/tmp/tapping_device.log`, but you can change it with `log_file: "new_path"` option
107
59
 
108
- ![image of print_traces output](https://github.com/st0012/tapping_device/blob/master/images/print_traces.png)
60
+ #### Use Cases
61
+ - Debug argument related issues
62
+ - Understand how a library uses your objects
109
63
 
110
- ### `print_mutations` - Display All State Changes At Once
64
+ ### Track State Mutations
111
65
 
112
- Another thing that often bothers developers in debugging is to track an object's internal state changes. And `tapping_device` allows you to see all state changes with just one line of code. Let me keep using [Discourse](https://github.com/discourse/discourse) to demonstrate it.
66
+ By tracking an object's traces, you'll be able to observe the state changes happen inside the object between each method call
113
67
 
114
- When updating a post, it uses an object called `PostRevisor` to revise it:
68
+ <img src="https://github.com/st0012/tapping_device/blob/master/images/print_mutations.png" alt="image of print_mutations output" width="50%">
115
69
 
116
- ```ruby
117
- # app/controllers/posts_controller.rb
118
- class PostsController
119
- def update
120
- # ......
121
- revisor = PostRevisor.new(post, topic)
122
- revisor.revise!(current_user, changes, opts)
123
- # ......
124
- end
125
- end
126
- ```
70
+ #### Helpers
127
71
 
128
- In the `PostReviser#revise!`, it uses many instance variables to track different information:
72
+ - `print_mutations(object)` - prints the result to stdout
73
+ - `write_mutations(object, log_file: "file_name")` - writes the result to a file
74
+ - the default file is `/tmp/tapping_device.log`, but you can change it with `log_file: "new_path"` option
129
75
 
130
- ```ruby
131
- # lib/post_revisor.rb
132
- def revise!(editor, fields, opts = {})
133
- @editor = editor
134
- @fields = fields.with_indifferent_access
135
- @opts = opts
76
+ #### Use Cases
77
+ - Debug state related issues
78
+ - Debug memoization issues
136
79
 
137
- @topic_changes = TopicChanges.new(@topic, editor)
138
-
139
- # ......
80
+ ### Track All Instances Of A Class
140
81
 
141
- @revised_at = @opts[:revised_at] || Time.now
142
- @last_version_at = @post.last_version_at || Time.now
82
+ It's not always easy to directly access the objects we want to track, especially when they're managed by a library (e.g. `ActiveRecord::Relation`). In such cases, you can use these helpers to track the class's instances:
143
83
 
144
- @version_changed = false
145
- @post_successfully_saved = true
84
+ - `print_instance_calls(ObjectKlass)`
85
+ - `print_instance_traces(ObjectKlass)`
86
+ - `print_instance_mutations(ObjectKlass)`
87
+ - `write_instance_calls(ObjectKlass)`
88
+ - `write_instance_traces(ObjectKlass)`
89
+ - `write_instance_mutations(ObjectKlass)`
146
90
 
147
- @validate_post = true
148
- # ......
149
- end
150
- ```
151
91
 
152
- Tracking the changes of that many instance variables can be a painful task, especially when we want to know the values before and after certain method call. This is why I created `print_mutations` to save us from this.
92
+ ### Use `with_HELPER_NAME` for chained method calls
153
93
 
154
- Like other helpers, you only need 1 line of code
94
+ In Ruby programs, we often chain multiple methods together like this:
155
95
 
156
96
  ```ruby
157
- # app/controllers/posts_controller.rb
158
- class PostsController
159
- def update
160
- # ......
161
- revisor = PostRevisor.new(post, topic)
162
- print_mutations(revisor)
163
- revisor.revise!(current_user, changes, opts)
164
- # ......
165
- end
166
- end
97
+ SomeService.new(params).perform
167
98
  ```
168
99
 
169
- And then you'll see all the state changes:
100
+ And to debug it, we'll need to break the method chain into
170
101
 
171
- <img src="https://github.com/st0012/tapping_device/blob/master/images/print_mutations.png" alt="image of print_mutations output" width="50%">
172
-
173
- Now you can see what method changes which states. And more importantly, you get to see all the sate changes at once!
174
-
175
- **You can try these examples on [my fork of discourse](https://github.com/st0012/discourse/tree/demo-for-tapping-device)**
102
+ ```ruby
103
+ service = SomeService.new(params)
104
+ print_calls(service, options)
105
+ service.perform
106
+ ```
176
107
 
177
- ### `write_*` helpers
108
+ This kind of code changes are usually annoying, and that's one of the problems I want to solve with `TappingDevice`.
178
109
 
179
- `tapping_device` also provides helpers that write the events into files:
110
+ So here's another option, just insert a `with_HELPER_NAME` call in between:
180
111
 
181
- - `write_calls(object)`
182
- - `write_traces(object)`
183
- - `write_mutations(object)`
112
+ ```ruby
113
+ SomeService.new(params).with_print_calls(options).perform
114
+ ```
184
115
 
185
- The default destination is `/tmp/tapping_device.log`. You can change it with the `log_file` option:
116
+ And it'll behave exactly like
186
117
 
187
118
  ```ruby
188
- write_calls(object, log_file: "/tmp/another_file")
119
+ service = SomeService.new(params)
120
+ print_calls(service, options)
121
+ service.perform
189
122
  ```
190
123
 
191
-
192
124
  ## Installation
193
125
  Add this line to your application's Gemfile:
194
126
 
@@ -296,6 +228,40 @@ The default is `false` because
296
228
  2. It's still unclear if this hack safe enough for most applications.
297
229
 
298
230
 
231
+ #### `ignore_private`
232
+
233
+ Sometimes we use many private methods to perform trivial operations, like
234
+
235
+ ```ruby
236
+ class Operation
237
+ def extras
238
+ dig_attribute("extras")
239
+ end
240
+
241
+ private
242
+
243
+ def data
244
+ @data
245
+ end
246
+
247
+ def dig_attribute(attr)
248
+ data.dig("attributes", attr)
249
+ end
250
+ end
251
+ ```
252
+
253
+ And we may not be interested in those method calls. If that's the case, you can use the `ignore_private` option
254
+
255
+ ```ruby
256
+ operation = Operation.new(params)
257
+ print_calls(operation, ignore_private: true) #=> only prints the `extras` call
258
+ ```
259
+
260
+ #### `only_private`
261
+
262
+ This option does the opposite of the `ignore_private` option does.
263
+
264
+
299
265
  ### Global Configuration
300
266
 
301
267
  If you don't want to pass options every time you use a helper, you can use global configuration to change the default values:
@@ -341,3 +307,4 @@ The gem is available as open-source under the terms of the [MIT License](https:/
341
307
  ## Code of Conduct
342
308
 
343
309
  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).
310
+
@@ -34,7 +34,7 @@ class TappingDevice
34
34
  def initialize(options = {}, &block)
35
35
  @block = block
36
36
  @output_block = nil
37
- @options = process_options(options)
37
+ @options = process_options(options.dup)
38
38
  @calls = []
39
39
  @disabled = false
40
40
  @with_condition = nil
@@ -94,15 +94,19 @@ class TappingDevice
94
94
  private
95
95
 
96
96
  def build_minimum_trace_point(event_type:)
97
- TracePoint.new(event_type) do |tp|
97
+ TracePoint.new(*event_type) do |tp|
98
98
  next unless filter_condition_satisfied?(tp)
99
- next if is_tapping_device_call?(tp)
100
99
 
101
100
  filepath, line_number = get_call_location(tp)
102
101
  payload = build_payload(tp: tp, filepath: filepath, line_number: line_number)
103
102
 
104
- next if should_be_skipped_by_paths?(filepath)
105
- next unless with_condition_satisfied?(payload)
103
+ unless @options[:force_recording]
104
+ next if is_tapping_device_call?(tp)
105
+ next if should_be_skipped_by_paths?(filepath)
106
+ next unless with_condition_satisfied?(payload)
107
+ next if payload.is_private_call? && @options[:ignore_private]
108
+ next if !payload.is_private_call? && @options[:only_private]
109
+ end
106
110
 
107
111
  yield(payload)
108
112
  end
@@ -148,6 +152,7 @@ class TappingDevice
148
152
  line_number: line_number,
149
153
  defined_class: tp.defined_class,
150
154
  trace: get_traces(tp),
155
+ is_private_call?: tp.defined_class.private_method_defined?(tp.callee_id),
151
156
  tp: tp
152
157
  })
153
158
  end
@@ -204,6 +209,10 @@ class TappingDevice
204
209
  options[:event_type] ||= config[:event_type]
205
210
  options[:hijack_attr_methods] ||= config[:hijack_attr_methods]
206
211
  options[:track_as_records] ||= config[:track_as_records]
212
+ options[:ignore_private] ||= config[:ignore_private]
213
+ options[:only_private] ||= config[:only_private]
214
+ # for debugging the gem more easily
215
+ options[:force_recording] ||= false
207
216
 
208
217
  options[:descendants] ||= []
209
218
  options[:root_device] ||= self
@@ -12,6 +12,8 @@ class TappingDevice
12
12
  event_type: :return,
13
13
  hijack_attr_methods: false,
14
14
  track_as_records: false,
15
+ ignore_private: false,
16
+ only_private: false
15
17
  }.merge(TappingDevice::Output::DEFAULT_OPTIONS)
16
18
 
17
19
  included do
@@ -7,7 +7,11 @@ class TappingDevice
7
7
  alias :raw_return_value :return_value
8
8
 
9
9
  def method_name(options = {})
10
- ":#{super(options)}"
10
+ if is_private_call?
11
+ ":#{super(options)} (private)"
12
+ else
13
+ ":#{super(options)}"
14
+ end
11
15
  end
12
16
 
13
17
  def arguments(options = {})
@@ -2,7 +2,7 @@ class TappingDevice
2
2
  class Payload < Hash
3
3
  ATTRS = [
4
4
  :target, :receiver, :method_name, :method_object, :arguments, :return_value, :filepath, :line_number,
5
- :defined_class, :trace, :tp, :ivar_changes
5
+ :defined_class, :trace, :tp, :ivar_changes, :is_private_call?
6
6
  ]
7
7
 
8
8
  ATTRS.each do |attr|
@@ -20,28 +20,29 @@ class TappingDevice
20
20
  TappingDevice::Trackers::MutationTracker.new(options, &block).track(object)
21
21
  end
22
22
 
23
- def print_traces(target, options = {})
24
- output_traces(target, options, output_action: :and_print)
25
- end
23
+ [:calls, :traces, :mutations].each do |subject|
24
+ [:print, :write].each do |output_action|
25
+ helper_method_name = "#{output_action}_#{subject}"
26
26
 
27
- def write_traces(target, options = {})
28
- output_traces(target, options, output_action: :and_write)
29
- end
27
+ define_method helper_method_name do |target, options = {}|
28
+ send("output_#{subject}", target, options, output_action: "and_#{output_action}")
29
+ end
30
30
 
31
- def print_calls(target, options = {})
32
- output_calls(target, options, output_action: :and_print)
33
- end
31
+ define_method "with_#{helper_method_name}" do |options = {}|
32
+ send(helper_method_name, self, options)
33
+ self
34
+ end
34
35
 
35
- def write_calls(target, options = {})
36
- output_calls(target, options, output_action: :and_write)
37
- end
36
+ define_method "#{output_action}_instance_#{subject}" do |target_klass, options = {}|
37
+ collection_proxy = AsyncCollectionProxy.new
38
38
 
39
- def print_mutations(target, options = {})
40
- output_mutations(target, options, output_action: :and_print)
41
- end
39
+ tap_init!(target_klass, options) do |payload|
40
+ collection_proxy << send(helper_method_name, payload.return_value, options)
41
+ end
42
42
 
43
- def write_mutations(target, options = {})
44
- output_mutations(target, options, output_action: :and_write)
43
+ collection_proxy
44
+ end
45
+ end
45
46
  end
46
47
 
47
48
  private
@@ -84,12 +85,15 @@ class TappingDevice
84
85
  [options, output_options]
85
86
  end
86
87
 
88
+ # CollectionProxy delegates chained actions to multiple devices
87
89
  class CollectionProxy
90
+ CHAINABLE_ACTIONS = [:stop!, :stop_when, :with]
91
+
88
92
  def initialize(devices)
89
93
  @devices = devices
90
94
  end
91
95
 
92
- [:stop!, :stop_when, :with].each do |method|
96
+ CHAINABLE_ACTIONS.each do |method|
93
97
  define_method method do |&block|
94
98
  @devices.each do |device|
95
99
  device.send(method, &block)
@@ -97,6 +101,32 @@ class TappingDevice
97
101
  end
98
102
  end
99
103
  end
104
+
105
+ # AsyncCollectionProxy delegates chained actions to multiple device "asyncronously"
106
+ # when we use tapping methods like `tap_init!` to create sub-devices
107
+ # we need to find a way to pass the chained actions to every sub-device that's created
108
+ # and this can only happen asyncronously as we won't know when'll that happen
109
+ class AsyncCollectionProxy < CollectionProxy
110
+ def initialize(devices = [])
111
+ super
112
+ @blocks = {}
113
+ end
114
+
115
+ CHAINABLE_ACTIONS.each do |method|
116
+ define_method method do |&block|
117
+ super(&block)
118
+ @blocks[method] = block
119
+ end
120
+ end
121
+
122
+ def <<(device)
123
+ @devices << device
124
+
125
+ @blocks.each do |method, block|
126
+ device.send(method, &block)
127
+ end
128
+ end
129
+ end
100
130
  end
101
131
  end
102
132
 
@@ -1,6 +1,14 @@
1
1
  class TappingDevice
2
2
  module Trackers
3
3
  class InitializationTracker < TappingDevice
4
+ def initialize(options = {}, &block)
5
+ super
6
+ event_type = @options[:event_type]
7
+ # if a class doesn't override the 'initialize' method
8
+ # Class.new will only trigger c_return or c_call
9
+ @options[:event_type] = [event_type, "c_#{event_type}"]
10
+ end
11
+
4
12
  def build_payload(tp:, filepath:, line_number:)
5
13
  payload = super
6
14
  payload[:return_value] = payload[:receiver]
@@ -1,3 +1,3 @@
1
1
  class TappingDevice
2
- VERSION = "0.5.3"
2
+ VERSION = "0.5.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.5.3
4
+ version: 0.5.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - st0012
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-06-21 00:00:00.000000000 Z
11
+ date: 2020-07-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord