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.
@@ -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,9 @@
1
+ class ObjectTracer
2
+ module Trackers
3
+ class MethodCallTracker < ObjectTracer
4
+ def filter_condition_satisfied?(tp)
5
+ is_from_target?(tp)
6
+ end
7
+ end
8
+ end
9
+ 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,3 @@
1
+ class ObjectTracer
2
+ VERSION = "1.0.0"
3
+ 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: []