Files
opentf/internal/plans/plan_test.go
Martin Atkins 5b5a285066 Replace github.com/go-test/deep with go-cmp
My original intention was just to reduce our number of dependencies by
standardizing on a single comparison library, but in the process of doing
so I found various examples of the kinds of problems that caused this
codebase to begin adopting go-cmp instead of go-test/deep in the first
place, which make it easy to accidentally write a false-positive test that
doesn't actually check what the author thinks is being checked:

- deep.Equal silently ignores unexported fields, so comparing two values
  that differ only in data in unexported fields succeeds even when it ought
  not to.

  TestContext2Apply_multiVarComprehensive in package tofu was an excellent
  example of this problem: it had various test assertions that were
  actually checking absolutely nothing, despite appearing to compare
  pairs of cty.Value.

- deep.Equal also silently ignores anything below a certain level of
  nesting, and so comparison of deep data structures can appear to succeed
  even though they don't actually match.

  There were a few examples where that problem had already been found and
  fixed by temporarily overriding the package deep global settings, but
  with go-cmp the default behavior already visits everything, or panics
  if it cannot.

This does mean that in a few cases this needed some more elaborate options
to cmp.Diff to align with the previous behavior, which is a little annoying
but overall I think better to be explicit about what each test is relying
on. Perhaps we can rework these tests to need fewer unusual cmp options
in future, but for this commit I want to keep focused on the smallest
possible changes to remove our dependency on github.com/go-test/deep .

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-10-13 08:17:40 -07:00

156 lines
4.3 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package plans
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/zclconf/go-cty/cty"
ctymsgpack "github.com/zclconf/go-cty/cty/msgpack"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs"
)
func TestProviderAddrs(t *testing.T) {
plan := &Plan{
VariableValues: map[string]DynamicValue{},
Changes: &Changes{
Resources: []*ResourceInstanceChangeSrc{
{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "woot",
}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.NewDefaultProvider("test"),
},
},
{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "woot",
}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
DeposedKey: "foodface",
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.NewDefaultProvider("test"),
},
},
{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "what",
}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule.Child("foo"),
Provider: addrs.NewDefaultProvider("test"),
},
},
},
},
}
got := plan.ProviderAddrs()
want := []addrs.AbsProviderConfig{
{
Module: addrs.RootModule.Child("foo"),
Provider: addrs.NewDefaultProvider("test"),
},
{
Module: addrs.RootModule,
Provider: addrs.NewDefaultProvider("test"),
},
}
if diff := cmp.Diff(want, got); diff != "" {
t.Error("wrong result:\n" + diff)
}
}
// Module outputs should not effect the result of Empty
func TestModuleOutputChangesEmpty(t *testing.T) {
changes := &Changes{
Outputs: []*OutputChangeSrc{
{
Addr: addrs.AbsOutputValue{
Module: addrs.RootModuleInstance.Child("child", addrs.NoKey),
OutputValue: addrs.OutputValue{
Name: "output",
},
},
ChangeSrc: ChangeSrc{
Action: Update,
Before: []byte("a"),
After: []byte("b"),
},
},
},
}
if !changes.Empty() {
t.Fatal("plan has no visible changes")
}
}
// TestVariableMapper checks that the mapper is decoding types correctly from the plan
func TestVariableMapper(t *testing.T) {
val1 := cty.StringVal("string value")
val2 := cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("bar")})
val3 := cty.MapVal(map[string]cty.Value{
"inner": cty.SetVal([]cty.Value{cty.StringVal("baz")}),
})
val4 := cty.ListVal([]cty.Value{cty.BoolVal(false)})
val5 := cty.SetVal([]cty.Value{
cty.ObjectVal(
map[string]cty.Value{
"inner": cty.ObjectVal(map[string]cty.Value{"foo": cty.NumberIntVal(25)}),
},
),
})
p := Plan{VariableValues: map[string]DynamicValue{
"raw_string": encodeDynamicValueWithType(t, val1, cty.DynamicPseudoType),
"object_of_strings": encodeDynamicValueWithType(t, val2, cty.DynamicPseudoType),
"map_of_sets_of_strings": encodeDynamicValueWithType(t, val3, cty.DynamicPseudoType),
"list_of_bools": encodeDynamicValueWithType(t, val4, cty.DynamicPseudoType),
"set_of_obj_of_obj_of_number": encodeDynamicValueWithType(t, val5, cty.DynamicPseudoType),
}}
vm := p.VariableMapper()
cases := map[string]cty.Value{
"raw_string": val1,
"object_of_strings": val2,
"map_of_sets_of_strings": val3,
"list_of_bools": val4,
"set_of_obj_of_obj_of_number": val5,
}
for varName, wantVal := range cases {
t.Run(varName, func(t *testing.T) {
val, diag := vm(&configs.Variable{Name: varName})
if diag.HasErrors() {
t.Fatalf("unexpected diagnostics from the variable mapper: %s", diag)
}
if !val.RawEquals(wantVal) {
t.Fatalf("returned value is not equal with the expected one.\n\twant:%s\n\tgot:%s\n", wantVal, val)
}
})
}
}
func encodeDynamicValueWithType(t *testing.T, value cty.Value, ty cty.Type) []byte {
data, err := ctymsgpack.Marshal(value, ty)
if err != nil {
t.Fatalf("failed to marshal JSON: %s", err)
}
return data
}