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 }