cli: Migrate validate command to views

This commit is contained in:
Alisdair McDiarmid 2021-03-18 11:14:58 -04:00
parent c6278bbe37
commit dd380d0b58
6 changed files with 506 additions and 149 deletions

View File

@ -0,0 +1,59 @@
package arguments
import (
"github.com/hashicorp/terraform/tfdiags"
)
// Validate represents the command-line arguments for the validate command.
type Validate struct {
// Path is the directory containing the configuration to be validated. If
// unspecified, validate will use the current directory.
Path string
// ViewType specifies which output format to use: human, JSON, or "raw".
ViewType ViewType
}
// ParseValidate processes CLI arguments, returning a Validate value and errors.
// If errors are encountered, a Validate value is still returned representing
// the best effort interpretation of the arguments.
func ParseValidate(args []string) (*Validate, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
validate := &Validate{
Path: ".",
}
var jsonOutput bool
cmdFlags := defaultFlagSet("validate")
cmdFlags.BoolVar(&jsonOutput, "json", false, "json")
if err := cmdFlags.Parse(args); err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to parse command-line flags",
err.Error(),
))
}
args = cmdFlags.Args()
if len(args) > 1 {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Too many command line arguments",
"Expected at most one positional argument.",
))
}
if len(args) > 0 {
validate.Path = args[0]
}
switch {
case jsonOutput:
validate.ViewType = ViewJSON
default:
validate.ViewType = ViewHuman
}
return validate, diags
}

View File

@ -0,0 +1,99 @@
package arguments
import (
"reflect"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/hashicorp/terraform/tfdiags"
)
func TestParseValidate_valid(t *testing.T) {
testCases := map[string]struct {
args []string
want *Validate
}{
"defaults": {
nil,
&Validate{
Path: ".",
ViewType: ViewHuman,
},
},
"json": {
[]string{"-json"},
&Validate{
Path: ".",
ViewType: ViewJSON,
},
},
"path": {
[]string{"-json", "foo"},
&Validate{
Path: "foo",
ViewType: ViewJSON,
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
got, diags := ParseValidate(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 TestParseValidate_invalid(t *testing.T) {
testCases := map[string]struct {
args []string
want *Validate
wantDiags tfdiags.Diagnostics
}{
"unknown flag": {
[]string{"-boop"},
&Validate{
Path: ".",
ViewType: ViewHuman,
},
tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"Failed to parse command-line flags",
"flag provided but not defined: -boop",
),
},
},
"too many arguments": {
[]string{"-json", "bar", "baz"},
&Validate{
Path: "bar",
ViewType: ViewJSON,
},
tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"Too many command line arguments",
"Expected at most one positional argument.",
),
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
got, gotDiags := ParseValidate(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))
}
})
}
}

View File

