diff --git a/assert/Asserter.go b/assert/Asserter.go index 5cb7522..ec8de10 100644 --- a/assert/Asserter.go +++ b/assert/Asserter.go @@ -1102,3 +1102,53 @@ func (a Asserter) OneOf(values any, blk /* func( */ any, msg ...Message) { } }, msg...) } + +// Unique will verify if the given list has unique elements. +func (a Asserter) Unique(values any, msg ...Message) { + a.TB.Helper() + + if values == nil { + return + } + + vs := reflect.ValueOf(values) + _, ok := oneOfSupportedKinds[vs.Kind()] + Must(a.TB).True(ok, Message(fmt.Sprintf("unexpected list type: %s", vs.Kind().String()))) + + if vs.Kind() == reflect.Array { + // Make the array addressable + arr := reflect.New(vs.Type()).Elem() + arr.Set(vs) // became addressable + vs = arr.Slice(0, vs.Len()) + } + + for i := 0; i < vs.Len(); i++ { + if i == 0 { + continue + } + + mem := vs.Slice(0, i) + element := vs.Index(i) + if !a.try(func(a Asserter) { a.NotContain(mem.Interface(), element.Interface()) }) { + a.fn(fmterror.Message{ + Method: "Unique", + Cause: `Duplicate element found.`, + Message: toMsg(msg), + Values: []fmterror.Value{ + { + Label: "values", + Value: values, + }, + { + Label: "duplicated element", + Value: element.Interface(), + }, + { + Label: "duplicate's index", + Value: i, + }, + }, + }.String()) + } + } +} diff --git a/assert/Asserter_test.go b/assert/Asserter_test.go index 9b741c9..d390b7f 100644 --- a/assert/Asserter_test.go +++ b/assert/Asserter_test.go @@ -17,6 +17,7 @@ import ( "go.llib.dev/testcase" "go.llib.dev/testcase/internal/doubles" + "go.llib.dev/testcase/pp" "go.llib.dev/testcase/sandbox" "go.llib.dev/testcase/assert" @@ -2136,3 +2137,117 @@ func TestAsserter_OneOf(t *testing.T) { }) }) } + +func TestAsserter_Unique(t *testing.T) { + t.Run("no duplicate", func(t *testing.T) { + t.Run("[]int", func(t *testing.T) { + dtb := &doubles.TB{} + assert.Should(dtb).Unique([]int{1, 2, 3}, "err-message") + assert.False(t, dtb.IsFailed) + assert.NotContain(t, dtb.Logs.String(), "err-message") + }) + t.Run("[]string", func(t *testing.T) { + dtb := &doubles.TB{} + assert.Should(dtb).Unique([]string{"a", "b", "c"}, "err-message") + assert.False(t, dtb.IsFailed) + assert.NotContain(t, dtb.Logs.String(), "err-message") + }) + t.Run("[]struct", func(t *testing.T) { + dtb := &doubles.TB{} + vs := []SampleStruct{ + { + Foo: "42", + Bar: 42, + Baz: true, + }, + { + Foo: "24", + Bar: 24, + Baz: false, + }, + } + assert.Should(dtb).Unique(vs, "err-message") + assert.False(t, dtb.IsFailed) + assert.NotContain(t, dtb.Logs.String(), "err-message") + }) + + t.Run("array", func(t *testing.T) { + dtb := &doubles.TB{} + vs := [3]int{1, 2, 3} + assert.Should(dtb).Unique(vs, "err-message") + assert.False(t, dtb.IsFailed) + assert.NotContain(t, dtb.Logs.String(), "err-message") + }) + }) + + t.Run("on non-unique lists, error is raised", func(t *testing.T) { + for _, vs := range []any{ + []int{1, 2, 1}, + [3]int{1, 2, 1}, + []string{"a", "b", "a"}, + []SampleStruct{ + { + Foo: "42", + Bar: 42, + Baz: true, + }, + { + Foo: "24", + Bar: 24, + Baz: false, + }, + { + Foo: "42", + Bar: 42, + Baz: true, + }, + }, + } { + dtb := &doubles.TB{} + assert.Should(dtb).Unique(vs) + assert.True(t, dtb.IsFailed) + assert.Contain(t, dtb.Logs.String(), "duplicated element") + assert.Contain(t, dtb.Logs.String(), "2") + assert.Contain(t, dtb.Logs.String(), pp.Format(reflect.ValueOf(vs).Index(2).Interface())) + } + }) + + t.Run("invalid type", func(t *testing.T) { + t.Run("", func(t *testing.T) { + dtb := &doubles.TB{} + out := sandbox.Run(func() { assert.Should(dtb).Unique("not slice") }) + assert.Equal(t, out.OK, false) + assert.True(t, dtb.IsFailed) + assert.NotEmpty(t, dtb.Logs.String()) + assert.Contain(t, dtb.Logs.String(), "unexpected list type: string") + }) + t.Run("", func(t *testing.T) { + dtb := &doubles.TB{} + out := sandbox.Run(func() { assert.Should(dtb).Unique(42) }) + assert.Equal(t, out.OK, false) + assert.True(t, dtb.IsFailed) + assert.NotEmpty(t, dtb.Logs.String()) + assert.Contain(t, dtb.Logs.String(), "unexpected list type: int") + }) + }) + + t.Run("nil value is ignored", func(t *testing.T) { + dtb := &doubles.TB{} + out := sandbox.Run(func() { assert.Should(dtb).Unique(nil) }) + assert.Equal(t, out.OK, true) + assert.False(t, dtb.IsFailed) + }) + + t.Run("message displayed on error", func(t *testing.T) { + dtb := &doubles.TB{} + assert.Should(dtb).Unique([]int{1, 2, 1}, "err-message") + assert.True(t, dtb.IsFailed) + assert.Contain(t, dtb.Logs.String(), "err-message") + }) +} + +type SampleStruct struct { + Foo string + Bar int + Baz bool +} diff --git a/assert/example_test.go b/assert/example_test.go index 70b6895..0d7565d 100644 --- a/assert/example_test.go +++ b/assert/example_test.go @@ -771,3 +771,13 @@ func ExampleEventually() { it.Must.True(rand.Intn(1) == 0) }) } + +func ExampleAsserter_Unique() { + var tb testing.TB + assert.Must(tb).Unique([]int{1, 2, 3}, "expected of unique values") +} + +func ExampleUnique() { + var tb testing.TB + assert.Unique(tb, []int{1, 2, 3}, "expected of unique values") +} diff --git a/assert/pkgfunc.go b/assert/pkgfunc.go index fa8fcbf..ff18b52 100644 --- a/assert/pkgfunc.go +++ b/assert/pkgfunc.go @@ -151,3 +151,9 @@ func AnyOf(tb testing.TB, blk func(a *A), msg ...Message) { tb.Helper() Must(tb).AnyOf(blk) } + +// Unique will verify if the given list has unique elements. +func Unique[T any](tb testing.TB, vs []T, msg ...Message) { + tb.Helper() + Must(tb).Unique(vs, msg...) +} diff --git a/assert/pkgfunc_test.go b/assert/pkgfunc_test.go index 3f19671..51c303d 100644 --- a/assert/pkgfunc_test.go +++ b/assert/pkgfunc_test.go @@ -466,13 +466,31 @@ func TestPublicFunctions(t *testing.T) { }) }, }, + // .Unique + { + Desc: ".Unique - happy", + Failed: false, + Assert: func(tb testing.TB) { + assert.Unique(tb, []int{1, 2, 3}) + }, + }, + { + Desc: ".Unique - rainy", + Failed: true, + Assert: func(tb testing.TB) { + assert.Unique(tb, []int{1, 2, 3, 4, 1}) + }, + }, } { t.Run(tc.Desc, func(t *testing.T) { stub := &doubles.TB{} - sandbox.Run(func() { + out := sandbox.Run(func() { tc.Assert(stub) }) - assert.Must(t).Equal(tc.Failed, stub.IsFailed) + assert.Must(t).Equal(tc.Failed, stub.IsFailed, "IsFailed expectations") + if tc.Failed { + assert.Must(t).False(out.OK, "Test was expected to fail with Fatal/FailNow") + } }) } }