github.com/opentofu/opentofu@v1.7.1/internal/tofu/graph_dot_test.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package tofu 7 8 import ( 9 "strings" 10 "testing" 11 12 "github.com/opentofu/opentofu/internal/dag" 13 ) 14 15 func TestGraphDot(t *testing.T) { 16 cases := []struct { 17 Name string 18 Graph testGraphFunc 19 Opts dag.DotOpts 20 Expect string 21 Error string 22 }{ 23 { 24 Name: "empty", 25 Graph: func() *Graph { return &Graph{} }, 26 Expect: ` 27 digraph { 28 compound = "true" 29 newrank = "true" 30 subgraph "root" { 31 } 32 }`, 33 }, 34 { 35 Name: "three-level", 36 Graph: func() *Graph { 37 var g Graph 38 root := &testDrawableOrigin{"root"} 39 g.Add(root) 40 41 levelOne := []interface{}{"foo", "bar"} 42 for i, s := range levelOne { 43 levelOne[i] = &testDrawable{ 44 VertexName: s.(string), 45 } 46 v := levelOne[i] 47 48 g.Add(v) 49 g.Connect(dag.BasicEdge(v, root)) 50 } 51 52 levelTwo := []string{"baz", "qux"} 53 for i, s := range levelTwo { 54 v := &testDrawable{ 55 VertexName: s, 56 } 57 58 g.Add(v) 59 g.Connect(dag.BasicEdge(v, levelOne[i])) 60 } 61 62 return &g 63 }, 64 Expect: ` 65 digraph { 66 compound = "true" 67 newrank = "true" 68 subgraph "root" { 69 "[root] bar" 70 "[root] baz" 71 "[root] foo" 72 "[root] qux" 73 "[root] root" 74 "[root] bar" -> "[root] root" 75 "[root] baz" -> "[root] foo" 76 "[root] foo" -> "[root] root" 77 "[root] qux" -> "[root] bar" 78 } 79 } 80 `, 81 }, 82 83 { 84 Name: "cycle", 85 Opts: dag.DotOpts{ 86 DrawCycles: true, 87 }, 88 Graph: func() *Graph { 89 var g Graph 90 root := &testDrawableOrigin{"root"} 91 g.Add(root) 92 93 vA := g.Add(&testDrawable{ 94 VertexName: "A", 95 }) 96 97 vB := g.Add(&testDrawable{ 98 VertexName: "B", 99 }) 100 101 vC := g.Add(&testDrawable{ 102 VertexName: "C", 103 }) 104 105 g.Connect(dag.BasicEdge(vA, root)) 106 g.Connect(dag.BasicEdge(vA, vC)) 107 g.Connect(dag.BasicEdge(vB, vA)) 108 g.Connect(dag.BasicEdge(vC, vB)) 109 110 return &g 111 }, 112 Expect: ` 113 digraph { 114 compound = "true" 115 newrank = "true" 116 subgraph "root" { 117 "[root] A" 118 "[root] B" 119 "[root] C" 120 "[root] root" 121 "[root] A" -> "[root] B" [color = "red", penwidth = "2.0"] 122 "[root] A" -> "[root] C" 123 "[root] A" -> "[root] root" 124 "[root] B" -> "[root] A" 125 "[root] B" -> "[root] C" [color = "red", penwidth = "2.0"] 126 "[root] C" -> "[root] A" [color = "red", penwidth = "2.0"] 127 "[root] C" -> "[root] B" 128 } 129 } 130 `, 131 }, 132 133 { 134 Name: "subgraphs, no depth restriction", 135 Opts: dag.DotOpts{ 136 MaxDepth: -1, 137 }, 138 Graph: func() *Graph { 139 var g Graph 140 root := &testDrawableOrigin{"root"} 141 g.Add(root) 142 143 var sub Graph 144 vSubRoot := sub.Add(&testDrawableOrigin{"sub_root"}) 145 146 var subsub Graph 147 subsub.Add(&testDrawableOrigin{"subsub_root"}) 148 vSubV := sub.Add(&testDrawableSubgraph{ 149 VertexName: "subsub", 150 SubgraphMock: &subsub, 151 }) 152 153 vSub := g.Add(&testDrawableSubgraph{ 154 VertexName: "sub", 155 SubgraphMock: &sub, 156 }) 157 158 g.Connect(dag.BasicEdge(vSub, root)) 159 sub.Connect(dag.BasicEdge(vSubV, vSubRoot)) 160 161 return &g 162 }, 163 Expect: ` 164 digraph { 165 compound = "true" 166 newrank = "true" 167 subgraph "root" { 168 "[root] root" 169 "[root] sub" 170 "[root] sub" -> "[root] root" 171 } 172 subgraph "cluster_sub" { 173 label = "sub" 174 "[sub] sub_root" 175 "[sub] subsub" 176 "[sub] subsub" -> "[sub] sub_root" 177 } 178 subgraph "cluster_subsub" { 179 label = "subsub" 180 "[subsub] subsub_root" 181 } 182 } 183 `, 184 }, 185 186 { 187 Name: "subgraphs, with depth restriction", 188 Opts: dag.DotOpts{ 189 MaxDepth: 1, 190 }, 191 Graph: func() *Graph { 192 var g Graph 193 root := &testDrawableOrigin{"root"} 194 g.Add(root) 195 196 var sub Graph 197 rootSub := sub.Add(&testDrawableOrigin{"sub_root"}) 198 199 var subsub Graph 200 subsub.Add(&testDrawableOrigin{"subsub_root"}) 201 202 subV := sub.Add(&testDrawableSubgraph{ 203 VertexName: "subsub", 204 SubgraphMock: &subsub, 205 }) 206 vSub := g.Add(&testDrawableSubgraph{ 207 VertexName: "sub", 208 SubgraphMock: &sub, 209 }) 210 211 g.Connect(dag.BasicEdge(vSub, root)) 212 sub.Connect(dag.BasicEdge(subV, rootSub)) 213 return &g 214 }, 215 Expect: ` 216 digraph { 217 compound = "true" 218 newrank = "true" 219 subgraph "root" { 220 "[root] root" 221 "[root] sub" 222 "[root] sub" -> "[root] root" 223 } 224 subgraph "cluster_sub" { 225 label = "sub" 226 "[sub] sub_root" 227 "[sub] subsub" 228 "[sub] subsub" -> "[sub] sub_root" 229 } 230 } 231 `, 232 }, 233 } 234 235 for _, tc := range cases { 236 tn := tc.Name 237 t.Run(tn, func(t *testing.T) { 238 g := tc.Graph() 239 var err error 240 //actual, err := GraphDot(g, &tc.Opts) 241 actual := string(g.Dot(&tc.Opts)) 242 243 if err == nil && tc.Error != "" { 244 t.Fatalf("%s: expected err: %s, got none", tn, tc.Error) 245 } 246 if err != nil && tc.Error == "" { 247 t.Fatalf("%s: unexpected err: %s", tn, err) 248 } 249 if err != nil && tc.Error != "" { 250 if !strings.Contains(err.Error(), tc.Error) { 251 t.Fatalf("%s: expected err: %s\nto contain: %s", tn, err, tc.Error) 252 } 253 return 254 } 255 256 expected := strings.TrimSpace(tc.Expect) + "\n" 257 if actual != expected { 258 t.Fatalf("%s:\n\nexpected:\n%s\n\ngot:\n%s", tn, expected, actual) 259 } 260 }) 261 } 262 } 263 264 type testGraphFunc func() *Graph 265 266 type testDrawable struct { 267 VertexName string 268 DependentOnMock []string 269 } 270 271 func (node *testDrawable) Name() string { 272 return node.VertexName 273 } 274 func (node *testDrawable) DotNode(n string, opts *dag.DotOpts) *dag.DotNode { 275 return &dag.DotNode{Name: n, Attrs: map[string]string{}} 276 } 277 func (node *testDrawable) DependableName() []string { 278 return []string{node.VertexName} 279 } 280 func (node *testDrawable) DependentOn() []string { 281 return node.DependentOnMock 282 } 283 284 type testDrawableOrigin struct { 285 VertexName string 286 } 287 288 func (node *testDrawableOrigin) Name() string { 289 return node.VertexName 290 } 291 func (node *testDrawableOrigin) DotNode(n string, opts *dag.DotOpts) *dag.DotNode { 292 return &dag.DotNode{Name: n, Attrs: map[string]string{}} 293 } 294 func (node *testDrawableOrigin) DotOrigin() bool { 295 return true 296 } 297 func (node *testDrawableOrigin) DependableName() []string { 298 return []string{node.VertexName} 299 } 300 301 type testDrawableSubgraph struct { 302 VertexName string 303 SubgraphMock *Graph 304 DependentOnMock []string 305 } 306 307 func (node *testDrawableSubgraph) Name() string { 308 return node.VertexName 309 } 310 func (node *testDrawableSubgraph) Subgraph() dag.Grapher { 311 return node.SubgraphMock 312 } 313 func (node *testDrawableSubgraph) DotNode(n string, opts *dag.DotOpts) *dag.DotNode { 314 return &dag.DotNode{Name: n, Attrs: map[string]string{}} 315 } 316 func (node *testDrawableSubgraph) DependentOn() []string { 317 return node.DependentOnMock 318 }