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.
- checksums.yaml +7 -0
- data/bin/detect_chaos +31 -0
- data/lib/chaos_detector.rb +22 -0
- data/lib/chaos_detector/chaos_graphs/chaos_edge.rb +32 -0
- data/lib/chaos_detector/chaos_graphs/chaos_graph.rb +389 -0
- data/lib/chaos_detector/chaos_graphs/domain_metrics.rb +19 -0
- data/lib/chaos_detector/chaos_graphs/domain_node.rb +57 -0
- data/lib/chaos_detector/chaos_graphs/function_node.rb +112 -0
- data/lib/chaos_detector/chaos_graphs/module_node.rb +86 -0
- data/lib/chaos_detector/chaos_utils.rb +57 -0
- data/lib/chaos_detector/graph_theory/appraiser.rb +162 -0
- data/lib/chaos_detector/graph_theory/edge.rb +76 -0
- data/lib/chaos_detector/graph_theory/graph.rb +144 -0
- data/lib/chaos_detector/graph_theory/loop_detector.rb +32 -0
- data/lib/chaos_detector/graph_theory/node.rb +70 -0
- data/lib/chaos_detector/graph_theory/node_metrics.rb +68 -0
- data/lib/chaos_detector/graph_theory/reduction.rb +40 -0
- data/lib/chaos_detector/graphing/directed_graphs.rb +396 -0
- data/lib/chaos_detector/graphing/graphs.rb +129 -0
- data/lib/chaos_detector/graphing/matrix_graphs.rb +101 -0
- data/lib/chaos_detector/navigator.rb +237 -0
- data/lib/chaos_detector/options.rb +51 -0
- data/lib/chaos_detector/stacker/comp_info.rb +42 -0
- data/lib/chaos_detector/stacker/fn_info.rb +44 -0
- data/lib/chaos_detector/stacker/frame.rb +34 -0
- data/lib/chaos_detector/stacker/frame_stack.rb +63 -0
- data/lib/chaos_detector/stacker/mod_info.rb +24 -0
- data/lib/chaos_detector/tracker.rb +276 -0
- data/lib/chaos_detector/utils/core_util.rb +117 -0
- data/lib/chaos_detector/utils/fs_util.rb +49 -0
- data/lib/chaos_detector/utils/lerp_util.rb +20 -0
- data/lib/chaos_detector/utils/log_util.rb +45 -0
- data/lib/chaos_detector/utils/str_util.rb +90 -0
- data/lib/chaos_detector/utils/tensor_util.rb +21 -0
- data/lib/chaos_detector/walkman.rb +214 -0
- 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
|