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:
parent
53926ea581
commit
f9fef56167
|
@ -1,6 +1,7 @@
|
||||||
package resource
|
package resource
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -18,14 +19,17 @@ import (
|
||||||
"github.com/hashicorp/errwrap"
|
"github.com/hashicorp/errwrap"
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
"github.com/hashicorp/logutils"
|
"github.com/hashicorp/logutils"
|
||||||
|
"github.com/mitchellh/colorstring"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/addrs"
|
"github.com/hashicorp/terraform/addrs"
|
||||||
|
"github.com/hashicorp/terraform/command/format"
|
||||||
"github.com/hashicorp/terraform/configs"
|
"github.com/hashicorp/terraform/configs"
|
||||||
"github.com/hashicorp/terraform/configs/configload"
|
"github.com/hashicorp/terraform/configs/configload"
|
||||||
"github.com/hashicorp/terraform/helper/logging"
|
"github.com/hashicorp/terraform/helper/logging"
|
||||||
"github.com/hashicorp/terraform/providers"
|
"github.com/hashicorp/terraform/providers"
|
||||||
"github.com/hashicorp/terraform/states"
|
"github.com/hashicorp/terraform/states"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
"github.com/hashicorp/terraform/tfdiags"
|
||||||
)
|
)
|
||||||
|
|
||||||
// flagSweep is a flag available when running tests on the command line. It
|
// flagSweep is a flag available when running tests on the command line. It
|
||||||
|
@ -541,8 +545,7 @@ func Test(t TestT, c TestCase) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
errored = true
|
errored = true
|
||||||
t.Error(fmt.Sprintf(
|
t.Error(fmt.Sprintf("Step %d error: %s", i, detailedErrorMessage(err)))
|
||||||
"Step %d error: %s", i, err))
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1223,3 +1226,47 @@ func primaryInstanceState(s *terraform.State, name string) (*terraform.InstanceS
|
||||||
ms := s.RootModule()
|
ms := s.RootModule()
|
||||||
return modulePrimaryInstanceState(s, ms, name)
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -74,7 +74,7 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if stepDiags.HasErrors() {
|
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
|
// 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 {
|
if !step.PlanOnly {
|
||||||
// Plan!
|
// Plan!
|
||||||
if p, stepDiags := ctx.Plan(); stepDiags.HasErrors() {
|
if p, stepDiags := ctx.Plan(); stepDiags.HasErrors() {
|
||||||
return state, fmt.Errorf("Error planning: %s", stepDiags.Err())
|
return state, newOperationError("plan", stepDiags)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[WARN] Test: Step plan: %s", legacyPlanComparisonString(newState, p.Changes))
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
if stepDiags.HasErrors() {
|
if stepDiags.HasErrors() {
|
||||||
return state, fmt.Errorf("Error applying: %s", stepDiags.Err())
|
return state, newOperationError("apply", stepDiags)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run any configured checks
|
// 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.
|
// We do this with TWO plans. One without a refresh.
|
||||||
var p *plans.Plan
|
var p *plans.Plan
|
||||||
if p, stepDiags = ctx.Plan(); stepDiags.HasErrors() {
|
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 !p.Changes.Empty() {
|
||||||
if step.ExpectNonEmptyPlan {
|
if step.ExpectNonEmptyPlan {
|
||||||
|
@ -136,7 +136,7 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep)
|
||||||
if !step.Destroy || (step.Destroy && !step.PreventPostDestroyRefresh) {
|
if !step.Destroy || (step.Destroy && !step.PreventPostDestroyRefresh) {
|
||||||
newState, stepDiags = ctx.Refresh()
|
newState, stepDiags = ctx.Refresh()
|
||||||
if stepDiags.HasErrors() {
|
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)
|
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() {
|
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()
|
empty := p.Changes.Empty()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue