cognitive_distance 0.0.1.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/.autotest +19 -0
  2. data/.gitignore +10 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +12 -0
  5. data/LICENSE.md +221 -0
  6. data/README.md +50 -0
  7. data/Rakefile +14 -0
  8. data/cognitive_distance.gemspec +23 -0
  9. data/lib/cognitive_distance.rb +23 -0
  10. data/lib/cognitive_distance/measurements.rb +16 -0
  11. data/lib/cognitive_distance/measurements/distinct_module_hops.rb +19 -0
  12. data/lib/cognitive_distance/measurements/measurement.rb +7 -0
  13. data/lib/cognitive_distance/measurements/module_hops.rb +17 -0
  14. data/lib/cognitive_distance/presenters.rb +2 -0
  15. data/lib/cognitive_distance/presenters/graph_to_dot.rb +11 -0
  16. data/lib/cognitive_distance/structures.rb +9 -0
  17. data/lib/cognitive_distance/structures/call_node.rb +74 -0
  18. data/lib/cognitive_distance/structures/call_node_root.rb +29 -0
  19. data/lib/cognitive_distance/structures/call_tree.rb +34 -0
  20. data/lib/cognitive_distance/structures/graph.rb +77 -0
  21. data/lib/cognitive_distance/structures/missing_call_context.rb +10 -0
  22. data/lib/cognitive_distance/tracer.rb +32 -0
  23. data/lib/cognitive_distance/transforms.rb +5 -0
  24. data/lib/cognitive_distance/transforms/call_tree_to_module_boundary_graph.rb +30 -0
  25. data/lib/cognitive_distance/version.rb +3 -0
  26. data/spec/cognitive_distance/measurements/distinct_module_hops_spec.rb +52 -0
  27. data/spec/cognitive_distance/measurements/measurement_spec.rb +44 -0
  28. data/spec/cognitive_distance/measurements/module_hops_spec.rb +54 -0
  29. data/spec/cognitive_distance/measurements_spec.rb +19 -0
  30. data/spec/cognitive_distance/presenters/graph_to_dot_spec.rb +26 -0
  31. data/spec/cognitive_distance/structures/call_node_root_spec.rb +31 -0
  32. data/spec/cognitive_distance/structures/call_node_spec.rb +91 -0
  33. data/spec/cognitive_distance/structures/call_tree_spec.rb +55 -0
  34. data/spec/cognitive_distance/structures/graph_spec.rb +142 -0
  35. data/spec/cognitive_distance/structures/missing_call_context_spec.rb +21 -0
  36. data/spec/cognitive_distance/tracer_spec.rb +57 -0
  37. data/spec/cognitive_distance/transforms/call_tree_to_module_boundary_graph_spec.rb +57 -0
  38. data/spec/spec_dummies.rb +60 -0
  39. data/spec/spec_helper.rb +12 -0
  40. metadata +111 -0
