333 lines
10 KiB
Go
333 lines
10 KiB
Go
package jsonvalue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"math"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type (
|
|
namedBool bool
|
|
namedString string
|
|
namedInt64 int64
|
|
namedUint64 uint64
|
|
namedFloat32 float32
|
|
namedFloat64 float64
|
|
namedKey string
|
|
namedMap map[namedKey]namedInt64
|
|
namedSlice []namedString
|
|
namedArray [1]map[string]int
|
|
)
|
|
|
|
func TestCopyPreservesSupportedScalarAndNumberTypes(t *testing.T) {
|
|
maxInt := int(^uint(0) >> 1)
|
|
minInt := -maxInt - 1
|
|
tests := []struct {
|
|
name string
|
|
value any
|
|
}{
|
|
{name: "bool", value: true},
|
|
{name: "named bool", value: namedBool(true)},
|
|
{name: "string", value: "value"},
|
|
{name: "named string", value: namedString("value")},
|
|
{name: "int", value: minInt},
|
|
{name: "int8", value: int8(-1 << 7)},
|
|
{name: "int16", value: int16(-1 << 15)},
|
|
{name: "int32", value: int32(-1 << 31)},
|
|
{name: "int64", value: int64(-1 << 63)},
|
|
{name: "named int64", value: namedInt64(1<<63 - 1)},
|
|
{name: "uint", value: ^uint(0)},
|
|
{name: "uint8", value: ^uint8(0)},
|
|
{name: "uint16", value: ^uint16(0)},
|
|
{name: "uint32", value: ^uint32(0)},
|
|
{name: "uint64", value: ^uint64(0)},
|
|
{name: "uintptr", value: ^uintptr(0)},
|
|
{name: "named uint64", value: namedUint64(^uint64(0))},
|
|
{name: "float32", value: float32(1.25)},
|
|
{name: "float64", value: float64(-2.5e100)},
|
|
{name: "named float32", value: namedFloat32(3.5)},
|
|
{name: "named float64", value: namedFloat64(-4.5e200)},
|
|
{name: "JSON number integer", value: json.Number("18446744073709551615")},
|
|
{name: "JSON number fraction", value: json.Number("-1.25e+2")},
|
|
{name: "JSON number beyond float64", value: json.Number("1e9999")},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, err := Copy(tc.value)
|
|
if err != nil {
|
|
t.Fatalf("copy value: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(got, tc.value) {
|
|
t.Fatalf("value or concrete type changed: got %#v (%T), want %#v (%T)", got, got, tc.value, tc.value)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCopyRejectsInvalidNumbers(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value any
|
|
}{
|
|
{name: "float32 NaN", value: float32(math.NaN())},
|
|
{name: "float64 NaN", value: math.NaN()},
|
|
{name: "named float NaN", value: namedFloat64(math.NaN())},
|
|
{name: "positive infinity", value: math.Inf(1)},
|
|
{name: "negative infinity", value: math.Inf(-1)},
|
|
{name: "empty JSON number", value: json.Number("")},
|
|
{name: "leading zero JSON number", value: json.Number("01")},
|
|
{name: "leading plus JSON number", value: json.Number("+1")},
|
|
{name: "trailing decimal JSON number", value: json.Number("1.")},
|
|
{name: "leading decimal JSON number", value: json.Number(".1")},
|
|
{name: "non-number JSON number", value: json.Number("NaN")},
|
|
{name: "spaced JSON number", value: json.Number(" 1")},
|
|
{name: "quoted JSON number", value: json.Number(`"1"`)},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if _, err := Copy(tc.value); err == nil {
|
|
t.Fatal("expected validation error")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCopyPreservesCompatibleCollectionsAndNilEmptyDistinctions(t *testing.T) {
|
|
collections := []struct {
|
|
name string
|
|
value any
|
|
}{
|
|
{name: "unnamed map", value: map[string]int{"limit": 2}},
|
|
{name: "named map", value: namedMap{"limit": 2}},
|
|
{name: "unnamed slice", value: []string{"one", "two"}},
|
|
{name: "named slice", value: namedSlice{"one", "two"}},
|
|
{name: "unnamed array", value: [2]int{1, 2}},
|
|
{name: "named array", value: namedArray{{"limit": 2}}},
|
|
{name: "empty map", value: map[string]int{}},
|
|
{name: "empty named map", value: namedMap{}},
|
|
{name: "empty slice", value: []string{}},
|
|
{name: "empty named slice", value: namedSlice{}},
|
|
{name: "empty array", value: [0]string{}},
|
|
}
|
|
|
|
for _, tc := range collections {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, err := Copy(tc.value)
|
|
if err != nil {
|
|
t.Fatalf("copy collection: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(got, tc.value) || reflect.TypeOf(got) != reflect.TypeOf(tc.value) {
|
|
t.Fatalf("collection changed: got %#v (%T), want %#v (%T)", got, got, tc.value, tc.value)
|
|
}
|
|
kind := reflect.ValueOf(got).Kind()
|
|
if (kind == reflect.Map || kind == reflect.Slice) && reflect.ValueOf(got).IsNil() {
|
|
t.Fatal("non-nil collection became nil")
|
|
}
|
|
})
|
|
}
|
|
|
|
var nilMap map[string]int
|
|
var nilSlice []string
|
|
var nilPointer *namedInt64
|
|
for _, value := range []any{nil, nilMap, nilSlice, nilPointer} {
|
|
got, err := Copy(value)
|
|
if err != nil {
|
|
t.Fatalf("copy null value: %v", err)
|
|
}
|
|
if got != nil {
|
|
t.Fatalf("null value became %#v (%T)", got, got)
|
|
}
|
|
}
|
|
|
|
gotNil, err := CopyMap(nil)
|
|
if err != nil || gotNil != nil {
|
|
t.Fatalf("nil CopyMap result = %#v, %v", gotNil, err)
|
|
}
|
|
gotEmpty, err := CopyMap(map[string]any{})
|
|
if err != nil || gotEmpty == nil || len(gotEmpty) != 0 {
|
|
t.Fatalf("empty CopyMap result = %#v, %v", gotEmpty, err)
|
|
}
|
|
}
|
|
|
|
func TestCopyHandlesIndirectionAndIsolatesNestedMutations(t *testing.T) {
|
|
integer := namedInt64(7)
|
|
nestedMap := namedMap{"limit": 2}
|
|
nestedSlice := namedSlice{"original"}
|
|
nestedArray := namedArray{{"limit": 3}}
|
|
shared := []any{map[string]int{"value": 4}}
|
|
input := map[string]any{
|
|
"integer": &integer,
|
|
"map": nestedMap,
|
|
"slice": nestedSlice,
|
|
"array": nestedArray,
|
|
"first": shared,
|
|
"second": shared,
|
|
}
|
|
|
|
copiedValue, err := Copy(input)
|
|
if err != nil {
|
|
t.Fatalf("copy mixed tree: %v", err)
|
|
}
|
|
copied := copiedValue.(map[string]any)
|
|
nestedMap["limit"] = 20
|
|
nestedSlice[0] = "changed"
|
|
nestedArray[0]["limit"] = 30
|
|
shared[0].(map[string]int)["value"] = 40
|
|
|
|
if got, ok := copied["integer"].(namedInt64); !ok || got != 7 {
|
|
t.Fatalf("pointer target changed: %#v", copied["integer"])
|
|
}
|
|
if got := copied["map"].(namedMap)["limit"]; got != 2 {
|
|
t.Fatalf("nested map aliased input: %d", got)
|
|
}
|
|
if got := copied["slice"].(namedSlice)[0]; got != "original" {
|
|
t.Fatalf("nested slice aliased input: %q", got)
|
|
}
|
|
if got := copied["array"].(namedArray)[0]["limit"]; got != 3 {
|
|
t.Fatalf("nested array aliased input: %d", got)
|
|
}
|
|
first := copied["first"].([]any)
|
|
second := copied["second"].([]any)
|
|
if got := first[0].(map[string]int)["value"]; got != 4 {
|
|
t.Fatalf("shared child aliased input: %d", got)
|
|
}
|
|
first[0].(map[string]int)["value"] = 99
|
|
if got := second[0].(map[string]int)["value"]; got != 4 {
|
|
t.Fatalf("repeated acyclic value shared copied output: %d", got)
|
|
}
|
|
}
|
|
|
|
func TestCopyAndCopyMapApplyDistinctEmptyKeyRules(t *testing.T) {
|
|
nested := map[string]any{"": []any{"original"}}
|
|
copiedValue, err := Copy(nested)
|
|
if err != nil {
|
|
t.Fatalf("Copy rejected empty schema key: %v", err)
|
|
}
|
|
nested[""].([]any)[0] = "changed"
|
|
if got := copiedValue.(map[string]any)[""].([]any)[0]; got != "original" {
|
|
t.Fatalf("copied schema value was not isolated: %v", got)
|
|
}
|
|
|
|
_, err = CopyMap(map[string]any{"nested": map[string]any{"": true}})
|
|
if err == nil || !strings.Contains(err.Error(), "extra_params.nested") {
|
|
t.Fatalf("CopyMap empty-key error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCopyRejectsUnsupportedValuesAndActiveCycles(t *testing.T) {
|
|
cyclicMap := map[string]any{}
|
|
cyclicMap["self"] = cyclicMap
|
|
cyclicSlice := []any{nil}
|
|
cyclicSlice[0] = cyclicSlice
|
|
var cyclicPointer any
|
|
cyclicPointer = &cyclicPointer
|
|
|
|
tests := []struct {
|
|
name string
|
|
value any
|
|
wantPath string
|
|
}{
|
|
{name: "non-string map key", value: map[int]string{1: "one"}, wantPath: "value"},
|
|
{name: "unsupported channel", value: make(chan int), wantPath: "value"},
|
|
{name: "deterministic map path", value: map[string]any{"z": make(chan int), "a": make(chan int)}, wantPath: "value.a"},
|
|
{name: "cyclic map", value: cyclicMap, wantPath: "value.self"},
|
|
{name: "cyclic slice", value: cyclicSlice, wantPath: "value[0]"},
|
|
{name: "cyclic pointer", value: cyclicPointer, wantPath: "value"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := Copy(tc.value)
|
|
if err == nil || !strings.Contains(err.Error(), tc.wantPath) {
|
|
t.Fatalf("error = %v, want structural path %q", err, tc.wantPath)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCopyEnforcesContainerDepth(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
depth int
|
|
wantErr bool
|
|
}{
|
|
{name: "just below", depth: maxContainerDepth - 1},
|
|
{name: "at limit", depth: maxContainerDepth},
|
|
{name: "over limit", depth: maxContainerDepth + 1, wantErr: true},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := Copy(alternatingContainers(tc.depth))
|
|
if tc.wantErr {
|
|
if err == nil || !strings.HasPrefix(err.Error(), "value") || !strings.Contains(err.Error(), "container depth limit") {
|
|
t.Fatalf("depth error = %v", err)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("copy depth %d: %v", tc.depth, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCopyEnforcesProducedNodeBudgetForRepeatedAcyclicValues(t *testing.T) {
|
|
shared := []any{true}
|
|
sharedOccurrences := (maxProducedNodes - 2) / 2
|
|
justBelow := repeatedValues(shared, sharedOccurrences, 0)
|
|
atLimit := repeatedValues(shared, sharedOccurrences, 1)
|
|
overLimit := repeatedValues(shared, sharedOccurrences, 2)
|
|
|
|
for name, value := range map[string]any{
|
|
"just below": justBelow,
|
|
"at limit": atLimit,
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
if _, err := Copy(value); err != nil {
|
|
t.Fatalf("copy value within work budget: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
_, err := Copy(overLimit)
|
|
if err == nil || !strings.HasPrefix(err.Error(), "value[") || !strings.Contains(err.Error(), "value work limit") {
|
|
t.Fatalf("work-budget error = %v", err)
|
|
}
|
|
|
|
_, err = Copy(make([]any, maxProducedNodes))
|
|
if err == nil || !strings.HasPrefix(err.Error(), "value:") || !strings.Contains(err.Error(), "value work limit") {
|
|
t.Fatalf("flat work-budget error = %v", err)
|
|
}
|
|
}
|
|
|
|
func alternatingContainers(depth int) any {
|
|
var value any = true
|
|
for level := 0; level < depth; level++ {
|
|
switch level % 3 {
|
|
case 0:
|
|
value = map[string]any{"child": value}
|
|
case 1:
|
|
value = []any{value}
|
|
default:
|
|
value = [1]any{value}
|
|
}
|
|
}
|
|
return value
|
|
}
|
|
|
|
func repeatedValues(shared []any, occurrences, leadingScalars int) []any {
|
|
values := make([]any, 0, leadingScalars+occurrences)
|
|
for i := 0; i < leadingScalars; i++ {
|
|
values = append(values, false)
|
|
}
|
|
for i := 0; i < occurrences; i++ {
|
|
values = append(values, shared)
|
|
}
|
|
return values
|
|
}
|