cognitive_distance 0.0.1.pre

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.
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
+