integrationtest: exercise some features Terraform uses

As an extra level of confidence in addition to the unit tests, this
integration test verifies that a certain set of features that Terraform
uses are able to work properly together.

Terraform is used as an example here just because it's a more advanced
consumer of HCL and thus it exercises some codepaths that most
applications don't need, such as ExprList and AbsTraversalForExpr.
This commit is contained in:
Martin Atkins 2018-01-13 00:58:50 -08:00
parent 76b0988d90
commit f3af67f344
2 changed files with 326 additions and 0 deletions

View File

@ -0,0 +1,5 @@
// Package integrationtest is an internal package that contains some
// tests that attempt to exercise many HCL features together in realistic
// scenarios. This is in addition to -- but not a substitute for -- unit tests
// that verify the behavior of each feature separately.
package integrationtest

View File

@ -0,0 +1,321 @@
package integrationtest
import (
"reflect"
"sort"
"testing"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/hcl2/hcldec"
"github.com/davecgh/go-spew/spew"
"github.com/hashicorp/hcl2/gohcl"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/hcl2/hcl/json"
)
// TestTerraformLike parses both a native syntax and a JSON representation
// of the same HashiCorp Terraform-like configuration structure and then makes
// assertions against the result of each.
//
// Terraform exercises a lot of different HCL codepaths, so this is not
// exhaustive but tries to cover a variety of different relevant scenarios.
func TestTerraformLike(t *testing.T) {
tests := map[string]func() (*hcl.File, hcl.Diagnostics){
"native syntax": func() (*hcl.File, hcl.Diagnostics) {
return hclsyntax.ParseConfig(
[]byte(terraformLikeNativeSyntax),
"config.tf", hcl.Pos{Line: 1, Column: 1},
)
},
"JSON": func() (*hcl.File, hcl.Diagnostics) {
return json.Parse(
[]byte(terraformLikeJSON),
"config.tf.json",
)
},
}
type Variable struct {
Name string `hcl:"name,label"`
}
type Resource struct {
Type string `hcl:"type,label"`
Name string `hcl:"name,label"`
Config hcl.Body `hcl:",remain"`
DependsOn hcl.Expression `hcl:"depends_on,attr"`
}
type Root struct {
Variables []*Variable `hcl:"variable,block"`
Resources []*Resource `hcl:"resource,block"`
}
instanceDecode := &hcldec.ObjectSpec{
"image_id": &hcldec.AttrSpec{
Name: "image_id",
Required: true,
Type: cty.String,
},
"instance_type": &hcldec.AttrSpec{
Name: "instance_type",
Required: true,
Type: cty.String,
},
}
securityGroupDecode := &hcldec.ObjectSpec{
"ingress": &hcldec.BlockListSpec{
TypeName: "ingress",
Nested: &hcldec.ObjectSpec{
"cidr_block": &hcldec.AttrSpec{
Name: "cidr_block",
Required: true,
Type: cty.String,
},
},
},
}
for name, loadFunc := range tests {
t.Run(name, func(t *testing.T) {
file, diags := loadFunc()
if len(diags) != 0 {
t.Errorf("unexpected diagnostics during parse")
for _, diag := range diags {
t.Logf("- %s", diag)
}
return
}
body := file.Body
var root Root
diags = gohcl.DecodeBody(body, nil, &root)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics during root eval")
for _, diag := range diags {
t.Logf("- %s", diag)
}
return
}
wantVars := []*Variable{
{
Name: "image_id",
},
}
if gotVars := root.Variables; !reflect.DeepEqual(gotVars, wantVars) {
t.Errorf("wrong Variables\ngot: %swant: %s", spew.Sdump(gotVars), spew.Sdump(wantVars))
}
if got, want := len(root.Resources), 3; got != want {
t.Fatalf("wrong number of Resources %d; want %d", got, want)
}
sort.Slice(root.Resources, func(i, j int) bool {
return root.Resources[i].Name < root.Resources[j].Name
})
t.Run("resource 0", func(t *testing.T) {
r := root.Resources[0]
if got, want := r.Type, "happycloud_security_group"; got != want {
t.Errorf("wrong type %q; want %q", got, want)
}
if got, want := r.Name, "private"; got != want {
t.Errorf("wrong type %q; want %q", got, want)
}
cfg, diags := hcldec.Decode(r.Config, securityGroupDecode, nil)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics decoding Config")
for _, diag := range diags {
t.Logf("- %s", diag)
}
return
}
wantCfg := cty.ObjectVal(map[string]cty.Value{
"ingress": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"cidr_block": cty.StringVal("10.0.0.0/8"),
}),
cty.ObjectVal(map[string]cty.Value{
"cidr_block": cty.StringVal("192.168.0.0/16"),
}),
}),
})
if !cfg.RawEquals(wantCfg) {
t.Errorf("wrong config\ngot: %#v\nwant: %#v", cfg, wantCfg)
}
})
t.Run("resource 1", func(t *testing.T) {
r := root.Resources[1]
if got, want := r.Type, "happycloud_security_group"; got != want {
t.Errorf("wrong type %q; want %q", got, want)
}
if got, want := r.Name, "public"; got != want {
t.Errorf("wrong type %q; want %q", got, want)
}
cfg, diags := hcldec.Decode(r.Config, securityGroupDecode, nil)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics decoding Config")
for _, diag := range diags {
t.Logf("- %s", diag)
}
return
}
wantCfg := cty.ObjectVal(map[string]cty.Value{
"ingress": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"cidr_block": cty.StringVal("0.0.0.0/0"),
}),
}),
})
if !cfg.RawEquals(wantCfg) {
t.Errorf("wrong config\ngot: %#v\nwant: %#v", cfg, wantCfg)
}
})
t.Run("resource 2", func(t *testing.T) {
r := root.Resources[2]
if got, want := r.Type, "happycloud_instance"; got != want {
t.Errorf("wrong type %q; want %q", got, want)
}
if got, want := r.Name, "test"; got != want {
t.Errorf("wrong type %q; want %q", got, want)
}
vars := hcldec.Variables(r.Config, &hcldec.AttrSpec{
Name: "image_id",
Type: cty.String,
})
if got, want := len(vars), 1; got != want {
t.Errorf("wrong number of variables in image_id %#v; want %#v", got, want)
}
if got, want := vars[0].RootName(), "var"; got != want {
t.Errorf("wrong image_id variable RootName %#v; want %#v", got, want)
}
ctx := &hcl.EvalContext{
Variables: map[string]cty.Value{
"var": cty.ObjectVal(map[string]cty.Value{
"image_id": cty.StringVal("image-1234"),
}),
},
}
cfg, diags := hcldec.Decode(r.Config, instanceDecode, ctx)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics decoding Config")
for _, diag := range diags {
t.Logf("- %s", diag)
}
return
}
wantCfg := cty.ObjectVal(map[string]cty.Value{
"instance_type": cty.StringVal("z3.weedy"),
"image_id": cty.StringVal("image-1234"),
})
if !cfg.RawEquals(wantCfg) {
t.Errorf("wrong config\ngot: %#v\nwant: %#v", cfg, wantCfg)
}
exprs, diags := hcl.ExprList(r.DependsOn)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics extracting depends_on")
for _, diag := range diags {
t.Logf("- %s", diag)
}
return
}
if got, want := len(exprs), 1; got != want {
t.Errorf("wrong number of depends_on exprs %#v; want %#v", got, want)
}
traversal, diags := hcl.AbsTraversalForExpr(exprs[0])
if len(diags) != 0 {
t.Errorf("unexpected diagnostics decoding depends_on[0]")
for _, diag := range diags {
t.Logf("- %s", diag)
}
return
}
if got, want := len(traversal), 2; got != want {
t.Errorf("wrong number of depends_on traversal steps %#v; want %#v", got, want)
}
if got, want := traversal.RootName(), "happycloud_security_group"; got != want {
t.Errorf("wrong depends_on traversal RootName %#v; want %#v", got, want)
}
})
})
}
}
const terraformLikeNativeSyntax = `
variable "image_id" {
}
resource "happycloud_instance" "test" {
instance_type = "z3.weedy"
image_id = var.image_id
depends_on = [
happycloud_security_group.public,
]
}
resource "happycloud_security_group" "public" {
ingress {
cidr_block = "0.0.0.0/0"
}
}
resource "happycloud_security_group" "private" {
ingress {
cidr_block = "10.0.0.0/8"
}
ingress {
cidr_block = "192.168.0.0/16"
}
}
`
const terraformLikeJSON = `
{
"variable": {
"image_id": {}
},
"resource": {
"happycloud_instance": {
"test": {
"instance_type": "z3.weedy",
"image_id": "${var.image_id}",
"depends_on": [
"happycloud_security_group.public"
]
}
},
"happycloud_security_group": {
"public": {
"ingress": {
"cidr_block": "0.0.0.0/0"
}
},
"private": {
"ingress": [
{
"cidr_block": "10.0.0.0/8"
},
{
"cidr_block": "192.168.0.0/16"
}
]
}
}
}
}
`