mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-04 13:17:43 -06:00
e309675853
The usual usage of diagnostics requires us to pass around source location information to everywhere that might generate a diagnostic, and that is always the best way to get the most precise diagnostic source locations. However, it's impractical to require source location information to be retained in every Terraform subsystem, and so this new idea of "contextual diagnostics" allows us to separate the generation of a diagnostic from the resolution of its source location, instead resolving the location information as a post-processing step once the call stack unwinds to a place where there is enough context to find it. This is necessarily a less precise approach than reading the source ranges directly from the configuration AST, but gives us an alternative to no diagnostics at all in portions of Terraform where full location information is not available. This is a best-effort sort of thing which will get as precise as it can but may return a range in a parent block if the precise location of a particular attribute cannot be found. Diagnostics that rely on this mechanism should include some other contextual information in the detail message to make up for any loss of precision that results.
140 lines
3.0 KiB
Go
140 lines
3.0 KiB
Go
package tfdiags
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/go-test/deep"
|
|
"github.com/hashicorp/hcl2/hcl"
|
|
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
func TestAttributeValue(t *testing.T) {
|
|
testConfig := `
|
|
foo {
|
|
bar = "hi"
|
|
}
|
|
foo {
|
|
bar = "bar"
|
|
}
|
|
bar {
|
|
bar = "woot"
|
|
}
|
|
baz "a" {
|
|
bar = "beep"
|
|
}
|
|
baz "b" {
|
|
bar = "boop"
|
|
}
|
|
`
|
|
f, parseDiags := hclsyntax.ParseConfig([]byte(testConfig), "test.tf", hcl.Pos{Line: 1, Column: 1})
|
|
if len(parseDiags) != 0 {
|
|
t.Fatal(parseDiags)
|
|
}
|
|
|
|
body := f.Body
|
|
var diags Diagnostics
|
|
diags = diags.Append(AttributeValue(
|
|
Error,
|
|
"foo[0].bar",
|
|
"detail",
|
|
cty.Path{
|
|
cty.GetAttrStep{Name: "foo"},
|
|
cty.IndexStep{Key: cty.NumberIntVal(0)},
|
|
cty.GetAttrStep{Name: "bar"},
|
|
},
|
|
))
|
|
diags = diags.Append(AttributeValue(
|
|
Error,
|
|
"foo[1].bar",
|
|
"detail",
|
|
cty.Path{
|
|
cty.GetAttrStep{Name: "foo"},
|
|
cty.IndexStep{Key: cty.NumberIntVal(1)},
|
|
cty.GetAttrStep{Name: "bar"},
|
|
},
|
|
))
|
|
diags = diags.Append(AttributeValue(
|
|
Error,
|
|
"bar.bar",
|
|
"detail",
|
|
cty.Path{
|
|
cty.GetAttrStep{Name: "bar"},
|
|
cty.GetAttrStep{Name: "bar"},
|
|
},
|
|
))
|
|
diags = diags.Append(AttributeValue(
|
|
Error,
|
|
`baz["a"].bar`,
|
|
"detail",
|
|
cty.Path{
|
|
cty.GetAttrStep{Name: "baz"},
|
|
cty.IndexStep{Key: cty.StringVal("a")},
|
|
cty.GetAttrStep{Name: "bar"},
|
|
},
|
|
))
|
|
diags = diags.Append(AttributeValue(
|
|
Error,
|
|
`baz["b"].bar`,
|
|
"detail",
|
|
cty.Path{
|
|
cty.GetAttrStep{Name: "baz"},
|
|
cty.IndexStep{Key: cty.StringVal("b")},
|
|
cty.GetAttrStep{Name: "bar"},
|
|
},
|
|
))
|
|
// Attribute value with subject already populated should not be disturbed.
|
|
// (in a real case, this might've been passed through from a deeper function
|
|
// in the call stack, for example.)
|
|
diags = diags.Append(&attributeDiagnostic{
|
|
diagnosticBase: diagnosticBase{
|
|
summary: "preexisting",
|
|
detail: "detail",
|
|
},
|
|
subject: &SourceRange{
|
|
Filename: "somewhere_else.tf",
|
|
},
|
|
})
|
|
|
|
gotDiags := diags.InConfigBody(body)
|
|
|
|
wantRanges := map[string]*SourceRange{
|
|
`foo[0].bar`: {
|
|
Filename: "test.tf",
|
|
Start: SourcePos{Line: 3, Column: 9, Byte: 15},
|
|
End: SourcePos{Line: 3, Column: 13, Byte: 19},
|
|
},
|
|
`foo[1].bar`: {
|
|
Filename: "test.tf",
|
|
Start: SourcePos{Line: 6, Column: 9, Byte: 36},
|
|
End: SourcePos{Line: 6, Column: 14, Byte: 41},
|
|
},
|
|
`bar.bar`: {
|
|
Filename: "test.tf",
|
|
Start: SourcePos{Line: 9, Column: 9, Byte: 58},
|
|
End: SourcePos{Line: 9, Column: 15, Byte: 64},
|
|
},
|
|
`baz["a"].bar`: {
|
|
Filename: "test.tf",
|
|
Start: SourcePos{Line: 12, Column: 9, Byte: 85},
|
|
End: SourcePos{Line: 12, Column: 15, Byte: 91},
|
|
},
|
|
`baz["b"].bar`: {
|
|
Filename: "test.tf",
|
|
Start: SourcePos{Line: 15, Column: 9, Byte: 112},
|
|
End: SourcePos{Line: 15, Column: 15, Byte: 118},
|
|
},
|
|
`preexisting`: {
|
|
Filename: "somewhere_else.tf",
|
|
},
|
|
}
|
|
gotRanges := make(map[string]*SourceRange)
|
|
for _, diag := range gotDiags {
|
|
gotRanges[diag.Description().Summary] = diag.Source().Subject
|
|
}
|
|
|
|
for _, problem := range deep.Equal(gotRanges, wantRanges) {
|
|
t.Error(problem)
|
|
}
|
|
}
|