From dc2e612d7671dd9d817c766d773518d24062d001 Mon Sep 17 00:00:00 2001 From: Adam Luzsi Date: Wed, 25 Oct 2023 02:06:36 +0200 Subject: [PATCH] assert package API change + random.Unique helper Align the `AnyOf` assertion helper in the assert package with other existing assertion helper functions. Introduce a `random.Unique` utility function to simplify the generation of unique random values in tests, reducing the risk of flaky test results. --- Spec_test.go | 6 +- assert/AnyOf.go | 38 ++---- assert/AnyOf_test.go | 28 ++-- assert/Asserter.go | 43 +++++- assert/Asserter_test.go | 129 ++++++++++++++++-- assert/equal.go | 6 +- assert/example_test.go | 73 ++++++++-- assert/message_test.go | 3 +- assert/pkgfunc.go | 25 ++++ assert/pkgfunc_test.go | 21 +++ clock/internal/chronos.go | 4 - clock/timecop/timecop.go | 2 + clock/timecop/timecop_test.go | 12 ++ .../reflects}/deepequal.go | 11 +- .../reflects}/deepequal_test.go | 6 +- random/Make_test.go | 28 ++-- random/Random_test.go | 16 +-- random/Unique.go | 42 ++++++ random/Unique_test.go | 64 +++++++++ 19 files changed, 448 insertions(+), 109 deletions(-) rename {assert/internal => internal/reflects}/deepequal.go (96%) rename {assert/internal => internal/reflects}/deepequal_test.go (98%) create mode 100644 random/Unique.go create mode 100644 random/Unique_test.go diff --git a/Spec_test.go b/Spec_test.go index a3dfad7..cdf8f30 100644 --- a/Spec_test.go +++ b/Spec_test.go @@ -1097,9 +1097,9 @@ func TestSpec_Test_flakyByStrategy_willRunAgainBasedOnTheStrategy(t *testing.T) } }, testcase.Flaky(strategy)) - assert.Must(t).AnyOf(func(a *assert.AnyOf) { - a.Test(func(t assert.It) { t.Must.Equal(strategyCallCount, testCount) }) - a.Test(func(t assert.It) { t.Must.Equal(strategyCallCount+1, testCount) }) // when there is no error, the total + assert.Must(t).AnyOf(func(a *assert.A) { + a.Case(func(t assert.It) { t.Must.Equal(strategyCallCount, testCount) }) + a.Case(func(t assert.It) { t.Must.Equal(strategyCallCount+1, testCount) }) // when there is no error, the total }) } diff --git a/assert/AnyOf.go b/assert/AnyOf.go index 845a22f..9cceb7d 100644 --- a/assert/AnyOf.go +++ b/assert/AnyOf.go @@ -10,30 +10,14 @@ import ( "go.llib.dev/testcase/internal/fmterror" ) -// OneOf function checks a list of values and matches an expectation against each element of the list. -// If any of the elements pass the assertion, then the assertion helper function does not fail the test. -func OneOf[V any](tb testing.TB, vs []V, blk func(it It, got V), msg ...Message) { - tb.Helper() - Must(tb).AnyOf(func(a *AnyOf) { - a.name = "OneOf" - a.cause = "None of the element matched the expectations" - for _, v := range vs { - a.Test(func(it It) { blk(it, v) }) - if a.OK() { - break - } - } - }, msg...) -} - -// AnyOf is an assertion helper that allows you run AnyOf.Test assertion blocks, that can fail, as lone at least one of them succeeds. +// A stands for Any Of, an assertion helper that allows you run A.Case assertion blocks, that can fail, as lone at least one of them succeeds. // common usage use-cases: // - list of interface, where test order, or the underlying structure's implementation is irrelevant for the behavior. // - list of big structures, where not all field value relevant, only a subset, like a structure it wraps under a field. // - list of structures with fields that has dynamic state values, which is irrelevant for the given test. // - structure that can have various state scenario, and you want to check all of them, and you expect to find one match with the input. // - fan out scenario, where you need to check in parallel that at least one of the worker received the event. -type AnyOf struct { +type A struct { TB testing.TB Fail func() @@ -44,10 +28,10 @@ type AnyOf struct { cause string } -// Test will test a block of assertion that must succeed in order to make AnyOf pass. -// You can have as much AnyOf.Test calls as you need, but if any of them pass with success, the rest will be skipped. -// Using Test is safe for concurrently. -func (ao *AnyOf) Test(blk func(t It)) { +// Case will test a block of assertion that must succeed in order to make A pass. +// You can have as much A.Case calls as you need, but if any of them pass with success, the rest will be skipped. +// Using Case is safe for concurrently. +func (ao *A) Case(blk func(t It)) { ao.TB.Helper() if ao.OK() { return @@ -70,8 +54,14 @@ func (ao *AnyOf) Test(blk func(t It)) { return } +// Test is an alias for A.Case +func (ao *A) Test(blk func(t It)) { + ao.TB.Helper() + ao.Test(blk) +} + // Finish will check if any of the assertion succeeded. -func (ao *AnyOf) Finish(msg ...Message) { +func (ao *A) Finish(msg ...Message) { ao.TB.Helper() if ao.OK() { return @@ -95,7 +85,7 @@ func (ao *AnyOf) Finish(msg ...Message) { ao.Fail() } -func (ao *AnyOf) OK() bool { +func (ao *A) OK() bool { ao.mutex.Lock() defer ao.mutex.Unlock() return ao.passed diff --git a/assert/AnyOf_test.go b/assert/AnyOf_test.go index 8919828..014a704 100644 --- a/assert/AnyOf_test.go +++ b/assert/AnyOf_test.go @@ -10,20 +10,20 @@ import ( "go.llib.dev/testcase/internal/doubles" ) -func TestAnyOf(t *testing.T) { +func TestA(t *testing.T) { s := testcase.NewSpec(t) stub := testcase.Let(s, func(t *testcase.T) *doubles.TB { return &doubles.TB{} }) - anyOf := testcase.Let(s, func(t *testcase.T) *assert.AnyOf { - return &assert.AnyOf{TB: stub.Get(t), Fail: stub.Get(t).Fail} + anyOf := testcase.Let(s, func(t *testcase.T) *assert.A { + return &assert.A{TB: stub.Get(t), Fail: stub.Get(t).Fail} }) subject := func(t *testcase.T, blk func(it assert.It)) { - anyOf.Get(t).Test(blk) + anyOf.Get(t).Case(blk) } - s.When(`there is at least one .Test with non failing ran`, func(s *testcase.Spec) { + s.When(`there is at least one .Case with non failing ran`, func(s *testcase.Spec) { s.Before(func(t *testcase.T) { subject(t, func(it assert.It) { /* no fail */ }) }) @@ -39,7 +39,7 @@ func TestAnyOf(t *testing.T) { t.Must.True(anyOf.Get(t).OK()) }) - s.And(`and new .Test calls are made`, func(s *testcase.Spec) { + s.And(`and new .Case calls are made`, func(s *testcase.Spec) { additionalTestBlkRan := testcase.LetValue(s, false) s.Before(func(t *testcase.T) { subject(t, func(it assert.It) { additionalTestBlkRan.Set(t, true) }) @@ -64,7 +64,7 @@ func TestAnyOf(t *testing.T) { }) }) - s.When(`.Test fails with .FailNow`, func(s *testcase.Spec) { + s.When(`.Case fails with .FailNow`, func(s *testcase.Spec) { s.Before(func(t *testcase.T) { subject(t, func(it assert.It) { it.Must.True(false) }) }) @@ -99,20 +99,20 @@ func TestAnyOf(t *testing.T) { }) } -func TestAnyOf_Test_cleanup(t *testing.T) { +func TestA_Case_cleanup(t *testing.T) { h := assert.Must(t) stub := &doubles.TB{} - anyOf := &assert.AnyOf{ + anyOf := &assert.A{ TB: stub, Fail: stub.Fail, } var cleanupRan bool - anyOf.Test(func(it assert.It) { + anyOf.Case(func(it assert.It) { it.Must.TB.Cleanup(func() { cleanupRan = true }) it.Must.True(false) // fail it }) - h.True(cleanupRan, "cleanup should have ran already after leaving the block of AnyOf.Test") + h.True(cleanupRan, "cleanup should have ran already after leaving the block of AnyOf.Case") anyOf.Finish() h.True(stub.IsFailed, "the provided testing.TB should have failed") @@ -120,14 +120,14 @@ func TestAnyOf_Test_cleanup(t *testing.T) { func TestAnyOf_Test_race(t *testing.T) { stub := &doubles.TB{} - anyOf := &assert.AnyOf{ + anyOf := &assert.A{ TB: stub, Fail: stub.Fail, } testcase.Race(func() { - anyOf.Test(func(it assert.It) {}) + anyOf.Case(func(it assert.It) {}) }, func() { - anyOf.Test(func(it assert.It) {}) + anyOf.Case(func(it assert.It) {}) }, func() { anyOf.Finish() }) diff --git a/assert/Asserter.go b/assert/Asserter.go index 8540689..27b2bda 100644 --- a/assert/Asserter.go +++ b/assert/Asserter.go @@ -782,9 +782,9 @@ func (a Asserter) containExactlySlice(exp reflect.Value, act reflect.Value, msg } } -func (a Asserter) AnyOf(blk func(a *AnyOf), msg ...Message) { +func (a Asserter) AnyOf(blk func(a *A), msg ...Message) { a.TB.Helper() - anyOf := &AnyOf{TB: a.TB, Fail: a.Fail} + anyOf := &A{TB: a.TB, Fail: a.Fail} defer anyOf.Finish(msg...) blk(anyOf) } @@ -1029,6 +1029,7 @@ func (a Asserter) NotWithin(timeout time.Duration, blk func(context.Context), ms } func (a Asserter) within(timeout time.Duration, blk func(context.Context)) bool { + a.TB.Helper() ctx, cancel := context.WithCancel(context.Background()) defer cancel() var done, isFailNow uint32 @@ -1063,3 +1064,41 @@ func (a Asserter) Eventually(durationOrCount any, blk func(it It)) { } retry.Assert(a.TB, blk) } + +var oneOfSupportedKinds = map[reflect.Kind]struct{}{ + reflect.Slice: {}, + reflect.Array: {}, +} + +// OneOf evaluates whether at least one element within the given values meets the conditions set in the assertion block. +func (a Asserter) OneOf(values any, blk /* func( */ any, msg ...Message) { + tb := a.TB + tb.Helper() + + vs := reflect.ValueOf(values) + _, ok := oneOfSupportedKinds[vs.Kind()] + Must(tb).True(ok, Message(fmt.Sprintf("unexpected list value type: %s", vs.Kind().String()))) + + var fnErrMsg = Message(fmt.Sprintf("invalid function signature\n\nExpected:\nfunc(it assert.It, v %s)", vs.Type().Elem())) + fn := reflect.ValueOf(blk) + Must(tb).Equal(fn.Kind(), reflect.Func, "blk argument must be a function") + Must(tb).Equal(fn.Type().NumIn(), 2, fnErrMsg) + Must(tb).Equal(fn.Type().In(0), reflect.TypeOf((*It)(nil)).Elem(), fnErrMsg) + Must(tb).Equal(fn.Type().In(1), vs.Type().Elem(), fnErrMsg) + + a.AnyOf(func(a *A) { + tb.Helper() + a.name = "OneOf" + a.cause = "None of the element matched the expectations" + + for i := 0; i < vs.Len(); i++ { + e := vs.Index(i) + a.Case(func(it It) { + fn.Call([]reflect.Value{reflect.ValueOf(it), e}) + }) + if a.OK() { + break + } + } + }, msg...) +} diff --git a/assert/Asserter_test.go b/assert/Asserter_test.go index 96b01b4..eb76ff5 100644 --- a/assert/Asserter_test.go +++ b/assert/Asserter_test.go @@ -501,7 +501,9 @@ func TestAsserter_Equal_types(t *testing.T) { }) t.Run("NOK", func(t *testing.T) { bi1 := big.NewInt(int64(rnd.IntB(128, 1024))) - bi2 := big.NewInt(int64(rnd.IntB(128, 1024))) + bi2 := random.Unique(func() *big.Int { + return big.NewInt(int64(rnd.IntB(128, 1024))) + }, bi1) dtb := &doubles.TB{} out := sandbox.Run(func() { assert.Equal(dtb, bi1, bi2) }) Equal(t, true, dtb.IsFailed) @@ -520,7 +522,9 @@ func TestAsserter_Equal_types(t *testing.T) { }) t.Run("NOK", func(t *testing.T) { bi1 := big.NewFloat(rnd.Float64()) - bi2 := big.NewFloat(rnd.Float64()) + bi2 := random.Unique(func() *big.Float { + return big.NewFloat(rnd.Float64()) + }, bi1) dtb := &doubles.TB{} out := sandbox.Run(func() { assert.Equal(dtb, bi1, bi2) }) Equal(t, true, dtb.IsFailed) @@ -529,7 +533,7 @@ func TestAsserter_Equal_types(t *testing.T) { }) t.Run("big.Rat", func(t *testing.T) { t.Run("OK", func(t *testing.T) { - a, b := int64(rnd.IntB(128, 256)), int64(rnd.IntB(0, 42)) + a, b := int64(rnd.IntB(128, 256)), int64(rnd.IntB(1, 42)) bi1 := big.NewRat(a, b) bi2 := big.NewRat(a, b) dtb := &doubles.TB{} @@ -538,8 +542,10 @@ func TestAsserter_Equal_types(t *testing.T) { Equal(t, true, out.OK) }) t.Run("NOK", func(t *testing.T) { - bi1 := big.NewRat(int64(rnd.IntB(128, 256)), int64(rnd.IntB(0, 42))) - bi2 := big.NewRat(int64(rnd.IntB(128, 256)), int64(rnd.IntB(0, 42))) + bi1 := big.NewRat(int64(rnd.IntB(128, 256)), int64(rnd.IntB(1, 42))) + bi2 := random.Unique(func() *big.Rat { + return big.NewRat(int64(rnd.IntB(128, 256)), int64(rnd.IntB(1, 42))) + }, bi1) dtb := &doubles.TB{} out := sandbox.Run(func() { assert.Equal(dtb, bi1, bi2) }) Equal(t, true, dtb.IsFailed) @@ -1475,11 +1481,11 @@ func TestAsserter_AnyOf(t *testing.T) { h := assert.Must(t) stub := &doubles.TB{} a := assert.Asserter{TB: stub, Fail: stub.Fail} - a.AnyOf(func(a *assert.AnyOf) { - a.Test(func(it assert.It) { + a.AnyOf(func(a *assert.A) { + a.Case(func(it assert.It) { /* happy-path */ }) - a.Test(func(it assert.It) { + a.Case(func(it assert.It) { it.Must.True(false) }) }) @@ -1490,8 +1496,8 @@ func TestAsserter_AnyOf(t *testing.T) { h := assert.Must(t) stub := &doubles.TB{} a := assert.Asserter{TB: stub, Fail: stub.Fail} - a.AnyOf(func(a *assert.AnyOf) { - a.Test(func(it assert.It) { + a.AnyOf(func(a *assert.A) { + a.Case(func(it assert.It) { it.Must.True(false) }) }) @@ -1502,8 +1508,8 @@ func TestAsserter_AnyOf(t *testing.T) { ro := sandbox.Run(func() { stub := &doubles.TB{} a := assert.Asserter{TB: stub, Fail: stub.FailNow} - a.AnyOf(func(a *assert.AnyOf) { - a.Test(func(it assert.It) { it.FailNow() }) + a.AnyOf(func(a *assert.A) { + a.Case(func(it assert.It) { it.FailNow() }) }) }) t.Log(`Asserter was used with FailNow, so the sandbox should not be OK`) @@ -2030,3 +2036,102 @@ func TestAsserter_Eventually(t *testing.T) { assert.True(t, dtb.IsFailed, "eventually fail") }) } + +func TestAsserter_OneOf(t *testing.T) { + s := testcase.NewSpec(t) + + stub := testcase.Let(s, func(t *testcase.T) *doubles.TB { + return &doubles.TB{} + }) + vs := testcase.Let(s, func(t *testcase.T) []string { + return random.Slice(t.Random.IntBetween(3, 7), func() string { + return t.Random.String() + }) + }) + + const msg = "optional assertion explanation" + blk := testcase.LetValue[func(assert.It, string)](s, nil) + act := func(t *testcase.T) sandbox.RunOutcome { + return sandbox.Run(func() { + assert.Must(stub.Get(t)).OneOf(vs.Get(t), blk.Get(t), msg) + }) + } + + s.When("passed block has no issue", func(s *testcase.Spec) { + blk.Let(s, func(t *testcase.T) func(assert.It, string) { + return func(it assert.It, s string) {} + }) + + s.Then("testing.TB is OK", func(t *testcase.T) { + act(t) + + t.Must.False(stub.Get(t).IsFailed) + }) + + s.Then("execution context is not killed", func(t *testcase.T) { + t.Must.True(act(t).OK) + }) + + s.Then("assert message explanation is not logged", func(t *testcase.T) { + act(t) + + t.Must.NotContain(stub.Get(t).Logs.String(), msg) + }) + }) + + s.When("passed keeps failing with testing.TB#FailNow", func(s *testcase.Spec) { + blk.Let(s, func(t *testcase.T) func(assert.It, string) { + return func(it assert.It, s string) { it.FailNow() } + }) + + s.Then("testing.TB is failed", func(t *testcase.T) { + act(t) + + t.Must.True(stub.Get(t).IsFailed) + }) + + s.Then("execution context is interrupted with FailNow", func(t *testcase.T) { + out := act(t) + t.Must.False(out.OK) + t.Must.True(out.Goexit) + }) + + s.Then("assert message explanation is logged using the testing.TB", func(t *testcase.T) { + act(t) + + t.Must.Contain(stub.Get(t).Logs.String(), msg) + }) + + s.Then("assertion failure message includes the assertion helper name", func(t *testcase.T) { + act(t) + + t.Must.Contain(stub.Get(t).Logs.String(), "OneOf") + t.Must.Contain(stub.Get(t).Logs.String(), "None of the element matched the expectations") + }) + }) + + s.When("assertion pass only for one of the slice element", func(s *testcase.Spec) { + blk.Let(s, func(t *testcase.T) func(assert.It, string) { + expected := t.Random.SliceElement(vs.Get(t)).(string) + return func(it assert.It, got string) { + it.Must.Equal(expected, got) + } + }) + + s.Then("testing.TB is OK", func(t *testcase.T) { + act(t) + + t.Must.False(stub.Get(t).IsFailed) + }) + + s.Then("execution context is not killed", func(t *testcase.T) { + t.Must.True(act(t).OK) + }) + + s.Then("assert message explanation is not logged", func(t *testcase.T) { + act(t) + + t.Must.NotContain(stub.Get(t).Logs.String(), msg) + }) + }) +} diff --git a/assert/equal.go b/assert/equal.go index df631d0..369a766 100644 --- a/assert/equal.go +++ b/assert/equal.go @@ -2,7 +2,7 @@ package assert import ( "fmt" - "go.llib.dev/testcase/assert/internal" + "go.llib.dev/testcase/internal/reflects" "math/big" "net" "reflect" @@ -12,7 +12,7 @@ import ( func eq(tb testing.TB, exp, act any) bool { tb.Helper() - isEq, err := internal.DeepEqual(exp, act) + isEq, err := reflects.DeepEqual(exp, act) Must(tb).NoError(err) return isEq } @@ -36,7 +36,7 @@ func RegisterEqual[T any, FN EqualFunc[T]](fn FN) struct{} { default: panic(fmt.Sprintf("unrecognised Equality checker function signature")) } - internal.RegisterIsEqual(reflect.TypeOf((*T)(nil)).Elem(), rfn) + reflects.RegisterIsEqual(reflect.TypeOf((*T)(nil)).Elem(), rfn) return struct{}{} } diff --git a/assert/example_test.go b/assert/example_test.go index 18ba0f7..fb08115 100644 --- a/assert/example_test.go +++ b/assert/example_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "go.llib.dev/testcase/random" "math/rand" "strings" "testing" @@ -104,15 +105,48 @@ func ExampleAsserter_AnyOf() { Bar() bool Baz() string } - assert.Must(tb).AnyOf(func(anyOf *assert.AnyOf) { + assert.Must(tb).AnyOf(func(anyOf *assert.A) { for _, testingCase := range list { - anyOf.Test(func(it assert.It) { + anyOf.Case(func(it assert.It) { it.Must.True(testingCase.Bar()) }) } }) } +func ExampleAnyOf_anyOfTheElement() { + var tb testing.TB + var list []interface { + Foo() int + Bar() bool + Baz() string + } + assert.AnyOf(tb, func(anyOf *assert.A) { + for _, testingCase := range list { + anyOf.Case(func(it assert.It) { + it.Must.True(testingCase.Bar()) + }) + } + }) +} + +func ExampleAnyOf_anyOfExpectedOutcome() { + var tb testing.TB + var rnd = random.New(random.CryptoSeed{}) + + outcome := rnd.Bool() + + assert.AnyOf(tb, func(a *assert.A) { + a.Case(func(it assert.It) { + it.Must.True(outcome) + }) + + a.Case(func(it assert.It) { + it.Must.False(outcome) + }) + }) +} + func ExampleAnyOf_listOfInterface() { var tb testing.TB type ExampleInterface interface { @@ -120,9 +154,9 @@ func ExampleAnyOf_listOfInterface() { Bar() bool Baz() string } - anyOf := assert.AnyOf{TB: tb, Fail: tb.FailNow} + anyOf := assert.A{TB: tb, Fail: tb.FailNow} for _, v := range []ExampleInterface{} { - anyOf.Test(func(it assert.It) { + anyOf.Case(func(it assert.It) { it.Must.True(v.Bar()) }) } @@ -138,9 +172,9 @@ func ExampleAnyOf_listOfCompositedStructuresWhereOnlyTheEmbededValueIsRelevant() A, B, C int // relevant data for the test } } - anyOf := assert.AnyOf{TB: tb, Fail: tb.FailNow} + anyOf := assert.A{TB: tb, Fail: tb.FailNow} for _, v := range []BigStruct{} { - anyOf.Test(func(it assert.It) { + anyOf.Case(func(it assert.It) { it.Must.Equal(42, v.WrappedStruct.A) it.Must.Equal(1, v.WrappedStruct.B) it.Must.Equal(2, v.WrappedStruct.C) @@ -155,9 +189,9 @@ func ExampleAnyOf_listOfStructuresWithIrrelevantValues() { IrrelevantStateValue int // not relevant data for the test ImportantValue int } - anyOf := assert.AnyOf{TB: tb, Fail: tb.FailNow} + anyOf := assert.A{TB: tb, Fail: tb.FailNow} for _, v := range []StructWithDynamicValues{} { - anyOf.Test(func(it assert.It) { + anyOf.Case(func(it assert.It) { it.Must.Equal(42, v.ImportantValue) }) } @@ -171,26 +205,26 @@ func ExampleAnyOf_structWithManyAcceptableState() { A, B, C int } var es ExampleStruct - anyOf := assert.AnyOf{TB: tb, Fail: tb.FailNow} - anyOf.Test(func(it assert.It) { + anyOf := assert.A{TB: tb, Fail: tb.FailNow} + anyOf.Case(func(it assert.It) { it.Must.Equal(`foo`, es.Type) it.Must.Equal(1, es.A) it.Must.Equal(2, es.B) it.Must.Equal(3, es.C) }) - anyOf.Test(func(it assert.It) { + anyOf.Case(func(it assert.It) { it.Must.Equal(`foo`, es.Type) it.Must.Equal(3, es.A) it.Must.Equal(2, es.B) it.Must.Equal(1, es.C) }) - anyOf.Test(func(it assert.It) { + anyOf.Case(func(it assert.It) { it.Must.Equal(`bar`, es.Type) it.Must.Equal(11, es.A) it.Must.Equal(12, es.B) it.Must.Equal(13, es.C) }) - anyOf.Test(func(it assert.It) { + anyOf.Case(func(it assert.It) { it.Must.Equal(`baz`, es.Type) it.Must.Equal(21, es.A) it.Must.Equal(22, es.B) @@ -210,10 +244,10 @@ func (ExamplePublisher) Close() error { return nil } func ExampleAnyOf_fanOutPublishing() { var tb testing.TB publisher := ExamplePublisher{} - anyOf := &assert.AnyOf{TB: tb, Fail: tb.FailNow} + anyOf := &assert.A{TB: tb, Fail: tb.FailNow} for i := 0; i < 42; i++ { publisher.Subscribe(func(event ExamplePublisherEvent) { - anyOf.Test(func(it assert.It) { + anyOf.Case(func(it assert.It) { it.Must.Equal(42, event.V) }) }) @@ -688,6 +722,15 @@ func ExampleOneOf() { }, "optional assertion explanation") } +func ExampleAsserter_OneOf() { + var tb testing.TB + values := []string{"foo", "bar", "baz"} + + assert.Must(tb).OneOf(values, func(it assert.It, got string) { + it.Must.Equal("bar", got) + }, "optional assertion explanation") +} + func ExampleMatch() { var tb testing.TB assert.Match(tb, "42", "[0-9]+") diff --git a/assert/message_test.go b/assert/message_test.go index f3d4c71..e0a6a5b 100644 --- a/assert/message_test.go +++ b/assert/message_test.go @@ -4,6 +4,7 @@ import ( "go.llib.dev/testcase/assert" "go.llib.dev/testcase/internal/doubles" "go.llib.dev/testcase/random" + "strings" "testing" ) @@ -19,5 +20,5 @@ func TestMessage(t *testing.T) { rnd := random.New(random.CryptoSeed{}) exp := assert.Message(rnd.String()) a.True(false, exp) - assert.Contain(t, dtb.Logs.String(), string(exp)) + assert.Contain(t, dtb.Logs.String(), strings.TrimSpace(string(exp))) } diff --git a/assert/pkgfunc.go b/assert/pkgfunc.go index 2ac31f0..a454e48 100644 --- a/assert/pkgfunc.go +++ b/assert/pkgfunc.go @@ -126,3 +126,28 @@ func Eventually[T time.Duration | int](tb testing.TB, durationOrCount T, blk fun tb.Helper() Must(tb).Eventually(durationOrCount, blk) } + +// OneOf function checks a list of values and matches an expectation against each element of the list. +// If any of the elements pass the assertion, then the assertion helper function does not fail the test. +func OneOf[V any](tb testing.TB, vs []V, blk func(it It, got V), msg ...Message) { + tb.Helper() + Must(tb).AnyOf(func(a *A) { + a.name = "OneOf" + a.cause = "None of the element matched the expectations" + for _, v := range vs { + a.Case(func(it It) { blk(it, v) }) + if a.OK() { + break + } + } + }, msg...) +} + +// AnyOf is an assertion helper that deems the test successful +// if any of the declared assertion cases pass. +// This is commonly used when multiple valid formats are acceptable +// or when working with a list where any element meeting a certain criteria is considered sufficient. +func AnyOf(tb testing.TB, blk func(a *A), msg ...Message) { + tb.Helper() + Must(tb).AnyOf(blk) +} diff --git a/assert/pkgfunc_test.go b/assert/pkgfunc_test.go index cdc9a3b..75fe84f 100644 --- a/assert/pkgfunc_test.go +++ b/assert/pkgfunc_test.go @@ -427,6 +427,27 @@ func TestPublicFunctions(t *testing.T) { }) }, }, + // .AnyOf + { + Desc: ".AnyOf - happy", + Failed: false, + Assert: func(tb testing.TB) { + assert.AnyOf(tb, func(a *assert.A) { + a.Case(func(it assert.It) { it.FailNow() }) + a.Case(func(it assert.It) {}) + }) + }, + }, + { + Desc: ".AnyOf - rainy value", + Failed: true, + Assert: func(tb testing.TB) { + assert.AnyOf(tb, func(a *assert.A) { + a.Case(func(it assert.It) { it.FailNow() }) + a.Case(func(it assert.It) { it.FailNow() }) + }) + }, + }, } { t.Run(tc.Desc, func(t *testing.T) { stub := &doubles.TB{} diff --git a/clock/internal/chronos.go b/clock/internal/chronos.go index 2d165b3..887366f 100644 --- a/clock/internal/chronos.go +++ b/clock/internal/chronos.go @@ -2,12 +2,8 @@ package internal import ( "time" - - "go.llib.dev/testcase/random" ) -var rnd = random.New(random.CryptoSeed{}) - func init() { chrono.Speed = 1 } diff --git a/clock/timecop/timecop.go b/clock/timecop/timecop.go index 894a6de..8db0010 100644 --- a/clock/timecop/timecop.go +++ b/clock/timecop/timecop.go @@ -19,6 +19,8 @@ func Travel[D time.Duration | time.Time](tb testing.TB, d D, tos ...TravelOption } } +const BlazingFast = 100 + func SetSpeed(tb testing.TB, multiplier float64) { tb.Helper() guardAgainstParallel(tb) diff --git a/clock/timecop/timecop_test.go b/clock/timecop/timecop_test.go index 5d059ad..2f494c6 100644 --- a/clock/timecop/timecop_test.go +++ b/clock/timecop/timecop_test.go @@ -14,6 +14,18 @@ import ( var rnd = random.New(random.CryptoSeed{}) +func TestSetSpeed_wBlazingFast(t *testing.T) { + timecop.SetSpeed(t, timecop.BlazingFast) + assert.Eventually(t, 5, func(it assert.It) { + var count int + deadline := clock.TimeNow().Add(time.Millisecond) + for clock.TimeNow().Before(deadline) { + count++ + } + assert.True(t, 1 <= count) + }) +} + func TestSetSpeed(t *testing.T) { t.Run("on zero", func(t *testing.T) { dtb := &doubles.TB{} diff --git a/assert/internal/deepequal.go b/internal/reflects/deepequal.go similarity index 96% rename from assert/internal/deepequal.go rename to internal/reflects/deepequal.go index d0da004..a95f93c 100644 --- a/assert/internal/deepequal.go +++ b/internal/reflects/deepequal.go @@ -1,7 +1,6 @@ -package internal +package reflects import ( - "go.llib.dev/testcase/internal/reflects" "go.llib.dev/testcase/internal/teardown" "reflect" ) @@ -42,11 +41,11 @@ func reflectDeepEqual(m *refMem, v1, v2 reflect.Value) (iseq bool, _ error) { switch v1.Kind() { case reflect.Struct: for i, n := 0, v1.NumField(); i < n; i++ { - f1, ok := reflects.TryToMakeAccessible(v1.Field(i)) + f1, ok := TryToMakeAccessible(v1.Field(i)) if !ok { continue } - f2, ok := reflects.TryToMakeAccessible(v2.Field(i)) + f2, ok := TryToMakeAccessible(v2.Field(i)) if !ok { continue } @@ -170,8 +169,8 @@ func reflectDeepEqual(m *refMem, v1, v2 reflect.Value) (iseq bool, _ error) { default: return reflect.DeepEqual( - reflects.Accessible(v1).Interface(), - reflects.Accessible(v2).Interface()), nil + Accessible(v1).Interface(), + Accessible(v2).Interface()), nil } } diff --git a/assert/internal/deepequal_test.go b/internal/reflects/deepequal_test.go similarity index 98% rename from assert/internal/deepequal_test.go rename to internal/reflects/deepequal_test.go index 9fedb97..8bf9f9b 100644 --- a/assert/internal/deepequal_test.go +++ b/internal/reflects/deepequal_test.go @@ -1,7 +1,7 @@ -package internal_test +package reflects_test import ( - "go.llib.dev/testcase/assert/internal" + "go.llib.dev/testcase/internal/reflects" "go.llib.dev/testcase/random" "reflect" "testing" @@ -240,7 +240,7 @@ func TestDeepEqual(t *testing.T) { } for _, tc := range tt { t.Run(tc.desc, func(t *testing.T) { - got, err := internal.DeepEqual(tc.v1, tc.v2) + got, err := reflects.DeepEqual(tc.v1, tc.v2) if !reflect.DeepEqual(tc.hasError, err) { t.Fatalf("DeepEqual() error = %v", err) } diff --git a/random/Make_test.go b/random/Make_test.go index 1771ae6..15ddb5c 100644 --- a/random/Make_test.go +++ b/random/Make_test.go @@ -107,9 +107,9 @@ func TestRandom_Make(t *testing.T) { var strings [42]string = rnd.Get(t).Make([42]string{}).([42]string) it.Must.NotNil(strings) - it.Must.AnyOf(func(anyOf *assert.AnyOf) { + it.Must.AnyOf(func(anyOf *assert.A) { for _, str := range strings { - anyOf.Test(func(it assert.It) { + anyOf.Case(func(it assert.It) { it.Must.NotEmpty(str) }) } @@ -120,9 +120,9 @@ func TestRandom_Make(t *testing.T) { var ints [42]int = rnd.Get(t).Make([42]int{}).([42]int) it.Must.NotNil(ints) - it.Must.AnyOf(func(anyOf *assert.AnyOf) { + it.Must.AnyOf(func(anyOf *assert.A) { for _, str := range ints { - anyOf.Test(func(it assert.It) { + anyOf.Case(func(it assert.It) { it.Must.NotEmpty(str) }) } @@ -134,9 +134,9 @@ func TestRandom_Make(t *testing.T) { var strings []string = rnd.Get(t).Make([]string{}).([]string) it.Must.NotNil(strings) - it.Must.AnyOf(func(anyOf *assert.AnyOf) { + it.Must.AnyOf(func(anyOf *assert.A) { for _, str := range strings { - anyOf.Test(func(it assert.It) { + anyOf.Case(func(it assert.It) { it.Must.NotEmpty(str) }) } @@ -147,9 +147,9 @@ func TestRandom_Make(t *testing.T) { var ints []int = rnd.Get(t).Make([]int{}).([]int) it.Must.NotNil(ints) - it.Must.AnyOf(func(anyOf *assert.AnyOf) { + it.Must.AnyOf(func(anyOf *assert.A) { for _, str := range ints { - anyOf.Test(func(it assert.It) { + anyOf.Case(func(it assert.It) { it.Must.NotEmpty(str) }) } @@ -291,9 +291,9 @@ func TestSlice_smoke(t *testing.T) { slice1 := random.Slice[int](length, rnd.Int) it.Must.Equal(length, len(slice1)) it.Must.NotEmpty(slice1) - it.Must.AnyOf(func(a *assert.AnyOf) { + it.Must.AnyOf(func(a *assert.A) { for _, v := range slice1 { - a.Test(func(it assert.It) { + a.Case(func(it assert.It) { it.Must.NotEmpty(v) }) } @@ -315,9 +315,9 @@ func TestMap_smoke(t *testing.T) { }) it.Must.Equal(length, len(map1)) it.Must.NotEmpty(map1) - it.Must.AnyOf(func(a *assert.AnyOf) { + it.Must.AnyOf(func(a *assert.A) { for k, v := range map1 { - a.Test(func(it assert.It) { + a.Case(func(it assert.It) { it.Must.NotEmpty(k) it.Must.NotEmpty(v) }) @@ -338,9 +338,9 @@ func TestMap_whenNotEnoughUniqueKeyCanBeGenerated_thenItReturnsWithLess(t *testi return rnd.SliceElement(keys).(string), rnd.Int() }) it.Must.NotEmpty(map1) - it.Must.AnyOf(func(a *assert.AnyOf) { + it.Must.AnyOf(func(a *assert.A) { for k, v := range map1 { - a.Test(func(it assert.It) { + a.Case(func(it assert.It) { it.Must.NotEmpty(k) it.Must.NotEmpty(v) }) diff --git a/random/Random_test.go b/random/Random_test.go index 840fa89..24f6cf4 100644 --- a/random/Random_test.go +++ b/random/Random_test.go @@ -458,9 +458,9 @@ func SpecRandomMethods(s *testcase.Spec, rnd testcase.Var[*random.Random]) { s.Then("it never returns a female name", func(t *testcase.T) { name := rnd.Get(t).Contact(sextype.Female).FirstName - t.Must.AnyOf(func(a *assert.AnyOf) { + t.Must.AnyOf(func(a *assert.A) { for i := 0; i < SamplingNumber; i++ { - a.Test(func(it assert.It) { + a.Case(func(it assert.It) { it.Must.NotEqual(name, act(t).FirstName) }) } @@ -481,9 +481,9 @@ func SpecRandomMethods(s *testcase.Spec, rnd testcase.Var[*random.Random]) { s.Then("it never returns a male name", func(t *testcase.T) { name := rnd.Get(t).Contact(sextype.Male).FirstName - t.Must.AnyOf(func(a *assert.AnyOf) { + t.Must.AnyOf(func(a *assert.A) { for i := 0; i < SamplingNumber; i++ { - a.Test(func(it assert.It) { + a.Case(func(it assert.It) { it.Must.NotEqual(name, act(t).FirstName) }) } @@ -617,9 +617,9 @@ func SpecRandomMethods(s *testcase.Spec, rnd testcase.Var[*random.Random]) { s.Then("it never returns a female name", func(t *testcase.T) { name := rnd.Get(t).Name().First(sextype.Female) - t.Must.AnyOf(func(a *assert.AnyOf) { + t.Must.AnyOf(func(a *assert.A) { for i := 0; i < SamplingNumber; i++ { - a.Test(func(it assert.It) { + a.Case(func(it assert.It) { it.Must.NotEqual(name, act(t)) }) } @@ -640,9 +640,9 @@ func SpecRandomMethods(s *testcase.Spec, rnd testcase.Var[*random.Random]) { s.Then("it never returns a male name", func(t *testcase.T) { name := rnd.Get(t).Name().First(sextype.Male) - t.Must.AnyOf(func(a *assert.AnyOf) { + t.Must.AnyOf(func(a *assert.A) { for i := 0; i < SamplingNumber; i++ { - a.Test(func(it assert.It) { + a.Case(func(it assert.It) { it.Must.NotEqual(name, act(t)) }) } diff --git a/random/Unique.go b/random/Unique.go new file mode 100644 index 0000000..f5fcc45 --- /dev/null +++ b/random/Unique.go @@ -0,0 +1,42 @@ +package random + +import ( + "go.llib.dev/testcase/clock" + "go.llib.dev/testcase/internal/reflects" + "time" +) + +// Unique function is a utility that helps with generating distinct values +// from those in a given exclusion list. +// If you need multiple unique values of the same type, +// this helper function can be useful for ensuring they're all different. +// +// rnd := random.New(random.CryptoSeed{}) +// v1 := random.Unique(rnd.Int) +// v2 := random.Unique(rnd.Int, v1) +// v3 := random.Unique(rnd.Int, v1, v2) +func Unique[T any](blk func() T, excludeList ...T) T { + if len(excludeList) == 0 { + return blk() + } + deadline := clock.TimeNow().Add(5 * time.Second) + for clock.TimeNow().Before(deadline) { + var ( + v T = blk() + ok bool = true + ) + for _, excluded := range excludeList { + isEqual, err := reflects.DeepEqual(v, excluded) + if err != nil { + panic(err.Error()) + } + if isEqual { + ok = false + } + } + if ok { + return v + } + } + panic("random.Unique failed to find a unique value") +} diff --git a/random/Unique_test.go b/random/Unique_test.go new file mode 100644 index 0000000..36c03bc --- /dev/null +++ b/random/Unique_test.go @@ -0,0 +1,64 @@ +package random_test + +import ( + "go.llib.dev/testcase/assert" + "go.llib.dev/testcase/clock/timecop" + "go.llib.dev/testcase/random" + "go.llib.dev/testcase/sandbox" + "testing" +) + +func ExampleUnique() { + // useful when you need random values which are not equal + rnd := random.New(random.CryptoSeed{}) + v1 := rnd.Int() + v2 := random.Unique(rnd.Int, v1) + v3 := random.Unique(rnd.Int, v1, v2) + + var tb testing.TB + assert.NotEqual(tb, v1, v3) + assert.NotEqual(tb, v2, v3) +} + +func TestUnique(t *testing.T) { + rnd := random.New(random.CryptoSeed{}) + t.Run("no exclude list given", func(t *testing.T) { + v := random.Unique(rnd.Int) + assert.NotEmpty(t, v) + }) + t.Run("exclude list has a value", func(t *testing.T) { + rnd.Repeat(128, 256, func() { + v1 := rnd.IntBetween(1, 3) + v2 := random.Unique(func() int { + return rnd.IntBetween(1, 3) + }, v1) + assert.NotEqual(t, v1, v2) + }) + }) + t.Run("exclude list has multiple values", func(t *testing.T) { + rnd.Repeat(128, 256, func() { + v1 := 0 + v2 := 1 + v3 := 2 + got := random.Unique(func() int { + return rnd.IntBetween(0, 3) + }, v1, v2, v3) + assert.NotEqual(t, got, v1) + assert.NotEqual(t, got, v2) + assert.NotEqual(t, got, v3) + }) + }) + t.Run("If the function takes too long to find a valid value, it will trigger a panic once a set time limit is reached", func(t *testing.T) { + timecop.SetSpeed(t, timecop.BlazingFast) + var ran bool + out := sandbox.Run(func() { + random.Unique(func() int { + ran = true + return 0 + }, 0) + }) + assert.True(t, ran) + assert.False(t, out.OK) + assert.NotEmpty(t, out.PanicValue) + }) +}