vendor: go get github.com/zclconf/go-cty@master

cty now guarantees that sets of primitive values will iterate in a
reasonable order. Previously it was the caller's responsibility to deal
with that, but we invariably neglected to do so, causing inconsistent
ordering. Since cty prioritizes consistent behavior over performance, it
now imposes its own sort on set elements as part of iterating over them so
that calling applications don't have to worry so much about it.

This change also causes cty to consistently push unknown and null values
in sets to the end of iteration, where before that was undefined. This
means that our diff output will now consistently list additions before
removals when showing sets, rather than the ordering being undefined as
before.

The ordering of known, non-null, non-primitive values is still not
contractually fixed but remains consistent for a particular version of
cty.
This commit is contained in:
Martin Atkins 2019-04-30 15:29:47 -07:00
parent 4b83ddb025
commit 6adcc7ab73
9 changed files with 125 additions and 59 deletions

View File

@ -2564,13 +2564,13 @@ func TestResourceChange_nestedSet(t *testing.T) {
~ ami = "ami-BEFORE" -> "ami-AFTER" ~ ami = "ami-BEFORE" -> "ami-AFTER"
id = "i-02ae66f368e8518a9" id = "i-02ae66f368e8518a9"
- root_block_device {
- volume_type = "gp2" -> null
}
+ root_block_device { + root_block_device {
+ new_field = "new_value" + new_field = "new_value"
+ volume_type = "gp2" + volume_type = "gp2"
} }
- root_block_device {
- volume_type = "gp2" -> null
}
} }
`, `,
}, },
@ -2624,12 +2624,12 @@ func TestResourceChange_nestedSet(t *testing.T) {
~ ami = "ami-BEFORE" -> "ami-AFTER" ~ ami = "ami-BEFORE" -> "ami-AFTER"
id = "i-02ae66f368e8518a9" id = "i-02ae66f368e8518a9"
- root_block_device { # forces replacement
- volume_type = "gp2" -> null
}
+ root_block_device { # forces replacement + root_block_device { # forces replacement
+ volume_type = "different" + volume_type = "different"
} }
- root_block_device { # forces replacement
- volume_type = "gp2" -> null
}
} }
`, `,
}, },

2
go.mod
View File

