| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package views | |
| import ( | |
| "errors" | |
| "fmt" | |
| "regexp" | |
| "testing" | |
| "time" | |
| "strings" | |
| "github.com/zclconf/go-cty/cty" | |
| "github.com/hashicorp/terraform/internal/addrs" | |
| "github.com/hashicorp/terraform/internal/command/arguments" | |
| "github.com/hashicorp/terraform/internal/plans" | |
| "github.com/hashicorp/terraform/internal/providers" | |
| "github.com/hashicorp/terraform/internal/states" | |
| "github.com/hashicorp/terraform/internal/terminal" | |
| "github.com/hashicorp/terraform/internal/terraform" | |
| ) | |
| func testUiHookResourceID(addr addrs.AbsResourceInstance) terraform.HookResourceIdentity { | |
| return terraform.HookResourceIdentity{ | |
| Addr: addr, | |
| ProviderAddr: addrs.Provider{ | |
| Type: "test", | |
| Namespace: "hashicorp", | |
| Hostname: "example.com", | |
| }, | |
| } | |
| } | |
| // Test the PreApply hook for creating a new resource | |
| func TestUiHookPreApply_create(t *testing.T) { | |
| streams, done := terminal.StreamsForTesting(t) | |
| view := NewView(streams) | |
| h := NewUiHook(view) | |
| h.resources = map[string]uiResourceState{ | |
| "test_instance.foo": { | |
| Op: uiResourceCreate, | |
| Start: time.Now(), | |
| }, | |
| } | |
| addr := addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "foo", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) | |
| priorState := cty.NullVal(cty.Object(map[string]cty.Type{ | |
| "id": cty.String, | |
| "bar": cty.List(cty.String), | |
| })) | |
| plannedNewState := cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("test"), | |
| "bar": cty.ListVal([]cty.Value{ | |
| cty.StringVal("baz"), | |
| }), | |
| }) | |
| action, err := h.PreApply(testUiHookResourceID(addr), addrs.NotDeposed, plans.Create, priorState, plannedNewState) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if action != terraform.HookActionContinue { | |
| t.Fatalf("Expected hook to continue, given: %#v", action) | |
| } | |
| // stop the background writer | |
| uiState := h.resources[addr.String()] | |
| close(uiState.DoneCh) | |
| <-uiState.done | |
| expectedOutput := "test_instance.foo: Creating...\n" | |
| result := done(t) | |
| output := result.Stdout() | |
| if output != expectedOutput { | |
| t.Fatalf("Output didn't match.\nExpected: %q\nGiven: %q", expectedOutput, output) | |
| } | |
| expectedErrOutput := "" | |
| errOutput := result.Stderr() | |
| if errOutput != expectedErrOutput { | |
| t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput) | |
| } | |
| } | |
| // Test the PreApply hook's use of a periodic timer to display "still working" | |
| // log lines | |
| func TestUiHookPreApply_periodicTimer(t *testing.T) { | |
| syncTest, streams, done := streamableSyncTest(t) | |
| syncTest(t, func(t *testing.T) { | |
| view := NewView(streams) | |
| h := NewUiHook(view) | |
| h.periodicUiTimer = 1 * time.Second | |
| h.resources = map[string]uiResourceState{ | |
| "test_instance.foo": { | |
| Op: uiResourceModify, | |
| Start: time.Now(), | |
| }, | |
| } | |
| addr := addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "foo", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) | |
| priorState := cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("test"), | |
| "bar": cty.ListValEmpty(cty.String), | |
| }) | |
| plannedNewState := cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("test"), | |
| "bar": cty.ListVal([]cty.Value{ | |
| cty.StringVal("baz"), | |
| }), | |
| }) | |
| action, err := h.PreApply(testUiHookResourceID(addr), addrs.NotDeposed, plans.Update, priorState, plannedNewState) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if action != terraform.HookActionContinue { | |
| t.Fatalf("Expected hook to continue, given: %#v", action) | |
| } | |
| time.Sleep(3005 * time.Millisecond) | |
| // stop the background writer | |
| uiState := h.resources[addr.String()] | |
| close(uiState.DoneCh) | |
| <-uiState.done | |
| expectedOutput := `test_instance.foo: Modifying... [id=test] | |
| test_instance.foo: Still modifying... [id=test, 00m01s elapsed] | |
| test_instance.foo: Still modifying... [id=test, 00m02s elapsed] | |
| test_instance.foo: Still modifying... [id=test, 00m03s elapsed] | |
| ` | |
| result := done(t) | |
| output := result.Stdout() | |
| if output != expectedOutput { | |
| t.Fatalf("Output didn't match.\nExpected: %q\nGiven: %q", expectedOutput, output) | |
| } | |
| expectedErrOutput := "" | |
| errOutput := result.Stderr() | |
| if errOutput != expectedErrOutput { | |
| t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput) | |
| } | |
| }) | |
| } | |
| // Test the PreApply hook's destroy path, including passing a deposed key as | |
| // the gen argument. | |
| func TestUiHookPreApply_destroy(t *testing.T) { | |
| streams, done := terminal.StreamsForTesting(t) | |
| view := NewView(streams) | |
| h := NewUiHook(view) | |
| h.resources = map[string]uiResourceState{ | |
| "test_instance.foo": { | |
| Op: uiResourceDestroy, | |
| Start: time.Now(), | |
| }, | |
| } | |
| addr := addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "foo", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) | |
| priorState := cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("abc123"), | |
| "verbs": cty.ListVal([]cty.Value{ | |
| cty.StringVal("boop"), | |
| }), | |
| }) | |
| plannedNewState := cty.NullVal(cty.Object(map[string]cty.Type{ | |
| "id": cty.String, | |
| "verbs": cty.List(cty.String), | |
| })) | |
| key := states.NewDeposedKey() | |
| action, err := h.PreApply(testUiHookResourceID(addr), key, plans.Delete, priorState, plannedNewState) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if action != terraform.HookActionContinue { | |
| t.Fatalf("Expected hook to continue, given: %#v", action) | |
| } | |
| // stop the background writer | |
| uiState := h.resources[addr.String()] | |
| close(uiState.DoneCh) | |
| <-uiState.done | |
| result := done(t) | |
| expectedOutput := fmt.Sprintf("test_instance.foo (deposed object %s): Destroying... [id=abc123]\n", key) | |
| output := result.Stdout() | |
| if output != expectedOutput { | |
| t.Fatalf("Output didn't match.\nExpected: %q\nGiven: %q", expectedOutput, output) | |
| } | |
| expectedErrOutput := "" | |
| errOutput := result.Stderr() | |
| if errOutput != expectedErrOutput { | |
| t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput) | |
| } | |
| } | |
| // Verify that colorize is called on format strings, not user input, by adding | |
| // valid color codes as resource names and IDs. | |
| func TestUiHookPostApply_colorInterpolation(t *testing.T) { | |
| streams, done := terminal.StreamsForTesting(t) | |
| view := NewView(streams) | |
| view.Configure(&arguments.View{NoColor: false}) | |
| h := NewUiHook(view) | |
| h.resources = map[string]uiResourceState{ | |
| "test_instance.foo[\"[red]\"]": { | |
| Op: uiResourceCreate, | |
| Start: time.Now(), | |
| }, | |
| } | |
| addr := addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "foo", | |
| }.Instance(addrs.StringKey("[red]")).Absolute(addrs.RootModuleInstance) | |
| newState := cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("[blue]"), | |
| }) | |
| action, err := h.PostApply(testUiHookResourceID(addr), addrs.NotDeposed, newState, nil) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if action != terraform.HookActionContinue { | |
| t.Fatalf("Expected hook to continue, given: %#v", action) | |
| } | |
| result := done(t) | |
| reset := "\x1b[0m" | |
| bold := "\x1b[1m" | |
| wantPrefix := reset + bold + `test_instance.foo["[red]"]: Creation complete after` | |
| wantSuffix := "[id=[blue]]" + reset + "\n" | |
| output := result.Stdout() | |
| if !strings.HasPrefix(output, wantPrefix) { | |
| t.Fatalf("wrong output prefix\n got: %#v\nwant: %#v", output, wantPrefix) | |
| } | |
| if !strings.HasSuffix(output, wantSuffix) { | |
| t.Fatalf("wrong output suffix\n got: %#v\nwant: %#v", output, wantSuffix) | |
| } | |
| expectedErrOutput := "" | |
| errOutput := result.Stderr() | |
| if errOutput != expectedErrOutput { | |
| t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput) | |
| } | |
| } | |
| // Test that the PostApply hook renders a total time. | |
| func TestUiHookPostApply_emptyState(t *testing.T) { | |
| streams, done := terminal.StreamsForTesting(t) | |
| view := NewView(streams) | |
| h := NewUiHook(view) | |
| h.resources = map[string]uiResourceState{ | |
| "data.google_compute_zones.available": { | |
| Op: uiResourceDestroy, | |
| Start: time.Now(), | |
| }, | |
| } | |
| addr := addrs.Resource{ | |
| Mode: addrs.DataResourceMode, | |
| Type: "google_compute_zones", | |
| Name: "available", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) | |
| newState := cty.NullVal(cty.Object(map[string]cty.Type{ | |
| "id": cty.String, | |
| "names": cty.List(cty.String), | |
| })) | |
| action, err := h.PostApply(testUiHookResourceID(addr), addrs.NotDeposed, newState, nil) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if action != terraform.HookActionContinue { | |
| t.Fatalf("Expected hook to continue, given: %#v", action) | |
| } | |
| result := done(t) | |
| expectedRegexp := "^data.google_compute_zones.available: Destruction complete after -?[a-z0-9µ.]+\n$" | |
| output := result.Stdout() | |
| if matched, _ := regexp.MatchString(expectedRegexp, output); !matched { | |
| t.Fatalf("Output didn't match regexp.\nExpected: %q\nGiven: %q", expectedRegexp, output) | |
| } | |
| expectedErrOutput := "" | |
| errOutput := result.Stderr() | |
| if errOutput != expectedErrOutput { | |
| t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput) | |
| } | |
| } | |
| func TestUiHookPreProvisionInstanceStep(t *testing.T) { | |
| streams, done := terminal.StreamsForTesting(t) | |
| view := NewView(streams) | |
| h := NewUiHook(view) | |
| addr := addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "foo", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) | |
| action, err := h.PreProvisionInstanceStep(testUiHookResourceID(addr), "local-exec") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if action != terraform.HookActionContinue { | |
| t.Fatalf("Expected hook to continue, given: %#v", action) | |
| } | |
| result := done(t) | |
| if got, want := result.Stdout(), "test_instance.foo: Provisioning with 'local-exec'...\n"; got != want { | |
| t.Fatalf("unexpected output\n got: %q\nwant: %q", got, want) | |
| } | |
| } | |
| // Test ProvisionOutput, including lots of edge cases for the output | |
| // whitespace/line ending logic. | |
| func TestUiHookProvisionOutput(t *testing.T) { | |
| addr := addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "foo", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) | |
| testCases := map[string]struct { | |
| provisioner string | |
| input string | |
| wantOutput string | |
| }{ | |
| "single line": { | |
| "local-exec", | |
| "foo\n", | |
| "test_instance.foo (local-exec): foo\n", | |
| }, | |
| "multiple lines": { | |
| "x", | |
| `foo | |
| bar | |
| baz | |
| `, | |
| `test_instance.foo (x): foo | |
| test_instance.foo (x): bar | |
| test_instance.foo (x): baz | |
| `, | |
| }, | |
| "trailing whitespace": { | |
| "x", | |
| "foo \nbar\n", | |
| "test_instance.foo (x): foo\ntest_instance.foo (x): bar\n", | |
| }, | |
| "blank lines": { | |
| "x", | |
| "foo\n\nbar\n\n\nbaz\n", | |
| `test_instance.foo (x): foo | |
| test_instance.foo (x): bar | |
| test_instance.foo (x): baz | |
| `, | |
| }, | |
| "no final newline": { | |
| "x", | |
| `foo | |
| bar`, | |
| `test_instance.foo (x): foo | |
| test_instance.foo (x): bar | |
| `, | |
| }, | |
| "CR, no LF": { | |
| "MacOS 9?", | |
| "foo\rbar\r", | |
| `test_instance.foo (MacOS 9?): foo | |
| test_instance.foo (MacOS 9?): bar | |
| `, | |
| }, | |
| "CRLF": { | |
| "winrm", | |
| "foo\r\nbar\r\n", | |
| `test_instance.foo (winrm): foo | |
| test_instance.foo (winrm): bar | |
| `, | |
| }, | |
| } | |
| for name, tc := range testCases { | |
| t.Run(name, func(t *testing.T) { | |
| streams, done := terminal.StreamsForTesting(t) | |
| view := NewView(streams) | |
| h := NewUiHook(view) | |
| h.ProvisionOutput(testUiHookResourceID(addr), tc.provisioner, tc.input) | |
| result := done(t) | |
| if got := result.Stdout(); got != tc.wantOutput { | |
| t.Fatalf("unexpected output\n got: %q\nwant: %q", got, tc.wantOutput) | |
| } | |
| }) | |
| } | |
| } | |
| // Test the PreRefresh hook in the normal path where the resource exists with | |
| // an ID key and value in the state. | |
| func TestUiHookPreRefresh(t *testing.T) { | |
| streams, done := terminal.StreamsForTesting(t) | |
| view := NewView(streams) | |
| h := NewUiHook(view) | |
| addr := addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "foo", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) | |
| priorState := cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("test"), | |
| "bar": cty.ListValEmpty(cty.String), | |
| }) | |
| action, err := h.PreRefresh(testUiHookResourceID(addr), addrs.NotDeposed, priorState) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if action != terraform.HookActionContinue { | |
| t.Fatalf("Expected hook to continue, given: %#v", action) | |
| } | |
| result := done(t) | |
| if got, want := result.Stdout(), "test_instance.foo: Refreshing state... [id=test]\n"; got != want { | |
| t.Fatalf("unexpected output\n got: %q\nwant: %q", got, want) | |
| } | |
| } | |
| // Test that PreRefresh still works if no ID key and value can be determined | |
| // from state. | |
| func TestUiHookPreRefresh_noID(t *testing.T) { | |
| streams, done := terminal.StreamsForTesting(t) | |
| view := NewView(streams) | |
| h := NewUiHook(view) | |
| addr := addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "foo", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) | |
| priorState := cty.ObjectVal(map[string]cty.Value{ | |
| "bar": cty.ListValEmpty(cty.String), | |
| }) | |
| action, err := h.PreRefresh(testUiHookResourceID(addr), addrs.NotDeposed, priorState) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if action != terraform.HookActionContinue { | |
| t.Fatalf("Expected hook to continue, given: %#v", action) | |
| } | |
| result := done(t) | |
| if got, want := result.Stdout(), "test_instance.foo: Refreshing state...\n"; got != want { | |
| t.Fatalf("unexpected output\n got: %q\nwant: %q", got, want) | |
| } | |
| } | |
| // Test the very simple PreImportState hook. | |
| func TestUiHookPreImportState(t *testing.T) { | |
| streams, done := terminal.StreamsForTesting(t) | |
| view := NewView(streams) | |
| h := NewUiHook(view) | |
| addr := addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "foo", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) | |
| action, err := h.PreImportState(testUiHookResourceID(addr), "test") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if action != terraform.HookActionContinue { | |
| t.Fatalf("Expected hook to continue, given: %#v", action) | |
| } | |
| result := done(t) | |
| if got, want := result.Stdout(), "test_instance.foo: Importing from ID \"test\"...\n"; got != want { | |
| t.Fatalf("unexpected output\n got: %q\nwant: %q", got, want) | |
| } | |
| } | |
| func TestUiHookPreApplyImport(t *testing.T) { | |
| testCases := map[string]struct { | |
| importingSrc plans.ImportingSrc | |
| want string | |
| }{ | |
| "id": { | |
| importingSrc: plans.ImportingSrc{ | |
| ID: "test", | |
| }, | |
| want: "test_instance.foo: Importing... [id=test]\n", | |
| }, | |
| "identity": { | |
| importingSrc: plans.ImportingSrc{ | |
| Identity: mustNewDynamicValue( | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("test"), | |
| }), | |
| cty.Object(map[string]cty.Type{ | |
| "id": cty.String, | |
| }), | |
| ), | |
| }, | |
| want: "test_instance.foo: Importing... [identity=id=test]\n", | |
| }, | |
| "identity type error": { | |
| importingSrc: plans.ImportingSrc{ | |
| Identity: mustNewDynamicValue( | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("test"), | |
| }), | |
| cty.DynamicPseudoType, | |
| ), | |
| }, | |
| want: "test_instance.foo: Importing... [identity=(type error)]\n", | |
| }, | |
| } | |
| addr := addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "foo", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) | |
| for name, tc := range testCases { | |
| t.Run(name, func(t *testing.T) { | |
| streams, done := terminal.StreamsForTesting(t) | |
| view := NewView(streams) | |
| h := NewUiHook(view) | |
| action, err := h.PreApplyImport(testUiHookResourceID(addr), tc.importingSrc) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if action != terraform.HookActionContinue { | |
| t.Fatalf("Expected hook to continue, given: %#v", action) | |
| } | |
| result := done(t) | |
| got := result.Stdout() | |
| if got != tc.want { | |
| t.Fatalf("unexpected output\n got: %q\nwant: %q", got, tc.want) | |
| } | |
| }) | |
| } | |
| } | |
| func TestUiHookPostApplyImport(t *testing.T) { | |
| testCases := map[string]struct { | |
| importingSrc plans.ImportingSrc | |
| want string | |
| }{ | |
| "id": { | |
| importingSrc: plans.ImportingSrc{ | |
| ID: "test", | |
| }, | |
| want: "test_instance.foo: Import complete [id=test]\n", | |
| }, | |
| "identity": { | |
| importingSrc: plans.ImportingSrc{ | |
| Identity: mustNewDynamicValue( | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("test"), | |
| }), | |
| cty.Object(map[string]cty.Type{ | |
| "id": cty.String, | |
| }), | |
| ), | |
| }, | |
| want: "test_instance.foo: Import complete [identity=id=test]\n", | |
| }, | |
| "identity type error": { | |
| importingSrc: plans.ImportingSrc{ | |
| Identity: mustNewDynamicValue( | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("test"), | |
| }), | |
| cty.DynamicPseudoType, | |
| ), | |
| }, | |
| want: "test_instance.foo: Import complete [identity=(type error)]\n", | |
| }, | |
| } | |
| addr := addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "foo", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) | |
| for name, tc := range testCases { | |
| t.Run(name, func(t *testing.T) { | |
| streams, done := terminal.StreamsForTesting(t) | |
| view := NewView(streams) | |
| h := NewUiHook(view) | |
| action, err := h.PostApplyImport(testUiHookResourceID(addr), tc.importingSrc) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if action != terraform.HookActionContinue { | |
| t.Fatalf("Expected hook to continue, given: %#v", action) | |
| } | |
| result := done(t) | |
| got := result.Stdout() | |
| if got != tc.want { | |
| t.Fatalf("unexpected output\n got: %q\nwant: %q", got, tc.want) | |
| } | |
| }) | |
| } | |
| } | |
| // Test the PostImportState UI hook. Again, this hook behaviour seems odd to | |
| // me (see below), so please don't consider these tests as justification for | |
| // keeping this behaviour. | |
| func TestUiHookPostImportState(t *testing.T) { | |
| streams, done := terminal.StreamsForTesting(t) | |
| view := NewView(streams) | |
| h := NewUiHook(view) | |
| addr := addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "foo", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) | |
| // The "Prepared [...] for import" lines display the type name of each of | |
| // the imported resources passed to the hook. I'm not sure how it's | |
| // possible for an import to result in a different resource type name than | |
| // the target address, but the hook works like this so we're covering it. | |
| imported := []providers.ImportedResource{ | |
| { | |
| TypeName: "test_some_instance", | |
| State: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("test"), | |
| }), | |
| }, | |
| { | |
| TypeName: "test_other_instance", | |
| State: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("test"), | |
| }), | |
| }, | |
| } | |
| action, err := h.PostImportState(testUiHookResourceID(addr), imported) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if action != terraform.HookActionContinue { | |
| t.Fatalf("Expected hook to continue, given: %#v", action) | |
| } | |
| result := done(t) | |
| want := `test_instance.foo: Import prepared! | |
| Prepared test_some_instance for import | |
| Prepared test_other_instance for import | |
| ` | |
| if got := result.Stdout(); got != want { | |
| t.Fatalf("unexpected output\n got: %q\nwant: %q", got, want) | |
| } | |
| } | |
| func TestUiHookEphemeralOp(t *testing.T) { | |
| streams, done := terminal.StreamsForTesting(t) | |
| view := NewView(streams) | |
| h := NewUiHook(view) | |
| addr := addrs.Resource{ | |
| Mode: addrs.EphemeralResourceMode, | |
| Type: "test_instance", | |
| Name: "foo", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) | |
| action, err := h.PreEphemeralOp(testUiHookResourceID(addr), plans.Close) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if action != terraform.HookActionContinue { | |
| t.Fatalf("Expected hook to continue, given: %#v", action) | |
| } | |
| action, err = h.PostEphemeralOp(testUiHookResourceID(addr), plans.Close, nil) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if action != terraform.HookActionContinue { | |
| t.Fatalf("Expected hook to continue, given: %#v", action) | |
| } | |
| result := done(t) | |
| want := `ephemeral.test_instance.foo: Closing... | |
| ephemeral.test_instance.foo: Closing complete after 0s | |
| ` | |
| if got := result.Stdout(); got != want { | |
| t.Fatalf("unexpected output\n got: %q\nwant: %q", got, want) | |
| } | |
| } | |
| func TestUiHookEphemeralOp_progress(t *testing.T) { | |
| syncTest, streams, done := streamableSyncTest(t) | |
| syncTest(t, func(t *testing.T) { | |
| view := NewView(streams) | |
| h := NewUiHook(view) | |
| h.periodicUiTimer = 1 * time.Second | |
| addr := addrs.Resource{ | |
| Mode: addrs.EphemeralResourceMode, | |
| Type: "test_instance", | |
| Name: "foo", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) | |
| action, err := h.PreEphemeralOp(testUiHookResourceID(addr), plans.Open) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if action != terraform.HookActionContinue { | |
| t.Fatalf("Expected hook to continue, given: %#v", action) | |
| } | |
| start := time.Now() | |
| time.Sleep(2005 * time.Millisecond) | |
| elapsed := time.Since(start).Round(time.Second) | |
| action, err = h.PostEphemeralOp(testUiHookResourceID(addr), plans.Open, nil) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if action != terraform.HookActionContinue { | |
| t.Fatalf("Expected hook to continue, given: %#v", action) | |
| } | |
| result := done(t) | |
| stdout := result.Stdout() | |
| // we do not test for equality because time.Sleep can take longer than declared time | |
| wantPrefix := `ephemeral.test_instance.foo: Opening... | |
| ephemeral.test_instance.foo: Still opening... [00m01s elapsed] | |
| ephemeral.test_instance.foo: Still opening... [00m02s elapsed]` | |
| if !strings.HasPrefix(stdout, wantPrefix) { | |
| t.Fatalf("unexpected prefix\n got: %q\nwant: %q", stdout, wantPrefix) | |
| } | |
| wantSuffix := fmt.Sprintf(`ephemeral.test_instance.foo: Opening complete after %s | |
| `, elapsed) | |
| if !strings.HasSuffix(stdout, wantSuffix) { | |
| t.Fatalf("unexpected prefix\n got: %q\nwant: %q", stdout, wantSuffix) | |
| } | |
| }) | |
| } | |
| func TestUiHookEphemeralOp_error(t *testing.T) { | |
| streams, done := terminal.StreamsForTesting(t) | |
| view := NewView(streams) | |
| h := NewUiHook(view) | |
| addr := addrs.Resource{ | |
| Mode: addrs.EphemeralResourceMode, | |
| Type: "test_instance", | |
| Name: "foo", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) | |
| action, err := h.PreEphemeralOp(testUiHookResourceID(addr), plans.Close) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if action != terraform.HookActionContinue { | |
| t.Fatalf("Expected hook to continue, given: %#v", action) | |
| } | |
| action, err = h.PostEphemeralOp(testUiHookResourceID(addr), plans.Close, errors.New("test error")) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if action != terraform.HookActionContinue { | |
| t.Fatalf("Expected hook to continue, given: %#v", action) | |
| } | |
| result := done(t) | |
| want := `ephemeral.test_instance.foo: Closing... | |
| ` | |
| if got := result.Stdout(); got != want { | |
| t.Fatalf("unexpected output\n got: %q\nwant: %q", got, want) | |
| } | |
| } | |
| func TestTruncateId(t *testing.T) { | |
| testCases := []struct { | |
| Input string | |
| Expected string | |
| MaxLen int | |
| }{ | |
| { | |
| Input: "Hello world", | |
| Expected: "H...d", | |
| MaxLen: 3, | |
| }, | |
| { | |
| Input: "Hello world", | |
| Expected: "H...d", | |
| MaxLen: 5, | |
| }, | |
| { | |
| Input: "Hello world", | |
| Expected: "He...d", | |
| MaxLen: 6, | |
| }, | |
| { | |
| Input: "Hello world", | |
| Expected: "He...ld", | |
| MaxLen: 7, | |
| }, | |
| { | |
| Input: "Hello world", | |
| Expected: "Hel...ld", | |
| MaxLen: 8, | |
| }, | |
| { | |
| Input: "Hello world", | |
| Expected: "Hel...rld", | |
| MaxLen: 9, | |
| }, | |
| { | |
| Input: "Hello world", | |
| Expected: "Hell...rld", | |
| MaxLen: 10, | |
| }, | |
| { | |
| Input: "Hello world", | |
| Expected: "Hello world", | |
| MaxLen: 11, | |
| }, | |
| { | |
| Input: "Hello world", | |
| Expected: "Hello world", | |
| MaxLen: 12, | |
| }, | |
| { | |
| Input: "あいうえおかきくけこさ", | |
| Expected: "あ...さ", | |
| MaxLen: 3, | |
| }, | |
| { | |
| Input: "あいうえおかきくけこさ", | |
| Expected: "あ...さ", | |
| MaxLen: 5, | |
| }, | |
| { | |
| Input: "あいうえおかきくけこさ", | |
| Expected: "あい...さ", | |
| MaxLen: 6, | |
| }, | |
| { | |
| Input: "あいうえおかきくけこさ", | |
| Expected: "あい...こさ", | |
| MaxLen: 7, | |
| }, | |
| { | |
| Input: "あいうえおかきくけこさ", | |
| Expected: "あいう...こさ", | |
| MaxLen: 8, | |
| }, | |
| { | |
| Input: "あいうえおかきくけこさ", | |
| Expected: "あいう...けこさ", | |
| MaxLen: 9, | |
| }, | |
| { | |
| Input: "あいうえおかきくけこさ", | |
| Expected: "あいうえ...けこさ", | |
| MaxLen: 10, | |
| }, | |
| { | |
| Input: "あいうえおかきくけこさ", | |
| Expected: "あいうえおかきくけこさ", | |
| MaxLen: 11, | |
| }, | |
| { | |
| Input: "あいうえおかきくけこさ", | |
| Expected: "あいうえおかきくけこさ", | |
| MaxLen: 12, | |
| }, | |
| } | |
| for i, tc := range testCases { | |
| testName := fmt.Sprintf("%d", i) | |
| t.Run(testName, func(t *testing.T) { | |
| out := truncateId(tc.Input, tc.MaxLen) | |
| if out != tc.Expected { | |
| t.Fatalf("Expected %q to be shortened to %d as %q (given: %q)", | |
| tc.Input, tc.MaxLen, tc.Expected, out) | |
| } | |
| }) | |
| } | |
| } | |
| func mustNewDynamicValue(val cty.Value, ty cty.Type) plans.DynamicValue { | |
| ret, err := plans.NewDynamicValue(val, ty) | |
| if err != nil { | |
| panic(err) | |
| } | |
| return ret | |
| } |