tapping_device 0.5.2 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.DS_Store +0 -0
- data/CHANGELOG.md +210 -0
- data/Gemfile.lock +16 -11
- data/README.md +178 -127
- data/lib/tapping_device.rb +45 -31
- data/lib/tapping_device/configurable.rb +27 -0
- data/lib/tapping_device/method_hijacker.rb +51 -0
- data/lib/tapping_device/output.rb +42 -0
- data/lib/tapping_device/output/file_writer.rb +21 -0
- data/lib/tapping_device/output/payload.rb +166 -0
- data/lib/tapping_device/output/stdout_writer.rb +9 -0
- data/lib/tapping_device/output/writer.rb +20 -0
- data/lib/tapping_device/payload.rb +2 -3
- data/lib/tapping_device/trackable.rb +78 -19
- data/lib/tapping_device/trackers/initialization_tracker.rb +28 -2
- data/lib/tapping_device/trackers/mutation_tracker.rb +3 -23
- data/lib/tapping_device/version.rb +1 -1
- data/tapping_device.gemspec +2 -0
- metadata +38 -3
- data/lib/tapping_device/output_payload.rb +0 -173
data/lib/tapping_device.rb
CHANGED
@@ -1,10 +1,15 @@
|
|
1
1
|
require "active_record"
|
2
|
+
require "active_support/core_ext/module/introspection"
|
3
|
+
require "pry" # for using Method#source
|
4
|
+
|
2
5
|
require "tapping_device/version"
|
3
6
|
require "tapping_device/manageable"
|
4
7
|
require "tapping_device/payload"
|
5
|
-
require "tapping_device/
|
8
|
+
require "tapping_device/output"
|
6
9
|
require "tapping_device/trackable"
|
10
|
+
require "tapping_device/configurable"
|
7
11
|
require "tapping_device/exceptions"
|
12
|
+
require "tapping_device/method_hijacker"
|
8
13
|
require "tapping_device/trackers/initialization_tracker"
|
9
14
|
require "tapping_device/trackers/passed_tracker"
|
10
15
|
require "tapping_device/trackers/association_call_tracker"
|
@@ -23,29 +28,19 @@ class TappingDevice
|
|
23
28
|
|
24
29
|
extend Manageable
|
25
30
|
|
31
|
+
include Configurable
|
32
|
+
include Output::Helpers
|
33
|
+
|
26
34
|
def initialize(options = {}, &block)
|
27
35
|
@block = block
|
28
36
|
@output_block = nil
|
29
|
-
@options = process_options(options)
|
37
|
+
@options = process_options(options.dup)
|
30
38
|
@calls = []
|
31
39
|
@disabled = false
|
32
40
|
@with_condition = nil
|
33
41
|
TappingDevice.devices << self
|
34
42
|
end
|
35
43
|
|
36
|
-
def and_print(payload_method = nil, &block)
|
37
|
-
@output_block =
|
38
|
-
if block
|
39
|
-
-> (output_payload) { puts(block.call(output_payload)) }
|
40
|
-
elsif payload_method
|
41
|
-
-> (output_payload) { puts(output_payload.send(payload_method)) }
|
42
|
-
else
|
43
|
-
raise "need to provide either a payload method name or a block"
|
44
|
-
end
|
45
|
-
|
46
|
-
self
|
47
|
-
end
|
48
|
-
|
49
44
|
def with(&block)
|
50
45
|
@with_condition = block
|
51
46
|
end
|
@@ -83,6 +78,8 @@ class TappingDevice
|
|
83
78
|
@target = object
|
84
79
|
validate_target!
|
85
80
|
|
81
|
+
MethodHijacker.new(@target).hijack_methods! if options[:hijack_attr_methods]
|
82
|
+
|
86
83
|
@trace_point = build_minimum_trace_point(event_type: options[:event_type]) do |payload|
|
87
84
|
record_call!(payload)
|
88
85
|
|
@@ -97,15 +94,19 @@ class TappingDevice
|
|
97
94
|
private
|
98
95
|
|
99
96
|
def build_minimum_trace_point(event_type:)
|
100
|
-
TracePoint.new(event_type) do |tp|
|
97
|
+
TracePoint.new(*event_type) do |tp|
|
101
98
|
next unless filter_condition_satisfied?(tp)
|
102
|
-
next if is_tapping_device_call?(tp)
|
103
99
|
|
104
100
|
filepath, line_number = get_call_location(tp)
|
105
101
|
payload = build_payload(tp: tp, filepath: filepath, line_number: line_number)
|
106
102
|
|
107
|
-
|
108
|
-
|
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
|
109
110
|
|
110
111
|
yield(payload)
|
111
112
|
end
|
@@ -130,7 +131,7 @@ class TappingDevice
|
|
130
131
|
|
131
132
|
if Module.respond_to?(:module_parents)
|
132
133
|
tp.defined_class.module_parents.include?(TappingDevice)
|
133
|
-
|
134
|
+
elsif Module.respond_to?(:parents)
|
134
135
|
tp.defined_class.parents.include?(TappingDevice)
|
135
136
|
end
|
136
137
|
end
|
@@ -151,15 +152,14 @@ class TappingDevice
|
|
151
152
|
line_number: line_number,
|
152
153
|
defined_class: tp.defined_class,
|
153
154
|
trace: get_traces(tp),
|
155
|
+
is_private_call?: tp.defined_class.private_method_defined?(tp.callee_id),
|
156
|
+
tag: options[:tag],
|
154
157
|
tp: tp
|
155
158
|
})
|
156
159
|
end
|
157
160
|
|
158
161
|
def get_method_object_from(target, method_name)
|
159
|
-
|
160
|
-
rescue ArgumentError
|
161
|
-
method_method = Object.method(:method).unbind
|
162
|
-
method_method.bind(target).call(method_name)
|
162
|
+
Object.instance_method(:method).bind(target).call(method_name)
|
163
163
|
rescue NameError
|
164
164
|
# if any part of the program uses Refinement to extend its methods
|
165
165
|
# we might still get NoMethodError when trying to get that method outside the scope
|
@@ -201,13 +201,19 @@ class TappingDevice
|
|
201
201
|
end
|
202
202
|
|
203
203
|
def process_options(options)
|
204
|
-
options[:filter_by_paths] ||= []
|
205
|
-
options[:exclude_by_paths] ||= []
|
206
|
-
options[:with_trace_to] ||=
|
207
|
-
options[:
|
208
|
-
options[:
|
204
|
+
options[:filter_by_paths] ||= config[:filter_by_paths]
|
205
|
+
options[:exclude_by_paths] ||= config[:exclude_by_paths]
|
206
|
+
options[:with_trace_to] ||= config[:with_trace_to]
|
207
|
+
options[:event_type] ||= config[:event_type]
|
208
|
+
options[:hijack_attr_methods] ||= config[:hijack_attr_methods]
|
209
|
+
options[:track_as_records] ||= config[:track_as_records]
|
210
|
+
options[:ignore_private] ||= config[:ignore_private]
|
211
|
+
options[:only_private] ||= config[:only_private]
|
212
|
+
# for debugging the gem more easily
|
213
|
+
options[:force_recording] ||= false
|
214
|
+
|
209
215
|
options[:descendants] ||= []
|
210
|
-
options[:
|
216
|
+
options[:root_device] ||= self
|
211
217
|
options
|
212
218
|
end
|
213
219
|
|
@@ -227,7 +233,7 @@ class TappingDevice
|
|
227
233
|
def record_call!(payload)
|
228
234
|
return if @disabled
|
229
235
|
|
230
|
-
|
236
|
+
write_output!(payload) if @output_writer
|
231
237
|
|
232
238
|
if @block
|
233
239
|
root_device.calls << @block.call(payload)
|
@@ -236,10 +242,18 @@ class TappingDevice
|
|
236
242
|
end
|
237
243
|
end
|
238
244
|
|
245
|
+
def write_output!(payload)
|
246
|
+
@output_writer.write!(payload)
|
247
|
+
end
|
248
|
+
|
239
249
|
def stop_if_condition_fulfilled!(payload)
|
240
250
|
if @stop_when&.call(payload)
|
241
251
|
stop!
|
242
252
|
root_device.stop!
|
243
253
|
end
|
244
254
|
end
|
255
|
+
|
256
|
+
def config
|
257
|
+
TappingDevice.config
|
258
|
+
end
|
245
259
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "active_support/configurable"
|
2
|
+
require "active_support/concern"
|
3
|
+
|
4
|
+
class TappingDevice
|
5
|
+
module Configurable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
DEFAULTS = {
|
9
|
+
filter_by_paths: [],
|
10
|
+
exclude_by_paths: [],
|
11
|
+
with_trace_to: 50,
|
12
|
+
event_type: :return,
|
13
|
+
hijack_attr_methods: false,
|
14
|
+
track_as_records: false,
|
15
|
+
ignore_private: false,
|
16
|
+
only_private: false
|
17
|
+
}.merge(TappingDevice::Output::DEFAULT_OPTIONS)
|
18
|
+
|
19
|
+
included do
|
20
|
+
include ActiveSupport::Configurable
|
21
|
+
|
22
|
+
DEFAULTS.each do |key, value|
|
23
|
+
config[key] = value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
class TappingDevice
|
2
|
+
class MethodHijacker
|
3
|
+
attr_reader :target
|
4
|
+
|
5
|
+
def initialize(target)
|
6
|
+
@target = target
|
7
|
+
end
|
8
|
+
|
9
|
+
def hijack_methods!
|
10
|
+
target.methods.each do |method_name|
|
11
|
+
if is_writer_method?(method_name)
|
12
|
+
redefine_writer_method!(method_name)
|
13
|
+
elsif is_reader_method?(method_name)
|
14
|
+
redefine_reader_method!(method_name)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def is_writer_method?(method_name)
|
22
|
+
has_definition_source?(method_name) && method_name.match?(/\w+=/) && target.method(method_name).source.match?(/attr_writer|attr_accessor/)
|
23
|
+
end
|
24
|
+
|
25
|
+
def is_reader_method?(method_name)
|
26
|
+
has_definition_source?(method_name) && target.method(method_name).source.match?(/attr_reader|attr_accessor/)
|
27
|
+
end
|
28
|
+
|
29
|
+
def has_definition_source?(method_name)
|
30
|
+
target.method(method_name).source_location
|
31
|
+
end
|
32
|
+
|
33
|
+
def redefine_writer_method!(method_name)
|
34
|
+
ivar_name = "@#{method_name.to_s.sub("=", "")}"
|
35
|
+
|
36
|
+
target.instance_eval <<-RUBY, __FILE__, __LINE__ + 1
|
37
|
+
def #{method_name}(val)
|
38
|
+
#{ivar_name} = val
|
39
|
+
end
|
40
|
+
RUBY
|
41
|
+
end
|
42
|
+
|
43
|
+
def redefine_reader_method!(method_name)
|
44
|
+
target.instance_eval <<-RUBY, __FILE__, __LINE__ + 1
|
45
|
+
def #{method_name}
|
46
|
+
@#{method_name}
|
47
|
+
end
|
48
|
+
RUBY
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require "tapping_device/output/payload"
|
2
|
+
require "tapping_device/output/writer"
|
3
|
+
require "tapping_device/output/stdout_writer"
|
4
|
+
require "tapping_device/output/file_writer"
|
5
|
+
|
6
|
+
class TappingDevice
|
7
|
+
module Output
|
8
|
+
DEFAULT_OPTIONS = {
|
9
|
+
inspect: false,
|
10
|
+
colorize: true,
|
11
|
+
log_file: "/tmp/tapping_device.log"
|
12
|
+
}
|
13
|
+
|
14
|
+
module Helpers
|
15
|
+
def and_write(payload_method = nil, options: {}, &block)
|
16
|
+
and_output(payload_method, options: options, writer_klass: FileWriter, &block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def and_print(payload_method = nil, options: {}, &block)
|
20
|
+
and_output(payload_method, options: options, writer_klass: StdoutWriter, &block)
|
21
|
+
end
|
22
|
+
|
23
|
+
def and_output(payload_method = nil, options: {}, writer_klass:, &block)
|
24
|
+
output_block = generate_output_block(payload_method, block)
|
25
|
+
@output_writer = writer_klass.new(options, output_block)
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def generate_output_block(payload_method, block)
|
32
|
+
if block
|
33
|
+
block
|
34
|
+
elsif payload_method
|
35
|
+
-> (output_payload, output_options) { output_payload.send(payload_method, output_options) }
|
36
|
+
else
|
37
|
+
raise "need to provide either a payload method name or a block"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,21 @@
|
|
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
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require "pastel"
|
2
|
+
|
3
|
+
class TappingDevice
|
4
|
+
module Output
|
5
|
+
class Payload < Payload
|
6
|
+
UNDEFINED = "[undefined]"
|
7
|
+
PRIVATE_MARK = " (private)"
|
8
|
+
|
9
|
+
PASTEL = Pastel.new
|
10
|
+
PASTEL.alias_color(:orange, :bright_red, :bright_yellow)
|
11
|
+
|
12
|
+
alias :raw_arguments :arguments
|
13
|
+
alias :raw_return_value :return_value
|
14
|
+
|
15
|
+
def method_name(options = {})
|
16
|
+
name = ":#{super(options)}"
|
17
|
+
|
18
|
+
name += " [#{tag}]" if tag
|
19
|
+
name += PRIVATE_MARK if is_private_call?
|
20
|
+
|
21
|
+
name
|
22
|
+
end
|
23
|
+
|
24
|
+
def arguments(options = {})
|
25
|
+
generate_string_result(raw_arguments, options[:inspect])
|
26
|
+
end
|
27
|
+
|
28
|
+
def return_value(options = {})
|
29
|
+
generate_string_result(raw_return_value, options[:inspect])
|
30
|
+
end
|
31
|
+
|
32
|
+
PAYLOAD_ATTRIBUTES = {
|
33
|
+
method_name: {symbol: "", color: :bright_blue},
|
34
|
+
location: {symbol: "from:", color: :green},
|
35
|
+
return_value: {symbol: "=>", color: :magenta},
|
36
|
+
arguments: {symbol: "<=", color: :orange},
|
37
|
+
ivar_changes: {symbol: "changes:\n", color: :blue},
|
38
|
+
defined_class: {symbol: "#", color: :yellow}
|
39
|
+
}
|
40
|
+
|
41
|
+
PAYLOAD_ATTRIBUTES.each do |attribute, attribute_options|
|
42
|
+
color = attribute_options[:color]
|
43
|
+
|
44
|
+
alias_method "original_#{attribute}".to_sym, attribute
|
45
|
+
|
46
|
+
# regenerate attributes with `colorize: true` support
|
47
|
+
define_method attribute do |options = {}|
|
48
|
+
call_result = send("original_#{attribute}", options)
|
49
|
+
|
50
|
+
if options[:colorize]
|
51
|
+
PASTEL.send(color, call_result)
|
52
|
+
else
|
53
|
+
call_result
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
define_method "#{attribute}_with_color" do |options = {}|
|
58
|
+
send(attribute, options.merge(colorize: true))
|
59
|
+
end
|
60
|
+
|
61
|
+
PAYLOAD_ATTRIBUTES.each do |and_attribute, and_attribute_options|
|
62
|
+
next if and_attribute == attribute
|
63
|
+
|
64
|
+
define_method "#{attribute}_and_#{and_attribute}" do |options = {}|
|
65
|
+
"#{send(attribute, options)} #{and_attribute_options[:symbol]} #{send(and_attribute, options)}"
|
66
|
+
end
|
67
|
+
|
68
|
+
define_method "#{attribute}_and_#{and_attribute}_with_color" do |options = {}|
|
69
|
+
send("#{attribute}_and_#{and_attribute}", options.merge(colorize: true))
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def passed_at(options = {})
|
75
|
+
with_method_head = options.fetch(:with_method_head, false)
|
76
|
+
arg_name = raw_arguments.keys.detect { |k| raw_arguments[k] == target }
|
77
|
+
|
78
|
+
return unless arg_name
|
79
|
+
|
80
|
+
arg_name = ":#{arg_name}"
|
81
|
+
arg_name = PASTEL.orange(arg_name) if options[:colorize]
|
82
|
+
msg = "Passed as #{arg_name} in '#{defined_class(options)}##{method_name(options)}' at #{location(options)}\n"
|
83
|
+
msg += " > #{method_head}\n" if with_method_head
|
84
|
+
msg
|
85
|
+
end
|
86
|
+
|
87
|
+
def detail_call_info(options = {})
|
88
|
+
<<~MSG
|
89
|
+
#{method_name_and_defined_class(options)}
|
90
|
+
from: #{location(options)}
|
91
|
+
<= #{arguments(options)}
|
92
|
+
=> #{return_value(options)}
|
93
|
+
|
94
|
+
MSG
|
95
|
+
end
|
96
|
+
|
97
|
+
def ivar_changes(options = {})
|
98
|
+
super.map do |ivar, value_changes|
|
99
|
+
before = generate_string_result(value_changes[:before], options[:inspect])
|
100
|
+
after = generate_string_result(value_changes[:after], options[:inspect])
|
101
|
+
|
102
|
+
if options[:colorize]
|
103
|
+
ivar = PASTEL.orange(ivar)
|
104
|
+
before = PASTEL.bright_blue(before.to_s)
|
105
|
+
after = PASTEL.bright_blue(after.to_s)
|
106
|
+
end
|
107
|
+
|
108
|
+
" #{ivar}: #{before} => #{after}"
|
109
|
+
end.join("\n")
|
110
|
+
end
|
111
|
+
|
112
|
+
def call_info_with_ivar_changes(options = {})
|
113
|
+
<<~MSG
|
114
|
+
#{method_name_and_defined_class(options)}
|
115
|
+
from: #{location(options)}
|
116
|
+
changes:
|
117
|
+
#{ivar_changes(options)}
|
118
|
+
|
119
|
+
MSG
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def generate_string_result(obj, inspect)
|
125
|
+
case obj
|
126
|
+
when Array
|
127
|
+
array_to_string(obj, inspect)
|
128
|
+
when Hash
|
129
|
+
hash_to_string(obj, inspect)
|
130
|
+
when UNDEFINED
|
131
|
+
UNDEFINED
|
132
|
+
when String
|
133
|
+
"\"#{obj}\""
|
134
|
+
when nil
|
135
|
+
"nil"
|
136
|
+
else
|
137
|
+
inspect ? obj.inspect : obj.to_s
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def array_to_string(array, inspect)
|
142
|
+
elements_string = array.map do |elem|
|
143
|
+
generate_string_result(elem, inspect)
|
144
|
+
end.join(", ")
|
145
|
+
"[#{elements_string}]"
|
146
|
+
end
|
147
|
+
|
148
|
+
def hash_to_string(hash, inspect)
|
149
|
+
elements_string = hash.map do |key, value|
|
150
|
+
"#{key.to_s}: #{generate_string_result(value, inspect)}"
|
151
|
+
end.join(", ")
|
152
|
+
"{#{elements_string}}"
|
153
|
+
end
|
154
|
+
|
155
|
+
def obj_to_string(element, inspect)
|
156
|
+
to_string_method = inspect ? :inspect : :to_s
|
157
|
+
|
158
|
+
if !inspect && element.is_a?(String)
|
159
|
+
"\"#{element}\""
|
160
|
+
else
|
161
|
+
element.send(to_string_method)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|