cli: Add initial command views abstraction
Terraform supports multiple output formats for several sub-commands.
The default format is user-readable text, but many sub-commands support
a `-json` flag to output a machine-readable format for the result. The
output command also supports a `-raw` flag for a simpler, scripting-
focused machine readable format.
This commit adds a "views" abstraction, intended to help ensure
consistency between the various output formats. This extracts the render
specific code from the command package, and moves it into a views
package. Each command is expected to create an interface for its view,
and one or more implementations of that interface.
By doing so, we separate the concerns of generating the sub-command
result from rendering the result in the specified output format. This
should make it easier to ensure that all output formats will be updated
together when changes occur in the result-generating phase.
There are some other consequences of this restructuring:
- Views now directly access the terminal streams, rather than the
now-redundant cli.Ui instance;
- With the reorganization of commands, parsing CLI arguments is now the
responsibility of a separate "arguments" package.
For now, views are added only for the output sub-command, as an example.
Because this command uses code which is shared with the apply and
refresh commands, those are also partially updated.
2021-01-27 21:51:40 +01:00
|
|
|
package arguments
|
|
|
|
|
|
|
|
import (
|
|
|
|
"reflect"
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
"github.com/davecgh/go-spew/spew"
|
2021-05-17 19:11:06 +02:00
|
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
cli: Add initial command views abstraction
Terraform supports multiple output formats for several sub-commands.
The default format is user-readable text, but many sub-commands support
a `-json` flag to output a machine-readable format for the result. The
output command also supports a `-raw` flag for a simpler, scripting-
focused machine readable format.
This commit adds a "views" abstraction, intended to help ensure
consistency between the various output formats. This extracts the render
specific code from the command package, and moves it into a views
package. Each command is expected to create an interface for its view,
and one or more implementations of that interface.
By doing so, we separate the concerns of generating the sub-command
result from rendering the result in the specified output format. This
should make it easier to ensure that all output formats will be updated
together when changes occur in the result-generating phase.
There are some other consequences of this restructuring:
- Views now directly access the terminal streams, rather than the
now-redundant cli.Ui instance;
- With the reorganization of commands, parsing CLI arguments is now the
responsibility of a separate "arguments" package.
For now, views are added only for the output sub-command, as an example.
Because this command uses code which is shared with the apply and
refresh commands, those are also partially updated.
2021-01-27 21:51:40 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
func TestParseOutput_valid(t *testing.T) {
|
|
|
|
testCases := map[string]struct {
|
|
|
|
args []string
|
|
|
|
want *Output
|
|
|
|
}{
|
|
|
|
"defaults": {
|
|
|
|
nil,
|
|
|
|
&Output{
|
|
|
|
Name: "",
|
|
|
|
ViewType: ViewHuman,
|
|
|
|
StatePath: "",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"json": {
|
|
|
|
[]string{"-json"},
|
|
|
|
&Output{
|
|
|
|
Name: "",
|
|
|
|
ViewType: ViewJSON,
|
|
|
|
StatePath: "",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"raw": {
|
|
|
|
[]string{"-raw", "foo"},
|
|
|
|
&Output{
|
|
|
|
Name: "foo",
|
|
|
|
ViewType: ViewRaw,
|
|
|
|
StatePath: "",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"state": {
|
|
|
|
[]string{"-state=foobar.tfstate", "-raw", "foo"},
|
|
|
|
&Output{
|
|
|
|
Name: "foo",
|
|
|
|
ViewType: ViewRaw,
|
|
|
|
StatePath: "foobar.tfstate",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for name, tc := range testCases {
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
got, diags := ParseOutput(tc.args)
|
|
|
|
if len(diags) > 0 {
|
|
|
|
t.Fatalf("unexpected diags: %v", diags)
|
|
|
|
}
|
|
|
|
if *got != *tc.want {
|
|
|
|
t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestParseOutput_invalid(t *testing.T) {
|
|
|
|
testCases := map[string]struct {
|
|
|
|
args []string
|
|
|
|
want *Output
|
|
|
|
wantDiags tfdiags.Diagnostics
|
|
|
|
}{
|
|
|
|
"unknown flag": {
|
|
|
|
[]string{"-boop"},
|
|
|
|
&Output{
|
|
|
|
Name: "",
|
|
|
|
ViewType: ViewHuman,
|
|
|
|
StatePath: "",
|
|
|
|
},
|
|
|
|
tfdiags.Diagnostics{
|
|
|
|
tfdiags.Sourceless(
|
|
|
|
tfdiags.Error,
|
|
|
|
"Failed to parse command-line flags",
|
|
|
|
"flag provided but not defined: -boop",
|
|
|
|
),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"json and raw specified": {
|
|
|
|
[]string{"-json", "-raw"},
|
|
|
|
&Output{
|
|
|
|
Name: "",
|
|
|
|
ViewType: ViewHuman,
|
|
|
|
StatePath: "",
|
|
|
|
},
|
|
|
|
tfdiags.Diagnostics{
|
|
|
|
tfdiags.Sourceless(
|
|
|
|
tfdiags.Error,
|
|
|
|
"Invalid output format",
|
|
|
|
"The -raw and -json options are mutually-exclusive.",
|
|
|
|
),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"raw with no name": {
|
|
|
|
[]string{"-raw"},
|
|
|
|
&Output{
|
|
|
|
Name: "",
|
|
|
|
ViewType: ViewRaw,
|
|
|
|
StatePath: "",
|
|
|
|
},
|
|
|
|
tfdiags.Diagnostics{
|
|
|
|
tfdiags.Sourceless(
|
|
|
|
tfdiags.Error,
|
|
|
|
"Output name required",
|
|
|
|
"You must give the name of a single output value when using the -raw option.",
|
|
|
|
),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"too many arguments": {
|
|
|
|
[]string{"-raw", "-state=foo.tfstate", "bar", "baz"},
|
|
|
|
&Output{
|
|
|
|
Name: "bar",
|
|
|
|
ViewType: ViewRaw,
|
|
|
|
StatePath: "foo.tfstate",
|
|
|
|
},
|
|
|
|
tfdiags.Diagnostics{
|
|
|
|
tfdiags.Sourceless(
|
|
|
|
tfdiags.Error,
|
|
|
|
"Unexpected argument",
|
|
|
|
"The output command expects exactly one argument with the name of an output variable or no arguments to show all outputs.",
|
|
|
|
),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for name, tc := range testCases {
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
got, gotDiags := ParseOutput(tc.args)
|
|
|
|
if *got != *tc.want {
|
|
|
|
t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want)
|
|
|
|
}
|
|
|
|
if !reflect.DeepEqual(gotDiags, tc.wantDiags) {
|
|
|
|
t.Errorf("wrong result\ngot: %s\nwant: %s", spew.Sdump(gotDiags), spew.Sdump(tc.wantDiags))
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|