Skip to content

Commit

Permalink
Adding plan checks for ExpectKnownValue, ExpectKnownOutputValue, …
Browse files Browse the repository at this point in the history
…and `ExpectKnownOutputValueAtPath` (#248)

* Add KnownValue interface and types (#243)

* Add ExpectKnownValue plan check (#243)

* Handling different permutations for equality checking of interface type and value, and known value type and value (#243)

* Adding tests for missing resource, and attribute value null (#243)

* Adding plan checks for known output value and known output value at path (#243)

* Adding documentation (#243)

* Adding changelog entries (#243)

* Adding TerraformVersionChecks (#243)

* Modifying to handle numerical values returned as json.Number for tfjson.Plan (#243)

* Renaming known value constructors (#243)

* Refactoring to Check interface (#243)

* Linting (#243)

* Modifying known value check error messages and tests (#243)

* Updating tests for ExpectKnownValue, ExpectKnownOutputValue and ExpectKnownOutputValueAtPaath (#243)

* Adding changelog entry to note the switch to using json.Number for numerical value representation in the plan (#243)

* Remove reference to state checks (#243)

* Moving concepts under title and removing reference to Framework types (#243)

* Updating Go doc comments to clarify usage of partial equality and remove references to KnownValue interface (#243)

* Modifying known-values.mdx page description (#243)

* Restructuring and updating references to knownvalue.Check (#243)

* Adding individual docs pages for each type of known value check (#243)

* Removing references to num elements (#243)

* Removing references to state (#243)

* Adding docs page for custom known value checks (#243)

* Fixing error message (#243)

* Refactoring to accomodate custom known value checks in ExpectKnownValue, ExpectKnownOutputValue and ExpectKnownOutputValueAtPath (#243)

* Apply suggestions from code review

Co-authored-by: Brian Flad <bflad417@gmail.com>

* Unexporting types that implement known value check (#266)

* Document usage of 512-bit precision in the number known value check (#266)

* Adding attribute or output path to error message (#266)

* Replacing alias in example code (#266)

* Rename file (#266)

* Renamed list, map, and set element length checks to <List|Map|Set>SizeExact (#243)

* Removing ObjectAttributesExact (#243)

* Renaming known value check types (#243)

---------

Co-authored-by: Brian Flad <bflad417@gmail.com>
  • Loading branch information
bendbennett and bflad committed Jan 15, 2024
1 parent 725d48a commit 198c751
Show file tree
Hide file tree
Showing 65 changed files with 8,982 additions and 38 deletions.
6 changes: 6 additions & 0 deletions .changes/unreleased/FEATURES-20231218-114539.yaml
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"
6 changes: 6 additions & 0 deletions .changes/unreleased/FEATURES-20231218-114553.yaml
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"
6 changes: 6 additions & 0 deletions .changes/unreleased/FEATURES-20231218-114611.yaml
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"
6 changes: 6 additions & 0 deletions .changes/unreleased/FEATURES-20231218-114739.yaml
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"
6 changes: 6 additions & 0 deletions .changes/unreleased/NOTES-20240104-083841.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: NOTES
body: Numerical values in the plan are now represented as json.Number, not float64.
Custom plan checks relying upon float64 representation may need altering
time: 2024-01-04T08:38:41.645745Z
custom:
Issue: "248"
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ require (
github.com/hashicorp/hc-install v0.6.2
github.com/hashicorp/hcl/v2 v2.19.1
github.com/hashicorp/logutils v1.0.0
github.com/hashicorp/terraform-exec v0.19.0
github.com/hashicorp/terraform-exec v0.20.0
github.com/hashicorp/terraform-json v0.20.0
github.com/hashicorp/terraform-plugin-go v0.20.0
github.com/hashicorp/terraform-plugin-log v0.9.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5R
github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/terraform-exec v0.19.0 h1:FpqZ6n50Tk95mItTSS9BjeOVUb4eg81SpgVtZNNtFSM=
github.com/hashicorp/terraform-exec v0.19.0/go.mod h1:tbxUpe3JKruE9Cuf65mycSIT8KiNPZ0FkuTE3H4urQg=
github.com/hashicorp/terraform-exec v0.20.0 h1:DIZnPsqzPGuUnq6cH8jWcPunBfY+C+M8JyYF3vpnuEo=
github.com/hashicorp/terraform-exec v0.20.0/go.mod h1:ckKGkJWbsNqFKV1itgMnE0hY9IYf1HoiekpuN0eWoDw=
github.com/hashicorp/terraform-json v0.20.0 h1:cJcvn4gIOTi0SD7pIy+xiofV1zFA3hza+6K+fo52IX8=
github.com/hashicorp/terraform-json v0.20.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk=
github.com/hashicorp/terraform-plugin-go v0.20.0 h1:oqvoUlL+2EUbKNsJbIt3zqqZ7wi6lzn4ufkn/UA51xQ=
Expand Down
2 changes: 1 addition & 1 deletion internal/plugintest/working_dir.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ func (wd *WorkingDir) SavedPlan(ctx context.Context) (*tfjson.Plan, error) {

logging.HelperResourceTrace(ctx, "Calling Terraform CLI show command for JSON plan")

plan, err := wd.tf.ShowPlanFile(context.Background(), wd.planFilename(), tfexec.Reattach(wd.reattachInfo))
plan, err := wd.tf.ShowPlanFile(context.Background(), wd.planFilename(), tfexec.Reattach(wd.reattachInfo), tfexec.JSONNumber(true))

logging.HelperResourceTrace(ctx, "Calling Terraform CLI show command for JSON plan")

Expand Down
44 changes: 44 additions & 0 deletions knownvalue/bool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package knownvalue

import (
"fmt"
"strconv"
)

var _ Check = boolExact{}

type boolExact struct {
value bool
}

// CheckValue determines whether the passed value is of type bool, and
// contains a matching bool value.
func (v boolExact) CheckValue(other any) error {
otherVal, ok := other.(bool)

if !ok {
return fmt.Errorf("expected bool value for BoolExact check, got: %T", other)
}

if otherVal != v.value {
return fmt.Errorf("expected value %t for BoolExact check, got: %t", v.value, otherVal)
}

return nil
}

// String returns the string representation of the bool value.
func (v boolExact) String() string {
return strconv.FormatBool(v.value)
}

// BoolExact returns a Check for asserting equality between the
// supplied bool and the value passed to the CheckValue method.
func BoolExact(value bool) boolExact {
return boolExact{
value: value,
}
}
83 changes: 83 additions & 0 deletions knownvalue/bool_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package knownvalue_test

import (
"fmt"
"testing"

"github.com/google/go-cmp/cmp"

"github.com/hashicorp/terraform-plugin-testing/knownvalue"
)

func TestBoolValue_CheckValue(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
self knownvalue.Check
other any
expectedError error
}{
"zero-nil": {
self: knownvalue.BoolExact(false),
expectedError: fmt.Errorf("expected bool value for BoolExact check, got: <nil>"),
},
"zero-other": {
self: knownvalue.BoolExact(false),
other: false, // checking against the underlying value field zero-value
},
"nil": {
self: knownvalue.BoolExact(false),
expectedError: fmt.Errorf("expected bool value for BoolExact check, got: <nil>"),
},
"wrong-type": {
self: knownvalue.BoolExact(true),
other: 1.23,
expectedError: fmt.Errorf("expected bool value for BoolExact check, got: float64"),
},
"not-equal": {
self: knownvalue.BoolExact(true),
other: false,
expectedError: fmt.Errorf("expected value true for BoolExact check, got: false"),
},
"equal": {
self: knownvalue.BoolExact(true),
other: true,
},
}

for name, testCase := range testCases {
name, testCase := name, testCase

t.Run(name, func(t *testing.T) {
t.Parallel()

got := testCase.self.CheckValue(testCase.other)

if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
})
}
}

func TestBoolValue_String(t *testing.T) {
t.Parallel()

got := knownvalue.BoolExact(true).String()

if diff := cmp.Diff(got, "true"); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
}

// equateErrorMessage reports errors to be equal if both are nil
// or both have the same message.
var equateErrorMessage = cmp.Comparer(func(x, y error) bool {
if x == nil || y == nil {
return x == nil && y == nil
}
return x.Error() == y.Error()
})
14 changes: 14 additions & 0 deletions knownvalue/check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package knownvalue

// Check defines an interface that is implemented to determine whether type and value match. Individual
// implementations determine how the match is performed (e.g., exact match, partial match).
type Check interface {
// CheckValue should assert the given known value against any expectations. Use the error
// return to signal unexpected values or implementation errors.
CheckValue(value any) error
// String should return a string representation of the type and value.
String() string
}
5 changes: 5 additions & 0 deletions knownvalue/doc.go
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
51 changes: 51 additions & 0 deletions knownvalue/float64.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package knownvalue

import (
"encoding/json"
"fmt"
"strconv"
)

var _ Check = float64Exact{}

type float64Exact struct {
value float64
}

// CheckValue determines whether the passed value is of type float64, and
// contains a matching float64 value.
func (v float64Exact) CheckValue(other any) error {
jsonNum, ok := other.(json.Number)

if !ok {
return fmt.Errorf("expected json.Number value for Float64Exact check, got: %T", other)
}

otherVal, err := jsonNum.Float64()

if err != nil {
return fmt.Errorf("expected json.Number to be parseable as float64 value for Float64Exact check: %s", err)
}

if otherVal != v.value {
return fmt.Errorf("expected value %s for Float64Exact check, got: %s", v.String(), strconv.FormatFloat(otherVal, 'f', -1, 64))
}

return nil
}

// String returns the string representation of the float64 value.
func (v float64Exact) String() string {
return strconv.FormatFloat(v.value, 'f', -1, 64)
}

// Float64Exact returns a Check for asserting equality between the
// supplied float64 and the value passed to the CheckValue method.
func Float64Exact(value float64) float64Exact {
return float64Exact{
value: value,
}
}
75 changes: 75 additions & 0 deletions knownvalue/float64_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package knownvalue_test

import (
"encoding/json"
"fmt"
"testing"

"github.com/google/go-cmp/cmp"

"github.com/hashicorp/terraform-plugin-testing/knownvalue"
)

func TestFloat64Value_CheckValue(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
self knownvalue.Check
other any
expectedError error
}{
"zero-nil": {
self: knownvalue.Float64Exact(0),
expectedError: fmt.Errorf("expected json.Number value for Float64Exact check, got: <nil>"),
},
"zero-other": {
self: knownvalue.Float64Exact(0),
other: json.Number("0.0"), // checking against the underlying value field zero-value
},
"nil": {
self: knownvalue.Float64Exact(1.234),
expectedError: fmt.Errorf("expected json.Number value for Float64Exact check, got: <nil>"),
},
"wrong-type": {
self: knownvalue.Float64Exact(1.234),
other: json.Number("str"),
expectedError: fmt.Errorf("expected json.Number to be parseable as float64 value for Float64Exact check: strconv.ParseFloat: parsing \"str\": invalid syntax"),
},
"not-equal": {
self: knownvalue.Float64Exact(1.234),
other: json.Number("4.321"),
expectedError: fmt.Errorf("expected value 1.234 for Float64Exact check, got: 4.321"),
},
"equal": {
self: knownvalue.Float64Exact(1.234),
other: json.Number("1.234"),
},
}

for name, testCase := range testCases {
name, testCase := name, testCase

t.Run(name, func(t *testing.T) {
t.Parallel()

got := testCase.self.CheckValue(testCase.other)

if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
})
}
}

func TestFloat64Value_String(t *testing.T) {
t.Parallel()

got := knownvalue.Float64Exact(1.234567890123e+09).String()

if diff := cmp.Diff(got, "1234567890.123"); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
}
51 changes: 51 additions & 0 deletions knownvalue/int64.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package knownvalue

import (
"encoding/json"
"fmt"
"strconv"
)

var _ Check = int64Exact{}

type int64Exact struct {
value int64
}

// CheckValue determines whether the passed value is of type int64, and
// contains a matching int64 value.
func (v int64Exact) CheckValue(other any) error {
jsonNum, ok := other.(json.Number)

if !ok {
return fmt.Errorf("expected json.Number value for Int64Exact check, got: %T", other)
}

otherVal, err := jsonNum.Int64()

if err != nil {
return fmt.Errorf("expected json.Number to be parseable as int64 value for Int64Exact check: %s", err)
}

if otherVal != v.value {
return fmt.Errorf("expected value %d for Int64Exact check, got: %d", v.value, otherVal)
}

return nil
}

// String returns the string representation of the int64 value.
func (v int64Exact) String() string {
return strconv.FormatInt(v.value, 10)
}

// Int64Exact returns a Check for asserting equality between the
// supplied int64 and the value passed to the CheckValue method.
func Int64Exact(value int64) int64Exact {
return int64Exact{
value: value,
}
}
Loading

0 comments on commit 198c751

Please sign in to comment.