6c4344623b
The main HCL package is more visible this way, and so it's easier than having to pick it out from dozens of other package directories.
351 lines
8.1 KiB
Go
351 lines
8.1 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/zclconf/go-cty/cty"
|
|
"github.com/zclconf/go-cty/cty/convert"
|
|
|
|
"github.com/hashicorp/hcl/v2/ext/typeexpr"
|
|
"github.com/hashicorp/hcl/v2/gohcl"
|
|
"github.com/hashicorp/hcl/v2"
|
|
)
|
|
|
|
type TestFile struct {
|
|
Result cty.Value
|
|
ResultType cty.Type
|
|
|
|
ChecksTraversals bool
|
|
ExpectedTraversals []*TestFileExpectTraversal
|
|
|
|
ExpectedDiags []*TestFileExpectDiag
|
|
|
|
ResultRange hcl.Range
|
|
ResultTypeRange hcl.Range
|
|
}
|
|
|
|
type TestFileExpectTraversal struct {
|
|
Traversal hcl.Traversal
|
|
Range hcl.Range
|
|
DeclRange hcl.Range
|
|
}
|
|
|
|
type TestFileExpectDiag struct {
|
|
Severity hcl.DiagnosticSeverity
|
|
Range hcl.Range
|
|
DeclRange hcl.Range
|
|
}
|
|
|
|
func (r *Runner) LoadTestFile(filename string) (*TestFile, hcl.Diagnostics) {
|
|
f, diags := r.parser.ParseHCLFile(filename)
|
|
if diags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
|
|
content, moreDiags := f.Body.Content(testFileSchema)
|
|
diags = append(diags, moreDiags...)
|
|
if moreDiags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
|
|
ret := &TestFile{
|
|
ResultType: cty.DynamicPseudoType,
|
|
}
|
|
|
|
if typeAttr, exists := content.Attributes["result_type"]; exists {
|
|
ty, moreDiags := typeexpr.TypeConstraint(typeAttr.Expr)
|
|
diags = append(diags, moreDiags...)
|
|
if !moreDiags.HasErrors() {
|
|
ret.ResultType = ty
|
|
}
|
|
ret.ResultTypeRange = typeAttr.Expr.Range()
|
|
}
|
|
|
|
if resultAttr, exists := content.Attributes["result"]; exists {
|
|
resultVal, moreDiags := resultAttr.Expr.Value(nil)
|
|
diags = append(diags, moreDiags...)
|
|
if !moreDiags.HasErrors() {
|
|
resultVal, err := convert.Convert(resultVal, ret.ResultType)
|
|
if err != nil {
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid result value",
|
|
Detail: fmt.Sprintf("The result value does not conform to the given result type: %s.", err),
|
|
Subject: resultAttr.Expr.Range().Ptr(),
|
|
})
|
|
} else {
|
|
ret.Result = resultVal
|
|
}
|
|
}
|
|
ret.ResultRange = resultAttr.Expr.Range()
|
|
}
|
|
|
|
for _, block := range content.Blocks {
|
|
switch block.Type {
|
|
|
|
case "traversals":
|
|
if ret.ChecksTraversals {
|
|
// Indicates a duplicate traversals block
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Duplicate \"traversals\" block",
|
|
Detail: fmt.Sprintf("Only one traversals block is expected."),
|
|
Subject: &block.TypeRange,
|
|
})
|
|
continue
|
|
}
|
|
expectTraversals, moreDiags := r.decodeTraversalsBlock(block)
|
|
diags = append(diags, moreDiags...)
|
|
if !moreDiags.HasErrors() {
|
|
ret.ChecksTraversals = true
|
|
ret.ExpectedTraversals = expectTraversals
|
|
}
|
|
|
|
case "diagnostics":
|
|
if len(ret.ExpectedDiags) > 0 {
|
|
// Indicates a duplicate diagnostics block
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Duplicate \"diagnostics\" block",
|
|
Detail: fmt.Sprintf("Only one diagnostics block is expected."),
|
|
Subject: &block.TypeRange,
|
|
})
|
|
continue
|
|
}
|
|
expectDiags, moreDiags := r.decodeDiagnosticsBlock(block)
|
|
diags = append(diags, moreDiags...)
|
|
ret.ExpectedDiags = expectDiags
|
|
|
|
default:
|
|
// Shouldn't get here, because the above cases are exhaustive for
|
|
// our test file schema.
|
|
panic(fmt.Sprintf("unsupported block type %q", block.Type))
|
|
}
|
|
}
|
|
|
|
if ret.Result != cty.NilVal && len(ret.ExpectedDiags) > 0 {
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Conflicting spec expectations",
|
|
Detail: "This test spec includes expected diagnostics, so it may not also include an expected result.",
|
|
Subject: &content.Attributes["result"].Range,
|
|
})
|
|
}
|
|
|
|
return ret, diags
|
|
}
|
|
|
|
func (r *Runner) decodeTraversalsBlock(block *hcl.Block) ([]*TestFileExpectTraversal, hcl.Diagnostics) {
|
|
var diags hcl.Diagnostics
|
|
|
|
content, moreDiags := block.Body.Content(testFileTraversalsSchema)
|
|
diags = append(diags, moreDiags...)
|
|
if moreDiags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
|
|
var ret []*TestFileExpectTraversal
|
|
for _, block := range content.Blocks {
|
|
// There's only one block type in our schema, so we can assume all
|
|
// blocks are of that type.
|
|
expectTraversal, moreDiags := r.decodeTraversalExpectBlock(block)
|
|
diags = append(diags, moreDiags...)
|
|
if expectTraversal != nil {
|
|
ret = append(ret, expectTraversal)
|
|
}
|
|
}
|
|
|
|
return ret, diags
|
|
}
|
|
|
|
func (r *Runner) decodeTraversalExpectBlock(block *hcl.Block) (*TestFileExpectTraversal, hcl.Diagnostics) {
|
|
var diags hcl.Diagnostics
|
|
|
|
rng, body, moreDiags := r.decodeRangeFromBody(block.Body)
|
|
diags = append(diags, moreDiags...)
|
|
|
|
content, moreDiags := body.Content(testFileTraversalExpectSchema)
|
|
diags = append(diags, moreDiags...)
|
|
if moreDiags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
|
|
var traversal hcl.Traversal
|
|
{
|
|
refAttr := content.Attributes["ref"]
|
|
traversal, moreDiags = hcl.AbsTraversalForExpr(refAttr.Expr)
|
|
diags = append(diags, moreDiags...)
|
|
if moreDiags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
}
|
|
|
|
return &TestFileExpectTraversal{
|
|
Traversal: traversal,
|
|
Range: rng,
|
|
DeclRange: block.DefRange,
|
|
}, diags
|
|
}
|
|
|
|
func (r *Runner) decodeDiagnosticsBlock(block *hcl.Block) ([]*TestFileExpectDiag, hcl.Diagnostics) {
|
|
var diags hcl.Diagnostics
|
|
|
|
content, moreDiags := block.Body.Content(testFileDiagnosticsSchema)
|
|
diags = append(diags, moreDiags...)
|
|
if moreDiags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
|
|
if len(content.Blocks) == 0 {
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Empty diagnostics block",
|
|
Detail: "If a diagnostics block is present, at least one expectation statement (\"error\" or \"warning\" block) must be included.",
|
|
Subject: &block.TypeRange,
|
|
})
|
|
return nil, diags
|
|
}
|
|
|
|
ret := make([]*TestFileExpectDiag, 0, len(content.Blocks))
|
|
for _, block := range content.Blocks {
|
|
rng, remain, moreDiags := r.decodeRangeFromBody(block.Body)
|
|
diags = append(diags, moreDiags...)
|
|
if diags.HasErrors() {
|
|
continue
|
|
}
|
|
|
|
// Should have nothing else in the block aside from the range definition.
|
|
_, moreDiags = remain.Content(&hcl.BodySchema{})
|
|
diags = append(diags, moreDiags...)
|
|
|
|
var severity hcl.DiagnosticSeverity
|
|
switch block.Type {
|
|
case "error":
|
|
severity = hcl.DiagError
|
|
case "warning":
|
|
severity = hcl.DiagWarning
|
|
default:
|
|
panic(fmt.Sprintf("unsupported block type %q", block.Type))
|
|
}
|
|
|
|
ret = append(ret, &TestFileExpectDiag{
|
|
Severity: severity,
|
|
Range: rng,
|
|
DeclRange: block.TypeRange,
|
|
})
|
|
}
|
|
return ret, diags
|
|
}
|
|
|
|
func (r *Runner) decodeRangeFromBody(body hcl.Body) (hcl.Range, hcl.Body, hcl.Diagnostics) {
|
|
type RawPos struct {
|
|
Line int `hcl:"line"`
|
|
Column int `hcl:"column"`
|
|
Byte int `hcl:"byte"`
|
|
}
|
|
type RawRange struct {
|
|
From RawPos `hcl:"from,block"`
|
|
To RawPos `hcl:"to,block"`
|
|
Remain hcl.Body `hcl:",remain"`
|
|
}
|
|
|
|
var raw RawRange
|
|
diags := gohcl.DecodeBody(body, nil, &raw)
|
|
|
|
return hcl.Range{
|
|
// We intentionally omit Filename here, because the test spec doesn't
|
|
// need to specify that explicitly: we can infer it to be the file
|
|
// path we pass to hcldec.
|
|
Start: hcl.Pos{
|
|
Line: raw.From.Line,
|
|
Column: raw.From.Column,
|
|
Byte: raw.From.Byte,
|
|
},
|
|
End: hcl.Pos{
|
|
Line: raw.To.Line,
|
|
Column: raw.To.Column,
|
|
Byte: raw.To.Byte,
|
|
},
|
|
}, raw.Remain, diags
|
|
}
|
|
|
|
var testFileSchema = &hcl.BodySchema{
|
|
Attributes: []hcl.AttributeSchema{
|
|
{
|
|
Name: "result",
|
|
},
|
|
{
|
|
Name: "result_type",
|
|
},
|
|
},
|
|
Blocks: []hcl.BlockHeaderSchema{
|
|
{
|
|
Type: "traversals",
|
|
},
|
|
{
|
|
Type: "diagnostics",
|
|
},
|
|
},
|
|
}
|
|
|
|
var testFileTraversalsSchema = &hcl.BodySchema{
|
|
Blocks: []hcl.BlockHeaderSchema{
|
|
{
|
|
Type: "expect",
|
|
},
|
|
},
|
|
}
|
|
|
|
var testFileTraversalExpectSchema = &hcl.BodySchema{
|
|
Attributes: []hcl.AttributeSchema{
|
|
{
|
|
Name: "ref",
|
|
Required: true,
|
|
},
|
|
},
|
|
Blocks: []hcl.BlockHeaderSchema{
|
|
{
|
|
Type: "range",
|
|
},
|
|
},
|
|
}
|
|
|
|
var testFileDiagnosticsSchema = &hcl.BodySchema{
|
|
Blocks: []hcl.BlockHeaderSchema{
|
|
{
|
|
Type: "error",
|
|
},
|
|
{
|
|
Type: "warning",
|
|
},
|
|
},
|
|
}
|
|
|
|
var testFileRangeSchema = &hcl.BodySchema{
|
|
Blocks: []hcl.BlockHeaderSchema{
|
|
{
|
|
Type: "from",
|
|
},
|
|
{
|
|
Type: "to",
|
|
},
|
|
},
|
|
}
|
|
|
|
var testFilePosSchema = &hcl.BodySchema{
|
|
Attributes: []hcl.AttributeSchema{
|
|
{
|
|
Name: "line",
|
|
Required: true,
|
|
},
|
|
{
|
|
Name: "column",
|
|
Required: true,
|
|
},
|
|
{
|
|
Name: "byte",
|
|
Required: true,
|
|
},
|
|
},
|
|
}
|