Skip to content

Commit

Permalink
function/stdlib: SetproductFunc precise mark handling
Browse files Browse the repository at this point in the history
Setproduct will now preserve marks on both the arguments and their
elements. Marks that apply to any entire argument will propagate to all
return values, while marks applied to a single element will remain
attached to individual values.

In the cases where this function returns a set result the marks still end
up aggregated at the root set value anyway due to cty's treatment of marks
in sets, but we can retain the precision in the cases where we'll return
a list.
  • Loading branch information
mildwonkey authored and apparentlymart committed Apr 20, 2021
1 parent 385197f commit af7c1a1
Show file tree
Hide file tree
Showing 2 changed files with 269 additions and 7 deletions.
21 changes: 14 additions & 7 deletions cty/function/stdlib/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -886,8 +886,9 @@ var ReverseListFunc = function.New(&function.Spec{
var SetProductFunc = function.New(&function.Spec{
Params: []function.Parameter{},
VarParam: &function.Parameter{
Name: "sets",
Type: cty.DynamicPseudoType,
Name: "sets",
Type: cty.DynamicPseudoType,
AllowMarked: true,
},
Type: func(args []cty.Value) (retType cty.Type, err error) {
if len(args) < 2 {
Expand Down Expand Up @@ -931,11 +932,15 @@ var SetProductFunc = function.New(&function.Spec{
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
ety := retType.ElementType()
var retMarks cty.ValueMarks

total := 1
for _, arg := range args {
arg, marks := arg.Unmark()
retMarks = cty.NewValueMarks(retMarks, marks)

if !arg.Length().IsKnown() {
return cty.UnknownVal(retType), nil
return cty.UnknownVal(retType).Mark(marks), nil
}

// Because of our type checking function, we are guaranteed that
Expand All @@ -948,9 +953,9 @@ var SetProductFunc = function.New(&function.Spec{
// If any of the arguments was an empty collection then our result
// is also an empty collection, which we'll short-circuit here.
if retType.IsListType() {
return cty.ListValEmpty(ety), nil
return cty.ListValEmpty(ety).Mark(retMarks), nil
}
return cty.SetValEmpty(ety), nil
return cty.SetValEmpty(ety).Mark(retMarks), nil
}

subEtys := ety.TupleElementTypes()
Expand All @@ -961,6 +966,8 @@ var SetProductFunc = function.New(&function.Spec{
s := 0
argVals := make([][]cty.Value, len(args))
for i, arg := range args {
// We've already stored the marks in retMarks
arg, _ := arg.Unmark()
argVals[i] = arg.AsValueSlice()
}

Expand Down Expand Up @@ -1000,9 +1007,9 @@ var SetProductFunc = function.New(&function.Spec{
}

if retType.IsListType() {
return cty.ListVal(productVals), nil
return cty.ListVal(productVals).WithMarks(retMarks), nil
}
return cty.SetVal(productVals), nil
return cty.SetVal(productVals).WithMarks(retMarks), nil
},
})

Expand Down
255 changes: 255 additions & 0 deletions cty/function/stdlib/collection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1927,3 +1927,258 @@ func TestFlatten(t *testing.T) {
})
}
}

func TestSetproduct(t *testing.T) {
tests := []struct {
Collections []cty.Value
Want cty.Value
Err string
}{
{
[]cty.Value{cty.ListValEmpty(cty.String)},
cty.NilVal,
`at least two arguments are required`,
},
{
[]cty.Value{
cty.ListVal([]cty.Value{cty.ListValEmpty(cty.String)}),
cty.ListVal([]cty.Value{cty.ListValEmpty(cty.String)}),
},
cty.ListVal([]cty.Value{cty.TupleVal([]cty.Value{cty.ListValEmpty(cty.String), cty.ListValEmpty(cty.String)})}),
``,
},
{
[]cty.Value{
cty.SetVal([]cty.Value{cty.ListValEmpty(cty.String)}),
cty.SetVal([]cty.Value{cty.ListValEmpty(cty.String)}),
},
cty.SetVal([]cty.Value{cty.TupleVal([]cty.Value{cty.ListValEmpty(cty.String), cty.ListValEmpty(cty.String)})}),
``,
},
{
[]cty.Value{
cty.SetVal([]cty.Value{cty.ListValEmpty(cty.String).Mark("a")}),
cty.SetVal([]cty.Value{cty.ListValEmpty(cty.String)}),
},
cty.SetVal([]cty.Value{cty.TupleVal([]cty.Value{cty.ListValEmpty(cty.String).Mark("a"), cty.ListValEmpty(cty.String)})}),
``,
},
{
[]cty.Value{
cty.TupleVal([]cty.Value{
cty.StringVal("the"),
cty.StringVal("brown"),
}),
cty.TupleVal([]cty.Value{
cty.StringVal("fox"),
cty.NumberIntVal(3),
}),
},
cty.ListVal([]cty.Value{
cty.TupleVal([]cty.Value{cty.StringVal("the"), cty.StringVal("fox")}),
cty.TupleVal([]cty.Value{cty.StringVal("the"), cty.StringVal("3")}),
cty.TupleVal([]cty.Value{cty.StringVal("brown"), cty.StringVal("fox")}),
cty.TupleVal([]cty.Value{cty.StringVal("brown"), cty.StringVal("3")}),
}),
``,
},
{
[]cty.Value{
cty.SetVal([]cty.Value{
cty.StringVal("the"),
cty.StringVal("brown"),
}),
cty.SetVal([]cty.Value{
cty.StringVal("quick"),
cty.StringVal("fox"),
}),
},
cty.SetVal([]cty.Value{
cty.TupleVal([]cty.Value{cty.StringVal("the"), cty.StringVal("quick")}),
cty.TupleVal([]cty.Value{cty.StringVal("the"), cty.StringVal("fox")}),
cty.TupleVal([]cty.Value{cty.StringVal("brown"), cty.StringVal("quick")}),
cty.TupleVal([]cty.Value{cty.StringVal("brown"), cty.StringVal("fox")}),
}),
``,
},
{ // The collection itself is not marked, just some elements
[]cty.Value{
cty.SetVal([]cty.Value{
cty.StringVal("the"),
cty.StringVal("brown").Mark("a"),
}),
cty.SetVal([]cty.Value{
cty.StringVal("quick"),
cty.StringVal("fox").Mark("b"),
}),
},
// Sets don't allow individually-marked elements, so the marks
// end up aggregating on the set itself anyway in this case.
cty.SetVal([]cty.Value{
cty.TupleVal([]cty.Value{cty.StringVal("the"), cty.StringVal("quick")}),
cty.TupleVal([]cty.Value{cty.StringVal("the"), cty.StringVal("fox")}),
cty.TupleVal([]cty.Value{cty.StringVal("brown"), cty.StringVal("quick")}),
cty.TupleVal([]cty.Value{cty.StringVal("brown"), cty.StringVal("fox")}),
}).Mark("a").Mark("b"),
``,
},
{ // The collections are marked
[]cty.Value{
cty.SetVal([]cty.Value{
cty.StringVal("the"),
cty.StringVal("brown"),
}).Mark("a"),
cty.SetVal([]cty.Value{
cty.StringVal("quick"),
cty.StringVal("fox"),
}).Mark("b"),
},
cty.SetVal([]cty.Value{
cty.TupleVal([]cty.Value{cty.StringVal("the"), cty.StringVal("quick")}),
cty.TupleVal([]cty.Value{cty.StringVal("the"), cty.StringVal("fox")}),
cty.TupleVal([]cty.Value{cty.StringVal("brown"), cty.StringVal("quick")}),
cty.TupleVal([]cty.Value{cty.StringVal("brown"), cty.StringVal("fox")}),
}).Mark("a").Mark("b"),
``,
},
{ // One collection is marked
[]cty.Value{
cty.SetVal([]cty.Value{
cty.StringVal("the"),
cty.StringVal("brown"),
}).Mark("a"),
cty.SetVal([]cty.Value{
cty.StringVal("quick"),
cty.StringVal("fox"),
}),
},
cty.SetVal([]cty.Value{
cty.TupleVal([]cty.Value{cty.StringVal("the"), cty.StringVal("quick")}),
cty.TupleVal([]cty.Value{cty.StringVal("the"), cty.StringVal("fox")}),
cty.TupleVal([]cty.Value{cty.StringVal("brown"), cty.StringVal("quick")}),
cty.TupleVal([]cty.Value{cty.StringVal("brown"), cty.StringVal("fox")}),
}).Mark("a"),
``,
},
{ // Inner and outer marks
[]cty.Value{
cty.SetVal([]cty.Value{
cty.StringVal("the"),
cty.StringVal("brown").Mark("a"),
}).Mark("b"),
cty.SetVal([]cty.Value{
cty.StringVal("quick"),
cty.StringVal("fox").Mark("c"),
}),
},
cty.SetVal([]cty.Value{
cty.TupleVal([]cty.Value{cty.StringVal("the"), cty.StringVal("quick")}),
cty.TupleVal([]cty.Value{cty.StringVal("the"), cty.StringVal("fox")}),
cty.TupleVal([]cty.Value{cty.StringVal("brown"), cty.StringVal("quick")}),
cty.TupleVal([]cty.Value{cty.StringVal("brown"), cty.StringVal("fox")}),
}).WithMarks(cty.NewValueMarks("b", "c", "a")),
``,
},

// SetproductFunc supports lists too, in which case it preserves the
// input order and returns a list as the result. In this case we can
// preserve the marks more precisely.
{ // The collection itself is not marked, just some elements
[]cty.Value{
cty.ListVal([]cty.Value{
cty.StringVal("the"),
cty.StringVal("brown").Mark("a"),
}),
cty.ListVal([]cty.Value{
cty.StringVal("quick"),
cty.StringVal("fox").Mark("b"),
}),
},
cty.ListVal([]cty.Value{
cty.TupleVal([]cty.Value{cty.StringVal("the"), cty.StringVal("quick")}),
cty.TupleVal([]cty.Value{cty.StringVal("the"), cty.StringVal("fox").Mark("b")}),
cty.TupleVal([]cty.Value{cty.StringVal("brown").Mark("a"), cty.StringVal("quick")}),
cty.TupleVal([]cty.Value{cty.StringVal("brown").Mark("a"), cty.StringVal("fox").Mark("b")}),
}),
``,
},
{ // The collections are marked
[]cty.Value{
cty.ListVal([]cty.Value{
cty.StringVal("the"),
cty.StringVal("brown"),
}).Mark("a"),
cty.ListVal([]cty.Value{
cty.StringVal("quick"),
cty.StringVal("fox"),
}).Mark("b"),
},
cty.ListVal([]cty.Value{
cty.TupleVal([]cty.Value{cty.StringVal("the"), cty.StringVal("quick")}),
cty.TupleVal([]cty.Value{cty.StringVal("the"), cty.StringVal("fox")}),
cty.TupleVal([]cty.Value{cty.StringVal("brown"), cty.StringVal("quick")}),
cty.TupleVal([]cty.Value{cty.StringVal("brown"), cty.StringVal("fox")}),
}).Mark("a").Mark("b"),
``,
},
{ // One collection is marked
[]cty.Value{
cty.ListVal([]cty.Value{
cty.StringVal("the"),
cty.StringVal("brown"),
}).Mark("a"),
cty.ListVal([]cty.Value{
cty.StringVal("quick"),
cty.StringVal("fox"),
}),
},
cty.ListVal([]cty.Value{
cty.TupleVal([]cty.Value{cty.StringVal("the"), cty.StringVal("quick")}),
cty.TupleVal([]cty.Value{cty.StringVal("the"), cty.StringVal("fox")}),
cty.TupleVal([]cty.Value{cty.StringVal("brown"), cty.StringVal("quick")}),
cty.TupleVal([]cty.Value{cty.StringVal("brown"), cty.StringVal("fox")}),
}).Mark("a"),
``,
},
{ // Inner and outer marks
[]cty.Value{
cty.ListVal([]cty.Value{
cty.StringVal("the"),
cty.StringVal("brown").Mark("a"),
}).Mark("b"),
cty.ListVal([]cty.Value{
cty.StringVal("quick"),
cty.StringVal("fox").Mark("c"),
}),
},
cty.ListVal([]cty.Value{
cty.TupleVal([]cty.Value{cty.StringVal("the"), cty.StringVal("quick")}),
cty.TupleVal([]cty.Value{cty.StringVal("the"), cty.StringVal("fox").Mark("c")}),
cty.TupleVal([]cty.Value{cty.StringVal("brown").Mark("a"), cty.StringVal("quick")}),
cty.TupleVal([]cty.Value{cty.StringVal("brown").Mark("a"), cty.StringVal("fox").Mark("c")}),
}).Mark("b"),
``,
},
}

for _, test := range tests {
t.Run(fmt.Sprintf("Setproduct(%#v)", test.Collections), func(t *testing.T) {
got, err := SetProduct(test.Collections...)
if test.Err != "" {
if err == nil {
t.Fatal("succeeded; want error")
}
if got, want := err.Error(), test.Err; got != want {
t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want)
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}

if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}

0 comments on commit af7c1a1

Please sign in to comment.