visualize_packs 0.3.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +36 -24
- data/bin/visualize_packs +39 -0
- data/lib/graph.dot.erb +162 -0
- data/lib/options.rb +33 -0
- data/lib/visualize_packs.rb +204 -20
- metadata +19 -93
- data/lib/visualize_packs/graph_interface.rb +0 -17
- data/lib/visualize_packs/legend.png +0 -0
- data/lib/visualize_packs/node_interface.rb +0 -29
- data/lib/visualize_packs/package_graph.rb +0 -54
- data/lib/visualize_packs/package_node.rb +0 -28
- data/lib/visualize_packs/package_relationships.rb +0 -170
- data/lib/visualize_packs/team_graph.rb +0 -56
- data/lib/visualize_packs/team_node.rb +0 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6f359f93291f20b1d7d77b62a32c52a7fa76baf86444201239f2b8e2bd629b83
|
4
|
+
data.tar.gz: e52a5b63f4ef80429727ed92d6e624329c6fe98904e0406b7b39b6f20a996121
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2ec569b998d850151acf257f1cff5baf31a6fa7f3453dc1505776d33027b71f26fcb369c2d1b05cfef6d8d468b47a5b826e78859d485169b3bdcc163a634335c
|
7
|
+
data.tar.gz: 692b0eba45954340cbc043efd5b3937009ff402549ae3557e9d0013e827962c289ca1b9dc00eab399d9b0627099db42276912e566dfbd5e978960442f9bef649
|
data/README.md
CHANGED
@@ -1,35 +1,47 @@
|
|
1
1
|
# visualize_packs
|
2
|
+
Visualize_packs helps you visualize the structure, intended and actual, of your package-based Ruby application.
|
2
3
|
|
3
|
-
This gem
|
4
|
+
This gem takes a minimal approach in that it only outputs a graph in the format of [graphviz](https://graphviz.org/)' [dot language](https://graphviz.org/doc/info/lang.html). Install graphviz and use one of the chains of commands from below to generate full or partial images of the graphs of your application.
|
4
5
|
|
5
|
-
|
6
|
+
## Visualize your entire application
|
7
|
+
```
|
8
|
+
bundle exec visualize_packs > packs.dot && dot packs.dot -Tpng -o packs.png && open packs.png
|
9
|
+
```
|
10
|
+
|
11
|
+
## packs.png for every package in your app
|
12
|
+
|
13
|
+
This will generate a local dependency diagram for every pack in your app
|
6
14
|
|
7
|
-
# CLI Usage
|
8
|
-
## bin/packs
|
9
|
-
For simpler use, add `bin/packs` via `use_packs` (https://github.com/rubyatscale/use_packs)
|
10
15
|
```
|
11
|
-
|
12
|
-
bin/packs visualize packs/a packs/b # subset of packs
|
13
|
-
bin/packs # enter interactive mode to select what packs to visualize
|
16
|
+
find . -iname 'package.yml' | sed 's/\/package.yml//g' | sed 's/\.\///' | xargs -I % sh -c "bundle exec visualize_packs --only=% > %/packs.dot && dot %/packs.dot -Tpng -o %/packs.png"
|
14
17
|
```
|
15
18
|
|
16
|
-
|
17
|
-
|
18
|
-
```ruby
|
19
|
-
# Select the packs you want to include
|
20
|
-
selected_packs = Packs.all
|
21
|
-
selected_packs = Packs.all.select{ |p| ['packs/my_pack_1', 'packs/my_pack_2'].include?(p.name) }
|
22
|
-
selected_packs = Packs.all.select{ |p| ['Team 1', 'Team 2'].include?(CodeOwnership.for_package(p)&.name) }
|
23
|
-
VisualizePacks.package_graph!(selected_packs)
|
19
|
+
If your app is large and has many packages and violations, the above graphs will likely be too big. Try this version to get only the edges to and from the focus package for each diagram:
|
20
|
+
|
24
21
|
```
|
22
|
+
find . -iname 'package.yml' | sed 's/\/package.yml//g' | sed 's/\.\///' | xargs -I % sh -c "bundle exec visualize_packs --only=% --only-edges-to-focus > %/packs.dot && dot %/packs.dot -Tpng -o %/packs.png"
|
23
|
+
```
|
24
|
+
|
25
25
|
|
26
|
-
##
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
selected_teams = CodeTeams.all.select{ |t| ['Team 1', 'Team 2'].include?(t.name) }
|
31
|
-
VisualizePacks.team_graph!(selected_teams)
|
26
|
+
## Get help
|
27
|
+
|
28
|
+
```
|
29
|
+
bundle exec visualize_packs --help
|
32
30
|
```
|
33
31
|
|
34
|
-
|
35
|
-
|
32
|
+
## What outputs look like
|
33
|
+
|
34
|
+
![Sample diagrams produced](https://github.com/shageman/visualize_packs/blob/main/diagram_examples.png?raw=true)
|
35
|
+
|
36
|
+
## Contributing
|
37
|
+
|
38
|
+
To contribute, install graphviz (and the `dot` command). You must also have Ruby 3.2.2 and bundler installed. Then
|
39
|
+
|
40
|
+
```
|
41
|
+
cd spec
|
42
|
+
./test.sh
|
43
|
+
```
|
44
|
+
|
45
|
+
Then, in `spec/sample_app` visually compare all `X.png` to `X_new.png` to make sure you are happy with the changes. If you, are, run `./update_cassettes.sh` in the same folder and commit the new test files with your changes.
|
46
|
+
|
47
|
+
If you have imagemagick installed, you can then also run `./create_comparison.sh` to create one big image of all the before (left) and after (right) versions of the sample diagrams.
|
data/bin/visualize_packs
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "pathname"
|
5
|
+
require "optparse"
|
6
|
+
require "ostruct"
|
7
|
+
|
8
|
+
require_relative '../lib/visualize_packs'
|
9
|
+
require_relative '../lib/options'
|
10
|
+
|
11
|
+
options = Options.new
|
12
|
+
|
13
|
+
OptionParser.new do |opt|
|
14
|
+
opt.on('--no-layers', "Don't show architectural layers") { |o| options.show_layers = false }
|
15
|
+
opt.on('--no-dependencies', "Don't show accepted dependencies") { |o| options.show_dependencies = false }
|
16
|
+
opt.on('--no-todos', "Don't show package todos") { |o| options.show_todos = false }
|
17
|
+
opt.on('--no-privacy', "Don't show privacy enforcement") { |o| options.show_privacy = false }
|
18
|
+
opt.on('--no-teams', "Don't show team colors") { |o| options.show_teams = false }
|
19
|
+
|
20
|
+
opt.on('--focus-on=PACKAGE', "Don't show privacy enforcement") { |o| options.focus_package = o }
|
21
|
+
opt.on('--only-edges-to-focus', "If focus is set, this shows only the edges to/from the focus node instead of all edges in the focussed graph. This only has effect when --focus-on is set.") { |o| options.show_only_edges_to_focus_package = true }
|
22
|
+
|
23
|
+
opt.on('--roll_nested_todos_into_top_level', "Don't show nested packages (not counting root). Connect edges to top-level package instead") { |o| options.roll_nested_todos_into_top_level = true }
|
24
|
+
opt.on('--focus_folder=FOLDER', "Draw package diagram only for packages in FOLDER") { |o| options.focus_folder = o }
|
25
|
+
opt.on('--no_nested_relationships', "Don't draw nested package relationships") { |o| options.show_nested_relationships = false }
|
26
|
+
|
27
|
+
opt.on('--remote-base-url=PACKAGE', "Link package nodes to an URL (affects graphviz SVG generation)") { |o| options.remote_base_url = o }
|
28
|
+
|
29
|
+
opt.on_tail("-h", "--help", "Show this message") do
|
30
|
+
puts opt
|
31
|
+
exit
|
32
|
+
end
|
33
|
+
end.parse!
|
34
|
+
|
35
|
+
puts VisualizePacks.package_graph!(
|
36
|
+
options,
|
37
|
+
ParsePackwerk::Configuration.fetch.raw,
|
38
|
+
Packs.all.map { |pack| ParsePackwerk.find(pack.name) }
|
39
|
+
)
|
data/lib/graph.dot.erb
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
digraph package_diagram {
|
2
|
+
rankdir=TD
|
3
|
+
graph [
|
4
|
+
labelloc="t"
|
5
|
+
fontname="Helvetica,Arial,sans-serif"
|
6
|
+
dpi=100
|
7
|
+
layout=dot
|
8
|
+
label=<%= title %>
|
9
|
+
fontsize=18
|
10
|
+
]
|
11
|
+
node [
|
12
|
+
fontname="Helvetica,Arial,sans-serif"
|
13
|
+
fontsize=26.0
|
14
|
+
fontcolor=black
|
15
|
+
fillcolor=white
|
16
|
+
color=black
|
17
|
+
height=1.0
|
18
|
+
style=filled
|
19
|
+
shape=plain
|
20
|
+
]
|
21
|
+
<%- architecture_layers.each_with_index do |layer_name, index| -%>
|
22
|
+
subgraph <%= layer_name -%> {
|
23
|
+
shape=box
|
24
|
+
color=darkgrey
|
25
|
+
fillcolor=lightblue
|
26
|
+
style=filled
|
27
|
+
<%- if layer_name != "NotInLayer" && options.show_layers -%>
|
28
|
+
label="<%= layer_name -%>"
|
29
|
+
cluster=true
|
30
|
+
rank = <%= index -%>
|
31
|
+
<%- else -%>
|
32
|
+
cluster=false
|
33
|
+
<%- end -%>
|
34
|
+
<%- grouped_packages[layer_name].each do |package| -%>
|
35
|
+
"<%= package.name -%>" [
|
36
|
+
fontsize=<%= options.focus_package && package.name == options.focus_package ? 18.0 : 12.0 -%>
|
37
|
+
<%- if options.remote_base_url %>
|
38
|
+
URL="<%= options.remote_base_url %>/<%= package.name == '.' ? '' : package.name -%>"
|
39
|
+
<%- end %>
|
40
|
+
<%- if options.show_teams && code_owner(package) -%>
|
41
|
+
style=filled
|
42
|
+
fillcolor="<%= node_color.call(code_owner(package)) %>"
|
43
|
+
<%- end -%>
|
44
|
+
label= <%- if package.enforce_privacy && options.show_privacy -%><
|
45
|
+
<table border='0' cellborder='1' cellspacing='0' cellpadding='16'><tr><td>
|
46
|
+
<table border='0' cellborder='1' cellspacing='0' cellpadding='4'>
|
47
|
+
<tr> <td port='private'> <%= package.name -%> </td> </tr>
|
48
|
+
</table>
|
49
|
+
</td></tr></table>
|
50
|
+
>
|
51
|
+
<%- else -%><
|
52
|
+
<table border='0' cellborder='1' cellspacing='0' cellpadding='4'>
|
53
|
+
<tr> <td align='left'> <%= package.name -%> </td> </tr>
|
54
|
+
</table>
|
55
|
+
>
|
56
|
+
<%- end -%>
|
57
|
+
]
|
58
|
+
|
59
|
+
<%- end -%>
|
60
|
+
}
|
61
|
+
<%- grouped_packages[layer_name].each do |package| -%>
|
62
|
+
<%- if options.show_layers -%>
|
63
|
+
<%- if index > 0 -%>
|
64
|
+
<%- grouped_packages[raw_config['architecture_layers'][index - 1]].each do |upper_layer_node| -%>
|
65
|
+
<%- if all_package_names.include?(upper_layer_node.name) && all_package_names.include?(package.name) -%>
|
66
|
+
"<%= upper_layer_node.name -%>" -> "<%= package.name -%>" [ style=invis ]
|
67
|
+
<%- end -%>
|
68
|
+
<%- end -%>
|
69
|
+
<%- end -%>
|
70
|
+
<%- end -%>
|
71
|
+
<%- end -%>
|
72
|
+
<%- end -%>
|
73
|
+
<%- if options.show_dependencies -%>
|
74
|
+
<%- all_packages.each do |package| -%>
|
75
|
+
<%- package.dependencies.each do |to_package| -%>
|
76
|
+
<%- if show_edge.call(package.name, to_package) -%>
|
77
|
+
"<%= package.name -%>" -> "<%= to_package -%>" [ color=darkgreen ]
|
78
|
+
<%- end -%>
|
79
|
+
<%- end -%>
|
80
|
+
<%- end -%>
|
81
|
+
<%- end -%>
|
82
|
+
<%- if options.show_todos -%>
|
83
|
+
<%- all_packages.each do |package| -%>
|
84
|
+
<%- violations_by_package = package.violations.group_by(&:to_package_name) -%>
|
85
|
+
<%- violations_by_package.keys.each do |violations_to_package| -%>
|
86
|
+
<%- violation_types = violations_by_package[violations_to_package].group_by(&:type) -%>
|
87
|
+
<%- violation_types.keys.each do |violation_type| -%>
|
88
|
+
<%- if show_edge.call(package.name, violations_to_package) -%>
|
89
|
+
"<%= package.name -%>" -> "<%= violations_to_package -%>"<%= violation_type == 'privacy' ? ':private' : '' -%> [ color=darkred style=dashed
|
90
|
+
constraint=false
|
91
|
+
# headlabel="<%= violation_type -%>"
|
92
|
+
<%- if violation_type == 'privacy' -%>
|
93
|
+
arrowhead=crow
|
94
|
+
<%- elsif violation_type == 'architecture' -%>
|
95
|
+
arrowhead=invodot
|
96
|
+
<%- elsif violation_type == 'visibility' -%>
|
97
|
+
arrowhead=obox
|
98
|
+
<%- elsif violation_type == 'dependency' -%>
|
99
|
+
arrowhead=odot
|
100
|
+
<%- end -%>
|
101
|
+
<%-
|
102
|
+
max_edge_width = 10
|
103
|
+
edge_width = (violation_types[violation_type].count / max_violation_count.to_f * max_edge_width).to_i
|
104
|
+
-%>
|
105
|
+
penwidth=<%= edge_width -%>
|
106
|
+
]
|
107
|
+
<%- end -%>
|
108
|
+
<%- end -%>
|
109
|
+
<%- end -%>
|
110
|
+
<%- end -%>
|
111
|
+
<%- end -%>
|
112
|
+
<%- if options.show_nested_relationships -%>
|
113
|
+
<%- all_packages.each do |package| -%>
|
114
|
+
<%- all_packages.each do |nested_package| -%>
|
115
|
+
<%- if nested_package.name.include?(package.name) && !(nested_package.name == package.name) -%>
|
116
|
+
"<%= package.name -%>" -> "<%= nested_package.name -%>" [ color=purple penwidth=3 ]
|
117
|
+
<%- end -%>
|
118
|
+
<%- end -%>
|
119
|
+
<%- end -%>
|
120
|
+
<%- end -%>
|
121
|
+
subgraph cluster_legend {
|
122
|
+
fontsize=16
|
123
|
+
label="Edges Styles and Arrow Heads"
|
124
|
+
A [ fontsize=12 shape=box label="package"]
|
125
|
+
B [ fontsize=12 shape=box label="package"]
|
126
|
+
C [ fontsize=12 shape=box label="package"]
|
127
|
+
D [ fontsize=12 shape=box label="package"]
|
128
|
+
E [ fontsize=12 shape=box label="package"]
|
129
|
+
F [ fontsize=12 shape=box label="package"]
|
130
|
+
G [ fontsize=12 shape=box label="package"]
|
131
|
+
H [ fontsize=12 shape=box label="package"]
|
132
|
+
I [ fontsize=12 shape=box label="package"]
|
133
|
+
J [ fontsize=12 shape=box label="package"]
|
134
|
+
K [ fontsize=12 shape=box label="package"]
|
135
|
+
L [ fontsize=12 shape=box label="package"]
|
136
|
+
A -> B [label="accepted dependency" color=darkgreen]
|
137
|
+
C -> D [label="privac todo" color=darkred style=dashed arrowhead=crow]
|
138
|
+
E -> F [label="architecture todo" color=darkred style=dashed arrowhead=invodot]
|
139
|
+
G -> H [label="visibility todo" color=darkred style=dashed arrowhead=obox]
|
140
|
+
I -> J [label="dependency todo" color=darkred style=dashed arrowhead=odot]
|
141
|
+
K -> L [label="nested package" color=purple penwidth=3]
|
142
|
+
}
|
143
|
+
<%- if options.show_teams && all_team_names != [] -%>
|
144
|
+
subgraph cluster_teams_legend {
|
145
|
+
fontsize=16
|
146
|
+
label="Team Colors"
|
147
|
+
|
148
|
+
<%- all_team_names.each do |team_name| -%>
|
149
|
+
<%- if team_name -%>
|
150
|
+
"<%= team_name %><%= team_name %>" [
|
151
|
+
label="<%= team_name %>"
|
152
|
+
style=filled
|
153
|
+
fillcolor="<%= node_color.call(team_name) %>"
|
154
|
+
fontsize=12
|
155
|
+
shape=box
|
156
|
+
]
|
157
|
+
<%- end %>
|
158
|
+
<%- end -%>
|
159
|
+
}
|
160
|
+
J -> "<%= all_team_names.last %><%= all_team_names.last %>" [style=invis]
|
161
|
+
<%- end -%>
|
162
|
+
}
|
data/lib/options.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
class Options
|
2
|
+
attr_accessor :show_layers
|
3
|
+
attr_accessor :show_dependencies
|
4
|
+
attr_accessor :show_todos
|
5
|
+
attr_accessor :show_privacy
|
6
|
+
attr_accessor :show_teams
|
7
|
+
|
8
|
+
attr_accessor :focus_package
|
9
|
+
attr_accessor :show_only_edges_to_focus_package
|
10
|
+
|
11
|
+
attr_accessor :roll_nested_todos_into_top_level
|
12
|
+
attr_accessor :focus_folder
|
13
|
+
attr_accessor :show_nested_relationships
|
14
|
+
|
15
|
+
attr_accessor :remote_base_url
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@show_layers = true
|
19
|
+
@show_dependencies = true
|
20
|
+
@show_todos = true
|
21
|
+
@show_privacy = true
|
22
|
+
@show_teams = true
|
23
|
+
|
24
|
+
@focus_package = nil
|
25
|
+
@show_only_edges_to_focus_package = false
|
26
|
+
|
27
|
+
@roll_nested_todos_into_top_level = false
|
28
|
+
@focus_folder = nil
|
29
|
+
@show_nested_relationships = true
|
30
|
+
|
31
|
+
@remote_base_url = nil
|
32
|
+
end
|
33
|
+
end
|
data/lib/visualize_packs.rb
CHANGED
@@ -1,29 +1,213 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
require 'erb'
|
3
2
|
require 'packs-specification'
|
4
3
|
require 'parse_packwerk'
|
5
|
-
require '
|
6
|
-
require 'graphviz'
|
7
|
-
require 'sorbet-runtime'
|
8
|
-
|
9
|
-
require 'visualize_packs/node_interface'
|
10
|
-
require 'visualize_packs/graph_interface'
|
11
|
-
require 'visualize_packs/team_node'
|
12
|
-
require 'visualize_packs/package_node'
|
13
|
-
require 'visualize_packs/team_graph'
|
14
|
-
require 'visualize_packs/package_graph'
|
15
|
-
require 'visualize_packs/package_relationships'
|
4
|
+
require 'digest/md5'
|
16
5
|
|
17
6
|
module VisualizePacks
|
18
|
-
extend T::Sig
|
19
7
|
|
20
|
-
|
21
|
-
|
22
|
-
|
8
|
+
def self.package_graph!(options, raw_config, packages)
|
9
|
+
raise ArgumentError, "Package #{options.focus_package} does not exist. Found packages #{packages.map(&:name).join(", ")}" if options.focus_package && !packages.map(&:name).include?(options.focus_package)
|
10
|
+
|
11
|
+
all_packages = filtered(packages, options.focus_package, options.focus_folder).sort_by {|x| x.name }
|
12
|
+
all_package_names = all_packages.map &:name
|
13
|
+
|
14
|
+
all_packages = remove_nested_packs(all_packages) if options.roll_nested_todos_into_top_level
|
15
|
+
|
16
|
+
show_edge = show_edge_builder(options, all_package_names)
|
17
|
+
node_color = node_color_builder()
|
18
|
+
max_violation_count = max_violation_count(all_packages, show_edge)
|
19
|
+
|
20
|
+
title = diagram_title(options, max_violation_count)
|
21
|
+
|
22
|
+
architecture_layers = (raw_config['architecture_layers'] || []) + ["NotInLayer"]
|
23
|
+
grouped_packages = architecture_layers.inject({}) do |result, key|
|
24
|
+
result[key] = []
|
25
|
+
result
|
26
|
+
end
|
27
|
+
|
28
|
+
all_packages.each do |package|
|
29
|
+
key = package.config['layer'] || "NotInLayer"
|
30
|
+
if architecture_layers.include?(key)
|
31
|
+
grouped_packages[key] << package
|
32
|
+
else
|
33
|
+
raise RuntimeError, "Package #{package.name} has architecture layer key #{key}. Known layers are only #{architecture_layers.join(", ")}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
all_team_names = all_packages.map { |p| code_owner(p) }.uniq
|
39
|
+
|
40
|
+
file = File.open(File.expand_path File.dirname(__FILE__) + "/graph.dot.erb")
|
41
|
+
templ = file.read.gsub(/^ *(<%.+%>) *$/, '\1')
|
42
|
+
template = ERB.new(templ, trim_mode: "<>-")
|
43
|
+
template.result(binding)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def self.code_owner(package)
|
49
|
+
package.config.dig("metadata", "owner") || package.config["owner"]
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.diagram_title(options, max_violation_count)
|
53
|
+
app_name = File.basename(Dir.pwd)
|
54
|
+
focus_edge_info = options.focus_package && options.show_only_edges_to_focus_package ? "showing only edges to/from focus pack" : "showing all edges between visible packs"
|
55
|
+
focus_info = options.focus_package || options.focus_folder ? "Focus on #{[options.focus_package, options.focus_folder].compact.join(' and ')} (#{focus_edge_info})" : "All packs"
|
56
|
+
skipped_info =
|
57
|
+
[
|
58
|
+
options.show_layers ? nil : "hiding layers",
|
59
|
+
options.show_dependencies ? nil : "hiding dependencies",
|
60
|
+
options.show_todos ? nil : "hiding todos",
|
61
|
+
options.show_privacy ? nil : "hiding privacy",
|
62
|
+
options.show_teams ? nil : "hiding teams",
|
63
|
+
options.roll_nested_todos_into_top_level ? "hiding nested packs" : nil,
|
64
|
+
options.show_nested_relationships ? nil : "hiding nested relationships",
|
65
|
+
].compact.join(', ').strip
|
66
|
+
main_title = "#{app_name}: #{focus_info}#{skipped_info != '' ? ' - ' + skipped_info : ''}"
|
67
|
+
sub_title = ""
|
68
|
+
if options.show_todos && max_violation_count
|
69
|
+
sub_title = "<br/><font point-size='12'>Widest todo edge is #{max_violation_count} violation#{max_violation_count > 1 ? 's' : ''}</font>"
|
70
|
+
end
|
71
|
+
"<<b>#{main_title}</b>#{sub_title}>"
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.show_edge_builder(options, all_package_names)
|
75
|
+
return lambda do |start_node, end_node|
|
76
|
+
(
|
77
|
+
!options.show_only_edges_to_focus_package &&
|
78
|
+
all_package_names.include?(start_node) &&
|
79
|
+
all_package_names.include?(end_node)
|
80
|
+
) ||
|
81
|
+
(
|
82
|
+
options.show_only_edges_to_focus_package &&
|
83
|
+
all_package_names.include?(start_node) &&
|
84
|
+
all_package_names.include?(end_node) &&
|
85
|
+
[start_node, end_node].include?(options.focus_package)
|
86
|
+
)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.node_color_builder
|
91
|
+
return lambda do |text|
|
92
|
+
return unless text
|
93
|
+
hash_value = Digest::SHA256.hexdigest(text.encode('utf-8'))
|
94
|
+
color_code = hash_value[0, 6]
|
95
|
+
r = color_code[0, 2].to_i(16) % 128 + 128
|
96
|
+
g = color_code[2, 2].to_i(16) % 128 + 128
|
97
|
+
b = color_code[4, 2].to_i(16) % 128 + 128
|
98
|
+
hex = "#%02X%02X%02X" % [r, g, b]
|
99
|
+
end
|
23
100
|
end
|
24
101
|
|
25
|
-
|
26
|
-
|
27
|
-
|
102
|
+
def self.max_violation_count(all_packages, show_edge)
|
103
|
+
violation_counts = {}
|
104
|
+
all_packages.each do |package|
|
105
|
+
violations_by_package = package.violations.group_by(&:to_package_name)
|
106
|
+
violations_by_package.keys.each do |violations_to_package|
|
107
|
+
violation_types = violations_by_package[violations_to_package].group_by(&:type)
|
108
|
+
violation_types.keys.each do |violation_type|
|
109
|
+
if show_edge.call(package.name, violations_to_package)
|
110
|
+
key = "#{package.name}->#{violations_to_package}:#{violation_type}"
|
111
|
+
violation_counts[key] = violation_types[violation_type].count
|
112
|
+
# violation_counts[key] += 1
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
violation_counts.values.max
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.filtered(packages, filter_package, filter_folder)
|
121
|
+
return packages unless filter_package || filter_folder
|
122
|
+
|
123
|
+
result = packages.map { |pack| pack.name }
|
124
|
+
|
125
|
+
if filter_package
|
126
|
+
result = [filter_package]
|
127
|
+
result += packages.select{ |p| p.dependencies.include? filter_package }.map { |pack| pack.name }
|
128
|
+
result += ParsePackwerk.find(filter_package).dependencies
|
129
|
+
result += packages.select{ |p| p.violations.map(&:to_package_name).include? filter_package }.map { |pack| pack.name }
|
130
|
+
result += ParsePackwerk.find(filter_package).violations.map(&:to_package_name)
|
131
|
+
result = result.uniq
|
132
|
+
end
|
133
|
+
|
134
|
+
if filter_folder
|
135
|
+
result = result.select { |p| p.include? filter_folder }
|
136
|
+
end
|
137
|
+
|
138
|
+
result.map { |pack_name| ParsePackwerk.find(pack_name) }
|
139
|
+
end
|
140
|
+
|
141
|
+
def self.all_nested_packages(all_package_names)
|
142
|
+
all_package_names.reject { |p| p == '.' }.inject({}) do |result, package|
|
143
|
+
package_map_tally = all_package_names.map { |other_package| Pathname.new(package).parent.to_s.include?(other_package) }
|
144
|
+
if package_map_tally.tally[true].nil?
|
145
|
+
#nothing to do
|
146
|
+
elsif package_map_tally.tally[true] > 1
|
147
|
+
raise "Can't handle multiple levels of nesting atm"
|
148
|
+
elsif package_map_tally.tally[true] == 1
|
149
|
+
result[package] = all_package_names[package_map_tally.find_index(true)]
|
150
|
+
end
|
151
|
+
result
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def self.remove_nested_packs(packages)
|
156
|
+
nested_packages = all_nested_packages(packages.map { |p| p.name })
|
157
|
+
|
158
|
+
# top-level packages
|
159
|
+
morphed_packages = packages.map do |package|
|
160
|
+
if nested_packages.include?(package.name)
|
161
|
+
package
|
162
|
+
else
|
163
|
+
# nested packages
|
164
|
+
nested_packages.keys.each do |nested_package_name|
|
165
|
+
if nested_packages[nested_package_name] == package.name
|
166
|
+
nested_package = packages.find { |p| p.name == nested_package_name }
|
167
|
+
|
168
|
+
package = ParsePackwerk::Package.new(
|
169
|
+
name: package.name,
|
170
|
+
enforce_dependencies: package.enforce_dependencies,
|
171
|
+
enforce_privacy: package.enforce_privacy,
|
172
|
+
public_path: package.public_path,
|
173
|
+
metadata: package.metadata,
|
174
|
+
dependencies: package.dependencies + nested_package.dependencies,
|
175
|
+
config: package.config,
|
176
|
+
violations: package.violations + nested_package.violations
|
177
|
+
)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
|
182
|
+
morphed_dependencies = package.dependencies.map do |d|
|
183
|
+
nested_packages[d] || d
|
184
|
+
end.uniq.reject { |p| p == package.name }
|
185
|
+
|
186
|
+
morphed_violations = package.violations.map do |v|
|
187
|
+
ParsePackwerk::Violation.new(
|
188
|
+
type: v.type,
|
189
|
+
to_package_name: nested_packages[v.to_package_name] || v.to_package_name,
|
190
|
+
class_name: v.class_name,
|
191
|
+
files: v.files
|
192
|
+
)
|
193
|
+
end.reject { |v| v.to_package_name == package.name }
|
194
|
+
|
195
|
+
|
196
|
+
new_package = ParsePackwerk::Package.new(
|
197
|
+
name: package.name,
|
198
|
+
enforce_dependencies: package.enforce_dependencies,
|
199
|
+
enforce_privacy: package.enforce_privacy,
|
200
|
+
public_path: package.public_path,
|
201
|
+
metadata: package.metadata,
|
202
|
+
dependencies: morphed_dependencies,
|
203
|
+
config: package.config,
|
204
|
+
violations: morphed_violations
|
205
|
+
)
|
206
|
+
# add dependencies TO nested packages to top-level package
|
207
|
+
# add violations TO nested packages to top-level package
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
morphed_packages.reject { |p| nested_packages.keys.include?(p.name) }
|
28
212
|
end
|
29
213
|
end
|
metadata
CHANGED
@@ -1,79 +1,23 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: visualize_packs
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gusto Engineers
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-08-
|
11
|
+
date: 2023-08-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ">="
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '0'
|
20
|
-
type: :runtime
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - ">="
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '0'
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: packs-specification
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - ">="
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '0'
|
34
|
-
type: :runtime
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - ">="
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '0'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: parse_packwerk
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - ">="
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '0'
|
48
|
-
type: :runtime
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - ">="
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '0'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: code_ownership
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - ">="
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '0'
|
62
|
-
type: :runtime
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - ">="
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '0'
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: rake
|
14
|
+
name: bundler
|
71
15
|
requirement: !ruby/object:Gem::Requirement
|
72
16
|
requirements:
|
73
17
|
- - ">="
|
74
18
|
- !ruby/object:Gem::Version
|
75
19
|
version: '0'
|
76
|
-
type: :
|
20
|
+
type: :development
|
77
21
|
prerelease: false
|
78
22
|
version_requirements: !ruby/object:Gem::Requirement
|
79
23
|
requirements:
|
@@ -81,55 +25,41 @@ dependencies:
|
|
81
25
|
- !ruby/object:Gem::Version
|
82
26
|
version: '0'
|
83
27
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
28
|
+
name: sorbet-runtime
|
85
29
|
requirement: !ruby/object:Gem::Requirement
|
86
30
|
requirements:
|
87
31
|
- - ">="
|
88
32
|
- !ruby/object:Gem::Version
|
89
33
|
version: '0'
|
90
|
-
type: :
|
34
|
+
type: :development
|
91
35
|
prerelease: false
|
92
36
|
version_requirements: !ruby/object:Gem::Requirement
|
93
37
|
requirements:
|
94
38
|
- - ">="
|
95
39
|
- !ruby/object:Gem::Version
|
96
40
|
version: '0'
|
97
|
-
- !ruby/object:Gem::Dependency
|
98
|
-
name: bundler
|
99
|
-
requirement: !ruby/object:Gem::Requirement
|
100
|
-
requirements:
|
101
|
-
- - "~>"
|
102
|
-
- !ruby/object:Gem::Version
|
103
|
-
version: 2.2.16
|
104
|
-
type: :development
|
105
|
-
prerelease: false
|
106
|
-
version_requirements: !ruby/object:Gem::Requirement
|
107
|
-
requirements:
|
108
|
-
- - "~>"
|
109
|
-
- !ruby/object:Gem::Version
|
110
|
-
version: 2.2.16
|
111
41
|
- !ruby/object:Gem::Dependency
|
112
42
|
name: rspec
|
113
43
|
requirement: !ruby/object:Gem::Requirement
|
114
44
|
requirements:
|
115
45
|
- - "~>"
|
116
46
|
- !ruby/object:Gem::Version
|
117
|
-
version: '3.
|
47
|
+
version: '3.12'
|
118
48
|
type: :development
|
119
49
|
prerelease: false
|
120
50
|
version_requirements: !ruby/object:Gem::Requirement
|
121
51
|
requirements:
|
122
52
|
- - "~>"
|
123
53
|
- !ruby/object:Gem::Version
|
124
|
-
version: '3.
|
54
|
+
version: '3.12'
|
125
55
|
- !ruby/object:Gem::Dependency
|
126
|
-
name:
|
56
|
+
name: packs-specification
|
127
57
|
requirement: !ruby/object:Gem::Requirement
|
128
58
|
requirements:
|
129
59
|
- - ">="
|
130
60
|
- !ruby/object:Gem::Version
|
131
61
|
version: '0'
|
132
|
-
type: :
|
62
|
+
type: :runtime
|
133
63
|
prerelease: false
|
134
64
|
version_requirements: !ruby/object:Gem::Requirement
|
135
65
|
requirements:
|
@@ -137,36 +67,32 @@ dependencies:
|
|
137
67
|
- !ruby/object:Gem::Version
|
138
68
|
version: '0'
|
139
69
|
- !ruby/object:Gem::Dependency
|
140
|
-
name:
|
70
|
+
name: parse_packwerk
|
141
71
|
requirement: !ruby/object:Gem::Requirement
|
142
72
|
requirements:
|
143
73
|
- - ">="
|
144
74
|
- !ruby/object:Gem::Version
|
145
|
-
version:
|
146
|
-
type: :
|
75
|
+
version: 0.20.0
|
76
|
+
type: :runtime
|
147
77
|
prerelease: false
|
148
78
|
version_requirements: !ruby/object:Gem::Requirement
|
149
79
|
requirements:
|
150
80
|
- - ">="
|
151
81
|
- !ruby/object:Gem::Version
|
152
|
-
version:
|
82
|
+
version: 0.20.0
|
153
83
|
description: A gem to visualize connections in a Ruby app that uses packs
|
154
84
|
email:
|
155
85
|
- dev@gusto.com
|
156
|
-
executables:
|
86
|
+
executables:
|
87
|
+
- visualize_packs
|
157
88
|
extensions: []
|
158
89
|
extra_rdoc_files: []
|
159
90
|
files:
|
160
91
|
- README.md
|
92
|
+
- bin/visualize_packs
|
93
|
+
- lib/graph.dot.erb
|
94
|
+
- lib/options.rb
|
161
95
|
- lib/visualize_packs.rb
|
162
|
-
- lib/visualize_packs/graph_interface.rb
|
163
|
-
- lib/visualize_packs/legend.png
|
164
|
-
- lib/visualize_packs/node_interface.rb
|
165
|
-
- lib/visualize_packs/package_graph.rb
|
166
|
-
- lib/visualize_packs/package_node.rb
|
167
|
-
- lib/visualize_packs/package_relationships.rb
|
168
|
-
- lib/visualize_packs/team_graph.rb
|
169
|
-
- lib/visualize_packs/team_node.rb
|
170
96
|
homepage: https://github.com/rubyatscale/visualize_packs
|
171
97
|
licenses:
|
172
98
|
- MIT
|
@@ -1,17 +0,0 @@
|
|
1
|
-
# typed: strict
|
2
|
-
|
3
|
-
module VisualizePacks
|
4
|
-
# This stores graphviz-independent views of our package graph.
|
5
|
-
# It should be optimized for fast lookup (leveraging internal indexes, which are stable due to the immutability of the package nodes)
|
6
|
-
# A `TeamGraph` should be able to consume this and basically just create a reduced version
|
7
|
-
# Lastly, each one should implement a common interface, and graphviz should use that interface and take in either types of graph via the interface
|
8
|
-
module GraphInterface
|
9
|
-
extend T::Sig
|
10
|
-
extend T::Helpers
|
11
|
-
interface!
|
12
|
-
|
13
|
-
sig { abstract.returns(T::Set[NodeInterface]) }
|
14
|
-
def nodes
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
Binary file
|
@@ -1,29 +0,0 @@
|
|
1
|
-
# typed: strict
|
2
|
-
|
3
|
-
module VisualizePacks
|
4
|
-
module NodeInterface
|
5
|
-
extend T::Sig
|
6
|
-
extend T::Helpers
|
7
|
-
interface!
|
8
|
-
|
9
|
-
sig { abstract.returns(String) }
|
10
|
-
def name
|
11
|
-
end
|
12
|
-
|
13
|
-
sig { abstract.returns(String) }
|
14
|
-
def group_name
|
15
|
-
end
|
16
|
-
|
17
|
-
sig { abstract.returns(T::Hash[String, Integer]) }
|
18
|
-
def violations_by_node_name
|
19
|
-
end
|
20
|
-
|
21
|
-
sig { abstract.returns(T::Array[String]) }
|
22
|
-
def dependencies
|
23
|
-
end
|
24
|
-
|
25
|
-
sig { abstract.params(node_name: String).returns(T::Boolean) }
|
26
|
-
def depends_on?(node_name)
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
@@ -1,54 +0,0 @@
|
|
1
|
-
# typed: strict
|
2
|
-
|
3
|
-
module VisualizePacks
|
4
|
-
class PackageGraph
|
5
|
-
extend T::Sig
|
6
|
-
include GraphInterface
|
7
|
-
|
8
|
-
sig { returns(T::Set[PackageNode]) }
|
9
|
-
attr_reader :package_nodes
|
10
|
-
|
11
|
-
sig { override.returns(T::Set[NodeInterface]) }
|
12
|
-
def nodes
|
13
|
-
package_nodes
|
14
|
-
end
|
15
|
-
|
16
|
-
sig { params(package_nodes: T::Set[PackageNode]).void }
|
17
|
-
def initialize(package_nodes:)
|
18
|
-
@package_nodes = package_nodes
|
19
|
-
@index_by_name = T.let({}, T::Hash[String, T.nilable(PackageNode)])
|
20
|
-
end
|
21
|
-
|
22
|
-
sig { returns(PackageGraph) }
|
23
|
-
def self.construct
|
24
|
-
package_nodes = Set.new
|
25
|
-
Packs.all.each do |p|
|
26
|
-
owner = CodeOwnership.for_package(p)
|
27
|
-
|
28
|
-
# Here we need to load the package violations and dependencies,
|
29
|
-
# so we need to use ParsePackwerk to parse that information.
|
30
|
-
package_info = ParsePackwerk.find(p.name)
|
31
|
-
next unless package_info # This should not happen unless packs/parse_packwerk change implementation
|
32
|
-
|
33
|
-
violations = package_info.violations
|
34
|
-
violations_by_package = violations.group_by(&:to_package_name).transform_values(&:count)
|
35
|
-
|
36
|
-
dependencies = package_info.dependencies
|
37
|
-
|
38
|
-
package_nodes << PackageNode.new(
|
39
|
-
name: p.name,
|
40
|
-
team_name: owner&.name || 'Unknown',
|
41
|
-
violations_by_package: violations_by_package,
|
42
|
-
dependencies: Set.new(dependencies)
|
43
|
-
)
|
44
|
-
end
|
45
|
-
|
46
|
-
PackageGraph.new(package_nodes: package_nodes)
|
47
|
-
end
|
48
|
-
|
49
|
-
sig { params(name: String).returns(T.nilable(PackageNode)) }
|
50
|
-
def package_by_name(name)
|
51
|
-
@index_by_name[name] ||= package_nodes.find { |node| node.name == name }
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
@@ -1,28 +0,0 @@
|
|
1
|
-
# typed: strict
|
2
|
-
|
3
|
-
module VisualizePacks
|
4
|
-
class PackageNode < T::Struct
|
5
|
-
extend T::Sig
|
6
|
-
include NodeInterface
|
7
|
-
|
8
|
-
const :name, String
|
9
|
-
const :team_name, String
|
10
|
-
const :violations_by_package, T::Hash[String, Integer]
|
11
|
-
const :dependencies, T::Set[String]
|
12
|
-
|
13
|
-
sig { override.returns(T::Hash[String, Integer]) }
|
14
|
-
def violations_by_node_name
|
15
|
-
violations_by_package
|
16
|
-
end
|
17
|
-
|
18
|
-
sig { override.returns(String) }
|
19
|
-
def group_name
|
20
|
-
team_name
|
21
|
-
end
|
22
|
-
|
23
|
-
sig { override.params(node_name: String).returns(T::Boolean) }
|
24
|
-
def depends_on?(node_name)
|
25
|
-
dependencies.include?(node_name) || (violations_by_package[node_name] || 0) > 0
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
@@ -1,170 +0,0 @@
|
|
1
|
-
# typed: strict
|
2
|
-
|
3
|
-
module VisualizePacks
|
4
|
-
class PackageRelationships
|
5
|
-
extend T::Sig
|
6
|
-
|
7
|
-
OUTPUT_FILENAME = T.let('packwerk.png'.freeze, String)
|
8
|
-
|
9
|
-
sig { params(teams: T::Array[CodeTeams::Team]).void }
|
10
|
-
def create_package_graph_for_teams!(teams)
|
11
|
-
packages = Packs.all.select do |package|
|
12
|
-
teams.map(&:name).include?(CodeOwnership.for_package(package)&.name)
|
13
|
-
end
|
14
|
-
|
15
|
-
create_package_graph!(packages)
|
16
|
-
end
|
17
|
-
|
18
|
-
sig { params(teams: T::Array[CodeTeams::Team], show_all_teams: T::Boolean).void }
|
19
|
-
def create_team_graph!(teams, show_all_teams: false)
|
20
|
-
package_graph = PackageGraph.construct
|
21
|
-
team_graph = TeamGraph.from_package_graph(package_graph)
|
22
|
-
node_names = teams.map(&:name)
|
23
|
-
|
24
|
-
draw_graph!(team_graph, node_names, show_all_nodes: show_all_teams)
|
25
|
-
end
|
26
|
-
|
27
|
-
sig { params(packages: T::Array[Packs::Pack]).void }
|
28
|
-
def create_package_graph!(packages)
|
29
|
-
graph = PackageGraph.construct
|
30
|
-
node_names = packages.map(&:name)
|
31
|
-
draw_graph!(graph, node_names)
|
32
|
-
end
|
33
|
-
|
34
|
-
sig { params(packages: T::Array[Packs::Pack], show_all_nodes: T::Boolean).void }
|
35
|
-
def create_graph!(packages, show_all_nodes: false)
|
36
|
-
graph = PackageGraph.construct
|
37
|
-
node_names = packages.map(&:name)
|
38
|
-
draw_graph!(graph, node_names, show_all_nodes: show_all_nodes)
|
39
|
-
end
|
40
|
-
|
41
|
-
sig { params(graph: GraphInterface, node_names: T::Array[String], show_all_nodes: T::Boolean).void }
|
42
|
-
def draw_graph!(graph, node_names, show_all_nodes: false)
|
43
|
-
# SFDP looks better than dot in some cases, but less good in other cases.
|
44
|
-
# If your visualization looks bad, change the layout to other_layout!
|
45
|
-
# https://graphviz.org/docs/layouts/
|
46
|
-
default_layout = :dot
|
47
|
-
# other_layout = :sfdp
|
48
|
-
graphviz_graph = GraphViz.new(
|
49
|
-
:G,
|
50
|
-
type: :digraph,
|
51
|
-
dpi: 100,
|
52
|
-
layout: default_layout,
|
53
|
-
label: "Visualization of #{node_names.count} packs, generated using `bin/packs`",
|
54
|
-
fontsize: 24,
|
55
|
-
labelloc: "t",
|
56
|
-
)
|
57
|
-
|
58
|
-
# Create graph nodes
|
59
|
-
graphviz_nodes = T.let({}, T::Hash[String, GraphViz::Node])
|
60
|
-
|
61
|
-
nodes_to_draw = graph.nodes.select{|n| node_names.include?(n.name) }
|
62
|
-
|
63
|
-
nodes_to_draw.each do |node|
|
64
|
-
graphviz_nodes[node.name] = add_node(node, graphviz_graph)
|
65
|
-
end
|
66
|
-
|
67
|
-
# Draw all edges
|
68
|
-
nodes_to_draw.each do |node|
|
69
|
-
node.dependencies.each do |to_node|
|
70
|
-
next unless node_names.include?(to_node)
|
71
|
-
|
72
|
-
add_dependency(
|
73
|
-
graph: graphviz_graph,
|
74
|
-
node1: T.must(graphviz_nodes[node.name]),
|
75
|
-
node2: T.must(graphviz_nodes[to_node]),
|
76
|
-
)
|
77
|
-
end
|
78
|
-
|
79
|
-
node.violations_by_node_name.each do |to_node_name, violation_count|
|
80
|
-
next unless node_names.include?(to_node_name)
|
81
|
-
|
82
|
-
add_violation(
|
83
|
-
graph: graphviz_graph,
|
84
|
-
node1: T.must(graphviz_nodes[node.name]),
|
85
|
-
node2: T.must(graphviz_nodes[to_node_name]),
|
86
|
-
violation_count: violation_count
|
87
|
-
)
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
add_legend(graphviz_graph)
|
92
|
-
|
93
|
-
# Save graph to filesystem
|
94
|
-
puts "Outputting to: #{OUTPUT_FILENAME}"
|
95
|
-
graphviz_graph.output(png: OUTPUT_FILENAME)
|
96
|
-
puts 'Finished!'
|
97
|
-
end
|
98
|
-
|
99
|
-
sig { params(node: NodeInterface, graph: GraphViz).returns(GraphViz::Node) }
|
100
|
-
def add_node(node, graph)
|
101
|
-
node_options = {
|
102
|
-
fontsize: 26.0,
|
103
|
-
fontcolor: 'black',
|
104
|
-
fillcolor: 'white',
|
105
|
-
color: 'black',
|
106
|
-
height: 1.0,
|
107
|
-
style: 'filled, rounded',
|
108
|
-
shape: 'box',
|
109
|
-
}
|
110
|
-
|
111
|
-
graph.add_nodes(node.name, **node_options)
|
112
|
-
end
|
113
|
-
|
114
|
-
sig { params(graph: GraphViz).void }
|
115
|
-
def add_legend(graph)
|
116
|
-
legend = graph.add_graph("legend")
|
117
|
-
|
118
|
-
# This commented out code was used to generate an image that I edited by hand.
|
119
|
-
# I was unable to figure out how to:
|
120
|
-
# - put a box around the legend
|
121
|
-
# - layout the node pairs in vertical order
|
122
|
-
# - give it a title
|
123
|
-
# So I just generated this using graphviz and then pulled the image in.
|
124
|
-
# a_node = legend.add_nodes("packs/a")
|
125
|
-
# b_node = legend.add_nodes("packs/b")
|
126
|
-
# c_node = legend.add_nodes("packs/c")
|
127
|
-
# d_node = legend.add_nodes("packs/d")
|
128
|
-
# e_node = legend.add_nodes("packs/e")
|
129
|
-
# f_node = legend.add_nodes("packs/f")
|
130
|
-
|
131
|
-
# add_dependency(graph: legend, node1: a_node, node2: b_node, label: 'Dependency in package.yml')
|
132
|
-
# add_violation(graph: legend, node1: c_node, node2: d_node, violation_count: 1, label: 'Violations (few)')
|
133
|
-
# add_violation(graph: legend, node1: e_node, node2: f_node, violation_count: 30, label: 'Violations (many)')
|
134
|
-
|
135
|
-
image = legend.add_node("",
|
136
|
-
shape: "image",
|
137
|
-
image: Pathname.new(__dir__).join("./legend.png").to_s,
|
138
|
-
)
|
139
|
-
end
|
140
|
-
|
141
|
-
sig { params(graph: GraphViz, node1: GraphViz::Node, node2: GraphViz::Node, violation_count: Integer, label: T.nilable(String)).void }
|
142
|
-
def add_violation(graph:, node1:, node2:, violation_count:, label: nil)
|
143
|
-
max_edge_width = 10
|
144
|
-
|
145
|
-
edge_width = [
|
146
|
-
[(violation_count / 5).to_i, 1].max, # rubocop:disable Lint/NumberConversion
|
147
|
-
max_edge_width,
|
148
|
-
].min
|
149
|
-
|
150
|
-
opts = { color: 'red', style: 'dashed', penwidth: edge_width }
|
151
|
-
if label
|
152
|
-
opts.merge!(label: label)
|
153
|
-
end
|
154
|
-
|
155
|
-
graph.add_edges(node1, node2, opts)
|
156
|
-
end
|
157
|
-
|
158
|
-
sig { params(graph: GraphViz, node1: GraphViz::Node, node2: GraphViz::Node, label: T.nilable(String)).void }
|
159
|
-
def add_dependency(graph:, node1:, node2:, label: nil)
|
160
|
-
opts = { color: 'darkgreen' }
|
161
|
-
if label
|
162
|
-
opts.merge!(label: label)
|
163
|
-
end
|
164
|
-
|
165
|
-
graph.add_edges(node1, node2, opts)
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
private_constant :PackageRelationships
|
170
|
-
end
|
@@ -1,56 +0,0 @@
|
|
1
|
-
# typed: strict
|
2
|
-
|
3
|
-
module VisualizePacks
|
4
|
-
#
|
5
|
-
# A team graph reduces a PackageGraph by aggregating over packages owned by teams
|
6
|
-
#
|
7
|
-
class TeamGraph
|
8
|
-
extend T::Sig
|
9
|
-
include GraphInterface
|
10
|
-
|
11
|
-
sig { override.returns(T::Set[NodeInterface]) }
|
12
|
-
def nodes
|
13
|
-
@team_nodes
|
14
|
-
end
|
15
|
-
|
16
|
-
sig { params(team_nodes: T::Set[TeamNode]).void }
|
17
|
-
def initialize(team_nodes:)
|
18
|
-
@team_nodes = team_nodes
|
19
|
-
end
|
20
|
-
|
21
|
-
sig { params(package_graph: PackageGraph).returns(TeamGraph) }
|
22
|
-
def self.from_package_graph(package_graph)
|
23
|
-
team_nodes = T.let(Set.new, T::Set[TeamNode])
|
24
|
-
package_graph.package_nodes.group_by(&:team_name).each do |team, package_nodes_for_team|
|
25
|
-
violations_by_team = {}
|
26
|
-
package_nodes_for_team.map(&:violations_by_package).each do |new_violations_by_package|
|
27
|
-
new_violations_by_package.each do |pack_name, count|
|
28
|
-
# We first get the pack owner of the violated package
|
29
|
-
other_package = package_graph.package_by_name(pack_name)
|
30
|
-
next if other_package.nil?
|
31
|
-
other_team = other_package.team_name
|
32
|
-
violations_by_team[other_team] ||= 0
|
33
|
-
# Then we add the violations on that team together
|
34
|
-
# TODO: We may want to ignore this if team == other_team to avoid arrows pointing to self, but maybe not!
|
35
|
-
violations_by_team[other_team] += count
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
dependencies = Set.new
|
40
|
-
package_nodes_for_team.map(&:dependencies).reduce(Set.new, :+).each do |dependency|
|
41
|
-
other_pack = package_graph.package_by_name(dependency)
|
42
|
-
next if other_pack.nil?
|
43
|
-
dependencies << other_pack.team_name
|
44
|
-
end
|
45
|
-
|
46
|
-
team_nodes << TeamNode.new(
|
47
|
-
name: team,
|
48
|
-
violations_by_team: violations_by_team,
|
49
|
-
dependencies: dependencies
|
50
|
-
)
|
51
|
-
end
|
52
|
-
|
53
|
-
TeamGraph.new(team_nodes: team_nodes)
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
@@ -1,27 +0,0 @@
|
|
1
|
-
# typed: strict
|
2
|
-
|
3
|
-
module VisualizePacks
|
4
|
-
class TeamNode < T::Struct
|
5
|
-
extend T::Sig
|
6
|
-
include NodeInterface
|
7
|
-
|
8
|
-
const :name, String
|
9
|
-
const :violations_by_team, T::Hash[String, Integer]
|
10
|
-
const :dependencies, T::Set[String]
|
11
|
-
|
12
|
-
sig { override.returns(T::Hash[String, Integer]) }
|
13
|
-
def violations_by_node_name
|
14
|
-
violations_by_team
|
15
|
-
end
|
16
|
-
|
17
|
-
sig { override.returns(String) }
|
18
|
-
def group_name
|
19
|
-
name
|
20
|
-
end
|
21
|
-
|
22
|
-
sig { override.params(node_name: String).returns(T::Boolean) }
|
23
|
-
def depends_on?(node_name)
|
24
|
-
dependencies.include?(node_name) || (violations_by_node_name[node_name] || 0) > 0
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|