diff --git a/internal/maps/maps.go b/internal/maps/maps.go index 33b8cbe..00235b6 100644 --- a/internal/maps/maps.go +++ b/internal/maps/maps.go @@ -6,22 +6,9 @@ // the future these functions are hopefully supported by the standard library. package maps -import ( - "iter" -) - -// Copy created a shallow copy of the given map. -func Copy[K comparable, V any](source map[K]V) map[K]V { - target := make(map[K]V) - for key, value := range source { - target[key] = value - } - return target -} - -// Add the given maps to a common base map overriding existing key values pairs -// added from previous maps if a new entry exists in a latter source map. -func Add[K comparable, V any](target map[K]V, sources ...map[K]V) map[K]V { +// Copy merges the given source maps into the target map, overriding existing +// values if the same key appears in a later source map. +func Copy[K comparable, V any](target map[K]V, sources ...map[K]V) map[K]V { for _, source := range sources { for k, v := range source { target[k] = v @@ -29,12 +16,3 @@ func Add[K comparable, V any](target map[K]V, sources ...map[K]V) map[K]V { } return target } - -// Collect collects all entries from the given iterator into a map. -func Collect[K comparable, V any](source iter.Seq2[K, V]) map[K]V { - target := make(map[K]V) - for k, v := range source { - target[k] = v - } - return target -} diff --git a/internal/maps/maps_test.go b/internal/maps/maps_test.go index 358d7e8..9a1739b 100644 --- a/internal/maps/maps_test.go +++ b/internal/maps/maps_test.go @@ -10,43 +10,12 @@ import ( ) type CopyParams struct { - input map[string]int - expect map[string]int -} - -var copyTestCases = map[string]CopyParams{ - "empty-map": { - input: map[string]int{}, - expect: map[string]int{}, - }, - "single-key-value-pair": { - input: map[string]int{"a": 1}, - expect: map[string]int{"a": 1}, - }, - "multiple-key-value-pairs": { - input: map[string]int{"a": 1, "b": 2, "c": 3}, - expect: map[string]int{"a": 1, "b": 2, "c": 3}, - }, -} - -func TestCopy(t *testing.T) { - test.Map(t, copyTestCases). - Run(func(t test.Test, param CopyParams) { - // When - expect := maps.Copy(param.input) - - // Then - assert.Equal(t, param.expect, expect) - }) -} - -type AddParams struct { target map[string]int sources []map[string]int expect map[string]int } -var addTestCases = map[string]AddParams{ +var copyTestCases = map[string]CopyParams{ "no-sources": { target: map[string]int{"a": 1}, sources: []map[string]int{}, @@ -69,48 +38,11 @@ var addTestCases = map[string]AddParams{ }, } -func TestAdd(t *testing.T) { - test.Map(t, addTestCases). - Run(func(t test.Test, param AddParams) { - // When - result := maps.Add(param.target, param.sources...) - - // Then - assert.Equal(t, param.expect, result) - }) -} - -type CollectParams struct { - input map[string]int - expect map[string]int -} - -var collectTestCases = map[string]CollectParams{ - "empty-map": { - input: map[string]int{}, - expect: map[string]int{}, - }, - "single-key-value-pair": { - input: map[string]int{"a": 1}, - expect: map[string]int{"a": 1}, - }, - "multiple-key-value-pairs": { - input: map[string]int{"a": 1, "b": 2, "c": 3}, - expect: map[string]int{"a": 1, "b": 2, "c": 3}, - }, -} - -func TestCollect(t *testing.T) { - test.Map(t, collectTestCases). - Run(func(t test.Test, param CollectParams) { +func TestCopy(t *testing.T) { + test.Map(t, copyTestCases). + Run(func(t test.Test, param CopyParams) { // When - result := maps.Collect(func(yield func(string, int) bool) { - for k, v := range param.input { - if !yield(k, v) { - return - } - } - }) + result := maps.Copy(param.target, param.sources...) // Then assert.Equal(t, param.expect, result) diff --git a/internal/mock/builder.go b/internal/mock/builder.go index 9e1973a..afd2edd 100644 --- a/internal/mock/builder.go +++ b/internal/mock/builder.go @@ -2,9 +2,8 @@ package mock import ( "regexp" + "slices" "strings" - - "github.com/tkrop/go-testing/internal/slices" ) var ( @@ -180,7 +179,7 @@ func (b *FileBuilder) calcUniqAlias(path string) string { alias := "" norm := strings.ToLower(baseReplacer.Replace(path)) - for _, prefix := range slices.Reverse(strings.Split(norm, "/")) { + for _, prefix := range slices.Backward(strings.Split(norm, "/")) { if alias != "" { alias = prefix + "_" + alias } else { diff --git a/internal/slices/slices.go b/internal/slices/slices.go index d6a970d..06eaf61 100644 --- a/internal/slices/slices.go +++ b/internal/slices/slices.go @@ -3,19 +3,11 @@ // must be consider as highly instable. package slices -// Reverse reverses the given slice. -func Reverse[T any](slice []T) []T { - for i, j := 0, len(slice)-1; i < j; i, j = i+1, j-1 { - slice[i], slice[j] = slice[j], slice[i] - } - return slice -} - // Permute permutates the given slice as is. func Permute[T any](slice []T) [][]T { perms := [][]T{} PermuteDo(slice, func(perm []T) { - perms = append(perms, Copy(perm)) + perms = append(perms, append([]T(nil), perm...)) }, 0) return perms } @@ -35,21 +27,3 @@ func PermuteDo[T any](slice []T, do func([]T), i int) { do(slice) } } - -// Copy makes a shallow copy of the given slice. -func Copy[T any](slice []T) []T { - return append(make([]T, 0, len(slice)), slice...) -} - -// Add appends the given slices into a single slice. -func Add[T any](slices ...[]T) []T { - items := 0 - for _, slice := range slices { - items += len(slice) - } - result := make([]T, 0, items) - for _, slice := range slices { - result = append(result, slice...) - } - return result -} diff --git a/internal/slices/slices_test.go b/internal/slices/slices_test.go index a795171..2927554 100644 --- a/internal/slices/slices_test.go +++ b/internal/slices/slices_test.go @@ -6,17 +6,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/tkrop/go-testing/internal/slices" - "github.com/tkrop/go-testing/test" ) -func TestReverse(t *testing.T) { - // When - result := slices.Reverse([]int{0, 1, 2, 3, 4}) - - // Then - assert.Equal(t, []int{4, 3, 2, 1, 0}, result) -} - func TestPermut(t *testing.T) { // When result := slices.Permute([]int{0, 1, 2}) @@ -31,75 +22,3 @@ func TestPermut(t *testing.T) { {2, 0, 1}, }, result) } - -type TestAddIntParams struct { - slices [][]int - expect []int -} - -var addIntTestCases = map[string]TestAddIntParams{ - "add-multiple-int-slices": { - slices: [][]int{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}, - expect: []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, - }, - "add-two-int-slices": { - slices: [][]int{{1, 2}, {3, 4}}, - expect: []int{1, 2, 3, 4}, - }, - "add-single-int-slice": { - slices: [][]int{{1, 2, 3}}, - expect: []int{1, 2, 3}, - }, - "add-empty-int-slices": { - slices: [][]int{{}, {1, 2}, {}}, - expect: []int{1, 2}, - }, - "add-all-empty-int-slices": { - slices: [][]int{{}, {}, {}}, - expect: []int{}, - }, - "add-no-int-slices": { - slices: [][]int{}, - expect: []int{}, - }, -} - -func TestAddInt(t *testing.T) { - test.Map(t, addIntTestCases). - Run(func(t test.Test, param TestAddIntParams) { - // When - result := slices.Add[int](param.slices...) - // Then - assert.Equal(t, param.expect, result) - }) -} - -type TestAddStringParams struct { - slices [][]string - expect []string -} - -var addStringTestCases = map[string]TestAddStringParams{ - "add-string-slices": { - slices: [][]string{{"hello", "world"}, {"foo", "bar"}}, - expect: []string{"hello", "world", "foo", "bar"}, - }, - "add-empty-string-slices": { - slices: [][]string{{}, {"test"}, {}}, - expect: []string{"test"}, - }, - "add-no-string-slices": { - slices: [][]string{}, - expect: []string{}, - }, -} - -func TestAdd(t *testing.T) { - test.Map(t, addStringTestCases). - Run(func(t test.Test, param TestAddStringParams) { - // When - result := slices.Add(param.slices...) - // Then - assert.Equal(t, param.expect, result) - }) -} diff --git a/reflect/reflect.go b/reflect/reflect.go index 8e644fc..938b841 100644 --- a/reflect/reflect.go +++ b/reflect/reflect.go @@ -4,6 +4,7 @@ package reflect import ( + "fmt" "reflect" "slices" "strings" @@ -73,25 +74,25 @@ type builder[T any] struct { // name. func NewBuilder[T any]() Builder[T] { var target T - return NewAccessor[T](target) + return NewAccessor(target) } // NewGetter creates a generic getter for a target struct type. The getter // allows you to access unexported fields of the struct by field name. func NewGetter[T any](target T) Getter[T] { - return NewAccessor[T](target) + return NewAccessor(target) } // NewSetter creates a generic setter for a target struct type. The setter // allows you to modify unexported fields of the struct by field name. func NewSetter[T any](target T) Setter[T] { - return NewAccessor[T](target) + return NewAccessor(target) } // NewFinder creates a generic finder for a target struct type. The finder // allows you to access unexported fields of the struct by field name. func NewFinder[T any](target T) Finder[T] { - return NewAccessor[T](target) + return NewAccessor(target) } // NewAccessor creates a generic builder/accessor for a given target struct. @@ -111,7 +112,7 @@ func NewAccessor[T any](target T) Builder[T] { if value.Kind() == reflect.Ptr { // Create a new instance if the pointer is nil. if value.Elem().Kind() == reflect.Invalid { - target = reflect.New(value.Type().Elem()).Interface().(T) + target = cast[T](reflect.New(value.Type().Elem()).Interface()) value = reflect.ValueOf(target) } @@ -211,12 +212,12 @@ func (b *builder[T]) Build() T { if b.wrapped { target := b.targetValueOf() if target.IsValid() { - return target.Interface().(T) + return cast[T](target.Interface()) } var t T return t } else { - return b.target.(T) + return cast[T](b.target) } } @@ -284,11 +285,11 @@ func Find[P, T any](param P, deflt T, names ...string) T { pt, dt := reflect.TypeOf(param), reflect.TypeOf(deflt) switch { case pt.Kind() == dt.Kind(): - return reflect.ValueOf(param).Interface().(T) + return cast[T](reflect.ValueOf(param).Interface()) case pt.Kind() == reflect.Struct: - return NewAccessor(param).Find(deflt, names...).(T) + return cast[T](NewAccessor(param).Find(deflt, names...)) case pt.Kind() == reflect.Ptr && pt.Elem().Kind() == reflect.Struct: - return NewAccessor(param).Find(deflt, names...).(T) + return cast[T](NewAccessor(param).Find(deflt, names...)) default: return deflt } @@ -307,3 +308,13 @@ func Name[P any](name string, param P) string { } return "" } + +// cast is a convenience function to cast the given argument to the specified +// type or panic controlled if the cast fails. +func cast[T any](arg any) T { + val, ok := arg.(T) + if !ok { + panic(fmt.Sprintf("cast failed [%T]: %v", val, arg)) + } + return val +} diff --git a/reflect/reflect_test.go b/reflect/reflect_test.go index 5879327..8636947 100644 --- a/reflect/reflect_test.go +++ b/reflect/reflect_test.go @@ -1,6 +1,7 @@ package reflect_test import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -15,6 +16,11 @@ type Struct struct { a any } +type ( + IntAlias int + StructPtrAlias *Struct +) + func NewStruct(s string, a any) Struct { return Struct{s: s, a: a} } func NewPtrStruct(s string, a any) *Struct { return &Struct{s: s, a: a} } @@ -820,6 +826,15 @@ func TestBuilderAny(t *testing.T) { func TestNewBuilder(t *testing.T) { t.Parallel() + t.Run("builder-ptr-alias-panic", + test.Run(test.Success, func(t test.Test) { + mock.NewMocks(t).Expect(test.Panic(fmt.Sprintf( + "cast failed [%T]: %v", StructPtrAlias(nil), + NewPtrStruct("", nil)))) + + _ = reflect.NewBuilder[StructPtrAlias]() + })) + t.Run("builder-struct", func(t *testing.T) { t.Parallel() // Given @@ -1049,6 +1064,21 @@ var findTestCases = map[string]FindParams{ } func TestFind(t *testing.T) { + t.Run("kind-match-with-type-alias-panics", + test.Run(test.Success, func(t test.Test) { + mock.NewMocks(t).Expect(test.Panic("cast failed [int]: 1")) + + _ = reflect.Find[any](IntAlias(1), 0) + })) + + t.Run("kind-match-with-pointer-type-alias-panics", + test.Run(test.Success, func(t test.Test) { + mock.NewMocks(t).Expect(test.Panic(fmt.Sprintf( + "cast failed [%T]: %v", StructPtrAlias(nil), new(Struct)))) + + _ = reflect.Find[*Struct, StructPtrAlias](new(Struct), nil) + })) + test.Map(t, findTestCases). Run(func(t test.Test, param FindParams) { // When diff --git a/test/factory.go b/test/factory.go index cc4a8c8..cb781fa 100644 --- a/test/factory.go +++ b/test/factory.go @@ -3,15 +3,16 @@ package test import ( "errors" "fmt" + "maps" "regexp" "runtime" + "slices" "strconv" "strings" "testing" "time" - "github.com/tkrop/go-testing/internal/maps" - "github.com/tkrop/go-testing/internal/slices" + imaps "github.com/tkrop/go-testing/internal/maps" "github.com/tkrop/go-testing/internal/sync" "github.com/tkrop/go-testing/reflect" ) @@ -145,13 +146,14 @@ type Factory[P any] interface { // used to structure tests and benchmarks into logical groups add labels // for tools. If called multiple times, only the last prefix is applied. Prefix(prefix string) Factory[P] - // Adds a generic filter function that allows to filter test cases based on + // Adds generic filter functions that allows to filter test cases based on // the name and the parameter set. If multiple filters are added, all of - // them must accept the test case for it to be executed. + // them must accept the test case, else the test case is excluded. Thus the + // filters are combined by default using a logical `and`. // // **Note:** A test case prefix is always applied to the test case name // before the test case parameters and name are passed to the filter. - Filter(filter FilterFunc[P]) Factory[P] + Filter(filter ...FilterFunc[P]) Factory[P] // Timeout sets up a timeout for the test cases executed by the test runner. // Setting a timeout is useful to prevent the test execution from waiting // too long in case of deadlocks. The timeout is not affecting the global @@ -198,7 +200,7 @@ type factory[P any] struct { // A prefix to prepend to each test case name. prefix string // A filters to include or exclude test cases. - filters []func(string, P) bool + filters []FilterFunc[P] // A timeout after which the test execution is stopped to prevent waiting // to long in case of deadlocks. timeout time.Duration @@ -234,10 +236,13 @@ func Param[P any](t Test, params ...P) Factory[P] { // Map creates a new parallel test runner with given test parameter sets // provided as a test case name to parameter sets mapping. +// +// Note: The test cases are sorted by their lexicographical order to ensure a +// deterministic, stable test order. func Map[P any](t Test, params ...map[string]P) Factory[P] { t.Helper() - return Any[P](t, maps.Add(maps.Copy(params[0]), params[1:]...)) + return Any[P](t, imaps.Copy(maps.Clone(params[0]), params[1:]...)) } // Slice creates a new parallel test runner with given test parameter sets @@ -246,13 +251,13 @@ func Map[P any](t Test, params ...map[string]P) Factory[P] { func Slice[P any](t Test, params ...[]P) Factory[P] { t.Helper() - return Any[P](t, slices.Add(params...)) + return Any[P](t, slices.Concat(params...)) } // Filter adds a generic filter function that allows to filter test cases based // on the name and the parameter set. -func (f *factory[P]) Filter(filter FilterFunc[P]) Factory[P] { - f.filters = append(f.filters, filter) +func (f *factory[P]) Filter(filter ...FilterFunc[P]) Factory[P] { + f.filters = append(f.filters, filter...) return f } @@ -346,8 +351,9 @@ func (f *factory[P]) dispatch( switch params := f.params.(type) { case map[string]P: f.parallel(parallel) - for name, param := range params { - name := reflect.Name(name, param) + for _, key := range f.sorted(params) { + param := params[key] + name := reflect.Name(key, param) f.filter(name, param, call) } @@ -370,6 +376,18 @@ func (f *factory[P]) dispatch( return f } +// sorted sorts the keys of the test case map by the lexicographical order of +// the test case name including the prefix to ensure a deterministic, stable +// test order independent of the natural map iteration order. +func (f *factory[P]) sorted(params map[string]P) []string { + return slices.SortedFunc(maps.Keys(params), func(a, b string) int { + return strings.Compare( + f.prefix+reflect.Name(a, params[a]), + f.prefix+reflect.Name(b, params[b]), + ) + }) +} + // filter filters the parameter set by its `(name, param)` pair before calling // the provided function. The function is only called when all registered // filters accept the test case. diff --git a/test/factory_test.go b/test/factory_test.go index b8c61e8..960fa41 100644 --- a/test/factory_test.go +++ b/test/factory_test.go @@ -1,6 +1,7 @@ package test_test import ( + "maps" "runtime" "strings" "sync" @@ -10,7 +11,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/tkrop/go-testing/internal/iter" - "github.com/tkrop/go-testing/internal/maps" "github.com/tkrop/go-testing/test" ) diff --git a/test/pattern.go b/test/pattern.go index 1c62908..d66b756 100644 --- a/test/pattern.go +++ b/test/pattern.go @@ -32,8 +32,8 @@ func Must[T any](arg T, err error) T { } // Cast is a convenience function to cast the given argument to the specified -// type or panic if the cast fails. The method allows to write concise test -// setup code granting meaningful type checks. +// type or panic controlled if the cast fails. The method allows to write +// concise test setup code granting meaningful type checks. func Cast[T any](arg any) T { val, ok := arg.(T) if !ok { @@ -151,8 +151,7 @@ func Main(main func()) func(t Test, param MainParams) { } // #nosec G204,G702 -- secured by calling only the dedicated test. - cmd := exec.CommandContext(ctx, os.Args[0], - "-test.run="+t.(*Context).t.Name()) + cmd := exec.CommandContext(ctx, os.Args[0], "-test.run="+t.Name()) // No stdout to allow propagation of coverage results. cmd.Stdin, cmd.Stderr = os.Stdin, os.Stderr