// Copyright (c) The OpenTofu Authors // SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2023 HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package tofu import ( "strings" "testing" "github.com/opentofu/opentofu/internal/dag" ) func TestGraphDot(t *testing.T) { cases := []struct { Name string Graph testGraphFunc Opts dag.DotOpts Expect string }{ { Name: "empty", Graph: func() *Graph { return &Graph{} }, Expect: ` digraph { compound = "true" newrank = "true" subgraph "root" { } }`, }, { Name: "three-level", Graph: func() *Graph { var g Graph root := &testDrawableOrigin{"root"} g.Add(root) levelOne := []interface{}{"foo", "bar"} for i, s := range levelOne { levelOne[i] = &testDrawable{ VertexName: s.(string), } v := levelOne[i] g.Add(v) g.Connect(dag.BasicEdge(v, root)) } levelTwo := []string{"baz", "qux"} for i, s := range levelTwo { v := &testDrawable{ VertexName: s, } g.Add(v) g.Connect(dag.BasicEdge(v, levelOne[i])) } return &g }, Expect: ` digraph { compound = "true" newrank = "true" subgraph "root" { "[root] bar" "[root] baz" "[root] foo" "[root] qux" "[root] root" "[root] bar" -> "[root] root" "[root] baz" -> "[root] foo" "[root] foo" -> "[root] root" "[root] qux" -> "[root] bar" } } `, }, { Name: "cycle", Opts: dag.DotOpts{ DrawCycles: true, }, Graph: func() *Graph { var g Graph root := &testDrawableOrigin{"root"} g.Add(root) vA := g.Add(&testDrawable{ VertexName: "A", }) vB := g.Add(&testDrawable{ VertexName: "B", }) vC := g.Add(&testDrawable{ VertexName: "C", }) g.Connect(dag.BasicEdge(vA, root)) g.Connect(dag.BasicEdge(vA, vC)) g.Connect(dag.BasicEdge(vB, vA)) g.Connect(dag.BasicEdge(vC, vB)) return &g }, Expect: ` digraph { compound = "true" newrank = "true" subgraph "root" { "[root] A" "[root] B" "[root] C" "[root] root" "[root] A" -> "[root] B" [color = "red", penwidth = "2.0"] "[root] A" -> "[root] C" "[root] A" -> "[root] root" "[root] B" -> "[root] A" "[root] B" -> "[root] C" [color = "red", penwidth = "2.0"] "[root] C" -> "[root] A" [color = "red", penwidth = "2.0"] "[root] C" -> "[root] B" } } `, }, { Name: "subgraphs, no depth restriction", Opts: dag.DotOpts{ MaxDepth: -1, }, Graph: func() *Graph { var g Graph root := &testDrawableOrigin{"root"} g.Add(root) var sub Graph vSubRoot := sub.Add(&testDrawableOrigin{"sub_root"}) var subsub Graph subsub.Add(&testDrawableOrigin{"subsub_root"}) vSubV := sub.Add(&testDrawableSubgraph{ VertexName: "subsub", SubgraphMock: &subsub, }) vSub := g.Add(&testDrawableSubgraph{ VertexName: "sub", SubgraphMock: &sub, }) g.Connect(dag.BasicEdge(vSub, root)) sub.Connect(dag.BasicEdge(vSubV, vSubRoot)) return &g }, Expect: ` digraph { compound = "true" newrank = "true" subgraph "root" { "[root] root" "[root] sub" "[root] sub" -> "[root] root" } subgraph "cluster_sub" { label = "sub" "[sub] sub_root" "[sub] subsub" "[sub] subsub" -> "[sub] sub_root" } subgraph "cluster_subsub" { label = "subsub" "[subsub] subsub_root" } } `, }, { Name: "subgraphs, with depth restriction", Opts: dag.DotOpts{ MaxDepth: 1, }, Graph: func() *Graph { var g Graph root := &testDrawableOrigin{"root"} g.Add(root) var sub Graph rootSub := sub.Add(&testDrawableOrigin{"sub_root"}) var subsub Graph subsub.Add(&testDrawableOrigin{"subsub_root"}) subV := sub.Add(&testDrawableSubgraph{ VertexName: "subsub", SubgraphMock: &subsub, }) vSub := g.Add(&testDrawableSubgraph{ VertexName: "sub", SubgraphMock: &sub, }) g.Connect(dag.BasicEdge(vSub, root)) sub.Connect(dag.BasicEdge(subV, rootSub)) return &g }, Expect: ` digraph { compound = "true" newrank = "true" subgraph "root" { "[root] root" "[root] sub" "[root] sub" -> "[root] root" } subgraph "cluster_sub" { label = "sub" "[sub] sub_root" "[sub] subsub" "[sub] subsub" -> "[sub] sub_root" } } `, }, } for _, tc := range cases { tn := tc.Name t.Run(tn, func(t *testing.T) { g := tc.Graph() actual := string(g.Dot(&tc.Opts)) expected := strings.TrimSpace(tc.Expect) + "\n" if actual != expected { t.Fatalf("%s:\n\nexpected:\n%s\n\ngot:\n%s", tn, expected, actual) } }) } } type testGraphFunc func() *Graph type testDrawable struct { VertexName string DependentOnMock []string } func (node *testDrawable) Name() string { return node.VertexName } func (node *testDrawable) DotNode(n string, opts *dag.DotOpts) *dag.DotNode { return &dag.DotNode{Name: n, Attrs: map[string]string{}} } func (node *testDrawable) DependableName() []string { return []string{node.VertexName} } func (node *testDrawable) DependentOn() []string { return node.DependentOnMock } type testDrawableOrigin struct { VertexName string } func (node *testDrawableOrigin) Name() string { return node.VertexName } func (node *testDrawableOrigin) DotNode(n string, opts *dag.DotOpts) *dag.DotNode { return &dag.DotNode{Name: n, Attrs: map[string]string{}} } func (node *testDrawableOrigin) DotOrigin() bool { return true } func (node *testDrawableOrigin) DependableName() []string { return []string{node.VertexName} } type testDrawableSubgraph struct { VertexName string SubgraphMock *Graph DependentOnMock []string } func (node *testDrawableSubgraph) Name() string { return node.VertexName } func (node *testDrawableSubgraph) Subgraph() dag.Grapher { return node.SubgraphMock } func (node *testDrawableSubgraph) DotNode(n string, opts *dag.DotOpts) *dag.DotNode { return &dag.DotNode{Name: n, Attrs: map[string]string{}} } func (node *testDrawableSubgraph) DependentOn() []string { return node.DependentOnMock }