@ -107,7 +107,7 @@ require (
github.com/xanzy/ssh-agent v0.2.1 github.com/xanzy/ssh-agent v0.2.1
github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 // indirect github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 // indirect
github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557 github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557
github.com/zclconf/go-cty v0.0.0-20190426224007-b18a157db9e2 github.com/zclconf/go-cty v0.0.0-20190430221426-d36a6f0dbffd
go.uber.org/atomic v1.3.2 // indirect go.uber.org/atomic v1.3.2 // indirect
go.uber.org/multierr v1.1.0 // indirect go.uber.org/multierr v1.1.0 // indirect
go.uber.org/zap v1.9.1 // indirect go.uber.org/zap v1.9.1 // indirect

4
go.sum
View File

@ -403,8 +403,8 @@ github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557/go.mod h1:ce1O1j6Ut
github.com/zclconf/go-cty v0.0.0-20181129180422-88fbe721e0f8/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= github.com/zclconf/go-cty v0.0.0-20181129180422-88fbe721e0f8/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
github.com/zclconf/go-cty v0.0.0-20190124225737-a385d646c1e9 h1:hHCAGde+QfwbqXSAqOmBd4NlOrJ6nmjWp+Nu408ezD4= github.com/zclconf/go-cty v0.0.0-20190124225737-a385d646c1e9 h1:hHCAGde+QfwbqXSAqOmBd4NlOrJ6nmjWp+Nu408ezD4=
github.com/zclconf/go-cty v0.0.0-20190124225737-a385d646c1e9/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= github.com/zclconf/go-cty v0.0.0-20190124225737-a385d646c1e9/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
github.com/zclconf/go-cty v0.0.0-20190426224007-b18a157db9e2 h1:Ai1LhlYNEqE39zGU07qHDNJ41iZVPZfZr1dSCoXrp1w= github.com/zclconf/go-cty v0.0.0-20190430221426-d36a6f0dbffd h1:NZOOU7h+pDtcKo6xlqm8PwnarS8nJ+6+I83jT8ZfLPI=
github.com/zclconf/go-cty v0.0.0-20190426224007-b18a157db9e2/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= github.com/zclconf/go-cty v0.0.0-20190430221426-d36a6f0dbffd/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
go.opencensus.io v0.18.0 h1:Mk5rgZcggtbvtAun5aJzAtjKKN/t0R3jJPlWILlv938= go.opencensus.io v0.18.0 h1:Mk5rgZcggtbvtAun5aJzAtjKKN/t0R3jJPlWILlv938=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=

View File

@ -2005,12 +2005,12 @@ func TestReverse(t *testing.T) {
}, },
{ {
cty.SetVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")}), cty.SetVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")}),
cty.ListVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")}), cty.ListVal([]cty.Value{cty.StringVal("b"), cty.StringVal("a")}), // set-of-string iterates in lexicographical order
"", "",
}, },
{ {
cty.SetVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b"), cty.StringVal("c")}), cty.SetVal([]cty.Value{cty.StringVal("b"), cty.StringVal("a"), cty.StringVal("c")}),
cty.ListVal([]cty.Value{cty.StringVal("a"), cty.StringVal("c"), cty.StringVal("b")}), cty.ListVal([]cty.Value{cty.StringVal("c"), cty.StringVal("b"), cty.StringVal("a")}), // set-of-string iterates in lexicographical order
"", "",
}, },
{ {

View File

@ -1,36 +1,15 @@
package set package set
type Iterator struct { type Iterator struct {
bucketIds []int vals []interface{}
vals map[int][]interface{} idx int
bucketIdx int
valIdx int
} }
func (it *Iterator) Value() interface{} { func (it *Iterator) Value() interface{} {
return it.currentBucket()[it.valIdx] return it.vals[it.idx]
} }
func (it *Iterator) Next() bool { func (it *Iterator) Next() bool {
if it.bucketIdx == -1 { it.idx++
// init return it.idx < len(it.vals)
if len(it.bucketIds) == 0 {
return false
}
it.valIdx = 0
it.bucketIdx = 0
return true
}
it.valIdx++
if it.valIdx >= len(it.currentBucket()) {
it.valIdx = 0
it.bucketIdx++
}
return it.bucketIdx < len(it.bucketIds)
}
func (it *Iterator) currentBucket() []interface{} {
return it.vals[it.bucketIds[it.bucketIdx]]
} }

View File

@ -75,8 +75,12 @@ func (s Set) Copy() Set {
return ret return ret
} }
// Iterator returns an iterator over values in the set, in an undefined order // Iterator returns an iterator over values in the set. If the set's rules
// that callers should not depend on. // implement OrderedRules then the result is ordered per those rules. If
// no order is provided, or if it is not a total order, then the iteration
// order is undefined but consistent for a particular version of cty. Do not
// rely on specific ordering between cty releases unless the rules order is a
// total order.
// //
// The pattern for using the returned iterator is: // The pattern for using the returned iterator is:
// //
@ -89,18 +93,11 @@ func (s Set) Copy() Set {
// Once an iterator has been created for a set, the set *must not* be mutated // Once an iterator has been created for a set, the set *must not* be mutated
// until the iterator is no longer in use. // until the iterator is no longer in use.
func (s Set) Iterator() *Iterator { func (s Set) Iterator() *Iterator {
// Sort the bucketIds to ensure that we always traverse in a vals := s.Values()
// consistent order.
bucketIds := make([]int, 0, len(s.vals))
for id := range s.vals {
bucketIds = append(bucketIds, id)
}
sort.Ints(bucketIds)
return &Iterator{ return &Iterator{
bucketIds: bucketIds, vals: vals,
vals: s.vals, idx: -1,
bucketIdx: -1,
} }
} }
@ -113,16 +110,30 @@ func (s Set) EachValue(cb func(interface{})) {
} }
} }
// Values returns a slice of all of the values in the set in no particular // Values returns a slice of all the values in the set. If the set rules have
// order. This is just a wrapper around EachValue that accumulates the results // an order then the result is in that order. If no order is provided or if
// in a slice for caller convenience. // it is not a total order then the result order is undefined, but consistent
// // for a particular set value within a specific release of cty.
// The returned slice will be nil if there are no values in the set.
func (s Set) Values() []interface{} { func (s Set) Values() []interface{} {
var ret []interface{} var ret []interface{}
s.EachValue(func(v interface{}) { // Sort the bucketIds to ensure that we always traverse in a
ret = append(ret, v) // consistent order.
}) bucketIDs := make([]int, 0, len(s.vals))
for id := range s.vals {
bucketIDs = append(bucketIDs, id)
}
sort.Ints(bucketIDs)
for _, bucketID := range bucketIDs {
ret = append(ret, s.vals[bucketID]...)
}
if orderRules, ok := s.rules.(OrderedRules); ok {
sort.SliceStable(ret, func(i, j int) bool {
return orderRules.Less(ret[i], ret[j])
})
}
return ret return ret
} }

