object_tracer 1.0.0
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 +7 -0
- data/.DS_Store +0 -0
- data/.github/workflows/gempush.yml +28 -0
- data/.github/workflows/ruby.yml +59 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +296 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +20 -0
- data/LICENSE.txt +21 -0
- data/Makefile +3 -0
- data/README.md +310 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/images/print_calls - single entry.png +0 -0
- data/images/print_calls.png +0 -0
- data/images/print_mutations.png +0 -0
- data/images/print_traces.png +0 -0
- data/lib/object_tracer.rb +258 -0
- data/lib/object_tracer/configuration.rb +34 -0
- data/lib/object_tracer/exceptions.rb +16 -0
- data/lib/object_tracer/manageable.rb +37 -0
- data/lib/object_tracer/method_hijacker.rb +55 -0
- data/lib/object_tracer/output.rb +41 -0
- data/lib/object_tracer/output/payload_wrapper.rb +186 -0
- data/lib/object_tracer/output/writer.rb +22 -0
- data/lib/object_tracer/payload.rb +40 -0
- data/lib/object_tracer/trackable.rb +133 -0
- data/lib/object_tracer/trackers/association_call_tracker.rb +17 -0
- data/lib/object_tracer/trackers/initialization_tracker.rb +53 -0
- data/lib/object_tracer/trackers/method_call_tracker.rb +9 -0
- data/lib/object_tracer/trackers/mutation_tracker.rb +111 -0
- data/lib/object_tracer/trackers/passed_tracker.rb +16 -0
- data/lib/object_tracer/version.rb +3 -0
- data/object_tracer.gemspec +29 -0
- metadata +113 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
class ObjectTracer
|
2
|
+
module Trackers
|
3
|
+
class AssociactionCallTracker < ObjectTracer
|
4
|
+
def validate_target!
|
5
|
+
raise NotAnActiveRecordInstanceError.new(target) unless target.is_a?(ActiveRecord::Base)
|
6
|
+
end
|
7
|
+
|
8
|
+
def filter_condition_satisfied?(tp)
|
9
|
+
return false unless is_from_target?(tp)
|
10
|
+
|
11
|
+
model_class = target.class
|
12
|
+
associations = model_class.reflections
|
13
|
+
associations.keys.include?(tp.callee_id.to_s)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
class ObjectTracer
|
2
|
+
module Trackers
|
3
|
+
class InitializationTracker < ObjectTracer
|
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
|
+
|
18
|
+
def build_payload(tp:, filepath:, line_number:)
|
19
|
+
payload = super
|
20
|
+
|
21
|
+
return payload if @is_active_record_model
|
22
|
+
|
23
|
+
payload.return_value = payload.receiver
|
24
|
+
payload.receiver = target
|
25
|
+
payload
|
26
|
+
end
|
27
|
+
|
28
|
+
def validate_target!
|
29
|
+
raise NotAClassError.new(target) unless target.is_a?(Class)
|
30
|
+
end
|
31
|
+
|
32
|
+
def filter_condition_satisfied?(tp)
|
33
|
+
receiver = tp.self
|
34
|
+
method_name = tp.callee_id
|
35
|
+
|
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
|
47
|
+
else
|
48
|
+
method_name == :initialize && receiver.is_a?(target)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
class ObjectTracer
|
2
|
+
module Trackers
|
3
|
+
class MutationTracker < ObjectTracer
|
4
|
+
def initialize(options, &block)
|
5
|
+
options[:hijack_attr_methods] = true
|
6
|
+
super
|
7
|
+
@snapshot_stack = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def track(object)
|
11
|
+
super
|
12
|
+
insert_snapshot_taking_trace_point
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def stop!
|
17
|
+
super
|
18
|
+
@ivar_snapshot_trace_point.disable
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# we need to snapshot instance variables at the beginning of every method call
|
24
|
+
# so we can get a correct state for the later comparison
|
25
|
+
def insert_snapshot_taking_trace_point
|
26
|
+
@ivar_snapshot_trace_point = build_minimum_trace_point(event_type: :call) do
|
27
|
+
snapshot_instance_variables
|
28
|
+
end
|
29
|
+
|
30
|
+
@ivar_snapshot_trace_point.enable unless ObjectTracer.suspend_new
|
31
|
+
end
|
32
|
+
|
33
|
+
def filter_condition_satisfied?(tp)
|
34
|
+
return false unless is_from_target?(tp)
|
35
|
+
|
36
|
+
if snapshot_capturing_event?(tp)
|
37
|
+
true
|
38
|
+
else
|
39
|
+
@latest_instance_variables = target_instance_variables
|
40
|
+
@instance_variables_snapshot = @snapshot_stack.pop
|
41
|
+
|
42
|
+
@latest_instance_variables != @instance_variables_snapshot
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_payload(tp:, filepath:, line_number:)
|
47
|
+
payload = super
|
48
|
+
|
49
|
+
if change_capturing_event?(tp)
|
50
|
+
payload.ivar_changes = capture_ivar_changes
|
51
|
+
end
|
52
|
+
|
53
|
+
payload
|
54
|
+
end
|
55
|
+
|
56
|
+
def capture_ivar_changes
|
57
|
+
changes = {}
|
58
|
+
additional_keys = @latest_instance_variables.keys - @instance_variables_snapshot.keys
|
59
|
+
additional_keys.each do |key|
|
60
|
+
changes[key] = {before: Output::PayloadWrapper::UNDEFINED, after: @latest_instance_variables[key]}
|
61
|
+
end
|
62
|
+
|
63
|
+
removed_keys = @instance_variables_snapshot.keys - @latest_instance_variables.keys
|
64
|
+
removed_keys.each do |key|
|
65
|
+
changes[key] = {before: @instance_variables_snapshot[key], after: Output::PayloadWrapper::UNDEFINED}
|
66
|
+
end
|
67
|
+
|
68
|
+
remained_keys = @latest_instance_variables.keys - additional_keys
|
69
|
+
remained_keys.each do |key|
|
70
|
+
next if @latest_instance_variables[key] == @instance_variables_snapshot[key]
|
71
|
+
changes[key] = {before: @instance_variables_snapshot[key], after: @latest_instance_variables[key]}
|
72
|
+
end
|
73
|
+
|
74
|
+
changes
|
75
|
+
end
|
76
|
+
|
77
|
+
def snapshot_instance_variables
|
78
|
+
@snapshot_stack.push(target_instance_variables)
|
79
|
+
end
|
80
|
+
|
81
|
+
def target_instance_variables
|
82
|
+
target.instance_variables.each_with_object({}) do |ivar, hash|
|
83
|
+
hash[ivar] = target.instance_variable_get(ivar)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def snapshot_capturing_event?(tp)
|
88
|
+
tp.event == :call
|
89
|
+
end
|
90
|
+
|
91
|
+
def change_capturing_event?(tp)
|
92
|
+
!snapshot_capturing_event?(tp)
|
93
|
+
end
|
94
|
+
|
95
|
+
# belows are debugging helpers
|
96
|
+
# I'll leave them for a while in case there's a bug in the tracker
|
97
|
+
def print_snapshot_stack(tp)
|
98
|
+
puts("===== STACK - #{tp.callee_id} (#{tp.event}) =====")
|
99
|
+
puts(@snapshot_stack)
|
100
|
+
puts("================ END STACK =================")
|
101
|
+
end
|
102
|
+
|
103
|
+
def print_state_comparison
|
104
|
+
puts("###############")
|
105
|
+
puts(@latest_instance_variables)
|
106
|
+
puts(@instance_variables_snapshot)
|
107
|
+
puts("###############")
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class ObjectTracer
|
2
|
+
module Trackers
|
3
|
+
# PassedTracker tracks calls that use the target object as an argument
|
4
|
+
class PassedTracker < ObjectTracer
|
5
|
+
def filter_condition_satisfied?(tp)
|
6
|
+
collect_arguments(tp).values.any? do |value|
|
7
|
+
# during comparison, Ruby might perform data type conversion like calling `to_sym` on the value
|
8
|
+
# but not every value supports every conversion methods
|
9
|
+
target == value rescue false
|
10
|
+
end
|
11
|
+
rescue
|
12
|
+
false
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
lib = File.expand_path("lib", __dir__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "object_tracer/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "object_tracer"
|
7
|
+
spec.version = ObjectTracer::VERSION
|
8
|
+
spec.authors = ["st0012"]
|
9
|
+
spec.email = ["stan001212@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = %q{object_tracer lets you understand what your Ruby objects do without digging into the code}
|
12
|
+
spec.description = %q{object_tracer lets you understand what your Ruby objects do without digging into the code}
|
13
|
+
spec.homepage = "https://github.com/st0012/object_tracer"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
17
|
+
spec.metadata["source_code_uri"] = "https://github.com/st0012/object_tracer"
|
18
|
+
spec.metadata["changelog_uri"] = "https://github.com/st0012/object_tracer/blob/master/CHANGELOG.md"
|
19
|
+
|
20
|
+
# Specify which files should be added to the gem when it is released.
|
21
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
22
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
23
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
24
|
+
end
|
25
|
+
spec.require_paths = ["lib"]
|
26
|
+
|
27
|
+
spec.add_dependency "method_source", "~> 1.0.0"
|
28
|
+
spec.add_dependency "pastel", "~> 0.7"
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: object_tracer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- st0012
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-05-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: method_source
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pastel
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.7'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.7'
|
41
|
+
description: object_tracer lets you understand what your Ruby objects do without digging
|
42
|
+
into the code
|
43
|
+
email:
|
44
|
+
- stan001212@gmail.com
|
45
|
+
executables: []
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- ".DS_Store"
|
50
|
+
- ".github/workflows/gempush.yml"
|
51
|
+
- ".github/workflows/ruby.yml"
|
52
|
+
- ".gitignore"
|
53
|
+
- ".rspec"
|
54
|
+
- ".ruby-version"
|
55
|
+
- ".travis.yml"
|
56
|
+
- CHANGELOG.md
|
57
|
+
- CODE_OF_CONDUCT.md
|
58
|
+
- Gemfile
|
59
|
+
- LICENSE.txt
|
60
|
+
- Makefile
|
61
|
+
- README.md
|
62
|
+
- Rakefile
|
63
|
+
- bin/console
|
64
|
+
- bin/setup
|
65
|
+
- images/print_calls - single entry.png
|
66
|
+
- images/print_calls.png
|
67
|
+
- images/print_mutations.png
|
68
|
+
- images/print_traces.png
|
69
|
+
- lib/object_tracer.rb
|
70
|
+
- lib/object_tracer/configuration.rb
|
71
|
+
- lib/object_tracer/exceptions.rb
|
72
|
+
- lib/object_tracer/manageable.rb
|
73
|
+
- lib/object_tracer/method_hijacker.rb
|
74
|
+
- lib/object_tracer/output.rb
|
75
|
+
- lib/object_tracer/output/payload_wrapper.rb
|
76
|
+
- lib/object_tracer/output/writer.rb
|
77
|
+
- lib/object_tracer/payload.rb
|
78
|
+
- lib/object_tracer/trackable.rb
|
79
|
+
- lib/object_tracer/trackers/association_call_tracker.rb
|
80
|
+
- lib/object_tracer/trackers/initialization_tracker.rb
|
81
|
+
- lib/object_tracer/trackers/method_call_tracker.rb
|
82
|
+
- lib/object_tracer/trackers/mutation_tracker.rb
|
83
|
+
- lib/object_tracer/trackers/passed_tracker.rb
|
84
|
+
- lib/object_tracer/version.rb
|
85
|
+
- object_tracer.gemspec
|
86
|
+
homepage: https://github.com/st0012/object_tracer
|
87
|
+
licenses:
|
88
|
+
- MIT
|
89
|
+
metadata:
|
90
|
+
homepage_uri: https://github.com/st0012/object_tracer
|
91
|
+
source_code_uri: https://github.com/st0012/object_tracer
|
92
|
+
changelog_uri: https://github.com/st0012/object_tracer/blob/master/CHANGELOG.md
|
93
|
+
post_install_message:
|
94
|
+
rdoc_options: []
|
95
|
+
require_paths:
|
96
|
+
- lib
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
requirements: []
|
108
|
+
rubygems_version: 3.2.15
|
109
|
+
signing_key:
|
110
|
+
specification_version: 4
|
111
|
+
summary: object_tracer lets you understand what your Ruby objects do without digging
|
112
|
+
into the code
|
113
|
+
test_files: []
|