diff --git a/helper/resource/testing.go b/helper/resource/testing.go index 4f50276e7f..40acfbf3d8 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -1,6 +1,7 @@ package resource import ( + "bytes" "flag" "fmt" "io" @@ -18,14 +19,17 @@ import ( "github.com/hashicorp/errwrap" "github.com/hashicorp/go-multierror" "github.com/hashicorp/logutils" + "github.com/mitchellh/colorstring" "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/command/format" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/configs/configload" "github.com/hashicorp/terraform/helper/logging" "github.com/hashicorp/terraform/providers" "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/tfdiags" ) // flagSweep is a flag available when running tests on the command line. It @@ -541,8 +545,7 @@ func Test(t TestT, c TestCase) { } } else { errored = true - t.Error(fmt.Sprintf( - "Step %d error: %s", i, err)) + t.Error(fmt.Sprintf("Step %d error: %s", i, detailedErrorMessage(err))) break } } @@ -1223,3 +1226,47 @@ func primaryInstanceState(s *terraform.State, name string) (*terraform.InstanceS ms := s.RootModule() return modulePrimaryInstanceState(s, ms, name) } + +// operationError is a specialized implementation of error used to describe +// failures during one of the several operations performed for a particular +// test case. +type operationError struct { + OpName string + Diags tfdiags.Diagnostics +} + +func newOperationError(opName string, diags tfdiags.Diagnostics) error { + return operationError{opName, diags} +} + +// Error returns a terse error string containing just the basic diagnostic +// messages, for situations where normal Go error behavior is appropriate. +func (err operationError) Error() string { + return fmt.Sprintf("errors during %s: %s", err.OpName, err.Diags.Err().Error()) +} + +// ErrorDetail is like Error except it includes verbosely-rendered diagnostics +// similar to what would come from a normal Terraform run, which include +// additional context not included in Error(). +func (err operationError) ErrorDetail() string { + var buf bytes.Buffer + fmt.Fprintf(&buf, "errors during %s:", err.OpName) + clr := &colorstring.Colorize{Disable: true, Colors: colorstring.DefaultColors} + for _, diag := range err.Diags { + diagStr := format.Diagnostic(diag, nil, clr, 78) + buf.WriteByte('\n') + buf.WriteString(diagStr) + } + return buf.String() +} + +// detailedErrorMessage is a helper for calling ErrorDetail on an error if +// it is an operationError or just taking Error otherwise. +func detailedErrorMessage(err error) string { + switch tErr := err.(type) { + case operationError: + return tErr.ErrorDetail() + default: + return err.Error() + } +} diff --git a/helper/resource/testing_config.go b/helper/resource/testing_config.go index 1f87627ad4..fd53243508 100644 --- a/helper/resource/testing_config.go +++ b/helper/resource/testing_config.go @@ -74,7 +74,7 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep) return nil, err } if stepDiags.HasErrors() { - return state, fmt.Errorf("Error refreshing: %s", stepDiags.Err()) + return state, newOperationError("refresh", stepDiags) } // If this step is a PlanOnly step, skip over this first Plan and subsequent @@ -82,7 +82,7 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep) if !step.PlanOnly { // Plan! if p, stepDiags := ctx.Plan(); stepDiags.HasErrors() { - return state, fmt.Errorf("Error planning: %s", stepDiags.Err()) + return state, newOperationError("plan", stepDiags) } else { log.Printf("[WARN] Test: Step plan: %s", legacyPlanComparisonString(newState, p.Changes)) } @@ -100,7 +100,7 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep) return nil, err } if stepDiags.HasErrors() { - return state, fmt.Errorf("Error applying: %s", stepDiags.Err()) + return state, newOperationError("apply", stepDiags) } // Run any configured checks @@ -121,7 +121,7 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep) // We do this with TWO plans. One without a refresh. var p *plans.Plan if p, stepDiags = ctx.Plan(); stepDiags.HasErrors() { - return state, fmt.Errorf("Error on follow-up plan: %s", stepDiags.Err()) + return state, newOperationError("follow-up plan", stepDiags) } if !p.Changes.Empty() { if step.ExpectNonEmptyPlan { @@ -136,7 +136,7 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep) if !step.Destroy || (step.Destroy && !step.PreventPostDestroyRefresh) { newState, stepDiags = ctx.Refresh() if stepDiags.HasErrors() { - return state, fmt.Errorf("Error on follow-up refresh: %s", stepDiags.Err()) + return state, newOperationError("follow-up refresh", stepDiags) } state, err = shimNewState(newState, schemas) @@ -145,7 +145,7 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep) } } if p, stepDiags = ctx.Plan(); stepDiags.HasErrors() { - return state, fmt.Errorf("Error on second follow-up plan: %s", stepDiags.Err()) + return state, newOperationError("second follow-up refresh", stepDiags) } empty := p.Changes.Empty()