chaos_detector 0.4.9

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 (36) hide show
  1. checksums.yaml +7 -0
  2. data/bin/detect_chaos +31 -0
  3. data/lib/chaos_detector.rb +22 -0
  4. data/lib/chaos_detector/chaos_graphs/chaos_edge.rb +32 -0
  5. data/lib/chaos_detector/chaos_graphs/chaos_graph.rb +389 -0
  6. data/lib/chaos_detector/chaos_graphs/domain_metrics.rb +19 -0
  7. data/lib/chaos_detector/chaos_graphs/domain_node.rb +57 -0
  8. data/lib/chaos_detector/chaos_graphs/function_node.rb +112 -0
  9. data/lib/chaos_detector/chaos_graphs/module_node.rb +86 -0
  10. data/lib/chaos_detector/chaos_utils.rb +57 -0
  11. data/lib/chaos_detector/graph_theory/appraiser.rb +162 -0
  12. data/lib/chaos_detector/graph_theory/edge.rb +76 -0
  13. data/lib/chaos_detector/graph_theory/graph.rb +144 -0
  14. data/lib/chaos_detector/graph_theory/loop_detector.rb +32 -0
  15. data/lib/chaos_detector/graph_theory/node.rb +70 -0
  16. data/lib/chaos_detector/graph_theory/node_metrics.rb +68 -0
  17. data/lib/chaos_detector/graph_theory/reduction.rb +40 -0
  18. data/lib/chaos_detector/graphing/directed_graphs.rb +396 -0
  19. data/lib/chaos_detector/graphing/graphs.rb +129 -0
  20. data/lib/chaos_detector/graphing/matrix_graphs.rb +101 -0
  21. data/lib/chaos_detector/navigator.rb +237 -0
  22. data/lib/chaos_detector/options.rb +51 -0
  23. data/lib/chaos_detector/stacker/comp_info.rb +42 -0
  24. data/lib/chaos_detector/stacker/fn_info.rb +44 -0
  25. data/lib/chaos_detector/stacker/frame.rb +34 -0
  26. data/lib/chaos_detector/stacker/frame_stack.rb +63 -0
  27. data/lib/chaos_detector/stacker/mod_info.rb +24 -0
  28. data/lib/chaos_detector/tracker.rb +276 -0
  29. data/lib/chaos_detector/utils/core_util.rb +117 -0
  30. data/lib/chaos_detector/utils/fs_util.rb +49 -0
  31. data/lib/chaos_detector/utils/lerp_util.rb +20 -0
  32. data/lib/chaos_detector/utils/log_util.rb +45 -0
  33. data/lib/chaos_detector/utils/str_util.rb +90 -0
  34. data/lib/chaos_detector/utils/tensor_util.rb +21 -0
  35. data/lib/chaos_detector/walkman.rb +214 -0
  36. metadata +147 -0
