terraform/internal/copy/copy_value_test.go
Martin Atkins d14c149001 copy: DeepCopyValue utility
Over the years we've gradually removed all use of various third-party
reflection-based libraries for deep-copying and projecting data structures
from main code, because they proved to be a maintenance nightmare and thus
better replaced by direct code that is less fun to write but far clearer
to read.

However, we've still got a few examples of _test_ code hanging on to these
helpers because the tradeoffs for unit tests tend to be different. That's
valid, but it's annoying to have to include several large and very general
dependencies just to support some tests whose needs are well-known and
relatively simple.

Therefore this new helper function copy.DeepCopyValue aims to be a more
straightforward replacement for mitchellh/copystructure (which also
indirectly subsumes mitchellh/reflectwalk) that eschews all of the
customizability that those libraries offered and instead focuses narrowly
on what our few remaining unit tests need.

In particular, this replacement doesn't offer any means for registering
helper functions to translate struct types with unexported fields. Those
instead just get left set to their zero value in the result. Since our
focus is only on unit test code, we can know ahead of time when that
limitation is relevant and code around it in the calling test rather than
complicating this utility code.
2024-03-08 13:59:55 -08:00

198 lines
6 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package copy
import (
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/google/go-cmp/cmp"
)
func TestCopyValue(t *testing.T) {
t.Run("pointer to something that needs copying", func(t *testing.T) {
// To test this we need to point to something that actually gets
// deep copied, because the pointer _itself_ is just a number,
// not mutably-aliased memory. (If the pointee is not something
// that can be mutably aliased then the result would just match the
// input, because no copying is needed.)
type V struct {
S string
}
input := &V{"hello"}
result := testDeepCopyValueLogged(t, input)
if input == result {
t.Errorf("result pointer matches input pointer")
}
if input.S != "hello" {
t.Errorf("input was modified before we modified it")
}
result.S = "goodbye"
if input.S != "hello" {
t.Errorf("modifying result also modified input")
}
})
t.Run("pointer to something that doesn't need copying", func(t *testing.T) {
// Strings are immutable and so we don't deep-copy them. Therefore
// a pointer to a string doesn't get modified during copy either.
s := "hello"
input := &s
result := testDeepCopyValueLogged(t, input)
if input != result {
t.Errorf("result pointer does not match input pointer")
}
})
t.Run("pointer that is nil", func(t *testing.T) {
var input *int
result := testDeepCopyValueLogged(t, input)
if result != nil {
t.Errorf("result is not nil")
}
})
t.Run("slice", func(t *testing.T) {
arr := [...]rune{'a', 'b', 'c', 'd'}
input := arr[0:2:4] // ab is in length, cd is hidden in extra capacity
result := testDeepCopyValueLogged(t, input)
if &input[0] == &result[0] {
t.Errorf("result shares backing array with input")
}
if got := len(result); got != 2 {
t.Fatalf("result has incorrect length %d", got)
}
if got := cap(result); got != 4 {
t.Fatalf("result has incorrect capacity %d", got)
}
// We'll expand the slices so we can view the excess capacity too
fullInput := input[0:4]
fullResult := result[0:4]
want := []rune{'a', 'b', 'c', 'd'}
if diff := cmp.Diff(want, fullInput); diff != "" {
t.Errorf("input was modified\n%s", diff)
}
if diff := cmp.Diff(want, fullResult); diff != "" {
t.Errorf("incorrect result\n%s", diff)
}
})
t.Run("slice that is nil", func(t *testing.T) {
var input []int
result := testDeepCopyValueLogged(t, input)
if result != nil {
t.Errorf("result is not nil")
}
})
t.Run("array", func(t *testing.T) {
// Arrays are passed by value anyway, so deep copying one really
// means deep copying anything they refer to that might contain
// mutably-aliased data. We'll use slices as the victims here;
// their backing arrays should be copied and thus the result
// should have different slices but with the same content.
input := [...][]rune{
{'a', 'b'},
{'c', 'd'},
}
result := testDeepCopyValueLogged(t, input)
if &result[0][0] == &input[0][0] {
t.Errorf("first element of result shares backing array with input")
}
if &result[1][0] == &input[1][0] {
t.Errorf("second element of result shares backing array with input")
}
want := [...][]rune{
{'a', 'b'},
{'c', 'd'},
}
if diff := cmp.Diff(want, result); diff != "" {
t.Errorf("incorrect result\n%s", diff)
}
})
t.Run("map", func(t *testing.T) {
// Maps are a bit tricky to test because they are an address-based
// data structure but the addresses of the internals are intentionally
// not exposed. Therefore we'll test this indirectly by making a
// map, copying it, and then modifying the copy. That should leave
// the original unchanged, if the copy was performed correctly.
input := map[string]string{"greeting": "hello"}
result := testDeepCopyValueLogged(t, input)
if len(input) != 1 {
t.Errorf("input length changed before we did any modifying")
}
if input["greeting"] != "hello" {
t.Errorf("input element changed before we did any modifying")
}
if len(result) != 1 {
t.Errorf("result length changed before we did any modifying")
}
if result["greeting"] != "hello" {
t.Errorf("result element changed before we did any modifying")
}
result["greeting"] = "hallo"
if input["greeting"] != "hello" {
t.Errorf("input element changed when we modified result")
}
})
t.Run("map that is nil", func(t *testing.T) {
var input map[string]string
result := testDeepCopyValueLogged(t, input)
if result != nil {
t.Errorf("result is not nil")
}
})
t.Run("struct", func(t *testing.T) {
type S struct {
Exported string
unexported string
}
input := S{
Exported: "beep",
unexported: "boop",
}
result := testDeepCopyValueLogged(t, input)
if result.Exported != "beep" {
t.Errorf("Exported field has wrong result")
}
if result.unexported != "" {
t.Errorf("unexported field got populated (should have been left as zero value)")
}
})
t.Run("interface", func(t *testing.T) {
// We'll create an interface that contains a pointer to something
// mutable, and then mutate it after copy to make sure that the
// two values can change independently.
type B struct {
S string
}
type A struct {
B *B
}
inputInner := &A{
&B{"hello"},
}
input := any(inputInner) // an interface value wrapping inputInner
result := testDeepCopyValueLogged(t, input)
if resultInner, ok := result.(*A); !ok {
t.Fatalf("result contains %T, not %T", result, resultInner)
}
if result.(*A) == input.(*A) {
t.Error("result has same address as input")
}
if result.(*A).B == input.(*A).B {
t.Error("result.b has same address as input")
}
if input.(*A).B.S != "hello" {
t.Errorf("input was modified before we modified it")
}
result.(*A).B.S = "goodbye"
if input.(*A).B.S != "hello" {
t.Errorf("modifying result also modified input")
}
})
}
func testDeepCopyValueLogged[T any](t *testing.T, input T) T {
t.Helper()
t.Logf("input: %s", spew.Sdump(input))
result := DeepCopyValue(input)
t.Logf("result: %s", spew.Sdump(result))
return result
}