mirror of https://github.com/hashicorp/terraform
[testing framework] implement expect_failures functionality (#33443)
parent
2622e89cfb
commit
f74a8d16cf
@ -0,0 +1,70 @@
|
||||
package addrs
|
||||
|
||||
import "github.com/hashicorp/terraform/internal/tfdiags"
|
||||
|
||||
// DiagnosticExtraCheckRule provides an interface for diagnostic ExtraInfo to
|
||||
// retrieve an embedded CheckRule from within a tfdiags.Diagnostic.
|
||||
type DiagnosticExtraCheckRule interface {
|
||||
// DiagnosticOriginatesFromCheckRule returns the CheckRule that the
|
||||
// surrounding diagnostic originated from.
|
||||
DiagnosticOriginatesFromCheckRule() CheckRule
|
||||
}
|
||||
|
||||
// DiagnosticOriginatesFromCheckRule checks if the provided diagnostic contains
|
||||
// a CheckRule as ExtraInfo and returns that CheckRule and true if it does. This
|
||||
// function returns an empty CheckRule and false if the diagnostic does not
|
||||
// contain a CheckRule.
|
||||
func DiagnosticOriginatesFromCheckRule(diag tfdiags.Diagnostic) (CheckRule, bool) {
|
||||
maybe := tfdiags.ExtraInfo[DiagnosticExtraCheckRule](diag)
|
||||
if maybe == nil {
|
||||
return CheckRule{}, false
|
||||
}
|
||||
return maybe.DiagnosticOriginatesFromCheckRule(), true
|
||||
}
|
||||
|
||||
// CheckRuleDiagnosticExtra is an object that can be attached to diagnostics
|
||||
// that originate from check rules.
|
||||
//
|
||||
// It implements the DiagnosticExtraCheckRule interface for retrieving the
|
||||
// concrete CheckRule that spawned the diagnostic.
|
||||
//
|
||||
// It also implements the tfdiags.DiagnosticExtraDoNotConsolidate interface, to
|
||||
// stop diagnostics created by check blocks being consolidated.
|
||||
//
|
||||
// It also implements the tfdiags.DiagnosticExtraUnwrapper interface, as nested
|
||||
// data blocks will attach this struct but do want to lose any extra info
|
||||
// embedded in the original diagnostic.
|
||||
type CheckRuleDiagnosticExtra struct {
|
||||
CheckRule CheckRule
|
||||
|
||||
wrapped interface{}
|
||||
}
|
||||
|
||||
var (
|
||||
_ DiagnosticExtraCheckRule = (*CheckRuleDiagnosticExtra)(nil)
|
||||
_ tfdiags.DiagnosticExtraDoNotConsolidate = (*CheckRuleDiagnosticExtra)(nil)
|
||||
_ tfdiags.DiagnosticExtraUnwrapper = (*CheckRuleDiagnosticExtra)(nil)
|
||||
_ tfdiags.DiagnosticExtraWrapper = (*CheckRuleDiagnosticExtra)(nil)
|
||||
)
|
||||
|
||||
func (c *CheckRuleDiagnosticExtra) UnwrapDiagnosticExtra() interface{} {
|
||||
return c.wrapped
|
||||
}
|
||||
|
||||
func (c *CheckRuleDiagnosticExtra) WrapDiagnosticExtra(inner interface{}) {
|
||||
if c.wrapped != nil {
|
||||
// This is a logical inconsistency, the caller should know whether they
|
||||
// have already wrapped an extra or not.
|
||||
panic("Attempted to wrap a diagnostic extra into a CheckRuleDiagnosticExtra that is already wrapping a different extra. This is a bug in Terraform, please report it.")
|
||||
}
|
||||
c.wrapped = inner
|
||||
}
|
||||
|
||||
func (c *CheckRuleDiagnosticExtra) DoNotConsolidateDiagnostic() bool {
|
||||
// Do not consolidate warnings from check blocks.
|
||||
return c.CheckRule.Container.CheckableKind() == CheckableCheck
|
||||
}
|
||||
|
||||
func (c *CheckRuleDiagnosticExtra) DiagnosticOriginatesFromCheckRule() CheckRule {
|
||||
return c.CheckRule
|
||||
}
|
||||
@ -0,0 +1,108 @@
|
||||
package addrs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
func TestCheckRuleDiagnosticExtra_WrapsExtra(t *testing.T) {
|
||||
var originals tfdiags.Diagnostics
|
||||
originals = originals.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "original error",
|
||||
Detail: "this is an error",
|
||||
Extra: "extra",
|
||||
})
|
||||
|
||||
overridden := tfdiags.OverrideAll(originals, tfdiags.Warning, func() tfdiags.DiagnosticExtraWrapper {
|
||||
return &CheckRuleDiagnosticExtra{}
|
||||
})
|
||||
|
||||
if overridden[0].ExtraInfo().(*CheckRuleDiagnosticExtra).wrapped.(string) != "extra" {
|
||||
t.Errorf("unexpected extra info: %v", overridden[0].ExtraInfo())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckRuleDiagnosticExtra_Unwraps(t *testing.T) {
|
||||
var originals tfdiags.Diagnostics
|
||||
originals = originals.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "original error",
|
||||
Detail: "this is an error",
|
||||
Extra: "extra",
|
||||
})
|
||||
|
||||
overridden := tfdiags.OverrideAll(originals, tfdiags.Warning, func() tfdiags.DiagnosticExtraWrapper {
|
||||
return &CheckRuleDiagnosticExtra{}
|
||||
})
|
||||
|
||||
result := tfdiags.ExtraInfo[string](overridden[0])
|
||||
if result != "extra" {
|
||||
t.Errorf("unexpected extra info: %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckRuleDiagnosticExtra_DoNotConsolidate(t *testing.T) {
|
||||
var diags tfdiags.Diagnostics
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "original error",
|
||||
Detail: "this is an error",
|
||||
Extra: &CheckRuleDiagnosticExtra{
|
||||
CheckRule: NewCheckRule(AbsOutputValue{
|
||||
Module: RootModuleInstance,
|
||||
OutputValue: OutputValue{
|
||||
Name: "output",
|
||||
},
|
||||
}, OutputPrecondition, 0),
|
||||
},
|
||||
})
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "original error",
|
||||
Detail: "this is an error",
|
||||
Extra: &CheckRuleDiagnosticExtra{
|
||||
CheckRule: NewCheckRule(AbsCheck{
|
||||
Module: RootModuleInstance,
|
||||
Check: Check{
|
||||
Name: "check",
|
||||
},
|
||||
}, CheckAssertion, 0),
|
||||
},
|
||||
})
|
||||
|
||||
if tfdiags.DoNotConsolidateDiagnostic(diags[0]) {
|
||||
t.Errorf("first diag should be consolidated but was not")
|
||||
}
|
||||
|
||||
if !tfdiags.DoNotConsolidateDiagnostic(diags[1]) {
|
||||
t.Errorf("second diag should not be consolidated but was")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestDiagnosticOriginatesFromCheckRule_Passes(t *testing.T) {
|
||||
var diags tfdiags.Diagnostics
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "original error",
|
||||
Detail: "this is an error",
|
||||
})
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "original error",
|
||||
Detail: "this is an error",
|
||||
Extra: &CheckRuleDiagnosticExtra{},
|
||||
})
|
||||
|
||||
if _, ok := DiagnosticOriginatesFromCheckRule(diags[0]); ok {
|
||||
t.Errorf("first diag did not originate from check rule but thinks it did")
|
||||
}
|
||||
|
||||
if _, ok := DiagnosticOriginatesFromCheckRule(diags[1]); !ok {
|
||||
t.Errorf("second diag did originate from check rule but this it did not")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
|
||||
variable "input" {
|
||||
type = string
|
||||
}
|
||||
|
||||
|
||||
resource "foo_resource" "a" {
|
||||
value = var.input
|
||||
}
|
||||
|
||||
output "output" {
|
||||
value = foo_resource.a.value
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
variables {
|
||||
input = "default"
|
||||
}
|
||||
|
||||
run "test_run_one" {
|
||||
expect_failures = [
|
||||
input.input,
|
||||
output.output,
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
variables {
|
||||
input = "default"
|
||||
}
|
||||
|
||||
run "test_run_one" {
|
||||
expect_failures = [
|
||||
foo_resource.a,
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,692 @@
|
||||
package moduletest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/configs"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
func TestRun_ValidateExpectedFailures(t *testing.T) {
|
||||
|
||||
type output struct {
|
||||
Description tfdiags.Description
|
||||
Severity tfdiags.Severity
|
||||
}
|
||||
|
||||
tcs := map[string]struct {
|
||||
ExpectedFailures []string
|
||||
Input tfdiags.Diagnostics
|
||||
Output []output
|
||||
}{
|
||||
"empty": {
|
||||
ExpectedFailures: nil,
|
||||
Input: nil,
|
||||
Output: nil,
|
||||
},
|
||||
"carries through simple diags": {
|
||||
Input: createDiagnostics(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
|
||||
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "simple error",
|
||||
Detail: "want to see this in the returned set",
|
||||
})
|
||||
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "simple warning",
|
||||
Detail: "want to see this in the returned set",
|
||||
})
|
||||
|
||||
return diags
|
||||
}),
|
||||
Output: []output{
|
||||
{
|
||||
Description: tfdiags.Description{
|
||||
Summary: "simple error",
|
||||
Detail: "want to see this in the returned set",
|
||||
},
|
||||
Severity: tfdiags.Error,
|
||||
},
|
||||
{
|
||||
Description: tfdiags.Description{
|
||||
Summary: "simple warning",
|
||||
Detail: "want to see this in the returned set",
|
||||
},
|
||||
Severity: tfdiags.Warning,
|
||||
},
|
||||
},
|
||||
},
|
||||
"expected failures did not fail": {
|
||||
ExpectedFailures: []string{
|
||||
"check.example",
|
||||
},
|
||||
Input: nil,
|
||||
Output: []output{
|
||||
{
|
||||
Description: tfdiags.Description{
|
||||
Summary: "Missing expected failure",
|
||||
Detail: "The checkable object, check.example, was expected to report an error but did not.",
|
||||
},
|
||||
Severity: tfdiags.Error,
|
||||
},
|
||||
},
|
||||
},
|
||||
"outputs": {
|
||||
ExpectedFailures: []string{
|
||||
"output.expected_one",
|
||||
"output.expected_two",
|
||||
},
|
||||
Input: createDiagnostics(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
|
||||
|
||||
// First, let's create an output that failed that isn't
|
||||
// expected. This should be unaffected by our function.
|
||||
diags = diags.Append(
|
||||
&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "unexpected failure",
|
||||
Detail: "this should not be removed",
|
||||
Extra: &addrs.CheckRuleDiagnosticExtra{
|
||||
CheckRule: addrs.NewCheckRule(addrs.AbsOutputValue{
|
||||
Module: addrs.RootModuleInstance,
|
||||
OutputValue: addrs.OutputValue{Name: "unexpected"},
|
||||
}, addrs.OutputPrecondition, 0),
|
||||
},
|
||||
})
|
||||
|
||||
// Second, let's create an output that failed but is expected.
|
||||
// Our function should remove this from the set of diags.
|
||||
diags = diags.Append(
|
||||
&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "expected failure",
|
||||
Detail: "this should be removed",
|
||||
Extra: &addrs.CheckRuleDiagnosticExtra{
|
||||
CheckRule: addrs.NewCheckRule(addrs.AbsOutputValue{
|
||||
Module: addrs.RootModuleInstance,
|
||||
OutputValue: addrs.OutputValue{Name: "expected_one"},
|
||||
}, addrs.OutputPrecondition, 0),
|
||||
},
|
||||
})
|
||||
|
||||
diags = diags.Append(
|
||||
&hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "expected warning",
|
||||
Detail: "this should not be removed",
|
||||
Extra: &addrs.CheckRuleDiagnosticExtra{
|
||||
CheckRule: addrs.NewCheckRule(addrs.AbsOutputValue{
|
||||
Module: addrs.RootModuleInstance,
|
||||
OutputValue: addrs.OutputValue{Name: "expected_one"},
|
||||
}, addrs.OutputPrecondition, 0),
|
||||
},
|
||||
})
|
||||
|
||||
// The error we are adding here is for expected_two but in a
|
||||
// child module. We expect that this diagnostic shouldn't
|
||||
// trigger our expected failure, and that an extra diagnostic
|
||||
// should be created complaining that the output wasn't actually
|
||||
// triggered.
|
||||
|
||||
diags = diags.Append(
|
||||
&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "error in child module",
|
||||
Detail: "this should not be removed",
|
||||
Extra: &addrs.CheckRuleDiagnosticExtra{
|
||||
CheckRule: addrs.NewCheckRule(addrs.AbsOutputValue{
|
||||
Module: []addrs.ModuleInstanceStep{
|
||||
{
|
||||
Name: "child_module",
|
||||
},
|
||||
},
|
||||
OutputValue: addrs.OutputValue{Name: "expected_two"},
|
||||
}, addrs.OutputPrecondition, 0),
|
||||
},
|
||||
})
|
||||
|
||||
return diags
|
||||
}),
|
||||
Output: []output{
|
||||
{
|
||||
Description: tfdiags.Description{
|
||||
Summary: "unexpected failure",
|
||||
Detail: "this should not be removed",
|
||||
},
|
||||
Severity: tfdiags.Error,
|
||||
},
|
||||
{
|
||||
Description: tfdiags.Description{
|
||||
Summary: "expected warning",
|
||||
Detail: "this should not be removed",
|
||||
},
|
||||
Severity: tfdiags.Warning,
|
||||
},
|
||||
{
|
||||
Description: tfdiags.Description{
|
||||
Summary: "error in child module",
|
||||
Detail: "this should not be removed",
|
||||
},
|
||||
Severity: tfdiags.Error,
|
||||
},
|
||||
{
|
||||
Description: tfdiags.Description{
|
||||
Summary: "Missing expected failure",
|
||||
Detail: "The checkable object, output.expected_two, was expected to report an error but did not.",
|
||||
},
|
||||
Severity: tfdiags.Error,
|
||||
},
|
||||
},
|
||||
},
|
||||
"resources": {
|
||||
ExpectedFailures: []string{
|
||||
"test_instance.single",
|
||||
"test_instance.all_instances",
|
||||
"test_instance.instance[0]",
|
||||
"test_instance.instance[2]",
|
||||
"test_instance.missing",
|
||||
},
|
||||
Input: createDiagnostics(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
|
||||
// First, we'll create an unexpected failure that should be
|
||||
// carried through untouched.
|
||||
diags = diags.Append(
|
||||
&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "unexpected failure",
|
||||
Detail: "this should not be removed",
|
||||
Extra: &addrs.CheckRuleDiagnosticExtra{
|
||||
CheckRule: addrs.NewCheckRule(addrs.AbsResourceInstance{
|
||||
Module: addrs.RootModuleInstance,
|
||||
Resource: addrs.ResourceInstance{
|
||||
Resource: addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_instance",
|
||||
Name: "unexpected",
|
||||
},
|
||||
},
|
||||
}, addrs.ResourcePrecondition, 0),
|
||||
},
|
||||
})
|
||||
|
||||
// Second, we'll create a failure from our test_instance.single
|
||||
// resource that should be removed.
|
||||
diags = diags.Append(
|
||||
&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "expected failure in test_instance.single",
|
||||
Detail: "this should be removed",
|
||||
Extra: &addrs.CheckRuleDiagnosticExtra{
|
||||
CheckRule: addrs.NewCheckRule(addrs.AbsResourceInstance{
|
||||
Module: addrs.RootModuleInstance,
|
||||
Resource: addrs.ResourceInstance{
|
||||
Resource: addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_instance",
|
||||
Name: "single",
|
||||
},
|
||||
},
|
||||
}, addrs.ResourcePrecondition, 0),
|
||||
},
|
||||
})
|
||||
|
||||
// Third, we'll create a warning from our test_instance.single
|
||||
// resource that should be propagated as it is only a warning.
|
||||
diags = diags.Append(
|
||||
&hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "expected warning in test_instance.single",
|
||||
Detail: "this should not be removed",
|
||||
Extra: &addrs.CheckRuleDiagnosticExtra{
|
||||
CheckRule: addrs.NewCheckRule(addrs.AbsResourceInstance{
|
||||
Module: addrs.RootModuleInstance,
|
||||
Resource: addrs.ResourceInstance{
|
||||
Resource: addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_instance",
|
||||
Name: "single",
|
||||
},
|
||||
},
|
||||
}, addrs.ResourcePrecondition, 0),
|
||||
},
|
||||
})
|
||||
|
||||
// Fourth, we'll create diagnostics from several instances of
|
||||
// the test_instance.all_instances which should all be removed.
|
||||
diags = diags.Append(
|
||||
&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "expected failure in test_instance.all_instances[0]",
|
||||
Detail: "this should be removed",
|
||||
Extra: &addrs.CheckRuleDiagnosticExtra{
|
||||
CheckRule: addrs.NewCheckRule(addrs.AbsResourceInstance{
|
||||
Module: addrs.RootModuleInstance,
|
||||
Resource: addrs.ResourceInstance{
|
||||
Resource: addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_instance",
|
||||
Name: "all_instances",
|
||||
},
|
||||
Key: addrs.IntKey(0),
|
||||
},
|
||||
}, addrs.ResourcePrecondition, 0),
|
||||
},
|
||||
})
|
||||
diags = diags.Append(
|
||||
&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "expected failure in test_instance.all_instances[1]",
|
||||
Detail: "this should be removed",
|
||||
Extra: &addrs.CheckRuleDiagnosticExtra{
|
||||
CheckRule: addrs.NewCheckRule(addrs.AbsResourceInstance{
|
||||
Module: addrs.RootModuleInstance,
|
||||
Resource: addrs.ResourceInstance{
|
||||
Resource: addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_instance",
|
||||
Name: "all_instances",
|
||||
},
|
||||
Key: addrs.IntKey(1),
|
||||
},
|
||||
}, addrs.ResourcePrecondition, 0),
|
||||
},
|
||||
})
|
||||
diags = diags.Append(
|
||||
&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "expected failure in test_instance.all_instances[2]",
|
||||
Detail: "this should be removed",
|
||||
Extra: &addrs.CheckRuleDiagnosticExtra{
|
||||
CheckRule: addrs.NewCheckRule(addrs.AbsResourceInstance{
|
||||
Module: addrs.RootModuleInstance,
|
||||
Resource: addrs.ResourceInstance{
|
||||
Resource: addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_instance",
|
||||
Name: "all_instances",
|
||||
},
|
||||
Key: addrs.IntKey(2),
|
||||
},
|
||||
}, addrs.ResourcePrecondition, 0),
|
||||
},
|
||||
})
|
||||
|
||||
// Fifth, we'll create diagnostics for several instances of
|
||||
// the test_instance.instance resource, only some of which
|
||||
// should be removed.
|
||||
diags = diags.Append(
|
||||
&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "expected failure in test_instance.instance[0]",
|
||||
Detail: "this should be removed",
|
||||
Extra: &addrs.CheckRuleDiagnosticExtra{
|
||||
CheckRule: addrs.NewCheckRule(addrs.AbsResourceInstance{
|
||||
Module: addrs.RootModuleInstance,
|
||||
Resource: addrs.ResourceInstance{
|
||||
Resource: addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_instance",
|
||||
Name: "instance",
|
||||
},
|
||||
Key: addrs.IntKey(0),
|
||||
},
|
||||
}, addrs.ResourcePrecondition, 0),
|
||||
},
|
||||
})
|
||||
diags = diags.Append(
|
||||
&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "expected failure in test_instance.instance[1]",
|
||||
Detail: "this should not be removed",
|
||||
Extra: &addrs.CheckRuleDiagnosticExtra{
|
||||
CheckRule: addrs.NewCheckRule(addrs.AbsResourceInstance{
|
||||
Module: addrs.RootModuleInstance,
|
||||
Resource: addrs.ResourceInstance{
|
||||
Resource: addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_instance",
|
||||
Name: "instance",
|
||||
},
|
||||
Key: addrs.IntKey(1),
|
||||
},
|
||||
}, addrs.ResourcePrecondition, 0),
|
||||
},
|
||||
})
|
||||
diags = diags.Append(
|
||||
&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "expected failure in test_instance.instance[2]",
|
||||
Detail: "this should be removed",
|
||||
Extra: &addrs.CheckRuleDiagnosticExtra{
|
||||
CheckRule: addrs.NewCheckRule(addrs.AbsResourceInstance{
|
||||
Module: addrs.RootModuleInstance,
|
||||
Resource: addrs.ResourceInstance{
|
||||
Resource: addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_instance",
|
||||
Name: "instance",
|
||||
},
|
||||
Key: addrs.IntKey(2),
|
||||
},
|
||||
}, addrs.ResourcePrecondition, 0),
|
||||
},
|
||||
})
|
||||
|
||||
// Finally, we'll create an error that originated from
|
||||
// test_instance.missing but in a child module which shouldn't
|
||||
// be removed.
|
||||
diags = diags.Append(
|
||||
&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "failure in child module",
|
||||
Detail: "this should not be removed",
|
||||
Extra: &addrs.CheckRuleDiagnosticExtra{
|
||||
CheckRule: addrs.NewCheckRule(addrs.AbsResourceInstance{
|
||||
Module: []addrs.ModuleInstanceStep{
|
||||
{
|
||||
Name: "child_module",
|
||||
},
|
||||
},
|
||||
Resource: addrs.ResourceInstance{
|
||||
Resource: addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_instance",
|
||||
Name: "missing",
|
||||
},
|
||||
},
|
||||
}, addrs.ResourcePrecondition, 0),
|
||||
},
|
||||
})
|
||||
|
||||
return diags
|
||||
}),
|
||||
Output: []output{
|
||||
{
|
||||
Description: tfdiags.Description{
|
||||
Summary: "unexpected failure",
|
||||
Detail: "this should not be removed",
|
||||
},
|
||||
Severity: tfdiags.Error,
|
||||
},
|
||||
{
|
||||
Description: tfdiags.Description{
|
||||
Summary: "expected warning in test_instance.single",
|
||||
Detail: "this should not be removed",
|
||||
},
|
||||
Severity: tfdiags.Warning,
|
||||
},
|
||||
{
|
||||
Description: tfdiags.Description{
|
||||
Summary: "expected failure in test_instance.instance[1]",
|
||||
Detail: "this should not be removed",
|
||||
},
|
||||
Severity: tfdiags.Error,
|
||||
},
|
||||
{
|
||||
Description: tfdiags.Description{
|
||||
Summary: "failure in child module",
|
||||
Detail: "this should not be removed",
|
||||
},
|
||||
Severity: tfdiags.Error,
|
||||
},
|
||||
{
|
||||
Description: tfdiags.Description{
|
||||
Summary: "Missing expected failure",
|
||||
Detail: "The checkable object, test_instance.missing, was expected to report an error but did not.",
|
||||
},
|
||||
Severity: tfdiags.Error,
|
||||
},
|
||||
},
|
||||
},
|
||||
"check_assertions": {
|
||||
ExpectedFailures: []string{
|
||||
"check.expected",
|
||||
"check.missing",
|
||||
},
|
||||
Input: createDiagnostics(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
|
||||
// First, we'll add an unexpected warning from a check block
|
||||
// assertion that should get upgraded to an error.
|
||||
diags = diags.Append(
|
||||
&hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "unexpected failure",
|
||||
Detail: "this should upgrade and not be removed",
|
||||
Extra: &addrs.CheckRuleDiagnosticExtra{
|
||||
CheckRule: addrs.NewCheckRule(addrs.AbsCheck{
|
||||
Module: addrs.RootModuleInstance,
|
||||
Check: addrs.Check{
|
||||
Name: "unexpected",
|
||||
},
|
||||
}, addrs.CheckAssertion, 0),
|
||||
},
|
||||
})
|
||||
|
||||
// Second, we'll add an unexpected warning from a check block
|
||||
// in a child module that should get upgrade to error.
|
||||
diags = diags.Append(
|
||||
&hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "expected failure in child module",
|
||||
Detail: "this should upgrade and not be removed",
|
||||
Extra: &addrs.CheckRuleDiagnosticExtra{
|
||||
CheckRule: addrs.NewCheckRule(addrs.AbsCheck{
|
||||
Module: []addrs.ModuleInstanceStep{
|
||||
{
|
||||
Name: "child_module",
|
||||
},
|
||||
},
|
||||
Check: addrs.Check{
|
||||
Name: "expected",
|
||||
},
|
||||
}, addrs.CheckAssertion, 0),
|
||||
},
|
||||
})
|
||||
|
||||
// Third, we'll add an expected warning from a check block
|
||||
// assertion that should be removed.
|
||||
diags = diags.Append(
|
||||
&hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "expected failure",
|
||||
Detail: "this should be removed",
|
||||
Extra: &addrs.CheckRuleDiagnosticExtra{
|
||||
CheckRule: addrs.NewCheckRule(addrs.AbsCheck{
|
||||
Module: addrs.RootModuleInstance,
|
||||
Check: addrs.Check{
|
||||
Name: "expected",
|
||||
},
|
||||
}, addrs.CheckAssertion, 0),
|
||||
},
|
||||
})
|
||||
|
||||
// The second expected failure has no diagnostics, we just want
|
||||
// to make sure that a new diagnostic is added for this case.
|
||||
|
||||
return diags
|
||||
}),
|
||||
Output: []output{
|
||||
{
|
||||
Description: tfdiags.Description{
|
||||
Summary: "unexpected failure",
|
||||
Detail: "this should upgrade and not be removed",
|
||||
},
|
||||
Severity: tfdiags.Error,
|
||||
},
|
||||
{
|
||||
Description: tfdiags.Description{
|
||||
Summary: "expected failure in child module",
|
||||
Detail: "this should upgrade and not be removed",
|
||||
},
|
||||
Severity: tfdiags.Error,
|
||||
},
|
||||
{
|
||||
Description: tfdiags.Description{
|
||||
Summary: "Missing expected failure",
|
||||
Detail: "The checkable object, check.missing, was expected to report an error but did not.",
|
||||
},
|
||||
Severity: tfdiags.Error,
|
||||
},
|
||||
},
|
||||
},
|
||||
"check_data_sources": {
|
||||
ExpectedFailures: []string{
|
||||
"check.expected",
|
||||
},
|
||||
Input: createDiagnostics(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
|
||||
// First, we'll add an unexpected warning from a check block
|
||||
// assertion that should be propagated as an error.
|
||||
diags = diags.Append(
|
||||
tfdiags.Override(
|
||||
tfdiags.Sourceless(tfdiags.Error, "unexpected failure", "this should be an error and not removed"),
|
||||
tfdiags.Warning,
|
||||
func() tfdiags.DiagnosticExtraWrapper {
|
||||
return &addrs.CheckRuleDiagnosticExtra{
|
||||
CheckRule: addrs.NewCheckRule(addrs.AbsCheck{
|
||||
Module: addrs.RootModuleInstance,
|
||||
Check: addrs.Check{
|
||||
Name: "unexpected",
|
||||
},
|
||||
}, addrs.CheckDataResource, 0),
|
||||
}
|
||||
}))
|
||||
|
||||
// Second, we'll add an unexpected warning from a check block
|
||||
// assertion that should remain as a warning.
|
||||
diags = diags.Append(
|
||||
tfdiags.Override(
|
||||
tfdiags.Sourceless(tfdiags.Warning, "unexpected warning", "this should be a warning and not removed"),
|
||||
tfdiags.Warning,
|
||||
func() tfdiags.DiagnosticExtraWrapper {
|
||||
return &addrs.CheckRuleDiagnosticExtra{
|
||||
CheckRule: addrs.NewCheckRule(addrs.AbsCheck{
|
||||
Module: addrs.RootModuleInstance,
|
||||
Check: addrs.Check{
|
||||
Name: "unexpected",
|
||||
},
|
||||
}, addrs.CheckDataResource, 0),
|
||||
}
|
||||
}))
|
||||
|
||||
// Third, we'll add an unexpected warning from a check block
|
||||
// in a child module that should be propagated as an error.
|
||||
diags = diags.Append(
|
||||
tfdiags.Override(
|
||||
tfdiags.Sourceless(tfdiags.Error, "expected failure from child module", "this should be an error and not removed"),
|
||||
tfdiags.Warning,
|
||||
func() tfdiags.DiagnosticExtraWrapper {
|
||||
return &addrs.CheckRuleDiagnosticExtra{
|
||||
CheckRule: addrs.NewCheckRule(addrs.AbsCheck{
|
||||
Module: []addrs.ModuleInstanceStep{
|
||||
{
|
||||
Name: "child_module",
|
||||
},
|
||||
},
|
||||
Check: addrs.Check{
|
||||
Name: "expected",
|
||||
},
|
||||
}, addrs.CheckDataResource, 0),
|
||||
}
|
||||
}))
|
||||
|
||||
// Fourth, we'll add an expected warning that should be removed.
|
||||
diags = diags.Append(
|
||||
tfdiags.Override(
|
||||
tfdiags.Sourceless(tfdiags.Error, "expected failure", "this should be removed"),
|
||||
tfdiags.Warning,
|
||||
func() tfdiags.DiagnosticExtraWrapper {
|
||||
return &addrs.CheckRuleDiagnosticExtra{
|
||||
CheckRule: addrs.NewCheckRule(addrs.AbsCheck{
|
||||
Module: addrs.RootModuleInstance,
|
||||
Check: addrs.Check{
|
||||
Name: "expected",
|
||||
},
|
||||
}, addrs.CheckDataResource, 0),
|
||||
}
|
||||
}))
|
||||
|
||||
return diags
|
||||
}),
|
||||
Output: []output{
|
||||
{
|
||||
Description: tfdiags.Description{
|
||||
Summary: "unexpected failure",
|
||||
Detail: "this should be an error and not removed",
|
||||
},
|
||||
Severity: tfdiags.Error,
|
||||
},
|
||||
{
|
||||
Description: tfdiags.Description{
|
||||
Summary: "unexpected warning",
|
||||
Detail: "this should be a warning and not removed",
|
||||
},
|
||||
Severity: tfdiags.Warning,
|
||||
},
|
||||
{
|
||||
Description: tfdiags.Description{
|
||||
Summary: "expected failure from child module",
|
||||
Detail: "this should be an error and not removed",
|
||||
},
|
||||
Severity: tfdiags.Error,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range tcs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var traversals []hcl.Traversal
|
||||
for _, ef := range tc.ExpectedFailures {
|
||||
traversal, diags := hclsyntax.ParseTraversalAbs([]byte(ef), "foo.tf", hcl.Pos{Line: 1, Column: 1})
|
||||
if diags.HasErrors() {
|
||||
t.Errorf("invalid expected failure %s: %v", ef, diags.Error())
|
||||
}
|
||||
traversals = append(traversals, traversal)
|
||||
}
|
||||
|
||||
if t.Failed() {
|
||||
return
|
||||
}
|
||||
|
||||
run := Run{
|
||||
Config: &configs.TestRun{
|
||||
ExpectFailures: traversals,
|
||||
},
|
||||
}
|
||||
|
||||
out := run.ValidateExpectedFailures(tc.Input)
|
||||
ix := 0
|
||||
for ; ix < len(tc.Output); ix++ {
|
||||
expected := tc.Output[ix]
|
||||
|
||||
if ix >= len(out) {
|
||||
t.Errorf("missing diagnostic at %d, expected: [%s] %s, %s", ix, expected.Severity, expected.Description.Summary, expected.Description.Detail)
|
||||
continue
|
||||
}
|
||||
|
||||
actual := output{
|
||||
Description: out[ix].Description(),
|
||||
Severity: out[ix].Severity(),
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(expected, actual); len(diff) > 0 {
|
||||
t.Errorf("mismatched diagnostic at %d:\n%s", ix, diff)
|
||||
}
|
||||
}
|
||||
|
||||
for ; ix < len(out); ix++ {
|
||||
actual := out[ix]
|
||||
t.Errorf("additional diagnostic at %d: [%s] %s, %s", ix, actual.Severity(), actual.Description().Summary, actual.Description().Detail)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func createDiagnostics(populate func(diags tfdiags.Diagnostics) tfdiags.Diagnostics) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
diags = populate(diags)
|
||||
return diags
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package tfdiags
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
)
|
||||
|
||||
var _ Diagnostic = CheckBlockDiagnostic{}
|
||||
|
||||
// CheckBlockDiagnostic is a diagnostic produced by a Terraform config Check block.
|
||||
//
|
||||
// It only ever returns warnings, and will not be consolidated as part of the
|
||||
// Diagnostics.ConsolidateWarnings function.
|
||||
type CheckBlockDiagnostic struct {
|
||||
diag Diagnostic
|
||||
}
|
||||
|
||||
// AsCheckBlockDiagnostics will wrap every diagnostic in diags in a
|
||||
// CheckBlockDiagnostic.
|
||||
func AsCheckBlockDiagnostics(diags Diagnostics) Diagnostics {
|
||||
if len(diags) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ret := make(Diagnostics, len(diags))
|
||||
for i, diag := range diags {
|
||||
ret[i] = CheckBlockDiagnostic{diag}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// AsCheckBlockDiagnostic will wrap a Diagnostic or a hcl.Diagnostic in a
|
||||
// CheckBlockDiagnostic.
|
||||
func AsCheckBlockDiagnostic(diag interface{}) Diagnostic {
|
||||
switch d := diag.(type) {
|
||||
case Diagnostic:
|
||||
return CheckBlockDiagnostic{d}
|
||||
case *hcl.Diagnostic:
|
||||
return CheckBlockDiagnostic{hclDiagnostic{d}}
|
||||
default:
|
||||
panic(fmt.Errorf("can't construct diagnostic from %T", diag))
|
||||
}
|
||||
}
|
||||
|
||||
// IsFromCheckBlock returns true if the specified Diagnostic is a
|
||||
// CheckBlockDiagnostic.
|
||||
func IsFromCheckBlock(diag Diagnostic) bool {
|
||||
_, ok := diag.(CheckBlockDiagnostic)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (c CheckBlockDiagnostic) Severity() Severity {
|
||||
// Regardless of the severity of the underlying diagnostic, check blocks
|
||||
// only ever report Warning severity.
|
||||
return Warning
|
||||
}
|
||||
|
||||
func (c CheckBlockDiagnostic) Description() Description {
|
||||
return c.diag.Description()
|
||||
}
|
||||
|
||||
func (c CheckBlockDiagnostic) Source() Source {
|
||||
return c.diag.Source()
|
||||
}
|
||||
|
||||
func (c CheckBlockDiagnostic) FromExpr() *FromExpr {
|
||||
return c.diag.FromExpr()
|
||||
}
|
||||
|
||||
func (c CheckBlockDiagnostic) ExtraInfo() interface{} {
|
||||
return c.diag.ExtraInfo()
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
package tfdiags
|
||||
|
||||
// overriddenDiagnostic implements the Diagnostic interface by wrapping another
|
||||
// Diagnostic while overriding the severity of the original Diagnostic.
|
||||
type overriddenDiagnostic struct {
|
||||
original Diagnostic
|
||||
severity Severity
|
||||
extra interface{}
|
||||
}
|
||||
|
||||
var _ Diagnostic = overriddenDiagnostic{}
|
||||
|
||||
// OverrideAll accepts a set of Diagnostics and wraps them with a new severity
|
||||
// and, optionally, a new ExtraInfo.
|
||||
func OverrideAll(originals Diagnostics, severity Severity, createExtra func() DiagnosticExtraWrapper) Diagnostics {
|
||||
var diags Diagnostics
|
||||
for _, diag := range originals {
|
||||
diags = diags.Append(Override(diag, severity, createExtra))
|
||||
}
|
||||
return diags
|
||||
}
|
||||
|
||||
// Override matches OverrideAll except it operates over a single Diagnostic
|
||||
// rather than multiple Diagnostics.
|
||||
func Override(original Diagnostic, severity Severity, createExtra func() DiagnosticExtraWrapper) Diagnostic {
|
||||
extra := original.ExtraInfo()
|
||||
if createExtra != nil {
|
||||
nw := createExtra()
|
||||
nw.WrapDiagnosticExtra(extra)
|
||||
extra = nw
|
||||
}
|
||||
|
||||
return overriddenDiagnostic{
|
||||
original: original,
|
||||
severity: severity,
|
||||
extra: extra,
|
||||
}
|
||||
}
|
||||
|
||||
// UndoOverride will return the original diagnostic that was overridden within
|
||||
// the OverrideAll function.
|
||||
//
|
||||
// If the provided Diagnostic was never overridden then it is simply returned
|
||||
// unchanged.
|
||||
func UndoOverride(diag Diagnostic) Diagnostic {
|
||||
if override, ok := diag.(overriddenDiagnostic); ok {
|
||||
return override.original
|
||||
}
|
||||
|
||||
// Then it wasn't overridden, so we'll just return the diag unchanged.
|
||||
return diag
|
||||
}
|
||||
|
||||
func (o overriddenDiagnostic) Severity() Severity {
|
||||
return o.severity
|
||||
}
|
||||
|
||||
func (o overriddenDiagnostic) Description() Description {
|
||||
return o.original.Description()
|
||||
}
|
||||
|
||||
func (o overriddenDiagnostic) Source() Source {
|
||||
return o.original.Source()
|
||||
}
|
||||
|
||||
func (o overriddenDiagnostic) FromExpr() *FromExpr {
|
||||
return o.original.FromExpr()
|
||||
}
|
||||
|
||||
func (o overriddenDiagnostic) ExtraInfo() interface{} {
|
||||
return o.extra
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
package tfdiags
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
)
|
||||
|
||||
func TestOverride_UpdatesSeverity(t *testing.T) {
|
||||
original := Sourceless(Error, "summary", "detail")
|
||||
override := Override(original, Warning, nil)
|
||||
|
||||
if override.Severity() != Warning {
|
||||
t.Errorf("expected warning but was %s", override.Severity())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverride_MaintainsExtra(t *testing.T) {
|
||||
original := hclDiagnostic{&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "summary",
|
||||
Detail: "detail",
|
||||
Extra: "extra",
|
||||
}}
|
||||
override := Override(original, Warning, nil)
|
||||
|
||||
if override.ExtraInfo().(string) != "extra" {
|
||||
t.Errorf("invalid extra info %v", override.ExtraInfo())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverride_WrapsExtra(t *testing.T) {
|
||||
original := hclDiagnostic{&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "summary",
|
||||
Detail: "detail",
|
||||
Extra: "extra",
|
||||
}}
|
||||
override := Override(original, Warning, func() DiagnosticExtraWrapper {
|
||||
return &extraWrapper{
|
||||
mine: "mine",
|
||||
}
|
||||
})
|
||||
|
||||
wrapper := override.ExtraInfo().(*extraWrapper)
|
||||
if wrapper.mine != "mine" {
|
||||
t.Errorf("invalid extra info %v", override.ExtraInfo())
|
||||
}
|
||||
if wrapper.original.(string) != "extra" {
|
||||
t.Errorf("invalid wrapped extra info %v", override.ExtraInfo())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUndoOverride(t *testing.T) {
|
||||
original := Sourceless(Error, "summary", "detail")
|
||||
override := Override(original, Warning, nil)
|
||||
restored := UndoOverride(override)
|
||||
|
||||
if restored.Severity() != Error {
|
||||
t.Errorf("expected warning but was %s", restored.Severity())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUndoOverride_NotOverridden(t *testing.T) {
|
||||
original := Sourceless(Error, "summary", "detail")
|
||||
restored := UndoOverride(original) // Shouldn't do anything bad.
|
||||
|
||||
if restored.Severity() != Error {
|
||||
t.Errorf("expected warning but was %s", restored.Severity())
|
||||
}
|
||||
}
|
||||
|
||||
type extraWrapper struct {
|
||||
mine string
|
||||
original interface{}
|
||||
}
|
||||
|
||||
func (e *extraWrapper) WrapDiagnosticExtra(inner interface{}) {
|
||||
e.original = inner
|
||||
}
|
||||
Loading…
Reference in new issue