From 3e9f56d431467c598bd46d10f1ce84270b80d037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Burzy=C5=84ski?= Date: Fri, 2 Jun 2023 23:14:55 +0200 Subject: [PATCH] fix: make EventuallyWithT concurrency safe --- assert/assertions.go | 37 ++++++++++++++++++------------------- assert/assertions_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 19 deletions(-) diff --git a/assert/assertions.go b/assert/assertions.go index 6ab0ec347..0af42cc93 100644 --- a/assert/assertions.go +++ b/assert/assertions.go @@ -1870,23 +1870,18 @@ func (c *CollectT) Errorf(format string, args ...interface{}) { } // FailNow panics. -func (c *CollectT) FailNow() { +func (*CollectT) FailNow() { panic("Assertion failed") } -// Reset clears the collected errors. -func (c *CollectT) Reset() { - c.errors = nil +// Deprecated: That was a method for internal usage that should not have been published. Now just panics. +func (*CollectT) Reset() { + panic("Reset() is deprecated") } -// Copy copies the collected errors to the supplied t. -func (c *CollectT) Copy(t TestingT) { - if tt, ok := t.(tHelper); ok { - tt.Helper() - } - for _, err := range c.errors { - t.Errorf("%v", err) - } +// Deprecated: That was a method for internal usage that should not have been published. Now just panics. +func (*CollectT) Copy(TestingT) { + panic("Copy() is deprecated") } // EventuallyWithT asserts that given condition will be met in waitFor time, @@ -1912,8 +1907,8 @@ func EventuallyWithT(t TestingT, condition func(collect *CollectT), waitFor time h.Helper() } - collect := new(CollectT) - ch := make(chan bool, 1) + var lastTickErrs []error + ch := make(chan []error, 1) timer := time.NewTimer(waitFor) defer timer.Stop() @@ -1924,19 +1919,23 @@ func EventuallyWithT(t TestingT, condition func(collect *CollectT), waitFor time for tick := ticker.C; ; { select { case <-timer.C: - collect.Copy(t) + for _, err := range lastTickErrs { + t.Errorf("%v", err) + } return Fail(t, "Condition never satisfied", msgAndArgs...) case <-tick: tick = nil - collect.Reset() go func() { + collect := new(CollectT) condition(collect) - ch <- len(collect.errors) == 0 + ch <- collect.errors }() - case v := <-ch: - if v { + case errs := <-ch: + if len(errs) == 0 { return true } + // Keep the errors from the last ended condition, so that they can be copied to t if timeout is reached. + lastTickErrs = errs tick = ticker.C } } diff --git a/assert/assertions_test.go b/assert/assertions_test.go index 162c71801..49a92bc4d 100644 --- a/assert/assertions_test.go +++ b/assert/assertions_test.go @@ -2786,6 +2786,35 @@ func TestEventuallyWithTTrue(t *testing.T) { Len(t, mockT.errors, 0) } +func TestEventuallyWithT_ConcurrencySafe(t *testing.T) { + mockT := new(testing.T) + + condition := func(collect *CollectT) { + True(collect, false) + } + + // To trigger race conditions, we run EventuallyWithT with a nanosecond tick. + False(t, EventuallyWithT(mockT, condition, 100*time.Millisecond, time.Nanosecond)) +} + +func TestEventuallyWithT_ReturnsTheLatestFinishedConditionErrors(t *testing.T) { + var calledOnce bool + condition := func(collect *CollectT) { + if calledOnce { + // Sleep to ensure that the second condition runs longer than timeout. + time.Sleep(time.Second) + return + } + + // The first condition will fail. We expect to get this error as a result. + True(collect, false) + calledOnce = true + } + + mockT := new(testing.T) + False(t, EventuallyWithT(mockT, condition, 100*time.Millisecond, 20*time.Millisecond)) +} + func TestNeverFalse(t *testing.T) { condition := func() bool { return false