tapping_device 0.5.3 → 0.6.0

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: d8adc4b6842ade7721eafaf72e9b94c8a6a852c1f9f9beebdf2556b20cef6d4a
4
+ data.tar.gz: ab22b319f6468837e92f851f00f863cf732dd3c0cc73e286d5f9569d00c34db6
5
5
  SHA512:
6
- metadata.gz: deaece3e6096551df6fe3c7eb492f6049e72b32f7e19b7a8d4067f8d5a6dc9b6fe56a41d3bd9855220ae11c8adcc377cd188f51c043835845be5c2d9250bc6d1
7
- data.tar.gz: 057766f277605fb3e80bd2797ee1256f0f6d81fbca56c95226199a72c7e14565dc353292d06711847e257d2f552e9eb3ddcd076ec35f1abd3aaf30cc9f686f5c
6
+ metadata.gz: 992f31b79a3c0a8abc8471ae643f02586cf76d505da493cfe66b37338781251c3dfdba43a158776431c93a342e8b9a02baff78118b71c679aba2f9114c65077c
7
+ data.tar.gz: cad971d539041804a9860225f17ae23754dc46ea6fa5b7dc15d674e5189c081dd4f6f2602eeea9c27e616fb1b959126fe452ef5f641e249d73392782b17a3c2c
@@ -1,6 +1,11 @@
1
1
  name: Ruby
2
2
 
3
- on: [push, pull_request]
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ branches:
7
+ - master
8
+ pull_request:
4
9
 
5
10
  jobs:
6
11
  test:
@@ -8,9 +13,12 @@ jobs:
8
13
  runs-on: ${{ matrix.os }}
9
14
  strategy:
10
15
  matrix:
11
- rails_version: ['5.2', '6']
12
- ruby_version: ['2.5', '2.6']
16
+ rails_version: ['5.2', '6.0.0', '6.1.0']
17
+ ruby_version: ['2.7', '3.0']
13
18
  os: [ubuntu-latest]
19
+ exclude:
20
+ - ruby_version: '3.0'
21
+ rails_version: '5.2'
14
22
  steps:
15
23
  - uses: actions/checkout@v1
16
24
 
@@ -34,7 +42,9 @@ jobs:
34
42
  bundle install --jobs 4 --retry 3
35
43
 
36
44
  - name: Run test with Rails ${{ matrix.rails_version }}
37
- run: bundle exec rake
45
+ env:
46
+ RAILS_VERSION: ${{ matrix.rails_version }}
47
+ run: make test
38
48
 
39
49
  - name: Publish Test Coverage
40
50
  uses: paambaati/codeclimate-action@v2.6.0
data/.gitignore CHANGED
@@ -7,5 +7,7 @@
7
7
  /spec/reports/
8
8
  /tmp/
9
9
 
10
+ Gemfile.lock
11
+
10
12
  # rspec failure tracking
11
13
  .rspec_status
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.6.5
1
+ 3.0.1
data/CHANGELOG.md CHANGED
@@ -2,15 +2,84 @@
2
2
 
