hcl/ext/dynblock/expand_body_test.go
Martin Atkins 3dfebdfc45 ext/dynblock: Stub out contents when for_each is unknown
Previously our behavior for an unknown for_each was to produce a single
block whose content was the result of evaluating content with the iterator
set to cty.DynamicVal. That produced a reasonable idea of the content, but
the number of blocks in the result was still not accurate, and that can
present a problem for applications that use unknown values to predict
the overall shape of a not-yet-complete structure.

We can't return an unknown block via the HCL API, but to make that
situation easier to recognize by callers we'll now go a little further and
force _all_ of the leaf attributes in such a block to be unknown values,
even if they are constants in the configuration. This allows a calling
application that is making predictions to use a single object whose
leaves are all unknown as a heuristic to recognize what is effectively
an unknown set of blocks.

This is still not a perfect heuristic, but is the best we can do here
within the HCL API assumptions. A fundamental assumption of the HCL API
is that it's possible to walk the block structure without evaluating any
expressions and the dynamic block extension is intentionally subverting
that assumption, so some oddities are to be expected. Calling applications
that need a fully reliable sense of the final structure should not use
the dynamic block extension.
2019-04-30 11:30:46 -07:00

384 lines
11 KiB
Go

package dynblock
import (
"testing"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcldec"
"github.com/hashicorp/hcl2/hcltest"
"github.com/zclconf/go-cty/cty"
)
func TestExpand(t *testing.T) {
srcBody := hcltest.MockBody(&hcl.BodyContent{
Blocks: hcl.Blocks{
{
Type: "a",
Labels: []string{"static0"},
LabelRanges: []hcl.Range{hcl.Range{}},
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
"val": hcltest.MockExprLiteral(cty.StringVal("static a 0")),
}),
}),
},
{
Type: "b",
Body: hcltest.MockBody(&hcl.BodyContent{
Blocks: hcl.Blocks{
{
Type: "c",
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
"val0": hcltest.MockExprLiteral(cty.StringVal("static c 0")),
}),
}),
},
{
Type: "dynamic",
Labels: []string{"c"},
LabelRanges: []hcl.Range{hcl.Range{}},
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
"for_each": hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
cty.StringVal("dynamic c 0"),
cty.StringVal("dynamic c 1"),
})),
"iterator": hcltest.MockExprVariable("dyn_c"),
}),
Blocks: hcl.Blocks{
{
Type: "content",
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
"val0": hcltest.MockExprTraversalSrc("dyn_c.value"),
}),
}),
},
},
}),
},
},
}),
},
{
Type: "dynamic",
Labels: []string{"a"},
LabelRanges: []hcl.Range{hcl.Range{}},
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
"for_each": hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
cty.StringVal("dynamic a 0"),
cty.StringVal("dynamic a 1"),
cty.StringVal("dynamic a 2"),
})),
"labels": hcltest.MockExprList([]hcl.Expression{
hcltest.MockExprTraversalSrc("a.key"),
}),
}),
Blocks: hcl.Blocks{
{
Type: "content",
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
"val": hcltest.MockExprTraversalSrc("a.value"),
}),
}),
},
},
}),
},
{
Type: "dynamic",
Labels: []string{"b"},
LabelRanges: []hcl.Range{hcl.Range{}},
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
"for_each": hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
cty.StringVal("dynamic b 0"),
cty.StringVal("dynamic b 1"),
})),
"iterator": hcltest.MockExprVariable("dyn_b"),
}),
Blocks: hcl.Blocks{
{
Type: "content",
Body: hcltest.MockBody(&hcl.BodyContent{
Blocks: hcl.Blocks{
{
Type: "c",
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
"val0": hcltest.MockExprLiteral(cty.StringVal("static c 1")),
"val1": hcltest.MockExprTraversalSrc("dyn_b.value"),
}),
}),
},
{
Type: "dynamic",
Labels: []string{"c"},
LabelRanges: []hcl.Range{hcl.Range{}},
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
"for_each": hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
cty.StringVal("dynamic c 2"),
cty.StringVal("dynamic c 3"),
})),
}),
Blocks: hcl.Blocks{
{
Type: "content",
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
"val0": hcltest.MockExprTraversalSrc("c.value"),
"val1": hcltest.MockExprTraversalSrc("dyn_b.value"),
}),
}),
},
},
}),
},
},
}),
},
},
}),
},
{
Type: "dynamic",
Labels: []string{"b"},
LabelRanges: []hcl.Range{hcl.Range{}},
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
"for_each": hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{
cty.StringVal("dynamic c nested 0"),
cty.StringVal("dynamic c nested 1"),
}),
})),
"iterator": hcltest.MockExprVariable("dyn_b"),
}),
Blocks: hcl.Blocks{
{
Type: "content",
Body: hcltest.MockBody(&hcl.BodyContent{
Blocks: hcl.Blocks{
{
Type: "dynamic",
Labels: []string{"c"},
LabelRanges: []hcl.Range{hcl.Range{}},
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
"for_each": hcltest.MockExprTraversalSrc("dyn_b.value"),
}),
Blocks: hcl.Blocks{
{
Type: "content",
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
"val0": hcltest.MockExprTraversalSrc("c.value"),
"val1": hcltest.MockExprTraversalSrc("dyn_b.key"),
}),
}),
},
},
}),
},
},
}),
},
},
}),
},
{
Type: "dynamic",
Labels: []string{"b"},
LabelRanges: []hcl.Range{hcl.Range{}},
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
"for_each": hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.String))),
"iterator": hcltest.MockExprVariable("dyn_b"),
}),
Blocks: hcl.Blocks{
{
Type: "content",
Body: hcltest.MockBody(&hcl.BodyContent{
Blocks: hcl.Blocks{
{
Type: "dynamic",
Labels: []string{"c"},
LabelRanges: []hcl.Range{hcl.Range{}},
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
"for_each": hcltest.MockExprTraversalSrc("dyn_b.value"),
}),
Blocks: hcl.Blocks{
{
Type: "content",
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
"val0": hcltest.MockExprTraversalSrc("c.value"),
"val1": hcltest.MockExprTraversalSrc("dyn_b.key"),
}),
}),
},
},
}),
},
},
}),
},
},
}),
},
{
Type: "a",
Labels: []string{"static1"},
LabelRanges: []hcl.Range{hcl.Range{}},
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
"val": hcltest.MockExprLiteral(cty.StringVal("static a 1")),
}),
}),
},
},
})
dynBody := Expand(srcBody, nil)
var remain hcl.Body
t.Run("PartialDecode", func(t *testing.T) {
decSpec := &hcldec.BlockMapSpec{
TypeName: "a",
LabelNames: []string{"key"},
Nested: &hcldec.AttrSpec{
Name: "val",
Type: cty.String,
Required: true,
},
}
var got cty.Value
var diags hcl.Diagnostics
got, remain, diags = hcldec.PartialDecode(dynBody, decSpec, nil)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics")
for _, diag := range diags {
t.Logf("- %s", diag)
}
return
}
want := cty.MapVal(map[string]cty.Value{
"static0": cty.StringVal("static a 0"),
"static1": cty.StringVal("static a 1"),
"0": cty.StringVal("dynamic a 0"),
"1": cty.StringVal("dynamic a 1"),
"2": cty.StringVal("dynamic a 2"),
})
if !got.RawEquals(want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want)
}
})
t.Run("Decode", func(t *testing.T) {
decSpec := &hcldec.BlockListSpec{
TypeName: "b",
Nested: &hcldec.BlockListSpec{
TypeName: "c",
Nested: &hcldec.ObjectSpec{
"val0": &hcldec.AttrSpec{
Name: "val0",
Type: cty.String,
},
"val1": &hcldec.AttrSpec{
Name: "val1",
Type: cty.String,
},
},
},
}
var got cty.Value
var diags hcl.Diagnostics
got, diags = hcldec.Decode(remain, decSpec, nil)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics")
for _, diag := range diags {
t.Logf("- %s", diag)
}
return
}
want := cty.ListVal([]cty.Value{
cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"val0": cty.StringVal("static c 0"),
"val1": cty.NullVal(cty.String),
}),
cty.ObjectVal(map[string]cty.Value{
"val0": cty.StringVal("dynamic c 0"),
"val1": cty.NullVal(cty.String),
}),
cty.ObjectVal(map[string]cty.Value{
"val0": cty.StringVal("dynamic c 1"),
"val1": cty.NullVal(cty.String),
}),
}),
cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"val0": cty.StringVal("static c 1"),
"val1": cty.StringVal("dynamic b 0"),
}),
cty.ObjectVal(map[string]cty.Value{
"val0": cty.StringVal("dynamic c 2"),
"val1": cty.StringVal("dynamic b 0"),
}),
cty.ObjectVal(map[string]cty.Value{
"val0": cty.StringVal("dynamic c 3"),
"val1": cty.StringVal("dynamic b 0"),
}),
}),
cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"val0": cty.StringVal("static c 1"),
"val1": cty.StringVal("dynamic b 1"),
}),
cty.ObjectVal(map[string]cty.Value{
"val0": cty.StringVal("dynamic c 2"),
"val1": cty.StringVal("dynamic b 1"),
}),
cty.ObjectVal(map[string]cty.Value{
"val0": cty.StringVal("dynamic c 3"),
"val1": cty.StringVal("dynamic b 1"),
}),
}),
cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"val0": cty.StringVal("dynamic c nested 0"),
"val1": cty.StringVal("foo"),
}),
cty.ObjectVal(map[string]cty.Value{
"val0": cty.StringVal("dynamic c nested 1"),
"val1": cty.StringVal("foo"),
}),
}),
cty.ListVal([]cty.Value{
// This one comes from a dynamic block with an unknown for_each
// value, so we produce a single block object with all of the
// leaf attribute values set to unknown values.
cty.ObjectVal(map[string]cty.Value{
"val0": cty.UnknownVal(cty.String),
"val1": cty.UnknownVal(cty.String),
}),
}),
})
if !got.RawEquals(want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want)
}
})
}