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