opentofu/internal/command/views/hook_ui_test.go
Martin Atkins 36d0a50427 Move terraform/ to internal/terraform/
This is part of a general effort to move all of Terraform's non-library
package surface under internal in order to reinforce that these are for
internal use within Terraform only.

If you were previously importing packages under this prefix into an
external codebase, you could pin to an earlier release tag as an interim
solution until you've make a plan to achieve the same functionality some
other way.
2021-05-17 14:09:07 -07:00

659 lines
17 KiB
Go

package views
import (
"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"
)
// 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(addr, states.CurrentGen, 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) {
streams, done := terminal.StreamsForTesting(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(addr, states.CurrentGen, 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(3100 * 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, 1s elapsed]
test_instance.foo: Still modifying... [id=test, 2s elapsed]
test_instance.foo: Still modifying... [id=test, 3s 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(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(addr, states.CurrentGen, 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(addr, states.CurrentGen, 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 TestPreProvisionInstanceStep(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(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 TestProvisionOutput(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(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 TestPreRefresh(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(addr, states.CurrentGen, 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 TestPreRefresh_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(addr, states.CurrentGen, 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 TestPreImportState(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(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)
}
}
// 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 TestPostImportState(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(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 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)
}
})
}
}