DSCGS-36 Go mod updated, test updated (#38)

This commit is contained in:
Artem Piskun 2020-02-28 14:50:43 +03:00 committed by GitHub
parent e287c7d9b3
commit b61a3c137a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 188 additions and 50 deletions

4
go.mod
View File

@ -1,5 +1,5 @@
module github.com/irlndts/go-discogs module github.com/irlndts/go-discogs
go 1.13.4 go 1.14
require github.com/google/go-cmp v0.3.1 require github.com/google/go-cmp v0.4.0

6
go.sum
View File

@ -1,2 +1,4 @@
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -22,7 +22,7 @@ type Series struct {
ID int `json:"id"` ID int `json:"id"`
Name string `json:"name"` Name string `json:"name"`
ResourceURL string `json:"resource_url"` ResourceURL string `json:"resource_url"`
ThumbnailURL string `json:"thumbnail_url"` ThumbnailURL string `json:"thumbnail_url,omitempty"`
} }
// ArtistSource ... // ArtistSource ...
@ -68,7 +68,7 @@ type LabelSource struct {
// Identifier ... // Identifier ...
type Identifier struct { type Identifier struct {
Description string `json:"description"` Description string `json:"description,omitempty"`
Type string `json:"type"` Type string `json:"type"`
Value string `json:"value"` Value string `json:"value"`
} }

File diff suppressed because one or more lines are too long

View File

@ -22,8 +22,8 @@
// equality is determined by recursively comparing the primitive kinds on both // equality is determined by recursively comparing the primitive kinds on both
// values, much like reflect.DeepEqual. Unlike reflect.DeepEqual, unexported // values, much like reflect.DeepEqual. Unlike reflect.DeepEqual, unexported
// fields are not compared by default; they result in panics unless suppressed // fields are not compared by default; they result in panics unless suppressed
// by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly compared // by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly
// using the AllowUnexported option. // compared using the Exporter option.
package cmp package cmp
import ( import (
@ -62,8 +62,8 @@ import (
// //
// Structs are equal if recursively calling Equal on all fields report equal. // Structs are equal if recursively calling Equal on all fields report equal.
// If a struct contains unexported fields, Equal panics unless an Ignore option // If a struct contains unexported fields, Equal panics unless an Ignore option
// (e.g., cmpopts.IgnoreUnexported) ignores that field or the AllowUnexported // (e.g., cmpopts.IgnoreUnexported) ignores that field or the Exporter option
// option explicitly permits comparing the unexported field. // explicitly permits comparing the unexported field.
// //
// Slices are equal if they are both nil or both non-nil, where recursively // Slices are equal if they are both nil or both non-nil, where recursively
// calling Equal on all non-ignored slice or array elements report equal. // calling Equal on all non-ignored slice or array elements report equal.
@ -80,6 +80,11 @@ import (
// Pointers and interfaces are equal if they are both nil or both non-nil, // Pointers and interfaces are equal if they are both nil or both non-nil,
// where they have the same underlying concrete type and recursively // where they have the same underlying concrete type and recursively
// calling Equal on the underlying values reports equal. // calling Equal on the underlying values reports equal.
//
// Before recursing into a pointer, slice element, or map, the current path
// is checked to detect whether the address has already been visited.
// If there is a cycle, then the pointed at values are considered equal
// only if both addresses were previously visited in the same path step.
func Equal(x, y interface{}, opts ...Option) bool { func Equal(x, y interface{}, opts ...Option) bool {
vx := reflect.ValueOf(x) vx := reflect.ValueOf(x)
vy := reflect.ValueOf(y) vy := reflect.ValueOf(y)
@ -137,6 +142,7 @@ type state struct {
// Calling statelessCompare must not result in observable changes to these. // Calling statelessCompare must not result in observable changes to these.
result diff.Result // The current result of comparison result diff.Result // The current result of comparison
curPath Path // The current path in the value tree curPath Path // The current path in the value tree
curPtrs pointerPath // The current set of visited pointers
reporters []reporter // Optional reporters reporters []reporter // Optional reporters
// recChecker checks for infinite cycles applying the same set of // recChecker checks for infinite cycles applying the same set of
@ -148,13 +154,14 @@ type state struct {
dynChecker dynChecker dynChecker dynChecker
// These fields, once set by processOption, will not change. // These fields, once set by processOption, will not change.
exporters map[reflect.Type]bool // Set of structs with unexported field visibility exporters []exporter // List of exporters for structs with unexported fields
opts Options // List of all fundamental and filter options opts Options // List of all fundamental and filter options
} }
func newState(opts []Option) *state { func newState(opts []Option) *state {
// Always ensure a validator option exists to validate the inputs. // Always ensure a validator option exists to validate the inputs.
s := &state{opts: Options{validator{}}} s := &state{opts: Options{validator{}}}
s.curPtrs.Init()
s.processOption(Options(opts)) s.processOption(Options(opts))
return s return s
} }
@ -174,13 +181,8 @@ func (s *state) processOption(opt Option) {
panic(fmt.Sprintf("cannot use an unfiltered option: %v", opt)) panic(fmt.Sprintf("cannot use an unfiltered option: %v", opt))
} }
s.opts = append(s.opts, opt) s.opts = append(s.opts, opt)
case visibleStructs: case exporter:
if s.exporters == nil { s.exporters = append(s.exporters, opt)
s.exporters = make(map[reflect.Type]bool)
}
for t := range opt {
s.exporters[t] = true
}
case reporter: case reporter:
s.reporters = append(s.reporters, opt) s.reporters = append(s.reporters, opt)
default: default:
@ -192,9 +194,9 @@ func (s *state) processOption(opt Option) {
// This function is stateless in that it does not alter the current result, // This function is stateless in that it does not alter the current result,
// or output to any registered reporters. // or output to any registered reporters.
func (s *state) statelessCompare(step PathStep) diff.Result { func (s *state) statelessCompare(step PathStep) diff.Result {
// We do not save and restore the curPath because all of the compareX // We do not save and restore curPath and curPtrs because all of the
// methods should properly push and pop from the path. // compareX methods should properly push and pop from them.
// It is an implementation bug if the contents of curPath differs from // It is an implementation bug if the contents of the paths differ from
// when calling this function to when returning from it. // when calling this function to when returning from it.
oldResult, oldReporters := s.result, s.reporters oldResult, oldReporters := s.result, s.reporters
@ -216,9 +218,17 @@ func (s *state) compareAny(step PathStep) {
} }
s.recChecker.Check(s.curPath) s.recChecker.Check(s.curPath)
// Obtain the current type and values. // Cycle-detection for slice elements (see NOTE in compareSlice).
t := step.Type() t := step.Type()
vx, vy := step.Values() vx, vy := step.Values()
if si, ok := step.(SliceIndex); ok && si.isSlice && vx.IsValid() && vy.IsValid() {
px, py := vx.Addr(), vy.Addr()
if eq, visited := s.curPtrs.Push(px, py); visited {
s.report(eq, reportByCycle)
return
}
defer s.curPtrs.Pop(px, py)
}
// Rule 1: Check whether an option applies on this node in the value tree. // Rule 1: Check whether an option applies on this node in the value tree.
if s.tryOptions(t, vx, vy) { if s.tryOptions(t, vx, vy) {
@ -354,6 +364,7 @@ func sanitizeValue(v reflect.Value, t reflect.Type) reflect.Value {
func (s *state) compareStruct(t reflect.Type, vx, vy reflect.Value) { func (s *state) compareStruct(t reflect.Type, vx, vy reflect.Value) {
var vax, vay reflect.Value // Addressable versions of vx and vy var vax, vay reflect.Value // Addressable versions of vx and vy
var mayForce, mayForceInit bool
step := StructField{&structField{}} step := StructField{&structField{}}
for i := 0; i < t.NumField(); i++ { for i := 0; i < t.NumField(); i++ {
step.typ = t.Field(i).Type step.typ = t.Field(i).Type
@ -375,7 +386,13 @@ func (s *state) compareStruct(t reflect.Type, vx, vy reflect.Value) {
vax = makeAddressable(vx) vax = makeAddressable(vx)
vay = makeAddressable(vy) vay = makeAddressable(vy)
} }
step.mayForce = s.exporters[t] if !mayForceInit {
for _, xf := range s.exporters {
mayForce = mayForce || xf(t)
}
mayForceInit = true
}
step.mayForce = mayForce
step.pvx = vax step.pvx = vax
step.pvy = vay step.pvy = vay
step.field = t.Field(i) step.field = t.Field(i)
@ -391,9 +408,21 @@ func (s *state) compareSlice(t reflect.Type, vx, vy reflect.Value) {
return return
} }
// TODO: Support cyclic data structures. // NOTE: It is incorrect to call curPtrs.Push on the slice header pointer
// since slices represents a list of pointers, rather than a single pointer.
// The pointer checking logic must be handled on a per-element basis
// in compareAny.
//
// A slice header (see reflect.SliceHeader) in Go is a tuple of a starting
// pointer P, a length N, and a capacity C. Supposing each slice element has
// a memory size of M, then the slice is equivalent to the list of pointers:
// [P+i*M for i in range(N)]
//
// For example, v[:0] and v[:1] are slices with the same starting pointer,
// but they are clearly different values. Using the slice pointer alone
// violates the assumption that equal pointers implies equal values.
step := SliceIndex{&sliceIndex{pathStep: pathStep{typ: t.Elem()}}} step := SliceIndex{&sliceIndex{pathStep: pathStep{typ: t.Elem()}, isSlice: isSlice}}
withIndexes := func(ix, iy int) SliceIndex { withIndexes := func(ix, iy int) SliceIndex {
if ix >= 0 { if ix >= 0 {
step.vx, step.xkey = vx.Index(ix), ix step.vx, step.xkey = vx.Index(ix), ix
@ -470,7 +499,12 @@ func (s *state) compareMap(t reflect.Type, vx, vy reflect.Value) {
return return
} }
// TODO: Support cyclic data structures. // Cycle-detection for maps.
if eq, visited := s.curPtrs.Push(vx, vy); visited {
s.report(eq, reportByCycle)
return
}
defer s.curPtrs.Pop(vx, vy)
// We combine and sort the two map keys so that we can perform the // We combine and sort the two map keys so that we can perform the
// comparisons in a deterministic order. // comparisons in a deterministic order.
@ -507,7 +541,12 @@ func (s *state) comparePtr(t reflect.Type, vx, vy reflect.Value) {
return return
} }
// TODO: Support cyclic data structures. // Cycle-detection for pointers.
if eq, visited := s.curPtrs.Push(vx, vy); visited {
s.report(eq, reportByCycle)
return
}
defer s.curPtrs.Pop(vx, vy)
vx, vy = vx.Elem(), vy.Elem() vx, vy = vx.Elem(), vy.Elem()
s.compareAny(Indirect{&indirect{pathStep{t.Elem(), vx, vy}}}) s.compareAny(Indirect{&indirect{pathStep{t.Elem(), vx, vy}}})

View File

@ -8,8 +8,8 @@ package cmp
import "reflect" import "reflect"
const supportAllowUnexported = false const supportExporters = false
func retrieveUnexportedField(reflect.Value, reflect.StructField) reflect.Value { func retrieveUnexportedField(reflect.Value, reflect.StructField) reflect.Value {
panic("retrieveUnexportedField is not implemented") panic("no support for forcibly accessing unexported fields")
} }

View File

@ -11,7 +11,7 @@ import (
"unsafe" "unsafe"
) )
const supportAllowUnexported = true const supportExporters = true
// retrieveUnexportedField uses unsafe to forcibly retrieve any field from // retrieveUnexportedField uses unsafe to forcibly retrieve any field from
// a struct such that the value has read-write permissions. // a struct such that the value has read-write permissions.
@ -19,5 +19,7 @@ const supportAllowUnexported = true
// The parent struct, v, must be addressable, while f must be a StructField // The parent struct, v, must be addressable, while f must be a StructField
// describing the field to retrieve. // describing the field to retrieve.
func retrieveUnexportedField(v reflect.Value, f reflect.StructField) reflect.Value { func retrieveUnexportedField(v reflect.Value, f reflect.StructField) reflect.Value {
return reflect.NewAt(f.Type, unsafe.Pointer(v.UnsafeAddr()+f.Offset)).Elem() // See https://github.com/google/go-cmp/issues/167 for discussion of the
// following expression.
return reflect.NewAt(f.Type, unsafe.Pointer(uintptr(unsafe.Pointer(v.UnsafeAddr()))+f.Offset)).Elem()
} }

View File

@ -225,8 +225,20 @@ func (validator) apply(s *state, vx, vy reflect.Value) {
// Unable to Interface implies unexported field without visibility access. // Unable to Interface implies unexported field without visibility access.
if !vx.CanInterface() || !vy.CanInterface() { if !vx.CanInterface() || !vy.CanInterface() {
const help = "consider using a custom Comparer; if you control the implementation of type, you can also consider AllowUnexported or cmpopts.IgnoreUnexported" const help = "consider using a custom Comparer; if you control the implementation of type, you can also consider using an Exporter, AllowUnexported, or cmpopts.IgnoreUnexported"
panic(fmt.Sprintf("cannot handle unexported field: %#v\n%s", s.curPath, help)) var name string
if t := s.curPath.Index(-2).Type(); t.Name() != "" {
// Named type with unexported fields.
name = fmt.Sprintf("%q.%v", t.PkgPath(), t.Name()) // e.g., "path/to/package".MyType
} else {
// Unnamed type with unexported fields. Derive PkgPath from field.
var pkgPath string
for i := 0; i < t.NumField() && pkgPath == ""; i++ {
pkgPath = t.Field(i).PkgPath
}
name = fmt.Sprintf("%q.(%v)", pkgPath, t.String()) // e.g., "path/to/package".(struct { a int })
}
panic(fmt.Sprintf("cannot handle unexported field at %#v:\n\t%v\n%s", s.curPath, name, help))
} }
panic("not reachable") panic("not reachable")
@ -360,9 +372,8 @@ func (cm comparer) String() string {
return fmt.Sprintf("Comparer(%s)", function.NameOf(cm.fnc)) return fmt.Sprintf("Comparer(%s)", function.NameOf(cm.fnc))
} }
// AllowUnexported returns an Option that forcibly allows operations on // Exporter returns an Option that specifies whether Equal is allowed to
// unexported fields in certain structs, which are specified by passing in a // introspect into the unexported fields of certain struct types.
// value of each struct type.
// //
// Users of this option must understand that comparing on unexported fields // Users of this option must understand that comparing on unexported fields
// from external packages is not safe since changes in the internal // from external packages is not safe since changes in the internal
@ -386,10 +397,24 @@ func (cm comparer) String() string {
// //
// In other cases, the cmpopts.IgnoreUnexported option can be used to ignore // In other cases, the cmpopts.IgnoreUnexported option can be used to ignore
// all unexported fields on specified struct types. // all unexported fields on specified struct types.
func AllowUnexported(types ...interface{}) Option { func Exporter(f func(reflect.Type) bool) Option {
if !supportAllowUnexported { if !supportExporters {
panic("AllowUnexported is not supported on purego builds, Google App Engine Standard, or GopherJS") panic("Exporter is not supported on purego builds")
} }
return exporter(f)
}
type exporter func(reflect.Type) bool
func (exporter) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption {
panic("not implemented")
}
// AllowUnexported returns an Options that allows Equal to forcibly introspect
// unexported fields of the specified struct types.
//
// See Exporter for the proper use of this option.
func AllowUnexported(types ...interface{}) Option {
m := make(map[reflect.Type]bool) m := make(map[reflect.Type]bool)
for _, typ := range types { for _, typ := range types {
t := reflect.TypeOf(typ) t := reflect.TypeOf(typ)
@ -398,13 +423,7 @@ func AllowUnexported(types ...interface{}) Option {
} }
m[t] = true m[t] = true
} }
return visibleStructs(m) return exporter(func(t reflect.Type) bool { return m[t] })
}
type visibleStructs map[reflect.Type]bool
func (visibleStructs) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption {
panic("not implemented")
} }
// Result represents the comparison result for a single node and // Result represents the comparison result for a single node and
@ -436,6 +455,11 @@ func (r Result) ByFunc() bool {
return r.flags&reportByFunc != 0 return r.flags&reportByFunc != 0
} }
// ByCycle reports whether a reference cycle was detected.
func (r Result) ByCycle() bool {
return r.flags&reportByCycle != 0
}
type resultFlags uint type resultFlags uint
const ( const (
@ -446,6 +470,7 @@ const (
reportByIgnore reportByIgnore
reportByMethod reportByMethod
reportByFunc reportByFunc
reportByCycle
) )
// Reporter is an Option that can be passed to Equal. When Equal traverses // Reporter is an Option that can be passed to Equal. When Equal traverses

View File

@ -10,6 +10,8 @@ import (
"strings" "strings"
"unicode" "unicode"
"unicode/utf8" "unicode/utf8"
"github.com/google/go-cmp/cmp/internal/value"
) )
// Path is a list of PathSteps describing the sequence of operations to get // Path is a list of PathSteps describing the sequence of operations to get
@ -41,7 +43,7 @@ type PathStep interface {
// In some cases, one or both may be invalid or have restrictions: // In some cases, one or both may be invalid or have restrictions:
// • For StructField, both are not interface-able if the current field // • For StructField, both are not interface-able if the current field
// is unexported and the struct type is not explicitly permitted by // is unexported and the struct type is not explicitly permitted by
// AllowUnexported to traverse unexported fields. // an Exporter to traverse unexported fields.
// • For SliceIndex, one may be invalid if an element is missing from // • For SliceIndex, one may be invalid if an element is missing from
// either the x or y slice. // either the x or y slice.
// • For MapIndex, one may be invalid if an entry is missing from // • For MapIndex, one may be invalid if an entry is missing from
@ -207,6 +209,7 @@ type SliceIndex struct{ *sliceIndex }
type sliceIndex struct { type sliceIndex struct {
pathStep pathStep
xkey, ykey int xkey, ykey int
isSlice bool // False for reflect.Array
} }
func (si SliceIndex) Type() reflect.Type { return si.typ } func (si SliceIndex) Type() reflect.Type { return si.typ }
@ -301,6 +304,72 @@ func (tf Transform) Func() reflect.Value { return tf.trans.fnc }
// The == operator can be used to detect the exact option used. // The == operator can be used to detect the exact option used.
func (tf Transform) Option() Option { return tf.trans } func (tf Transform) Option() Option { return tf.trans }
// pointerPath represents a dual-stack of pointers encountered when
// recursively traversing the x and y values. This data structure supports
// detection of cycles and determining whether the cycles are equal.
// In Go, cycles can occur via pointers, slices, and maps.
//
// The pointerPath uses a map to represent a stack; where descension into a
// pointer pushes the address onto the stack, and ascension from a pointer
// pops the address from the stack. Thus, when traversing into a pointer from
// reflect.Ptr, reflect.Slice element, or reflect.Map, we can detect cycles
// by checking whether the pointer has already been visited. The cycle detection
// uses a seperate stack for the x and y values.
//
// If a cycle is detected we need to determine whether the two pointers
// should be considered equal. The definition of equality chosen by Equal
// requires two graphs to have the same structure. To determine this, both the
// x and y values must have a cycle where the previous pointers were also
// encountered together as a pair.
//
// Semantically, this is equivalent to augmenting Indirect, SliceIndex, and
// MapIndex with pointer information for the x and y values.
// Suppose px and py are two pointers to compare, we then search the
// Path for whether px was ever encountered in the Path history of x, and
// similarly so with py. If either side has a cycle, the comparison is only
// equal if both px and py have a cycle resulting from the same PathStep.
//
// Using a map as a stack is more performant as we can perform cycle detection
// in O(1) instead of O(N) where N is len(Path).
type pointerPath struct {
// mx is keyed by x pointers, where the value is the associated y pointer.
mx map[value.Pointer]value.Pointer
// my is keyed by y pointers, where the value is the associated x pointer.
my map[value.Pointer]value.Pointer
}
func (p *pointerPath) Init() {
p.mx = make(map[value.Pointer]value.Pointer)
p.my = make(map[value.Pointer]value.Pointer)
}
// Push indicates intent to descend into pointers vx and vy where
// visited reports whether either has been seen before. If visited before,
// equal reports whether both pointers were encountered together.
// Pop must be called if and only if the pointers were never visited.
//
// The pointers vx and vy must be a reflect.Ptr, reflect.Slice, or reflect.Map
// and be non-nil.
func (p pointerPath) Push(vx, vy reflect.Value) (equal, visited bool) {
px := value.PointerOf(vx)
py := value.PointerOf(vy)
_, ok1 := p.mx[px]
_, ok2 := p.my[py]
if ok1 || ok2 {
equal = p.mx[px] == py && p.my[py] == px // Pointers paired together
return equal, true
}
p.mx[px] = py
p.my[py] = px
return false, false
}
// Pop ascends from pointers vx and vy.
func (p pointerPath) Pop(vx, vy reflect.Value) {
delete(p.mx, value.PointerOf(vx))
delete(p.my, value.PointerOf(vy))
}
// isExported reports whether the identifier is exported. // isExported reports whether the identifier is exported.
func isExported(id string) bool { func isExported(id string) bool {
r, _ := utf8.DecodeRuneInString(id) r, _ := utf8.DecodeRuneInString(id)

3
vendor/modules.txt vendored
View File

@ -1,4 +1,5 @@
# github.com/google/go-cmp v0.3.1 # github.com/google/go-cmp v0.4.0
## explicit
github.com/google/go-cmp/cmp github.com/google/go-cmp/cmp
github.com/google/go-cmp/cmp/internal/diff github.com/google/go-cmp/cmp/internal/diff
github.com/google/go-cmp/cmp/internal/flags github.com/google/go-cmp/cmp/internal/flags