mirror of
https://github.com/hashicorp/terraform.git
synced 2026-03-21 10:00:09 -04:00
198 lines
6 KiB
Go
198 lines
6 KiB
Go
// Copyright IBM Corp. 2014, 2026
|
|
// 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
|
|
}
|