From 21a0594a4f389eb35fcd61f875638c85846de436 Mon Sep 17 00:00:00 2001 From: Adam Luzsi Date: Sun, 22 Sep 2024 21:04:12 +0200 Subject: [PATCH] instroduce assert.NoneOf --- assert/AnyOf.go | 66 ++++++++++++++- assert/AnyOf_test.go | 147 ++++++++++++++++++++++++++++++++- assert/example_test.go | 16 +++- assert/pkgfunc.go | 16 ---- internal/doubles/RecorderTB.go | 1 + 5 files changed, 225 insertions(+), 21 deletions(-) diff --git a/assert/AnyOf.go b/assert/AnyOf.go index 8007f7d..43d119a 100644 --- a/assert/AnyOf.go +++ b/assert/AnyOf.go @@ -51,7 +51,6 @@ func (ao *A) Case(blk func(t It)) { ao.mutex.Lock() defer ao.mutex.Unlock() ao.passed = true - return } // Test is an alias for A.Case @@ -90,3 +89,68 @@ func (ao *A) OK() bool { defer ao.mutex.Unlock() return ao.passed } + +// OneOf function checks a list of values and matches an expectation against each element of the list. +// If any slice element meets the assertion, it is considered passed. +func OneOf[T any](tb testing.TB, vs []T, blk func(t It, got T), 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...) +} + +// NoneOf function checks a list of values and matches an expectation against each element of the list. +// If any slice element meets the assertion, it is considered failed. +func NoneOf[T any](tb testing.TB, vs []T, blk func(t It, got T), msg ...Message) { + tb.Helper() + + var check = func(v T) bool { + tb.Helper() + dtb := &doubles.RecorderTB{TB: tb} + sandbox.Run(func() { + tb.Helper() + blk(MakeIt(dtb), v) + }) + + assertFailed := dtb.IsFailed + dtb.IsFailed = false // reset IsFailed for Cleanup + + sandbox.Run(func() { + tb.Helper() + dtb.CleanupNow() + }) + if hasCleanupFailed := dtb.IsFailed; hasCleanupFailed { + dtb.Forward() + } + + return assertFailed + } + + for i, v := range vs { + if !check(v) { + tb.Log(fmterror.Message{ + Method: "NoneOf", + Cause: "One of the element matched the expectations", + Message: toMsg(msg), + Values: []fmterror.Value{ + { + Label: "index", + Value: i, + }, + { + Label: "value", + Value: v, + }, + }, + }) + tb.FailNow() + } + } +} diff --git a/assert/AnyOf_test.go b/assert/AnyOf_test.go index 223f344..457bcd2 100644 --- a/assert/AnyOf_test.go +++ b/assert/AnyOf_test.go @@ -165,7 +165,7 @@ func TestOneOf(t *testing.T) { t.Must.False(stub.Get(t).IsFailed) }) - s.Then("execution context is not killed", func(t *testcase.T) { + s.Then("testing runtime is not killed", func(t *testcase.T) { t.Must.True(act(t).OK) }) @@ -187,7 +187,7 @@ func TestOneOf(t *testing.T) { t.Must.True(stub.Get(t).IsFailed) }) - s.Then("execution context is interrupted with FailNow", func(t *testcase.T) { + s.Then("testing runtime is interrupted with FailNow", func(t *testcase.T) { out := act(t) t.Must.False(out.OK) t.Must.True(out.Goexit) @@ -221,7 +221,7 @@ func TestOneOf(t *testing.T) { t.Must.False(stub.Get(t).IsFailed) }) - s.Then("execution context is not killed", func(t *testcase.T) { + s.Then("testing runtime is not killed", func(t *testcase.T) { t.Must.True(act(t).OK) }) @@ -233,6 +233,147 @@ func TestOneOf(t *testing.T) { }) } +func TestNoneOf(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.NoneOf(stub.Get(t), 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 failed", func(t *testcase.T) { + act(t) + + t.Must.True(stub.Get(t).IsFailed) + }) + + s.Then("testing runtime is not killed", func(t *testcase.T) { + t.Must.False(act(t).OK) + }) + + s.Then("assert message explanation is not logged", func(t *testcase.T) { + act(t) + + t.Must.Contain(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 not failed as all the assertion failed as expected", func(t *testcase.T) { + act(t) + + t.Must.False(stub.Get(t).IsFailed) + }) + + s.Then("testing runtime is not interrupted with FailNow", func(t *testcase.T) { + out := act(t) + t.Must.True(out.OK) + t.Must.False(out.Goexit) + }) + + 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("the assertion would fail, but the cleanup fails as well", func(s *testcase.Spec) { + blk.Let(s, func(t *testcase.T) func(assert.It, string) { + return func(i assert.It, s string) { + i.Cleanup(func() { + i.Log("cleanup-failed") + i.FailNow() + }) + + i.FailNow() + } + }) + + s.Then("the assertion fails because we don't expect failure in the cleanup", func(t *testcase.T) { + out := act(t) + t.Must.False(out.OK) + t.Must.True(out.Goexit) + assert.Contain(t, stub.Get(t).Logs.String(), "cleanup-failed") + }) + }) + + s.When("cleanup is part of the assertion block", func(s *testcase.Spec) { + cleanupOK := testcase.LetValue(s, false) + + blk.Let(s, func(t *testcase.T) func(assert.It, string) { + return func(i assert.It, s string) { + i.Cleanup(func() { cleanupOK.Set(t, true) }) + + if t.Random.Bool() { + i.FailNow() + } + } + }) + + s.Then("cleanup is done", func(t *testcase.T) { + act(t) + + assert.True(t, cleanupOK.Get(t)) + }) + }) + + s.When("assertions pass for at least one of the slice value", 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 marked as failed", func(t *testcase.T) { + act(t) + + t.Must.True(stub.Get(t).IsFailed) + }) + + s.Then("testing runtime is interrupted", func(t *testcase.T) { + out := act(t) + t.Must.False(out.OK) + t.Must.True(out.Goexit) + }) + + s.Then("assert message explanation is logged", 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(), "NoneOf") + t.Must.Contain(stub.Get(t).Logs.String(), "One of the element matched the expectations") + }) + }) +} + func TestA_Test_smoke(t *testing.T) { assert.AnyOf(t, func(a *assert.A) { a.Test(func(t assert.It) { diff --git a/assert/example_test.go b/assert/example_test.go index c8f1f39..3af9809 100644 --- a/assert/example_test.go +++ b/assert/example_test.go @@ -706,7 +706,7 @@ func ExampleAsserter_NotWithin() { a := assert.Must(tb) a.NotWithin(time.Second, func(ctx context.Context) { - return // FAIL + // FAIL }) a.NotWithin(time.Nanosecond, func(ctx context.Context) { @@ -723,6 +723,20 @@ func ExampleOneOf() { }, "optional assertion explanation") } +func ExampleNoneOf() { + var tb testing.TB + values := []string{"foo", "bar", "baz"} + + assert.NoneOf(tb, values, func(t assert.It, got string) { + assert.NotEmpty(t, got) + assert.True(t, strings.HasPrefix(got, "b")) + assert.True(t, strings.HasSuffix(got, "z")) + // at this point, our assertion passed for "baz", + // and NoneOf will report the failure + // that this value passed while it was not expected to. + }, "optional assertion explanation") +} + func ExampleAsserter_OneOf() { var tb testing.TB values := []string{"foo", "bar", "baz"} diff --git a/assert/pkgfunc.go b/assert/pkgfunc.go index 6650682..e2a5017 100644 --- a/assert/pkgfunc.go +++ b/assert/pkgfunc.go @@ -127,22 +127,6 @@ func Eventually[T time.Duration | int](tb testing.TB, durationOrCount T, blk fun 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(t 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 diff --git a/internal/doubles/RecorderTB.go b/internal/doubles/RecorderTB.go index fcc351e..6e99327 100644 --- a/internal/doubles/RecorderTB.go +++ b/internal/doubles/RecorderTB.go @@ -63,6 +63,7 @@ func (rtb *RecorderTB) Forward() { } func (rtb *RecorderTB) CleanupNow() { + rtb.TB.Helper() defer rtb.withPassthrough()() td := &teardown.Teardown{} for _, event := range rtb.records {