From a213c4a648880c9e92e18db5dd36bd0a9b7cb65e Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Wed, 6 Jun 2018 10:43:58 -0700 Subject: [PATCH] functions: add tests and support for unknown values --- lang/funcs/collection.go | 96 ++++++++++++------ lang/funcs/collection_test.go | 184 ++++++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+), 32 deletions(-) diff --git a/lang/funcs/collection.go b/lang/funcs/collection.go index c206a6abd..3ad0d22e3 100644 --- a/lang/funcs/collection.go +++ b/lang/funcs/collection.go @@ -171,9 +171,16 @@ var CompactFunc = function.New(&function.Spec{ }, Type: function.StaticReturnType(cty.List(cty.String)), Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + listVal := args[0] + if !listVal.IsWhollyKnown() { + // If some of the element values aren't known yet then we + // can't yet return a compacted list + return cty.UnknownVal(retType), nil + } + var outputList []cty.Value - for it := args[0].ElementIterator(); it.Next(); { + for it := listVal.ElementIterator(); it.Next(); { _, v := it.Element() if v.AsString() == "" { continue @@ -263,11 +270,19 @@ var DistinctFunc = function.New(&function.Spec{ Type: cty.List(cty.DynamicPseudoType), }, }, - Type: function.StaticReturnType(cty.List(cty.DynamicPseudoType)), + Type: func(args []cty.Value) (cty.Type, error) { + return args[0].Type(), nil + }, + // Type: function.StaticReturnType(cty.List(cty.DynamicPseudoType)), Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + listVal := args[0] + + if !listVal.IsWhollyKnown() { + return cty.UnknownVal(retType), nil + } var list []cty.Value - for it := args[0].ElementIterator(); it.Next(); { + for it := listVal.ElementIterator(); it.Next(); { _, v := it.Element() list, err = appendIfMissing(list, v) if err != nil { @@ -296,6 +311,11 @@ var ChunklistFunc = function.New(&function.Spec{ return cty.List(args[0].Type()), nil }, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + listVal := args[0] + if !listVal.IsWhollyKnown() { + return cty.UnknownVal(retType), nil + } + var size int err = gocty.FromCtyValue(args[1], &size) if err != nil { @@ -310,7 +330,7 @@ var ChunklistFunc = function.New(&function.Spec{ // if size is 0, returns a list made of the initial list if size == 0 { - output = append(output, args[0]) + output = append(output, listVal) return cty.ListVal(output), nil } @@ -319,7 +339,7 @@ var ChunklistFunc = function.New(&function.Spec{ l := args[0].LengthInt() i := 0 - for it := args[0].ElementIterator(); it.Next(); { + for it := listVal.ElementIterator(); it.Next(); { _, v := it.Element() chunk = append(chunk, v) @@ -347,9 +367,12 @@ var FlattenFunc = function.New(&function.Spec{ Type: function.StaticReturnType(cty.List(cty.DynamicPseudoType)), Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { inputList := args[0] + if !inputList.IsWhollyKnown() { + return cty.UnknownVal(retType), nil + } if inputList.LengthInt() == 0 { - return cty.ListValEmpty(cty.DynamicPseudoType), nil + return cty.ListValEmpty(retType.ElementType()), nil } outputList := make([]cty.Value, 0) @@ -458,11 +481,14 @@ var LookupFunc = function.New(&function.Spec{ AllowDynamicType: true, AllowNull: true, }, - Type: function.StaticReturnType(cty.DynamicPseudoType), - Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + Type: func(args []cty.Value) (ret cty.Type, err error) { if len(args) < 1 || len(args) > 3 { - return cty.NilVal, fmt.Errorf("lookup() takes two or three arguments, got %d", len(args)) + return cty.NilType, fmt.Errorf("lookup() takes two or three arguments, got %d", len(args)) } + + return args[0].Type().ElementType(), nil + }, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { var defaultVal cty.Value defaultValueSet := false @@ -474,6 +500,10 @@ var LookupFunc = function.New(&function.Spec{ mapVar := args[0] lookupKey := args[1].AsString() + if !mapVar.IsWhollyKnown() { + return cty.UnknownVal(retType), nil + } + if mapVar.HasIndex(cty.StringVal(lookupKey)) == cty.True { v := mapVar.Index(cty.StringVal(lookupKey)) if ty := v.Type(); !ty.Equals(cty.NilType) { @@ -489,13 +519,11 @@ var LookupFunc = function.New(&function.Spec{ } if defaultValueSet { - defaultType := defaultVal.Type() - switch { - case defaultType.Equals(cty.String): - return cty.StringVal(defaultVal.AsString()), nil - case defaultType.Equals(cty.Number): - return cty.NumberVal(defaultVal.AsBigFloat()), nil + defaultVal, err = convert.Convert(defaultVal, retType) + if err != nil { + return cty.NilVal, err } + return defaultVal, nil } return cty.UnknownVal(cty.DynamicPseudoType), fmt.Errorf( @@ -537,6 +565,12 @@ var MapFunc = function.New(&function.Spec{ return cty.Map(valType), nil }, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + for _, arg := range args { + if !arg.IsWhollyKnown() { + return cty.UnknownVal(retType), nil + } + } + outputMap := make(map[string]cty.Value) for i := 0; i < len(args); i += 2 { @@ -612,6 +646,10 @@ var MatchkeysFunc = function.New(&function.Spec{ return cty.ListValEmpty(retType.ElementType()), nil } + if !args[0].IsWhollyKnown() || !args[0].IsWhollyKnown() { + return cty.UnknownVal(retType), nil + } + i := 0 for it := keys.ElementIterator(); it.Next(); { _, key := it.Element() @@ -659,7 +697,9 @@ var MergeFunc = function.New(&function.Spec{ outputMap := make(map[string]cty.Value) for _, arg := range args { - + if !arg.IsWhollyKnown() { + return cty.UnknownVal(retType), nil + } if !arg.Type().IsObjectType() && !arg.Type().IsMapType() { return cty.NilVal, fmt.Errorf("arguments must be maps or objects, got %#v", arg.Type().FriendlyName()) } @@ -694,6 +734,9 @@ var SliceFunc = function.New(&function.Spec{ }, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { inputList := args[0] + if !inputList.IsWhollyKnown() { + return cty.UnknownVal(retType), nil + } var startIndex, endIndex int if err = gocty.FromCtyValue(args[1], &startIndex); err != nil { @@ -791,25 +834,14 @@ var ValuesFunc = function.New(&function.Spec{ }, }, Type: func(args []cty.Value) (ret cty.Type, err error) { - values := args[0] - argTypes := make([]cty.Type, values.LengthInt()) - index := 0 - - for it := values.ElementIterator(); it.Next(); { - _, v := it.Element() - argTypes[index] = v.Type() - index++ - } - - valType, _ := convert.UnifyUnsafe(argTypes) - if valType == cty.NilType { - return cty.NilType, fmt.Errorf("map elements must have the same type") - } - - return cty.List(valType), nil + return cty.List(args[0].Type().ElementType()), nil }, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { mapVar := args[0] + if !mapVar.IsWhollyKnown() { + return cty.UnknownVal(retType), nil + } + keys, err := Keys(mapVar) if err != nil { return cty.NilVal, err diff --git a/lang/funcs/collection_test.go b/lang/funcs/collection_test.go index 9abaeef8b..49ebb16f5 100644 --- a/lang/funcs/collection_test.go +++ b/lang/funcs/collection_test.go @@ -299,6 +299,34 @@ func TestCoalesceList(t *testing.T) { }), false, }, + { // list with unknown values + []cty.Value{ + cty.ListVal([]cty.Value{ + cty.StringVal("first"), cty.StringVal("second"), + }), + cty.ListVal([]cty.Value{ + cty.UnknownVal(cty.String), + }), + }, + cty.ListVal([]cty.Value{ + cty.StringVal("first"), cty.StringVal("second"), + }), + false, + }, + { // list with unknown values + []cty.Value{ + cty.ListVal([]cty.Value{ + cty.UnknownVal(cty.String), + }), + cty.ListVal([]cty.Value{ + cty.StringVal("third"), cty.StringVal("fourth"), + }), + }, + cty.ListVal([]cty.Value{ + cty.UnknownVal(cty.String), + }), + false, + }, { []cty.Value{ cty.MapValEmpty(cty.DynamicPseudoType), @@ -375,6 +403,15 @@ func TestCompact(t *testing.T) { }), false, }, + { + cty.ListVal([]cty.Value{ + cty.StringVal("test"), + cty.UnknownVal(cty.String), + cty.StringVal(""), + }), + cty.UnknownVal(cty.List(cty.String)), + false, + }, { // errors on list of lists cty.ListVal([]cty.Value{ cty.ListVal([]cty.Value{ @@ -422,6 +459,12 @@ func TestContains(t *testing.T) { cty.NumberIntVal(3), cty.NumberIntVal(4), }) + listWithUnknown := cty.ListVal([]cty.Value{ + cty.StringVal("the"), + cty.StringVal("quick"), + cty.StringVal("brown"), + cty.UnknownVal(cty.String), + }) tests := []struct { List cty.Value @@ -435,6 +478,12 @@ func TestContains(t *testing.T) { cty.BoolVal(true), false, }, + { + listWithUnknown, + cty.StringVal("the"), + cty.BoolVal(true), + false, + }, { listOfStrings, cty.StringVal("penguin"), @@ -509,6 +558,16 @@ func TestIndex(t *testing.T) { cty.NumberIntVal(0), false, }, + { + cty.ListVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("b"), + cty.UnknownVal(cty.String), + }), + cty.StringVal("a"), + cty.NumberIntVal(0), + false, + }, { cty.ListVal([]cty.Value{ cty.StringVal("a"), @@ -620,6 +679,16 @@ func TestDistinct(t *testing.T) { }), false, }, + { + cty.ListVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("b"), + cty.StringVal("a"), + cty.UnknownVal(cty.String), + }), + cty.UnknownVal(cty.List(cty.String)), + false, + }, { cty.ListVal([]cty.Value{ cty.StringVal("a"), @@ -765,6 +834,16 @@ func TestChunklist(t *testing.T) { }), false, }, + { + cty.ListVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("b"), + cty.UnknownVal(cty.String), + }), + cty.NumberIntVal(1), + cty.UnknownVal(cty.List(cty.List(cty.String))), + false, + }, } for _, test := range tests { @@ -812,6 +891,20 @@ func TestFlatten(t *testing.T) { }), false, }, + { + cty.ListVal([]cty.Value{ + cty.ListVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("b"), + }), + cty.ListVal([]cty.Value{ + cty.UnknownVal(cty.String), + cty.StringVal("d"), + }), + }), + cty.UnknownVal(cty.List(cty.DynamicPseudoType)), + false, + }, { cty.ListValEmpty(cty.String), cty.ListValEmpty(cty.DynamicPseudoType), @@ -861,6 +954,11 @@ func TestKeys(t *testing.T) { cty.NilVal, true, }, + { // Unknown map + cty.UnknownVal(cty.Map(cty.String)), + cty.UnknownVal(cty.List(cty.String)), + false, + }, } for _, test := range tests { @@ -927,6 +1025,17 @@ func TestList(t *testing.T) { }), false, }, + { + []cty.Value{ + cty.StringVal("Hello"), + cty.UnknownVal(cty.String), + }, + cty.ListVal([]cty.Value{ + cty.StringVal("Hello"), + cty.UnknownVal(cty.String), + }), + false, + }, } for _, test := range tests { @@ -962,6 +1071,10 @@ func TestLookup(t *testing.T) { cty.StringVal("baz"), }), }) + mapWithUnknowns := cty.MapVal(map[string]cty.Value{ + "foo": cty.StringVal("bar"), + "baz": cty.UnknownVal(cty.String), + }) tests := []struct { Values []cty.Value @@ -1046,6 +1159,14 @@ func TestLookup(t *testing.T) { cty.NilVal, true, }, + { + []cty.Value{ + mapWithUnknowns, + cty.StringVal("baz"), + }, + cty.UnknownVal(cty.String), + false, + }, } for _, test := range tests { @@ -1084,6 +1205,14 @@ func TestMap(t *testing.T) { }), false, }, + { + []cty.Value{ + cty.StringVal("hello"), + cty.UnknownVal(cty.String), + }, + cty.UnknownVal(cty.Map(cty.String)), + false, + }, { []cty.Value{ cty.StringVal("hello"), @@ -1307,6 +1436,23 @@ func TestMatchkeys(t *testing.T) { }), false, }, + { // unknowns + cty.ListVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("b"), + cty.UnknownVal(cty.String), + }), + cty.ListVal([]cty.Value{ + cty.StringVal("ref1"), + cty.StringVal("ref2"), + cty.UnknownVal(cty.String), + }), + cty.ListVal([]cty.Value{ + cty.StringVal("ref1"), + }), + cty.UnknownVal(cty.List(cty.String)), + false, + }, // errors { // different types cty.ListVal([]cty.Value{ @@ -1396,6 +1542,18 @@ func TestMerge(t *testing.T) { }), false, }, + { // handle unknowns + []cty.Value{ + cty.MapVal(map[string]cty.Value{ + "a": cty.UnknownVal(cty.String), + }), + cty.MapVal(map[string]cty.Value{ + "c": cty.StringVal("d"), + }), + }, + cty.DynamicVal, + false, + }, { // merge with conflicts is ok, last in wins []cty.Value{ cty.MapVal(map[string]cty.Value{ @@ -1548,6 +1706,10 @@ func TestSlice(t *testing.T) { cty.NumberIntVal(1), cty.NumberIntVal(2), }) + listWithUnknowns := cty.ListVal([]cty.Value{ + cty.StringVal("a"), + cty.UnknownVal(cty.String), + }) tests := []struct { List cty.Value StartIndex cty.Value @@ -1564,6 +1726,13 @@ func TestSlice(t *testing.T) { }), false, }, + { // unknowns in the list + listWithUnknowns, + cty.NumberIntVal(1), + cty.NumberIntVal(2), + cty.UnknownVal(cty.List(cty.String)), + false, + }, { // normal usage listOfInts, cty.NumberIntVal(1), @@ -1661,6 +1830,13 @@ func TestTranspose(t *testing.T) { }), false, }, + { // map - unknown value + cty.MapVal(map[string]cty.Value{ + "key1": cty.UnknownVal(cty.List(cty.String)), + }), + cty.UnknownVal(cty.Map(cty.List(cty.String))), + false, + }, { // bad map - empty value cty.MapVal(map[string]cty.Value{ "key1": cty.ListValEmpty(cty.String), @@ -1736,6 +1912,14 @@ func TestValues(t *testing.T) { }), false, }, + { // map with unknowns + cty.MapVal(map[string]cty.Value{ + "hello": cty.ListVal([]cty.Value{cty.StringVal("world")}), + "what's": cty.UnknownVal(cty.List(cty.String)), + }), + cty.UnknownVal(cty.List(cty.List(cty.String))), + false, + }, } for _, test := range tests {