Merge pull request #27788 from hashicorp/alisdair/command-views-output-tests
views: Expand test coverage for views.Output
This commit is contained in:
commit
20cb28b25f
|
@ -47,58 +47,6 @@ func TestOutput(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOutput_nestedListAndMap(t *testing.T) {
|
|
||||||
originalState := states.BuildState(func(s *states.SyncState) {
|
|
||||||
s.SetOutputValue(
|
|
||||||
addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
|
|
||||||
cty.ListVal([]cty.Value{
|
|
||||||
cty.MapVal(map[string]cty.Value{
|
|
||||||
"key": cty.StringVal("value"),
|
|
||||||
"key2": cty.StringVal("value2"),
|
|
||||||
}),
|
|
||||||
cty.MapVal(map[string]cty.Value{
|
|
||||||
"key": cty.StringVal("value"),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
statePath := testStateFile(t, originalState)
|
|
||||||
|
|
||||||
view, done := testView(t)
|
|
||||||
c := &OutputCommand{
|
|
||||||
Meta: Meta{
|
|
||||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
|
||||||
View: view,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{
|
|
||||||
"-state", statePath,
|
|
||||||
}
|
|
||||||
code := c.Run(args)
|
|
||||||
output := done(t)
|
|
||||||
if code != 0 {
|
|
||||||
t.Fatalf("bad: \n%s", output.Stderr())
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := strings.TrimSpace(output.Stdout())
|
|
||||||
expected := strings.TrimSpace(`
|
|
||||||
foo = tolist([
|
|
||||||
tomap({
|
|
||||||
"key" = "value"
|
|
||||||
"key2" = "value2"
|
|
||||||
}),
|
|
||||||
tomap({
|
|
||||||
"key" = "value"
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
`)
|
|
||||||
if actual != expected {
|
|
||||||
t.Fatalf("wrong output\ngot: %s\nwant: %s", actual, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOutput_json(t *testing.T) {
|
func TestOutput_json(t *testing.T) {
|
||||||
originalState := states.BuildState(func(s *states.SyncState) {
|
originalState := states.BuildState(func(s *states.SyncState) {
|
||||||
s.SetOutputValue(
|
s.SetOutputValue(
|
||||||
|
@ -163,36 +111,6 @@ func TestOutput_emptyOutputs(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOutput_jsonEmptyOutputs(t *testing.T) {
|
|
||||||
originalState := states.NewState()
|
|
||||||
statePath := testStateFile(t, originalState)
|
|
||||||
|
|
||||||
p := testProvider()
|
|
||||||
view, done := testView(t)
|
|
||||||
c := &OutputCommand{
|
|
||||||
Meta: Meta{
|
|
||||||
testingOverrides: metaOverridesForProvider(p),
|
|
||||||
View: view,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{
|
|
||||||
"-state", statePath,
|
|
||||||
"-json",
|
|
||||||
}
|
|
||||||
code := c.Run(args)
|
|
||||||
output := done(t)
|
|
||||||
if code != 0 {
|
|
||||||
t.Fatalf("bad: \n%s", output.Stderr())
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := strings.TrimSpace(output.Stdout())
|
|
||||||
expected := "{}"
|
|
||||||
if actual != expected {
|
|
||||||
t.Fatalf("bad:\n%#v\n%#v", expected, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOutput_badVar(t *testing.T) {
|
func TestOutput_badVar(t *testing.T) {
|
||||||
originalState := states.BuildState(func(s *states.SyncState) {
|
originalState := states.BuildState(func(s *states.SyncState) {
|
||||||
s.SetOutputValue(
|
s.SetOutputValue(
|
||||||
|
|
|
@ -166,10 +166,9 @@ func (v *OutputRaw) Output(name string, outputs map[string]*states.OutputValue)
|
||||||
return diags
|
return diags
|
||||||
}
|
}
|
||||||
// If we get out here then we should have a valid string to print.
|
// If we get out here then we should have a valid string to print.
|
||||||
// We're writing it directly to the output here so that a shell caller
|
// We're writing it using Print here so that a shell caller will get
|
||||||
// will get exactly the value and no extra whitespace.
|
// exactly the value and no extra whitespace (including trailing newline).
|
||||||
str := strV.AsString()
|
v.streams.Print(strV.AsString())
|
||||||
fmt.Fprint(v.streams.Stdout.File, str)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,256 @@
|
||||||
package views
|
package views
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/command/arguments"
|
||||||
"github.com/hashicorp/terraform/internal/terminal"
|
"github.com/hashicorp/terraform/internal/terminal"
|
||||||
"github.com/hashicorp/terraform/states"
|
"github.com/hashicorp/terraform/states"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Test various single output values for human-readable UI. Note that since
|
||||||
|
// OutputHuman defers to repl.FormatValue to render a single value, most of the
|
||||||
|
// test coverage should be in that package.
|
||||||
|
func TestOutputHuman_single(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
value cty.Value
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
"string": {
|
||||||
|
value: cty.StringVal("hello"),
|
||||||
|
want: "\"hello\"\n",
|
||||||
|
},
|
||||||
|
"list of maps": {
|
||||||
|
value: cty.ListVal([]cty.Value{
|
||||||
|
cty.MapVal(map[string]cty.Value{
|
||||||
|
"key": cty.StringVal("value"),
|
||||||
|
"key2": cty.StringVal("value2"),
|
||||||
|
}),
|
||||||
|
cty.MapVal(map[string]cty.Value{
|
||||||
|
"key": cty.StringVal("value"),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
want: `tolist([
|
||||||
|
tomap({
|
||||||
|
"key" = "value"
|
||||||
|
"key2" = "value2"
|
||||||
|
}),
|
||||||
|
tomap({
|
||||||
|
"key" = "value"
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
streams, done := terminal.StreamsForTesting(t)
|
||||||
|
v := NewOutput(arguments.ViewHuman, NewView(streams))
|
||||||
|
|
||||||
|
outputs := map[string]*states.OutputValue{
|
||||||
|
"foo": {Value: tc.value},
|
||||||
|
}
|
||||||
|
diags := v.Output("foo", outputs)
|
||||||
|
|
||||||
|
if diags.HasErrors() {
|
||||||
|
if !tc.wantErr {
|
||||||
|
t.Fatalf("unexpected diagnostics: %s", diags)
|
||||||
|
}
|
||||||
|
} else if tc.wantErr {
|
||||||
|
t.Fatalf("succeeded, but want error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := done(t).Stdout(), tc.want; got != want {
|
||||||
|
t.Errorf("wrong result\ngot: %q\nwant: %q", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sensitive output values are rendered to the console intentionally when
|
||||||
|
// requesting a single output.
|
||||||
|
func TestOutput_sensitive(t *testing.T) {
|
||||||
|
testCases := map[string]arguments.ViewType{
|
||||||
|
"human": arguments.ViewHuman,
|
||||||
|
"json": arguments.ViewJSON,
|
||||||
|
"raw": arguments.ViewRaw,
|
||||||
|
}
|
||||||
|
for name, vt := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
streams, done := terminal.StreamsForTesting(t)
|
||||||
|
v := NewOutput(vt, NewView(streams))
|
||||||
|
|
||||||
|
outputs := map[string]*states.OutputValue{
|
||||||
|
"foo": {
|
||||||
|
Value: cty.StringVal("secret"),
|
||||||
|
Sensitive: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
diags := v.Output("foo", outputs)
|
||||||
|
|
||||||
|
if diags.HasErrors() {
|
||||||
|
t.Fatalf("unexpected diagnostics: %s", diags)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test for substring match here because we don't care about exact
|
||||||
|
// output format in this test, just the presence of the sensitive
|
||||||
|
// value.
|
||||||
|
if got, want := done(t).Stdout(), "secret"; !strings.Contains(got, want) {
|
||||||
|
t.Errorf("wrong result\ngot: %q\nwant: %q", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Showing all outputs is supported by human and JSON output format.
|
||||||
|
func TestOutput_all(t *testing.T) {
|
||||||
|
outputs := map[string]*states.OutputValue{
|
||||||
|
"foo": {
|
||||||
|
Value: cty.StringVal("secret"),
|
||||||
|
Sensitive: true,
|
||||||
|
},
|
||||||
|
"bar": {
|
||||||
|
Value: cty.ListVal([]cty.Value{cty.True, cty.False, cty.True}),
|
||||||
|
},
|
||||||
|
"baz": {
|
||||||
|
Value: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"boop": cty.NumberIntVal(5),
|
||||||
|
"beep": cty.StringVal("true"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
vt arguments.ViewType
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
"human": {
|
||||||
|
arguments.ViewHuman,
|
||||||
|
`bar = tolist([
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
])
|
||||||
|
baz = {
|
||||||
|
"beep" = "true"
|
||||||
|
"boop" = 5
|
||||||
|
}
|
||||||
|
foo = <sensitive>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"json": {
|
||||||
|
arguments.ViewJSON,
|
||||||
|
`{
|
||||||
|
"bar": {
|
||||||
|
"sensitive": false,
|
||||||
|
"type": [
|
||||||
|
"list",
|
||||||
|
"bool"
|
||||||
|
],
|
||||||
|
"value": [
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"baz": {
|
||||||
|
"sensitive": false,
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
{
|
||||||
|
"beep": "string",
|
||||||
|
"boop": "number"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"value": {
|
||||||
|
"beep": "true",
|
||||||
|
"boop": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foo": {
|
||||||
|
"sensitive": true,
|
||||||
|
"type": "string",
|
||||||
|
"value": "secret"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
streams, done := terminal.StreamsForTesting(t)
|
||||||
|
v := NewOutput(tc.vt, NewView(streams))
|
||||||
|
diags := v.Output("", outputs)
|
||||||
|
|
||||||
|
if diags.HasErrors() {
|
||||||
|
t.Fatalf("unexpected diagnostics: %s", diags)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := done(t).Stdout(); got != tc.want {
|
||||||
|
t.Errorf("wrong result\ngot: %q\nwant: %q", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON output format supports empty outputs by rendering an empty object
|
||||||
|
// without diagnostics.
|
||||||
|
func TestOutputJSON_empty(t *testing.T) {
|
||||||
|
streams, done := terminal.StreamsForTesting(t)
|
||||||
|
v := NewOutput(arguments.ViewJSON, NewView(streams))
|
||||||
|
|
||||||
|
diags := v.Output("", map[string]*states.OutputValue{})
|
||||||
|
|
||||||
|
if diags.HasErrors() {
|
||||||
|
t.Fatalf("unexpected diagnostics: %s", diags)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := done(t).Stdout(), "{}\n"; got != want {
|
||||||
|
t.Errorf("wrong result\ngot: %q\nwant: %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Human and raw formats render a warning if there are no outputs.
|
||||||
|
func TestOutput_emptyWarning(t *testing.T) {
|
||||||
|
testCases := map[string]arguments.ViewType{
|
||||||
|
"human": arguments.ViewHuman,
|
||||||
|
"raw": arguments.ViewRaw,
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, vt := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
streams, done := terminal.StreamsForTesting(t)
|
||||||
|
v := NewOutput(vt, NewView(streams))
|
||||||
|
|
||||||
|
diags := v.Output("", map[string]*states.OutputValue{})
|
||||||
|
|
||||||
|
if got, want := done(t).Stdout(), ""; got != want {
|
||||||
|
t.Errorf("wrong result\ngot: %q\nwant: %q", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(diags) != 1 {
|
||||||
|
t.Fatalf("expected 1 diagnostic, got %d", len(diags))
|
||||||
|
}
|
||||||
|
|
||||||
|
if diags.HasErrors() {
|
||||||
|
t.Fatalf("unexpected error diagnostics: %s", diags)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := diags[0].Description().Summary, "No outputs found"; got != want {
|
||||||
|
t.Errorf("unexpected diagnostics: %s", diags)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raw output is a simple unquoted output format designed for shell scripts,
|
||||||
|
// which relies on the cty.AsString() implementation. This test covers
|
||||||
|
// formatting for supported value types.
|
||||||
func TestOutputRaw(t *testing.T) {
|
func TestOutputRaw(t *testing.T) {
|
||||||
values := map[string]cty.Value{
|
values := map[string]cty.Value{
|
||||||
"str": cty.StringVal("bar"),
|
"str": cty.StringVal("bar"),
|
||||||
|
@ -16,6 +259,7 @@ func TestOutputRaw(t *testing.T) {
|
||||||
"bool": cty.True,
|
"bool": cty.True,
|
||||||
"obj": cty.EmptyObjectVal,
|
"obj": cty.EmptyObjectVal,
|
||||||
"null": cty.NullVal(cty.String),
|
"null": cty.NullVal(cty.String),
|
||||||
|
"unknown": cty.UnknownVal(cty.String),
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := map[string]struct {
|
tests := map[string]struct {
|
||||||
|
@ -28,15 +272,13 @@ func TestOutputRaw(t *testing.T) {
|
||||||
"bool": {WantOutput: "true"},
|
"bool": {WantOutput: "true"},
|
||||||
"obj": {WantErr: true},
|
"obj": {WantErr: true},
|
||||||
"null": {WantErr: true},
|
"null": {WantErr: true},
|
||||||
|
"unknown": {WantErr: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, test := range tests {
|
for name, test := range tests {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
streams, done := terminal.StreamsForTesting(t)
|
streams, done := terminal.StreamsForTesting(t)
|
||||||
view := NewView(streams)
|
v := NewOutput(arguments.ViewRaw, NewView(streams))
|
||||||
v := &OutputRaw{
|
|
||||||
View: *view,
|
|
||||||
}
|
|
||||||
|
|
||||||
value := values[name]
|
value := values[name]
|
||||||
outputs := map[string]*states.OutputValue{
|
outputs := map[string]*states.OutputValue{
|
||||||
|
@ -58,3 +300,64 @@ func TestOutputRaw(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Raw cannot render all outputs.
|
||||||
|
func TestOutputRaw_all(t *testing.T) {
|
||||||
|
streams, done := terminal.StreamsForTesting(t)
|
||||||
|
v := NewOutput(arguments.ViewRaw, NewView(streams))
|
||||||
|
|
||||||
|
outputs := map[string]*states.OutputValue{
|
||||||
|
"foo": {Value: cty.StringVal("secret")},
|
||||||
|
"bar": {Value: cty.True},
|
||||||
|
}
|
||||||
|
diags := v.Output("", outputs)
|
||||||
|
|
||||||
|
if got, want := done(t).Stdout(), ""; got != want {
|
||||||
|
t.Errorf("wrong result\ngot: %q\nwant: %q", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !diags.HasErrors() {
|
||||||
|
t.Fatalf("expected diagnostics, got %s", diags)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := diags.Err().Error(), "Raw output format is only supported for single outputs"; got != want {
|
||||||
|
t.Errorf("unexpected diagnostics: %s", diags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All outputs render an error if a specific output is requested which is
|
||||||
|
// missing from the map of outputs.
|
||||||
|
func TestOutput_missing(t *testing.T) {
|
||||||
|
testCases := map[string]arguments.ViewType{
|
||||||
|
"human": arguments.ViewHuman,
|
||||||
|
"json": arguments.ViewJSON,
|
||||||
|
"raw": arguments.ViewRaw,
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, vt := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
streams, done := terminal.StreamsForTesting(t)
|
||||||
|
v := NewOutput(vt, NewView(streams))
|
||||||
|
|
||||||
|
diags := v.Output("foo", map[string]*states.OutputValue{
|
||||||
|
"bar": {Value: cty.StringVal("boop")},
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(diags) != 1 {
|
||||||
|
t.Fatalf("expected 1 diagnostic, got %d", len(diags))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !diags.HasErrors() {
|
||||||
|
t.Fatalf("expected error diagnostics, got %s", diags)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := diags[0].Description().Summary, `Output "foo" not found`; got != want {
|
||||||
|
t.Errorf("unexpected diagnostics: %s", diags)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := done(t).Stdout(), ""; got != want {
|
||||||
|
t.Errorf("wrong result\ngot: %q\nwant: %q", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue