chaos_detector 0.4.9

Sign up to get free protection for your applications and to get access to all the features.
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,76 @@
1
+ module ChaosDetector
2
+ module GraphTheory
3
+ class Edge
4
+ attr_accessor :edge_type
5
+ attr_writer :graph_props
6
+ attr_accessor :src_node
7
+ attr_accessor :dep_node
8
+ attr_accessor :reduction
9
+
10
+ EDGE_TYPES = {
11
+ default: 0,
12
+ superclass: 1,
13
+ association: 2,
14
+ class_association: 3
15
+ }.freeze
16
+
17
+ def initialize(src_node, dep_node, edge_type: :default, reduction: nil)
18
+ raise ArgumentError, 'src_node is required ' unless src_node
19
+ raise ArgumentError, 'dep_node is required ' unless dep_node
20
+
21
+ @src_node = src_node
22
+ @dep_node = dep_node
23
+ @reduction = reduction
24
+ @edge_type = edge_type
25
+ @graph_props = {}
26
+ end
27
+
28
+ def edge_rank
29
+ EDGE_TYPES.fetch(@edge_type, 0)
30
+ end
31
+
32
+ def weight
33
+ @reduction&.reduction_sum || 1
34
+ end
35
+
36
+ def hash
37
+ [@src_node, @dep_node].hash
38
+ end
39
+
40
+ def eql?(other)
41
+ self == other
42
+ end
43
+
44
+ # Default behavior is accessor for @graph_props
45
+ def graph_props
46
+ @graph_props
47
+ end
48
+
49
+
50
+
51
+ def ==(other)
52
+ # puts "Checking src and dep"
53
+ src_node == other.src_node && dep_node == other.dep_node
54
+ end
55
+
56
+ def to_s
57
+ s = format('[%s] -> [%s]', src_node.title, dep_node.title)
58
+ s << "(#{reduction.reduction_sum})" if reduction&.reduction_sum.to_i > 1
59
+ s
60
+ end
61
+
62
+ # Mutate this Edge; combining attributes from other:
63
+ def merge!(other)
64
+ raise ArgumentError, ('Argument other should be Edge object (was %s)' % other.class) unless other.is_a?(Edge)
65
+
66
+ if EDGE_TYPES.dig(other.edge_type) > EDGE_TYPES.dig(edge_type)
67
+ @edge_type = other.edge_type
68
+ end
69
+
70
+ # puts("EDGE REDUCTION: #{@reduction.class} -- #{other.class} // #{other.reduction.class}")
71
+ @reduction = ChaosDetector::GraphTheory::Reduction.combine(@reduction, other.reduction)
72
+ self
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,144 @@
1
+ # Maintains all nodes and edges as stack calls are pushed and popped via Frames.
2
+ module ChaosDetector
3
+ module GraphTheory
4
+ class Graph
5
+ attr_reader :root_node
6
+ attr_reader :edges
7
+
8
+ def node_count
9
+ @nodes.length
10
+ end
11
+
12
+ def edge_count
13
+ @edges.length
14
+ end
15
+
16
+ def initialize(root_node:, nodes: nil, edges: nil)
17
+ # raise ArgumentError, 'Root node required.' unless root_node
18
+
19
+ @root_node = root_node
20
+ @nodes = nodes || []
21
+ @edges = edges || []
22
+ end
23
+
24
+ def nodes(include_root: true)
25
+ include_root ? @nodes : @nodes.reject(&:root?)
26
+ end
27
+
28
+ # Return a new Graph object that only includes the given nodes and matching edges:
29
+ # def arrange_with(nodes:, include_root: true)
30
+ # gnodes = nodes.map(&:clone)
31
+ # if include_root
32
+ # gnodes << root_node unless(gnodes.include?(root_node))
33
+ # else
34
+ # gnodes.delete(root_node)
35
+ # end
36
+ #
37
+ # gedges = edges.filter do |edge|
38
+ # gnodes.include?(edge.src_node) && gnodes.include?(edge.dep_node)
39
+ # end
40
+ #
41
+ # ChaosDetector::GraphTheory::Graph.new(
42
+ # root_node: root_node,
43
+ # nodes: gnodes,
44
+ # edges: gedges
45
+ # ).tap do |graph|
46
+ # graph.whack_node(root_node) unless include_root
47
+ # end
48
+ # end
49
+
50
+ # Remove node and src and dep edges:
51
+ def whack_node(node)
52
+ @edges.delete_if { |e| e.src_node == node || e.dep_node == node }
53
+ @nodes.delete(node)
54
+ end
55
+
56
+ def traversal
57
+ to_enum(:traverse).map(&:itself) # {|n| puts "TNode:#{n}"; n.label}
58
+ end
59
+
60
+ # Possibly useful:
61
+ # def traversal(loop_detector: nil)
62
+ # trace_nodes = []
63
+
64
+ # traverse do |node|
65
+ # if loop_detector.nil? || loop_detector.tolerates?(trace_nodes, node)
66
+ # trace_nodes << node
67
+ # end
68
+ # end
69
+
70
+ # trace_nodes
71
+ # end
72
+
73
+ ### Depth-first traversal
74
+ # Consumes each edge as it used
75
+ def traverse(origin_node: nil)
76
+ raise ArgumentError, 'traverse requires block' unless block_given?
77
+ edges = @edges
78
+ nodes_to_visit = [origin_node || root_node]
79
+ while nodes_to_visit.length > 0
80
+ node = nodes_to_visit.shift
81
+ yield(node)
82
+ out_edges, edges = edges.partition { |e| e.src_node == node }
83
+ child_nodes = out_edges.map(&:dep_node)
84
+ nodes_to_visit = child_nodes + nodes_to_visit
85
+ end
86
+ end
87
+
88
+ def children(node)
89
+ @edges.select { |e| e.src_node == node }.map(&:dep_node).inject do |child_nodes|
90
+ puts "Found children for #{node.label}: #{child_nodes}"
91
+ end
92
+ end
93
+
94
+ def node_for(obj)
95
+ raise ArgumentError, '#node_for requires obj' unless obj
96
+
97
+ node_n = @nodes.index(obj)
98
+ if node_n
99
+ @nodes[node_n]
100
+ else
101
+ yield.tap { |n| @nodes << n }
102
+ end
103
+ end
104
+
105
+ def edges_for_node(node)
106
+ edges.filter do |e|
107
+ e.src_node == node || e.dep_node == node
108
+ end
109
+ end
110
+
111
+ def edge_for_nodes(src_node, dep_node)
112
+ # puts "EEEDGE_FOR_NODES::: #{src_node.to_s} / #{dep_node.class.to_s}"
113
+ edge = edges.find do |e|
114
+ e.src_node == src_node && e.dep_node == dep_node
115
+ end
116
+
117
+ edges << edge = ChaosDetector::GraphTheory::Edge.new(src_node, dep_node) if edge.nil?
118
+
119
+ edge
120
+ end
121
+
122
+ def ==(other)
123
+ root_node == other.root_node &&
124
+ nodes == other.nodes &&
125
+ edges == other.edges
126
+ end
127
+
128
+ def to_s
129
+ format('Nodes: %d, Edges: %d', @nodes.length, @edges.length)
130
+ end
131
+
132
+ def inspect
133
+ buffy = []
134
+ buffy << "\tNodes (#{nodes.length})"
135
+ buffy.concat(nodes.map { |n| "\t\t#{n.title}"})
136
+
137
+ # buffy << "Edges (#{@edges.length})"
138
+ # buffy.concat(@edges.map {|e|"\t\t#{e.to_s}"})
139
+
140
+ buffy.join("\n")
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,32 @@
1
+ # Maintains all nodes and edges as stack calls are pushed and popped via Frames.
2
+ module ChaosDetector
3
+ module GraphTheory
4
+ class LoopDetector
5
+ def initialize(detection: :simple, lookback: 0, tolerance: 0, grace_period: 0)
6
+ @detection = detection
7
+ @lookback = lookback
8
+ @tolerance = tolerance
9
+ @grace_period = grace_period
10
+ end
11
+
12
+ def tolerates?(nodes, node)
13
+ return true if (nodes.length <= @grace_period)
14
+ # return false if (lookback.zero? && tolerance.zero? && nodes.include?(node))
15
+
16
+ # TODO: lookback
17
+ nodes.count(node) > tolerance
18
+
19
+ end
20
+
21
+ private
22
+ def form_lookback
23
+ end
24
+
25
+ class << self
26
+ def simple
27
+ @simple ||= LoopDetector.new
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,70 @@
1
+ require 'chaos_detector/utils/str_util'
2
+ require 'chaos_detector/chaos_utils'
3
+
4
+ module ChaosDetector
5
+ module GraphTheory
6
+ class Node
7
+ ROOT_NODE_NAME = 'ROOT'.freeze
8
+
9
+ attr_writer :graph_props
10
+ attr_reader :is_root
11
+ attr_accessor :node_origin
12
+ attr_reader :reduction
13
+
14
+ def root?
15
+ !!is_root
16
+ end
17
+
18
+ def initialize(name: nil, root: false, node_origin: nil, reduction: nil)
19
+ raise ArgumentError, "Must have name or be root (name=#{name})" unless ChaosUtils.aught?(name) || root
20
+
21
+ @is_root = root
22
+ @name = @is_root ? ROOT_NODE_NAME : name
23
+ @node_origin = node_origin
24
+ @reduction = reduction
25
+ @graph_props = {}
26
+ end
27
+
28
+ def ==(other)
29
+ name == other.name &&
30
+ is_root == other.is_root
31
+ end
32
+
33
+ # Should be a reusable unique hash key for node:
34
+ def to_k
35
+ ChaosDetector::Utils::StrUtil.snakeize(name)
36
+ end
37
+
38
+ def to_s(_scope=nil)
39
+ name
40
+ end
41
+
42
+ def name
43
+ @is_root ? ROOT_NODE_NAME : @name
44
+ end
45
+
46
+ def title
47
+ name
48
+ end
49
+
50
+ def subtitle
51
+ nil
52
+ end
53
+
54
+ def root?
55
+ !!is_root
56
+ end
57
+
58
+ # Default behavior is accessor for @graph_props
59
+ def graph_props
60
+ @graph_props
61
+ end
62
+
63
+ # Mutate this Edge; combining attributes from other:
64
+ def merge!(other)
65
+ @reduction = ChaosDetector::GraphTheory::Reduction.combine(@reduction, other.reduction)
66
+ self
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,68 @@
1
+ require 'forwardable'
2
+
3
+ module ChaosDetector
4
+ module GraphTheory
5
+ class NodeMetrics
6
+ GRAPH_PROPERTIES = %i[ total_couplings afference efference instability ].freeze
7
+
8
+ extend Forwardable
9
+ attr_accessor :afference
10
+ attr_accessor :efference
11
+ attr_accessor :terminal_routes
12
+ attr_accessor :circular_routes
13
+ attr_reader :node
14
+
15
+ def_delegators :@node, :title, :subtitle
16
+
17
+ def reduction_sum
18
+ @node&.reduction&.reduction_sum || 1
19
+ end
20
+
21
+ def reduction_count
22
+ @node&.reduction&.reduction_count || 1
23
+ end
24
+
25
+ def initialize(node, afference: 0, efference: 0, terminal_routes:, circular_routes:)
26
+ raise ArgumentError("node is required") if node.nil?
27
+
28
+ @node = node
29
+ @afference = afference
30
+ @efference = efference
31
+ @terminal_routes = terminal_routes || []
32
+ @circular_routes = circular_routes || []
33
+ end
34
+
35
+ # https://en.wikipedia.org/wiki/Software_package_metrics
36
+ # I = Ce / (Ce + Ca).
37
+ # I = efference / (total couplings)
38
+ # Value from 0.0 to 1.0
39
+ # I = 0.0 is maximally stable while
40
+ # I = 1.0 is maximally unstable.
41
+ def instability
42
+ cT = total_couplings.to_f
43
+ (cT.zero?) ? 0.0 : @efference / cT
44
+ end
45
+
46
+ def total_couplings
47
+ @afference + @efference
48
+ end
49
+
50
+ def summary
51
+ 'I = Ce / (Ce + Ca)'
52
+ end
53
+
54
+ def to_h
55
+ {
56
+ afference: afference,
57
+ efference: efference,
58
+ instability: instability.round(2),
59
+ total_couplings: total_couplings
60
+ }
61
+ end
62
+
63
+ def to_s
64
+ "Ce: #{@efference}, Ca: #{@afference}, I: #{instability}"
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,40 @@
1
+ # Trackking of reduction(merging/removing dups)
2
+ require 'set'
3
+ module ChaosDetector
4
+ module GraphTheory
5
+ class Reduction
6
+ attr_reader :reduction_count
7
+ attr_reader :reduction_sum
8
+
9
+ def initialize(reduction_count: 1, reduction_sum: 1)
10
+ @reduction_count = reduction_count
11
+ @reduction_sum = reduction_sum
12
+ end
13
+
14
+ def merge!(other)
15
+ @reduction_sum += (other&.reduction_count || 1)
16
+ @reduction_count += 1 #(other&.reduction_count || 1)
17
+ self
18
+ end
19
+
20
+ def to_s
21
+ 'Reduction (count/sum)=(%d, %d)' % [reduction_count, reduction_sum]
22
+ end
23
+
24
+ class << self
25
+ def combine(primary, secondary)
26
+ raise ArgumentError, ('Argument #primary should be Reduction object (was %s)' % primary.class) unless primary.is_a?(ChaosDetector::GraphTheory::Reduction)
27
+ # raise ArgumentError, ('Argument #secondary should be Reduction object (was %s)' % secondary.class) unless secondary.is_a?(ChaosDetector::GraphTheory::Reduction)
28
+
29
+ combined = primary ? primary.clone(freeze: false) : ChaosDetector::GraphTheory::Reduction.new
30
+ combined.merge!(secondary)
31
+ end
32
+
33
+ def combine_all(reductions)
34
+ red_sum = reductions.reduce(0) { |tally, r| tally + (r ? r.reduction_count : 1) }
35
+ Reduction.new(reduction_count: reductions.count, reduction_sum: red_sum)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,396 @@
1
+ require 'ruby-graphviz'
2
+ require 'chaos_detector/utils/str_util'
3
+ require 'chaos_detector/chaos_utils'
4
+
5
+ module ChaosDetector
6
+ module Graphing
7
+ class DirectedGraphs
8
+ attr_reader :root_graph
9
+ attr_reader :node_hash
10
+ attr_reader :cluster_node_hash
11
+ attr_reader :render_folder
12
+ attr_reader :rendered_path
13
+ attr_reader :edges
14
+
15
+ BR = '<BR/>'
16
+ LF = "\r"
17
+ EDGE_MIN = 0.75
18
+ EDGE_BASELINE = 10.5
19
+
20
+ CLR_BLACK = 'black'.freeze
21
+ CLR_DARKRED = 'red4'.freeze
22
+ CLR_DARKGREEN = 'darkgreen'.freeze
23
+ CLR_BRIGHTGREEN = 'yellowgreen'.freeze
24
+ CLR_CYAN = 'cyan'.freeze
25
+ CLR_GREY = 'snow3'.freeze
26
+ CLR_ORANGE = 'orange'.freeze
27
+ CLR_NICEGREY = 'snow4'.freeze
28
+ CLR_PALEGREEN = 'palegreen'.freeze
29
+ CLR_PINK = 'deeppink1'.freeze
30
+ CLR_PURPLE = '#662D91'.freeze
31
+ CLR_SLATE = '#778899'.freeze
32
+ CLR_WHITE = 'white'.freeze
33
+ CLR_BLUE = 'darkblue'.freeze
34
+
35
+ GRAPH_ATTRS = {
36
+ type: :digraph,
37
+ bgcolor: CLR_SLATE,
38
+ center: 'false',
39
+ clusterrank: 'local',
40
+ color: CLR_WHITE,
41
+ compound: 'true',
42
+ # concentrate: 'true',
43
+ # engine: 'dot',
44
+ fontcolor: CLR_WHITE,
45
+ fontname: 'Georgia',
46
+ fontsize: '48',
47
+ labelloc: 't',
48
+ labeljust: 'l',
49
+ # mindist: '0.5',
50
+ # nojustify: 'true',
51
+ pad: '0.5',
52
+ # pack: 'true',
53
+ # packmode: 'graph',
54
+ pencolor: CLR_WHITE,
55
+ # outputorder: 'edgesfirst',
56
+ # ordering: 'out',
57
+ outputorder: 'nodesfirst',
58
+ nodesep: '0.5',
59
+ # sep: '0.5',
60
+ newrank: 'false',
61
+ rankdir: 'LR',
62
+ ranksep: '1.5',
63
+ # ranksep: 'equally',
64
+ ratio: 'auto',
65
+ # size: '50',
66
+ # page: '50',
67
+ size: '34,44',
68
+ # splines: 'spline',
69
+ # strict: 'true'
70
+ }.freeze
71
+
72
+ SUBDOMAIN_ATTRS = {
73
+ pencolor: CLR_ORANGE,
74
+ bgcolor: CLR_NICEGREY,
75
+ fillcolor: CLR_NICEGREY,
76
+ fontsize: '24',
77
+ rank: 'same',
78
+ fontname: 'Verdana',
79
+ labelloc: 't',
80
+ margin: '32,32',
81
+ # pencolor: CLR_GREY,
82
+ penwidth: '2',
83
+ style: 'rounded'
84
+ }.freeze
85
+
86
+ NODE_ATTRS = {
87
+ color: CLR_WHITE,
88
+ fontname: 'Verdana',
89
+ fontsize: '12',
90
+ # fixedsize: 'shape',
91
+ # height: '2.0',
92
+ # width: '2.0',
93
+ # fillcolor: CLR_WHITE,
94
+ fontcolor: CLR_WHITE,
95
+ margin: '0.25, 0.125',
96
+ shape: 'egg',
97
+ }.freeze
98
+
99
+ EDGE_ATTRS = {
100
+ color:CLR_WHITE,
101
+ constraint: 'true',
102
+ dir:'forward',
103
+ fontname:'Verdana',
104
+ fontcolor:CLR_ORANGE,
105
+ fontsize:'16',
106
+ # minlen: '3.0',
107
+ style:'solid',
108
+ penwidth:'1.0',
109
+ }.freeze
110
+
111
+ STUB_NODE_ATTRS = {
112
+ fixedsize: 'true',
113
+ size: '0.5, 0.5',
114
+ style: 'invis',
115
+ }
116
+
117
+ # Status messages:
118
+ PRE_STATUS_MSG = %(
119
+ Will update %<count>d record types from [%<from_type>s] to [%<to_type>s]'
120
+ ).freeze
121
+
122
+ TBL_HTML = <<~HTML
123
+ <TABLE BORDER='0' CELLBORDER='1' CELLSPACING='0' CELLPADDING='4'>
124
+ %s
125
+ </TABLE>
126
+ HTML
127
+
128
+ TBL_ROW_HTML = %(<TR BGCOLOR="%<color>s">%<cells>s</TR>)
129
+ TBL_CELL_HTML = %(<TD>%s</TD>)
130
+ BOLD_HTML = %(<BOLD>%s</BOLD>)
131
+
132
+ # TODO: integrate options as needed:
133
+ def initialize(render_folder: nil)
134
+ @root_graph = nil
135
+ @node_hash = {}
136
+ @cluster_node_hash = {}
137
+ @edges = Set.new
138
+ @render_folder = render_folder || 'render'
139
+ end
140
+
141
+ def create_directed_graph(title, graph_attrs: nil)
142
+ @title = title
143
+
144
+ @node_hash.clear
145
+ @cluster_node_hash.clear
146
+ @cluster_node_hash.clear
147
+
148
+ lbl = title_html(@title, subtitle: graph_attrs&.dig(:subtitle))
149
+
150
+ attrs = {
151
+ label: "<#{lbl}>",
152
+ **GRAPH_ATTRS,
153
+ }
154
+ attrs.merge(graph_attrs) if graph_attrs&.any?
155
+ attrs.delete(:subtitle)
156
+
157
+ @root_graph = GraphViz.digraph(:G, attrs)
158
+ end
159
+
160
+ def node_label(node, metrics_table: false)
161
+ if metrics_table
162
+ tbl_hash = {title: node.title, subtitle: node.subtitle, **node.graph_props}
163
+ html = html_tbl_from(hash: tbl_hash) do |k, v|
164
+ if k==:title
165
+ [in_font(k, font_size: 24), in_font(v, font_size: 16)]#[BOLD_HTML % k, BOLD_HTML % v]
166
+ else
167
+ [k, v]
168
+ end
169
+ end
170
+ else
171
+ html = title_html(node.title, subtitle: node.subtitle)
172
+ end
173
+ html.strip!
174
+ # puts '_' * 50
175
+ # puts "html: #{html}"
176
+ '<%s>' % html
177
+ end
178
+
179
+ # HTML Label with subtitle:
180
+ def title_html(title, subtitle:nil, font_size:24, subtitle_fontsize:nil)
181
+ lbl_buf = [in_font(title, font_size: font_size)]
182
+
183
+ sub_fontsize = subtitle_fontsize || 3 * font_size / 4
184
+ if ChaosUtils.aught?(subtitle)
185
+ lbl_buf << in_font(subtitle, font_size: sub_fontsize)
186
+ end
187
+
188
+ # Fake out some padding:
189
+ lbl_buf << in_font(' ', font_size: sub_fontsize)
190
+
191
+ lbl_buf.join(BR)
192
+ end
193
+
194
+ def in_font(str, font_size:12)
195
+ "<FONT POINT-SIZE='#{font_size}'>#{str}</FONT>"
196
+ end
197
+
198
+ def html_tbl_from(hash:)
199
+ trs = hash.map.with_index do |h, n|
200
+ k, v = h
201
+ key_content, val_content = yield(k, v) if block_given?
202
+ key_td = TBL_CELL_HTML % (key_content || k)
203
+ val_td = TBL_CELL_HTML % (val_content || v)
204
+ td_html = [key_td, val_td].join
205
+ html = format(TBL_ROW_HTML, {
206
+ color: n.even? ? 'blue' : 'white',
207
+ cells: td_html.strip
208
+ })
209
+ html.strip
210
+ end
211
+
212
+ TBL_HTML % trs.join().strip
213
+ end
214
+
215
+ def assert_graph_state
216
+ raise '@root_graph is not set yet. Call create_directed_graph.' unless @root_graph
217
+ end
218
+
219
+ # Add node to given parent_node, assuming parent_node is a subgraph
220
+ def add_node_to_parent(node, parent_node:, as_cluster: false, metrics_table:false)
221
+ assert_graph_state
222
+ raise 'node is required' unless node
223
+
224
+ parent_graph = if parent_node
225
+ _clust, p_graph = find_graph_node(parent_node)
226
+ raise "Couldn't find parent node: #{parent_node}" unless p_graph
227
+ p_graph
228
+ else
229
+ @root_graph
230
+ end
231
+
232
+ add_node_to_graph(node, graph: parent_graph, as_cluster: as_cluster, metrics_table: metrics_table)
233
+ end
234
+
235
+ def add_node_to_graph(node, graph: nil, as_cluster: false, metrics_table:false)
236
+ assert_graph_state
237
+ raise 'node is required' unless node
238
+
239
+ parent_graph = graph || @root_graph
240
+ key = node.to_k
241
+
242
+ attrs = { label: node_label(node, metrics_table: metrics_table) }
243
+
244
+ if as_cluster
245
+ # tab good shape
246
+ subgraph_name = "cluster_#{key}"
247
+ attrs.merge!(SUBDOMAIN_ATTRS)
248
+ # attrs = {}.merge(SUBDOMAIN_ ATTRS)
249
+ @cluster_node_hash[key] = parent_graph.add_graph(subgraph_name, attrs)
250
+
251
+ @cluster_node_hash[key].add_nodes(node_key(node, cluster: :stub), STUB_NODE_ATTRS)
252
+
253
+ # @cluster_node_hash[key].attrs(attrs)
254
+ # puts ("attrs: #{key}: #{attrs} / #{@cluster_node_hash.length}")
255
+ else
256
+ attrs = attrs.merge!(NODE_ATTRS)
257
+ @node_hash[key] = parent_graph.add_nodes(key, attrs)
258
+ end
259
+ end
260
+
261
+ def append_nodes(nodes, as_cluster: false, metrics_table: false)
262
+ assert_graph_state
263
+ return unless nodes
264
+ # raise 'node is required' unless nodes
265
+
266
+ nodes.each do |node|
267
+ parent_node = block_given? ? yield(node) : nil
268
+ # puts "gotit #{parent_node}" if parent_node
269
+ add_node_to_parent(node, parent_node: parent_node, as_cluster: as_cluster, metrics_table: metrics_table)
270
+ end
271
+ end
272
+
273
+ def node_key(node, cluster: false)
274
+ if cluster==:stub
275
+ "cluster_stub_#{node.to_k}"
276
+ elsif !!cluster
277
+ "cluster_#{node.to_k}"
278
+ else
279
+ node.to_k
280
+ end
281
+ end
282
+
283
+ def add_edges(edges, calc_weight:true, node_safe:true)
284
+ assert_graph_state
285
+
286
+ # @node_hash.each do |k, v|
287
+ # log("NODE_HASH: Has value for #{ChaosUtils.decorate(k)} => #{ChaosUtils.decorate(v)}")
288
+ # end
289
+
290
+ weight_max = edges.map(&:weight).max
291
+
292
+ edges.each do |edge|
293
+ src_clust, src = find_graph_node(edge.src_node)
294
+ dep_clust, dep = find_graph_node(edge.dep_node)
295
+ next unless !node_safe || (src && dep)
296
+
297
+ @edges << [src, dep]
298
+ edge_attrs = build_edge_attrs(edge, calc_weight: calc_weight, max_weight: weight_max, src_clust: src_clust, dep_clust: dep_clust)
299
+
300
+ @root_graph.add_edges(
301
+ node_key(edge.src_node, cluster: src_clust ? :stub : false),
302
+ node_key(edge.dep_node, cluster: dep_clust ? :stub : false),
303
+ edge_attrs
304
+ ) # , {label: e.reduce_sum, penwidth: weight})
305
+ end
306
+ end
307
+
308
+ def render_graph
309
+ assert_graph_state
310
+
311
+ filename = "#{@title}.png"
312
+ @rendered_path = File.join(@render_folder, filename).to_s
313
+
314
+ log("Rendering graph to to #{@rendered_path}")
315
+ ChaosDetector::Utils::FSUtil.ensure_paths_to_file(@rendered_path)
316
+ @root_graph.output(png: @rendered_path)
317
+ self
318
+ end
319
+
320
+ private
321
+
322
+ def build_edge_attrs(edge, calc_weight: true, max_weight: nil, src_clust: nil, dep_clust: nil)
323
+ edge_attrs = EDGE_ATTRS.dup
324
+
325
+ # Edge attaches to cluster if possible:
326
+ edge_attrs[:ltail] = node_key(edge.src_node, cluster: true) if src_clust
327
+ edge_attrs[:lhead] = node_key(edge.dep_node, cluster: true) if dep_clust
328
+
329
+ # Proportional edge weight:
330
+ if calc_weight && max_weight
331
+ edge_attrs.merge!(
332
+ label: edge.weight,
333
+ penwidth: edge_weight(edge.weight / max_weight)
334
+ )
335
+ end
336
+
337
+ # Intra-domain:
338
+ if edge.src_node.domain_name == edge.dep_node.domain_name
339
+ edge_attrs.merge!(
340
+ style: 'dotted',
341
+ color: CLR_ORANGE,
342
+ constraint: 'true',
343
+ )
344
+ end
345
+
346
+ # Props for edge_type:
347
+ edge_attrs.merge!(
348
+ case edge.edge_type
349
+ when :superclass
350
+ {
351
+ arrowhead: 'empty',
352
+ arrowsize: 1.0,
353
+ color: CLR_BLUE
354
+ }
355
+ when :association
356
+ {
357
+ arrowhead: 'diamond',
358
+ arrowsize: 1.0,
359
+ color: CLR_ORANGE
360
+ }
361
+ when :class_association
362
+ {
363
+ arrowhead: 'diamond',
364
+ arrowsize: 1.0,
365
+ color: CLR_PINK
366
+ }
367
+ else
368
+ {
369
+ arrowhead: 'open',
370
+ arrowsize: 1.0
371
+ }
372
+ end
373
+ )
374
+ end
375
+
376
+ def find_graph_node(node)
377
+ assert_graph_state
378
+ # log("NODE_HASH: LOOKING UP #{ChaosUtils.decorate(node)}")
379
+ cnode = @cluster_node_hash[node.to_k]
380
+ if cnode
381
+ [true, cnode]
382
+ else
383
+ [false, @node_hash[node.to_k]]
384
+ end
385
+ end
386
+
387
+ def edge_weight(n, edge_min: EDGE_MIN, edge_baseline: EDGE_BASELINE)
388
+ edge_min + n * edge_baseline
389
+ end
390
+
391
+ def log(msg, **opts)
392
+ ChaosUtils.log_msg(msg, subject: 'DGraphDiagram', **opts)
393
+ end
394
+ end
395
+ end
396
+ end