helper/resource: print full diagnostics for operation errors in tests

This causes the output to include additional helpful context such as
the values of variables referenced in the config, etc. The output is in
the same format as normal Terraform CLI error output, though we don't
retain a source code cache in this codepath so it will not include a
source code snippet.
This commit is contained in:
Martin Atkins 2018-12-07 12:17:55 -08:00
parent 53926ea581
commit f9fef56167
2 changed files with 55 additions and 8 deletions

View File

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

View File

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