cognitive_distance 0.0.1.pre
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +19 -0
- data/.gitignore +10 -0
- data/.travis.yml +5 -0
- data/Gemfile +12 -0
- data/LICENSE.md +221 -0
- data/README.md +50 -0
- data/Rakefile +14 -0
- data/cognitive_distance.gemspec +23 -0
- data/lib/cognitive_distance.rb +23 -0
- data/lib/cognitive_distance/measurements.rb +16 -0
- data/lib/cognitive_distance/measurements/distinct_module_hops.rb +19 -0
- data/lib/cognitive_distance/measurements/measurement.rb +7 -0
- data/lib/cognitive_distance/measurements/module_hops.rb +17 -0
- data/lib/cognitive_distance/presenters.rb +2 -0
- data/lib/cognitive_distance/presenters/graph_to_dot.rb +11 -0
- data/lib/cognitive_distance/structures.rb +9 -0
- data/lib/cognitive_distance/structures/call_node.rb +74 -0
- data/lib/cognitive_distance/structures/call_node_root.rb +29 -0
- data/lib/cognitive_distance/structures/call_tree.rb +34 -0
- data/lib/cognitive_distance/structures/graph.rb +77 -0
- data/lib/cognitive_distance/structures/missing_call_context.rb +10 -0
- data/lib/cognitive_distance/tracer.rb +32 -0
- data/lib/cognitive_distance/transforms.rb +5 -0
- data/lib/cognitive_distance/transforms/call_tree_to_module_boundary_graph.rb +30 -0
- data/lib/cognitive_distance/version.rb +3 -0
- data/spec/cognitive_distance/measurements/distinct_module_hops_spec.rb +52 -0
- data/spec/cognitive_distance/measurements/measurement_spec.rb +44 -0
- data/spec/cognitive_distance/measurements/module_hops_spec.rb +54 -0
- data/spec/cognitive_distance/measurements_spec.rb +19 -0
- data/spec/cognitive_distance/presenters/graph_to_dot_spec.rb +26 -0
- data/spec/cognitive_distance/structures/call_node_root_spec.rb +31 -0
- data/spec/cognitive_distance/structures/call_node_spec.rb +91 -0
- data/spec/cognitive_distance/structures/call_tree_spec.rb +55 -0
- data/spec/cognitive_distance/structures/graph_spec.rb +142 -0
- data/spec/cognitive_distance/structures/missing_call_context_spec.rb +21 -0
- data/spec/cognitive_distance/tracer_spec.rb +57 -0
- data/spec/cognitive_distance/transforms/call_tree_to_module_boundary_graph_spec.rb +57 -0
- data/spec/spec_dummies.rb +60 -0
- data/spec/spec_helper.rb +12 -0
- metadata +111 -0
@@ -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,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,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,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
|
+
|