hashicorp / terraform-plugin-testing

Module for testing Terraform providers
Mozilla Public License 2.0
44 stars 11 forks source link

Implement comparison state checks to replace `TestCheckResourceAttrPtr`, `TestCheckResourceAttrPair` and `TestCheckTypeSetElemAttrPair` #330

Open bendbennett opened 2 months ago

bendbennett commented 2 months ago

Closes: #295 References: #282 References: #187

Background

During the implementation of Consider Adding State Check Equivalents to Plan Checks, it was intended that the pre-built TestCheckFunc implementations would be deprecated. However, these deprecations were not included at that time partly because there was no satisfactory re-implementation of TestCheckResourceAttrPtr, which used the new style state checks which leverage tfjson.State, and neither TestCheckResourceAttrPair, nor TestCheckTypeSetElemAttrPair were re-implemented as it was unclear whether there was a need for such checks.

Comparison State Checks and the ValueComparer interface

The concepts from Consider Multiple Value Checking Interface have been used to implement comparison state checks to replace the built-in TestCheckFunc checks for TestCheckResourceAttrPtr, TestCheckResourceAttrPair, and TestCheckTypeSetElemAttrPair. All three implementations make use of the ValueComparer interface.

Replacement for TestCheckResourceAttrPtr - CompareValue

var _ StateCheck = &compareValue{}

type compareValue struct {
    resourceAddresses []string
    attributePaths    []tfjsonpath.Path
    stateValues       []any
    comparer          compare.ValueComparer
}

func (e *compareValue) AddStateValue(resourceAddress string, attributePath tfjsonpath.Path) StateCheck {
    e.resourceAddresses = append(e.resourceAddresses, resourceAddress)
    e.attributePaths = append(e.attributePaths, attributePath)

    return e
}

// CheckState implements the state check logic.
func (e *compareValue) CheckState(ctx context.Context, req CheckStateRequest, resp *CheckStateResponse) {
    /* ... */

    err = e.comparer.CompareValues(e.stateValues...)

    /* ... */
}

// CompareValue returns a state check that compares sequential values retrieved from state.
func CompareValue(comparer compare.ValueComparer) *compareValue {
    return &compareValue{
        comparer: comparer,
    }
}

Replacement for TestCheckResourceAttrPair - CompareValuePairs

var _ StateCheck = &compareValuePairs{}

type compareValuePairs struct {
    resourceAddressOne string
    attributePathOne   tfjsonpath.Path
    resourceAddressTwo string
    attributePathTwo   tfjsonpath.Path
    comparer           compare.ValueComparer
}

// CheckState implements the state check logic.
func (e *compareValuePairs) CheckState(ctx context.Context, req CheckStateRequest, resp *CheckStateResponse) {
    /* ... */

    err = e.comparer.CompareValues(resultOne, resultTwo)

    /* ... */
}

func CompareValuePairs(resourceAddressOne string, attributePathOne tfjsonpath.Path, resourceAddressTwo string, attributePathTwo tfjsonpath.Path, comparer compare.ValueComparer) StateCheck {
    return &compareValuePairs{
        resourceAddressOne: resourceAddressOne,
        attributePathOne:   attributePathOne,
        resourceAddressTwo: resourceAddressTwo,
        attributePathTwo:   attributePathTwo,
        comparer:           comparer,
    }
}

Replacement for TestCheckTypeSetElemAttrPair - CompareValueCollection

var _ StateCheck = &compareValueCollection{}

type compareValueCollection struct {
    resourceAddressOne string
    collectionPath     []tfjsonpath.Path
    resourceAddressTwo string
    attributePath      tfjsonpath.Path
    comparer           compare.ValueComparer
}

// CheckState implements the state check logic.
func (e *compareValueCollection) CheckState(ctx context.Context, req CheckStateRequest, resp *CheckStateResponse) {
    /* ... */

    for _, v := range results {
        switch resultTwo.(type) {
        case []any:
            errs = append(errs, e.comparer.CompareValues([]any{v}, resultTwo))
        default:
            errs = append(errs, e.comparer.CompareValues(v, resultTwo))
        }
    }

    /* ... */
}

// CompareValueCollection returns a state check that iterates over each element in a collection and compares the value of each element
// with the value of an attribute using the given value comparer.
func CompareValueCollection(resourceAddressOne string, collectionPath []tfjsonpath.Path, resourceAddressTwo string, attributePath tfjsonpath.Path, comparer compare.ValueComparer) StateCheck {
    return &compareValueCollection{
        resourceAddressOne: resourceAddressOne,
        collectionPath:     collectionPath,
        resourceAddressTwo: resourceAddressTwo,
        attributePath:      attributePath,
        comparer:           comparer,
    }
}

Implementations of the ValueComparer interface

type ValueComparer interface {
    CompareValues(values ...any) error
}
type ValuesSame struct{}

func (v ValuesSame) CompareValues(values ...any) error {
    for i := 1; i < len(values); i++ {
        if values[i-1] != values[i] {
            return fmt.Errorf("expected values to be the same, but they differ: %v != %v", values[i-1], values[i])
        }
    }

    return nil
}

There are implementations of the ValueComparer interface for:

These various implementations can be used in conjunction with CompareValue, CompareValueCollection, and CompareValuePairs. In the case of CompareValueCollection, and CompareValuePairs, both ValuesSame, and ValuesSameAny comparisons, and ValuesDiffer, ValuesDifferAny, and ValuesDifferAll are equivalent as these two comparison state checks interrogate 2 values at a time.