-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding plan checks for ExpectKnownValue
, ExpectKnownOutputValue
, and ExpectKnownOutputValueAtPath
#248
Adding plan checks for ExpectKnownValue
, ExpectKnownOutputValue
, and ExpectKnownOutputValueAtPath
#248
Changes from 9 commits
34cee4a
6915f0e
f503594
4f8eed9
a1f35f6
853a3e4
f88c44b
0ccf758
f161203
e13bdaa
916674e
450339d
31cbf09
73a5300
3f14259
5e61524
41eb4de
94bdfa7
f09f7e9
7c5c177
1638808
213b81a
fa8f125
91417e1
463c0e3
a661490
6b626f4
ad071ae
efb0b98
9e5cde1
d14429e
f1b9656
ed9d236
9bdf445
0849854
4f283cf
edcddb6
5f4c952
aab63ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
kind: FEATURES | ||
body: 'plancheck: Added `ExpectKnownValue` plan check, which asserts that a given | ||
resource attribute has a defined type, and value' | ||
time: 2023-12-18T11:45:39.181954Z | ||
custom: | ||
Issue: "248" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
kind: FEATURES | ||
body: 'plancheck: Added `ExpectKnownOutputValue` plan check, which asserts that a | ||
given output value has a defined type, and value' | ||
time: 2023-12-18T11:45:53.272412Z | ||
custom: | ||
Issue: "248" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
kind: FEATURES | ||
body: 'plancheck: Added `ExpectKnownOutputValueAtPath` plan check, which asserts that | ||
a given output value at a specified path has a defined type, and value' | ||
time: 2023-12-18T11:46:11.58053Z | ||
custom: | ||
Issue: "248" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
kind: FEATURES | ||
body: 'knownvalue: Introduced new `knownvalue` package which contains types for working | ||
with plan checks and state checks' | ||
time: 2023-12-18T11:47:39.059813Z | ||
custom: | ||
Issue: "248" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: MPL-2.0 | ||
|
||
package knownvalue | ||
|
||
import "strconv" | ||
|
||
var _ KnownValue = BoolValue{} | ||
|
||
// BoolValue is a KnownValue for asserting equality between the value | ||
// supplied to NewBoolValue and the value passed to the Equal method. | ||
type BoolValue struct { | ||
value bool | ||
} | ||
|
||
// Equal determines whether the passed value is of type bool, and | ||
// contains a matching bool value. | ||
func (v BoolValue) Equal(other any) bool { | ||
otherVal, ok := other.(bool) | ||
|
||
if !ok { | ||
return false | ||
} | ||
|
||
if otherVal != v.value { | ||
return false | ||
} | ||
|
||
return true | ||
} | ||
|
||
// String returns the string representation of the bool value. | ||
func (v BoolValue) String() string { | ||
return strconv.FormatBool(v.value) | ||
} | ||
|
||
// NewBoolValue returns a KnownValue for asserting equality between the | ||
// supplied bool and the value passed to the Equal method. | ||
func NewBoolValue(value bool) BoolValue { | ||
bendbennett marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return BoolValue{ | ||
value: value, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: MPL-2.0 | ||
|
||
package knownvalue_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
|
||
"github.com/hashicorp/terraform-plugin-testing/knownvalue" | ||
) | ||
|
||
func TestBoolValue_Equal(t *testing.T) { | ||
t.Parallel() | ||
|
||
testCases := map[string]struct { | ||
other any | ||
expected bool | ||
}{ | ||
"nil": {}, | ||
"wrong-type": { | ||
other: 1.23, | ||
}, | ||
"not-equal": { | ||
other: false, | ||
}, | ||
"equal": { | ||
other: true, | ||
expected: true, | ||
}, | ||
} | ||
|
||
for name, testCase := range testCases { | ||
name, testCase := name, testCase | ||
|
||
t.Run(name, func(t *testing.T) { | ||
t.Parallel() | ||
|
||
got := knownvalue.NewBoolValue(true).Equal(testCase.other) | ||
bendbennett marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if diff := cmp.Diff(got, testCase.expected); diff != "" { | ||
t.Errorf("unexpected difference: %s", diff) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestBoolValue_String(t *testing.T) { | ||
t.Parallel() | ||
|
||
got := knownvalue.NewBoolValue(true).String() | ||
|
||
if diff := cmp.Diff(got, "true"); diff != "" { | ||
t.Errorf("unexpected difference: %s", diff) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: MPL-2.0 | ||
|
||
// Package knownvalue contains the known value interface, and types implementing the known value interface. | ||
package knownvalue |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: MPL-2.0 | ||
|
||
package knownvalue | ||
|
||
import ( | ||
"strconv" | ||
) | ||
|
||
var _ KnownValue = Float64Value{} | ||
|
||
// Float64Value is a KnownValue for asserting equality between the value | ||
// supplied to NewFloat64Value and the value passed to the Equal method. | ||
type Float64Value struct { | ||
value float64 | ||
} | ||
|
||
// Equal determines whether the passed value is of type float64, and | ||
// contains a matching float64 value. | ||
func (v Float64Value) Equal(other any) bool { | ||
otherVal, ok := other.(float64) | ||
|
||
if !ok { | ||
return false | ||
} | ||
|
||
if otherVal != v.value { | ||
return false | ||
} | ||
|
||
return true | ||
} | ||
|
||
// String returns the string representation of the float64 value. | ||
func (v Float64Value) String() string { | ||
return strconv.FormatFloat(v.value, 'f', -1, 64) | ||
} | ||
|
||
// NewFloat64Value returns a KnownValue for asserting equality between the | ||
// supplied float64 and the value passed to the Equal method. | ||
func NewFloat64Value(value float64) Float64Value { | ||
return Float64Value{ | ||
value: value, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: MPL-2.0 | ||
|
||
package knownvalue_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
|
||
"github.com/hashicorp/terraform-plugin-testing/knownvalue" | ||
) | ||
|
||
func TestFloat64Value_Equal(t *testing.T) { | ||
t.Parallel() | ||
|
||
testCases := map[string]struct { | ||
other any | ||
expected bool | ||
}{ | ||
"nil": {}, | ||
"wrong-type": { | ||
other: 123, | ||
}, | ||
"not-equal": { | ||
other: false, | ||
}, | ||
"equal": { | ||
other: 1.23, | ||
expected: true, | ||
}, | ||
} | ||
|
||
for name, testCase := range testCases { | ||
name, testCase := name, testCase | ||
|
||
t.Run(name, func(t *testing.T) { | ||
t.Parallel() | ||
|
||
got := knownvalue.NewFloat64Value(1.23).Equal(testCase.other) | ||
|
||
if diff := cmp.Diff(got, testCase.expected); diff != "" { | ||
t.Errorf("unexpected difference: %s", diff) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestFloat64Value_String(t *testing.T) { | ||
t.Parallel() | ||
|
||
got := knownvalue.NewFloat64Value(1.234567890123e+09).String() | ||
|
||
if diff := cmp.Diff(got, "1234567890.123"); diff != "" { | ||
t.Errorf("unexpected difference: %s", diff) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: MPL-2.0 | ||
|
||
package knownvalue | ||
|
||
import ( | ||
"strconv" | ||
) | ||
|
||
var _ KnownValue = Int64Value{} | ||
|
||
// Int64Value is a KnownValue for asserting equality between the value | ||
// supplied to NewInt64Value and the value passed to the Equal method. | ||
type Int64Value struct { | ||
value int64 | ||
} | ||
|
||
// Equal determines whether the passed value is of type int64, and | ||
// contains a matching int64 value. | ||
func (v Int64Value) Equal(other any) bool { | ||
otherVal, ok := other.(int64) | ||
|
||
if !ok { | ||
return false | ||
} | ||
|
||
if otherVal != v.value { | ||
return false | ||
} | ||
|
||
return true | ||
} | ||
|
||
// String returns the string representation of the int64 value. | ||
func (v Int64Value) String() string { | ||
return strconv.FormatInt(v.value, 10) | ||
} | ||
|
||
// NewInt64Value returns a KnownValue for asserting equality between the | ||
// supplied int64 and the value passed to the Equal method. | ||
func NewInt64Value(value int64) Int64Value { | ||
return Int64Value{ | ||
value: value, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: MPL-2.0 | ||
|
||
package knownvalue_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
|
||
"github.com/hashicorp/terraform-plugin-testing/knownvalue" | ||
) | ||
|
||
func TestInt64Value_Equal(t *testing.T) { | ||
t.Parallel() | ||
|
||
testCases := map[string]struct { | ||
other any | ||
expected bool | ||
}{ | ||
"nil": {}, | ||
"wrong-type": { | ||
other: 1.23, | ||
}, | ||
"not-equal": { | ||
other: false, | ||
}, | ||
"equal": { | ||
other: int64(123), | ||
expected: true, | ||
}, | ||
} | ||
|
||
for name, testCase := range testCases { | ||
name, testCase := name, testCase | ||
|
||
t.Run(name, func(t *testing.T) { | ||
t.Parallel() | ||
|
||
got := knownvalue.NewInt64Value(123).Equal(testCase.other) | ||
|
||
if diff := cmp.Diff(got, testCase.expected); diff != "" { | ||
t.Errorf("unexpected difference: %s", diff) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestInt64Value_String(t *testing.T) { | ||
t.Parallel() | ||
|
||
got := knownvalue.NewInt64Value(1234567890123).String() | ||
|
||
if diff := cmp.Diff(got, "1234567890123"); diff != "" { | ||
t.Errorf("unexpected difference: %s", diff) | ||
} | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. super nit 🦸🏻 - Should this file be named |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: MPL-2.0 | ||
|
||
package knownvalue | ||
|
||
// KnownValue defines an interface that is implemented to determine equality on the basis of type and value. | ||
type KnownValue interface { | ||
bendbennett marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// Equal should perform equality testing. | ||
Equal(other any) bool | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Open question: Do we want to call this "check", "match", or another name? While exact equality is one use case with these, there are other use cases as well, e.g.
I guess I'm just trying to ensure folks aren't confused or turned away for the not-exactly-equal use cases. Seeing everything written out now, I almost wonder if this interface should be something like: type Check interface {
CheckValue(value any) error
String() string
} There's at least two benefits I see here beyond the potentially clearer naming:
I hesitate to say going down the fully-type-named interfaces, e.g. type BoolCheck interface {
CheckBool(value bool) error
String() string
} Unless we'd consider passing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like the idea of returning an error and the additional flexibility that this yields. In terms of the fully-type-named interfaces, I'm wondering about the internals. For instance, implementation of func (v BoolValue) CheckBool(other bool) error {
if other != v.value {
return fmt.Errorf("%t does not equal %t", other, v.value)
}
return nil
} With the call to switch reflect.TypeOf(result).Kind() {
case reflect.Bool:
v, ok := e.knownValue.(knownvalue.BoolValue)
if !ok {
resp.Error = fmt.Errorf("wrong type: attribute value is bool, known value type is %T", e.knownValue)
return
}
err := v.CheckBool(result.(bool))
if err != nil {
resp.Error = fmt.Errorf("attribute value: %v does not equal expected value: %s", result, v)
return
} However, the internals for all of the func (v ObjectValue) CheckObject(other map[string]any) error {
if len(other) != len(v.value) {
return fmt.Errorf("%v does not equal %v", other, v.value)
}
for k, v := range v.value {
otherItem, ok := other[k]
if !ok {
return fmt.Errorf("%s is missing from %v", k, other)
}
switch reflect.TypeOf(otherItem).Kind() {
case reflect.Bool:
boolCheck, ok := v.(BoolValue)
if !ok {
return fmt.Errorf("wrong type: %T, known value type is %T", otherItem, v)
}
if err := boolCheck.CheckBool(otherItem.(bool)); err != nil {
return fmt.Errorf("%v does not equal %v", otherItem, v)
}
// Reflection logic for reflect.Map, reflect.Slice, reflect.String
default:
return fmt.Errorf("unrecognised type: %T, known value type is %T", otherItem, v)
}
}
return nil
} I'll go ahead and implement the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In that situation, yes, I would expect the collection/object checks to need to figure out what their element or per-object-attribute check type is. Another option would be to not support that functionality and leave it up to developer implementations to figure out what to do with making more generic collection/object checks that support elements/object attributes, but I think there's a lot of value in being able to offer that out of the box. Not sure if it will make the logic easier/harder, but another way of doing this is performing a type switch on the // note also including the object attribute name (k from example) and not assuming
// it was an equality check error in error messaging (the checks can say so
// themselves, amongst other possible errors :) )
switch check := v.(type) {
case BoolValue:
otherValue, ok := otherItem.(bool)
if !ok {
return fmt.Errorf("%s object attribute: expected bool value for BoolValue check, got: %T", k, otherItem)
}
if err := check.CheckBool(otherValue); err != nil {
return fmt.Errorf("%s object attribute: %s", k, err)
}
// ...
default:
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess a potential wrinkle here is, what if there is a dynamically typed value is being checked? Hmm. I would normally presume the value should be statically typed based on the given Terraform configuration. If not, we would be fine as long as there is some sort of escape hatch for a "I don't know the value type and I'll do all the work to figure it out" type of check, e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've refactored to use the type Check interface {
CheckValue(value any) error
String() string
} Before proceeding down the route of using fully-type-named interface, thought it might be worth considering how the Currently, taking var _ PlanCheck = expectKnownValue{}
type expectKnownValue struct {
resourceAddress string
attributePath tfjsonpath.Path
knownValue knownvalue.Check
}
func (e expectKnownValue) CheckPlan(ctx context.Context, req CheckPlanRequest, resp *CheckPlanResponse) {
/* ... */
}
func ExpectKnownValue(resourceAddress string, attributePath tfjsonpath.Path, knownValue knownvalue.Check) PlanCheck {
return expectKnownValue{
resourceAddress: resourceAddress,
attributePath: attributePath,
knownValue: knownValue,
}
} As an example, var _ Check = BoolValue{}
type BoolValue struct {
value bool
}
func (v BoolValue) CheckValue(other any) error {
/* ... */
}
/* ... */
func BoolValueExact(value bool) BoolValue {
return BoolValue{
value: value,
}
} If we refactor type BoolCheck interface {
CheckBool(value bool) error
String() string
}
var _ BoolCheck = BoolValue{}
type BoolValue struct {
value bool
}
func (v BoolValue) CheckBool(value bool) error {
/* ... */
}
/* ... */
func BoolValueExact(value bool) BoolValue {
return BoolValue{
value: value,
}
} Using fully-type-named interfaces will require that we reconsider how the constructor and struct field holding the checks will be handled for plancheck.ExpectKnownValue(
"test_resource.one",
tfjsonpath.New("bool_attribute"),
knownvalue.BoolValueExact(true),
), |
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two questions:
boolValue
here)? It'll clean up the public Go package documentation nicely and I'm not sure if there is anything that should need to reference/extend the implementation directly since its value field is unexported.boolValueExact
for some additional clarity while reading the codeWe can always export additional things in the future if we need! 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for reducing the amount of exported types 👍🏻
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good to me.
The known value check types have all be modified so that they are camel-cased versions of the exported functions, for example:
boolValueExact
=>BoolValueExact
listElementsExact =>
ListElementsExact`listValueExact
=>ListValueExact
listValuePartial
=>ListValuePartial
(note that the constructor for the know value partial types has changed from<List|Map|Object|Set>ValuePartialMatch
to<List|Map|Object|Set>ValuePartial
)