@@ -0,0 +1,19 @@
1
+ require 'chaos_detector/graph_theory/graph_theory'
2
+
3
+ # Dependency Count total/normalized
4
+ #
5
+ module ChaosDetector
6
+ module ChaosGraphs
7
+ class DomainMetrics
8
+ attr_reader :dep_count
9
+ attr_reader :dep_count_norm
10
+
11
+ def initialize
12
+ @dep_count = 0
13
+ @dep_count_norm = 0.0
14
+ end
15
+
16
+ def to_s; end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,57 @@
1
+ require 'chaos_detector/graph_theory/node'
2
+ # Domain node
3
+ module ChaosDetector
4
+ module ChaosGraphs
5
+ class DomainNode < ChaosDetector::GraphTheory::Node
6
+ alias domain_name name
7
+
8
+ def initialize(domain_name: nil, node_origin: nil, is_root: false, reduction: nil)
9
+ super(name: domain_name, root: is_root, node_origin: node_origin, reduction: reduction)
10
+ end
11
+
12
+ def hash
13
+ domain_name.hash
14
+ end
15
+
16
+ def eql?(other)
17
+ self == other
18
+ end
19
+
20
+ def ==(other)
21
+ domain_name&.to_s == other&.domain_name&.to_s
22
+ end
23
+
24
+ def title
25
+ super
26
+ end
27
+
28
+ def subtitle
29
+ root? ? 'Root Node' : ''
30
+ end
31
+
32
+ def graph_props
33
+ props = super
34
+ if reduction
35
+ props.merge!(
36
+ cardinality_modules: reduction.reduction_count,
37
+ cardinality_functions: reduction.reduction_sum
38
+ )
39
+ end
40
+ super.merge(props)
41
+ end
42
+
43
+ # Must be name/domain_name for comparisons:
44
+ def to_s
45
+ domain_name
46
+ end
47
+
48
+ class << self
49
+ attr_reader :root_node
50
+ def root_node(force_new: false)
51
+ @root_node = new(is_root: true) if force_new || @root_node.nil?
52
+ @root_node
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,112 @@
1
+ require 'chaos_detector/stacker/fn_info'
2
+ require 'chaos_detector/stacker/mod_info'
3
+ require 'forwardable'
4
+ require 'chaos_detector/graph_theory/edge'
5
+ require 'chaos_detector/graph_theory/node'
6
+
7
+ require 'chaos_detector/chaos_utils'
8
+
9
+ module ChaosDetector
10
+ module ChaosGraphs
11
+ class FunctionNode < ChaosDetector::GraphTheory::Node
12
+ extend Forwardable
13
+ alias fn_name name
14
+ attr_accessor :domain_name
15
+ attr_accessor :fn_path
16
+ attr_accessor :fn_line
17
+ attr_accessor :fn_line_end
18
+
19
+ # Modules to which this Function Node is associated:
20
+ attr_reader :mod_infos
21
+ def_delegator :@mod_infos, :first, :mod_info_prime
22
+
23
+ def initialize(
24
+ fn_name: nil,
25
+ fn_path: nil,
26
+ fn_line: nil,
27
+ domain_name: nil,
28
+ is_root: false,
29
+ mod_info: nil,
30
+ reduction: nil
31
+ )
32
+ super(name: fn_name, root: is_root, reduction: reduction)
33
+
34
+ @domain_name = domain_name&.to_s
35
+ @fn_path = fn_path
36
+ @fn_line = fn_line
37
+ @mod_infos = []
38
+
39
+ # Add module info, if supplied:
40
+ add_module(mod_info)
41
+ end
42
+
43
+ def add_module(mod_info)
44
+ @mod_infos << mod_info if mod_info
45
+ end
46
+
47
+ def add_module_attrs(mod_name:, mod_path:, mod_type:)
48
+ add_module(ChaosDetector::Stacker::ModInfo.new(mod_name: mod_name, mod_path: mod_path, mod_type: mod_type))
49
+ end
50
+
51
+ def hash
52
+ [fn_name, fn_path].hash
53
+ end
54
+
55
+ def eql?(other)
56
+ self == other
57
+ end
58
+
59
+ def ==(other)
60
+ ChaosDetector::Stacker::FnInfo.match?(self, other)
61
+ end
62
+
63
+ def domain_name
64
+ @domain_name
65
+ end
66
+
67
+ def to_s
68
+ ChaosUtils.decorate_tuple([domain_name, fn_name, super, short_path], clamp: :bracket)
69
+ end
70
+
71
+ def to_info
72
+ FnInfo.new(fn_name: fn_name, fn_line: fn_line, fn_path: fn_path)
73
+ end
74
+
75
+ def title
76
+ fn_name
77
+ end
78
+
79
+ def subtitle
80
+ '(%s)%s' % [domain_name, short_path]
81
+ end
82
+
83
+ def short_path
84
+ ChaosDetector::Utils::StrUtil.humanize_module(@fn_path, sep_token: '/')
85
+ end
86
+
87
+ class << self
88
+ attr_reader :root_node
89
+ def root_node(force_new: false)
90
+ @root_node = new(is_root: true) if force_new || @root_node.nil?
91
+ @root_node
92
+ end
93
+
94
+ def match?(obj1, obj2)
95
+ raise 'Domains differ, but fn_info is the same. Weird.' if \
96
+ obj1.fn_name == obj2.fn_name \
97
+ && obj1.fn_path == obj2.fn_path \
98
+ && obj1.domain_name != other.domain_name
99
+
100
+ fn_path == other.fn_path &&
101
+ (fn_name == other.fn_name || line_match?(other.fn_line, fn_line))
102
+ end
103
+
104
+ def line_match?(l1, l2)
105
+ return false if l1.nil? || l2.nil?
106
+
107
+ (l2 - l1).between?(0, 1)
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,86 @@
1
+ require 'chaos_detector/stacker/mod_info'
2
+ require 'chaos_detector/graph_theory/node'
3
+ require 'chaos_detector/utils/str_util'
4
+ require 'chaos_detector/chaos_utils'
5
+
6
+ module ChaosDetector
7
+ module ChaosGraphs
8
+ # Consider putting action/event in this class and naming it accordingly
9
+ class ModuleNode < ChaosDetector::GraphTheory::Node
10
+ attr_reader :mod_type # :unknown, :module, :class
11
+ attr_reader :mod_path # :unknown, :module, :class
12
+ attr_reader :domain_name
13
+
14
+ alias mod_name name
15
+
16
+ def initialize(mod_name: nil, mod_path: nil, is_root: false, node_origin: nil, domain_name: nil, mod_type: nil, reduction: nil)
17
+ super(name: mod_name, root: is_root, node_origin: node_origin, reduction: reduction)
18
+ @domain_name = domain_name&.to_s
19
+ @mod_path = mod_path
20
+ @mod_type = mod_type
21
+ end
22
+
23
+ def hash
24
+ [mod_name, mod_type, mod_path].hash
25
+ # [mod_name, mod_type, domain_name].hash
26
+ end
27
+
28
+ def eql?(other)
29
+ self == other
30
+ end
31
+
32
+ def ==(other)
33
+ # TODO? Checking domain name vs path name due to mixins/metacoding:
34
+ mod_name == other.mod_name &&
35
+ mod_type == other.mod_type &&
36
+ mod_path == other.mod_path
37
+ end
38
+
39
+ def title
40
+ mod_name
41
+ end
42
+
43
+ def subtitle
44
+ '%s[%s]' % [short_mod_type, domain_name]
45
+ end
46
+
47
+ def graph_props
48
+ props = super
49
+ if reduction
50
+ props.merge!(
51
+ cardinality_functions: reduction.reduction_count
52
+ )
53
+ end
54
+ super.merge(props)
55
+ end
56
+
57
+ def short_mod_type
58
+ mod_type && "(#{mod_type[0]})"
59
+ end
60
+
61
+ def short_path
62
+ ChaosDetector::Utils::StrUtil.humanize_module(@mod_path, sep_token: '/')
63
+ end
64
+
65
+ def to_info
66
+ ChaosDetector::Stacker::ModInfo.new(mod_name: mod_name, mod_path: mod_path, mod_type: mod_type)
67
+ end
68
+
69
+ def to_k
70
+ ChaosDetector::Utils::StrUtil.snakeize([domain_name, mod_name, @mod_type, @mod_path].compact.map(&:to_s))
71
+ end
72
+
73
+ def to_s
74
+ [super, domain_name, @mod_type, short_path].join(', ')
75
+ end
76
+
77
+ class << self
78
+ attr_reader :root_node
79
+ def root_node(force_new: false)
80
+ @root_node = new(is_root: true) if force_new || @root_node.nil?
81
+ @root_node
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,57 @@
1
+ require 'chaos_detector/utils/core_util'
2
+ require 'chaos_detector/utils/fs_util'
3
+ require 'chaos_detector/utils/str_util'
4
+ require 'chaos_detector/utils/lerp_util'
5
+ require 'chaos_detector/utils/log_util'
6
+
7
+ module ChaosUtils
8
+ class << self
9
+ def delerp(val, min:, max:)
10
+ ChaosDetector::Utils::LerpUtil.delerp(val, min: min, max: max)
11
+ end
12
+
13
+ def lerp(pct, min:, max:)
14
+ ChaosDetector::Utils::LerpUtil.lerp(val, min: min, max: max)
15
+ end
16
+
17
+ def log_msg(msg, **args)
18
+ ChaosDetector::Utils::LogUtil.log(msg, **args)
19
+ end
20
+
21
+ def decorate(text, **args)
22
+ ChaosDetector::Utils::StrUtil.decorate(text, **args)
23
+ end
24
+
25
+ def decorate_pair(src, dest, **args)
26
+ ChaosDetector::Utils::StrUtil.decorate_pair(src, dest, **args)
27
+ end
28
+
29
+ def decorate_tuple(tuple, **args)
30
+ ChaosDetector::Utils::StrUtil.decorate_tuple(tuple, **args)
31
+ end
32
+
33
+ def assert(expected_result=true, msg=nil, &block)
34
+ ChaosDetector::Utils::CoreUtil.assert(expected_result, msg, &block)
35
+ end
36
+
37
+ def aught?(obj)
38
+ ChaosDetector::Utils::CoreUtil.aught?(obj)
39
+ end
40
+
41
+ def naught?(obj)
42
+ ChaosDetector::Utils::CoreUtil.naught?(obj)
43
+ end
44
+
45
+ def rel_path(dir_path, from_path:)
46
+ ChaosDetector::Utils::FSUtil.rel_path(dir_path, from_path: from_path)
47
+ end
48
+
49
+ def squish(str)
50
+ ChaosDetector::Utils::StrUtil.squish(str)
51
+ end
52
+
53
+ def with(obj)
54
+ ChaosDetector::Utils::CoreUtil.with(obj) {yield obj}
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,162 @@
1
+ require 'matrix'
2
+ require 'chaos_detector/chaos_utils'
3
+ require_relative 'node_metrics'
4
+
5
+ module ChaosDetector
6
+ module GraphTheory
7
+ class Appraiser
8
+ attr_reader :cyclomatic_complexity
9
+ attr_reader :adjacency_matrix
10
+
11
+ def initialize(graph)
12
+ @graph = graph
13
+ @adjacency_matrix = nil
14
+ @cyclomatic_complexity = nil
15
+ @nodes_appraised = {}
16
+ end
17
+
18
+ def appraise(update_nodes:true)
19
+ log('Appraising nodes.')
20
+ @nodes_appraised = appraise_nodes!(update_nodes: update_nodes)
21
+
22
+ # TODO: Store adjacency (to each other node) as a node metric?
23
+ @adjacency_matrix = build_adjacency_matrix(@graph.nodes)
24
+
25
+ # log('Measuring cyclomatic complexity.')
26
+ # measure_cyclomatic_complexity
27
+
28
+ log("Performed appraisal: %s" % to_s)
29
+ end
30
+
31
+ def metrics_for(node:)
32
+ raise ArgumentError, 'Node is required' if node.nil?
33
+ raise ArgumentError, ('Node [%s] has no metrics' % node) if !@nodes_appraised&.include?(node)
34
+ log('has no metrics', object: node) if !@nodes_appraised&.include?(node)
35
+ @nodes_appraised[node]
36
+ end
37
+
38
+ def to_s
39
+ format('N: %d, E: %d', @graph.nodes.length, @graph.edges.length)
40
+ end
41
+
42
+ def report
43
+ buffy = [to_s]
44
+
45
+ # buffy << "Circular References #{@circular_paths.length} / #{@circular_paths.uniq.length}"
46
+ # buffy.concat(@circular_paths.map do |p|
47
+ # ' ' + p.map(&:title).join(' -> ')
48
+ # end)
49
+
50
+ # Gather nodes:
51
+ buffy << 'Nodes:'
52
+ buffy.concat(@nodes_appraised.map { |n, m| " (#{n.title})[#{n.subtitle}]: #{m}" })
53
+
54
+ buffy.join("\n")
55
+ end
56
+
57
+ # Returns Hash<Node, NodeMetrics>
58
+ # update_nodes: Updates all nodes' :graph_props to appraisal metrics hash:
59
+ def appraise_nodes!(update_nodes: true)
60
+ node_metrics = @graph.nodes.map do |node|
61
+ metrics = appraise_node(node)
62
+ [node, metrics]
63
+ end.to_h
64
+
65
+ if update_nodes
66
+ node_metrics.each do |node, metrics|
67
+ node.graph_props.merge!(metrics.to_h)
68
+ end
69
+ end
70
+
71
+ node_metrics
72
+ end
73
+
74
+ def build_adjacency_matrix(nodes)
75
+ matrix_dim = nodes.size
76
+ # nodes = @graph.nodes
77
+ Matrix.build(matrix_dim) do |row, col|
78
+ node_src = nodes[row]
79
+ node_dep = nodes[col]
80
+ # puts "Adjacency found for #{node_src}, #{node_dep}: #{edge&.reduction} / #{edge&.reduction&.reduction_sum.to_i}"
81
+ # puts "Adjacency found for #{row}:#{col} -> #{node_src}, #{node_dep}: #{adjacency?(node_src, node_dep)}"
82
+ adjacency?(node_src, node_dep)
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ # For each node, measure fan-in(Ca) and fan-out(Ce)
89
+ def appraise_node(node)
90
+ circular_routes, terminal_routes = appraise_node_routes(node)
91
+ ChaosDetector::GraphTheory::NodeMetrics.new(
92
+ node,
93
+ afference: @graph.edges.count { |e| e.dep_node == node },
94
+ efference: @graph.edges.count { |e| e.src_node == node },
95
+ circular_routes: circular_routes,
96
+ terminal_routes: terminal_routes
97
+ )
98
+ end
99
+
100
+ def log(msg, **opts)
101
+ ChaosUtils.log_msg(msg, subject: 'Appraiser', **opts)
102
+ end
103
+
104
+ def fan_out_edges(node)
105
+ @graph.edges.find_all { |e| e.src_node == node }
106
+ end
107
+
108
+ def fan_out_nodes(node)
109
+ @graph.edges.find_all { |e| e.src_node == node }.map(&:dep_node)
110
+ end
111
+
112
+ def fan_in_nodes(node)
113
+ @graph.edges.find_all { |e| e.dep_node == node }.map(&:src_node)
114
+ end
115
+
116
+ def appraise_node_routes(_node)
117
+ # Traverse starting at each node to see if
118
+ # and how many ways we come back to ourselves
119
+ terminal_routes = []
120
+ circular_routes = []
121
+
122
+ [terminal_routes, circular_routes]
123
+ end
124
+
125
+ def adjacency?(node_src, node_dest)
126
+ edge = @graph.edges.find{|e| e.src_node == node_src && e.dep_node == node_dest }
127
+ edge&.reduction&.reduction_sum.to_i
128
+ end
129
+
130
+ # Coupling: Each node couplet (Example for 100 nodes, we'd have 100 * 99 potential couplets)
131
+ # Capture how many other nodes depend upon both nodes in couplet [directly, indirectly]
132
+ # Capture how many other nodes from other domains depend upon both [directly, indirectly]
133
+ # TODO??
134
+ def node_matrix
135
+ node_matrix = Matrix.build(@graph.nodes.length) do |row, col|
136
+ end
137
+ node_matrix
138
+ end
139
+
140
+ def measure_cyclomatic_complexity
141
+ @circular_paths = []
142
+ @full_paths = []
143
+ traverse_nodes([@graph.root_node])
144
+ @path_count_uniq = @circular_paths.uniq.count + @full_paths.uniq.count
145
+ @cyclomatic_complexity = @graph.edges.count - @graph.nodes.count + (2 * @path_count_uniq)
146
+ end
147
+
148
+ # @return positive integer indicating distance in number of vertices
149
+ # from node_src to node_dep. If multiple routes, calculate shortest:
150
+ def node_distance(node_src, node_dep); end
151
+
152
+ def normalize(ary, property, norm_property)
153
+ vector = Vector.elements(ary.map { |obj| obj.send(property)})
154
+ vector = vector.normalize
155
+ ary.each_with_index do |obj, i|
156
+ obj.send("#{norm_property}=", vector[i])
157
+ end
158
+ ary
159
+ end
160
+ end
161
+ end
162
+ end