Skip to content

Commit

Permalink
feat: add assert.Consistentlyf
Browse files Browse the repository at this point in the history
This changeset adds the `assert.Consistently` and it's associated
functions to assert that a condition is true over the entire period of
`waitFor`. This is useful when testing the behavior of asynchronous
functions.

Closes #1087
  • Loading branch information
jfmyers9 committed Jun 4, 2024
1 parent 1b4fca7 commit 5e6e95d
Show file tree
Hide file tree
Showing 6 changed files with 400 additions and 0 deletions.
36 changes: 36 additions & 0 deletions assert/assertion_format.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

72 changes: 72 additions & 0 deletions assert/assertion_forward.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

87 changes: 87 additions & 0 deletions assert/assertions.go
Original file line number Diff line number Diff line change
Expand Up @@ -2038,6 +2038,93 @@ func EventuallyWithT(t TestingT, condition func(collect *CollectT), waitFor time
}
}

// Consistently asserts that given condition will be met for the entire
// duration of waitFor time, periodically checking target function each tick.
//
// assert.Consistently(t, func() bool { return true; }, time.Second, 10*time.Millisecond)
func Consistently(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}

ch := make(chan bool, 1)

timer := time.NewTimer(waitFor)
defer timer.Stop()

ticker := time.NewTicker(tick)
defer ticker.Stop()

for tick := ticker.C; ; {
select {
case <-timer.C:
return true
case <-tick:
tick = nil
go func() { ch <- condition() }()
case v := <-ch:
if !v {
return Fail(t, "Condition never satisfied", msgAndArgs...)
}
tick = ticker.C
}
}
}

// ConsistentlyWithT asserts that given condition will be met for the entire
// waitFor time, periodically checking target function each tick. In contrast
// to Consistently, it supplies a CollectT to the condition function, so that
// the condition function can use the CollectT to call other assertions. The
// condition is considered "met" if no errors are raised across all ticks. The
// supplied CollectT collects all errors from one tick (if there are any). If
// the condition is not met once before waitFor, the collected error of the
// failing tick are copied to t.
//
// externalValue := false
// go func() {
// time.Sleep(8*time.Second)
// externalValue = true
// }()
// assert.ConsistentlyWithT(t, func(c *assert.CollectT) {
// // add assertions as needed; any assertion failure will fail the current tick
// assert.True(c, externalValue, "expected 'externalValue' to be true")
// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false")
func ConsistentlyWithT(t TestingT, condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}

ch := make(chan []error, 1)

timer := time.NewTimer(waitFor)
defer timer.Stop()

ticker := time.NewTicker(tick)
defer ticker.Stop()

for tick := ticker.C; ; {
select {
case <-timer.C:
return true
case <-tick:
tick = nil
go func() {
collect := new(CollectT)
defer func() {
ch <- collect.errors
}()
condition(collect)
}()
case errs := <-ch:
if len(errs) > 0 {
return Fail(t, "Condition never satisfied", msgAndArgs...)
}

tick = ticker.C
}
}
}

// Never asserts that the given condition doesn't satisfy in waitFor time,
// periodically checking the target function each tick.
//
Expand Down
49 changes: 49 additions & 0 deletions assert/assertions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2898,6 +2898,29 @@ func TestEventuallyTrue(t *testing.T) {
True(t, Eventually(t, condition, 100*time.Millisecond, 20*time.Millisecond))
}

func TestConsistentlyTrue(t *testing.T) {
condition := func() bool {
return true
}

True(t, Consistently(t, condition, 100*time.Millisecond, 20*time.Millisecond))
}

func TestConsistentlyFalse(t *testing.T) {
mockT := new(testing.T)

state := 0
condition := func() bool {
defer func() {
state += 1
}()

return state != 2
}

False(t, Consistently(mockT, condition, 100*time.Millisecond, 20*time.Millisecond))
}

// errorsCapturingT is a mock implementation of TestingT that captures errors reported with Errorf.
type errorsCapturingT struct {
errors []error
Expand Down Expand Up @@ -2970,6 +2993,32 @@ func TestEventuallyWithT_ReturnsTheLatestFinishedConditionErrors(t *testing.T) {
Len(t, mockT.errors, 2)
}

func TestConsistentlyWithTTrue(t *testing.T) {
mockT := new(errorsCapturingT)

condition := func(collect *CollectT) {
True(collect, true)
}

True(t, ConsistentlyWithT(mockT, condition, 100*time.Millisecond, 20*time.Millisecond))
Len(t, mockT.errors, 0)
}

func TestConsistentlyWithTFalse(t *testing.T) {
mockT := new(errorsCapturingT)

state := 0
condition := func(collect *CollectT) {
defer func() {
state += 1
}()
False(collect, state == 2)
}

False(t, ConsistentlyWithT(mockT, condition, 100*time.Millisecond, 20*time.Millisecond))
Len(t, mockT.errors, 1)
}

func TestNeverFalse(t *testing.T) {
condition := func() bool {
return false
Expand Down
84 changes: 84 additions & 0 deletions require/require.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 5e6e95d

Please sign in to comment.