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  }