hcl/zcl/hclhil/structure_test.go
Martin Atkins fde586e193 hclhil: Expression.Value implementation
Currently only deals with the literal HCL structure. Later we will also
support HIL parsing and evaluation within strings, to achieve parity with
existing uses of HCL/HIL together. For now, this has parity with uses of
HCL alone, with the exception that float and int values are not
distinguished, because cty does not make this distinction.
2017-05-21 18:42:39 -07:00

500 lines
9.7 KiB
Go

package hclhil
import (
"fmt"
"reflect"
"testing"
"github.com/apparentlymart/go-cty/cty"
"github.com/apparentlymart/go-zcl/zcl"
"github.com/davecgh/go-spew/spew"
hclast "github.com/hashicorp/hcl/hcl/ast"
hcltoken "github.com/hashicorp/hcl/hcl/token"
)
func TestBodyPartialContent(t *testing.T) {
tests := []struct {
Source string
Schema *zcl.BodySchema
Want *zcl.BodyContent
DiagCount int
}{
{
``,
&zcl.BodySchema{},
&zcl.BodyContent{
Attributes: zcl.Attributes{},
},
0,
},
{
`foo = 1`,
&zcl.BodySchema{},
&zcl.BodyContent{
Attributes: zcl.Attributes{},
},
0,
},
{
`foo = 1`,
&zcl.BodySchema{
Attributes: []zcl.AttributeSchema{
{
Name: "foo",
},
},
},
&zcl.BodyContent{
Attributes: zcl.Attributes{
"foo": {
Name: "foo",
Expr: &expression{
src: &hclast.LiteralType{
Token: hcltoken.Token{
Type: hcltoken.NUMBER,
Text: `1`,
Pos: hcltoken.Pos{
Offset: 6,
Line: 1,
Column: 7,
},
},
},
},
Range: zcl.Range{
Start: zcl.Pos{Byte: 0, Line: 1, Column: 1},
End: zcl.Pos{Byte: 1, Line: 1, Column: 2},
},
NameRange: zcl.Range{
Start: zcl.Pos{Byte: 0, Line: 1, Column: 1},
End: zcl.Pos{Byte: 1, Line: 1, Column: 2},
},
},
},
},
0,
},
{
``,
&zcl.BodySchema{
Attributes: []zcl.AttributeSchema{
{
Name: "foo",
Required: true,
},
},
},
&zcl.BodyContent{
Attributes: zcl.Attributes{},
},
1, // missing required attribute
},
{
`foo {}`,
&zcl.BodySchema{
Blocks: []zcl.BlockHeaderSchema{
{
Type: "foo",
},
},
},
&zcl.BodyContent{
Attributes: zcl.Attributes{},
Blocks: zcl.Blocks{
{
Type: "foo",
Body: &body{
oli: &hclast.ObjectList{},
},
DefRange: zcl.Range{
Start: zcl.Pos{Byte: 4, Line: 1, Column: 5},
End: zcl.Pos{Byte: 5, Line: 1, Column: 6},
},
TypeRange: zcl.Range{
Start: zcl.Pos{Byte: 0, Line: 1, Column: 1},
End: zcl.Pos{Byte: 1, Line: 1, Column: 2},
},
},
},
},
0,
},
{
`foo "unwanted" {}`,
&zcl.BodySchema{
Blocks: []zcl.BlockHeaderSchema{
{
Type: "foo",
},
},
},
&zcl.BodyContent{
Attributes: zcl.Attributes{},
},
1, // no labels are expected
},
{
`foo {}`,
&zcl.BodySchema{
Blocks: []zcl.BlockHeaderSchema{
{
Type: "foo",
LabelNames: []string{"name"},
},
},
},
&zcl.BodyContent{
Attributes: zcl.Attributes{},
},
1, // missing name
},
{
`foo "wanted" {}`,
&zcl.BodySchema{
Blocks: []zcl.BlockHeaderSchema{
{
Type: "foo",
LabelNames: []string{"name"},
},
},
},
&zcl.BodyContent{
Attributes: zcl.Attributes{},
Blocks: zcl.Blocks{
{
Type: "foo",
Labels: []string{"wanted"},
Body: &body{
oli: &hclast.ObjectList{},
},
DefRange: zcl.Range{
Start: zcl.Pos{Byte: 13, Line: 1, Column: 14},
End: zcl.Pos{Byte: 14, Line: 1, Column: 15},
},
TypeRange: zcl.Range{
Start: zcl.Pos{Byte: 0, Line: 1, Column: 1},
End: zcl.Pos{Byte: 1, Line: 1, Column: 2},
},
LabelRanges: []zcl.Range{
{
Start: zcl.Pos{Byte: 4, Line: 1, Column: 5},
End: zcl.Pos{Byte: 5, Line: 1, Column: 6},
},
},
},
},
},
0,
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
file, diags := Parse([]byte(test.Source), "test.hcl")
if len(diags) != 0 {
t.Fatalf("diagnostics from parse: %s", diags.Error())
}
got, _, diags := file.Body.PartialContent(test.Schema)
if len(diags) != test.DiagCount {
t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount)
for _, diag := range diags {
t.Logf(" - %s", diag.Error())
}
}
if !reflect.DeepEqual(got, test.Want) {
t.Errorf("wrong result\ngot: %s\nwant: %s", spew.Sdump(got), spew.Sdump(test.Want))
}
})
}
}
func TestBodyJustAttributes(t *testing.T) {
tests := []struct {
Source string
Want zcl.Attributes
DiagCount int
}{
{
``,
zcl.Attributes{},
0,
},
{
`foo = "a"`,
zcl.Attributes{
"foo": &zcl.Attribute{
Name: "foo",
Expr: &expression{
src: &hclast.LiteralType{
Token: hcltoken.Token{
Type: hcltoken.STRING,
Pos: hcltoken.Pos{
Offset: 6,
Line: 1,
Column: 7,
},
Text: `"a"`,
},
},
},
Range: zcl.Range{
Start: zcl.Pos{Byte: 0, Line: 1, Column: 1},
End: zcl.Pos{Byte: 1, Line: 1, Column: 2},
},
NameRange: zcl.Range{
Start: zcl.Pos{Byte: 0, Line: 1, Column: 1},
End: zcl.Pos{Byte: 1, Line: 1, Column: 2},
},
},
},
0,
},
{
`foo = {}`,
zcl.Attributes{
"foo": &zcl.Attribute{
Name: "foo",
Expr: &expression{
src: &hclast.ObjectType{
List: &hclast.ObjectList{},
Lbrace: hcltoken.Pos{
Offset: 6,
Line: 1,
Column: 7,
},
Rbrace: hcltoken.Pos{
Offset: 7,
Line: 1,
Column: 8,
},
},
},
Range: zcl.Range{
Start: zcl.Pos{Byte: 0, Line: 1, Column: 1},
End: zcl.Pos{Byte: 1, Line: 1, Column: 2},
},
NameRange: zcl.Range{
Start: zcl.Pos{Byte: 0, Line: 1, Column: 1},
End: zcl.Pos{Byte: 1, Line: 1, Column: 2},
},
},
},
0,
},
{
`foo {}`,
zcl.Attributes{
"foo": &zcl.Attribute{
Name: "foo",
Expr: &expression{
src: &hclast.ObjectType{
List: &hclast.ObjectList{},
Lbrace: hcltoken.Pos{
Offset: 4,
Line: 1,
Column: 5,
},
Rbrace: hcltoken.Pos{
Offset: 5,
Line: 1,
Column: 6,
},
},
},
Range: zcl.Range{
Start: zcl.Pos{Byte: 0, Line: 1, Column: 1},
End: zcl.Pos{Byte: 1, Line: 1, Column: 2},
},
NameRange: zcl.Range{
Start: zcl.Pos{Byte: 0, Line: 1, Column: 1},
End: zcl.Pos{Byte: 1, Line: 1, Column: 2},
},
},
},
1, // warning about using block syntax
},
{
`foo "bar" {}`,
zcl.Attributes{},
1, // blocks are not allowed here
},
{
`
foo = 1
foo = 2
`,
zcl.Attributes{
"foo": &zcl.Attribute{
Name: "foo",
Expr: &expression{
src: &hclast.LiteralType{
Token: hcltoken.Token{
Type: hcltoken.NUMBER,
Pos: hcltoken.Pos{
Offset: 14,
Line: 2,
Column: 14,
},
Text: `1`,
},
},
},
Range: zcl.Range{
Start: zcl.Pos{Byte: 8, Line: 2, Column: 8},
End: zcl.Pos{Byte: 9, Line: 2, Column: 9},
},
NameRange: zcl.Range{
Start: zcl.Pos{Byte: 8, Line: 2, Column: 8},
End: zcl.Pos{Byte: 9, Line: 2, Column: 9},
},
},
},
1, // duplicate definition of foo
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
file, diags := Parse([]byte(test.Source), "test.hcl")
if len(diags) != 0 {
t.Fatalf("diagnostics from parse: %s", diags.Error())
}
got, diags := file.Body.JustAttributes()
if len(diags) != test.DiagCount {
t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount)
for _, diag := range diags {
t.Logf(" - %s", diag.Error())
}
}
if !reflect.DeepEqual(got, test.Want) {
t.Errorf("wrong result\ngot: %s\nwant: %s", spew.Sdump(got), spew.Sdump(test.Want))
}
})
}
}
func TestExpressionValue(t *testing.T) {
tests := []struct {
Source string // HCL source assigning a value to attribute "v"
Want cty.Value
DiagCount int
}{
{
`v = 1`,
cty.NumberIntVal(1),
0,
},
{
`v = 1.5`,
cty.NumberFloatVal(1.5),
0,
},
{
`v = "hello"`,
cty.StringVal("hello"),
0,
},
{
`v = <<EOT
heredoc
EOT
`,
cty.StringVal("heredoc\n"),
0,
},
{
`v = true`,
cty.True,
0,
},
{
`v = false`,
cty.False,
0,
},
{
`v = []`,
cty.EmptyTupleVal,
0,
},
{
`v = ["hello", 5, true, 3.4]`,
cty.TupleVal([]cty.Value{
cty.StringVal("hello"),
cty.NumberIntVal(5),
cty.True,
cty.NumberFloatVal(3.4),
}),
0,
},
{
`v = {}`,
cty.EmptyObjectVal,
0,
},
{
`v = {
string = "hello"
int = 5
bool = true
float = 3.4
list = []
object = {}
}`,
cty.ObjectVal(map[string]cty.Value{
"string": cty.StringVal("hello"),
"int": cty.NumberIntVal(5),
"bool": cty.True,
"float": cty.NumberFloatVal(3.4),
"list": cty.EmptyTupleVal,
"object": cty.EmptyObjectVal,
}),
0,
},
{
`v {}`,
cty.EmptyObjectVal,
0, // warns about using block syntax during content extraction, but we ignore that here
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
file, diags := Parse([]byte(test.Source), "test.hcl")
if len(diags) != 0 {
t.Fatalf("diagnostics from parse: %s", diags.Error())
}
content, diags := file.Body.Content(&zcl.BodySchema{
Attributes: []zcl.AttributeSchema{
{
Name: "v",
Required: true,
},
},
})
expr := content.Attributes["v"].Expr
got, diags := expr.Value(nil)
if len(diags) != test.DiagCount {
t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount)
for _, diag := range diags {
t.Logf(" - %s", diag.Error())
}
}
if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}