github.com/magodo/terraform@v0.11.12-beta1/terraform/context_test.go (about) 1 package terraform 2 3 import ( 4 "fmt" 5 "strings" 6 "testing" 7 "time" 8 9 "github.com/hashicorp/go-version" 10 "github.com/hashicorp/terraform/flatmap" 11 tfversion "github.com/hashicorp/terraform/version" 12 ) 13 14 func TestNewContextRequiredVersion(t *testing.T) { 15 cases := []struct { 16 Name string 17 Module string 18 Version string 19 Value string 20 Err bool 21 }{ 22 { 23 "no requirement", 24 "", 25 "0.1.0", 26 "", 27 false, 28 }, 29 30 { 31 "doesn't match", 32 "", 33 "0.1.0", 34 "> 0.6.0", 35 true, 36 }, 37 38 { 39 "matches", 40 "", 41 "0.7.0", 42 "> 0.6.0", 43 false, 44 }, 45 46 { 47 "module matches", 48 "context-required-version-module", 49 "0.5.0", 50 "", 51 false, 52 }, 53 54 { 55 "module doesn't match", 56 "context-required-version-module", 57 "0.4.0", 58 "", 59 true, 60 }, 61 } 62 63 for i, tc := range cases { 64 t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { 65 // Reset the version for the tests 66 old := tfversion.SemVer 67 tfversion.SemVer = version.Must(version.NewVersion(tc.Version)) 68 defer func() { tfversion.SemVer = old }() 69 70 name := "context-required-version" 71 if tc.Module != "" { 72 name = tc.Module 73 } 74 mod := testModule(t, name) 75 if tc.Value != "" { 76 mod.Config().Terraform.RequiredVersion = tc.Value 77 } 78 _, err := NewContext(&ContextOpts{ 79 Module: mod, 80 }) 81 if (err != nil) != tc.Err { 82 t.Fatalf("err: %s", err) 83 } 84 if err != nil { 85 return 86 } 87 }) 88 } 89 } 90 91 func TestNewContextState(t *testing.T) { 92 cases := map[string]struct { 93 Input *ContextOpts 94 Err bool 95 }{ 96 "empty TFVersion": { 97 &ContextOpts{ 98 State: &State{}, 99 }, 100 false, 101 }, 102 103 "past TFVersion": { 104 &ContextOpts{ 105 State: &State{TFVersion: "0.1.2"}, 106 }, 107 false, 108 }, 109 110 "equal TFVersion": { 111 &ContextOpts{ 112 State: &State{TFVersion: tfversion.Version}, 113 }, 114 false, 115 }, 116 117 "future TFVersion": { 118 &ContextOpts{ 119 State: &State{TFVersion: "99.99.99"}, 120 }, 121 true, 122 }, 123 124 "future TFVersion, allowed": { 125 &ContextOpts{ 126 State: &State{TFVersion: "99.99.99"}, 127 StateFutureAllowed: true, 128 }, 129 false, 130 }, 131 } 132 133 for k, tc := range cases { 134 ctx, err := NewContext(tc.Input) 135 if (err != nil) != tc.Err { 136 t.Fatalf("%s: err: %s", k, err) 137 } 138 if err != nil { 139 continue 140 } 141 142 // Version should always be set to our current 143 if ctx.state.TFVersion != tfversion.Version { 144 t.Fatalf("%s: state not set to current version", k) 145 } 146 } 147 } 148 149 func testContext2(t *testing.T, opts *ContextOpts) *Context { 150 t.Helper() 151 // Enable the shadow graph 152 opts.Shadow = true 153 154 ctx, err := NewContext(opts) 155 if err != nil { 156 t.Fatalf("failed to create test context\n\n%s\n", err) 157 } 158 159 return ctx 160 } 161 162 func testDataApplyFn( 163 info *InstanceInfo, 164 d *InstanceDiff) (*InstanceState, error) { 165 return testApplyFn(info, new(InstanceState), d) 166 } 167 168 func testDataDiffFn( 169 info *InstanceInfo, 170 c *ResourceConfig) (*InstanceDiff, error) { 171 return testDiffFn(info, new(InstanceState), c) 172 } 173 174 func testApplyFn( 175 info *InstanceInfo, 176 s *InstanceState, 177 d *InstanceDiff) (*InstanceState, error) { 178 if d.Destroy { 179 return nil, nil 180 } 181 182 id := "foo" 183 if idAttr, ok := d.Attributes["id"]; ok && !idAttr.NewComputed { 184 id = idAttr.New 185 } 186 187 result := &InstanceState{ 188 ID: id, 189 Attributes: make(map[string]string), 190 } 191 192 // Copy all the prior attributes 193 for k, v := range s.Attributes { 194 result.Attributes[k] = v 195 } 196 197 if d != nil { 198 result = result.MergeDiff(d) 199 } 200 return result, nil 201 } 202 203 func testDiffFn( 204 info *InstanceInfo, 205 s *InstanceState, 206 c *ResourceConfig) (*InstanceDiff, error) { 207 diff := new(InstanceDiff) 208 diff.Attributes = make(map[string]*ResourceAttrDiff) 209 210 if s != nil { 211 diff.DestroyTainted = s.Tainted 212 } 213 214 for k, v := range c.Raw { 215 // Ignore __-prefixed keys since they're used for magic 216 if k[0] == '_' && k[1] == '_' { 217 continue 218 } 219 220 if k == "nil" { 221 return nil, nil 222 } 223 224 // This key is used for other purposes 225 if k == "compute_value" { 226 continue 227 } 228 229 if k == "compute" { 230 attrDiff := &ResourceAttrDiff{ 231 Old: "", 232 New: "", 233 NewComputed: true, 234 } 235 236 if cv, ok := c.Config["compute_value"]; ok { 237 if cv.(string) == "1" { 238 attrDiff.NewComputed = false 239 attrDiff.New = fmt.Sprintf("computed_%s", v.(string)) 240 } 241 } 242 243 diff.Attributes[v.(string)] = attrDiff 244 continue 245 } 246 247 // If this key is not computed, then look it up in the 248 // cleaned config. 249 found := false 250 for _, ck := range c.ComputedKeys { 251 if ck == k { 252 found = true 253 break 254 } 255 } 256 if !found { 257 v = c.Config[k] 258 } 259 260 for k, attrDiff := range testFlatAttrDiffs(k, v) { 261 if k == "require_new" { 262 attrDiff.RequiresNew = true 263 } 264 if _, ok := c.Raw["__"+k+"_requires_new"]; ok { 265 attrDiff.RequiresNew = true 266 } 267 268 if attr, ok := s.Attributes[k]; ok { 269 attrDiff.Old = attr 270 } 271 272 diff.Attributes[k] = attrDiff 273 } 274 } 275 276 for _, k := range c.ComputedKeys { 277 diff.Attributes[k] = &ResourceAttrDiff{ 278 Old: "", 279 NewComputed: true, 280 } 281 } 282 283 // If we recreate this resource because it's tainted, we keep all attrs 284 if !diff.RequiresNew() { 285 for k, v := range diff.Attributes { 286 if v.NewComputed { 287 continue 288 } 289 290 old, ok := s.Attributes[k] 291 if !ok { 292 continue 293 } 294 295 if old == v.New { 296 delete(diff.Attributes, k) 297 } 298 } 299 } 300 301 if !diff.Empty() { 302 diff.Attributes["type"] = &ResourceAttrDiff{ 303 Old: "", 304 New: info.Type, 305 } 306 } 307 308 return diff, nil 309 } 310 311 // generate ResourceAttrDiffs for nested data structures in tests 312 func testFlatAttrDiffs(k string, i interface{}) map[string]*ResourceAttrDiff { 313 diffs := make(map[string]*ResourceAttrDiff) 314 // check for strings and empty containers first 315 switch t := i.(type) { 316 case string: 317 diffs[k] = &ResourceAttrDiff{New: t} 318 return diffs 319 case map[string]interface{}: 320 if len(t) == 0 { 321 diffs[k] = &ResourceAttrDiff{New: ""} 322 return diffs 323 } 324 case []interface{}: 325 if len(t) == 0 { 326 diffs[k] = &ResourceAttrDiff{New: ""} 327 return diffs 328 } 329 } 330 331 flat := flatmap.Flatten(map[string]interface{}{k: i}) 332 333 for k, v := range flat { 334 attrDiff := &ResourceAttrDiff{ 335 Old: "", 336 New: v, 337 } 338 diffs[k] = attrDiff 339 } 340 341 return diffs 342 } 343 344 func testProvider(prefix string) *MockResourceProvider { 345 p := new(MockResourceProvider) 346 p.RefreshFn = func(info *InstanceInfo, s *InstanceState) (*InstanceState, error) { 347 return s, nil 348 } 349 p.ResourcesReturn = []ResourceType{ 350 ResourceType{ 351 Name: fmt.Sprintf("%s_instance", prefix), 352 }, 353 } 354 355 return p 356 } 357 358 func testProvisioner() *MockResourceProvisioner { 359 p := new(MockResourceProvisioner) 360 return p 361 } 362 363 func checkStateString(t *testing.T, state *State, expected string) { 364 t.Helper() 365 actual := strings.TrimSpace(state.String()) 366 expected = strings.TrimSpace(expected) 367 368 if actual != expected { 369 t.Fatalf("state does not match! actual:\n%s\n\nexpected:\n%s", actual, expected) 370 } 371 } 372 373 func resourceState(resourceType, resourceID string) *ResourceState { 374 return &ResourceState{ 375 Type: resourceType, 376 Primary: &InstanceState{ 377 ID: resourceID, 378 Attributes: map[string]string{ 379 "id": resourceID, 380 }, 381 }, 382 } 383 } 384 385 // Test helper that gives a function 3 seconds to finish, assumes deadlock and 386 // fails test if it does not. 387 func testCheckDeadlock(t *testing.T, f func()) { 388 t.Helper() 389 timeout := make(chan bool, 1) 390 done := make(chan bool, 1) 391 go func() { 392 time.Sleep(3 * time.Second) 393 timeout <- true 394 }() 395 go func(f func(), done chan bool) { 396 defer func() { done <- true }() 397 f() 398 }(f, done) 399 select { 400 case <-timeout: 401 t.Fatalf("timed out! probably deadlock") 402 case <-done: 403 // ok 404 } 405 } 406 407 const testContextGraph = ` 408 root: root 409 aws_instance.bar 410 aws_instance.bar -> provider.aws 411 aws_instance.foo 412 aws_instance.foo -> provider.aws 413 provider.aws 414 root 415 root -> aws_instance.bar 416 root -> aws_instance.foo 417 ` 418 419 const testContextRefreshModuleStr = ` 420 aws_instance.web: (tainted) 421 ID = bar 422 provider = provider.aws 423 424 module.child: 425 aws_instance.web: 426 ID = new 427 provider = provider.aws 428 ` 429 430 const testContextRefreshOutputStr = ` 431 aws_instance.web: 432 ID = foo 433 provider = provider.aws 434 foo = bar 435 436 Outputs: 437 438 foo = bar 439 ` 440 441 const testContextRefreshOutputPartialStr = ` 442 <no state> 443 ` 444 445 const testContextRefreshTaintedStr = ` 446 aws_instance.web: (tainted) 447 ID = foo 448 provider = provider.aws 449 `