mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-26 00:41:27 -06:00
3822650e15
There is some existing practice in the "terraform" package of returning a special error type ValidationError from EvalNode implementations in order to return warnings without halting the graph walk even though a non-nil error was returned. This is a diagnostics-flavored version of that approach, allowing us to avoid totally reworking the EvalNode concept around diagnostics and retaining the ability to return non-fatal errors. NonFatalErr is equivalent to the former terraform.ValidationError, while ErrWithWarnings is a helper that automatically treats any errors as fatal but returns NonFatalError if the diagnostics contains only warnings.
440 lines
11 KiB
Go
440 lines
11 KiB
Go
package tfdiags
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/go-multierror"
|
|
|
|
"github.com/davecgh/go-spew/spew"
|
|
"github.com/hashicorp/hcl2/hcl"
|
|
)
|
|
|
|
func TestBuild(t *testing.T) {
|
|
type diagFlat struct {
|
|
Severity Severity
|
|
Summary string
|
|
Detail string
|
|
Subject *SourceRange
|
|
Context *SourceRange
|
|
}
|
|
|
|
tests := map[string]struct {
|
|
Cons func(Diagnostics) Diagnostics
|
|
Want []diagFlat
|
|
}{
|
|
"nil": {
|
|
func(diags Diagnostics) Diagnostics {
|
|
return diags
|
|
},
|
|
nil,
|
|
},
|
|
"fmt.Errorf": {
|
|
func(diags Diagnostics) Diagnostics {
|
|
diags = diags.Append(fmt.Errorf("oh no bad"))
|
|
return diags
|
|
},
|
|
[]diagFlat{
|
|
{
|
|
Severity: Error,
|
|
Summary: "oh no bad",
|
|
},
|
|
},
|
|
},
|
|
"errors.New": {
|
|
func(diags Diagnostics) Diagnostics {
|
|
diags = diags.Append(errors.New("oh no bad"))
|
|
return diags
|
|
},
|
|
[]diagFlat{
|
|
{
|
|
Severity: Error,
|
|
Summary: "oh no bad",
|
|
},
|
|
},
|
|
},
|
|
"hcl.Diagnostic": {
|
|
func(diags Diagnostics) Diagnostics {
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Something bad happened",
|
|
Detail: "It was really, really bad.",
|
|
Subject: &hcl.Range{
|
|
Filename: "foo.tf",
|
|
Start: hcl.Pos{Line: 1, Column: 10, Byte: 9},
|
|
End: hcl.Pos{Line: 2, Column: 3, Byte: 25},
|
|
},
|
|
Context: &hcl.Range{
|
|
Filename: "foo.tf",
|
|
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
|
|
End: hcl.Pos{Line: 3, Column: 1, Byte: 30},
|
|
},
|
|
})
|
|
return diags
|
|
},
|
|
[]diagFlat{
|
|
{
|
|
Severity: Error,
|
|
Summary: "Something bad happened",
|
|
Detail: "It was really, really bad.",
|
|
Subject: &SourceRange{
|
|
Filename: "foo.tf",
|
|
Start: SourcePos{Line: 1, Column: 10, Byte: 9},
|
|
End: SourcePos{Line: 2, Column: 3, Byte: 25},
|
|
},
|
|
Context: &SourceRange{
|
|
Filename: "foo.tf",
|
|
Start: SourcePos{Line: 1, Column: 1, Byte: 0},
|
|
End: SourcePos{Line: 3, Column: 1, Byte: 30},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"hcl.Diagnostics": {
|
|
func(diags Diagnostics) Diagnostics {
|
|
diags = diags.Append(hcl.Diagnostics{
|
|
{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Something bad happened",
|
|
Detail: "It was really, really bad.",
|
|
},
|
|
{
|
|
Severity: hcl.DiagWarning,
|
|
Summary: "Also, somebody sneezed",
|
|
Detail: "How rude!",
|
|
},
|
|
})
|
|
return diags
|
|
},
|
|
[]diagFlat{
|
|
{
|
|
Severity: Error,
|
|
Summary: "Something bad happened",
|
|
Detail: "It was really, really bad.",
|
|
},
|
|
{
|
|
Severity: Warning,
|
|
Summary: "Also, somebody sneezed",
|
|
Detail: "How rude!",
|
|
},
|
|
},
|
|
},
|
|
"multierror.Error": {
|
|
func(diags Diagnostics) Diagnostics {
|
|
err := multierror.Append(nil, errors.New("bad thing A"))
|
|
err = multierror.Append(err, errors.New("bad thing B"))
|
|
diags = diags.Append(err)
|
|
return diags
|
|
},
|
|
[]diagFlat{
|
|
{
|
|
Severity: Error,
|
|
Summary: "bad thing A",
|
|
},
|
|
{
|
|
Severity: Error,
|
|
Summary: "bad thing B",
|
|
},
|
|
},
|
|
},
|
|
"concat Diagnostics": {
|
|
func(diags Diagnostics) Diagnostics {
|
|
var moreDiags Diagnostics
|
|
moreDiags = moreDiags.Append(errors.New("bad thing A"))
|
|
moreDiags = moreDiags.Append(errors.New("bad thing B"))
|
|
return diags.Append(moreDiags)
|
|
},
|
|
[]diagFlat{
|
|
{
|
|
Severity: Error,
|
|
Summary: "bad thing A",
|
|
},
|
|
{
|
|
Severity: Error,
|
|
Summary: "bad thing B",
|
|
},
|
|
},
|
|
},
|
|
"single Diagnostic": {
|
|
func(diags Diagnostics) Diagnostics {
|
|
return diags.Append(SimpleWarning("Don't forget your toothbrush!"))
|
|
},
|
|
[]diagFlat{
|
|
{
|
|
Severity: Warning,
|
|
Summary: "Don't forget your toothbrush!",
|
|
},
|
|
},
|
|
},
|
|
"multiple appends": {
|
|
func(diags Diagnostics) Diagnostics {
|
|
diags = diags.Append(SimpleWarning("Don't forget your toothbrush!"))
|
|
diags = diags.Append(fmt.Errorf("exploded"))
|
|
return diags
|
|
},
|
|
[]diagFlat{
|
|
{
|
|
Severity: Warning,
|
|
Summary: "Don't forget your toothbrush!",
|
|
},
|
|
{
|
|
Severity: Error,
|
|
Summary: "exploded",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
gotDiags := test.Cons(nil)
|
|
var got []diagFlat
|
|
for _, item := range gotDiags {
|
|
desc := item.Description()
|
|
source := item.Source()
|
|
got = append(got, diagFlat{
|
|
Severity: item.Severity(),
|
|
Summary: desc.Summary,
|
|
Detail: desc.Detail,
|
|
Subject: source.Subject,
|
|
Context: source.Context,
|
|
})
|
|
}
|
|
|
|
if !reflect.DeepEqual(got, test.Want) {
|
|
t.Errorf("wrong result\ngot: %swant: %s", spew.Sdump(got), spew.Sdump(test.Want))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDiagnosticsErr(t *testing.T) {
|
|
t.Run("empty", func(t *testing.T) {
|
|
var diags Diagnostics
|
|
err := diags.Err()
|
|
if err != nil {
|
|
t.Errorf("got non-nil error %#v; want nil", err)
|
|
}
|
|
})
|
|
t.Run("warning only", func(t *testing.T) {
|
|
var diags Diagnostics
|
|
diags = diags.Append(SimpleWarning("bad"))
|
|
err := diags.Err()
|
|
if err != nil {
|
|
t.Errorf("got non-nil error %#v; want nil", err)
|
|
}
|
|
})
|
|
t.Run("one error", func(t *testing.T) {
|
|
var diags Diagnostics
|
|
diags = diags.Append(errors.New("didn't work"))
|
|
err := diags.Err()
|
|
if err == nil {
|
|
t.Fatalf("got nil error %#v; want non-nil", err)
|
|
}
|
|
if got, want := err.Error(), "didn't work"; got != want {
|
|
t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
})
|
|
t.Run("two errors", func(t *testing.T) {
|
|
var diags Diagnostics
|
|
diags = diags.Append(errors.New("didn't work"))
|
|
diags = diags.Append(errors.New("didn't work either"))
|
|
err := diags.Err()
|
|
if err == nil {
|
|
t.Fatalf("got nil error %#v; want non-nil", err)
|
|
}
|
|
want := strings.TrimSpace(`
|
|
2 problems:
|
|
|
|
- didn't work
|
|
- didn't work either
|
|
`)
|
|
if got := err.Error(); got != want {
|
|
t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
})
|
|
t.Run("error and warning", func(t *testing.T) {
|
|
var diags Diagnostics
|
|
diags = diags.Append(errors.New("didn't work"))
|
|
diags = diags.Append(SimpleWarning("didn't work either"))
|
|
err := diags.Err()
|
|
if err == nil {
|
|
t.Fatalf("got nil error %#v; want non-nil", err)
|
|
}
|
|
// Since this "as error" mode is just a fallback for
|
|
// non-diagnostics-aware situations like tests, we don't actually
|
|
// distinguish warnings and errors here since the point is to just
|
|
// get the messages rendered. User-facing code should be printing
|
|
// each diagnostic separately, so won't enter this codepath,
|
|
want := strings.TrimSpace(`
|
|
2 problems:
|
|
|
|
- didn't work
|
|
- didn't work either
|
|
`)
|
|
if got := err.Error(); got != want {
|
|
t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestDiagnosticsErrWithWarnings(t *testing.T) {
|
|
t.Run("empty", func(t *testing.T) {
|
|
var diags Diagnostics
|
|
err := diags.ErrWithWarnings()
|
|
if err != nil {
|
|
t.Errorf("got non-nil error %#v; want nil", err)
|
|
}
|
|
})
|
|
t.Run("warning only", func(t *testing.T) {
|
|
var diags Diagnostics
|
|
diags = diags.Append(SimpleWarning("bad"))
|
|
err := diags.ErrWithWarnings()
|
|
if err == nil {
|
|
t.Errorf("got nil error; want NonFatalError")
|
|
return
|
|
}
|
|
if _, ok := err.(NonFatalError); !ok {
|
|
t.Errorf("got %T; want NonFatalError", err)
|
|
}
|
|
})
|
|
t.Run("one error", func(t *testing.T) {
|
|
var diags Diagnostics
|
|
diags = diags.Append(errors.New("didn't work"))
|
|
err := diags.ErrWithWarnings()
|
|
if err == nil {
|
|
t.Fatalf("got nil error %#v; want non-nil", err)
|
|
}
|
|
if got, want := err.Error(), "didn't work"; got != want {
|
|
t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
})
|
|
t.Run("two errors", func(t *testing.T) {
|
|
var diags Diagnostics
|
|
diags = diags.Append(errors.New("didn't work"))
|
|
diags = diags.Append(errors.New("didn't work either"))
|
|
err := diags.ErrWithWarnings()
|
|
if err == nil {
|
|
t.Fatalf("got nil error %#v; want non-nil", err)
|
|
}
|
|
want := strings.TrimSpace(`
|
|
2 problems:
|
|
|
|
- didn't work
|
|
- didn't work either
|
|
`)
|
|
if got := err.Error(); got != want {
|
|
t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
})
|
|
t.Run("error and warning", func(t *testing.T) {
|
|
var diags Diagnostics
|
|
diags = diags.Append(errors.New("didn't work"))
|
|
diags = diags.Append(SimpleWarning("didn't work either"))
|
|
err := diags.ErrWithWarnings()
|
|
if err == nil {
|
|
t.Fatalf("got nil error %#v; want non-nil", err)
|
|
}
|
|
// Since this "as error" mode is just a fallback for
|
|
// non-diagnostics-aware situations like tests, we don't actually
|
|
// distinguish warnings and errors here since the point is to just
|
|
// get the messages rendered. User-facing code should be printing
|
|
// each diagnostic separately, so won't enter this codepath,
|
|
want := strings.TrimSpace(`
|
|
2 problems:
|
|
|
|
- didn't work
|
|
- didn't work either
|
|
`)
|
|
if got := err.Error(); got != want {
|
|
t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestDiagnosticsNonFatalErr(t *testing.T) {
|
|
t.Run("empty", func(t *testing.T) {
|
|
var diags Diagnostics
|
|
err := diags.NonFatalErr()
|
|
if err != nil {
|
|
t.Errorf("got non-nil error %#v; want nil", err)
|
|
}
|
|
})
|
|
t.Run("warning only", func(t *testing.T) {
|
|
var diags Diagnostics
|
|
diags = diags.Append(SimpleWarning("bad"))
|
|
err := diags.NonFatalErr()
|
|
if err == nil {
|
|
t.Errorf("got nil error; want NonFatalError")
|
|
return
|
|
}
|
|
if _, ok := err.(NonFatalError); !ok {
|
|
t.Errorf("got %T; want NonFatalError", err)
|
|
}
|
|
})
|
|
t.Run("one error", func(t *testing.T) {
|
|
var diags Diagnostics
|
|
diags = diags.Append(errors.New("didn't work"))
|
|
err := diags.NonFatalErr()
|
|
if err == nil {
|
|
t.Fatalf("got nil error %#v; want non-nil", err)
|
|
}
|
|
if got, want := err.Error(), "didn't work"; got != want {
|
|
t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
if _, ok := err.(NonFatalError); !ok {
|
|
t.Errorf("got %T; want NonFatalError", err)
|
|
}
|
|
})
|
|
t.Run("two errors", func(t *testing.T) {
|
|
var diags Diagnostics
|
|
diags = diags.Append(errors.New("didn't work"))
|
|
diags = diags.Append(errors.New("didn't work either"))
|
|
err := diags.NonFatalErr()
|
|
if err == nil {
|
|
t.Fatalf("got nil error %#v; want non-nil", err)
|
|
}
|
|
want := strings.TrimSpace(`
|
|
2 problems:
|
|
|
|
- didn't work
|
|
- didn't work either
|
|
`)
|
|
if got := err.Error(); got != want {
|
|
t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
if _, ok := err.(NonFatalError); !ok {
|
|
t.Errorf("got %T; want NonFatalError", err)
|
|
}
|
|
})
|
|
t.Run("error and warning", func(t *testing.T) {
|
|
var diags Diagnostics
|
|
diags = diags.Append(errors.New("didn't work"))
|
|
diags = diags.Append(SimpleWarning("didn't work either"))
|
|
err := diags.NonFatalErr()
|
|
if err == nil {
|
|
t.Fatalf("got nil error %#v; want non-nil", err)
|
|
}
|
|
// Since this "as error" mode is just a fallback for
|
|
// non-diagnostics-aware situations like tests, we don't actually
|
|
// distinguish warnings and errors here since the point is to just
|
|
// get the messages rendered. User-facing code should be printing
|
|
// each diagnostic separately, so won't enter this codepath,
|
|
want := strings.TrimSpace(`
|
|
2 problems:
|
|
|
|
- didn't work
|
|
- didn't work either
|
|
`)
|
|
if got := err.Error(); got != want {
|
|
t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
if _, ok := err.(NonFatalError); !ok {
|
|
t.Errorf("got %T; want NonFatalError", err)
|
|
}
|
|
})
|
|
}
|