diff --git a/VERSION b/VERSION index a24809a..50f402a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.48 +0.0.49 diff --git a/internal/reflect/reflect.go b/internal/reflect/reflect.go index d1ba01b..b7956ed 100644 --- a/internal/reflect/reflect.go +++ b/internal/reflect/reflect.go @@ -12,12 +12,66 @@ type ( Value = reflect.Value // Type alias for `reflect.Type`. Type = reflect.Type + // Kind alias for `reflect.Kind`. + Kind = reflect.Kind ) // Aliases for constant values. const ( + // Invalid alias for `reflect.Invalid`. + Invalid = reflect.Invalid + // Bool alias for `reflect.Bool`. + Bool = reflect.Bool + // Int alias for `reflect.Int`. + Int = reflect.Int + // Int8 alias for `reflect.Int8`. + Int8 = reflect.Int8 + // Int16 alias for `reflect.Int16`. + Int16 = reflect.Int16 + // Int32 alias for `reflect.Int32`. + Int32 = reflect.Int32 + // Int64 alias for `reflect.Int64`. + Int64 = reflect.Int64 + // Uint alias for `reflect.Uint`. + Uint = reflect.Uint + // Uint8 alias for `reflect.Uint8`. + Uint8 = reflect.Uint8 + // Uint16 alias for `reflect.Uint16`. + Uint16 = reflect.Uint16 + // Uint32 alias for `reflect.Uint32`. + Uint32 = reflect.Uint32 + // Uint64 alias for `reflect.Uint64`. + Uint64 = reflect.Uint64 + // Uintptr alias for `reflect.Uintptr`. + Uintptr = reflect.Uintptr + // Float32 alias for `reflect.Float32`. + Float32 = reflect.Float32 + // Float64 alias for `reflect.Float64`. + Float64 = reflect.Float64 + // Complex64 alias for `reflect.Complex64`. + Complex64 = reflect.Complex64 + // Complex128 alias for `reflect.Complex128`. + Complex128 = reflect.Complex128 + // Array alias for `reflect.Array`. + Array = reflect.Array + // Chan alias for `reflect.Chan`. + Chan = reflect.Chan // Func alias for `reflect.Func`. Func = reflect.Func + // Interface alias for `reflect.Interface`. + Interface = reflect.Interface + // Map alias for `reflect.Map`. + Map = reflect.Map + // Pointer alias for `reflect.Pointer`. + Pointer = reflect.Pointer + // Slice alias for `reflect.Slice`. + Slice = reflect.Slice + // String alias for `reflect.String`. + String = reflect.String + // Struct alias for `reflect.Struct`. + Struct = reflect.Struct + // UnsafePointer alias for `reflect.UnsafePointer`. + UnsafePointer = reflect.UnsafePointer ) // Aliases for function values. diff --git a/mock/matcher.go b/mock/matcher.go index d67ba97..156e853 100644 --- a/mock/matcher.go +++ b/mock/matcher.go @@ -14,84 +14,43 @@ const ( // DefaultContext provides the default number of context lines to show // before and after the changes in a diff. DefaultContext = 3 - // DefaultSkippingSize is the maximum size of the string representation of a value - // presented in the output. + // DefaultSkippingSize is the maximum size of the string representation of + // a value presented in the output. DefaultSkippingSize = 50 // bufio.MaxScanTokenSize - 100 - // DefaultSkippingTail is the size of the tail after the skipped value part. + // DefaultSkippingTail is the size of the tail after the skipped value + // part. DefaultSkippingTail = 5 ) -// MatcherConfig holds configuration settings for matchers. -type MatcherConfig struct { - // Size of string representation before skipping part of it in output. - skippingSize int - // Tail size after the skipped part string representation. - skippingTail int - - // Internal diff lib settings. - dlib *difflib.UnifiedDiff - // Internal spew config settings. - spew *spew.ConfigState - // Internal spew config settings (disabled methods for time). - spewTime *spew.ConfigState -} - -// NewMatcherConfig creates a new matcher configuration instance with default -// values. -func NewMatcherConfig() *MatcherConfig { - return &MatcherConfig{ - skippingSize: DefaultSkippingSize, - skippingTail: DefaultSkippingTail, - dlib: &difflib.UnifiedDiff{ - Context: DefaultContext, - FromFile: "Want", FromDate: "", - ToFile: "Got", ToDate: "", - }, - spew: &spew.ConfigState{ - Indent: " ", - DisableMethods: true, - DisablePointerAddresses: true, - DisableCapacities: true, - SortKeys: true, - }, - spewTime: &spew.ConfigState{ - Indent: " ", - DisablePointerAddresses: true, - DisableCapacities: true, - SortKeys: true, - }, - } -} - -// Context specifies the number of context lines to show before and after -// changes in a diff. The default, 3, means no context lines. +// Context sets the number of context lines to show before and after changes in +// a diff. The default, 3, means no context lines. func Context(context int) ConfigFunc { return func(mocks *Mocks) { - mocks.matcher.dlib.Context = context + mocks.matcher.Context(context) } } -// FromFile specifies the label to use for the "from" side of the diff. -// Default is "Want". +// FromFile sets the label to use for the "from" side of the diff. Default is +// `Want`. func FromFile(file string) ConfigFunc { return func(mocks *Mocks) { - mocks.matcher.dlib.FromFile = file + mocks.matcher.FromFile(file) } } -// FromDate specifies the label to use for the "from" date of the diff. -// Default is empty. +// FromDate sets the label to use for the "from" date of the diff. Default is +// empty. func FromDate(date string) ConfigFunc { return func(mocks *Mocks) { - mocks.matcher.dlib.FromDate = date + mocks.matcher.FromDate(date) } } -// ToFile specifies the label to use for the "to" side of the diff. Default is -// "Got". +// ToFile sets the label to use for the "to" side of the diff. Default is +// `Got`. func ToFile(file string) ConfigFunc { return func(mocks *Mocks) { - mocks.matcher.dlib.ToFile = file + mocks.matcher.ToFile(file) } } @@ -99,175 +58,273 @@ func ToFile(file string) ConfigFunc { // empty. func ToDate(date string) ConfigFunc { return func(mocks *Mocks) { - mocks.matcher.dlib.ToDate = date + mocks.matcher.ToDate(date) } } -// Indent specifies the string to use for each indentation level. The global -// config instance that all top-level functions use set this to a single space -// by default. If you would like more indentation, you might set this to a tab -// with "\t" or perhaps two spaces with " ". +// Indent sets the string to use for each indentation level. The global config +// instance that all top-level functions use set this to a single space by +// default. If you would like more indentation, you might set this to a tab +// with `\t` or perhaps two spaces with ` `. func Indent(indent string) ConfigFunc { return func(mocks *Mocks) { - mocks.matcher.spewTime.Indent = indent - mocks.matcher.spew.Indent = indent + mocks.matcher.Indent(indent) } } -// MaxDepth controls the maximum number of levels to descend into nested data +// MaxDepth sets the maximum number of levels to descend into nested data // structures. The default 0 means there is no limit. Circular data structures // are properly detected, so it is not necessary to set this value unless you // specifically want to limit deeply nested structures. func MaxDepth(maxDepth int) ConfigFunc { return func(mocks *Mocks) { - mocks.matcher.spewTime.MaxDepth = maxDepth - mocks.matcher.spew.MaxDepth = maxDepth + mocks.matcher.MaxDepth(maxDepth) } } -// DisableMethods specifies whether or not error and Stringer interfaces are +// DisableMethods sets whether or not error and `Stringer` interfaces are // invoked for types that implement them. Default is true, meaning that these // methods will not be invoked. func DisableMethods(disable bool) ConfigFunc { return func(mocks *Mocks) { - mocks.matcher.spewTime.DisableMethods = true - mocks.matcher.spew.DisableMethods = disable + mocks.matcher.DisableMethods(disable) } } -// DisablePointerMethods specifies whether or not to check for and invoke error -// and Stringer interfaces on types which only accept a pointer receiver when -// the current type is not a pointer. +// DisablePointerMethods sets whether or not to check for and invoke error and +// `Stringer` interfaces on types which only accept a pointer receiver when the +// current type is not a pointer. // // *Note:* This might be an unsafe action since calling one a pointer receiver // could technically mutate the value. In practice, types which choose to -// satisfy an error or Stringer interface with a pointer receiver should not +// satisfy an error or `Stringer` interface with a pointer receiver should not // mutate their state inside these methods. As a result, this option relies on // access to the unsafe package, so it will not have any effect when running in // environments without access to the unsafe package such as Google App Engine // or with the "safe" build tag specified. func DisablePointerMethods(disable bool) ConfigFunc { return func(mocks *Mocks) { - mocks.matcher.spewTime.DisablePointerMethods = disable - mocks.matcher.spew.DisablePointerMethods = disable + mocks.matcher.DisablePointerMethods(disable) } } -// DisablePointerAddresses specifies whether to disable the printing of pointer +// DisablePointerAddresses sets whether to disable the printing of pointer // addresses. This is useful when diffing data structures in tests. func DisablePointerAddresses(disable bool) ConfigFunc { return func(mocks *Mocks) { - mocks.matcher.spewTime.DisablePointerAddresses = disable - mocks.matcher.spew.DisablePointerAddresses = disable + mocks.matcher.DisablePointerAddresses(disable) } } -// DisableCapacities specifies whether to disable the printing of capacities -// for arrays, slices, maps and channels. This is useful when diffing data +// DisableCapacities sets whether to disable the printing of capacities for +// arrays, slices, maps and channels. This is useful when diffing data // structures in tests. func DisableCapacities(disable bool) ConfigFunc { return func(mocks *Mocks) { - mocks.matcher.spewTime.DisableCapacities = disable - mocks.matcher.spew.DisableCapacities = disable + mocks.matcher.DisableCapacities(disable) } } -// ContinueOnMethod specifies whether or not recursion should continue once a -// custom error or Stringer interface is invoked. The default, false, means it -// will print the results of invoking the custom error or Stringer interface -// and return immediately instead of continuing to recurse into the internals -// of the data type. +// ContinueOnMethod sets whether or not recursion should continue once a custom +// error or `Stringer` interface is invoked. The default, false, means it will +// print the results of invoking the custom error or `Stringer` interface and +// return immediately instead of continuing to recurse into the internals of +// the data type. // // *Note:* This flag does not have any effect if method invocation is disabled // via the DisableMethods or DisablePointerMethods options. func ContinueOnMethod(enable bool) ConfigFunc { return func(mocks *Mocks) { - mocks.matcher.spewTime.ContinueOnMethod = enable - mocks.matcher.spew.ContinueOnMethod = enable + mocks.matcher.ContinueOnMethod(enable) } } -// SortKeys specifies map keys should be sorted before being printed. Use this -// to have a more deterministic, diffable output. Note that only native types -// (bool, int, uint, floats, uintptr and string) and types that support the -// error or Stringer interfaces (if methods are enabled) are supported, with -// other types sorted according to the reflect.Value.String() output which +// SortKeys sets whether map keys should be sorted before being printed. Use +// this to have a more deterministic, diffable output. Note that only native +// types (bool, int, uint, floats, uintptr and string) and types that support +// the error or `Stringer` interfaces (if methods are enabled) are supported, +// with other types sorted according to the reflect.Value.String() output which // guarantees display stability. func SortKeys(sort bool) ConfigFunc { return func(mocks *Mocks) { - mocks.matcher.spewTime.SortKeys = sort - mocks.matcher.spew.SortKeys = sort + mocks.matcher.SortKeys(sort) } } -// SpewKeys specifies that, as a last resort attempt, map keys should be spewed -// to strings and sorted by those strings. This is only considered if SortKeys -// is true. +// SpewKeys sets that, as a last resort attempt, map keys should be spewed to +// strings and sorted by those strings. This is only considered if keys are +// sorted (see `SortKeys`). func SpewKeys(spew bool) ConfigFunc { return func(mocks *Mocks) { - mocks.matcher.spewTime.SpewKeys = spew - mocks.matcher.spew.SpewKeys = spew + mocks.matcher.SpewKeys(spew) } } -// Equal is an improved `gomock.Matcher` that matches via `reflect.DeepEqual` -// showing detailed diff when there is a mismatch. -type Equal struct { - config *MatcherConfig - want any - diff string +// DiffConfig holds configuration settings for matchers. +type DiffConfig struct { + // Size of string representation before skipping part of it in output. + skippingSize int + // Tail size after the skipped part string representation. + skippingTail int + + // Internal diff lib settings. + dlib *difflib.UnifiedDiff + // Internal spew config settings. + spew *spew.ConfigState + // Internal spew config settings (disabled methods for time). + spewTime *spew.ConfigState } -// Equal returns an improved equals matcher showing a detailed diff when there -// is a mismatch in the expected and actual values. -func (mocks *Mocks) Equal(want any) *Equal { - return &Equal{ - config: mocks.matcher, - want: want, - diff: "", +// NewDiffConfig creates a new matcher configuration instance with default +// values. +func NewDiffConfig() *DiffConfig { + return &DiffConfig{ + skippingSize: DefaultSkippingSize, + skippingTail: DefaultSkippingTail, + dlib: &difflib.UnifiedDiff{ + Context: DefaultContext, + FromFile: "Want", FromDate: "", + ToFile: "Got", ToDate: "", + }, + spew: &spew.ConfigState{ + Indent: " ", + DisableMethods: true, + DisablePointerAddresses: true, + DisableCapacities: true, + SortKeys: true, + }, + spewTime: &spew.ConfigState{ + Indent: " ", + DisablePointerAddresses: true, + DisableCapacities: true, + SortKeys: true, + }, } } -// Matches returns whether the actual value is equal to the expected value. -func (eq *Equal) Matches(got any) bool { - if !gomock.Eq(eq.want).Matches(got) { - eq.diff = eq.Diff(eq.want, got) - return false - } - return true +// Context sets the number of context lines to show before and after changes in +// a diff. The default, 3, means no context lines. +func (c *DiffConfig) Context(context int) { + c.dlib.Context = context } -// Got returns a string representation of the actual value. -func (eq *Equal) Got(got any) string { - return fmt.Sprintf("%T(%s)", got, eq.skip(got)) +// FromFile sets the label to use for the "from" side of the diff. Default is +// `Want`. +func (c *DiffConfig) FromFile(file string) { + c.dlib.FromFile = file } -// String returns a string representation of the expected value along with the -// diff between expected and actual values as long as both are of the same type -// and are a struct, map, slice, array or string. Otherwise the diff is hidden. -func (eq *Equal) String() string { - if eq.diff != "" { - return fmt.Sprintf("%T(%s)\nDiff (-want, +got):\n%s", - eq.want, eq.skip(eq.want), eq.diff) - } - return fmt.Sprintf("%T(%s)", eq.want, eq.skip(eq.want)) +// FromDate sets the label to use for the "from" date of the diff. Default is +// empty. +func (c *DiffConfig) FromDate(date string) { + c.dlib.FromDate = date } -// skip returns a truncated string representation of the given value. -func (eq *Equal) skip(v any) string { - config, value := eq.config, fmt.Sprintf("%#v", v) - if len(value) > config.skippingSize { - return value[0:config.skippingSize-config.skippingTail] + - "<... skipped ...>" + - value[len(value)-config.skippingTail:] - } - return value +// ToFile sets the label to use for the "to" side of the diff. Default is +// `Got`. +func (c *DiffConfig) ToFile(file string) { + c.dlib.ToFile = file +} + +// ToDate sets the label to use for the "to" date of the diff. Default is +// empty. +func (c *DiffConfig) ToDate(date string) { + c.dlib.ToDate = date +} + +// Indent sets the string to use for each indentation level. The global config +// instance that all top-level functions use set this to a single space by +// default. If you would like more indentation, you might set this to a tab +// with `\t` or perhaps two spaces with ` `. +func (c *DiffConfig) Indent(indent string) { + c.spewTime.Indent = indent + c.spew.Indent = indent +} + +// MaxDepth sets the maximum number of levels to descend into nested data +// structures. The default 0 means there is no limit. Circular data structures +// are properly detected, so it is not necessary to set this value unless you +// specifically want to limit deeply nested structures. +func (c *DiffConfig) MaxDepth(maxDepth int) { + c.spewTime.MaxDepth = maxDepth + c.spew.MaxDepth = maxDepth +} + +// DisableMethods sets whether or not error and `Stringer` interfaces are +// invoked for types that implement them. Default is true, meaning that these +// methods will not be invoked. +func (c *DiffConfig) DisableMethods(disable bool) { + c.spewTime.DisableMethods = true + c.spew.DisableMethods = disable +} + +// DisablePointerMethods sets whether or not to check for and invoke error and +// `Stringer` interfaces on types which only accept a pointer receiver when the +// current type is not a pointer. +// +// *Note:* This might be an unsafe action since calling one a pointer receiver +// could technically mutate the value. In practice, types which choose to +// satisfy an error or `Stringer` interface with a pointer receiver should not +// mutate their state inside these methods. As a result, this option relies on +// access to the unsafe package, so it will not have any effect when running in +// environments without access to the unsafe package such as Google App Engine +// or with the "safe" build tag specified. +func (c *DiffConfig) DisablePointerMethods(disable bool) { + c.spewTime.DisablePointerMethods = disable + c.spew.DisablePointerMethods = disable +} + +// DisablePointerAddresses sets whether to disable the printing of pointer +// addresses. This is useful when diffing data structures in tests. +func (c *DiffConfig) DisablePointerAddresses(disable bool) { + c.spewTime.DisablePointerAddresses = disable + c.spew.DisablePointerAddresses = disable +} + +// DisableCapacities sets whether to disable the printing of capacities +// for arrays, slices, maps and channels. This is useful when diffing data +// structures in tests. +func (c *DiffConfig) DisableCapacities(disable bool) { + c.spewTime.DisableCapacities = disable + c.spew.DisableCapacities = disable +} + +// ContinueOnMethod sets whether or not recursion should continue once a custom +// error or `Stringer` interface is invoked. The default, false, means it will +// print the results of invoking the custom error or `Stringer` interface and +// return immediately instead of continuing to recurse into the internals of +// the data type. +// +// *Note:* This flag does not have any effect if method invocation is disabled +// via the DisableMethods or DisablePointerMethods options. +func (c *DiffConfig) ContinueOnMethod(enable bool) { + c.spewTime.ContinueOnMethod = enable + c.spew.ContinueOnMethod = enable +} + +// SortKeys sets map keys should be sorted before being printed. Use this to +// have a more deterministic, diffable output. Note that only native types +// (bool, int, uint, floats, uintptr and string) and types that support the +// error or `Stringer` interfaces (if methods are enabled) are supported, with +// other types sorted according to the reflect.Value.String() output which +// guarantees display stability. +func (c *DiffConfig) SortKeys(sort bool) { + c.spewTime.SortKeys = sort + c.spew.SortKeys = sort +} + +// SpewKeys sets that, as a last resort attempt, map keys should be spewed +// to strings and sorted by those strings. This is only considered if keys are +// sorted (see `SortKeys`). +func (c *DiffConfig) SpewKeys(spew bool) { + c.spewTime.SpewKeys = spew + c.spew.SpewKeys = spew } // Diff returns a diff of the expected value and the actual value as long as // both are of the same type and are a struct, map, slice, array or string. // Otherwise it returns an empty string. -func (eq *Equal) Diff(want, got any) string { +func (c *DiffConfig) Diff(want, got any) string { if want == nil || got == nil { return "" } @@ -291,26 +348,78 @@ func (eq *Equal) Diff(want, got any) string { var estr, astr string - config := eq.config switch etype { case reflect.TypeOf(""): estr = reflect.ValueOf(want).String() astr = reflect.ValueOf(got).String() case reflect.TypeOf(time.Time{}): - estr = config.spewTime.Sdump(want) - astr = config.spewTime.Sdump(got) + estr = c.spewTime.Sdump(want) + astr = c.spewTime.Sdump(got) default: - estr = config.spew.Sdump(want) - astr = config.spew.Sdump(got) + estr = c.spew.Sdump(want) + astr = c.spew.Sdump(got) } - dlib := config.dlib diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ A: difflib.SplitLines(estr), B: difflib.SplitLines(astr), - FromFile: dlib.FromFile, FromDate: dlib.FromDate, - ToFile: dlib.ToFile, ToDate: dlib.ToDate, - Context: dlib.Context, + FromFile: c.dlib.FromFile, FromDate: c.dlib.FromDate, + ToFile: c.dlib.ToFile, ToDate: c.dlib.ToDate, + Context: c.dlib.Context, }) return diff } + +// Equal is an improved `gomock.Matcher` that matches via `reflect.DeepEqual` +// showing detailed diff when there is a mismatch. +type Equal struct { + config *DiffConfig + want any + diff string +} + +// Equal returns an improved equals matcher showing a detailed diff when there +// is a mismatch in the expected and actual values. +func (mocks *Mocks) Equal(want any) *Equal { + return &Equal{ + config: mocks.matcher, + want: want, + diff: "", + } +} + +// Matches returns whether the actual value is equal to the expected value. +func (eq *Equal) Matches(got any) bool { + if !gomock.Eq(eq.want).Matches(got) { + eq.diff = eq.config.Diff(eq.want, got) + return false + } + return true +} + +// Got returns a string representation of the actual value. +func (eq *Equal) Got(got any) string { + return fmt.Sprintf("%T(%s)", got, eq.skip(got)) +} + +// String returns a string representation of the expected value along with the +// diff between expected and actual values as long as both are of the same type +// and are a struct, map, slice, array or string. Otherwise the diff is hidden. +func (eq *Equal) String() string { + if eq.diff != "" { + return fmt.Sprintf("%T(%s)\nDiff (-want, +got):\n%s", + eq.want, eq.skip(eq.want), eq.diff) + } + return fmt.Sprintf("%T(%s)", eq.want, eq.skip(eq.want)) +} + +// skip returns a truncated string representation of the given value. +func (eq *Equal) skip(v any) string { + config, value := eq.config, fmt.Sprintf("%#v", v) + if len(value) > config.skippingSize { + return value[0:config.skippingSize-config.skippingTail] + + "<... skipped ...>" + + value[len(value)-config.skippingTail:] + } + return value +} diff --git a/mock/matcher_test.go b/mock/matcher_test.go index 51b0a3d..04e2f43 100644 --- a/mock/matcher_test.go +++ b/mock/matcher_test.go @@ -21,240 +21,29 @@ const ( nameMatcher = "matcher" ) -func GetMatcherConfigAccessor( - mocks *mock.Mocks, -) reflect.Builder[*mock.MatcherConfig] { - return reflect.NewAccessor(reflect.NewAccessor(mocks). - Get(nameMatcher).(*mock.MatcherConfig)) -} - -type ConfigParams struct { - config mock.ConfigFunc - access func(mocks *mock.Mocks) any - expect any -} - -var configTestCases = map[string]ConfigParams{ - // Diff config options. - "diff context": { - config: mock.Context(7), - access: func(mocks *mock.Mocks) any { - return GetMatcherConfigAccessor(mocks). - Get(nameDlib).(*difflib.UnifiedDiff).Context - }, - expect: 7, - }, - "diff from-file": { - config: mock.FromFile("expect"), - access: func(mocks *mock.Mocks) any { - return GetMatcherConfigAccessor(mocks). - Get(nameDlib).(*difflib.UnifiedDiff).FromFile - }, - expect: "expect", - }, - "diff from-date": { - config: mock.FromDate("2025-10-27"), - access: func(mocks *mock.Mocks) any { - return GetMatcherConfigAccessor(mocks). - Get(nameDlib).(*difflib.UnifiedDiff).FromDate - }, - expect: "2025-10-27", - }, - "diff to-file": { - config: mock.ToFile("actual.txt"), - access: func(mocks *mock.Mocks) any { - return GetMatcherConfigAccessor(mocks). - Get(nameDlib).(*difflib.UnifiedDiff).ToFile - }, - expect: "actual.txt", - }, - "diff to-date": { - config: mock.ToDate("2025-10-28"), - access: func(mocks *mock.Mocks) any { - return GetMatcherConfigAccessor(mocks). - Get(nameDlib).(*difflib.UnifiedDiff).ToDate - }, - expect: "2025-10-28", - }, - - // Spew config options. - "spew indent": { - config: mock.Indent("\t"), - access: func(mocks *mock.Mocks) any { - return GetMatcherConfigAccessor(mocks). - Get(nameSpew).(*spew.ConfigState).Indent - }, - expect: "\t", - }, - "spew max-depth": { - config: mock.MaxDepth(5), - access: func(mocks *mock.Mocks) any { - return GetMatcherConfigAccessor(mocks). - Get(nameSpew).(*spew.ConfigState).MaxDepth - }, - expect: 5, - }, - "spew disable-methods": { - config: mock.DisableMethods(false), - access: func(mocks *mock.Mocks) any { - return GetMatcherConfigAccessor(mocks). - Get(nameSpew).(*spew.ConfigState).DisableMethods - }, - expect: false, - }, - "spew disable-pointer-methods": { - config: mock.DisablePointerMethods(true), - access: func(mocks *mock.Mocks) any { - return GetMatcherConfigAccessor(mocks). - Get(nameSpew).(*spew.ConfigState).DisablePointerMethods - }, - expect: true, - }, - "spew disable-pointer-addresses": { - config: mock.DisablePointerAddresses(false), - access: func(mocks *mock.Mocks) any { - return GetMatcherConfigAccessor(mocks). - Get(nameSpew).(*spew.ConfigState).DisablePointerAddresses - }, - expect: false, - }, - "spew disable-capacities": { - config: mock.DisableCapacities(false), - access: func(mocks *mock.Mocks) any { - return GetMatcherConfigAccessor(mocks). - Get(nameSpew).(*spew.ConfigState).DisableCapacities - }, - expect: false, - }, - "spew continue-on-method": { - config: mock.ContinueOnMethod(true), - access: func(mocks *mock.Mocks) any { - return GetMatcherConfigAccessor(mocks). - Get(nameSpew).(*spew.ConfigState).ContinueOnMethod - }, - expect: true, - }, - "spew sort-keys": { - config: mock.SortKeys(false), - access: func(mocks *mock.Mocks) any { - return GetMatcherConfigAccessor(mocks). - Get(nameSpew).(*spew.ConfigState).SortKeys - }, - expect: false, - }, - "spew spew-keys": { - config: mock.SpewKeys(true), - access: func(mocks *mock.Mocks) any { - return GetMatcherConfigAccessor(mocks). - Get(nameSpew).(*spew.ConfigState).SpewKeys - }, - expect: true, - }, - - // Spew config options. - "spew-time indent": { - config: mock.Indent("\t"), - access: func(mocks *mock.Mocks) any { - return GetMatcherConfigAccessor(mocks). - Get(nameSpewTime).(*spew.ConfigState).Indent - }, - expect: "\t", - }, - "spew-time max-depth": { - config: mock.MaxDepth(5), - access: func(mocks *mock.Mocks) any { - return GetMatcherConfigAccessor(mocks). - Get(nameSpewTime).(*spew.ConfigState).MaxDepth - }, - expect: 5, - }, - "spew-time disable-methods": { - config: mock.DisableMethods(false), - access: func(mocks *mock.Mocks) any { - return GetMatcherConfigAccessor(mocks). - Get(nameSpewTime).(*spew.ConfigState).DisableMethods - }, - expect: true, // exception: default is true for spewtime. - }, - "spew-time disable-pointer-methods": { - config: mock.DisablePointerMethods(true), - access: func(mocks *mock.Mocks) any { - return GetMatcherConfigAccessor(mocks). - Get(nameSpewTime).(*spew.ConfigState).DisablePointerMethods - }, - expect: true, - }, - "spew-time disable-pointer-addresses": { - config: mock.DisablePointerAddresses(false), - access: func(mocks *mock.Mocks) any { - return GetMatcherConfigAccessor(mocks). - Get(nameSpewTime).(*spew.ConfigState).DisablePointerAddresses - }, - expect: false, - }, - "spew-time disable-capacities": { - config: mock.DisableCapacities(false), - access: func(mocks *mock.Mocks) any { - return GetMatcherConfigAccessor(mocks). - Get(nameSpewTime).(*spew.ConfigState).DisableCapacities - }, - expect: false, - }, - "spew-time continue-on-method": { - config: mock.ContinueOnMethod(true), - access: func(mocks *mock.Mocks) any { - return GetMatcherConfigAccessor(mocks). - Get(nameSpewTime).(*spew.ConfigState).ContinueOnMethod - }, - expect: true, - }, - "spew-time sort-keys": { - config: mock.SortKeys(false), - access: func(mocks *mock.Mocks) any { - return GetMatcherConfigAccessor(mocks). - Get(nameSpewTime).(*spew.ConfigState).SortKeys - }, - expect: false, - }, - "spew-time spew-keys": { - config: mock.SpewKeys(true), - access: func(mocks *mock.Mocks) any { - return GetMatcherConfigAccessor(mocks). - Get(nameSpewTime).(*spew.ConfigState).SpewKeys - }, - expect: true, - }, +// diff creates the complete expected diff output in unified diff format. It +// takes a hunk header (e.g., "-1 +1" or "-1,3 +1,3") and the diff content, and +// constructs the full diff with standard headers and trailing space+newline. +func diff(hunk, content string) string { + return "--- Want\n+++ Got\n@@ -" + hunk + " @@\n" + content } -func TestMatcherConfig(t *testing.T) { - test.Map(t, configTestCases). - Run(func(t test.Test, param ConfigParams) { - // Given - mocks := mock.NewMocks(t) - require.NotEqual(t, param.expect, param.access(mocks)) - - // When - mocks.Config(param.config) - - // Then - assert.Equal(t, param.expect, param.access(mocks)) - }) +// GetDiffConfigAccessor returns a builder to access the matcher config of +// the given mocks. +func GetDiffConfigAccessor( + mocks *mock.Mocks, +) reflect.Builder[*mock.DiffConfig] { + return reflect.NewAccessor(reflect.NewAccessor(mocks). + Get(nameMatcher).(*mock.DiffConfig)) } -type EqualDiffParams struct { +type DiffParams struct { want any got any diff string } -// diff creates the complete expected diff output in unified diff format. It -// takes a hunk header (e.g., "-1 +1" or "-1,3 +1,3") and the diff content, and -// constructs the full diff with standard headers and trailing space+newline. -func diff(hunk, content string) string { - return "--- Want\n+++ Got\n@@ -" + hunk + " @@\n" + content -} - -var equalDiffTestCases = map[string]EqualDiffParams{ +var diffTestCases = map[string]DiffParams{ // Nil cases. "nil want": { want: nil, @@ -453,21 +242,233 @@ var equalDiffTestCases = map[string]EqualDiffParams{ }, } -func TestEqualDiff(t *testing.T) { - test.Map(t, equalDiffTestCases). - Run(func(t test.Test, param EqualDiffParams) { +func TestDiff(t *testing.T) { + test.Map(t, diffTestCases). + Run(func(t test.Test, param DiffParams) { // Given - mocks := mock.NewMocks(t) - matcher := mocks.Equal(param.want) + config := mock.NewDiffConfig() // When - diff := matcher.Diff(param.want, param.got) + diff := config.Diff(param.want, param.got) // Then assert.Equal(t, param.diff, diff) }) } +type ConfigParams struct { + config mock.ConfigFunc + access func(mocks *mock.Mocks) any + expect any +} + +var configTestCases = map[string]ConfigParams{ + // Diff config options. + "diff context": { + config: mock.Context(7), + access: func(mocks *mock.Mocks) any { + return GetDiffConfigAccessor(mocks). + Get(nameDlib).(*difflib.UnifiedDiff).Context + }, + expect: 7, + }, + "diff from-file": { + config: mock.FromFile("expect"), + access: func(mocks *mock.Mocks) any { + return GetDiffConfigAccessor(mocks). + Get(nameDlib).(*difflib.UnifiedDiff).FromFile + }, + expect: "expect", + }, + "diff from-date": { + config: mock.FromDate("2025-10-27"), + access: func(mocks *mock.Mocks) any { + return GetDiffConfigAccessor(mocks). + Get(nameDlib).(*difflib.UnifiedDiff).FromDate + }, + expect: "2025-10-27", + }, + "diff to-file": { + config: mock.ToFile("actual.txt"), + access: func(mocks *mock.Mocks) any { + return GetDiffConfigAccessor(mocks). + Get(nameDlib).(*difflib.UnifiedDiff).ToFile + }, + expect: "actual.txt", + }, + "diff to-date": { + config: mock.ToDate("2025-10-28"), + access: func(mocks *mock.Mocks) any { + return GetDiffConfigAccessor(mocks). + Get(nameDlib).(*difflib.UnifiedDiff).ToDate + }, + expect: "2025-10-28", + }, + + // Spew config options. + "spew indent": { + config: mock.Indent("\t"), + access: func(mocks *mock.Mocks) any { + return GetDiffConfigAccessor(mocks). + Get(nameSpew).(*spew.ConfigState).Indent + }, + expect: "\t", + }, + "spew max-depth": { + config: mock.MaxDepth(5), + access: func(mocks *mock.Mocks) any { + return GetDiffConfigAccessor(mocks). + Get(nameSpew).(*spew.ConfigState).MaxDepth + }, + expect: 5, + }, + "spew disable-methods": { + config: mock.DisableMethods(false), + access: func(mocks *mock.Mocks) any { + return GetDiffConfigAccessor(mocks). + Get(nameSpew).(*spew.ConfigState).DisableMethods + }, + expect: false, + }, + "spew disable-pointer-methods": { + config: mock.DisablePointerMethods(true), + access: func(mocks *mock.Mocks) any { + return GetDiffConfigAccessor(mocks). + Get(nameSpew).(*spew.ConfigState).DisablePointerMethods + }, + expect: true, + }, + "spew disable-pointer-addresses": { + config: mock.DisablePointerAddresses(false), + access: func(mocks *mock.Mocks) any { + return GetDiffConfigAccessor(mocks). + Get(nameSpew).(*spew.ConfigState).DisablePointerAddresses + }, + expect: false, + }, + "spew disable-capacities": { + config: mock.DisableCapacities(false), + access: func(mocks *mock.Mocks) any { + return GetDiffConfigAccessor(mocks). + Get(nameSpew).(*spew.ConfigState).DisableCapacities + }, + expect: false, + }, + "spew continue-on-method": { + config: mock.ContinueOnMethod(true), + access: func(mocks *mock.Mocks) any { + return GetDiffConfigAccessor(mocks). + Get(nameSpew).(*spew.ConfigState).ContinueOnMethod + }, + expect: true, + }, + "spew sort-keys": { + config: mock.SortKeys(false), + access: func(mocks *mock.Mocks) any { + return GetDiffConfigAccessor(mocks). + Get(nameSpew).(*spew.ConfigState).SortKeys + }, + expect: false, + }, + "spew spew-keys": { + config: mock.SpewKeys(true), + access: func(mocks *mock.Mocks) any { + return GetDiffConfigAccessor(mocks). + Get(nameSpew).(*spew.ConfigState).SpewKeys + }, + expect: true, + }, + + // Spew config options. + "spew-time indent": { + config: mock.Indent("\t"), + access: func(mocks *mock.Mocks) any { + return GetDiffConfigAccessor(mocks). + Get(nameSpewTime).(*spew.ConfigState).Indent + }, + expect: "\t", + }, + "spew-time max-depth": { + config: mock.MaxDepth(5), + access: func(mocks *mock.Mocks) any { + return GetDiffConfigAccessor(mocks). + Get(nameSpewTime).(*spew.ConfigState).MaxDepth + }, + expect: 5, + }, + "spew-time disable-methods": { + config: mock.DisableMethods(false), + access: func(mocks *mock.Mocks) any { + return GetDiffConfigAccessor(mocks). + Get(nameSpewTime).(*spew.ConfigState).DisableMethods + }, + expect: true, // exception: default is true for spewtime. + }, + "spew-time disable-pointer-methods": { + config: mock.DisablePointerMethods(true), + access: func(mocks *mock.Mocks) any { + return GetDiffConfigAccessor(mocks). + Get(nameSpewTime).(*spew.ConfigState).DisablePointerMethods + }, + expect: true, + }, + "spew-time disable-pointer-addresses": { + config: mock.DisablePointerAddresses(false), + access: func(mocks *mock.Mocks) any { + return GetDiffConfigAccessor(mocks). + Get(nameSpewTime).(*spew.ConfigState).DisablePointerAddresses + }, + expect: false, + }, + "spew-time disable-capacities": { + config: mock.DisableCapacities(false), + access: func(mocks *mock.Mocks) any { + return GetDiffConfigAccessor(mocks). + Get(nameSpewTime).(*spew.ConfigState).DisableCapacities + }, + expect: false, + }, + "spew-time continue-on-method": { + config: mock.ContinueOnMethod(true), + access: func(mocks *mock.Mocks) any { + return GetDiffConfigAccessor(mocks). + Get(nameSpewTime).(*spew.ConfigState).ContinueOnMethod + }, + expect: true, + }, + "spew-time sort-keys": { + config: mock.SortKeys(false), + access: func(mocks *mock.Mocks) any { + return GetDiffConfigAccessor(mocks). + Get(nameSpewTime).(*spew.ConfigState).SortKeys + }, + expect: false, + }, + "spew-time spew-keys": { + config: mock.SpewKeys(true), + access: func(mocks *mock.Mocks) any { + return GetDiffConfigAccessor(mocks). + Get(nameSpewTime).(*spew.ConfigState).SpewKeys + }, + expect: true, + }, +} + +func TestMatcherConfig(t *testing.T) { + test.Map(t, configTestCases). + Run(func(t test.Test, param ConfigParams) { + // Given + mocks := mock.NewMocks(t) + require.NotEqual(t, param.expect, param.access(mocks)) + + // When + mocks.Config(param.config) + + // Then + assert.Equal(t, param.expect, param.access(mocks)) + }) +} + type EqualMatchesParams struct { want any got any diff --git a/mock/mocks.go b/mock/mocks.go index 8e98819..3a4db94 100644 --- a/mock/mocks.go +++ b/mock/mocks.go @@ -84,7 +84,7 @@ type Mocks struct { args map[any]any // Internal matcher settings. - matcher *MatcherConfig + matcher *DiffConfig } // NewMocks creates a new mock handler using given test reporter, e.g. @@ -95,7 +95,7 @@ func NewMocks(t gomock.TestReporter, fncalls ...ConfigFunc) *Mocks { wg: sync.NewLenientWaitGroup(), mocks: map[reflect.Value]any{}, args: map[any]any{}, - matcher: NewMatcherConfig(), + matcher: NewDiffConfig(), }).Config(fncalls...).syncWith(t) } diff --git a/test/pattern.go b/test/pattern.go index f9a735b..9bcaabd 100644 --- a/test/pattern.go +++ b/test/pattern.go @@ -14,6 +14,13 @@ import ( "github.com/tkrop/go-testing/internal/reflect" ) +// Ptr is a convenience function to obtain the pointer to the given value. +// This is particularly useful to create pointers to literal values in test +// setup code, e.g., `test.Ptr(42)` or `test.Ptr("value")`. +func Ptr[T any](v T) *T { + return &v +} + // Must is a convenience method returning the value of the first argument and // that panics on any error in the second argument using the provided error. // The method allows to write concise test setup code. @@ -35,13 +42,6 @@ func Cast[T any](arg any) T { return val } -// Ptr is a convenience function to obtain the pointer to the given value. -// This is particularly useful to create pointers to literal values in test -// setup code, e.g., `test.Ptr(42)` or `test.Ptr("value")`. -func Ptr[T any](v T) *T { - return &v -} - // First is a convenience function to return the first argument and ignore all // others arguments. The method allows to write concise test setup code. func First[T any](arg T, _ ...any) T { return arg } diff --git a/test/pattern_test.go b/test/pattern_test.go index 21e714a..130521c 100644 --- a/test/pattern_test.go +++ b/test/pattern_test.go @@ -30,6 +30,97 @@ type ( var testFunc TestFunc = func(a *any) any { return a } +type PtrParams struct { + value any +} + +var ptrTestCases = map[string]PtrParams{ + // Primitive types + "bool true": {value: true}, + "bool false": {value: false}, + + // Integer types + "int": {value: 42}, + "int zero": {value: 0}, + "int negative": {value: -123}, + "int8": {value: int8(127)}, + "int16": {value: int16(32767)}, + "int32": {value: int32(2147483647)}, + "int64": {value: int64(9223372036854775807)}, + + // Unsigned integer types + "uint": {value: uint(42)}, + "uint8": {value: uint8(255)}, + "uint16": {value: uint16(65535)}, + "uint32": {value: uint32(4294967295)}, + "uint64": {value: uint64(18446744073709551615)}, + "byte": {value: byte(255)}, + "rune": {value: rune('A')}, + + // Floating point types + "float32": {value: float32(3.14)}, + "float32 zero": {value: float32(0.0)}, + "float64": {value: 3.141592653589793}, + "float64 negative": {value: -2.718281828}, + + // Complex types + "complex64": {value: complex64(1 + 2i)}, + "complex128": {value: complex(3.0, 4.0)}, + + // String types + "string": {value: "hello world"}, + "string empty": {value: ""}, + "string unicode": {value: "Hello, 🌍"}, + + // Slice literals + "slice int": {value: []int{1, 2, 3}}, + "slice string": {value: []string{"a", "b", "c"}}, + "slice empty": {value: []int{}}, + "slice nil": {value: []int(nil)}, + + // Map literals + "map string int": {value: map[string]int{"one": 1, "two": 2}}, + "map empty": {value: map[string]int{}}, + "map nil": {value: map[string]int(nil)}, + + // Struct literals + "struct": {value: TestStruct{name: "test", id: 42}}, + "struct zero": {value: TestStruct{}}, + "struct anonymous": {value: struct{ X int }{X: 10}}, + + // Named types + "named slice": {value: TestSlice{"x", "y", "z"}}, + "named map": {value: TestMap{"foo": 1, "bar": 2}}, + + // Pointer types + "pointer to int": {value: test.Ptr(42)}, + "pointer to string": {value: test.Ptr("value")}, + "pointer to struct": {value: &TestStruct{name: "ptr", id: 99}}, + + // Array literals + "array int": {value: [3]int{1, 2, 3}}, + "array string": {value: [2]string{"hello", "world"}}, + + // Interface types + "interface any": {value: any("interface value")}, +} + +func TestPtr(t *testing.T) { + test.Map(t, ptrTestCases).Run(func(t test.Test, param PtrParams) { + // When + result := test.Ptr(param.value) + + // Then + assert.Equal(t, ¶m.value, result) + + rvalue := reflect.ValueOf(result) + assert.Equal(t, reflect.Ptr, rvalue.Kind()) + + value := rvalue.Elem().Interface() + assert.Equal(t, param.value, value) + }) +} + type MustParams struct { setup mock.SetupFunc arg any @@ -303,97 +394,6 @@ func TestCast(t *testing.T) { }) } -type PtrParams struct { - value any -} - -var ptrTestCases = map[string]PtrParams{ - // Primitive types - "bool true": {value: true}, - "bool false": {value: false}, - - // Integer types - "int": {value: 42}, - "int zero": {value: 0}, - "int negative": {value: -123}, - "int8": {value: int8(127)}, - "int16": {value: int16(32767)}, - "int32": {value: int32(2147483647)}, - "int64": {value: int64(9223372036854775807)}, - - // Unsigned integer types - "uint": {value: uint(42)}, - "uint8": {value: uint8(255)}, - "uint16": {value: uint16(65535)}, - "uint32": {value: uint32(4294967295)}, - "uint64": {value: uint64(18446744073709551615)}, - "byte": {value: byte(255)}, - "rune": {value: rune('A')}, - - // Floating point types - "float32": {value: float32(3.14)}, - "float32 zero": {value: float32(0.0)}, - "float64": {value: 3.141592653589793}, - "float64 negative": {value: -2.718281828}, - - // Complex types - "complex64": {value: complex64(1 + 2i)}, - "complex128": {value: complex(3.0, 4.0)}, - - // String types - "string": {value: "hello world"}, - "string empty": {value: ""}, - "string unicode": {value: "Hello, 🌍"}, - - // Slice literals - "slice int": {value: []int{1, 2, 3}}, - "slice string": {value: []string{"a", "b", "c"}}, - "slice empty": {value: []int{}}, - "slice nil": {value: []int(nil)}, - - // Map literals - "map string int": {value: map[string]int{"one": 1, "two": 2}}, - "map empty": {value: map[string]int{}}, - "map nil": {value: map[string]int(nil)}, - - // Struct literals - "struct": {value: TestStruct{name: "test", id: 42}}, - "struct zero": {value: TestStruct{}}, - "struct anonymous": {value: struct{ X int }{X: 10}}, - - // Named types - "named slice": {value: TestSlice{"x", "y", "z"}}, - "named map": {value: TestMap{"foo": 1, "bar": 2}}, - - // Pointer types - "pointer to int": {value: test.Ptr(42)}, - "pointer to string": {value: test.Ptr("value")}, - "pointer to struct": {value: &TestStruct{name: "ptr", id: 99}}, - - // Array literals - "array int": {value: [3]int{1, 2, 3}}, - "array string": {value: [2]string{"hello", "world"}}, - - // Interface types - "interface any": {value: any("interface value")}, -} - -func TestPtr(t *testing.T) { - test.Map(t, ptrTestCases).Run(func(t test.Test, param PtrParams) { - // When - result := test.Ptr(param.value) - - // Then - assert.Equal(t, ¶m.value, result) - - rvalue := reflect.ValueOf(result) - assert.Equal(t, reflect.Ptr, rvalue.Kind()) - - value := rvalue.Elem().Interface() - assert.Equal(t, param.value, value) - }) -} - type RecoverParams struct { setup any expect test.Expect