3
3
  ## [Unreleased](https://github.com/st0012/tapping_device/tree/HEAD)
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.7...HEAD)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - Support Ruby 3.0 [\#71](https://github.com/st0012/tapping_device/pull/71) ([st0012](https://github.com/st0012))
10
+
11
+ **Merged pull requests:**
12
+
13
+ - Drop activerecord requirement [\#73](https://github.com/st0012/tapping_device/pull/73) ([st0012](https://github.com/st0012))
14
+ - Improve file-writing tests [\#72](https://github.com/st0012/tapping_device/pull/72) ([st0012](https://github.com/st0012))
15
+ - Simplify output logic with Ruby' Logger class [\#70](https://github.com/st0012/tapping_device/pull/70) ([st0012](https://github.com/st0012))
16
+ - Refactor Payload classes [\#68](https://github.com/st0012/tapping_device/pull/68) ([st0012](https://github.com/st0012))
17
+
18
+ ## [v0.5.7](https://github.com/st0012/tapping_device/tree/v0.5.7) (2020-09-09)
19
+
20
+ [Full Changelog](https://github.com/st0012/tapping_device/compare/v0.5.6...v0.5.7)
21
+
22
+ **Closed issues:**
23
+
24
+ - Support tag option [\#64](https://github.com/st0012/tapping_device/issues/64)
25
+
26
+ **Merged pull requests:**
27
+
28
+ - Use pastel to replace handmade colorizing logic [\#66](https://github.com/st0012/tapping_device/pull/66) ([st0012](https://github.com/st0012))
29
+ - Add tag option [\#65](https://github.com/st0012/tapping_device/pull/65) ([st0012](https://github.com/st0012))
30
+
31
+ ## [v0.5.6](https://github.com/st0012/tapping_device/tree/v0.5.6) (2020-07-17)
32
+
33
+ [Full Changelog](https://github.com/st0012/tapping_device/compare/v0.5.5...v0.5.6)
34
+
35
+ ## [v0.5.5](https://github.com/st0012/tapping_device/tree/v0.5.5) (2020-07-16)
36
+
37
+ [Full Changelog](https://github.com/st0012/tapping_device/compare/v0.5.4...v0.5.5)
38
+
39
+ **Fixed bugs:**
40
+
41
+ - InitializationTracker's logic can cause error [\#60](https://github.com/st0012/tapping_device/issues/60)
42
+
43
+ **Closed issues:**
44
+
45
+ - Refactor get\_method\_from\_object [\#59](https://github.com/st0012/tapping_device/issues/59)
46
+
47
+ **Merged pull requests:**
48
+
49
+ - Fix init tracker [\#61](https://github.com/st0012/tapping_device/pull/61) ([st0012](https://github.com/st0012))
50
+
51
+ ## [v0.5.4](https://github.com/st0012/tapping_device/tree/v0.5.4) (2020-07-05)
52
+
53
+ [Full Changelog](https://github.com/st0012/tapping_device/compare/v0.5.3...v0.5.4)
54
+
55
+ **Closed issues:**
56
+
57
+ - Add with\_print\_calls method [\#52](https://github.com/st0012/tapping_device/issues/52)
58
+ - Tapping any instance of class [\#51](https://github.com/st0012/tapping_device/issues/51)
59
+ - Add ignore\_private option [\#50](https://github.com/st0012/tapping_device/issues/50)
60
+
61
+ **Merged pull requests:**
62
+
63
+ - Restructure README.md [\#58](https://github.com/st0012/tapping_device/pull/58) ([st0012](https://github.com/st0012))
64
+ - Better support on private methods [\#57](https://github.com/st0012/tapping_device/pull/57) ([st0012](https://github.com/st0012))
65
+ - Add with\_\* helpers \(e.g. with\_print\_calls\) [\#56](https://github.com/st0012/tapping_device/pull/56) ([st0012](https://github.com/st0012))
66
+ - Add force\_recording option for debugging [\#55](https://github.com/st0012/tapping_device/pull/55) ([st0012](https://github.com/st0012))
67
+ - Add print\_instance\_\* and write\_instance\_\* helpers [\#54](https://github.com/st0012/tapping_device/pull/54) ([st0012](https://github.com/st0012))
68
+ - Fix tap\_init by adding c\_\* event type [\#53](https://github.com/st0012/tapping_device/pull/53) ([st0012](https://github.com/st0012))
69
+
70
+ ## [v0.5.3](https://github.com/st0012/tapping_device/tree/v0.5.3) (2020-06-21)
71
+
72
+ [Full Changelog](https://github.com/st0012/tapping_device/compare/v0.5.2...v0.5.3)
6
73
 
7
74
  **Closed issues:**
8
75
 
76
+ - Global Configuration [\#46](https://github.com/st0012/tapping_device/issues/46)
9
77
  - Support write\_\* helpers [\#44](https://github.com/st0012/tapping_device/issues/44)
10
78
  - Use Method\#source to replace Payload\#method\_head’s implementation [\#19](https://github.com/st0012/tapping_device/issues/19)
11
79
 
12
80
  **Merged pull requests:**
13
81
 
82
+ - Support Global Configuration [\#48](https://github.com/st0012/tapping_device/pull/48) ([st0012](https://github.com/st0012))
14
83
  - Support write\_\* helpers [\#47](https://github.com/st0012/tapping_device/pull/47) ([st0012](https://github.com/st0012))
15
84
  - Hijack attr methods with `hijack\_attr\_methods` option [\#45](https://github.com/st0012/tapping_device/pull/45) ([st0012](https://github.com/st0012))
16
85
 
data/Gemfile CHANGED
@@ -2,3 +2,14 @@ source "https://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in tapping_device.gemspec
4
4
  gemspec
5
+
6
+ rails_version = ENV["RAILS_VERSION"]
7
+ rails_version = "6.1.0" if rails_version.nil?
8
+
9
+ if rails_version.to_f < 6
10
+ gem "sqlite3", "~> 1.3.0"
11
+ else
12
+ gem "sqlite3"
13
+ end
14
+
15
+ gem "activerecord", "~> #{rails_version}"
data/Makefile ADDED
@@ -0,0 +1,3 @@
1
+ test:
2
+ bundle exec rspec
3
+ WITH_ACTIVE_RECORD=true bundle exec rspec spec/active_record_spec.rb
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
+
@@ -1,4 +1,3 @@
1
- require "active_record"
2
1
  require "active_support/core_ext/module/introspection"
3
2
  require "pry" # for using Method#source
4
3
 
@@ -34,7 +33,7 @@ class TappingDevice
34
33
  def initialize(options = {}, &block)
35
34
  @block = block
36
35
  @output_block = nil
37
- @options = process_options(options)
36
+ @options = process_options(options.dup)
38
37
  @calls = []
39
38
  @disabled = false
40
39
  @with_condition = nil
@@ -94,15 +93,19 @@ class TappingDevice
94
93
  private
95
94
 
96
95
  def build_minimum_trace_point(event_type:)
97
- TracePoint.new(event_type) do |tp|
96
+ TracePoint.new(*event_type) do |tp|
98
97
  next unless filter_condition_satisfied?(tp)
99
- next if is_tapping_device_call?(tp)
100
98
 
101
99
  filepath, line_number = get_call_location(tp)
102
100
  payload = build_payload(tp: tp, filepath: filepath, line_number: line_number)
103
101
 
104
- next if should_be_skipped_by_paths?(filepath)
105
- next unless with_condition_satisfied?(payload)
102
+ unless @options[:force_recording]
103
+ next if is_tapping_device_call?(tp)
104
+ next if should_be_skipped_by_paths?(filepath)
105
+ next unless with_condition_satisfied?(payload)
106
+ next if payload.is_private_call? && @options[:ignore_private]
107
+ next if !payload.is_private_call? && @options[:only_private]
108
+ end
106
109
 
107
110
  yield(payload)
108
111
  end
@@ -137,7 +140,7 @@ class TappingDevice
137
140
  end
138
141
 
139
142
  def build_payload(tp:, filepath:, line_number:)
140
- Payload.init({
143
+ Payload.new(
141
144
  target: @target,
142
145
  receiver: tp.self,
143
146
  method_name: tp.callee_id,
@@ -148,15 +151,14 @@ class TappingDevice
148
151
  line_number: line_number,
149
152
  defined_class: tp.defined_class,
150
153
  trace: get_traces(tp),
154
+ is_private_call: tp.defined_class.private_method_defined?(tp.callee_id),
155
+ tag: options[:tag],
151
156
  tp: tp
152
- })
157
+ )
153
158
  end
154
159
 
155
160
  def get_method_object_from(target, method_name)
156
- target.method(method_name)
157
- rescue ArgumentError
158
- method_method = Object.method(:method).unbind
159
- method_method.bind(target).call(method_name)
161
+ Object.instance_method(:method).bind(target).call(method_name)
160
162
  rescue NameError
161
163
  # if any part of the program uses Refinement to extend its methods
162
164
  # we might still get NoMethodError when trying to get that method outside the scope
@@ -204,6 +206,10 @@ class TappingDevice
204
206
  options[:event_type] ||= config[:event_type]
205
207
  options[:hijack_attr_methods] ||= config[:hijack_attr_methods]
206
208
  options[:track_as_records] ||= config[:track_as_records]
209
+ options[:ignore_private] ||= config[:ignore_private]
210
+ options[:only_private] ||= config[:only_private]
211
+ # for debugging the gem more easily
212
+ options[:force_recording] ||= false
207
213
 
208
214
  options[:descendants] ||= []
209
215
  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
@@ -20,10 +20,14 @@ class TappingDevice
20
20
 
21
21
  def is_writer_method?(method_name)
22
22
  has_definition_source?(method_name) && method_name.match?(/\w+=/) && target.method(method_name).source.match?(/attr_writer|attr_accessor/)
23
+ rescue MethodSource::SourceNotFoundError
24
+ false
23
25
  end
24
26
 
25
27
  def is_reader_method?(method_name)
26
28
  has_definition_source?(method_name) && target.method(method_name).source.match?(/attr_reader|attr_accessor/)
29
+ rescue MethodSource::SourceNotFoundError
30
+ false
27
31
  end
28
32
 
29
33
  def has_definition_source?(method_name)
@@ -1,7 +1,6 @@
1
- require "tapping_device/output/payload"
1
+ require "logger"
2
+ require "tapping_device/output/payload_wrapper"
2
3
  require "tapping_device/output/writer"
3
- require "tapping_device/output/stdout_writer"
4
- require "tapping_device/output/file_writer"
5
4
 
6
5
  class TappingDevice
7
6
  module Output
@@ -13,16 +12,16 @@ class TappingDevice
13
12
 
14
13
  module Helpers
15
14
  def and_write(payload_method = nil, options: {}, &block)
16
- and_output(payload_method, options: options, writer_klass: FileWriter, &block)
15
+ and_output(payload_method, options: options, logger: Logger.new(options[:log_file]), &block)
17
16
  end
18
17
 
19
18
  def and_print(payload_method = nil, options: {}, &block)
20
- and_output(payload_method, options: options, writer_klass: StdoutWriter, &block)
19
+ and_output(payload_method, options: options, logger: Logger.new($stdout), &block)
21
20
  end
22
21
 
23
- def and_output(payload_method = nil, options: {}, writer_klass:, &block)
22
+ def and_output(payload_method = nil, options: {}, logger:, &block)
24
23
  output_block = generate_output_block(payload_method, block)
25
- @output_writer = writer_klass.new(options, output_block)
24
+ @output_writer = Writer.new(options: options, output_block: output_block, logger: logger)
26
25
  self
27
26
  end
28
27
 
@@ -1,13 +1,44 @@
1
+ require "pastel"
2
+
1
3
  class TappingDevice
2
4
  module Output
3
- class Payload < Payload
5
+ class PayloadWrapper
4
6
  UNDEFINED = "[undefined]"
7
+ PRIVATE_MARK = " (private)"
8
+
9
+ PASTEL = Pastel.new
10
+ PASTEL.alias_color(:orange, :bright_red, :bright_yellow)
11
+
12
+ TappingDevice::Payload::ATTRS.each do |attr|
13
+ define_method attr do |options = {}|
14
+ @payload.send(attr)
15
+ end
16
+ end
17
+
18
+ alias :is_private_call? :is_private_call
19
+
20
+ def method_head
21
+ @payload.method_head
22
+ end
23
+
24
+ def location(options = {})
25
+ @payload.location(options)
26
+ end
5
27
 
6
28
  alias :raw_arguments :arguments
7
29
  alias :raw_return_value :return_value
8
30
 
31
+ def initialize(payload)
32
+ @payload = payload
33
+ end
34
+
9
35
  def method_name(options = {})
10
- ":#{super(options)}"
36
+ name = ":#{@payload.method_name}"
37
+
38
+ name += " [#{tag}]" if tag
39
+ name += PRIVATE_MARK if is_private_call?
40
+
41
+ name
11
42
  end
12
43
 
13
44
  def arguments(options = {})
@@ -18,29 +49,13 @@ class TappingDevice
18
49
  generate_string_result(raw_return_value, options[:inspect])
19
50
  end
20
51
 
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
52
  PAYLOAD_ATTRIBUTES = {
38
- method_name: {symbol: "", color: COLORS[:blue]},
39
- location: {symbol: "from:", color: COLORS[:green]},
40
- return_value: {symbol: "=>", color: COLORS[:megenta]},
41
- arguments: {symbol: "<=", color: COLORS[:orange]},
42
- ivar_changes: {symbol: "changes:\n", color: COLORS[:blue]},
43
- defined_class: {symbol: "#", color: COLORS[:yellow]}
53
+ method_name: {symbol: "", color: :bright_blue},
54
+ location: {symbol: "from:", color: :green},
55
+ return_value: {symbol: "=>", color: :magenta},
56
+ arguments: {symbol: "<=", color: :orange},
57
+ ivar_changes: {symbol: "changes:\n", color: :blue},
58
+ defined_class: {symbol: "#", color: :yellow}
44
59
  }
45
60
 
46
61
  PAYLOAD_ATTRIBUTES.each do |attribute, attribute_options|
@@ -53,7 +68,7 @@ class TappingDevice
53
68
  call_result = send("original_#{attribute}", options)
54
69
 
55
70
  if options[:colorize]
56
- "#{color}#{call_result}#{COLORS[:reset]}"
71
+ PASTEL.send(color, call_result)
57
72
  else
58
73
  call_result
59
74
  end
@@ -83,7 +98,7 @@ class TappingDevice
83
98
  return unless arg_name
84
99
 
85
100
  arg_name = ":#{arg_name}"
86
- arg_name = value_with_color(arg_name, :orange) if options[:colorize]
101
+ arg_name = PASTEL.orange(arg_name) if options[:colorize]
87
102
  msg = "Passed as #{arg_name} in '#{defined_class(options)}##{method_name(options)}' at #{location(options)}\n"
88
103
  msg += " > #{method_head}\n" if with_method_head
89
104
  msg
@@ -100,17 +115,17 @@ class TappingDevice
100
115
  end
101
116
 
102
117
  def ivar_changes(options = {})
103
- super.map do |ivar, value_changes|
118
+ @payload.ivar_changes.map do |ivar, value_changes|
104
119
  before = generate_string_result(value_changes[:before], options[:inspect])
105
120
  after = generate_string_result(value_changes[:after], options[:inspect])
106
121
 
107
122
  if options[:colorize]
108
- ivar = "#{COLORS[:orange]}#{ivar}#{COLORS[:reset]}"
109
- before = "#{COLORS[:blue]}#{before.to_s}#{COLORS[:reset]}"
110
- after = "#{COLORS[:blue]}#{after.to_s}#{COLORS[:reset]}"
123
+ ivar = PASTEL.orange(ivar)
124
+ before = PASTEL.bright_blue(before.to_s)
125
+ after = PASTEL.bright_blue(after.to_s)
111
126
  end
112
127
 
113
- " #{ivar}: #{before.to_s} => #{after.to_s}"
128
+ " #{ivar}: #{before} => #{after}"
114
129
  end.join("\n")
115
130
  end
116
131
 
@@ -126,10 +141,6 @@ class TappingDevice
126
141
 
127
142
  private
128
143
 
129
- def value_with_color(value, color)
130
- "#{COLORS[color]}#{value}#{COLORS[:reset]}"
131
- end
132
-
133
144
  def generate_string_result(obj, inspect)
134
145
  case obj
135
146
  when Array
@@ -1,19 +1,21 @@
1
1
  class TappingDevice
2
2
  module Output
3
3
  class Writer
4
- def initialize(options, output_block)
4
+ def initialize(options:, output_block:, logger:)
5
5
  @options = options
6
6
  @output_block = output_block
7
+ @logger = logger
7
8
  end
8
9
 
9
10
  def write!(payload)
10
- raise NotImplementedError
11
+ output = generate_output(payload)
12
+ @logger << output
11
13
  end
12
14
 
13
15
  private
14
16
 
15
17
  def generate_output(payload)
16
- @output_block.call(Output::Payload.init(payload), @options)
18
+ @output_block.call(PayloadWrapper.new(payload), @options)
17
19
  end
18
20
  end
19
21
  end
@@ -1,22 +1,32 @@
1
1
  class TappingDevice
2
- class Payload < Hash
2
+ class Payload
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, :tag, :tp, :ivar_changes, :is_private_call
6
6
  ]
7
7
 
8
- ATTRS.each do |attr|
9
- define_method attr do |options = {}|
10
- self[attr]
11
- end
12
- end
8
+ attr_accessor(*ATTRS)
9
+
10
+ alias :is_private_call? :is_private_call
13
11
 
14
- def self.init(hash)
15
- h = new
16
- hash.each do |k, v|
17
- h[k] = v
18
- end
19
- h
12
+ def initialize(
13
+ target:, receiver:, method_name:, method_object:, arguments:, return_value:, filepath:, line_number:,
14
+ defined_class:, trace:, tag:, tp:, is_private_call:
15
+ )
16
+ @target = target
17
+ @receiver = receiver
18
+ @method_name = method_name
19
+ @method_object = method_object
20
+ @arguments = arguments
21
+ @return_value = return_value
22
+ @filepath = filepath
23
+ @line_number = line_number
24
+ @defined_class = defined_class
25
+ @trace = trace
26
+ @tag = tag
27
+ @tp = tp
28
+ @ivar_changes = {}
29
+ @is_private_call = is_private_call
20
30
  end
21
31
 
22
32
  def method_head
@@ -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.merge(force_recording: true)) 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,10 +1,27 @@
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
+
12
+ def track(object)
13
+ super
14
+ @is_active_record_model = defined?(ActiveRecord) && target.ancestors.include?(ActiveRecord::Base)
15
+ self
16
+ end
17
+
4
18
  def build_payload(tp:, filepath:, line_number:)
5
19
  payload = super
6
- payload[:return_value] = payload[:receiver]
7
- payload[:receiver] = target
20
+
21
+ return payload if @is_active_record_model
22
+
23
+ payload.return_value = payload.receiver
24
+ payload.receiver = target
8
25
  payload
9
26
  end
10
27
 
@@ -16,8 +33,17 @@ class TappingDevice
16
33
  receiver = tp.self
17
34
  method_name = tp.callee_id
18
35
 
19
- if target.ancestors.include?(ActiveRecord::Base)
20
- method_name == :new && receiver.ancestors.include?(target)
36
+ if @is_active_record_model
37
+ # ActiveRecord redefines model classes' .new method,
38
+ # so instead of calling Model#initialize, it'll actually call Model.new
39
+ # see https://github.com/rails/rails/blob/master/activerecord/lib/active_record/inheritance.rb#L50
40
+ method_name == :new &&
41
+ receiver.is_a?(Class) &&
42
+ # this checks if the model class is the target class or a subclass of it
43
+ receiver.ancestors.include?(target) &&
44
+ # Model.new triggers both c_return and return events. so we should only return in 1 type of the events
45
+ # otherwise the callback will be triggered twice
46
+ tp.event == :return
21
47
  else
22
48
  method_name == :initialize && receiver.is_a?(target)
23
49
  end
@@ -47,7 +47,7 @@ class TappingDevice
47
47
  payload = super
48
48
 
49
49
  if change_capturing_event?(tp)
50
- payload[:ivar_changes] = capture_ivar_changes
50
+ payload.ivar_changes = capture_ivar_changes
51
51
  end
52
52
 
53
53
  payload
@@ -55,15 +55,14 @@ class TappingDevice
55
55
 
56
56
  def capture_ivar_changes
57
57
  changes = {}
58
-
59
58
  additional_keys = @latest_instance_variables.keys - @instance_variables_snapshot.keys
60
59
  additional_keys.each do |key|
61
- changes[key] = {before: Output::Payload::UNDEFINED, after: @latest_instance_variables[key]}
60
+ changes[key] = {before: Output::PayloadWrapper::UNDEFINED, after: @latest_instance_variables[key]}
62
61
  end
63
62
 
64
63
  removed_keys = @instance_variables_snapshot.keys - @latest_instance_variables.keys
65
64
  removed_keys.each do |key|
66
- changes[key] = {before: @instance_variables_snapshot[key], after: Output::Payload::UNDEFINED}
65
+ changes[key] = {before: @instance_variables_snapshot[key], after: Output::PayloadWrapper::UNDEFINED}
67
66
  end
68
67
 
69
68
  remained_keys = @latest_instance_variables.keys - additional_keys
@@ -1,3 +1,3 @@
1
1
  class TappingDevice
2
- VERSION = "0.5.3"
2
+ VERSION = "0.6.0"
3
3
  end
@@ -26,16 +26,10 @@ Gem::Specification.new do |spec|
26
26
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
27
  spec.require_paths = ["lib"]
28
28
 
29
- if ENV["RAILS_VERSION"]
30
- spec.add_dependency "activerecord", "~> #{ENV["RAILS_VERSION"]}"
31
- else
32
- spec.add_dependency "activerecord", ">= 5.2"
33
- end
34
-
35
29
  spec.add_dependency "pry" # for using Method#source in MutationTracker
36
30
  spec.add_dependency "activesupport"
31
+ spec.add_dependency "pastel"
37
32
 
38
- spec.add_development_dependency "sqlite3", ">= 1.3.6"
39
33
  spec.add_development_dependency "database_cleaner"
40
34
  spec.add_development_dependency "bundler", "~> 2.0"
41
35
  spec.add_development_dependency "rake", "~> 13.0"
metadata CHANGED
@@ -1,29 +1,15 @@
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.6.0
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: 2021-04-25 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: activerecord
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '5.2'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '5.2'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: pry
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -53,19 +39,19 @@ dependencies:
53
39
  - !ruby/object:Gem::Version
54
40
  version: '0'
55
41
  - !ruby/object:Gem::Dependency
56
- name: sqlite3
42
+ name: pastel
57
43
  requirement: !ruby/object:Gem::Requirement
58
44
  requirements:
59
45
  - - ">="
60
46
  - !ruby/object:Gem::Version
61
- version: 1.3.6
62
- type: :development
47
+ version: '0'
48
+ type: :runtime
63
49
  prerelease: false
64
50
  version_requirements: !ruby/object:Gem::Requirement
65
51
  requirements:
66
52
  - - ">="
67
53
  - !ruby/object:Gem::Version
68
- version: 1.3.6
54
+ version: '0'
69
55
  - !ruby/object:Gem::Dependency
70
56
  name: database_cleaner
71
57
  requirement: !ruby/object:Gem::Requirement
@@ -154,8 +140,8 @@ files:
154
140
  - CHANGELOG.md
155
141
  - CODE_OF_CONDUCT.md
156
142
  - Gemfile
157
- - Gemfile.lock
158
143
  - LICENSE.txt
144
+ - Makefile
159
145
  - README.md
160
146
  - Rakefile
161
147
  - bin/console
@@ -170,9 +156,7 @@ files:
170
156
  - lib/tapping_device/manageable.rb
171
157
  - lib/tapping_device/method_hijacker.rb
172
158
  - lib/tapping_device/output.rb
173
- - lib/tapping_device/output/file_writer.rb
174
- - lib/tapping_device/output/payload.rb
175
- - lib/tapping_device/output/stdout_writer.rb
159
+ - lib/tapping_device/output/payload_wrapper.rb
176
160
  - lib/tapping_device/output/writer.rb
177
161
  - lib/tapping_device/payload.rb
178
162
  - lib/tapping_device/trackable.rb
@@ -205,7 +189,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
205
189
  - !ruby/object:Gem::Version
206
190
  version: '0'
207
191
  requirements: []
208
- rubygems_version: 3.0.3
192
+ rubygems_version: 3.2.15
209
193
  signing_key:
210
194
  specification_version: 4
211
195
  summary: tapping_device lets you understand what your Ruby objects do without digging
data/Gemfile.lock DELETED
@@ -1,74 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- tapping_device (0.5.3)
5
- activerecord (>= 5.2)
6
- activesupport
7
- pry
8
-
9
- GEM
10
- remote: https://rubygems.org/
11
- specs:
12
- activemodel (6.0.3.2)
13
- activesupport (= 6.0.3.2)
14
- activerecord (6.0.3.2)
15
- activemodel (= 6.0.3.2)
16
- activesupport (= 6.0.3.2)
17
- activesupport (6.0.3.2)
18
- concurrent-ruby (~> 1.0, >= 1.0.2)
19
- i18n (>= 0.7, < 2)
20
- minitest (~> 5.1)
21
- tzinfo (~> 1.1)
22
- zeitwerk (~> 2.2, >= 2.2.2)
23
- coderay (1.1.3)
24
- concurrent-ruby (1.1.6)
25
- database_cleaner (1.7.0)
26
- diff-lcs (1.3)
27
- docile (1.3.2)
28
- i18n (1.8.3)
29
- concurrent-ruby (~> 1.0)
30
- json (2.3.0)
31
- method_source (1.0.0)
32
- minitest (5.14.1)
33
- pry (0.13.1)
34
- coderay (~> 1.1)
35
- method_source (~> 1.0)
36
- rake (13.0.1)
37
- rspec (3.8.0)
38
- rspec-core (~> 3.8.0)
39
- rspec-expectations (~> 3.8.0)
40
- rspec-mocks (~> 3.8.0)
41
- rspec-core (3.8.2)
42
- rspec-support (~> 3.8.0)
43
- rspec-expectations (3.8.4)
44
- diff-lcs (>= 1.2.0, < 2.0)
45
- rspec-support (~> 3.8.0)
46
- rspec-mocks (3.8.1)
47
- diff-lcs (>= 1.2.0, < 2.0)
48
- rspec-support (~> 3.8.0)
49
- rspec-support (3.8.2)
50
- simplecov (0.17.1)
51
- docile (~> 1.1)
52
- json (>= 1.8, < 3)
53
- simplecov-html (~> 0.10.0)
54
- simplecov-html (0.10.2)
55
- sqlite3 (1.4.1)
56
- thread_safe (0.3.6)
57
- tzinfo (1.2.7)
58
- thread_safe (~> 0.1)
59
- zeitwerk (2.3.0)
60
-
61
- PLATFORMS
62
- ruby
63
-
64
- DEPENDENCIES
65
- bundler (~> 2.0)
66
- database_cleaner
67
- rake (~> 13.0)
68
- rspec (~> 3.0)
69
- simplecov (= 0.17.1)
70
- sqlite3 (>= 1.3.6)
71
- tapping_device!
72
-
73
- BUNDLED WITH
74
- 2.1.1
@@ -1,21 +0,0 @@
1
- class TappingDevice
2
- module Output
3
- class FileWriter < Writer
4
- def initialize(options, output_block)
5
- @path = options[:log_file]
6
-
7
- File.write(@path, "") # clean file
8
-
9
- super
10
- end
11
-
12
- def write!(payload)
13
- output = generate_output(payload)
14
-
15
- File.open(@path, "a") do |f|
16
- f << output
17
- end
18
- end
19
- end
20
- end
21
- end
@@ -1,9 +0,0 @@
1
- class TappingDevice
2
- module Output
3
- class StdoutWriter < Writer
4
- def write!(payload)
5
- puts(generate_output(payload))
6
- end
7
- end
8
- end
9
- end