opentofu/internal/command/views/json/diagnostic_test.go
Martin Atkins 90ea7b0bc5 tfdiags: Treat unknown-related or sensitive-related messages differently
By observing the sorts of questions people ask in the community, and the
ways they ask them, we've inferred that various different people have been
confused by Terraform reporting that a value won't be known until apply
or that a value is sensitive as part of an error message when that message
doesn't actually relate to the known-ness and sensitivity of any value.

Quite reasonably, someone who sees Terraform discussing an unfamiliar
concept like unknown values can assume that it must be somehow relevant to
the problem being discussed, and so in that sense Terraform's current
error messages are giving "too much information": information that isn't
actually helpful in understanding the problem being described, and in the
worst case is a distraction from understanding the problem being described.

With that in mind then, here we introduce an explicit annotation on
diagnostic objects that are directly talking about unknown values or
sensitive values, and then the diagnostic renderer will react to that to
avoid using the terminology "known only after apply" or "sensitive" in the
generated diagnostic annotations unless we're rendering a message that is
explicitly related to one of those topics.

This ends up being a bit of a cross-cutting concern because the code that
generates these diagnostics and the code that renders them are in separate
packages and are not directly aware of each other. With that in mind, the
logic for actually deciding for a particular diagnostic whether it's
flagged in one of these special ways lives inside the tfdiags package as
an intermediation point, which both the diagnostic generator (in the core
package) and the diagnostic renderer can both depend on.
2022-06-23 13:52:23 -07:00

952 lines
25 KiB
Go