@ -1,14 +1,14 @@
package command
import (
"encoding/json"
"fmt"
"path/filepath"
"strings"
"github.com/zclconf/go-cty/cty"
viewsjson "github.com/hashicorp/terraform/command/views/json"
"github.com/hashicorp/terraform/command/arguments"
"github.com/hashicorp/terraform/command/views"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
)
@ -18,43 +18,37 @@ type ValidateCommand struct {
Meta
}
func (c *ValidateCommand) Run(args []string) int {
args = c.Meta.process(args)
func (c *ValidateCommand) Run(rawArgs []string) int {
// Parse and apply global view arguments
common, rawArgs := arguments.ParseView(rawArgs)
c.View.Configure(common)
var jsonOutput bool
cmdFlags := c.Meta.defaultFlagSet("validate")
cmdFlags.BoolVar(&jsonOutput, "json", false, "produce JSON output")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
// Parse and validate flags
args, diags := arguments.ParseValidate(rawArgs)
if diags.HasErrors() {
c.View.Diagnostics(diags)
c.View.HelpPrompt("validate")
return 1
}
var diags tfdiags.Diagnostics
view := views.NewValidate(args.ViewType, c.View)
// After this point, we must only produce JSON output if JSON mode is
// enabled, so all errors should be accumulated into diags and we'll
// print out a suitable result at the end, depending on the format
// selection. All returns from this point on must be tail-calls into
// c.showResults in order to produce the expected output.
args = cmdFlags.Args()
// view.Results in order to produce the expected output.
var dirPath string
if len(args) == 1 {
dirPath = args[0]
} else {
dirPath = "."
}
dir, err := filepath.Abs(dirPath)
dir, err := filepath.Abs(args.Path)
if err != nil {
diags = diags.Append(fmt.Errorf("unable to locate module: %s", err))
return c.showResults(diags, jsonOutput)
return view.Results(diags)
}
// Check for user-supplied plugin path
if c.pluginPath, err = c.loadPluginPath(); err != nil {
diags = diags.Append(fmt.Errorf("error loading plugin path: %s", err))
return c.showResults(diags, jsonOutput)
return view.Results(diags)
}
validateDiags := c.validate(dir)
@ -66,7 +60,7 @@ func (c *ValidateCommand) Run(args []string) int {
// check before submitting a change.
diags = diags.Append(c.providerDevOverrideRuntimeWarnings())
return c.showResults(diags, jsonOutput)
return view.Results(diags)
}
func (c *ValidateCommand) validate(dir string) tfdiags.Diagnostics {
@ -116,80 +110,13 @@ func (c *ValidateCommand) validate(dir string) tfdiags.Diagnostics {
return diags
}
func (c *ValidateCommand) showResults(diags tfdiags.Diagnostics, jsonOutput bool) int {
switch {
case jsonOutput:
// FormatVersion represents the version of the json format and will be
// incremented for any change to this format that requires changes to a
// consuming parser.
const FormatVersion = "0.1"
type Output struct {
FormatVersion string `json:"format_version"`
// We include some summary information that is actually redundant
// with the detailed diagnostics, but avoids the need for callers
// to re-implement our logic for deciding these.
Valid bool `json:"valid"`
ErrorCount int `json:"error_count"`
WarningCount int `json:"warning_count"`
Diagnostics []*viewsjson.Diagnostic `json:"diagnostics"`
}
output := Output{
FormatVersion: FormatVersion,
Valid: true, // until proven otherwise
}
configSources := c.configSources()
for _, diag := range diags {
output.Diagnostics = append(output.Diagnostics, viewsjson.NewDiagnostic(diag, configSources))
switch diag.Severity() {
case tfdiags.Error:
output.ErrorCount++
output.Valid = false
case tfdiags.Warning:
output.WarningCount++
}
}
if output.Diagnostics == nil {
// Make sure this always appears as an array in our output, since
// this is easier to consume for dynamically-typed languages.
output.Diagnostics = []*viewsjson.Diagnostic{}
}
j, err := json.MarshalIndent(&output, "", " ")
if err != nil {
// Should never happen because we fully-control the input here
panic(err)
}
c.Ui.Output(string(j))
default:
if len(diags) == 0 {
c.Ui.Output(c.Colorize().Color("[green][bold]Success![reset] The configuration is valid.\n"))
} else {
c.showDiagnostics(diags)
if !diags.HasErrors() {
c.Ui.Output(c.Colorize().Color("[green][bold]Success![reset] The configuration is valid, but there were some validation warnings as shown above.\n"))
}
}
}
if diags.HasErrors() {
return 1
}
return 0
}
func (c *ValidateCommand) Synopsis() string {
return "Check whether the configuration is valid"
}
func (c *ValidateCommand) Help() string {
helpText := `
Usage: terraform [global options] validate [options] [dir]
Usage: terraform [global options] validate [options]
Validate the configuration files in a directory, referring only to the
configuration and not accessing any remote services such as remote state,
@ -209,8 +136,6 @@ Usage: terraform [global options] validate [options] [dir]
validation without accessing any configured remote backend, use:
terraform init -backend=false
If dir is not specified, then the current directory will be used.
To verify configuration in the context of a particular run (a particular
target workspace, input variable values, etc), use the 'terraform plan'
command instead, which includes an implied validation check.

View File

@ -9,15 +9,15 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/mitchellh/cli"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/providers"
)
func setupTest(fixturepath string, args ...string) (*cli.MockUi, int) {
ui := new(cli.MockUi)
func setupTest(t *testing.T, fixturepath string, args ...string) (*terminal.TestOutput, int) {
view, done := testView(t)
p := testProvider()
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
@ -45,19 +45,20 @@ func setupTest(fixturepath string, args ...string) (*cli.MockUi, int) {
c := &ValidateCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
args = append(args, "-no-color")
args = append(args, testFixturePath(fixturepath))
code := c.Run(args)
return ui, code
return done(t), code
}
func TestValidateCommand(t *testing.T) {
if ui, code := setupTest("validate-valid"); code != 0 {
t.Fatalf("unexpected non-successful exit code %d\n\n%s", code, ui.ErrorWriter.String())
if output, code := setupTest(t, "validate-valid"); code != 0 {
t.Fatalf("unexpected non-successful exit code %d\n\n%s", code, output.Stderr())
}
}
@ -69,135 +70,137 @@ func TestValidateCommandWithTfvarsFile(t *testing.T) {
defer os.RemoveAll(td)
defer testChdir(t, td)()
ui := new(cli.MockUi)
view, done := testView(t)
c := &ValidateCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
View: view,
},
}
args := []string{}
if code := c.Run(args); code != 0 {
t.Fatalf("bad %d\n\n%s", code, ui.ErrorWriter.String())
code := c.Run(args)
output := done(t)
if code != 0 {
t.Fatalf("bad %d\n\n%s", code, output.Stderr())
}
}
func TestValidateFailingCommand(t *testing.T) {
if ui, code := setupTest("validate-invalid"); code != 1 {
t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
if output, code := setupTest(t, "validate-invalid"); code != 1 {
t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr())
}
}
func TestValidateFailingCommandMissingQuote(t *testing.T) {
ui, code := setupTest("validate-invalid/missing_quote")
output, code := setupTest(t, "validate-invalid/missing_quote")
if code != 1 {
t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr())
}
wantError := "Error: Invalid reference"
if !strings.Contains(ui.ErrorWriter.String(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
if !strings.Contains(output.Stderr(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
}
}
func TestValidateFailingCommandMissingVariable(t *testing.T) {
ui, code := setupTest("validate-invalid/missing_var")
output, code := setupTest(t, "validate-invalid/missing_var")
if code != 1 {
t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr())
}
wantError := "Error: Reference to undeclared input variable"
if !strings.Contains(ui.ErrorWriter.String(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
if !strings.Contains(output.Stderr(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
}
}
func TestSameProviderMutipleTimesShouldFail(t *testing.T) {
ui, code := setupTest("validate-invalid/multiple_providers")
output, code := setupTest(t, "validate-invalid/multiple_providers")
if code != 1 {
t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr())
}
wantError := "Error: Duplicate provider configuration"
if !strings.Contains(ui.ErrorWriter.String(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
if !strings.Contains(output.Stderr(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
}
}
func TestSameModuleMultipleTimesShouldFail(t *testing.T) {
ui, code := setupTest("validate-invalid/multiple_modules")
output, code := setupTest(t, "validate-invalid/multiple_modules")
if code != 1 {
t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr())
}
wantError := "Error: Duplicate module call"
if !strings.Contains(ui.ErrorWriter.String(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
if !strings.Contains(output.Stderr(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
}
}
func TestSameResourceMultipleTimesShouldFail(t *testing.T) {
ui, code := setupTest("validate-invalid/multiple_resources")
output, code := setupTest(t, "validate-invalid/multiple_resources")
if code != 1 {
t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr())
}
wantError := `Error: Duplicate resource "aws_instance" configuration`
if !strings.Contains(ui.ErrorWriter.String(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
if !strings.Contains(output.Stderr(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
}
}
func TestOutputWithoutValueShouldFail(t *testing.T) {
ui, code := setupTest("validate-invalid/outputs")
output, code := setupTest(t, "validate-invalid/outputs")
if code != 1 {
t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr())
}
wantError := `The argument "value" is required, but no definition was found.`
if !strings.Contains(ui.ErrorWriter.String(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
if !strings.Contains(output.Stderr(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
}
wantError = `An argument named "values" is not expected here. Did you mean "value"?`
if !strings.Contains(ui.ErrorWriter.String(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
if !strings.Contains(output.Stderr(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
}
}
func TestModuleWithIncorrectNameShouldFail(t *testing.T) {
ui, code := setupTest("validate-invalid/incorrectmodulename")
output, code := setupTest(t, "validate-invalid/incorrectmodulename")
if code != 1 {
t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr())
}
wantError := `Error: Invalid module instance name`
if !strings.Contains(ui.ErrorWriter.String(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
if !strings.Contains(output.Stderr(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
}
wantError = `Error: Variables not allowed`
if !strings.Contains(ui.ErrorWriter.String(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
if !strings.Contains(output.Stderr(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
}
}
func TestWronglyUsedInterpolationShouldFail(t *testing.T) {
ui, code := setupTest("validate-invalid/interpolation")
output, code := setupTest(t, "validate-invalid/interpolation")
if code != 1 {
t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr())
}
wantError := `Error: Variables not allowed`
if !strings.Contains(ui.ErrorWriter.String(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
if !strings.Contains(output.Stderr(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
}
wantError = `A single static variable reference is required`
if !strings.Contains(ui.ErrorWriter.String(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
if !strings.Contains(output.Stderr(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
}
}
func TestMissingDefinedVar(t *testing.T) {
ui, code := setupTest("validate-invalid/missing_defined_var")
output, code := setupTest(t, "validate-invalid/missing_defined_var")
// This is allowed because validate tests only that variables are referenced
// correctly, not that they all have defined values.
if code != 0 {
t.Fatalf("Should have passed: %d\n\n%s", code, ui.ErrorWriter.String())
t.Fatalf("Should have passed: %d\n\n%s", code, output.Stderr())
}
}
@ -237,9 +240,9 @@ func TestValidate_json(t *testing.T) {
t.Fatalf("failed to unmarshal expected JSON: %s", err)
}
ui, code := setupTest(tc.path, "-json")
output, code := setupTest(t, tc.path, "-json")
gotString := ui.OutputWriter.String()
gotString := output.Stdout()
err = json.Unmarshal([]byte(gotString), &got)
if err != nil {
t.Fatalf("failed to unmarshal actual JSON: %s", err)
@ -256,7 +259,7 @@ func TestValidate_json(t *testing.T) {
t.Errorf("wrong exit code: want 1, got %d", code)
}
if errorOutput := ui.ErrorWriter.String(); errorOutput != "" {
if errorOutput := output.Stderr(); errorOutput != "" {
t.Errorf("unexpected error output:\n%s", errorOutput)
}
})

138
command/views/validate.go Normal file
View File

@ -0,0 +1,138 @@
package views
import (
"encoding/json"
"fmt"
"github.com/hashicorp/terraform/command/arguments"
"github.com/hashicorp/terraform/command/format"
viewsjson "github.com/hashicorp/terraform/command/views/json"
"github.com/hashicorp/terraform/tfdiags"
)
// The Validate is used for the validate command.
type Validate interface {
// Results renders the diagnostics returned from a validation walk, and
// returns a CLI exit code: 0 if there are no errors, 1 otherwise
Results(diags tfdiags.Diagnostics) int
// Diagnostics renders early diagnostics, resulting from argument parsing.
Diagnostics(diags tfdiags.Diagnostics)
}
// NewValidate returns an initialized Validate implementation for the given ViewType.
func NewValidate(vt arguments.ViewType, view *View) Validate {
switch vt {
case arguments.ViewJSON:
return &ValidateJSON{view: view}
case arguments.ViewHuman:
return &ValidateHuman{view: view}
default:
panic(fmt.Sprintf("unknown view type %v", vt))
}
}
// The ValidateHuman implementation renders diagnostics in a human-readable form,
// along with a success/failure message if Terraform is able to execute the
// validation walk.
type ValidateHuman struct {
view *View
}
var _ Validate = (*ValidateHuman)(nil)
func (v *ValidateHuman) Results(diags tfdiags.Diagnostics) int {
columns := v.view.outputColumns()
if len(diags) == 0 {
v.view.streams.Println(format.WordWrap(v.view.colorize.Color(validateSuccess), columns))
} else {
v.Diagnostics(diags)
if !diags.HasErrors() {
v.view.streams.Println(format.WordWrap(v.view.colorize.Color(validateWarnings), columns))
}
}
if diags.HasErrors() {
return 1
}
return 0
}
const validateSuccess = "[green][bold]Success![reset] The configuration is valid.\n"
const validateWarnings = "[green][bold]Success![reset] The configuration is valid, but there were some validation warnings as shown above.\n"
func (v *ValidateHuman) Diagnostics(diags tfdiags.Diagnostics) {
v.view.Diagnostics(diags)
}
// The ValidateJSON implementation renders validation results as a JSON object.
// This object includes top-level fields summarizing the result, and an array
// of JSON diagnostic objects.
type ValidateJSON struct {
view *View
}
var _ Validate = (*ValidateJSON)(nil)
func (v *ValidateJSON) Results(diags tfdiags.Diagnostics) int {
// FormatVersion represents the version of the json format and will be
// incremented for any change to this format that requires changes to a
// consuming parser.
const FormatVersion = "0.1"
type Output struct {
FormatVersion string `json:"format_version"`
// We include some summary information that is actually redundant
// with the detailed diagnostics, but avoids the need for callers
// to re-implement our logic for deciding these.
Valid bool `json:"valid"`
ErrorCount int `json:"error_count"`
WarningCount int `json:"warning_count"`
Diagnostics []*viewsjson.Diagnostic `json:"diagnostics"`
}
output := Output{
FormatVersion: FormatVersion,
Valid: true, // until proven otherwise
}
configSources := v.view.configSources()
for _, diag := range diags {
output.Diagnostics = append(output.Diagnostics, viewsjson.NewDiagnostic(diag, configSources))
switch diag.Severity() {
case tfdiags.Error:
output.ErrorCount++
output.Valid = false
case tfdiags.Warning:
output.WarningCount++
}
}
if output.Diagnostics == nil {
// Make sure this always appears as an array in our output, since
// this is easier to consume for dynamically-typed languages.
output.Diagnostics = []*viewsjson.Diagnostic{}
}
j, err := json.MarshalIndent(&output, "", " ")
if err != nil {
// Should never happen because we fully-control the input here
panic(err)
}
v.view.streams.Println(string(j))
if diags.HasErrors() {
return 1
}
return 0
}
// Diagnostics should only be called if the validation walk cannot be executed.
// In this case, we choose to render human-readable diagnostic output,
// primarily for backwards compatibility.
func (v *ValidateJSON) Diagnostics(diags tfdiags.Diagnostics) {
v.view.Diagnostics(diags)
}

View File

@ -0,0 +1,133 @@
package views
import (
"encoding/json"
"strings"
"testing"
"github.com/hashicorp/terraform/command/arguments"
"github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/tfdiags"
)
func TestValidateHuman(t *testing.T) {
testCases := map[string]struct {
diag tfdiags.Diagnostic
wantSuccess bool
wantSubstring string
}{
"success": {
nil,
true,
"The configuration is valid.",
},
"warning": {
tfdiags.Sourceless(
tfdiags.Warning,
"Your shoelaces are untied",
"Watch out, or you'll trip!",
),
true,
"The configuration is valid, but there were some validation warnings",
},
"error": {
tfdiags.Sourceless(
tfdiags.Error,
"Configuration is missing random_pet",
"Every configuration should have a random_pet.",
),
false,
"Error: Configuration is missing random_pet",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
view := NewView(streams)
view.Configure(&arguments.View{NoColor: true})
v := NewValidate(arguments.ViewHuman, view)
var diags tfdiags.Diagnostics
if tc.diag != nil {
diags = diags.Append(tc.diag)
}
ret := v.Results(diags)
if tc.wantSuccess && ret != 0 {
t.Errorf("expected 0 return code, got %d", ret)
} else if !tc.wantSuccess && ret != 1 {
t.Errorf("expected 1 return code, got %d", ret)
}
got := done(t).All()
if strings.Contains(got, "Success!") != tc.wantSuccess {
t.Errorf("unexpected output:\n%s", got)
}
if !strings.Contains(got, tc.wantSubstring) {
t.Errorf("expected output to include %q, but was:\n%s", tc.wantSubstring, got)
}
})
}
}
func TestValidateJSON(t *testing.T) {
testCases := map[string]struct {
diag tfdiags.Diagnostic
wantSuccess bool
}{
"success": {
nil,
true,
},
"warning": {
tfdiags.Sourceless(
tfdiags.Warning,
"Your shoelaces are untied",
"Watch out, or you'll trip!",
),
true,
},
"error": {
tfdiags.Sourceless(
tfdiags.Error,
"Configuration is missing random_pet",
"Every configuration should have a random_pet.",
),
false,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
view := NewView(streams)
view.Configure(&arguments.View{NoColor: true})
v := NewValidate(arguments.ViewJSON, view)
var diags tfdiags.Diagnostics
if tc.diag != nil {
diags = diags.Append(tc.diag)
}
ret := v.Results(diags)
if tc.wantSuccess && ret != 0 {
t.Errorf("expected 0 return code, got %d", ret)
} else if !tc.wantSuccess && ret != 1 {
t.Errorf("expected 1 return code, got %d", ret)
}
got := done(t).All()
// Make sure the result looks like JSON; we comprehensively test
// the structure of this output in the command package tests.
var result map[string]interface{}
if err := json.Unmarshal([]byte(got), &result); err != nil {
t.Fatal(err)
}
})
}
}