blob: 57ed35ee7f57a364c69378602b874e5119f96eb2 [file] [log] [blame] [edit]
package main
import (
"fmt"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/ext/typeexpr"
"github.com/hashicorp/hcl/v2/gohcl"
)
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,
},
},
}