package json
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcltest"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
func TestNewDiagnostic(t *testing.T) {
// Common HCL for diags with source ranges. This does not have any real
// semantic errors, but we can synthesize fake HCL errors which will
// exercise the diagnostic rendering code using this
sources := map[string][]byte{
"test.tf": []byte(`resource "test_resource" "test" {
foo = var.boop["hello!"]
bar = {
baz = maybe
}
}
`),
"short.tf": []byte("bad source code"),
"odd-comment.tf": []byte("foo\n\n#\n"),
"values.tf": []byte(`[
var.a,
var.b,
var.c,
var.d,
var.e,
var.f,
var.g,
var.h,
var.i,
var.j,
var.k,
]
`),
}
testCases := map[string]struct {
diag interface{} // allow various kinds of diags
want *Diagnostic
}{
"sourceless warning": {
tfdiags.Sourceless(
tfdiags.Warning,
"Oh no",
"Something is broken",
),
&Diagnostic{
Severity: "warning",
Summary: "Oh no",
Detail: "Something is broken",
},
},
"error with source code unavailable": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Bad news",
Detail: "It went wrong",
Subject: &hcl.Range{
Filename: "modules/oops/missing.tf",
Start: hcl.Pos{Line: 1, Column: 6, Byte: 5},
End: hcl.Pos{Line: 2, Column: 12, Byte: 33},
},
},
&Diagnostic{
Severity: "error",
Summary: "Bad news",
Detail: "It went wrong",
Range: &DiagnosticRange{
Filename: "modules/oops/missing.tf",
Start: Pos{
Line: 1,
Column: 6,
Byte: 5,
},
End: Pos{
Line: 2,
Column: 12,
Byte: 33,
},
},
},
},
"error with source code subject": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Tiny explosion",
Detail: "Unexpected detonation while parsing",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 10, Byte: 9},
End: hcl.Pos{Line: 1, Column: 25, Byte: 24},
},
},
&Diagnostic{
Severity: "error",
Summary: "Tiny explosion",
Detail: "Unexpected detonation while parsing",
Range: &DiagnosticRange{
Filename: "test.tf",
Start: Pos{
Line: 1,
Column: 10,
Byte: 9,
},
End: Pos{
Line: 1,
Column: 25,
Byte: 24,
},
},
Snippet: &DiagnosticSnippet{
Context: strPtr(`resource "test_resource" "test"`),
Code: `resource "test_resource" "test" {`,
StartLine: 1,
HighlightStartOffset: 9,
HighlightEndOffset: 24,
Values: []DiagnosticExpressionValue{},
},
},
},
"error with source code subject but no context": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Nonsense input",
Detail: "What you wrote makes no sense",
Subject: &hcl.Range{
Filename: "short.tf",
Start: hcl.Pos{Line: 1, Column: 5, Byte: 4},
End: hcl.Pos{Line: 1, Column: 10, Byte: 9},
},
},
&Diagnostic{
Severity: "error",
Summary: "Nonsense input",
Detail: "What you wrote makes no sense",
Range: &DiagnosticRange{
Filename: "short.tf",
Start: Pos{
Line: 1,
Column: 5,
Byte: 4,
},
End: Pos{
Line: 1,
Column: 10,
Byte: 9,
},
},
Snippet: &DiagnosticSnippet{
Context: nil,
Code: (`bad source code`),
StartLine: (1),
HighlightStartOffset: (4),
HighlightEndOffset: (9),
Values: []DiagnosticExpressionValue{},
},
},
},
"error with multi-line snippet": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "In this house we respect booleans",
Detail: "True or false, there is no maybe",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 4, Column: 11, Byte: 81},
End: hcl.Pos{Line: 4, Column: 16, Byte: 86},
},
Context: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 3, Column: 3, Byte: 63},
End: hcl.Pos{Line: 5, Column: 4, Byte: 90},
},
},
&Diagnostic{
Severity: "error",
Summary: "In this house we respect booleans",
Detail: "True or false, there is no maybe",
Range: &DiagnosticRange{
Filename: "test.tf",
Start: Pos{
Line: 4,
Column: 11,
Byte: 81,
},
End: Pos{
Line: 4,
Column: 16,
Byte: 86,
},
},
Snippet: &DiagnosticSnippet{
Context: strPtr(`resource "test_resource" "test"`),
Code: " bar = {\n baz = maybe\n }",
StartLine: 3,
HighlightStartOffset: 20,
HighlightEndOffset: 25,
Values: []DiagnosticExpressionValue{},
},
},
},
"error with empty highlight range at end of source code": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "You forgot something",
Detail: "Please finish your thought",
Subject: &hcl.Range{
Filename: "short.tf",
Start: hcl.Pos{Line: 1, Column: 16, Byte: 15},
End: hcl.Pos{Line: 1, Column: 16, Byte: 15},
},
},
&Diagnostic{
Severity: "error",
Summary: "You forgot something",
Detail: "Please finish your thought",
Range: &DiagnosticRange{
Filename: "short.tf",
Start: Pos{
Line: 1,
Column: 16,
Byte: 15,
},
End: Pos{
Line: 1,
Column: 17,
Byte: 16,
},
},
Snippet: &DiagnosticSnippet{
Code: ("bad source code"),
StartLine: (1),
HighlightStartOffset: (15),
HighlightEndOffset: (15),
Values: []DiagnosticExpressionValue{},
},
},
},
"error with unset highlight end position": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "There is no end",
Detail: "But there is a beginning",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 16, Byte: 15},
End: hcl.Pos{Line: 0, Column: 0, Byte: 0},
},
},
&Diagnostic{
Severity: "error",
Summary: "There is no end",
Detail: "But there is a beginning",
Range: &DiagnosticRange{
Filename: "test.tf",
Start: Pos{
Line: 1,
Column: 16,
Byte: 15,
},
End: Pos{
Line: 1,
Column: 17,
Byte: 16,
},
},
Snippet: &DiagnosticSnippet{
Context: strPtr(`resource "test_resource" "test"`),
Code: `resource "test_resource" "test" {`,
StartLine: 1,
HighlightStartOffset: 15,
HighlightEndOffset: 16,
Values: []DiagnosticExpressionValue{},
},
},
},
"error whose range starts at a newline": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid newline",
Detail: "How awkward!",
Subject: &hcl.Range{
Filename: "odd-comment.tf",
Start: hcl.Pos{Line: 2, Column: 5, Byte: 4},
End: hcl.Pos{Line: 3, Column: 1, Byte: 6},
},
},
&Diagnostic{
Severity: "error",
Summary: "Invalid newline",
Detail: "How awkward!",
Range: &DiagnosticRange{
Filename: "odd-comment.tf",
Start: Pos{
Line: 2,
Column: 5,
Byte: 4,
},
End: Pos{
Line: 3,
Column: 1,
Byte: 6,
},
},
Snippet: &DiagnosticSnippet{
Code: `#`,
StartLine: 2,
Values: []DiagnosticExpressionValue{},
// Due to the range starting at a newline on a blank
// line, we end up stripping off the initial newline
// to produce only a one-line snippet. That would
// therefore cause the start offset to naturally be
// -1, just before the Code we returned, but then we
// force it to zero so that the result will still be
// in range for a byte-oriented slice of Code.
HighlightStartOffset: 0,
HighlightEndOffset: 1,
},
},
},
"error with source code subject and known expression": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Wrong noises",
Detail: "Biological sounds are not allowed",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 2, Column: 9, Byte: 42},
End: hcl.Pos{Line: 2, Column: 26, Byte: 59},
},
Expression: hcltest.MockExprTraversal(hcl.Traversal{
hcl.TraverseRoot{Name: "var"},
hcl.TraverseAttr{Name: "boop"},
hcl.TraverseIndex{Key: cty.StringVal("hello!")},
}),
EvalContext: &hcl.EvalContext{
Variables: map[string]cty.Value{
"var": cty.ObjectVal(map[string]cty.Value{
"boop": cty.MapVal(map[string]cty.Value{
"hello!": cty.StringVal("bleurgh"),
}),
}),
},
},
},
&Diagnostic{
Severity: "error",
Summary: "Wrong noises",
Detail: "Biological sounds are not allowed",
Range: &DiagnosticRange{
Filename: "test.tf",
Start: Pos{
Line: 2,
Column: 9,
Byte: 42,
},
End: Pos{
Line: 2,
Column: 26,
Byte: 59,
},
},
Snippet: &DiagnosticSnippet{
Context: strPtr(`resource "test_resource" "test"`),
Code: (` foo = var.boop["hello!"]`),
StartLine: (2),
HighlightStartOffset: (8),
HighlightEndOffset: (25),
Values: []DiagnosticExpressionValue{
{
Traversal: `var.boop["hello!"]`,
Statement: `is "bleurgh"`,
},
},
},
},
},
"error with source code subject and expression referring to sensitive value": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Wrong noises",
Detail: "Biological sounds are not allowed",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 2, Column: 9, Byte: 42},
End: hcl.Pos{Line: 2, Column: 26, Byte: 59},
},
Expression: hcltest.MockExprTraversal(hcl.Traversal{
hcl.TraverseRoot{Name: "var"},
hcl.TraverseAttr{Name: "boop"},
hcl.TraverseIndex{Key: cty.StringVal("hello!")},
}),
EvalContext: &hcl.EvalContext{
Variables: map[string]cty.Value{
"var": cty.ObjectVal(map[string]cty.Value{
"boop": cty.MapVal(map[string]cty.Value{
"hello!": cty.StringVal("bleurgh").Mark(marks.Sensitive),
}),
}),
},
},
Extra: diagnosticCausedBySensitive(true),
},
&Diagnostic{
Severity: "error",
Summary: "Wrong noises",
Detail: "Biological sounds are not allowed",
Range: &DiagnosticRange{
Filename: "test.tf",
Start: Pos{
Line: 2,
Column: 9,
Byte: 42,
},
End: Pos{
Line: 2,
Column: 26,
Byte: 59,
},
},
Snippet: &DiagnosticSnippet{
Context: strPtr(`resource "test_resource" "test"`),
Code: (` foo = var.boop["hello!"]`),
StartLine: (2),
HighlightStartOffset: (8),
HighlightEndOffset: (25),
Values: []DiagnosticExpressionValue{
{
Traversal: `var.boop["hello!"]`,
Statement: `has a sensitive value`,
},
},
},
},
},
"error with source code subject and expression referring to sensitive value when not caused by sensitive values": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Wrong noises",
Detail: "Biological sounds are not allowed",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 2, Column: 9, Byte: 42},
End: hcl.Pos{Line: 2, Column: 26, Byte: 59},
},
Expression: hcltest.MockExprTraversal(hcl.Traversal{
hcl.TraverseRoot{Name: "var"},
hcl.TraverseAttr{Name: "boop"},
hcl.TraverseIndex{Key: cty.StringVal("hello!")},
}),
EvalContext: &hcl.EvalContext{
Variables: map[string]cty.Value{
"var": cty.ObjectVal(map[string]cty.Value{
"boop": cty.MapVal(map[string]cty.Value{
"hello!": cty.StringVal("bleurgh").Mark(marks.Sensitive),
}),
}),
},
},
},
&Diagnostic{
Severity: "error",
Summary: "Wrong noises",
Detail: "Biological sounds are not allowed",
Range: &DiagnosticRange{
Filename: "test.tf",
Start: Pos{
Line: 2,
Column: 9,
Byte: 42,
},
End: Pos{
Line: 2,
Column: 26,
Byte: 59,
},
},
Snippet: &DiagnosticSnippet{
Context: strPtr(`resource "test_resource" "test"`),
Code: (` foo = var.boop["hello!"]`),
StartLine: (2),
HighlightStartOffset: (8),
HighlightEndOffset: (25),
Values: []DiagnosticExpressionValue{
// The sensitive value is filtered out because this is
// not a sensitive-value-related diagnostic message.
},
},
},
},
"error with source code subject and expression referring to a collection containing a sensitive value": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Wrong noises",
Detail: "Biological sounds are not allowed",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 2, Column: 9, Byte: 42},
End: hcl.Pos{Line: 2, Column: 26, Byte: 59},
},
Expression: hcltest.MockExprTraversal(hcl.Traversal{
hcl.TraverseRoot{Name: "var"},
hcl.TraverseAttr{Name: "boop"},
}),
EvalContext: &hcl.EvalContext{
Variables: map[string]cty.Value{
"var": cty.ObjectVal(map[string]cty.Value{
"boop": cty.MapVal(map[string]cty.Value{
"hello!": cty.StringVal("bleurgh").Mark(marks.Sensitive),
}),
}),
},
},
},
&Diagnostic{
Severity: "error",
Summary: "Wrong noises",
Detail: "Biological sounds are not allowed",
Range: &DiagnosticRange{
Filename: "test.tf",
Start: Pos{
Line: 2,
Column: 9,
Byte: 42,
},
End: Pos{
Line: 2,
Column: 26,
Byte: 59,
},
},
Snippet: &DiagnosticSnippet{
Context: strPtr(`resource "test_resource" "test"`),
Code: (` foo = var.boop["hello!"]`),
StartLine: (2),
HighlightStartOffset: (8),
HighlightEndOffset: (25),
Values: []DiagnosticExpressionValue{
{
Traversal: `var.boop`,
Statement: `is map of string with 1 element`,
},
},
},
},
},
"error with source code subject and unknown string expression": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Wrong noises",
Detail: "Biological sounds are not allowed",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 2, Column: 9, Byte: 42},
End: hcl.Pos{Line: 2, Column: 26, Byte: 59},
},
Expression: hcltest.MockExprTraversal(hcl.Traversal{
hcl.TraverseRoot{Name: "var"},
hcl.TraverseAttr{Name: "boop"},
hcl.TraverseIndex{Key: cty.StringVal("hello!")},
}),
EvalContext: &hcl.EvalContext{
Variables: map[string]cty.Value{
"var": cty.ObjectVal(map[string]cty.Value{
"boop": cty.MapVal(map[string]cty.Value{
"hello!": cty.UnknownVal(cty.String),
}),
}),
},
},
Extra: diagnosticCausedByUnknown(true),
},
&Diagnostic{
Severity: "error",
Summary: "Wrong noises",
Detail: "Biological sounds are not allowed",
Range: &DiagnosticRange{
Filename: "test.tf",
Start: Pos{
Line: 2,
Column: 9,
Byte: 42,
},
End: Pos{
Line: 2,
Column: 26,
Byte: 59,
},
},
Snippet: &DiagnosticSnippet{
Context: strPtr(`resource "test_resource" "test"`),
Code: (` foo = var.boop["hello!"]`),
StartLine: (2),
HighlightStartOffset: (8),
HighlightEndOffset: (25),
Values: []DiagnosticExpressionValue{
{
Traversal: `var.boop["hello!"]`,
Statement: `is a string, known only after apply`,
},
},
},
},
},
"error with source code subject and unknown expression of unknown type": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Wrong noises",
Detail: "Biological sounds are not allowed",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 2, Column: 9, Byte: 42},
End: hcl.Pos{Line: 2, Column: 26, Byte: 59},
},
Expression: hcltest.MockExprTraversal(hcl.Traversal{
hcl.TraverseRoot{Name: "var"},
hcl.TraverseAttr{Name: "boop"},
hcl.TraverseIndex{Key: cty.StringVal("hello!")},
}),
EvalContext: &hcl.EvalContext{
Variables: map[string]cty.Value{
"var": cty.ObjectVal(map[string]cty.Value{
"boop": cty.MapVal(map[string]cty.Value{
"hello!": cty.UnknownVal(cty.DynamicPseudoType),
}),
}),
},
},
Extra: diagnosticCausedByUnknown(true),
},
&Diagnostic{
Severity: "error",
Summary: "Wrong noises",
Detail: "Biological sounds are not allowed",
Range: &DiagnosticRange{
Filename: "test.tf",
Start: Pos{
Line: 2,
Column: 9,
Byte: 42,
},
End: Pos{
Line: 2,
Column: 26,
Byte: 59,
},
},
Snippet: &DiagnosticSnippet{
Context: strPtr(`resource "test_resource" "test"`),
Code: (` foo = var.boop["hello!"]`),
StartLine: (2),
HighlightStartOffset: (8),
HighlightEndOffset: (25),
Values: []DiagnosticExpressionValue{
{
Traversal: `var.boop["hello!"]`,
Statement: `will be known only after apply`,
},
},
},
},
},
"error with source code subject and unknown expression of unknown type when not caused by unknown values": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Wrong noises",
Detail: "Biological sounds are not allowed",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 2, Column: 9, Byte: 42},
End: hcl.Pos{Line: 2, Column: 26, Byte: 59},
},
Expression: hcltest.MockExprTraversal(hcl.Traversal{
hcl.TraverseRoot{Name: "var"},
hcl.TraverseAttr{Name: "boop"},
hcl.TraverseIndex{Key: cty.StringVal("hello!")},
}),
EvalContext: &hcl.EvalContext{
Variables: map[string]cty.Value{
"var": cty.ObjectVal(map[string]cty.Value{
"boop": cty.MapVal(map[string]cty.Value{
"hello!": cty.UnknownVal(cty.DynamicPseudoType),
}),
}),
},
},
},
&Diagnostic{
Severity: "error",
Summary: "Wrong noises",
Detail: "Biological sounds are not allowed",
Range: &DiagnosticRange{
Filename: "test.tf",
Start: Pos{
Line: 2,
Column: 9,
Byte: 42,
},
End: Pos{
Line: 2,
Column: 26,
Byte: 59,
},
},
Snippet: &DiagnosticSnippet{
Context: strPtr(`resource "test_resource" "test"`),
Code: (` foo = var.boop["hello!"]`),
StartLine: (2),
HighlightStartOffset: (8),
HighlightEndOffset: (25),
Values: []DiagnosticExpressionValue{
// The unknown value is filtered out because this is
// not an unknown-value-related diagnostic message.
},
},
},
},
"error with source code subject with multiple expression values": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Catastrophic failure",
Detail: "Basically, everything went wrong",
Subject: &hcl.Range{
Filename: "values.tf",
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
End: hcl.Pos{Line: 13, Column: 2, Byte: 102},
},
Expression: hcltest.MockExprList([]hcl.Expression{
hcltest.MockExprTraversalSrc("var.a"),
hcltest.MockExprTraversalSrc("var.b"),
hcltest.MockExprTraversalSrc("var.c"),
hcltest.MockExprTraversalSrc("var.d"),
hcltest.MockExprTraversalSrc("var.e"),
hcltest.MockExprTraversalSrc("var.f"),
hcltest.MockExprTraversalSrc("var.g"),
hcltest.MockExprTraversalSrc("var.h"),
hcltest.MockExprTraversalSrc("var.i"),
hcltest.MockExprTraversalSrc("var.j"),
hcltest.MockExprTraversalSrc("var.k"),
}),
EvalContext: &hcl.EvalContext{
Variables: map[string]cty.Value{
"var": cty.ObjectVal(map[string]cty.Value{
"a": cty.True,
"b": cty.NumberFloatVal(123.45),
"c": cty.NullVal(cty.String),
"d": cty.StringVal("secret").Mark(marks.Sensitive),
"e": cty.False,
"f": cty.ListValEmpty(cty.String),
"g": cty.MapVal(map[string]cty.Value{
"boop": cty.StringVal("beep"),
}),
"h": cty.ListVal([]cty.Value{
cty.StringVal("boop"),
cty.StringVal("beep"),
cty.StringVal("blorp"),
}),
"i": cty.EmptyObjectVal,
"j": cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
}),
"k": cty.ObjectVal(map[string]cty.Value{
"a": cty.True,
"b": cty.False,
}),
}),
},
},
Extra: diagnosticCausedBySensitive(true),
},
&Diagnostic{
Severity: "error",
Summary: "Catastrophic failure",
Detail: "Basically, everything went wrong",
Range: &DiagnosticRange{
Filename: "values.tf",
Start: Pos{
Line: 1,
Column: 1,
Byte: 0,
},
End: Pos{
Line: 13,
Column: 2,
Byte: 102,
},
},
Snippet: &DiagnosticSnippet{
Code: `[
var.a,
var.b,
var.c,
var.d,
var.e,
var.f,
var.g,
var.h,
var.i,
var.j,
var.k,
]`,
StartLine: (1),
HighlightStartOffset: (0),
HighlightEndOffset: (102),
Values: []DiagnosticExpressionValue{
{
Traversal: `var.a`,
Statement: `is true`,
},
{
Traversal: `var.b`,
Statement: `is 123.45`,
},
{
Traversal: `var.c`,
Statement: `is null`,
},
{
Traversal: `var.d`,
Statement: `has a sensitive value`,
},
{
Traversal: `var.e`,
Statement: `is false`,
},
{
Traversal: `var.f`,
Statement: `is empty list of string`,
},
{
Traversal: `var.g`,
Statement: `is map of string with 1 element`,
},
{
Traversal: `var.h`,
Statement: `is list of string with 3 elements`,
},
{
Traversal: `var.i`,
Statement: `is object with no attributes`,
},
{
Traversal: `var.j`,
Statement: `is object with 1 attribute "foo"`,
},
{
Traversal: `var.k`,
Statement: `is object with 2 attributes`,
},
},
},
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
// Convert the diag into a tfdiags.Diagnostic
var diags tfdiags.Diagnostics
diags = diags.Append(tc.diag)
got := NewDiagnostic(diags[0], sources)
if !cmp.Equal(tc.want, got) {
t.Fatalf("wrong result\n:%s", cmp.Diff(tc.want, got))
}
})
t.Run(fmt.Sprintf("golden test for %s", name), func(t *testing.T) {
// Convert the diag into a tfdiags.Diagnostic
var diags tfdiags.Diagnostics
diags = diags.Append(tc.diag)
got := NewDiagnostic(diags[0], sources)
// Render the diagnostic to indented JSON
gotBytes, err := json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatal(err)
}
// Compare against the golden reference
filename := path.Join(
"testdata",
"diagnostic",
fmt.Sprintf("%s.json", strings.ReplaceAll(name, " ", "-")),
)
// Generate golden reference by uncommenting the next two lines:
// gotBytes = append(gotBytes, '\n')
// os.WriteFile(filename, gotBytes, 0644)
wantFile, err := os.Open(filename)
if err != nil {
t.Fatalf("failed to open golden file: %s", err)
}
defer wantFile.Close()
wantBytes, err := ioutil.ReadAll(wantFile)
if err != nil {
t.Fatalf("failed to read output file: %s", err)
}
// Don't care about leading or trailing whitespace
gotString := strings.TrimSpace(string(gotBytes))
wantString := strings.TrimSpace(string(wantBytes))
if !cmp.Equal(wantString, gotString) {
t.Fatalf("wrong result\n:%s", cmp.Diff(wantString, gotString))
}
})
}
}
// Helper function to make constructing literal Diagnostics easier. There
// are fields which are pointer-to-string to ensure that the rendered JSON
// results in `null` for an empty value, rather than `""`.
func strPtr(s string) *string { return &s }
// diagnosticCausedByUnknown is a testing helper for exercising our logic
// for selectively showing unknown values alongside our source snippets for
// diagnostics that are explicitly marked as being caused by unknown values.
type diagnosticCausedByUnknown bool
var _ tfdiags.DiagnosticExtraBecauseUnknown = diagnosticCausedByUnknown(true)
func (e diagnosticCausedByUnknown) DiagnosticCausedByUnknown() bool {
return bool(e)
}
// diagnosticCausedBySensitive is a testing helper for exercising our logic
// for selectively showing sensitive values alongside our source snippets for
// diagnostics that are explicitly marked as being caused by sensitive values.
type diagnosticCausedBySensitive bool
var _ tfdiags.DiagnosticExtraBecauseSensitive = diagnosticCausedBySensitive(true)
func (e diagnosticCausedBySensitive) DiagnosticCausedBySensitive() bool {
return bool(e)
}