opentofu/lang/eval_test.go
Martin Atkins 479c6b2466 move "configschema" from "config" to "configs"
The "config" package is no longer used and will be removed as part
of the 0.12 release cleanup. Since configschema is part of the
"new world" of configuration modelling, it makes more sense for
it to live as a subdirectory of the newer "configs" package.
2018-10-16 18:50:29 -07:00

545 lines
12 KiB
Go

package lang
import (
"bytes"
"encoding/json"
"testing"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
)
func TestScopeEvalContext(t *testing.T) {
data := &dataForTests{
CountAttrs: map[string]cty.Value{
"index": cty.NumberIntVal(0),
},
ResourceInstances: map[string]cty.Value{
"null_resource.foo": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("bar"),
}),
"data.null_data_source.foo": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("bar"),
}),
"null_resource.multi": cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi0"),
}),
cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi1"),
}),
}),
"null_resource.multi[1]": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi1"),
}),
},
LocalValues: map[string]cty.Value{
"foo": cty.StringVal("bar"),
},
Modules: map[string]cty.Value{
"module.foo": cty.ObjectVal(map[string]cty.Value{
"output0": cty.StringVal("bar0"),
"output1": cty.StringVal("bar1"),
}),
},
PathAttrs: map[string]cty.Value{
"module": cty.StringVal("foo/bar"),
},
TerraformAttrs: map[string]cty.Value{
"workspace": cty.StringVal("default"),
},
InputVariables: map[string]cty.Value{
"baz": cty.StringVal("boop"),
},
}
tests := []struct {
Expr string
Want map[string]cty.Value
}{
{
`12`,
map[string]cty.Value{},
},
{
`count.index`,
map[string]cty.Value{
"count": cty.ObjectVal(map[string]cty.Value{
"index": cty.NumberIntVal(0),
}),
},
},
{
`local.foo`,
map[string]cty.Value{
"local": cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
}),
},
},
{
`null_resource.foo`,
map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("bar"),
}),
}),
},
},
{
`null_resource.foo.attr`,
map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("bar"),
}),
}),
},
},
{
`null_resource.multi`,
map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"multi": cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi0"),
}),
cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi1"),
}),
}),
}),
},
},
{
`null_resource.multi[1]`,
map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"multi": cty.TupleVal([]cty.Value{
cty.DynamicVal,
cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi1"),
}),
}),
}),
},
},
{
`foo(null_resource.multi, null_resource.multi[1])`,
map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"multi": cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi0"),
}),
cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi1"),
}),
}),
}),
},
},
{
`data.null_data_source.foo`,
map[string]cty.Value{
"data": cty.ObjectVal(map[string]cty.Value{
"null_data_source": cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("bar"),
}),
}),
}),
},
},
{
`module.foo`,
map[string]cty.Value{
"module": cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"output0": cty.StringVal("bar0"),
"output1": cty.StringVal("bar1"),
}),
}),
},
},
{
`module.foo.output1`,
map[string]cty.Value{
"module": cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"output1": cty.StringVal("bar1"),
}),
}),
},
},
{
`path.module`,
map[string]cty.Value{
"path": cty.ObjectVal(map[string]cty.Value{
"module": cty.StringVal("foo/bar"),
}),
},
},
{
`self.baz`,
map[string]cty.Value{
// In the test function below we set "SelfAddr" to be
// one of the resources in our dataset, causing it to get
// expanded here and then copied into "self".
"null_resource": cty.ObjectVal(map[string]cty.Value{
"multi": cty.TupleVal([]cty.Value{
cty.DynamicVal,
cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi1"),
}),
}),
}),
"self": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi1"),
}),
},
},
{
`terraform.workspace`,
map[string]cty.Value{
"terraform": cty.ObjectVal(map[string]cty.Value{
"workspace": cty.StringVal("default"),
}),
},
},
{
`var.baz`,
map[string]cty.Value{
"var": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("boop"),
}),
},
},
}
for _, test := range tests {
t.Run(test.Expr, func(t *testing.T) {
expr, parseDiags := hclsyntax.ParseExpression([]byte(test.Expr), "", hcl.Pos{Line: 1, Column: 1})
if len(parseDiags) != 0 {
t.Errorf("unexpected diagnostics during parse")
for _, diag := range parseDiags {
t.Errorf("- %s", diag)
}
return
}
refs, refsDiags := ReferencesInExpr(expr)
if refsDiags.HasErrors() {
t.Fatal(refsDiags.Err())
}
scope := &Scope{
Data: data,
// "self" will just be an arbitrary one of the several resource
// instances we have in our test dataset.
SelfAddr: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "null_resource",
Name: "multi",
},
Key: addrs.IntKey(1),
},
}
ctx, ctxDiags := scope.EvalContext(refs)
if ctxDiags.HasErrors() {
t.Fatal(ctxDiags.Err())
}
// For easier test assertions we'll just remove any top-level
// empty objects from our variables map.
for k, v := range ctx.Variables {
if v.RawEquals(cty.EmptyObjectVal) {
delete(ctx.Variables, k)
}
}
gotVal := cty.ObjectVal(ctx.Variables)
wantVal := cty.ObjectVal(test.Want)
if !gotVal.RawEquals(wantVal) {
// We'll JSON-ize our values here just so it's easier to
// read them in the assertion output.
gotJSON := formattedJSONValue(gotVal)
wantJSON := formattedJSONValue(wantVal)
t.Errorf(
"wrong result\nexpr: %s\ngot: %s\nwant: %s",
test.Expr, gotJSON, wantJSON,
)
}
})
}
}
func TestScopeExpandEvalBlock(t *testing.T) {
schema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
},
},
BlockTypes: map[string]*configschema.NestedBlock{
"bar": {
Nesting: configschema.NestingMap,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"baz": {
Type: cty.String,
},
},
},
},
},
}
data := &dataForTests{
LocalValues: map[string]cty.Value{
"greeting": cty.StringVal("howdy"),
"list": cty.ListVal([]cty.Value{
cty.StringVal("elem0"),
cty.StringVal("elem1"),
}),
"map": cty.MapVal(map[string]cty.Value{
"key1": cty.StringVal("val1"),
"key2": cty.StringVal("val2"),
}),
},
}
tests := map[string]struct {
Config string
Want cty.Value
}{
"empty": {
`
`,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.NullVal(cty.String),
"bar": cty.MapValEmpty(cty.Object(map[string]cty.Type{
"baz": cty.String,
})),
}),
},
"literal attribute": {
`
foo = "hello"
`,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("hello"),
"bar": cty.MapValEmpty(cty.Object(map[string]cty.Type{
"baz": cty.String,
})),
}),
},
"variable attribute": {
`
foo = local.greeting
`,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("howdy"),
"bar": cty.MapValEmpty(cty.Object(map[string]cty.Type{
"baz": cty.String,
})),
}),
},
"one static block": {
`
bar "static" {}
`,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.NullVal(cty.String),
"bar": cty.MapVal(map[string]cty.Value{
"static": cty.ObjectVal(map[string]cty.Value{
"baz": cty.NullVal(cty.String),
}),
}),
}),
},
"two static blocks": {
`
bar "static0" {
baz = 0
}
bar "static1" {
baz = 1
}
`,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.NullVal(cty.String),
"bar": cty.MapVal(map[string]cty.Value{
"static0": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("0"),
}),
"static1": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("1"),
}),
}),
}),
},
"dynamic blocks from list": {
`
dynamic "bar" {
for_each = local.list
labels = [bar.value]
content {
baz = bar.key
}
}
`,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.NullVal(cty.String),
"bar": cty.MapVal(map[string]cty.Value{
"elem0": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("0"),
}),
"elem1": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("1"),
}),
}),
}),
},
"dynamic blocks from map": {
`
dynamic "bar" {
for_each = local.map
labels = [bar.key]
content {
baz = bar.value
}
}
`,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.NullVal(cty.String),
"bar": cty.MapVal(map[string]cty.Value{
"key1": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("val1"),
}),
"key2": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("val2"),
}),
}),
}),
},
"everything at once": {
`
foo = "whoop"
bar "static0" {
baz = "s0"
}
dynamic "bar" {
for_each = local.list
labels = [bar.value]
content {
baz = bar.key
}
}
bar "static1" {
baz = "s1"
}
dynamic "bar" {
for_each = local.map
labels = [bar.key]
content {
baz = bar.value
}
}
bar "static2" {
baz = "s2"
}
`,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("whoop"),
"bar": cty.MapVal(map[string]cty.Value{
"key1": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("val1"),
}),
"key2": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("val2"),
}),
"elem0": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("0"),
}),
"elem1": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("1"),
}),
"static0": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("s0"),
}),
"static1": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("s1"),
}),
"static2": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("s2"),
}),
}),
}),
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
file, parseDiags := hclsyntax.ParseConfig([]byte(test.Config), "", hcl.Pos{Line: 1, Column: 1})
if len(parseDiags) != 0 {
t.Errorf("unexpected diagnostics during parse")
for _, diag := range parseDiags {
t.Errorf("- %s", diag)
}
return
}
body := file.Body
scope := &Scope{
Data: data,
}
body, expandDiags := scope.ExpandBlock(body, schema)
if expandDiags.HasErrors() {
t.Fatal(expandDiags.Err())
}
got, valDiags := scope.EvalBlock(body, schema)
if valDiags.HasErrors() {
t.Fatal(valDiags.Err())
}
if !got.RawEquals(test.Want) {
// We'll JSON-ize our values here just so it's easier to
// read them in the assertion output.
gotJSON := formattedJSONValue(got)
wantJSON := formattedJSONValue(test.Want)
t.Errorf(
"wrong result\nconfig: %s\ngot: %s\nwant: %s",
test.Config, gotJSON, wantJSON,
)
}
})
}
}
func formattedJSONValue(val cty.Value) string {
val = cty.UnknownAsNull(val) // since JSON can't represent unknowns
j, err := ctyjson.Marshal(val, val.Type())
if err != nil {
panic(err)
}
var buf bytes.Buffer
json.Indent(&buf, j, "", " ")
return buf.String()
}