gun_dog 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []