github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/resource/graph/dependency_graph_rapid_test.go (about)

     1  // Copyright 2016-2021, Pulumi Corporation.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Model-checks dependency_graph functionality against simple models
    16  // using property-based testing.
    17  //
    18  // Currently this assumes a simplified model of `resource.State`
    19  // relevant to dependency calculations; `dependency_graph` only
    20  // accesses these fields:
    21  //
    22  //	type State struct {
    23  //	    Dependencies []resource.URN
    24  //	    URN          resource.URN
    25  //	    Parent       resource.URN
    26  //	    Provider     string
    27  //	    Custom       bool
    28  //	}
    29  //
    30  // At the moment only `Custom=true` (Custom, not Component) resources
    31  // are tested.
    32  package graph
    33  
    34  import (
    35  	"bytes"
    36  	"fmt"
    37  	"io"
    38  	"testing"
    39  
    40  	"pgregory.net/rapid"
    41  
    42  	"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
    43  	"github.com/stretchr/testify/assert"
    44  	"github.com/stretchr/testify/require"
    45  )
    46  
    47  // Models ------------------------------------------------------------------------------------------
    48  
    49  var isParent R = func(child, parent *resource.State) bool {
    50  	return child.Parent == parent.URN
    51  }
    52  
    53  var hasProvider R = func(res, provider *resource.State) bool {
    54  	return resource.URN(res.Provider) == provider.URN
    55  }
    56  
    57  var hasDependency R = func(res, dependency *resource.State) bool {
    58  	for _, dep := range res.Dependencies {
    59  		if dependency.URN == dep {
    60  			return true
    61  		}
    62  	}
    63  	return false
    64  }
    65  
    66  var expectedDependenciesOf R = union(isParent, hasProvider, hasDependency)
    67  
    68  // Verify `DependneciesOf` against `expectedDependenciesOf`.
    69  func TestRapidDependenciesOf(t *testing.T) {
    70  	t.Parallel()
    71  
    72  	graphCheck(t, func(t *rapid.T, universe []*resource.State) {
    73  		dg := NewDependencyGraph(universe)
    74  		for _, a := range universe {
    75  			aD := dg.DependenciesOf(a)
    76  			for _, b := range universe {
    77  				if isParent(a, b) {
    78  					assert.Truef(t, aD[b],
    79  						"DependenciesOf(%v) is missing a parent %v",
    80  						a.URN, b.URN)
    81  				}
    82  				if hasProvider(a, b) {
    83  					assert.Truef(t, aD[b],
    84  						"DependenciesOf(%v) is missing a provider %v",
    85  						a.URN, b.URN)
    86  				}
    87  				if hasDependency(a, b) {
    88  					assert.Truef(t, aD[b],
    89  						"DependenciesOf(%v) is missing a dependecy %v",
    90  						a.URN, b.URN)
    91  				}
    92  				if aD[b] {
    93  					assert.True(t, expectedDependenciesOf(a, b),
    94  						"DependenciesOf(%v) includes an unexpected %v",
    95  						a.URN, b.URN)
    96  				}
    97  			}
    98  		}
    99  	})
   100  }
   101  
   102  // Additionally verify no immediate loops in `DependenciesOf`, no `B
   103  // in DependenciesOf(A) && A in DependenciesOf(B)`.
   104  func TestRapidDependenciesOfAntisymmetric(t *testing.T) {
   105  	t.Parallel()
   106  
   107  	graphCheck(t, func(t *rapid.T, universe []*resource.State) {
   108  		dg := NewDependencyGraph(universe)
   109  		for _, a := range universe {
   110  			aD := dg.DependenciesOf(a)
   111  			for _, b := range universe {
   112  				bD := dg.DependenciesOf(b)
   113  				assert.Falsef(t, aD[b] && bD[a],
   114  					"DependenciesOf symmetric over (%v, %v)", a.URN, b.URN)
   115  			}
   116  		}
   117  	})
   118  }
   119  
   120  // Model `DependingOn`.
   121  func expectedDependingOn(universe []*resource.State, includeChildren bool) R {
   122  	if !includeChildren {
   123  		// TODO currently DependingOn is not the inverse transitive
   124  		// closure of `dependenciesOf`. Should this be
   125  		// `expectedDependenciesOf`?
   126  		restrictedDependenciesOf := union(hasProvider, hasDependency)
   127  		return inverse(transitively(universe)(restrictedDependenciesOf))
   128  	}
   129  
   130  	dependingOn := expectedDependingOn(universe, false)
   131  	return transitively(universe)(func(a, b *resource.State) bool {
   132  		if dependingOn(a, b) || isParent(b, a) {
   133  			return true
   134  		}
   135  		for _, x := range universe {
   136  			if dependingOn(x, b) && isParent(x, a) {
   137  				return true
   138  			}
   139  		}
   140  		return false
   141  	})
   142  }
   143  
   144  // Verify `DependingOn` against `expectedDependingOn`. Note that
   145  // `DependingOn` is specialised with an empty ignore map, the ignore
   146  // map is not tested yet.
   147  func TestRapidDependingOn(t *testing.T) {
   148  	t.Parallel()
   149  
   150  	test := func(t *rapid.T, universe []*resource.State, includingChildren bool) {
   151  		expected := expectedDependingOn(universe, includingChildren)
   152  		dg := NewDependencyGraph(universe)
   153  		dependingOn := func(a, b *resource.State) bool {
   154  			for _, x := range dg.DependingOn(a, nil, includingChildren) {
   155  				if b.URN == x.URN {
   156  					return true
   157  				}
   158  			}
   159  			return false
   160  		}
   161  		for _, a := range universe {
   162  			for _, b := range universe {
   163  				actual := dependingOn(a, b)
   164  				assert.Equalf(t, expected(a, b), actual,
   165  					"Unexpected %v in dg.DependingOn(%v) = %v",
   166  					b.URN, a.URN, actual)
   167  			}
   168  		}
   169  	}
   170  
   171  	//nolint:paralleltest // false positive because range var isn't used directly in t.Run(name) arg
   172  	for _, includingChildren := range []bool{false, true} {
   173  		includingChildren := includingChildren
   174  		t.Run(fmt.Sprintf("includingChildren=%v", includingChildren), func(t *testing.T) {
   175  			t.Parallel()
   176  
   177  			graphCheck(t, func(t *rapid.T, universe []*resource.State) {
   178  				test(t, universe, includingChildren)
   179  			})
   180  		})
   181  	}
   182  }
   183  
   184  // Verify `DependingOn` results are ordered, if `D1` in
   185  // `DependingOn(D2)` then `D1` appears before `D2`.
   186  func TestRapidDependingOnOrdered(t *testing.T) {
   187  	t.Parallel()
   188  
   189  	test := func(t *rapid.T, universe []*resource.State, includingChildren bool) {
   190  		expectedDependingOn := expectedDependingOn(universe, includingChildren)
   191  		dg := NewDependencyGraph(universe)
   192  		for _, a := range universe {
   193  			depOnA := dg.DependingOn(a, nil, includingChildren)
   194  			for d1i, d1 := range depOnA {
   195  				for d2i, d2 := range depOnA {
   196  					if expectedDependingOn(d2, d1) {
   197  						require.Truef(t, d2i < d1i,
   198  							"%v should appear before %v",
   199  							d2.URN, d1.URN)
   200  					}
   201  				}
   202  			}
   203  		}
   204  	}
   205  
   206  	//nolint:paralleltest // false positive because range var isn't used directly in t.Run(name) arg
   207  	for _, includingChildren := range []bool{false, true} {
   208  		includingChildren := includingChildren
   209  		t.Run(fmt.Sprintf("includingChildren=%v", includingChildren), func(t *testing.T) {
   210  			t.Parallel()
   211  
   212  			graphCheck(t, func(t *rapid.T, universe []*resource.State) {
   213  				test(t, universe, includingChildren)
   214  			})
   215  		})
   216  	}
   217  }
   218  
   219  func TestRapidTransitiveDependenciesOf(t *testing.T) {
   220  	t.Parallel()
   221  
   222  	graphCheck(t, func(t *rapid.T, universe []*resource.State) {
   223  		expectedInTDepsOf := transitively(universe)(expectedDependenciesOf)
   224  		dg := NewDependencyGraph(universe)
   225  		for _, a := range universe {
   226  			tda := dg.TransitiveDependenciesOf(a)
   227  			for _, b := range universe {
   228  				assert.Equalf(t,
   229  					expectedInTDepsOf(a, b),
   230  					tda[b],
   231  					"Mismatch on a=%v, b=%b",
   232  					a.URN,
   233  					b.URN)
   234  			}
   235  		}
   236  	})
   237  }
   238  
   239  // Generators --------------------------------------------------------------------------------------
   240  
   241  // Generates ordered values of type `[]ResourceState` that:
   242  //
   243  // - Have unique URNs
   244  // - May reference preceding resouces in the slice as r.Parent
   245  // - May reference preceding resouces in the slice in r.Dependencies
   246  //
   247  // In other words these slices conform with `NewDependencyGraph`
   248  // ordering assumptions. There is a tradedoff: generated values will
   249  // not test any error-checking code in `NewDependencyGraph`, but will
   250  // more efficiently explore more complicated properties on the valid
   251  // subspace of inputs.
   252  //
   253  // What is not currently done but may need to be extended:
   254  //
   255  // - Support Component resources
   256  // - Support non-nil r.Provider references
   257  func resourceStateSliceGenerator() *rapid.Generator {
   258  	urnGen := rapid.StringMatching(`urn:pulumi:a::b::c:d:e::[abcd][123]`)
   259  
   260  	stateGen := rapid.Custom(func(t *rapid.T) *resource.State {
   261  		urn := urnGen.Draw(t, "URN").(string)
   262  		return &resource.State{
   263  			Custom: true,
   264  			URN:    resource.URN(urn),
   265  		}
   266  	})
   267  
   268  	getUrn := func(st *resource.State) resource.URN { return st.URN }
   269  
   270  	statesGen := rapid.SliceOfDistinct(stateGen, getUrn)
   271  
   272  	return rapid.Custom(func(t *rapid.T) []*resource.State {
   273  		states := statesGen.Draw(t, "states").([]*resource.State)
   274  
   275  		randInt := rapid.IntRange(-len(states), len(states))
   276  
   277  		for i, r := range states {
   278  			// Any resource at index `i` may want to declare `j < i` as parent.
   279  			// Sample negative `j` to means "no parent".
   280  			j := randInt.Draw(t, fmt.Sprintf("j%d", i)).(int)
   281  			if j >= 0 && j < i {
   282  				r.Parent = states[j].URN
   283  			}
   284  			// Similarly we can depend on resources defined prior.
   285  			deps := rapid.SliceOfDistinct(
   286  				randInt,
   287  				func(i int) int { return i },
   288  			).Draw(t, fmt.Sprintf("deps%d", i)).([]int)
   289  			for _, dep := range deps {
   290  				if dep >= 0 && dep < i {
   291  					r.Dependencies = append(r.Dependencies, states[dep].URN)
   292  				}
   293  			}
   294  		}
   295  
   296  		return states
   297  	})
   298  }
   299  
   300  // Helper code: relations --------------------------------------------------------------------------
   301  
   302  // Shorthand for relations over `*resource.State`
   303  type R = func(a, b *resource.State) bool
   304  
   305  // Union of one or more relations.
   306  func union(rs ...R) R {
   307  	return func(a, b *resource.State) bool {
   308  		for _, r := range rs {
   309  			if r(a, b) {
   310  				return true
   311  			}
   312  		}
   313  		return false
   314  	}
   315  }
   316  
   317  // Flips the relation, `inverse(R)(a,b) = R(b,a)`.
   318  func inverse(r R) R {
   319  	return func(a, b *resource.State) bool {
   320  		return r(b, a)
   321  	}
   322  }
   323  
   324  // Memoized transitive closure of a relation.
   325  func transitively(universe []*resource.State) func(R) R {
   326  	return func(rel R) R {
   327  		trel := make(map[*resource.State]map[*resource.State]bool)
   328  		for _, a := range universe {
   329  			trel[a] = make(map[*resource.State]bool)
   330  			for _, b := range universe {
   331  				if rel(a, b) {
   332  					trel[a][b] = true
   333  				}
   334  			}
   335  		}
   336  
   337  		extend := func() bool {
   338  			more := false
   339  			for _, a := range universe {
   340  				for _, b := range universe {
   341  					if !trel[a][b] {
   342  						for _, x := range universe {
   343  							if trel[x][b] && rel(a, x) {
   344  								trel[a][b] = true
   345  								more = true
   346  							}
   347  						}
   348  					}
   349  				}
   350  			}
   351  			return more
   352  		}
   353  
   354  		for extend() {
   355  		}
   356  
   357  		return func(a, b *resource.State) bool {
   358  			return trel[a][b]
   359  		}
   360  	}
   361  }
   362  
   363  // Helper code: misc -------------------------------------------------------------------------------
   364  
   365  func printState(w io.Writer, st *resource.State) {
   366  	fmt.Fprintf(w, "%s", st.URN)
   367  	if st.Parent != "" {
   368  		fmt.Fprintf(w, " parent=%s", st.Parent)
   369  	}
   370  	if len(st.Dependencies) > 0 {
   371  		fmt.Fprintf(w, " deps=[")
   372  		for _, d := range st.Dependencies {
   373  			fmt.Fprintf(w, "%s, ", d)
   374  		}
   375  		fmt.Fprintf(w, "]")
   376  	}
   377  	fmt.Fprintf(w, "\n")
   378  }
   379  
   380  func showStates(sts []*resource.State) string {
   381  	buf := &bytes.Buffer{}
   382  	fmt.Fprintf(buf, "[\n\n")
   383  	for _, st := range sts {
   384  		printState(buf, st)
   385  		fmt.Fprintf(buf, "\n\n")
   386  	}
   387  	fmt.Fprintf(buf, "]")
   388  	return buf.String()
   389  }
   390  
   391  func graphCheck(t *testing.T, check func(*rapid.T, []*resource.State)) {
   392  	rss := resourceStateSliceGenerator()
   393  	rapid.Check(t, func(t *rapid.T) {
   394  		universe := rss.Draw(t, "universe").([]*resource.State)
   395  		t.Logf("Checking universe: %s", showStates(universe))
   396  		check(t, universe)
   397  	})
   398  }