Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 75 additions & 11 deletions json/golang_encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,21 +136,85 @@ func TestEncodeRenamedByteSlice(t *testing.T) {
}
}

var unsupportedValues = []any{
math.NaN(),
math.Inf(-1),
math.Inf(1),
type SamePointerNoCycle struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you confirm that these tests broke with the behavior on tip before you applied your patches? Ie, that the patches fix the tests

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I sure did. Essentially the main branch of segmentio/encoding/json fails all of the stdlib cycle tests. It tried to implement detection of cycles involving regular pointers, but failed at that goal, and since there hadn't been any meaningful tests covering cycle detection, that broken feature went unnoticed.

This PR fixes not only cycle detection involving pointers, but also adds (working) support for data cycles involving maps and slices, as well as codec generation support for recursively defined slices, e.g. type T []T. There may be other hypothetical cases that aren't yet covered, but as of this PR, segmentio/encoding/json has parity with the stdlib encoding/json (at least in terms of passing all the relevant stdlib tests) for any kind of cycle.

Ptr1, Ptr2 *SamePointerNoCycle
}

var samePointerNoCycle = &SamePointerNoCycle{}

type PointerCycle struct {
Ptr *PointerCycle
}

var pointerCycle = &PointerCycle{}

type PointerCycleIndirect struct {
Ptrs []any
}

type RecursiveSlice []RecursiveSlice

var (
pointerCycleIndirect = &PointerCycleIndirect{}
mapCycle = make(map[string]any)
sliceCycle = []any{nil}
sliceNoCycle = []any{nil, nil}
recursiveSliceCycle = []RecursiveSlice{nil}
)

func init() {
ptr := &SamePointerNoCycle{}
samePointerNoCycle.Ptr1 = ptr
samePointerNoCycle.Ptr2 = ptr

pointerCycle.Ptr = pointerCycle
pointerCycleIndirect.Ptrs = []any{pointerCycleIndirect}

mapCycle["x"] = mapCycle
sliceCycle[0] = sliceCycle
sliceNoCycle[1] = sliceNoCycle[:1]
for i := startDetectingCyclesAfter; i > 0; i-- {
sliceNoCycle = []any{sliceNoCycle}
}
recursiveSliceCycle[0] = recursiveSliceCycle
}

func TestSamePointerNoCycle(t *testing.T) {
if _, err := Marshal(samePointerNoCycle); err != nil {
t.Fatalf("Marshal error: %v", err)
}
}

func TestSliceNoCycle(t *testing.T) {
if _, err := Marshal(sliceNoCycle); err != nil {
t.Fatalf("Marshal error: %v", err)
}
}

func TestUnsupportedValues(t *testing.T) {
for _, v := range unsupportedValues {
if _, err := Marshal(v); err != nil {
if _, ok := err.(*UnsupportedValueError); !ok {
t.Errorf("for %v, got %T want UnsupportedValueError", v, err)
tests := []struct {
CaseName
in any
}{
{Name(""), math.NaN()},
{Name(""), math.Inf(-1)},
{Name(""), math.Inf(1)},
{Name(""), pointerCycle},
{Name(""), pointerCycleIndirect},
{Name(""), mapCycle},
{Name(""), sliceCycle},
{Name(""), recursiveSliceCycle},
}
for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
if _, err := Marshal(tt.in); err != nil {
if _, ok := err.(*UnsupportedValueError); !ok {
t.Errorf("%s: Marshal error:\n\tgot: %T\n\twant: %T", tt.Where, err, new(UnsupportedValueError))
}
} else {
t.Errorf("%s: Marshal error: got nil, want non-nil", tt.Where)
}
} else {
t.Errorf("for %v, expected error", v)
}
})
}
}

Expand Down
30 changes: 30 additions & 0 deletions json/golang_shim_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ package json

import (
"bytes"
"fmt"
"path"
"reflect"
"runtime"
"sync"
"testing"
)
Expand Down Expand Up @@ -68,3 +71,30 @@ func errorWithPrefixes(t *testing.T, prefixes []any, format string, elements ...
}
t.Errorf(fullFormat, allElements...)
}

// =============================================================================
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Want to share what file these were taken from?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/golang/go/blob/f22d37d574836ed4f1b42e283dda55dbd4d80aca/src/encoding/json/stream_test.go#L27-L47

The strategy has been (I didn't come up with it, but am continuing it):

  1. Use as many stdlib json tests as possible (e.g. the tests the stdlib encoding/json package uses for itself). Copy those test files more or less intact.
  2. Copy whatever test helper types/functions are needed for the above intact copying to work without modifications.

This golang_shim_test.go serves to fulfill point 2.


// CaseName is a case name annotated with a file and line.
type CaseName struct {
Name string
Where CasePos
}

// Name annotates a case name with the file and line of the caller.
func Name(s string) (c CaseName) {
c.Name = s
runtime.Callers(2, c.Where.pc[:])
return c
}

// CasePos represents a file and line number.
type CasePos struct{ pc [1]uintptr }

func (pos CasePos) String() string {
frames := runtime.CallersFrames(pos.pc[:])
frame, _ := frames.Next()
return fmt.Sprintf("%s:%d", path.Base(frame.File), frame.Line)
}