gun_dog 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 61ac60f86383d4ec2f4df7d5dba8d6ab0f503dc7
4
+ data.tar.gz: 4dba94dd287a55fd391fdae2bce8168d2dc84bf9
5
+ SHA512:
6
+ metadata.gz: 340f5de44210a25028553ddb84a31183b997327258eb30331c1cfb0683450f77aef5518fb2cb18492d3aa427828bcd827782d05c7bccdfe6889777dc12d60d38
7
+ data.tar.gz: 595cb603c9f1c57030f5cbffa9e4ceb419ffe95869e3f085fb79aaea7fb82405cc56186d6439299e36b89527c75fc88f0f439e73a969a374294957cbef32534c
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.4
5
+ before_install: gem install bundler -v 1.15.4
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in gun_dog.gemspec
6
+ gemspec
data/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # GunDog
2
+
3
+ GunDog is a Tracepoint tool for finding the interface of particular classes
4
+ within a given context.
5
+
6
+ Often times you'd like to refactor a class, but are not sure about what sort of
7
+ calls the class may receive. GunDog sets up special Tracepoint listeners to log
8
+ and record code execution metrics on a given class.
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ ```ruby gem 'gun_dog' ```
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install gun_dog
23
+
24
+ ## Usage
25
+
26
+ To use GunDog find the general area were you'd like to refactor a class.
27
+
28
+ ```
29
+ trace = GunDog.trace(MyClassName) do
30
+ some_code_that_executes_your_class
31
+ end
32
+ ```
33
+
34
+ Trace is a GunDog::TraceReport object that can be saved to JSON (and loaded from
35
+ JSON) for analysis.
36
+
37
+ GunDog is still a pup - here are some upcoming features.
38
+
39
+ - [ ] TraceReport introspect methods from CallRecords
40
+ - [ ] TraceReport pretty reports
41
+ - [ ] Trace Multiple Classes with one Dog
42
+ - [ ] Inhibit: Generate warnings when methods in a TraceReport are called.
43
+
44
+
45
+ ## Development
46
+
47
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
48
+ `rake spec` to run the tests. You can also run `bin/console` for an interactive
49
+ prompt that will allow you to experiment.
50
+
51
+ To install this gem onto your local machine, run `bundle exec rake install`. To
52
+ release a new version, update the version number in `version.rb`, and then run
53
+ `bundle exec rake release`, which will create a git tag for the version, push
54
+ git commits and tags, and push the `.gem` file to
55
+ [rubygems.org](https://rubygems.org).
56
+
57
+ ## Contributing
58
+
59
+ Bug reports and pull requests are welcome on GitHub at
60
+ https://github.com/stephenprater/gun_dog.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "gun_dog"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/gun_dog.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "gun_dog/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "gun_dog"
8
+ spec.version = GunDog::VERSION
9
+ spec.authors = ["Stephen Prater"]
10
+ spec.email = ["me@stephenprater.com"]
11
+
12
+ spec.summary = %q{Log callsite information for a given class.}
13
+ spec.description = %q{Log callsite information for a given class.}
14
+ spec.homepage = "http://github.com/stephenprater/gun_dog"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_dependency "multi_json", "~> 1.12"
24
+ spec.add_dependency "activesupport", ">= 4.2.9"
25
+
26
+ spec.add_development_dependency "bundler", "~> 1.15"
27
+ spec.add_development_dependency "rake", "~> 10.0"
28
+ spec.add_development_dependency "rspec", "~> 3.0"
29
+ spec.add_development_dependency "pry-byebug"
30
+ end
@@ -0,0 +1,73 @@
1
+ module GunDog
2
+ class CallRecord
3
+ attr_accessor :args, :return_value, :method_name
4
+ attr_accessor :stack
5
+
6
+ attr_writer :internal, :cyclical
7
+
8
+ def self.from_json(json)
9
+ cr = new(const_get(json['klass']),
10
+ json['method_name'],
11
+ class_method: json['class_method'])
12
+
13
+ cr.instance_eval do
14
+ @internal = json['internal']
15
+ @cyclical = json['cyclical']
16
+ @args = json['args']
17
+ @return_value = json['return_value']
18
+ @stack = json['stack']
19
+ end
20
+
21
+ cr
22
+ end
23
+
24
+ def initialize(klass, method_name, class_method: false)
25
+ @klass = klass
26
+ @method_name = method_name
27
+ @class_method = class_method
28
+ end
29
+
30
+ def method_location
31
+ "#{@klass}#{method_separator}#{method_name}"
32
+ end
33
+
34
+ def internal?
35
+ !!@internal
36
+ end
37
+
38
+ def cyclical?
39
+ !!@cyclical
40
+ end
41
+
42
+ def class_method?
43
+ !!@class_method
44
+ end
45
+
46
+ def to_s
47
+ "def #{method_name}(#{type_signatures(args)}) => #{return_value}"
48
+ end
49
+
50
+ def as_json
51
+ {
52
+ "klass" => @klass,
53
+ "method_name" => method_name,
54
+ "class_method" => class_method?,
55
+ "internal" => internal?,
56
+ "cyclical" => cyclical?,
57
+ "args" => args,
58
+ "return_value" => return_value,
59
+ "stack" => stack
60
+ }.reject { |_,v| v.nil? }
61
+ end
62
+
63
+ private
64
+
65
+ def type_signatures(args)
66
+ args.each_pair.map { |k,v| "#{k} : #{v}" }.join(', ')
67
+ end
68
+
69
+ def method_separator
70
+ class_method? ? '.' : '#'
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,24 @@
1
+ module GunDog
2
+ class IndexedArray < Array
3
+ attr_reader :lut
4
+
5
+ def initialize(*args)
6
+ super(args)
7
+ @lut = {}
8
+ end
9
+
10
+ def <<(obj)
11
+ super(obj)
12
+ @lut[obj.object_id] = length
13
+ obj
14
+ end
15
+
16
+ def index_of_object(obj)
17
+ @lut.fetch(obj.object_id)
18
+ end
19
+
20
+ def find_object(obj)
21
+ at(index_of_object(obj))
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,7 @@
1
+ module GunDog
2
+ class MethodOwnerStackFrame < Struct.new(:klass, :method_name)
3
+ def to_s
4
+ "#{klass}##{method_name}"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,79 @@
1
+ module GunDog
2
+ class TraceMaker
3
+ attr_reader :trace_report, :return_trace, :call_trace, :klass
4
+
5
+ def initialize(klass, &exec_block)
6
+ @klass = klass
7
+ @trace_report = TraceReport.new(klass)
8
+ @trace_report.stack << MethodOwnerStackFrame.new(GunDog, :trace)
9
+ @exec_block = exec_block
10
+ end
11
+
12
+ def exec
13
+ set_trace
14
+
15
+ call_trace.enable do
16
+ return_trace.enable do
17
+ @exec_block.call
18
+ end
19
+ end
20
+
21
+ trace_report.finalize_report
22
+ trace_report
23
+ ensure
24
+ call_trace.disable
25
+ return_trace.disable
26
+ end
27
+
28
+ def set_trace
29
+ set_return_trace
30
+ set_call_trace
31
+ end
32
+
33
+ def set_return_trace
34
+ @return_trace ||= TracePoint.new(:return) do |tp|
35
+ trace_report.stack.pop
36
+ end
37
+ end
38
+
39
+ def set_call_trace
40
+ @call_trace ||= TracePoint.new(:call) do |tp|
41
+ trace_report.stack << MethodOwnerStackFrame.new(tp.defined_class, tp.method_id)
42
+ next unless tp.defined_class == klass || tp.defined_class == klass.singleton_class
43
+
44
+ tp.disable
45
+
46
+ call_record = CallRecord.new(klass, tp.method_id, class_method: tp.defined_class == klass.singleton_class)
47
+ called_method = tp.self.method(tp.method_id)
48
+ call_record.args = called_method.parameters.each.with_object({}) do |p, memo|
49
+ memo[p.last] = tp.binding.local_variable_get(p.last).class
50
+ end
51
+
52
+ trace_report.call_records << call_record
53
+
54
+ set_method_return_trace(call_record).enable
55
+
56
+ tp.enable
57
+ end
58
+ end
59
+
60
+ def set_method_return_trace(call_record)
61
+ # instantiate a new return tracepoint to watch for the return of this
62
+ # method only
63
+ #
64
+ TracePoint.new(:return) do |mrt|
65
+ next if mrt.method_id != call_record.method_name
66
+ mrt.disable
67
+ call_record.return_value = mrt.return_value.class
68
+
69
+ if trace_report.stack.internal_stack?
70
+ call_record.internal = true
71
+ call_record.stack = trace_report.stack.dup
72
+ elsif trace_report.stack.cyclical_stack?
73
+ call_record.cyclical = true
74
+ call_record.stack = trace_report.stack.dup
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,74 @@
1
+ module GunDog
2
+ class TraceReport
3
+ attr_reader :klass
4
+
5
+ def self.load(filename)
6
+ json = MultiJson.load(File.open(filename, 'r') { |f| f.read })
7
+ from_json(json)
8
+ end
9
+
10
+ def self.from_json(json)
11
+ tr = new(nil)
12
+
13
+ tr.instance_eval do
14
+ @klass = Kernel.const_get(json['klass'])
15
+ @stack = TraceStack.from_json(json.slice('klass','collaborating_classes'))
16
+ @call_records = json['call_records'].map { |cr| CallRecord.from_json(cr) }
17
+ end
18
+
19
+ tr
20
+ end
21
+
22
+ def call_records
23
+ @call_records ||= []
24
+ end
25
+
26
+ def initialize(klass)
27
+ @klass = klass
28
+ end
29
+
30
+ def collaborating_classes
31
+ stack.collaborating_classes - [GunDog, klass]
32
+ end
33
+
34
+ def stack
35
+ @stack ||= TraceStack.new(klass)
36
+ end
37
+
38
+ def finalize_report
39
+ @call_records.freeze
40
+ @stack.clear.freeze
41
+ @finalized = true
42
+ end
43
+
44
+ def finalized?
45
+ !!@finalized
46
+ end
47
+
48
+ def save(filename)
49
+ File.open(filename, 'w') { |f| f.puts(to_json) }
50
+ end
51
+
52
+
53
+ def as_json
54
+ {
55
+ "klass" => klass,
56
+ "collaborating_classes" => collaborating_classes.to_a,
57
+ "call_records" => call_records.map(&:as_json)
58
+ }
59
+ end
60
+
61
+ def to_json
62
+ MultiJson.dump(as_json)
63
+ end
64
+
65
+ def find_call_record(doc_name)
66
+ @find_cache ||= @call_records.group_by(&:method_location)
67
+ @find_cache[doc_name]
68
+ end
69
+
70
+ def method_list
71
+ @call_records.map(&:method_location)
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,57 @@
1
+ module GunDog
2
+ class TraceStack < Array
3
+ attr_reader :collaborating_classes, :klass
4
+
5
+ def self.from_json(json)
6
+ ts = new(json['klass'])
7
+
8
+ ts.instance_eval do
9
+ @collaborating_classes = Set.new(json['collaborating_classes'].map { |k| Kernel.const_get(k) })
10
+ end
11
+
12
+ ts
13
+ end
14
+
15
+ def classes_in_stack
16
+ self.group_by(&:klass).keys.to_set
17
+ end
18
+
19
+ def initialize(klass)
20
+ @klass = klass
21
+ @collaborating_classes = Set.new
22
+ end
23
+
24
+ def internal_stack?
25
+ # if the set of the classes contained in the trace is equivalent to the
26
+ # set of our own class then it is an interal stack (ie - all methods are
27
+ # internal to the traced class)
28
+ call_stack.classes_in_stack == self_set
29
+ end
30
+
31
+ def cyclical_stack?
32
+ # if the set of classes contained in the trace is a superset of the set
33
+ # of our class then it is a cyclical stack (ie, it contains calls both
34
+ # within and without of the class)
35
+ call_stack.classes_in_stack > self_set
36
+ end
37
+
38
+ def preceded_by_traced_klass?
39
+ traced_klasses = self.map(&:klass)
40
+ traced_klasses.include?(klass) || traced_klasses.include?(klass.singleton_class)
41
+ end
42
+
43
+ def self_set
44
+ [klass].to_set
45
+ end
46
+
47
+ def call_stack
48
+ # the stack excluding the sentinel (element zero) and our selves (element -1)
49
+ self.slice(1 .. -2) || TraceStack.new(klass)
50
+ end
51
+
52
+ def <<(frame)
53
+ collaborating_classes.add(frame.klass) if preceded_by_traced_klass?
54
+ super(frame)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,3 @@
1
+ module GunDog
2
+ VERSION = "0.0.1"
3
+ end
data/lib/gun_dog.rb ADDED
@@ -0,0 +1,17 @@
1
+ require "gun_dog/version"
2
+ require 'json'
3
+ require 'multi_json'
4
+ require 'active_support/core_ext/hash/slice'
5
+ require 'active_support/core_ext/string/filters'
6
+
7
+ module GunDog
8
+ autoload :MethodOwnerStackFrame, 'gun_dog/method_owner_stack_frame'
9
+ autoload :CallRecord, 'gun_dog/call_record'
10
+ autoload :TraceMaker, 'gun_dog/trace_maker'
11
+ autoload :TraceReport, 'gun_dog/trace_report'
12
+ autoload :TraceStack, 'gun_dog/trace_stack'
13
+
14
+ def self.trace(klass, &block)
15
+ TraceMaker.new(klass, &block).exec
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,144 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gun_dog
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Stephen Prater
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-10-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: multi_json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.12'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.12'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 4.2.9
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 4.2.9
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.15'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.15'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry-byebug
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Log callsite information for a given class.
98
+ email:
99
+ - me@stephenprater.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - ".rspec"
106
+ - ".travis.yml"
107
+ - Gemfile
108
+ - README.md
109
+ - Rakefile
110
+ - bin/console
111
+ - bin/setup
112
+ - gun_dog.gemspec
113
+ - lib/gun_dog.rb
114
+ - lib/gun_dog/call_record.rb
115
+ - lib/gun_dog/indexed_array.rb
116
+ - lib/gun_dog/method_owner_stack_frame.rb
117
+ - lib/gun_dog/trace_maker.rb
118
+ - lib/gun_dog/trace_report.rb
119
+ - lib/gun_dog/trace_stack.rb
120
+ - lib/gun_dog/version.rb
121
+ homepage: http://github.com/stephenprater/gun_dog
122
+ licenses: []
123
+ metadata: {}
124
+ post_install_message:
125
+ rdoc_options: []
126
+ require_paths:
127
+ - lib
128
+ required_ruby_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ required_rubygems_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ requirements: []
139
+ rubyforge_project:
140
+ rubygems_version: 2.5.2
141
+ signing_key:
142
+ specification_version: 4
143
+ summary: Log callsite information for a given class.
144
+ test_files: []