View File

@ -23,3 +23,21 @@ type Rules interface {
// be equivalent. // be equivalent.
Equivalent(interface{}, interface{}) bool Equivalent(interface{}, interface{}) bool
} }
// OrderedRules is an extension of Rules that can apply a partial order to
// element values. When a set's Rules implements OrderedRules an iterator
// over the set will return items in the order described by the rules.
//
// If the given order is not a total order (that is, some pairs of non-equivalent
// elements do not have a defined order) then the resulting iteration order
// is undefined but consistent for a particular version of cty. The exact
// order in that case is not part of the contract and is subject to change
// between versions.
type OrderedRules interface {
Rules
// Less returns true if and only if the first argument should sort before
// the second argument. If the second argument should sort before the first
// or if there is no defined order for the values, return false.
Less(interface{}, interface{}) bool
}

View File

@ -6,6 +6,8 @@ import (
"hash/crc32" "hash/crc32"
"math/big" "math/big"
"sort" "sort"
"github.com/zclconf/go-cty/cty/set"
) )
// setRules provides a Rules implementation for the ./set package that // setRules provides a Rules implementation for the ./set package that
@ -19,6 +21,8 @@ type setRules struct {
Type Type Type Type
} }
var _ set.OrderedRules = setRules{}
// Hash returns a hash value for the receiver that can be used for equality // Hash returns a hash value for the receiver that can be used for equality
// checks where some inaccuracy is tolerable. // checks where some inaccuracy is tolerable.
// //
@ -58,6 +62,60 @@ func (r setRules) Equivalent(v1 interface{}, v2 interface{}) bool {
return eqv.v == true return eqv.v == true
} }
// Less is an implementation of set.OrderedRules so that we can iterate over
// set elements in a consistent order, where such an order is possible.
func (r setRules) Less(v1, v2 interface{}) bool {
v1v := Value{
ty: r.Type,
v: v1,
}
v2v := Value{
ty: r.Type,
v: v2,
}
if v1v.RawEquals(v2v) { // Easy case: if they are equal then v1 can't be less
return false
}
// Null values always sort after non-null values
if v2v.IsNull() && !v1v.IsNull() {
return true
} else if v1v.IsNull() {
return false
}
// Unknown values always sort after known values
if v1v.IsKnown() && !v2v.IsKnown() {
return true
} else if !v1v.IsKnown() {
return false
}
switch r.Type {
case String:
// String values sort lexicographically
return v1v.AsString() < v2v.AsString()
case Bool:
// Weird to have a set of bools, but if we do then false sorts before true.
if v2v.True() || !v1v.True() {
return true
}
return false
case Number:
v1f := v1v.AsBigFloat()
v2f := v2v.AsBigFloat()
return v1f.Cmp(v2f) < 0
default:
// No other types have a well-defined ordering, so we just produce a
// default consistent-but-undefined ordering then. This situation is
// not considered a compatibility constraint; callers should rely only
// on the ordering rules for primitive values.
v1h := makeSetHashBytes(v1v)
v2h := makeSetHashBytes(v2v)
return bytes.Compare(v1h, v2h) < 0
}
}
func makeSetHashBytes(val Value) []byte { func makeSetHashBytes(val Value) []byte {
var buf bytes.Buffer var buf bytes.Buffer
appendSetHashBytes(val, &buf) appendSetHashBytes(val, &buf)

2
vendor/modules.txt vendored
View File

@ -434,7 +434,7 @@ github.com/vmihailenco/msgpack/codes
github.com/xanzy/ssh-agent github.com/xanzy/ssh-agent
# github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557 # github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557
github.com/xlab/treeprint github.com/xlab/treeprint
# github.com/zclconf/go-cty v0.0.0-20190426224007-b18a157db9e2 # github.com/zclconf/go-cty v0.0.0-20190430221426-d36a6f0dbffd
github.com/zclconf/go-cty/cty github.com/zclconf/go-cty/cty
github.com/zclconf/go-cty/cty/gocty github.com/zclconf/go-cty/cty/gocty
github.com/zclconf/go-cty/cty/convert github.com/zclconf/go-cty/cty/convert