blob: e83af305c9d1c9206f837a2d03a687b395a5a47e [file] [log] [blame]
// 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
}