Skip to content

Commit

Permalink
introduce assert.Unique
Browse files Browse the repository at this point in the history
  • Loading branch information
adamluzsi committed Jul 10, 2024
1 parent dac4da0 commit 856ca6f
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 2 deletions.
50 changes: 50 additions & 0 deletions assert/Asserter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
}
115 changes: 115 additions & 0 deletions assert/Asserter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
10 changes: 10 additions & 0 deletions assert/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
6 changes: 6 additions & 0 deletions assert/pkgfunc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...)
}
22 changes: 20 additions & 2 deletions assert/pkgfunc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
})
}
}

0 comments on commit 856ca6f

Please sign in to comment.