github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/command/views/hook_ui_test.go (about) 1 package views 2 3 import ( 4 "fmt" 5 "regexp" 6 "testing" 7 "time" 8 9 "strings" 10 11 "github.com/zclconf/go-cty/cty" 12 13 "github.com/hashicorp/terraform/internal/addrs" 14 "github.com/hashicorp/terraform/internal/command/arguments" 15 "github.com/hashicorp/terraform/internal/plans" 16 "github.com/hashicorp/terraform/internal/providers" 17 "github.com/hashicorp/terraform/internal/states" 18 "github.com/hashicorp/terraform/internal/terminal" 19 "github.com/hashicorp/terraform/internal/terraform" 20 ) 21 22 // Test the PreApply hook for creating a new resource 23 func TestUiHookPreApply_create(t *testing.T) { 24 streams, done := terminal.StreamsForTesting(t) 25 view := NewView(streams) 26 h := NewUiHook(view) 27 h.resources = map[string]uiResourceState{ 28 "test_instance.foo": { 29 Op: uiResourceCreate, 30 Start: time.Now(), 31 }, 32 } 33 34 addr := addrs.Resource{ 35 Mode: addrs.ManagedResourceMode, 36 Type: "test_instance", 37 Name: "foo", 38 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) 39 40 priorState := cty.NullVal(cty.Object(map[string]cty.Type{ 41 "id": cty.String, 42 "bar": cty.List(cty.String), 43 })) 44 plannedNewState := cty.ObjectVal(map[string]cty.Value{ 45 "id": cty.StringVal("test"), 46 "bar": cty.ListVal([]cty.Value{ 47 cty.StringVal("baz"), 48 }), 49 }) 50 51 action, err := h.PreApply(addr, states.CurrentGen, plans.Create, priorState, plannedNewState) 52 if err != nil { 53 t.Fatal(err) 54 } 55 if action != terraform.HookActionContinue { 56 t.Fatalf("Expected hook to continue, given: %#v", action) 57 } 58 59 // stop the background writer 60 uiState := h.resources[addr.String()] 61 close(uiState.DoneCh) 62 <-uiState.done 63 64 expectedOutput := "test_instance.foo: Creating...\n" 65 result := done(t) 66 output := result.Stdout() 67 if output != expectedOutput { 68 t.Fatalf("Output didn't match.\nExpected: %q\nGiven: %q", expectedOutput, output) 69 } 70 71 expectedErrOutput := "" 72 errOutput := result.Stderr() 73 if errOutput != expectedErrOutput { 74 t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput) 75 } 76 } 77 78 // Test the PreApply hook's use of a periodic timer to display "still working" 79 // log lines 80 func TestUiHookPreApply_periodicTimer(t *testing.T) { 81 streams, done := terminal.StreamsForTesting(t) 82 view := NewView(streams) 83 h := NewUiHook(view) 84 h.periodicUiTimer = 1 * time.Second 85 h.resources = map[string]uiResourceState{ 86 "test_instance.foo": { 87 Op: uiResourceModify, 88 Start: time.Now(), 89 }, 90 } 91 92 addr := addrs.Resource{ 93 Mode: addrs.ManagedResourceMode, 94 Type: "test_instance", 95 Name: "foo", 96 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) 97 98 priorState := cty.ObjectVal(map[string]cty.Value{ 99 "id": cty.StringVal("test"), 100 "bar": cty.ListValEmpty(cty.String), 101 }) 102 plannedNewState := cty.ObjectVal(map[string]cty.Value{ 103 "id": cty.StringVal("test"), 104 "bar": cty.ListVal([]cty.Value{ 105 cty.StringVal("baz"), 106 }), 107 }) 108 109 action, err := h.PreApply(addr, states.CurrentGen, plans.Update, priorState, plannedNewState) 110 if err != nil { 111 t.Fatal(err) 112 } 113 if action != terraform.HookActionContinue { 114 t.Fatalf("Expected hook to continue, given: %#v", action) 115 } 116 117 time.Sleep(3100 * time.Millisecond) 118 119 // stop the background writer 120 uiState := h.resources[addr.String()] 121 close(uiState.DoneCh) 122 <-uiState.done 123 124 expectedOutput := `test_instance.foo: Modifying... [id=test] 125 test_instance.foo: Still modifying... [id=test, 1s elapsed] 126 test_instance.foo: Still modifying... [id=test, 2s elapsed] 127 test_instance.foo: Still modifying... [id=test, 3s elapsed] 128 ` 129 result := done(t) 130 output := result.Stdout() 131 if output != expectedOutput { 132 t.Fatalf("Output didn't match.\nExpected: %q\nGiven: %q", expectedOutput, output) 133 } 134 135 expectedErrOutput := "" 136 errOutput := result.Stderr() 137 if errOutput != expectedErrOutput { 138 t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput) 139 } 140 } 141 142 // Test the PreApply hook's destroy path, including passing a deposed key as 143 // the gen argument. 144 func TestUiHookPreApply_destroy(t *testing.T) { 145 streams, done := terminal.StreamsForTesting(t) 146 view := NewView(streams) 147 h := NewUiHook(view) 148 h.resources = map[string]uiResourceState{ 149 "test_instance.foo": { 150 Op: uiResourceDestroy, 151 Start: time.Now(), 152 }, 153 } 154 155 addr := addrs.Resource{ 156 Mode: addrs.ManagedResourceMode, 157 Type: "test_instance", 158 Name: "foo", 159 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) 160 161 priorState := cty.ObjectVal(map[string]cty.Value{ 162 "id": cty.StringVal("abc123"), 163 "verbs": cty.ListVal([]cty.Value{ 164 cty.StringVal("boop"), 165 }), 166 }) 167 plannedNewState := cty.NullVal(cty.Object(map[string]cty.Type{ 168 "id": cty.String, 169 "verbs": cty.List(cty.String), 170 })) 171 172 key := states.NewDeposedKey() 173 action, err := h.PreApply(addr, key, plans.Delete, priorState, plannedNewState) 174 if err != nil { 175 t.Fatal(err) 176 } 177 if action != terraform.HookActionContinue { 178 t.Fatalf("Expected hook to continue, given: %#v", action) 179 } 180 181 // stop the background writer 182 uiState := h.resources[addr.String()] 183 close(uiState.DoneCh) 184 <-uiState.done 185 186 result := done(t) 187 expectedOutput := fmt.Sprintf("test_instance.foo (deposed object %s): Destroying... [id=abc123]\n", key) 188 output := result.Stdout() 189 if output != expectedOutput { 190 t.Fatalf("Output didn't match.\nExpected: %q\nGiven: %q", expectedOutput, output) 191 } 192 193 expectedErrOutput := "" 194 errOutput := result.Stderr() 195 if errOutput != expectedErrOutput { 196 t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput) 197 } 198 } 199 200 // Verify that colorize is called on format strings, not user input, by adding 201 // valid color codes as resource names and IDs. 202 func TestUiHookPostApply_colorInterpolation(t *testing.T) { 203 streams, done := terminal.StreamsForTesting(t) 204 view := NewView(streams) 205 view.Configure(&arguments.View{NoColor: false}) 206 h := NewUiHook(view) 207 h.resources = map[string]uiResourceState{ 208 "test_instance.foo[\"[red]\"]": { 209 Op: uiResourceCreate, 210 Start: time.Now(), 211 }, 212 } 213 214 addr := addrs.Resource{ 215 Mode: addrs.ManagedResourceMode, 216 Type: "test_instance", 217 Name: "foo", 218 }.Instance(addrs.StringKey("[red]")).Absolute(addrs.RootModuleInstance) 219 220 newState := cty.ObjectVal(map[string]cty.Value{ 221 "id": cty.StringVal("[blue]"), 222 }) 223 224 action, err := h.PostApply(addr, states.CurrentGen, newState, nil) 225 if err != nil { 226 t.Fatal(err) 227 } 228 if action != terraform.HookActionContinue { 229 t.Fatalf("Expected hook to continue, given: %#v", action) 230 } 231 result := done(t) 232 233 reset := "\x1b[0m" 234 bold := "\x1b[1m" 235 wantPrefix := reset + bold + `test_instance.foo["[red]"]: Creation complete after` 236 wantSuffix := "[id=[blue]]" + reset + "\n" 237 output := result.Stdout() 238 239 if !strings.HasPrefix(output, wantPrefix) { 240 t.Fatalf("wrong output prefix\n got: %#v\nwant: %#v", output, wantPrefix) 241 } 242 243 if !strings.HasSuffix(output, wantSuffix) { 244 t.Fatalf("wrong output suffix\n got: %#v\nwant: %#v", output, wantSuffix) 245 } 246 247 expectedErrOutput := "" 248 errOutput := result.Stderr() 249 if errOutput != expectedErrOutput { 250 t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput) 251 } 252 } 253 254 // Test that the PostApply hook renders a total time. 255 func TestUiHookPostApply_emptyState(t *testing.T) { 256 streams, done := terminal.StreamsForTesting(t) 257 view := NewView(streams) 258 h := NewUiHook(view) 259 h.resources = map[string]uiResourceState{ 260 "data.google_compute_zones.available": { 261 Op: uiResourceDestroy, 262 Start: time.Now(), 263 }, 264 } 265 266 addr := addrs.Resource{ 267 Mode: addrs.DataResourceMode, 268 Type: "google_compute_zones", 269 Name: "available", 270 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) 271 272 newState := cty.NullVal(cty.Object(map[string]cty.Type{ 273 "id": cty.String, 274 "names": cty.List(cty.String), 275 })) 276 277 action, err := h.PostApply(addr, states.CurrentGen, newState, nil) 278 if err != nil { 279 t.Fatal(err) 280 } 281 if action != terraform.HookActionContinue { 282 t.Fatalf("Expected hook to continue, given: %#v", action) 283 } 284 result := done(t) 285 286 expectedRegexp := "^data.google_compute_zones.available: Destruction complete after -?[a-z0-9µ.]+\n$" 287 output := result.Stdout() 288 if matched, _ := regexp.MatchString(expectedRegexp, output); !matched { 289 t.Fatalf("Output didn't match regexp.\nExpected: %q\nGiven: %q", expectedRegexp, output) 290 } 291 292 expectedErrOutput := "" 293 errOutput := result.Stderr() 294 if errOutput != expectedErrOutput { 295 t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput) 296 } 297 } 298 299 func TestPreProvisionInstanceStep(t *testing.T) { 300 streams, done := terminal.StreamsForTesting(t) 301 view := NewView(streams) 302 h := NewUiHook(view) 303 304 addr := addrs.Resource{ 305 Mode: addrs.ManagedResourceMode, 306 Type: "test_instance", 307 Name: "foo", 308 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) 309 310 action, err := h.PreProvisionInstanceStep(addr, "local-exec") 311 if err != nil { 312 t.Fatal(err) 313 } 314 if action != terraform.HookActionContinue { 315 t.Fatalf("Expected hook to continue, given: %#v", action) 316 } 317 result := done(t) 318 319 if got, want := result.Stdout(), "test_instance.foo: Provisioning with 'local-exec'...\n"; got != want { 320 t.Fatalf("unexpected output\n got: %q\nwant: %q", got, want) 321 } 322 } 323 324 // Test ProvisionOutput, including lots of edge cases for the output 325 // whitespace/line ending logic. 326 func TestProvisionOutput(t *testing.T) { 327 addr := addrs.Resource{ 328 Mode: addrs.ManagedResourceMode, 329 Type: "test_instance", 330 Name: "foo", 331 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) 332 333 testCases := map[string]struct { 334 provisioner string 335 input string 336 wantOutput string 337 }{ 338 "single line": { 339 "local-exec", 340 "foo\n", 341 "test_instance.foo (local-exec): foo\n", 342 }, 343 "multiple lines": { 344 "x", 345 `foo 346 bar 347 baz 348 `, 349 `test_instance.foo (x): foo 350 test_instance.foo (x): bar 351 test_instance.foo (x): baz 352 `, 353 }, 354 "trailing whitespace": { 355 "x", 356 "foo \nbar\n", 357 "test_instance.foo (x): foo\ntest_instance.foo (x): bar\n", 358 }, 359 "blank lines": { 360 "x", 361 "foo\n\nbar\n\n\nbaz\n", 362 `test_instance.foo (x): foo 363 test_instance.foo (x): bar 364 test_instance.foo (x): baz 365 `, 366 }, 367 "no final newline": { 368 "x", 369 `foo 370 bar`, 371 `test_instance.foo (x): foo 372 test_instance.foo (x): bar 373 `, 374 }, 375 "CR, no LF": { 376 "MacOS 9?", 377 "foo\rbar\r", 378 `test_instance.foo (MacOS 9?): foo 379 test_instance.foo (MacOS 9?): bar 380 `, 381 }, 382 "CRLF": { 383 "winrm", 384 "foo\r\nbar\r\n", 385 `test_instance.foo (winrm): foo 386 test_instance.foo (winrm): bar 387 `, 388 }, 389 } 390 391 for name, tc := range testCases { 392 t.Run(name, func(t *testing.T) { 393 streams, done := terminal.StreamsForTesting(t) 394 view := NewView(streams) 395 h := NewUiHook(view) 396 397 h.ProvisionOutput(addr, tc.provisioner, tc.input) 398 result := done(t) 399 400 if got := result.Stdout(); got != tc.wantOutput { 401 t.Fatalf("unexpected output\n got: %q\nwant: %q", got, tc.wantOutput) 402 } 403 }) 404 } 405 } 406 407 // Test the PreRefresh hook in the normal path where the resource exists with 408 // an ID key and value in the state. 409 func TestPreRefresh(t *testing.T) { 410 streams, done := terminal.StreamsForTesting(t) 411 view := NewView(streams) 412 h := NewUiHook(view) 413 414 addr := addrs.Resource{ 415 Mode: addrs.ManagedResourceMode, 416 Type: "test_instance", 417 Name: "foo", 418 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) 419 420 priorState := cty.ObjectVal(map[string]cty.Value{ 421 "id": cty.StringVal("test"), 422 "bar": cty.ListValEmpty(cty.String), 423 }) 424 425 action, err := h.PreRefresh(addr, states.CurrentGen, priorState) 426 427 if err != nil { 428 t.Fatal(err) 429 } 430 if action != terraform.HookActionContinue { 431 t.Fatalf("Expected hook to continue, given: %#v", action) 432 } 433 result := done(t) 434 435 if got, want := result.Stdout(), "test_instance.foo: Refreshing state... [id=test]\n"; got != want { 436 t.Fatalf("unexpected output\n got: %q\nwant: %q", got, want) 437 } 438 } 439 440 // Test that PreRefresh still works if no ID key and value can be determined 441 // from state. 442 func TestPreRefresh_noID(t *testing.T) { 443 streams, done := terminal.StreamsForTesting(t) 444 view := NewView(streams) 445 h := NewUiHook(view) 446 447 addr := addrs.Resource{ 448 Mode: addrs.ManagedResourceMode, 449 Type: "test_instance", 450 Name: "foo", 451 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) 452 453 priorState := cty.ObjectVal(map[string]cty.Value{ 454 "bar": cty.ListValEmpty(cty.String), 455 }) 456 457 action, err := h.PreRefresh(addr, states.CurrentGen, priorState) 458 459 if err != nil { 460 t.Fatal(err) 461 } 462 if action != terraform.HookActionContinue { 463 t.Fatalf("Expected hook to continue, given: %#v", action) 464 } 465 result := done(t) 466 467 if got, want := result.Stdout(), "test_instance.foo: Refreshing state...\n"; got != want { 468 t.Fatalf("unexpected output\n got: %q\nwant: %q", got, want) 469 } 470 } 471 472 // Test the very simple PreImportState hook. 473 func TestPreImportState(t *testing.T) { 474 streams, done := terminal.StreamsForTesting(t) 475 view := NewView(streams) 476 h := NewUiHook(view) 477 478 addr := addrs.Resource{ 479 Mode: addrs.ManagedResourceMode, 480 Type: "test_instance", 481 Name: "foo", 482 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) 483 484 action, err := h.PreImportState(addr, "test") 485 486 if err != nil { 487 t.Fatal(err) 488 } 489 if action != terraform.HookActionContinue { 490 t.Fatalf("Expected hook to continue, given: %#v", action) 491 } 492 result := done(t) 493 494 if got, want := result.Stdout(), "test_instance.foo: Importing from ID \"test\"...\n"; got != want { 495 t.Fatalf("unexpected output\n got: %q\nwant: %q", got, want) 496 } 497 } 498 499 // Test the PostImportState UI hook. Again, this hook behaviour seems odd to 500 // me (see below), so please don't consider these tests as justification for 501 // keeping this behaviour. 502 func TestPostImportState(t *testing.T) { 503 streams, done := terminal.StreamsForTesting(t) 504 view := NewView(streams) 505 h := NewUiHook(view) 506 507 addr := addrs.Resource{ 508 Mode: addrs.ManagedResourceMode, 509 Type: "test_instance", 510 Name: "foo", 511 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) 512 513 // The "Prepared [...] for import" lines display the type name of each of 514 // the imported resources passed to the hook. I'm not sure how it's 515 // possible for an import to result in a different resource type name than 516 // the target address, but the hook works like this so we're covering it. 517 imported := []providers.ImportedResource{ 518 { 519 TypeName: "test_some_instance", 520 State: cty.ObjectVal(map[string]cty.Value{ 521 "id": cty.StringVal("test"), 522 }), 523 }, 524 { 525 TypeName: "test_other_instance", 526 State: cty.ObjectVal(map[string]cty.Value{ 527 "id": cty.StringVal("test"), 528 }), 529 }, 530 } 531 532 action, err := h.PostImportState(addr, imported) 533 534 if err != nil { 535 t.Fatal(err) 536 } 537 if action != terraform.HookActionContinue { 538 t.Fatalf("Expected hook to continue, given: %#v", action) 539 } 540 result := done(t) 541 542 want := `test_instance.foo: Import prepared! 543 Prepared test_some_instance for import 544 Prepared test_other_instance for import 545 ` 546 if got := result.Stdout(); got != want { 547 t.Fatalf("unexpected output\n got: %q\nwant: %q", got, want) 548 } 549 } 550 551 func TestTruncateId(t *testing.T) { 552 testCases := []struct { 553 Input string 554 Expected string 555 MaxLen int 556 }{ 557 { 558 Input: "Hello world", 559 Expected: "H...d", 560 MaxLen: 3, 561 }, 562 { 563 Input: "Hello world", 564 Expected: "H...d", 565 MaxLen: 5, 566 }, 567 { 568 Input: "Hello world", 569 Expected: "He...d", 570 MaxLen: 6, 571 }, 572 { 573 Input: "Hello world", 574 Expected: "He...ld", 575 MaxLen: 7, 576 }, 577 { 578 Input: "Hello world", 579 Expected: "Hel...ld", 580 MaxLen: 8, 581 }, 582 { 583 Input: "Hello world", 584 Expected: "Hel...rld", 585 MaxLen: 9, 586 }, 587 { 588 Input: "Hello world", 589 Expected: "Hell...rld", 590 MaxLen: 10, 591 }, 592 { 593 Input: "Hello world", 594 Expected: "Hello world", 595 MaxLen: 11, 596 }, 597 { 598 Input: "Hello world", 599 Expected: "Hello world", 600 MaxLen: 12, 601 }, 602 { 603 Input: "あいうえおかきくけこさ", 604 Expected: "あ...さ", 605 MaxLen: 3, 606 }, 607 { 608 Input: "あいうえおかきくけこさ", 609 Expected: "あ...さ", 610 MaxLen: 5, 611 }, 612 { 613 Input: "あいうえおかきくけこさ", 614 Expected: "あい...さ", 615 MaxLen: 6, 616 }, 617 { 618 Input: "あいうえおかきくけこさ", 619 Expected: "あい...こさ", 620 MaxLen: 7, 621 }, 622 { 623 Input: "あいうえおかきくけこさ", 624 Expected: "あいう...こさ", 625 MaxLen: 8, 626 }, 627 { 628 Input: "あいうえおかきくけこさ", 629 Expected: "あいう...けこさ", 630 MaxLen: 9, 631 }, 632 { 633 Input: "あいうえおかきくけこさ", 634 Expected: "あいうえ...けこさ", 635 MaxLen: 10, 636 }, 637 { 638 Input: "あいうえおかきくけこさ", 639 Expected: "あいうえおかきくけこさ", 640 MaxLen: 11, 641 }, 642 { 643 Input: "あいうえおかきくけこさ", 644 Expected: "あいうえおかきくけこさ", 645 MaxLen: 12, 646 }, 647 } 648 for i, tc := range testCases { 649 testName := fmt.Sprintf("%d", i) 650 t.Run(testName, func(t *testing.T) { 651 out := truncateId(tc.Input, tc.MaxLen) 652 if out != tc.Expected { 653 t.Fatalf("Expected %q to be shortened to %d as %q (given: %q)", 654 tc.Input, tc.MaxLen, tc.Expected, out) 655 } 656 }) 657 } 658 }