diff --git a/assert/assert_assertions.go b/assert/assert_assertions.go index 3ffa69906..961ea56dc 100644 --- a/assert/assert_assertions.go +++ b/assert/assert_assertions.go @@ -15,6 +15,48 @@ import ( "github.com/go-openapi/testify/v2/internal/assertions" ) +// Blocked asserts that a channel is blocked on receive. +// +// It always fails if the operand is not a channel, or if the channel is send-only. +// +// # Usage +// +// ch := make(chan struct{}) +// assertions.Blocked(t, ch) +// +// # Examples +// +// success: make(chan struct{}) +// failure: sendChanMessage() +// +// Upon failure, the test [T] is marked as failed and continues execution. +func Blocked(t T, ch any, msgAndArgs ...any) bool { + if h, ok := t.(H); ok { + h.Helper() + } + return assertions.Blocked(t, ch, msgAndArgs...) +} + +// BlockedT asserts that a channel is blocked on receive. +// +// # Usage +// +// ch := make(chan struct{}) +// assertions.BlockedT(t, ch) +// +// # Examples +// +// success: make(chan struct{}) +// failure: sendChanMessage() +// +// Upon failure, the test [T] is marked as failed and continues execution. +func BlockedT[E any, CHAN ~chan E](t T, ch CHAN, msgAndArgs ...any) bool { + if h, ok := t.(H); ok { + h.Helper() + } + return assertions.BlockedT[E, CHAN](t, ch, msgAndArgs...) +} + // Condition uses a comparison function to assert a complex condition. // // # Usage @@ -2168,6 +2210,56 @@ func NoGoRoutineLeak(t T, tested func(), msgAndArgs ...any) bool { return assertions.NoGoRoutineLeak(t, tested, msgAndArgs...) } +// NotBlocked asserts that a channel is not blocked on receive. +// +// It always fails if the operand is not a channel, or if the channel is send-only. +// +// A closed channel doesn't block and returns true. +// Notice that this consumes any message available in the channel. +// +// # Usage +// +// ch := make(chan struct{}, 1) +// ch <- struct{}{} +// assertions.NotBlocked(t, ch) +// +// # Examples +// +// success: sendChanMessage() +// failure: make(chan struct{}) +// +// Upon failure, the test [T] is marked as failed and continues execution. +func NotBlocked(t T, ch any, msgAndArgs ...any) bool { + if h, ok := t.(H); ok { + h.Helper() + } + return assertions.NotBlocked(t, ch, msgAndArgs...) +} + +// NotBlockedT asserts that a channel is not blocked on receive. +// +// A closed channel doesn't block and returns true. +// Notice that this consumes any message available in the channel. +// +// # Usage +// +// ch := make(chan struct{}, 1) +// ch <- struct{}{} +// assertions.NotBlockedT(t, ch) +// +// # Examples +// +// success: sendChanMessage() +// failure: make(chan struct{}) +// +// Upon failure, the test [T] is marked as failed and continues execution. +func NotBlockedT[E any, CHAN ~chan E](t T, ch CHAN, msgAndArgs ...any) bool { + if h, ok := t.(H); ok { + h.Helper() + } + return assertions.NotBlockedT[E, CHAN](t, ch, msgAndArgs...) +} + // NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the // specified substring or element. // diff --git a/assert/assert_assertions_test.go b/assert/assert_assertions_test.go index e5622eb31..285f6e3a4 100644 --- a/assert/assert_assertions_test.go +++ b/assert/assert_assertions_test.go @@ -17,6 +17,60 @@ import ( "time" ) +func TestBlocked(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + mock := new(mockT) + result := Blocked(mock, make(chan struct{})) + if !result { + t.Error("Blocked should return true on success") + } + }) + + t.Run("failure", func(t *testing.T) { + t.Parallel() + + mock := new(mockT) + result := Blocked(mock, sendChanMessage()) + if result { + t.Error("Blocked should return false on failure") + } + if !mock.failed { + t.Error("Blocked should mark test as failed") + } + }) +} + +func TestBlockedT(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + mock := new(mockT) + result := BlockedT(mock, make(chan struct{})) + if !result { + t.Error("BlockedT should return true on success") + } + }) + + t.Run("failure", func(t *testing.T) { + t.Parallel() + + mock := new(mockT) + result := BlockedT(mock, sendChanMessage()) + if result { + t.Error("BlockedT should return false on failure") + } + if !mock.failed { + t.Error("BlockedT should mark test as failed") + } + }) +} + func TestCondition(t *testing.T) { t.Parallel() @@ -2198,6 +2252,60 @@ func TestNoGoRoutineLeak(t *testing.T) { }) } +func TestNotBlocked(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + mock := new(mockT) + result := NotBlocked(mock, sendChanMessage()) + if !result { + t.Error("NotBlocked should return true on success") + } + }) + + t.Run("failure", func(t *testing.T) { + t.Parallel() + + mock := new(mockT) + result := NotBlocked(mock, make(chan struct{})) + if result { + t.Error("NotBlocked should return false on failure") + } + if !mock.failed { + t.Error("NotBlocked should mark test as failed") + } + }) +} + +func TestNotBlockedT(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + mock := new(mockT) + result := NotBlockedT(mock, sendChanMessage()) + if !result { + t.Error("NotBlockedT should return true on success") + } + }) + + t.Run("failure", func(t *testing.T) { + t.Parallel() + + mock := new(mockT) + result := NotBlockedT(mock, make(chan struct{})) + if result { + t.Error("NotBlockedT should return false on failure") + } + if !mock.failed { + t.Error("NotBlockedT should mark test as failed") + } + }) +} + func TestNotContains(t *testing.T) { t.Parallel() @@ -3583,6 +3691,13 @@ func httpBody(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintf(w, "Hello, %s!", name) } +func sendChanMessage() chan struct{} { + ch := make(chan struct{}, 1) + ch <- struct{}{} + + return ch +} + //nolint:gochecknoglobals // this is on purpose to share a common pointer when testing var ( staticVar = "static string" @@ -3608,4 +3723,6 @@ func (d *dummyError) Error() string { return "dummy error" } +var dummyChan = make(chan struct{}) + type myType float64 diff --git a/assert/assert_examples_test.go b/assert/assert_examples_test.go index 35d913e93..37f84c86d 100644 --- a/assert/assert_examples_test.go +++ b/assert/assert_examples_test.go @@ -20,6 +20,22 @@ import ( "github.com/go-openapi/testify/v2/assert" ) +func ExampleBlocked() { + t := new(testing.T) // should come from testing, e.g. func TestBlocked(t *testing.T) + success := assert.Blocked(t, make(chan struct{})) + fmt.Printf("success: %t\n", success) + + // Output: success: true +} + +func ExampleBlockedT() { + t := new(testing.T) // should come from testing, e.g. func TestBlockedT(t *testing.T) + success := assert.BlockedT(t, make(chan struct{})) + fmt.Printf("success: %t\n", success) + + // Output: success: true +} + func ExampleCondition() { t := new(testing.T) // should come from testing, e.g. func TestCondition(t *testing.T) success := assert.Condition(t, func() bool { @@ -688,6 +704,22 @@ func ExampleNoGoRoutineLeak() { // Output: success: true } +func ExampleNotBlocked() { + t := new(testing.T) // should come from testing, e.g. func TestNotBlocked(t *testing.T) + success := assert.NotBlocked(t, sendChanMessage()) + fmt.Printf("success: %t\n", success) + + // Output: success: true +} + +func ExampleNotBlockedT() { + t := new(testing.T) // should come from testing, e.g. func TestNotBlockedT(t *testing.T) + success := assert.NotBlockedT(t, sendChanMessage()) + fmt.Printf("success: %t\n", success) + + // Output: success: true +} + func ExampleNotContains() { t := new(testing.T) // should come from testing, e.g. func TestNotContains(t *testing.T) success := assert.NotContains(t, []string{"A", "B"}, "C") @@ -1108,6 +1140,13 @@ func httpBody(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintf(w, "Hello, %s!", name) } +func sendChanMessage() chan struct{} { + ch := make(chan struct{}, 1) + ch <- struct{}{} + + return ch +} + //nolint:gochecknoglobals // this is on purpose to share a common pointer when testing var ( staticVar = "static string" @@ -1133,4 +1172,6 @@ func (d *dummyError) Error() string { return "dummy error" } +var dummyChan = make(chan struct{}) + type myType float64 diff --git a/assert/assert_format.go b/assert/assert_format.go index 6af8c4fed..0a14dfaa2 100644 --- a/assert/assert_format.go +++ b/assert/assert_format.go @@ -15,6 +15,26 @@ import ( "github.com/go-openapi/testify/v2/internal/assertions" ) +// Blockedf is the same as [Blocked], but it accepts a format string to format arguments like [fmt.Printf]. +// +// Upon failure, the test [T] is marked as failed and continues execution. +func Blockedf(t T, ch any, msg string, args ...any) bool { + if h, ok := t.(H); ok { + h.Helper() + } + return assertions.Blocked(t, ch, forwardArgs(msg, args)) +} + +// BlockedTf is the same as [BlockedT], but it accepts a format string to format arguments like [fmt.Printf]. +// +// Upon failure, the test [T] is marked as failed and continues execution. +func BlockedTf[E any, CHAN ~chan E](t T, ch CHAN, msg string, args ...any) bool { + if h, ok := t.(H); ok { + h.Helper() + } + return assertions.BlockedT[E, CHAN](t, ch, forwardArgs(msg, args)) +} + // Conditionf is the same as [Condition], but it accepts a format string to format arguments like [fmt.Printf]. // // Upon failure, the test [T] is marked as failed and continues execution. @@ -835,6 +855,26 @@ func NoGoRoutineLeakf(t T, tested func(), msg string, args ...any) bool { return assertions.NoGoRoutineLeak(t, tested, forwardArgs(msg, args)) } +// NotBlockedf is the same as [NotBlocked], but it accepts a format string to format arguments like [fmt.Printf]. +// +// Upon failure, the test [T] is marked as failed and continues execution. +func NotBlockedf(t T, ch any, msg string, args ...any) bool { + if h, ok := t.(H); ok { + h.Helper() + } + return assertions.NotBlocked(t, ch, forwardArgs(msg, args)) +} + +// NotBlockedTf is the same as [NotBlockedT], but it accepts a format string to format arguments like [fmt.Printf]. +// +// Upon failure, the test [T] is marked as failed and continues execution. +func NotBlockedTf[E any, CHAN ~chan E](t T, ch CHAN, msg string, args ...any) bool { + if h, ok := t.(H); ok { + h.Helper() + } + return assertions.NotBlockedT[E, CHAN](t, ch, forwardArgs(msg, args)) +} + // NotContainsf is the same as [NotContains], but it accepts a format string to format arguments like [fmt.Printf]. // // Upon failure, the test [T] is marked as failed and continues execution. diff --git a/assert/assert_format_test.go b/assert/assert_format_test.go index f7c8f67f6..6c4e42a05 100644 --- a/assert/assert_format_test.go +++ b/assert/assert_format_test.go @@ -17,6 +17,60 @@ import ( "time" ) +func TestBlockedf(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + mock := new(mockT) + result := Blockedf(mock, make(chan struct{}), "test message") + if !result { + t.Error("Blockedf should return true on success") + } + }) + + t.Run("failure", func(t *testing.T) { + t.Parallel() + + mock := new(mockT) + result := Blockedf(mock, sendChanMessage(), "test message") + if result { + t.Error("Blockedf should return false on failure") + } + if !mock.failed { + t.Error("Blockedf should mark test as failed") + } + }) +} + +func TestBlockedTf(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + mock := new(mockT) + result := BlockedTf(mock, make(chan struct{}), "test message") + if !result { + t.Error("BlockedTf should return true on success") + } + }) + + t.Run("failure", func(t *testing.T) { + t.Parallel() + + mock := new(mockT) + result := BlockedTf(mock, sendChanMessage(), "test message") + if result { + t.Error("BlockedTf should return false on failure") + } + if !mock.failed { + t.Error("BlockedTf should mark test as failed") + } + }) +} + func TestConditionf(t *testing.T) { t.Parallel() @@ -2198,6 +2252,60 @@ func TestNoGoRoutineLeakf(t *testing.T) { }) } +func TestNotBlockedf(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + mock := new(mockT) + result := NotBlockedf(mock, sendChanMessage(), "test message") + if !result { + t.Error("NotBlockedf should return true on success") + } + }) + + t.Run("failure", func(t *testing.T) { + t.Parallel() + + mock := new(mockT) + result := NotBlockedf(mock, make(chan struct{}), "test message") + if result { + t.Error("NotBlockedf should return false on failure") + } + if !mock.failed { + t.Error("NotBlockedf should mark test as failed") + } + }) +} + +func TestNotBlockedTf(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + mock := new(mockT) + result := NotBlockedTf(mock, sendChanMessage(), "test message") + if !result { + t.Error("NotBlockedTf should return true on success") + } + }) + + t.Run("failure", func(t *testing.T) { + t.Parallel() + + mock := new(mockT) + result := NotBlockedTf(mock, make(chan struct{}), "test message") + if result { + t.Error("NotBlockedTf should return false on failure") + } + if !mock.failed { + t.Error("NotBlockedTf should mark test as failed") + } + }) +} + func TestNotContainsf(t *testing.T) { t.Parallel() diff --git a/assert/assert_forward.go b/assert/assert_forward.go index bb7614204..af1c8a65c 100644 --- a/assert/assert_forward.go +++ b/assert/assert_forward.go @@ -30,6 +30,26 @@ func New(t T) *Assertions { } } +// Blocked is the same as [Blocked], as a method rather than a package-level function. +// +// Upon failure, the test [T] is marked as failed and continues execution. +func (a *Assertions) Blocked(ch any, msgAndArgs ...any) bool { + if h, ok := a.T.(H); ok { + h.Helper() + } + return assertions.Blocked(a.T, ch, msgAndArgs...) +} + +// Blockedf is the same as [Assertions.Blocked], but it accepts a format string to format arguments like [fmt.Printf]. +// +// Upon failure, the test [T] is marked as failed and continues execution. +func (a *Assertions) Blockedf(ch any, msg string, args ...any) bool { + if h, ok := a.T.(H); ok { + h.Helper() + } + return assertions.Blocked(a.T, ch, forwardArgs(msg, args)) +} + // Condition is the same as [Condition], as a method rather than a package-level function. // // Upon failure, the test [T] is marked as failed and continues execution. @@ -1110,6 +1130,26 @@ func (a *Assertions) NoGoRoutineLeakf(tested func(), msg string, args ...any) bo return assertions.NoGoRoutineLeak(a.T, tested, forwardArgs(msg, args)) } +// NotBlocked is the same as [NotBlocked], as a method rather than a package-level function. +// +// Upon failure, the test [T] is marked as failed and continues execution. +func (a *Assertions) NotBlocked(ch any, msgAndArgs ...any) bool { + if h, ok := a.T.(H); ok { + h.Helper() + } + return assertions.NotBlocked(a.T, ch, msgAndArgs...) +} + +// NotBlockedf is the same as [Assertions.NotBlocked], but it accepts a format string to format arguments like [fmt.Printf]. +// +// Upon failure, the test [T] is marked as failed and continues execution. +func (a *Assertions) NotBlockedf(ch any, msg string, args ...any) bool { + if h, ok := a.T.(H); ok { + h.Helper() + } + return assertions.NotBlocked(a.T, ch, forwardArgs(msg, args)) +} + // NotContains is the same as [NotContains], as a method rather than a package-level function. // // Upon failure, the test [T] is marked as failed and continues execution. diff --git a/assert/assert_forward_test.go b/assert/assert_forward_test.go index 455a03729..e5f35f411 100644 --- a/assert/assert_forward_test.go +++ b/assert/assert_forward_test.go @@ -16,6 +16,35 @@ import ( "time" ) +func TestAssertionsBlocked(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + mock := new(mockT) + a := New(mock) + result := a.Blocked(make(chan struct{})) + if !result { + t.Error("Assertions.Blocked should return true on success") + } + }) + + t.Run("failure", func(t *testing.T) { + t.Parallel() + + mock := new(mockT) + a := New(mock) + result := a.Blocked(sendChanMessage()) + if result { + t.Error("Assertions.Blocked should return false on failure") + } + if !mock.failed { + t.Error("Assertions.Blocked should mark test as failed") + } + }) +} + func TestAssertionsCondition(t *testing.T) { t.Parallel() @@ -1532,6 +1561,35 @@ func TestAssertionsNoGoRoutineLeak(t *testing.T) { }) } +func TestAssertionsNotBlocked(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + mock := new(mockT) + a := New(mock) + result := a.NotBlocked(sendChanMessage()) + if !result { + t.Error("Assertions.NotBlocked should return true on success") + } + }) + + t.Run("failure", func(t *testing.T) { + t.Parallel() + + mock := new(mockT) + a := New(mock) + result := a.NotBlocked(make(chan struct{})) + if result { + t.Error("Assertions.NotBlocked should return false on failure") + } + if !mock.failed { + t.Error("Assertions.NotBlocked should mark test as failed") + } + }) +} + func TestAssertionsNotContains(t *testing.T) { t.Parallel() @@ -2326,6 +2384,35 @@ func TestAssertionsZero(t *testing.T) { }) } +func TestAssertionsBlockedf(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + mock := new(mockT) + a := New(mock) + result := a.Blockedf(make(chan struct{}), "test message") + if !result { + t.Error("Assertions.Blockedf should return true on success") + } + }) + + t.Run("failure", func(t *testing.T) { + t.Parallel() + + mock := new(mockT) + a := New(mock) + result := a.Blockedf(sendChanMessage(), "test message") + if result { + t.Error("Assertions.Blockedf should return false on failure") + } + if !mock.failed { + t.Error("Assertions.Blockedf should mark test as failed") + } + }) +} + func TestAssertionsConditionf(t *testing.T) { t.Parallel() @@ -3842,6 +3929,35 @@ func TestAssertionsNoGoRoutineLeakf(t *testing.T) { }) } +func TestAssertionsNotBlockedf(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + mock := new(mockT) + a := New(mock) + result := a.NotBlockedf(sendChanMessage(), "test message") + if !result { + t.Error("Assertions.NotBlockedf should return true on success") + } + }) + + t.Run("failure", func(t *testing.T) { + t.Parallel() + + mock := new(mockT) + a := New(mock) + result := a.NotBlockedf(make(chan struct{}), "test message") + if result { + t.Error("Assertions.NotBlockedf should return false on failure") + } + if !mock.failed { + t.Error("Assertions.NotBlockedf should mark test as failed") + } + }) +} + func TestAssertionsNotContainsf(t *testing.T) { t.Parallel() diff --git a/codegen/internal/generator/templates/assertion_test_shared.gotmpl b/codegen/internal/generator/templates/assertion_test_shared.gotmpl index aed2c1045..2801a8b5a 100644 --- a/codegen/internal/generator/templates/assertion_test_shared.gotmpl +++ b/codegen/internal/generator/templates/assertion_test_shared.gotmpl @@ -20,6 +20,13 @@ func httpBody(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintf(w, "Hello, %s!", name) } +func sendChanMessage() chan struct{} { + ch := make(chan struct{}, 1) + ch <- struct{}{} + + return ch +} + //nolint:gochecknoglobals // this is on purpose to share a common pointer when testing var ( staticVar = "static string" @@ -45,6 +52,8 @@ func (d *dummyError) Error() string { return "dummy error" } +var dummyChan = make(chan struct{}) + type myType float64 {{- end }} diff --git a/docs/doc-site/api/_index.md b/docs/doc-site/api/_index.md index 46c7fac0c..5c167c590 100644 --- a/docs/doc-site/api/_index.md +++ b/docs/doc-site/api/_index.md @@ -34,7 +34,7 @@ Each domain contains assertions regrouped by their use case (e.g. http, json, er - [Boolean](./boolean.md) - Asserting Boolean Values (4) - [Collection](./collection.md) - Asserting Slices And Maps (23) - [Comparison](./comparison.md) - Comparing Ordered Values (12) -- [Condition](./condition.md) - Expressing Assertions Using Conditions (5) +- [Condition](./condition.md) - Expressing Assertions Using Conditions (9) - [Equality](./equality.md) - Asserting Two Things Are Equal (16) - [Error](./error.md) - Asserting Errors (8) - [File](./file.md) - Asserting OS Files (6) diff --git a/docs/doc-site/api/condition.md b/docs/doc-site/api/condition.md index 8ff6b8484..b01feadcb 100644 --- a/docs/doc-site/api/condition.md +++ b/docs/doc-site/api/condition.md @@ -5,6 +5,10 @@ weight: 4 domains: - "condition" keywords: + - "Blocked" + - "Blockedf" + - "BlockedT" + - "BlockedTf" - "Condition" - "Conditionf" - "Consistently" @@ -15,6 +19,10 @@ keywords: - "EventuallyWithf" - "Never" - "Neverf" + - "NotBlocked" + - "NotBlockedf" + - "NotBlockedT" + - "NotBlockedTf" --- Expressing Assertions Using Conditions @@ -26,17 +34,243 @@ Expressing Assertions Using Conditions _All links point to _ -This domain exposes 5 functionalities. +This domain exposes 9 functionalities. Generic assertions are marked with a {{% icon icon="star" color=orange %}}. ```tree +- [Blocked](#blocked) | angles-right +- [BlockedT[E any, CHAN ~chan E]](#blockedte-any-chan-chan-e) | star | orange - [Condition](#condition) | angles-right - [Consistently[C Conditioner]](#consistentlyc-conditioner) | star | orange - [Eventually[C Conditioner]](#eventuallyc-conditioner) | star | orange - [EventuallyWith[C CollectibleConditioner]](#eventuallywithc-collectibleconditioner) | star | orange - [Never[C NeverConditioner]](#neverc-neverconditioner) | star | orange +- [NotBlocked](#notblocked) | angles-right +- [NotBlockedT[E any, CHAN ~chan E]](#notblockedte-any-chan-chan-e) | star | orange ``` +### Blocked{#blocked} +Blocked asserts that a channel is blocked on receive. + +It always fails if the operand is not a channel, or if the channel is send-only. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + ch := make(chan struct{}) + assertions.Blocked(t, ch) + success: make(chan struct{}) + failure: sendChanMessage() +``` +{{< /tab >}} +{{% tab title="Testable Examples (assert)" %}} +{{% cards %}} +{{% card %}} + + +*[Copy and click to open Go Playground](https://go.dev/play/)* + + +```go +// real-world test would inject *testing.T from TestBlocked(t *testing.T) +package main + +import ( + "fmt" + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + +func main() { + t := new(testing.T) // should come from testing, e.g. func TestBlocked(t *testing.T) + success := assert.Blocked(t, make(chan struct{})) + fmt.Printf("success: %t\n", success) + +} + +``` +{{% /card %}} + + +{{% /cards %}} +{{< /tab >}} + + +{{% tab title="Testable Examples (require)" %}} +{{% cards %}} +{{% card %}} + + +*[Copy and click to open Go Playground](https://go.dev/play/)* + + +```go +// real-world test would inject *testing.T from TestBlocked(t *testing.T) +package main + +import ( + "fmt" + "testing" + + "github.com/go-openapi/testify/v2/require" +) + +func main() { + t := new(testing.T) // should come from testing, e.g. func TestBlocked(t *testing.T) + require.Blocked(t, make(chan struct{})) + fmt.Println("passed") + +} + +``` +{{% /card %}} + + +{{% /cards %}} +{{< /tab >}} + + +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} + +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.Blocked(t T, ch any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Blocked) | package-level function | +| [`assert.Blockedf(t T, ch any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Blockedf) | formatted variant | +| [`assert.(*Assertions).Blocked(ch any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Blocked) | method variant | +| [`assert.(*Assertions).Blockedf(ch any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Blockedf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.Blocked(t T, ch any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Blocked) | package-level function | +| [`require.Blockedf(t T, ch any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Blockedf) | formatted variant | +| [`require.(*Assertions).Blocked(ch any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Blocked) | method variant | +| [`require.(*Assertions).Blockedf(ch any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Blockedf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.Blocked(t T, ch any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Blocked) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Blocked](https://github.com/go-openapi/testify/blob/master/internal/assertions/condition.go#L56) +{{% /tab %}} +{{< /tabs >}} + +### BlockedT[E any, CHAN ~chan E] {{% icon icon="star" color=orange %}}{#blockedte-any-chan-chan-e} +BlockedT asserts that a channel is blocked on receive. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + ch := make(chan struct{}) + assertions.BlockedT(t, ch) + success: make(chan struct{}) + failure: sendChanMessage() +``` +{{< /tab >}} +{{% tab title="Testable Examples (assert)" %}} +{{% cards %}} +{{% card %}} + + +*[Copy and click to open Go Playground](https://go.dev/play/)* + + +```go +// real-world test would inject *testing.T from TestBlockedT(t *testing.T) +package main + +import ( + "fmt" + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + +func main() { + t := new(testing.T) // should come from testing, e.g. func TestBlockedT(t *testing.T) + success := assert.BlockedT(t, make(chan struct{})) + fmt.Printf("success: %t\n", success) + +} + +``` +{{% /card %}} + + +{{% /cards %}} +{{< /tab >}} + + +{{% tab title="Testable Examples (require)" %}} +{{% cards %}} +{{% card %}} + + +*[Copy and click to open Go Playground](https://go.dev/play/)* + + +```go +// real-world test would inject *testing.T from TestBlockedT(t *testing.T) +package main + +import ( + "fmt" + "testing" + + "github.com/go-openapi/testify/v2/require" +) + +func main() { + t := new(testing.T) // should come from testing, e.g. func TestBlockedT(t *testing.T) + require.BlockedT(t, make(chan struct{})) + fmt.Println("passed") + +} + +``` +{{% /card %}} + + +{{% /cards %}} +{{< /tab >}} + + +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} + +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.BlockedT[E any, CHAN ~chan E](t T, ch CHAN, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#BlockedT) | package-level function | +| [`assert.BlockedTf[E any, CHAN ~chan E](t T, ch CHAN, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#BlockedTf) | formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.BlockedT[E any, CHAN ~chan E](t T, ch CHAN, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#BlockedT) | package-level function | +| [`require.BlockedTf[E any, CHAN ~chan E](t T, ch CHAN, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#BlockedTf) | formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.BlockedT[E any, CHAN ~chan E](t T, ch CHAN, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#BlockedT) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#BlockedT](https://github.com/go-openapi/testify/blob/master/internal/assertions/condition.go#L104) +{{% /tab %}} +{{< /tabs >}} + ### Condition{#condition} Condition uses a comparison function to assert a complex condition. @@ -148,7 +382,7 @@ func main() { |--|--| | [`assertions.Condition(t T, comp func() bool, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Condition) | internal implementation | -**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Condition](https://github.com/go-openapi/testify/blob/master/internal/assertions/condition.go#L28) +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Condition](https://github.com/go-openapi/testify/blob/master/internal/assertions/condition.go#L29) {{% /tab %}} {{< /tabs >}} @@ -531,7 +765,7 @@ func main() { |--|--| | [`assertions.Consistently[C Conditioner](t T, condition C, timeout time.Duration, tick time.Duration, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Consistently) | internal implementation | -**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Consistently](https://github.com/go-openapi/testify/blob/master/internal/assertions/condition.go#L253) +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Consistently](https://github.com/go-openapi/testify/blob/master/internal/assertions/condition.go#L414) {{% /tab %}} {{< /tabs >}} @@ -1053,7 +1287,7 @@ func main() { |--|--| | [`assertions.Eventually[C Conditioner](t T, condition C, timeout time.Duration, tick time.Duration, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Eventually) | internal implementation | -**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Eventually](https://github.com/go-openapi/testify/blob/master/internal/assertions/condition.go#L129) +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Eventually](https://github.com/go-openapi/testify/blob/master/internal/assertions/condition.go#L290) {{% /tab %}} {{< /tabs >}} @@ -1300,7 +1534,7 @@ func main() { |--|--| | [`assertions.EventuallyWith[C CollectibleConditioner](t T, condition C, timeout time.Duration, tick time.Duration, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#EventuallyWith) | internal implementation | -**Source:** [github.com/go-openapi/testify/v2/internal/assertions#EventuallyWith](https://github.com/go-openapi/testify/blob/master/internal/assertions/condition.go#L330) +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#EventuallyWith](https://github.com/go-openapi/testify/blob/master/internal/assertions/condition.go#L491) {{% /tab %}} {{< /tabs >}} @@ -1600,7 +1834,265 @@ func main() { |--|--| | [`assertions.Never[C NeverConditioner](t T, condition C, timeout time.Duration, tick time.Duration, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Never) | internal implementation | -**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Never](https://github.com/go-openapi/testify/blob/master/internal/assertions/condition.go#L187) +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Never](https://github.com/go-openapi/testify/blob/master/internal/assertions/condition.go#L348) +{{% /tab %}} +{{< /tabs >}} + +### NotBlocked{#notblocked} +NotBlocked asserts that a channel is not blocked on receive. + +It always fails if the operand is not a channel, or if the channel is send-only. + +A closed channel doesn't block and returns true. +Notice that this consumes any message available in the channel. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + ch := make(chan struct{}, 1) + ch <- struct{}{} + assertions.NotBlocked(t, ch) + success: sendChanMessage() + failure: make(chan struct{}) +``` +{{< /tab >}} +{{% tab title="Testable Examples (assert)" %}} +{{% cards %}} +{{% card %}} + + +*[Copy and click to open Go Playground](https://go.dev/play/)* + + +```go +// real-world test would inject *testing.T from TestNotBlocked(t *testing.T) +package main + +import ( + "fmt" + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + +func main() { + t := new(testing.T) // should come from testing, e.g. func TestNotBlocked(t *testing.T) + success := assert.NotBlocked(t, sendChanMessage()) + fmt.Printf("success: %t\n", success) + +} + +func sendChanMessage() chan struct{} { + ch := make(chan struct{}, 1) + ch <- struct{}{} + + return ch +} + +``` +{{% /card %}} + + +{{% /cards %}} +{{< /tab >}} + + +{{% tab title="Testable Examples (require)" %}} +{{% cards %}} +{{% card %}} + + +*[Copy and click to open Go Playground](https://go.dev/play/)* + + +```go +// real-world test would inject *testing.T from TestNotBlocked(t *testing.T) +package main + +import ( + "fmt" + "testing" + + "github.com/go-openapi/testify/v2/require" +) + +func main() { + t := new(testing.T) // should come from testing, e.g. func TestNotBlocked(t *testing.T) + require.NotBlocked(t, sendChanMessage()) + fmt.Println("passed") + +} + +func sendChanMessage() chan struct{} { + ch := make(chan struct{}, 1) + ch <- struct{}{} + + return ch +} + +``` +{{% /card %}} + + +{{% /cards %}} +{{< /tab >}} + + +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} + +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.NotBlocked(t T, ch any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotBlocked) | package-level function | +| [`assert.NotBlockedf(t T, ch any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotBlockedf) | formatted variant | +| [`assert.(*Assertions).NotBlocked(ch any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotBlocked) | method variant | +| [`assert.(*Assertions).NotBlockedf(ch any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotBlockedf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.NotBlocked(t T, ch any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotBlocked) | package-level function | +| [`require.NotBlockedf(t T, ch any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotBlockedf) | formatted variant | +| [`require.(*Assertions).NotBlocked(ch any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotBlocked) | method variant | +| [`require.(*Assertions).NotBlockedf(ch any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotBlockedf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.NotBlocked(t T, ch any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#NotBlocked) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#NotBlocked](https://github.com/go-openapi/testify/blob/master/internal/assertions/condition.go#L141) +{{% /tab %}} +{{< /tabs >}} + +### NotBlockedT[E any, CHAN ~chan E] {{% icon icon="star" color=orange %}}{#notblockedte-any-chan-chan-e} +NotBlockedT asserts that a channel is not blocked on receive. + +A closed channel doesn't block and returns true. +Notice that this consumes any message available in the channel. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + ch := make(chan struct{}, 1) + ch <- struct{}{} + assertions.NotBlockedT(t, ch) + success: sendChanMessage() + failure: make(chan struct{}) +``` +{{< /tab >}} +{{% tab title="Testable Examples (assert)" %}} +{{% cards %}} +{{% card %}} + + +*[Copy and click to open Go Playground](https://go.dev/play/)* + + +```go +// real-world test would inject *testing.T from TestNotBlockedT(t *testing.T) +package main + +import ( + "fmt" + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + +func main() { + t := new(testing.T) // should come from testing, e.g. func TestNotBlockedT(t *testing.T) + success := assert.NotBlockedT(t, sendChanMessage()) + fmt.Printf("success: %t\n", success) + +} + +func sendChanMessage() chan struct{} { + ch := make(chan struct{}, 1) + ch <- struct{}{} + + return ch +} + +``` +{{% /card %}} + + +{{% /cards %}} +{{< /tab >}} + + +{{% tab title="Testable Examples (require)" %}} +{{% cards %}} +{{% card %}} + + +*[Copy and click to open Go Playground](https://go.dev/play/)* + + +```go +// real-world test would inject *testing.T from TestNotBlockedT(t *testing.T) +package main + +import ( + "fmt" + "testing" + + "github.com/go-openapi/testify/v2/require" +) + +func main() { + t := new(testing.T) // should come from testing, e.g. func TestNotBlockedT(t *testing.T) + require.NotBlockedT(t, sendChanMessage()) + fmt.Println("passed") + +} + +func sendChanMessage() chan struct{} { + ch := make(chan struct{}, 1) + ch <- struct{}{} + + return ch +} + +``` +{{% /card %}} + + +{{% /cards %}} +{{< /tab >}} + + +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} + +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.NotBlockedT[E any, CHAN ~chan E](t T, ch CHAN, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotBlockedT) | package-level function | +| [`assert.NotBlockedTf[E any, CHAN ~chan E](t T, ch CHAN, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotBlockedTf) | formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.NotBlockedT[E any, CHAN ~chan E](t T, ch CHAN, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotBlockedT) | package-level function | +| [`require.NotBlockedTf[E any, CHAN ~chan E](t T, ch CHAN, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotBlockedTf) | formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.NotBlockedT[E any, CHAN ~chan E](t T, ch CHAN, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#NotBlockedT) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#NotBlockedT](https://github.com/go-openapi/testify/blob/master/internal/assertions/condition.go#L188) {{% /tab %}} {{< /tabs >}} diff --git a/docs/doc-site/api/metrics.md b/docs/doc-site/api/metrics.md index 3dc2283ea..cb39474d1 100644 --- a/docs/doc-site/api/metrics.md +++ b/docs/doc-site/api/metrics.md @@ -15,14 +15,14 @@ Counts for core functionality, and generated variants (formatted, forward, forwa | Kind | Count | Note | | ------------------------- | ----------------- | ---- | -| All core functions | 137 | Maintained core | -| All core assertions | 133 | Usage with `*testing.T` | -| Generic assertions | 51 | Type-safe assertions ("T" suffix) | +| All core functions | 141 | Maintained core | +| All core assertions | 137 | Usage with `*testing.T` | +| Generic assertions | 53 | Type-safe assertions ("T" suffix) | | Helpers (not assertions) | 4 | General-purpose utilities, not assertions | | Others | 0 | | -| assert/require variants | 430 | Generated variants | -| Total assertions variants | 860 | Available assertions API | -| Total API surface | 870 | | +| assert/require variants | 442 | Generated variants | +| Total assertions variants | 884 | Available assertions API | +| Total API surface | 894 | | ## Quick index @@ -30,6 +30,8 @@ Table of core assertions, excluding variants. Each function is side by side with | Assertion | Opposite | Domain | Kind | | ------------------------ | ----------------- | ------ | ---- | +| [Blocked](condition/#blocked) | [NotBlocked](condition/#notblocked) | condition | | +| [BlockedT[E any, CHAN ~chan E]](condition/#blockedte-any-chan-chan-e) {{% icon icon="star" color=orange %}} | [NotBlockedT](condition/#notblockedte-any-chan-chan-e) | condition | | | [CallerInfo](common/#callerinfo) | | common | helper | | [Condition](condition/#condition) | | condition | | | [Consistently[C Conditioner]](condition/#consistentlyc-conditioner) {{% icon icon="star" color=orange %}} | | condition | | diff --git a/docs/doc-site/api/number.md b/docs/doc-site/api/number.md index 65ce72d74..697444fd5 100644 --- a/docs/doc-site/api/number.md +++ b/docs/doc-site/api/number.md @@ -765,8 +765,6 @@ Python's [math.isclose](https://docs.python.org/3/library/math.html#math.isclose See also [InEpsilon](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#InEpsilon). - - #### Behavior with IEEE floating point arithmetic - NaN is matched only by a NaN, e.g. this works: [InEpsilonSymmetric]([math.NaN](), [math.Sqrt](-1), 0.0) diff --git a/docs/doc-site/project/maintainers/ROADMAP.md b/docs/doc-site/project/maintainers/ROADMAP.md index 8e91b8f0a..35dcae504 100644 --- a/docs/doc-site/project/maintainers/ROADMAP.md +++ b/docs/doc-site/project/maintainers/ROADMAP.md @@ -39,8 +39,8 @@ timeline section Q2 2026 📝 v2.5 (May 2026) : synctest opt-in for Eventually, Never, Consistently, EventuallyWith : NoFileDescriptorLeak (macOS) + : InEpsilonSymmetric, Blocked/NotBlocked : export internal tools (spew, difflib) - : New candidate features from upstream : go1.25+ 🔍 v2.6 (June 2026) : (tentative) : go build guards (codegen) diff --git a/docs/doc-site/usage/TRACKING.md b/docs/doc-site/usage/TRACKING.md index 0ec9253de..29711fb04 100644 --- a/docs/doc-site/usage/TRACKING.md +++ b/docs/doc-site/usage/TRACKING.md @@ -86,12 +86,14 @@ This table catalogs all upstream PRs and issues from [github.com/stretchr/testif | [#1606] | PR | Consistently assertion | ✅ Adapted | | [#1848] | PR | Subset (garbled error message) | ✅ Adapted | | [#1839] | PR | Number equality with symmetric role | ✅ Adapted | +| [#1859] | Issue | Channel assertions | ✅ Adapted | [#994]: https://github.com/stretchr/testify/pull/994 [#1232]: https://github.com/stretchr/testify/pull/1232 [#1356]: https://github.com/stretchr/testify/pull/1356 [#1467]: https://github.com/stretchr/testify/pull/1467 [#1480]: https://github.com/stretchr/testify/pull/1480 +[#1576]: https://github.com/stretchr/testify/pull/1576 [#1772]: https://github.com/stretchr/testify/pull/1772 [#1797]: https://github.com/stretchr/testify/pull/1797 [#1816]: https://github.com/stretchr/testify/issues/1816 @@ -101,7 +103,7 @@ This table catalogs all upstream PRs and issues from [github.com/stretchr/testif [#1606]: https://github.com/stretchr/testify/pull/1606 [#1839]: https://github.com/stretchr/testify/pull/1839 [#1848]: https://github.com/stretchr/testify/pull/1848 -[#1876]: https://github.com/stretchr/testify/pull/1876 +[#1859]: https://github.com/stretchr/testify/pull/1859 ### Superseded by Our Implementation @@ -120,7 +122,6 @@ This table catalogs all upstream PRs and issues from [github.com/stretchr/testif | [#1576] | Issue/PR | `EqualValues` assertion | 🔍 Monitoring [#1863]- Wrong equality when comparing float32 and float64| | [#1601] | Issue | `NoFieldIsZero` assertion | 🔍 Monitoring - Considering implementation | | [#1840] | Issue | JSON presence check without exact values | 🔍 Monitoring - Interesting for testing APIs with generated IDs | -| [#1859] | Issue | Channel assertions | 🔍 Monitoring - aligned with synctest support | | [#1860] | Issue+PR | `ErrorAsType[E]` for Go 1.26+ - PR: [#1861] | 🔍 Monitoring - Interesting UX syntax | ### Informational (Not Implemented) @@ -135,7 +136,7 @@ This table catalogs all upstream PRs and issues from [github.com/stretchr/testif [#1845]: https://github.com/stretchr/testify/pull/1845 [#1147]: https://github.com/stretchr/testify/issues/1147 [#1308]: https://github.com/stretchr/testify/pull/1308 -[#576]: https://github.com/stretchr/testify/pull/1576 +[#1576]: https://github.com/stretchr/testify/pull/1576 [#1859]: https://github.com/stretchr/testify/pull/1859 [#1860]: https://github.com/stretchr/testify/pull/1860 [#1861]: https://github.com/stretchr/testify/pull/1861 @@ -146,9 +147,9 @@ This table catalogs all upstream PRs and issues from [github.com/stretchr/testif | Category | Count | |----------|-------| -| **Implemented/Merged** | 25 | +| **Implemented/Merged** | 26 | | **Superseded** | 4 | -| **Monitoring** | 5 | +| **Monitoring** | 4 | | **Informational** | 3 | | **Total Processed** | 37 | diff --git a/hack/doc-site/hugo/metrics.yaml b/hack/doc-site/hugo/metrics.yaml index 3d6436a76..9bed5a14c 100644 --- a/hack/doc-site/hugo/metrics.yaml +++ b/hack/doc-site/hugo/metrics.yaml @@ -1,10 +1,10 @@ params: metrics: domains: 19 - functions: 137 - assertions: 133 - generics: 51 - nongeneric_assertions: 82 + functions: 141 + assertions: 137 + generics: 53 + nongeneric_assertions: 84 helpers: 4 others: 0 by_domain: @@ -22,7 +22,7 @@ params: count: 12 condition: name: Condition - count: 5 + count: 9 equality: name: Equality count: 16 @@ -65,6 +65,6 @@ params: yaml: name: Yaml count: 5 - package_variants: 430 - total_variants: 860 - total_functions: 870 + package_variants: 442 + total_variants: 884 + total_functions: 894 diff --git a/internal/assertions/condition.go b/internal/assertions/condition.go index 13aab6b0d..5dc2c7df0 100644 --- a/internal/assertions/condition.go +++ b/internal/assertions/condition.go @@ -7,6 +7,7 @@ import ( "context" "errors" "fmt" + "reflect" "runtime" "sync" "sync/atomic" @@ -39,6 +40,166 @@ func Condition(t T, comp func() bool, msgAndArgs ...any) bool { return result } +// Blocked asserts that a channel is blocked on receive. +// +// It always fails if the operand is not a channel, or if the channel is send-only. +// +// # Usage +// +// ch := make(chan struct{}) +// assertions.Blocked(t, ch) +// +// # Examples +// +// success: make(chan struct{}) +// failure: sendChanMessage() +func Blocked(t T, ch any, msgAndArgs ...any) bool { + // Domain: condition + // Opposite: NotBlocked + if h, ok := t.(H); ok { + h.Helper() + } + + chanType := reflect.TypeOf(ch) + if chanType == nil || chanType.Kind() != reflect.Chan { + return Fail(t, fmt.Sprintf("Expected a channel but got: %T", ch), msgAndArgs...) + } + if chanType.ChanDir()&reflect.RecvDir == 0 { + return Fail(t, "Expected channel direction to allow receive", msgAndArgs...) + } + + chanValue := reflect.ValueOf(ch) + chosen, recv, ok := reflect.Select([]reflect.SelectCase{ + { + Dir: reflect.SelectRecv, + Chan: chanValue, + }, + { + Dir: reflect.SelectDefault, + }, + }) + + if chosen == 0 { + if ok { + return Fail(t, fmt.Sprintf("Channel receive should have blocked, but got: %v", recv), msgAndArgs...) + } + + return Fail(t, "Channel receive should have blocked, but channel was closed", msgAndArgs...) + } + + return true +} + +// BlockedT asserts that a channel is blocked on receive. +// +// # Usage +// +// ch := make(chan struct{}) +// assertions.BlockedT(t, ch) +// +// # Examples +// +// success: make(chan struct{}) +// failure: sendChanMessage() +func BlockedT[E any, CHAN ~chan E](t T, ch CHAN, msgAndArgs ...any) bool { + // Domain: condition + // Opposite: NotBlockedT + if h, ok := t.(H); ok { + h.Helper() + } + + rch := (<-chan E)(ch) // impose compile-time check that the channel is indeed open for receive + select { + case r, ok := <-rch: + if ok { + return Fail(t, fmt.Sprintf("Channel receive should have blocked, but got: %v", r), msgAndArgs...) + } + + return Fail(t, "Channel receive should have blocked, but channel was closed", msgAndArgs...) + default: + return true + } +} + +// NotBlocked asserts that a channel is not blocked on receive. +// +// It always fails if the operand is not a channel, or if the channel is send-only. +// +// A closed channel doesn't block and returns true. +// Notice that this consumes any message available in the channel. +// +// # Usage +// +// ch := make(chan struct{}, 1) +// ch <- struct{}{} +// assertions.NotBlocked(t, ch) +// +// # Examples +// +// success: sendChanMessage() +// failure: make(chan struct{}) +func NotBlocked(t T, ch any, msgAndArgs ...any) bool { + // Domain: condition + if h, ok := t.(H); ok { + h.Helper() + } + + chanType := reflect.TypeOf(ch) + if chanType == nil || chanType.Kind() != reflect.Chan { + return Fail(t, fmt.Sprintf("Expected a channel but got: %T", ch), msgAndArgs...) + } + if chanType.ChanDir()&reflect.RecvDir == 0 { + return Fail(t, "Expected channel direction to allow receive", msgAndArgs...) + } + + chanValue := reflect.ValueOf(ch) + chosen, _, _ := reflect.Select([]reflect.SelectCase{ + { + Dir: reflect.SelectRecv, + Chan: chanValue, + }, + { + Dir: reflect.SelectDefault, + }, + }) + + if chosen == 0 { + return true + } + + return Fail(t, "Channel receive should not have blocked", msgAndArgs...) +} + +// NotBlockedT asserts that a channel is not blocked on receive. +// +// A closed channel doesn't block and returns true. +// Notice that this consumes any message available in the channel. +// +// # Usage +// +// ch := make(chan struct{}, 1) +// ch <- struct{}{} +// assertions.NotBlockedT(t, ch) +// +// # Examples +// +// success: sendChanMessage() +// failure: make(chan struct{}) +func NotBlockedT[E any, CHAN ~chan E](t T, ch CHAN, msgAndArgs ...any) bool { + // Domain: condition + if h, ok := t.(H); ok { + h.Helper() + } + + rch := (<-chan E)(ch) // impose compile-time check that the channel is indeed open for receive + select { + case <-rch: + return true + default: + return Fail(t, "Channel receive should not have blocked", msgAndArgs...) + } +} + // Eventually asserts that the given condition will be met before timeout, // periodically checking the target function on each tick. // diff --git a/internal/assertions/condition_test.go b/internal/assertions/condition_test.go index 39b258758..5076ba323 100644 --- a/internal/assertions/condition_test.go +++ b/internal/assertions/condition_test.go @@ -178,6 +178,84 @@ func conditionFailCases() iter.Seq[failCase] { assertion: func(t T) bool { return Condition(t, func() bool { return false }) }, wantError: "condition failed", }, + { + name: "Blocked/non-channel", + assertion: func(t T) bool { return Blocked(t, "not a channel") }, + wantContains: []string{"Expected a channel but got"}, + }, + { + name: "Blocked/nil-interface", + assertion: func(t T) bool { return Blocked(t, nil) }, + wantContains: []string{"Expected a channel but got"}, + }, + { + name: "Blocked/buffered-with-value", + assertion: func(t T) bool { + ch := make(chan int, 1) + ch <- 42 + return Blocked(t, ch) + }, + wantContains: []string{"Channel receive should have blocked", "42"}, + }, + { + name: "BlockedT/buffered-with-value", + assertion: func(t T) bool { + ch := make(chan int, 1) + ch <- 42 + return BlockedT(t, ch) + }, + wantContains: []string{"Channel receive should have blocked", "42"}, + }, + { + name: "NotBlocked/empty-unbuffered", + assertion: func(t T) bool { + return NotBlocked(t, make(chan int)) + }, + wantContains: []string{"Channel receive should not have blocked"}, + }, + { + name: "NotBlockedT/empty-unbuffered", + assertion: func(t T) bool { + return NotBlockedT(t, make(chan int)) + }, + wantContains: []string{"Channel receive should not have blocked"}, + }, + { + name: "Blocked/closed-channel", + assertion: func(t T) bool { + ch := make(chan int) + close(ch) + return Blocked(t, ch) + }, + wantContains: []string{"channel was closed"}, + }, + { + name: "BlockedT/closed-channel", + assertion: func(t T) bool { + ch := make(chan int) + close(ch) + return BlockedT(t, ch) + }, + wantContains: []string{"channel was closed"}, + }, + { + name: "Blocked/send-only-rejected", + assertion: func(t T) bool { + ch := make(chan int) + var so chan<- int = ch + return Blocked(t, so) + }, + wantContains: []string{"channel direction"}, + }, + { + name: "NotBlocked/send-only-rejected", + assertion: func(t T) bool { + ch := make(chan int) + var so chan<- int = ch + return NotBlocked(t, so) + }, + wantContains: []string{"channel direction"}, + }, }) } @@ -355,3 +433,246 @@ func TestConditionPanicRecovery(t *testing.T) { } }) } + +// ======================================= +// Test ConditionBlocked / ConditionNotBlocked +// ======================================= + +func TestConditionBlocked(t *testing.T) { + t.Parallel() + + for tc := range blockedCases() { + t.Run(tc.name, tc.test) + } + + // Reflect-only inputs (cannot be expressed via the generic [BlockedT] constraint). + t.Run("reflect-only/non-channel-string", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + shouldPassOrFail(t, mock, Blocked(mock, "not a channel"), false) + }) + t.Run("reflect-only/non-channel-int", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + shouldPassOrFail(t, mock, Blocked(mock, 42), false) + }) + t.Run("reflect-only/non-channel-slice", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + shouldPassOrFail(t, mock, Blocked(mock, []int{1, 2, 3}), false) + }) + t.Run("reflect-only/nil-interface", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + shouldPassOrFail(t, mock, Blocked(mock, nil), false) + }) + t.Run("reflect-only/recv-only-empty-blocks", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + ch := make(chan int) + var ro <-chan int = ch + shouldPassOrFail(t, mock, Blocked(mock, ro), true) + }) + t.Run("reflect-only/recv-only-with-value-fails", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + ch := make(chan int, 1) + ch <- 7 + var ro <-chan int = ch + shouldPassOrFail(t, mock, Blocked(mock, ro), false) + }) + // Send-only must be rejected cleanly: passing it to reflect.Select with + // a Recv direction would otherwise panic at runtime. + t.Run("reflect-only/send-only-rejected", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + ch := make(chan int) + var so chan<- int = ch + shouldPassOrFail(t, mock, Blocked(mock, so), false) + }) +} + +func TestConditionNotBlocked(t *testing.T) { + t.Parallel() + + for tc := range notBlockedCases() { + t.Run(tc.name, tc.test) + } + + // Multi-value buffer: NotBlocked consumes exactly one element per call. + t.Run("buffered-multi-consumes-one", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + ch := make(chan int, 3) + ch <- 1 + ch <- 2 + ch <- 3 + shouldPassOrFail(t, mock, NotBlocked(mock, ch), true) + // Two values must remain. + if got := len(ch); got != 2 { + t.Errorf("expected 2 values left in channel, got %d", got) + } + }) + + // Reflect-only inputs (cannot be expressed via the generic [NotBlockedT] constraint). + t.Run("reflect-only/non-channel-string", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + shouldPassOrFail(t, mock, NotBlocked(mock, "not a channel"), false) + }) + t.Run("reflect-only/non-channel-int", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + shouldPassOrFail(t, mock, NotBlocked(mock, 42), false) + }) + t.Run("reflect-only/nil-interface", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + shouldPassOrFail(t, mock, NotBlocked(mock, nil), false) + }) + t.Run("reflect-only/recv-only-with-value-passes", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + ch := make(chan int, 1) + ch <- 7 + var ro <-chan int = ch + shouldPassOrFail(t, mock, NotBlocked(mock, ro), true) + }) + t.Run("reflect-only/recv-only-empty-fails", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + ch := make(chan int) + var ro <-chan int = ch + shouldPassOrFail(t, mock, NotBlocked(mock, ro), false) + }) + t.Run("reflect-only/send-only-rejected", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + ch := make(chan int) + var so chan<- int = ch + shouldPassOrFail(t, mock, NotBlocked(mock, so), false) + }) +} + +// blockedCases exercises both Blocked and BlockedT against the same input. +// Channels are produced by a factory: each subtest gets its own fresh channel +// so that the X variant cannot drain the buffer and silently turn the XT +// variant into a different scenario. +// +// The generic CHAN constraint of BlockedT is `~chan E`, so named bidirectional +// channel types are accepted, but recv-only / send-only channels must be tested +// against the reflect-based [Blocked] only — see the reflect-only subtests in +// [TestConditionBlocked]. +func blockedCases() iter.Seq[genericTestCase] { + return slices.Values([]genericTestCase{ + // Empty channels block on receive. + {"unbuffered-empty/blocks", testAllBlocked(func() chan int { return make(chan int) }, true)}, + {"buffered-empty/blocks", testAllBlocked(func() chan int { return make(chan int, 4) }, true)}, + {"unbuffered-struct/blocks", testAllBlocked(func() chan struct{} { return make(chan struct{}) }, true)}, + {"unbuffered-string/blocks", testAllBlocked(func() chan string { return make(chan string) }, true)}, + {"unbuffered-error/blocks", testAllBlocked(func() chan error { return make(chan error) }, true)}, + + // Typed nil channels block forever (default branch fires => "blocked"). + {"typed-nil-int/blocks", testAllBlocked(func() chan int { return nil }, true)}, + {"typed-nil-struct/blocks", testAllBlocked(func() chan struct{} { return nil }, true)}, + + // Non-empty channels do not block. + {"buffered-with-value/fails", testAllBlocked(filledChanFactory(42), false)}, + {"buffered-with-string/fails", testAllBlocked(filledChanFactory("hello"), false)}, + {"buffered-with-zero-struct/fails", testAllBlocked(filledChanFactory(struct{}{}), false)}, + + // Closed channels do NOT block (zero value is received immediately). + // The Fail message currently shows the zero value, which is mildly + // misleading; pinned here so the behavior is intentional. + {"closed-int/fails", testAllBlocked(closedChanFactory[int](), false)}, + {"closed-struct/fails", testAllBlocked(closedChanFactory[struct{}](), false)}, + }) +} + +// notBlockedCases is the dual of blockedCases — same input shape, opposite expected outcome. +func notBlockedCases() iter.Seq[genericTestCase] { + return slices.Values([]genericTestCase{ + // Empty channels block => NotBlocked fails. + {"unbuffered-empty/fails", testAllNotBlocked(func() chan int { return make(chan int) }, false)}, + {"buffered-empty/fails", testAllNotBlocked(func() chan int { return make(chan int, 4) }, false)}, + {"unbuffered-struct/fails", testAllNotBlocked(func() chan struct{} { return make(chan struct{}) }, false)}, + {"unbuffered-string/fails", testAllNotBlocked(func() chan string { return make(chan string) }, false)}, + + // Typed nil channels block forever => NotBlocked fails. + {"typed-nil-int/fails", testAllNotBlocked(func() chan int { return nil }, false)}, + {"typed-nil-struct/fails", testAllNotBlocked(func() chan struct{} { return nil }, false)}, + + // Non-empty channels do not block => NotBlocked passes. + {"buffered-with-value/passes", testAllNotBlocked(filledChanFactory(42), true)}, + {"buffered-with-string/passes", testAllNotBlocked(filledChanFactory("hello"), true)}, + {"buffered-with-zero-struct/passes", testAllNotBlocked(filledChanFactory(struct{}{}), true)}, + + // Closed channels are not blocked (zero value is received immediately). + {"closed-int/passes", testAllNotBlocked(closedChanFactory[int](), true)}, + {"closed-struct/passes", testAllNotBlocked(closedChanFactory[struct{}](), true)}, + }) +} + +// testAllBlocked tests both Blocked and BlockedT against fresh channels +// produced by the factory. +func testAllBlocked[E any](newCh func() chan E, shouldPass bool) func(*testing.T) { + return func(t *testing.T) { + t.Parallel() + + label := "should fail" + if shouldPass { + label = "should pass" + } + t.Run(label, func(t *testing.T) { + t.Run("with Blocked", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + shouldPassOrFail(t, mock, Blocked(mock, newCh()), shouldPass) + }) + t.Run("with BlockedT", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + shouldPassOrFail(t, mock, BlockedT(mock, newCh()), shouldPass) + }) + }) + } +} + +func testAllNotBlocked[E any](newCh func() chan E, shouldPass bool) func(*testing.T) { + return func(t *testing.T) { + t.Parallel() + + label := "should fail" + if shouldPass { + label = "should pass" + } + t.Run(label, func(t *testing.T) { + t.Run("with NotBlocked", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + shouldPassOrFail(t, mock, NotBlocked(mock, newCh()), shouldPass) + }) + t.Run("with NotBlockedT", func(t *testing.T) { + t.Parallel() + mock := new(mockT) + shouldPassOrFail(t, mock, NotBlockedT(mock, newCh()), shouldPass) + }) + }) + } +} + +func filledChanFactory[E any](v E) func() chan E { + return func() chan E { + ch := make(chan E, 1) + ch <- v + return ch + } +} + +func closedChanFactory[E any]() func() chan E { + return func() chan E { + ch := make(chan E) + close(ch) + return ch + } +} diff --git a/require/require_assertions.go b/require/require_assertions.go index 368c4b372..f7203b6ed 100644 --- a/require/require_assertions.go +++ b/require/require_assertions.go @@ -15,6 +15,56 @@ import ( "github.com/go-openapi/testify/v2/internal/assertions" ) +// Blocked asserts that a channel is blocked on receive. +// +// It always fails if the operand is not a channel, or if the channel is send-only. +// +// # Usage +// +// ch := make(chan struct{}) +// assertions.Blocked(t, ch) +// +// # Examples +// +// success: make(chan struct{}) +// failure: sendChanMessage() +// +// Upon failure, the test [T] is marked as failed and stops execution. +func Blocked(t T, ch any, msgAndArgs ...any) { + if h, ok := t.(H); ok { + h.Helper() + } + if assertions.Blocked(t, ch, msgAndArgs...) { + return + } + + t.FailNow() +} + +// BlockedT asserts that a channel is blocked on receive. +// +// # Usage +// +// ch := make(chan struct{}) +// assertions.BlockedT(t, ch) +// +// # Examples +// +// success: make(chan struct{}) +// failure: sendChanMessage() +// +// Upon failure, the test [T] is marked as failed and stops execution. +func BlockedT[E any, CHAN ~chan E](t T, ch CHAN, msgAndArgs ...any) { + if h, ok := t.(H); ok { + h.Helper() + } + if assertions.BlockedT[E, CHAN](t, ch, msgAndArgs...) { + return + } + + t.FailNow() +} + // Condition uses a comparison function to assert a complex condition. // // # Usage @@ -2492,6 +2542,64 @@ func NoGoRoutineLeak(t T, tested func(), msgAndArgs ...any) { t.FailNow() } +// NotBlocked asserts that a channel is not blocked on receive. +// +// It always fails if the operand is not a channel, or if the channel is send-only. +// +// A closed channel doesn't block and returns true. +// Notice that this consumes any message available in the channel. +// +// # Usage +// +// ch := make(chan struct{}, 1) +// ch <- struct{}{} +// assertions.NotBlocked(t, ch) +// +// # Examples +// +// success: sendChanMessage() +// failure: make(chan struct{}) +// +// Upon failure, the test [T] is marked as failed and stops execution. +func NotBlocked(t T, ch any, msgAndArgs ...any) { + if h, ok := t.(H); ok { + h.Helper() + } + if assertions.NotBlocked(t, ch, msgAndArgs...) { + return + } + + t.FailNow() +} + +// NotBlockedT asserts that a channel is not blocked on receive. +// +// A closed channel doesn't block and returns true. +// Notice that this consumes any message available in the channel. +// +// # Usage +// +// ch := make(chan struct{}, 1) +// ch <- struct{}{} +// assertions.NotBlockedT(t, ch) +// +// # Examples +// +// success: sendChanMessage() +// failure: make(chan struct{}) +// +// Upon failure, the test [T] is marked as failed and stops execution. +func NotBlockedT[E any, CHAN ~chan E](t T, ch CHAN, msgAndArgs ...any) { + if h, ok := t.(H); ok { + h.Helper() + } + if assertions.NotBlockedT[E, CHAN](t, ch, msgAndArgs...) { + return + } + + t.FailNow() +} + // NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the // specified substring or element. // diff --git a/require/require_assertions_test.go b/require/require_assertions_test.go index e0a8be4b8..4c4536290 100644 --- a/require/require_assertions_test.go +++ b/require/require_assertions_test.go @@ -17,6 +17,52 @@ import ( "time" ) +func TestBlocked(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + mock := new(mockFailNowT) + Blocked(mock, make(chan struct{})) + // require functions don't return a value + }) + + t.Run("failure", func(t *testing.T) { + t.Parallel() + + mock := new(mockFailNowT) + Blocked(mock, sendChanMessage()) + // require functions don't return a value + if !mock.failed { + t.Error("Blocked should call FailNow()") + } + }) +} + +func TestBlockedT(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + mock := new(mockFailNowT) + BlockedT(mock, make(chan struct{})) + // require functions don't return a value + }) + + t.Run("failure", func(t *testing.T) { + t.Parallel() + + mock := new(mockFailNowT) + BlockedT(mock, sendChanMessage()) + // require functions don't return a value + if !mock.failed { + t.Error("BlockedT should call FailNow()") + } + }) +} + func TestCondition(t *testing.T) { t.Parallel() @@ -1876,6 +1922,52 @@ func TestNoGoRoutineLeak(t *testing.T) { }) } +func TestNotBlocked(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + mock := new(mockFailNowT) + NotBlocked(mock, sendChanMessage()) + // require functions don't return a value + }) + + t.Run("failure", func(t *testing.T) { + t.Parallel() + + mock := new(mockFailNowT) + NotBlocked(mock, make(chan struct{})) + // require functions don't return a value + if !mock.failed { + t.Error("NotBlocked should call FailNow()") + } + }) +} + +func TestNotBlockedT(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + mock := new(mockFailNowT) + NotBlockedT(mock, sendChanMessage()) + // require functions don't return a value + }) + + t.Run("failure", func(t *testing.T) { + t.Parallel() + + mock := new(mockFailNowT) + NotBlockedT(mock, make(chan struct{})) + // require functions don't return a value + if !mock.failed { + t.Error("NotBlockedT should call FailNow()") + } + }) +} + func TestNotContains(t *testing.T) { t.Parallel() @@ -3062,6 +3154,13 @@ func httpBody(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintf(w, "Hello, %s!", name) } +func sendChanMessage() chan struct{} { + ch := make(chan struct{}, 1) + ch <- struct{}{} + + return ch +} + //nolint:gochecknoglobals // this is on purpose to share a common pointer when testing var ( staticVar = "static string" @@ -3087,4 +3186,6 @@ func (d *dummyError) Error() string { return "dummy error" } +var dummyChan = make(chan struct{}) + type myType float64 diff --git a/require/require_examples_test.go b/require/require_examples_test.go index 54168f7f5..5d4448fd4 100644 --- a/require/require_examples_test.go +++ b/require/require_examples_test.go @@ -21,6 +21,22 @@ import ( "github.com/go-openapi/testify/v2/require" ) +func ExampleBlocked() { + t := new(testing.T) // should come from testing, e.g. func TestBlocked(t *testing.T) + require.Blocked(t, make(chan struct{})) + fmt.Println("passed") + + // Output: passed +} + +func ExampleBlockedT() { + t := new(testing.T) // should come from testing, e.g. func TestBlockedT(t *testing.T) + require.BlockedT(t, make(chan struct{})) + fmt.Println("passed") + + // Output: passed +} + func ExampleCondition() { t := new(testing.T) // should come from testing, e.g. func TestCondition(t *testing.T) require.Condition(t, func() bool { @@ -689,6 +705,22 @@ func ExampleNoGoRoutineLeak() { // Output: passed } +func ExampleNotBlocked() { + t := new(testing.T) // should come from testing, e.g. func TestNotBlocked(t *testing.T) + require.NotBlocked(t, sendChanMessage()) + fmt.Println("passed") + + // Output: passed +} + +func ExampleNotBlockedT() { + t := new(testing.T) // should come from testing, e.g. func TestNotBlockedT(t *testing.T) + require.NotBlockedT(t, sendChanMessage()) + fmt.Println("passed") + + // Output: passed +} + func ExampleNotContains() { t := new(testing.T) // should come from testing, e.g. func TestNotContains(t *testing.T) require.NotContains(t, []string{"A", "B"}, "C") @@ -1109,6 +1141,13 @@ func httpBody(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintf(w, "Hello, %s!", name) } +func sendChanMessage() chan struct{} { + ch := make(chan struct{}, 1) + ch <- struct{}{} + + return ch +} + //nolint:gochecknoglobals // this is on purpose to share a common pointer when testing var ( staticVar = "static string" @@ -1134,4 +1173,6 @@ func (d *dummyError) Error() string { return "dummy error" } +var dummyChan = make(chan struct{}) + type myType float64 diff --git a/require/require_format.go b/require/require_format.go index 37aec6e86..d9605be77 100644 --- a/require/require_format.go +++ b/require/require_format.go @@ -15,6 +15,34 @@ import ( "github.com/go-openapi/testify/v2/internal/assertions" ) +// Blockedf is the same as [Blocked], but it accepts a format string to format arguments like [fmt.Printf]. +// +// Upon failure, the test [T] is marked as failed and stops execution. +func Blockedf(t T, ch any, msg string, args ...any) { + if h, ok := t.(H); ok { + h.Helper() + } + if assertions.Blocked(t, ch, forwardArgs(msg, args)) { + return + } + + t.FailNow() +} + +// BlockedTf is the same as [BlockedT], but it accepts a format string to format arguments like [fmt.Printf]. +// +// Upon failure, the test [T] is marked as failed and stops execution. +func BlockedTf[E any, CHAN ~chan E](t T, ch CHAN, msg string, args ...any) { + if h, ok := t.(H); ok { + h.Helper() + } + if assertions.BlockedT[E, CHAN](t, ch, forwardArgs(msg, args)) { + return + } + + t.FailNow() +} + // Conditionf is the same as [Condition], but it accepts a format string to format arguments like [fmt.Printf]. // // Upon failure, the test [T] is marked as failed and stops execution. @@ -1159,6 +1187,34 @@ func NoGoRoutineLeakf(t T, tested func(), msg string, args ...any) { t.FailNow() } +// NotBlockedf is the same as [NotBlocked], but it accepts a format string to format arguments like [fmt.Printf]. +// +// Upon failure, the test [T] is marked as failed and stops execution. +func NotBlockedf(t T, ch any, msg string, args ...any) { + if h, ok := t.(H); ok { + h.Helper() + } + if assertions.NotBlocked(t, ch, forwardArgs(msg, args)) { + return + } + + t.FailNow() +} + +// NotBlockedTf is the same as [NotBlockedT], but it accepts a format string to format arguments like [fmt.Printf]. +// +// Upon failure, the test [T] is marked as failed and stops execution. +func NotBlockedTf[E any, CHAN ~chan E](t T, ch CHAN, msg string, args ...any) { + if h, ok := t.(H); ok { + h.Helper() + } + if assertions.NotBlockedT[E, CHAN](t, ch, forwardArgs(msg, args)) { + return + } + + t.FailNow() +} + // NotContainsf is the same as [NotContains], but it accepts a format string to format arguments like [fmt.Printf]. // // Upon failure, the test [T] is marked as failed and stops execution. diff --git a/require/require_format_test.go b/require/require_format_test.go index 1be223ddb..6efade060 100644 --- a/require/require_format_test.go +++ b/require/require_format_test.go @@ -17,6 +17,52 @@ import ( "time" ) +func TestBlockedf(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + mock := new(mockFailNowT) + Blockedf(mock, make(chan struct{}), "test message") + // require functions don't return a value + }) + + t.Run("failure", func(t *testing.T) { + t.Parallel() + + mock := new(mockFailNowT) + Blockedf(mock, sendChanMessage(), "test message") + // require functions don't return a value + if !mock.failed { + t.Error("Blockedf should call FailNow()") + } + }) +} + +func TestBlockedTf(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + mock := new(mockFailNowT) + BlockedTf(mock, make(chan struct{}), "test message") + // require functions don't return a value + }) + + t.Run("failure", func(t *testing.T) { + t.Parallel() + + mock := new(mockFailNowT) + BlockedTf(mock, sendChanMessage(), "test message") + // require functions don't return a value + if !mock.failed { + t.Error("BlockedTf should call FailNow()") + } + }) +} + func TestConditionf(t *testing.T) { t.Parallel() @@ -1876,6 +1922,52 @@ func TestNoGoRoutineLeakf(t *testing.T) { }) } +func TestNotBlockedf(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + mock := new(mockFailNowT) + NotBlockedf(mock, sendChanMessage(), "test message") + // require functions don't return a value + }) + + t.Run("failure", func(t *testing.T) { + t.Parallel() + + mock := new(mockFailNowT) + NotBlockedf(mock, make(chan struct{}), "test message") + // require functions don't return a value + if !mock.failed { + t.Error("NotBlockedf should call FailNow()") + } + }) +} + +func TestNotBlockedTf(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + mock := new(mockFailNowT) + NotBlockedTf(mock, sendChanMessage(), "test message") + // require functions don't return a value + }) + + t.Run("failure", func(t *testing.T) { + t.Parallel() + + mock := new(mockFailNowT) + NotBlockedTf(mock, make(chan struct{}), "test message") + // require functions don't return a value + if !mock.failed { + t.Error("NotBlockedTf should call FailNow()") + } + }) +} + func TestNotContainsf(t *testing.T) { t.Parallel() diff --git a/require/require_forward.go b/require/require_forward.go index dc7cc8fbd..037c499df 100644 --- a/require/require_forward.go +++ b/require/require_forward.go @@ -30,6 +30,34 @@ func New(t T) *Assertions { } } +// Blocked is the same as [Blocked], as a method rather than a package-level function. +// +// Upon failure, the test [T] is marked as failed and stops execution. +func (a *Assertions) Blocked(ch any, msgAndArgs ...any) { + if h, ok := a.T.(H); ok { + h.Helper() + } + if assertions.Blocked(a.T, ch, msgAndArgs...) { + return + } + + a.T.FailNow() +} + +// Blockedf is the same as [Assertions.Blocked], but it accepts a format string to format arguments like [fmt.Printf]. +// +// Upon failure, the test [T] is marked as failed and stops execution. +func (a *Assertions) Blockedf(ch any, msg string, args ...any) { + if h, ok := a.T.(H); ok { + h.Helper() + } + if assertions.Blocked(a.T, ch, forwardArgs(msg, args)) { + return + } + + a.T.FailNow() +} + // Condition is the same as [Condition], as a method rather than a package-level function. // // Upon failure, the test [T] is marked as failed and stops execution. @@ -1534,6 +1562,34 @@ func (a *Assertions) NoGoRoutineLeakf(tested func(), msg string, args ...any) { a.T.FailNow() } +// NotBlocked is the same as [NotBlocked], as a method rather than a package-level function. +// +// Upon failure, the test [T] is marked as failed and stops execution. +func (a *Assertions) NotBlocked(ch any, msgAndArgs ...any) { + if h, ok := a.T.(H); ok { + h.Helper() + } + if assertions.NotBlocked(a.T, ch, msgAndArgs...) { + return + } + + a.T.FailNow() +} + +// NotBlockedf is the same as [Assertions.NotBlocked], but it accepts a format string to format arguments like [fmt.Printf]. +// +// Upon failure, the test [T] is marked as failed and stops execution. +func (a *Assertions) NotBlockedf(ch any, msg string, args ...any) { + if h, ok := a.T.(H); ok { + h.Helper() + } + if assertions.NotBlocked(a.T, ch, forwardArgs(msg, args)) { + return + } + + a.T.FailNow() +} + // NotContains is the same as [NotContains], as a method rather than a package-level function. // // Upon failure, the test [T] is marked as failed and stops execution. diff --git a/require/require_forward_test.go b/require/require_forward_test.go index 6dbb35eaf..da1ec59c9 100644 --- a/require/require_forward_test.go +++ b/require/require_forward_test.go @@ -16,6 +16,31 @@ import ( "time" ) +func TestAssertionsBlocked(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + mock := new(mockFailNowT) + a := New(mock) + a.Blocked(make(chan struct{})) + // require functions don't return a value + }) + + t.Run("failure", func(t *testing.T) { + t.Parallel() + + mock := new(mockFailNowT) + a := New(mock) + a.Blocked(sendChanMessage()) + // require functions don't return a value + if !mock.failed { + t.Error("Assertions.Blocked should call FailNow()") + } + }) +} + func TestAssertionsCondition(t *testing.T) { t.Parallel() @@ -1324,6 +1349,31 @@ func TestAssertionsNoGoRoutineLeak(t *testing.T) { }) } +func TestAssertionsNotBlocked(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + mock := new(mockFailNowT) + a := New(mock) + a.NotBlocked(sendChanMessage()) + // require functions don't return a value + }) + + t.Run("failure", func(t *testing.T) { + t.Parallel() + + mock := new(mockFailNowT) + a := New(mock) + a.NotBlocked(make(chan struct{})) + // require functions don't return a value + if !mock.failed { + t.Error("Assertions.NotBlocked should call FailNow()") + } + }) +} + func TestAssertionsNotContains(t *testing.T) { t.Parallel() @@ -2008,6 +2058,31 @@ func TestAssertionsZero(t *testing.T) { }) } +func TestAssertionsBlockedf(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + mock := new(mockFailNowT) + a := New(mock) + a.Blockedf(make(chan struct{}), "test message") + // require functions don't return a value + }) + + t.Run("failure", func(t *testing.T) { + t.Parallel() + + mock := new(mockFailNowT) + a := New(mock) + a.Blockedf(sendChanMessage(), "test message") + // require functions don't return a value + if !mock.failed { + t.Error("Assertions.Blockedf should call FailNow()") + } + }) +} + func TestAssertionsConditionf(t *testing.T) { t.Parallel() @@ -3316,6 +3391,31 @@ func TestAssertionsNoGoRoutineLeakf(t *testing.T) { }) } +func TestAssertionsNotBlockedf(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + mock := new(mockFailNowT) + a := New(mock) + a.NotBlockedf(sendChanMessage(), "test message") + // require functions don't return a value + }) + + t.Run("failure", func(t *testing.T) { + t.Parallel() + + mock := new(mockFailNowT) + a := New(mock) + a.NotBlockedf(make(chan struct{}), "test message") + // require functions don't return a value + if !mock.failed { + t.Error("Assertions.NotBlockedf should call FailNow()") + } + }) +} + func TestAssertionsNotContainsf(t *testing.T) { t.Parallel()