@@ -0,0 +1,2 @@
1
+ require 'cognitive_distance/presenters/graph_to_dot'
2
+
@@ -0,0 +1,11 @@
1
+ module CognitiveDistance::Presenters
2
+ class GraphToDot
3
+ def present graph, name='graphname', &label
4
+ label ||= lambda { |v| v.to_s }
5
+ "digraph #{name.to_s.inspect} {\n" + graph.inject("") { |str, (v1,v2)|
6
+ str << "#{label[v1].inspect} -> #{label[v2].inspect};\n"
7
+ } + "}"
8
+ end
9
+ end
10
+ end
11
+
@@ -0,0 +1,9 @@
1
+ require 'cognitive_distance/structures/missing_call_context'
2
+ require 'cognitive_distance/structures/call_node'
3
+ require 'cognitive_distance/structures/call_node_root'
4
+ require 'cognitive_distance/structures/call_tree'
5
+ require 'cognitive_distance/structures/graph'
6
+
7
+ module CognitiveDistance::Structures
8
+ end
9
+
@@ -0,0 +1,74 @@
1
+ module CognitiveDistance::Structures
2
+ class CallNode
3
+ include Enumerable
4
+
5
+ NO_CONTEXT = MissingCallContext.new.freeze
6
+ attr_reader :children, :parent
7
+ attr_accessor :trace_file, :trace_line, :trace_class,
8
+ :trace_method, :trace_binding
9
+
10
+ def initialize par=nil
11
+ @parent = par
12
+ @children = []
13
+ yield self if block_given?
14
+ end
15
+
16
+ # Create a new call node, add it to children and return it
17
+ def push! k, m, f, l, b
18
+ CallNode.new(self) do |node|
19
+ node.trace_file = f
20
+ node.trace_line = l
21
+ node.trace_class = k
22
+ node.trace_method = m
23
+ node.trace_binding = b
24
+ end.tap do |node|
25
+ children << node
26
+ end
27
+ end
28
+
29
+ # TODO: Maybe we shouldn't blindly assume that the pop! belongs
30
+ # to us?
31
+ def pop! *args # k, m, f, l, b
32
+ parent
33
+ end
34
+
35
+ def context
36
+ @context ||= create_context
37
+ @context
38
+ end
39
+
40
+ def each &block
41
+ return enum_for(:each) unless block
42
+ yield self
43
+ children.each do |node|
44
+ node.each(&block)
45
+ end
46
+ self
47
+ end
48
+
49
+ def empty?
50
+ children.empty?
51
+ end
52
+
53
+ def size
54
+ children.inject(children.size) { |s,c| s + c.size }
55
+ end
56
+
57
+ def freeze
58
+ context # Ensure context is set
59
+ children.each(&:freeze)
60
+ children.freeze
61
+ super
62
+ end
63
+
64
+ def to_a
65
+ [self, children.map(&:to_a)]
66
+ end
67
+
68
+ private
69
+ def create_context
70
+ trace_binding.eval("self") rescue NO_CONTEXT
71
+ end
72
+ end
73
+ end
74
+
@@ -0,0 +1,29 @@
1
+ module CognitiveDistance::Structures
2
+ # Some special behavior on root
3
+ class CallNodeRoot < CallNode
4
+ # We never go looking for the parents of root.
5
+ def pop! *args
6
+ self
7
+ end
8
+
9
+ # A root node does not correspond to a trace line, thus has
10
+ # no context, ever.
11
+ def context
12
+ NO_CONTEXT
13
+ end
14
+
15
+ # Do the same as CallNode, except we don't yield self.
16
+ def each &block
17
+ return enum_for(:each) unless block
18
+ children.each do |node|
19
+ node.each(&block)
20
+ end
21
+ self
22
+ end
23
+
24
+ # Do the same as CallNode, except we don't include self.
25
+ def to_a
26
+ @children.map(&:to_a)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,34 @@
1
+ require 'forwardable'
2
+
3
+ module CognitiveDistance::Structures
4
+ class CallTree
5
+ extend Forwardable
6
+ include Enumerable
7
+
8
+ def_delegators :@root, :empty?, :size, :to_a, :each
9
+ attr_reader :root
10
+
11
+ def initialize
12
+ @root = CallNodeRoot.new
13
+ @current_node = @root
14
+ end
15
+
16
+ def called k, m, f, l, b
17
+ # Set the current node to the node given back
18
+ @current_node = @current_node.push! k, m, f, l, b
19
+ self
20
+ end
21
+
22
+ def returned k, m, f, l, b
23
+ # Set the current node to the node given back
24
+ @current_node = @current_node.pop! k, m, f, l, b
25
+ self
26
+ end
27
+
28
+ def freeze
29
+ @root.freeze
30
+ @current_node = @root
31
+ super
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,77 @@
1
+ module CognitiveDistance::Structures
2
+ class Graph
3
+ include Enumerable
4
+
5
+ def initialize
6
+ # The keys are the out-bound vertex
7
+ @links = {}
8
+ end
9
+
10
+ def link from, *tos
11
+ @links[from] ||= []
12
+ @links[from] += tos
13
+ self
14
+ end
15
+
16
+ def bilink n1, n2
17
+ link n1, n2
18
+ link n2, n1
19
+ self
20
+ end
21
+
22
+ # Really?
23
+ def vertices
24
+ @links.inject([]) { |vs, (n, ns)|
25
+ vs << n << ns
26
+ }.flatten.uniq
27
+ end
28
+
29
+ def edges vertex=nil
30
+ @links.inject([]) { |es, (n, ns)|
31
+ es + (ns.map { |n2| [n, n2] })
32
+ }
33
+ end
34
+ alias :to_a :edges
35
+
36
+ def any_edges vertex
37
+ edges.select { |(n1,n2)| n1 == vertex || n2 == vertex }
38
+ end
39
+
40
+ def in_edges vertex
41
+ edges.select { |(n1,n2)| n2 == vertex }
42
+ end
43
+
44
+ # We could improve this because the keys of @links indicate the
45
+ # out-bound edges for a given vertex, but for now let's go with
46
+ # consistent
47
+ def out_edges vertex
48
+ edges.select { |(n1,n2)| n1 == vertex }
49
+ end
50
+
51
+ def empty?
52
+ @links.empty?
53
+ end
54
+
55
+ def each &block
56
+ edges.each &block
57
+ end
58
+
59
+ # Two graphs are equal if they have the same edges and vertices
60
+ # As our graphs do not contain any unlinked vertices, we can get by
61
+ # with testing mutual inclusion of edges
62
+ def == other
63
+ return true if equal?(other) # save some time
64
+ return false unless other.respond_to?(:to_a)
65
+ e1s = to_a
66
+ e2s = other.to_a
67
+ e1s.size == e2s.size && e1s.all? { |e1| e2s.include? e1 }
68
+ end
69
+
70
+ # Duck-typing through `to_a` isn't enough, you actually have to be an instance
71
+ # of Graph (or an instance of a subclass)
72
+ def eql? other
73
+ Graph === other && self == other
74
+ end
75
+ end
76
+ end
77
+
@@ -0,0 +1,10 @@
1
+ module CognitiveDistance::Structures
2
+ # Stupid simple class to ensure that when we use object identity
3
+ # comparisons, we always get false with this.
4
+ class MissingCallContext
5
+ def equal? anything
6
+ false
7
+ end
8
+ end
9
+ end
10
+
@@ -0,0 +1,32 @@
1
+ module CognitiveDistance
2
+ class Tracer
3
+ def initialize measured
4
+ @traced = measured
5
+ end
6
+
7
+ def trace *args, &block
8
+ __with_kernel_trace__ do
9
+ @traced.__send__ *args, &block
10
+ end
11
+ end
12
+
13
+ private
14
+ def __with_kernel_trace__
15
+ tree = CognitiveDistance::Structures::CallTree.new
16
+ # Ruby, I love you. Even if exceptions are raised, or catch/throw is
17
+ # employed, you still give me a 'return' event as the stack unwinds.
18
+ # *hug*
19
+ Kernel.set_trace_func lambda { |ev, fn, no, meth, bind, klass|
20
+ case ev
21
+ when 'call'
22
+ tree.called klass, meth, fn, no, bind
23
+ when 'return'
24
+ tree.returned klass, meth, fn, no, bind
25
+ end
26
+ }
27
+ yield rescue nil
28
+ ::Kernel.set_trace_func nil
29
+ tree
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,5 @@
1
+ require 'cognitive_distance/transforms/call_tree_to_module_boundary_graph'
2
+
3
+ module CognitiveDistance::Transforms
4
+ end
5
+
@@ -0,0 +1,30 @@
1
+ module CognitiveDistance::Transforms
2
+ class CallTreeToModuleBoundaryGraph
3
+ def self.transform tree
4
+ new.transform(tree)
5
+ end
6
+
7
+ def transform tree
8
+ CognitiveDistance::Structures::Graph.new.tap do |graph|
9
+ link_nodes graph, tree.to_a
10
+ end
11
+ end
12
+
13
+ private
14
+ def link_nodes graph, node_arr, prefix=""
15
+ node_arr.each do |parent, children|
16
+ link_boundary_crossings graph, parent, children, "#{prefix}\t"
17
+ end
18
+ end
19
+
20
+ def link_boundary_crossings graph, parent, children, prefix=""
21
+ children.each do |(child, _)|
22
+ if !parent.context.equal?(child.context)
23
+ graph.link parent, child
24
+ end
25
+ end
26
+ link_nodes graph, children, prefix
27
+ end
28
+ end
29
+ end
30
+
@@ -0,0 +1,3 @@
1
+ module CognitiveDistance
2
+ VERSION = "0.0.1.pre"
3
+ end
@@ -0,0 +1,52 @@
1
+ require File.expand_path('../../../spec_helper', __FILE__)
2
+
3
+ describe CognitiveDistance::Measurements::DistinctModuleHops do
4
+ def get_meta_class klass
5
+ klass.class_eval "class << self; self; end"
6
+ end
7
+
8
+ def make_node ctx
9
+ mock = CognitiveDistance::Structures::CallNode.new
10
+ mock.trace_class = ctx
11
+ def mock.context
12
+ trace_class
13
+ end
14
+ mock
15
+ end
16
+
17
+ before do
18
+ graph = @graph = CognitiveDistance::Structures::Graph.new
19
+ # Stub out transform
20
+ klass = get_meta_class(CognitiveDistance::Transforms::CallTreeToModuleBoundaryGraph)
21
+ klass.send(:alias_method, :transform_orig, :transform)
22
+ klass.send(:define_method, :transform) { |*_| graph }
23
+ @measurement = CognitiveDistance::Measurements::DistinctModuleHops.new
24
+ n1, n2, n3 = 3.times.map { |i| make_node "module #{i}" }
25
+ n4 = make_node("module 0") # A different object, but the same context
26
+ @graph.link n1, n2, n3
27
+ @graph.link n4, n2
28
+ end
29
+
30
+ after do
31
+ klass = get_meta_class(CognitiveDistance::Transforms::CallTreeToModuleBoundaryGraph)
32
+ klass.send(:remove_method, :transform)
33
+ klass.send(:alias_method, :transform, :transform_orig)
34
+ klass.send(:remove_method, :transform_orig)
35
+ end
36
+
37
+ it "only counts distinct module boundary crossings" do
38
+ tree = MiniTest::Mock.new
39
+ @measurement.measure(tree).must_equal 2
40
+ end
41
+
42
+ it "measures an object trace directly" do
43
+ # Even though this does create a real trace, we're still intercepting
44
+ # the transformation, so we can honey badger away.
45
+ CognitiveDistance::Measurements::DistinctModuleHops.measure("test", :length).must_equal 2
46
+ end
47
+
48
+ it "registers its measurement" do
49
+ CognitiveDistance::Measurements.measure_distinct_module_hops("test", :length).must_equal 2
50
+ end
51
+ end
52
+
@@ -0,0 +1,44 @@
1
+ require File.expand_path('../../../spec_helper', __FILE__)
2
+
3
+ describe CognitiveDistance::Measurements::Measurement do
4
+ it "registers a named measurement" do
5
+ class Foobar
6
+ extend CognitiveDistance::Measurements::Measurement
7
+ register :lameness
8
+ end
9
+ CognitiveDistance::Measurements.must_respond_to :measure_lameness
10
+ end
11
+
12
+ it "performs the registered measurement" do
13
+ class Foobaz
14
+ extend CognitiveDistance::Measurements::Measurement
15
+ register :suckiness
16
+
17
+ class << self
18
+ attr_reader :received_args
19
+ def measure *args
20
+ @received_args = args
21
+ end
22
+ end
23
+ end
24
+ CognitiveDistance::Measurements.measure_suckiness('foo', 'bar', 'blarg')
25
+ Foobaz.received_args.must_equal ['foo', 'bar', 'blarg']
26
+ end
27
+
28
+ it "allows a measurement method to be defined" do
29
+ class Fooboss
30
+ extend CognitiveDistance::Measurements::Measurement
31
+ register :unfortunately, :most_unfortunate
32
+
33
+ class << self
34
+ attr_reader :received_args
35
+ def most_unfortunate *args
36
+ @received_args = args
37
+ end
38
+ end
39
+ end
40
+ CognitiveDistance::Measurements.measure_unfortunately(:x, :y, :z)
41
+ Fooboss.received_args.must_equal [:x, :y, :z]
42
+ end
43
+ end
44
+
@@ -0,0 +1,54 @@
1
+ require File.expand_path('../../../spec_helper', __FILE__)
2
+
3
+ describe CognitiveDistance::Measurements::ModuleHops do
4
+ def get_meta_class klass
5
+ klass.class_eval "class << self; self; end"
6
+ end
7
+
8
+ before do
9
+ graph = @graph = MiniTest::Mock.new
10
+ # Stub out transform
11
+ klass = get_meta_class(CognitiveDistance::Transforms::CallTreeToModuleBoundaryGraph)
12
+ klass.send(:alias_method, :transform_orig, :transform)
13
+ klass.send(:define_method, :transform) { |*_| graph }
14
+ @measurement = CognitiveDistance::Measurements::ModuleHops.new
15
+ end
16
+
17
+ after do
18
+ klass = get_meta_class(CognitiveDistance::Transforms::CallTreeToModuleBoundaryGraph)
19
+ klass.send(:remove_method, :transform)
20
+ klass.send(:alias_method, :transform, :transform_orig)
21
+ klass.send(:remove_method, :transform_orig)
22
+ end
23
+
24
+ it "only counts all module boundary crossings" do
25
+ tree = MiniTest::Mock.new
26
+ @graph.expect :edges, [
27
+ [ "module 1", "module 2" ],
28
+ [ "module 1", "module 4" ],
29
+ [ "module 1", "module 2" ]
30
+ ]
31
+ @measurement.measure(tree).must_equal 3
32
+ end
33
+
34
+ it "measures an object trace directly" do
35
+ @graph.expect :edges, [
36
+ [ "module 1", "module 2" ],
37
+ [ "module 1", "module 4" ],
38
+ [ "module 1", "module 2" ]
39
+ ]
40
+ # Even though this does create a real trace, we're still intercepting
41
+ # the transformation, so we can honey badger away.
42
+ CognitiveDistance::Measurements::ModuleHops.measure("test", :length).must_equal 3
43
+ end
44
+
45
+ it "registers its measurement" do
46
+ @graph.expect :edges, [
47
+ [ "module 1", "module 2" ],
48
+ [ "module 1", "module 4" ],
49
+ [ "module 1", "module 2" ]
50
+ ]
51
+ CognitiveDistance::Measurements.measure_module_hops("test", :length).must_equal 3
52
+ end
53
+ end
54
+