Merge pull request #27788 from hashicorp/alisdair/command-views-output-tests

views: Expand test coverage for views.Output
This commit is contained in:
Alisdair McDiarmid 2021-02-16 09:07:18 -05:00 committed by GitHub
commit 20cb28b25f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 310 additions and 90 deletions

View File

@ -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(

View File

@ -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
} }

View File

@ -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)
}
})
}
}