chaos_detector 0.4.9
Sign up to get free protection for your applications and to get access to all the features.
- 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
|