Skip to content

Commit 01fe5e6

Browse files
johnstcnmafredri
andauthored
chore: add testutil.Eventually and friends (coder#3389)
This PR adds a `testutil` function aimed to replace `require.Eventually`. Before: ```go require.Eventually(t, func() bool { ... }, testutil.WaitShort, testutil.IntervalFast) ``` After: ```go require.True(t, testutil.EventuallyShort(t, func(ctx context.Context) bool { ... })) // or the full incantation if you need more control ctx, cancel := context.WithTimeout(ctx.Background(), testutil.WaitLong) require.True(t, testutil.Eventually(t, ctx, func(ctx context.Context) bool { ... }, testutil.IntervalSlow)) ``` Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
1 parent 46d64c6 commit 01fe5e6

File tree

5 files changed

+150
-12
lines changed

5 files changed

+150
-12
lines changed

coderd/coderdtest/coderdtest.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -411,11 +411,11 @@ func AwaitTemplateVersionJob(t *testing.T, client *codersdk.Client, version uuid
411411

412412
t.Logf("waiting for template version job %s", version)
413413
var templateVersion codersdk.TemplateVersion
414-
require.Eventually(t, func() bool {
414+
require.True(t, testutil.EventuallyShort(t, func(ctx context.Context) bool {
415415
var err error
416-
templateVersion, err = client.TemplateVersion(context.Background(), version)
416+
templateVersion, err = client.TemplateVersion(ctx, version)
417417
return assert.NoError(t, err) && templateVersion.Job.CompletedAt != nil
418-
}, testutil.WaitShort, testutil.IntervalFast)
418+
}))
419419
return templateVersion
420420
}
421421

@@ -425,11 +425,10 @@ func AwaitWorkspaceBuildJob(t *testing.T, client *codersdk.Client, build uuid.UU
425425

426426
t.Logf("waiting for workspace build job %s", build)
427427
var workspaceBuild codersdk.WorkspaceBuild
428-
require.Eventually(t, func() bool {
429-
var err error
430-
workspaceBuild, err = client.WorkspaceBuild(context.Background(), build)
428+
require.True(t, testutil.EventuallyShort(t, func(ctx context.Context) bool {
429+
workspaceBuild, err := client.WorkspaceBuild(ctx, build)
431430
return assert.NoError(t, err) && workspaceBuild.Job.CompletedAt != nil
432-
}, testutil.WaitShort, testutil.IntervalFast)
431+
}))
433432
return workspaceBuild
434433
}
435434

@@ -439,21 +438,22 @@ func AwaitWorkspaceAgents(t *testing.T, client *codersdk.Client, build uuid.UUID
439438

440439
t.Logf("waiting for workspace agents (build %s)", build)
441440
var resources []codersdk.WorkspaceResource
442-
require.Eventually(t, func() bool {
441+
require.True(t, testutil.EventuallyLong(t, func(ctx context.Context) bool {
443442
var err error
444-
resources, err = client.WorkspaceResourcesByBuild(context.Background(), build)
443+
resources, err = client.WorkspaceResourcesByBuild(ctx, build)
445444
if !assert.NoError(t, err) {
446445
return false
447446
}
448447
for _, resource := range resources {
449448
for _, agent := range resource.Agents {
450449
if agent.Status != codersdk.WorkspaceAgentConnected {
450+
t.Logf("agent %s not connected yet", agent.Name)
451451
return false
452452
}
453453
}
454454
}
455455
return true
456-
}, testutil.WaitLong, testutil.IntervalMedium)
456+
}))
457457
return resources
458458
}
459459

coderd/coderdtest/coderdtest_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func TestNew(t *testing.T) {
1919
})
2020
user := coderdtest.CreateFirstUser(t, client)
2121
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
22-
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
22+
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
2323
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
2424
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
2525
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)

scripts/rules.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func useStandardTimeoutsAndDelaysInTests(m dsl.Matcher) {
9191
m.Import("github.com/coder/coder/testutil")
9292

9393
m.Match(`context.WithTimeout($ctx, $duration)`).
94-
Where(m.File().Imports("testing") && !m["duration"].Text.Matches("^testutil\\.")).
94+
Where(m.File().Imports("testing") && !m.File().PkgPath.Matches("testutil$") && !m["duration"].Text.Matches("^testutil\\.")).
9595
At(m["duration"]).
9696
Report("Do not use magic numbers in test timeouts and delays. Use the standard testutil.Wait* or testutil.Interval* constants instead.")
9797

testutil/eventually.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package testutil
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
// Eventually is like require.Eventually except it allows passing
12+
// a context into the condition. It is safe to use with `require.*`.
13+
//
14+
// If ctx times out, the test will fail, but not immediately.
15+
// It is the caller's responsibility to exit early if required.
16+
//
17+
// It is the caller's responsibility to ensure that ctx has a
18+
// deadline or timeout set. Eventually will panic if this is not
19+
// the case in order to avoid potentially waiting forever.
20+
//
21+
// condition is not run in a goroutine; use the provided
22+
// context argument for cancellation if required.
23+
func Eventually(ctx context.Context, t testing.TB, condition func(context.Context) bool, tick time.Duration) bool {
24+
t.Helper()
25+
26+
if _, ok := ctx.Deadline(); !ok {
27+
panic("developer error: must set deadline or timeout on ctx")
28+
}
29+
30+
ticker := time.NewTicker(tick)
31+
defer ticker.Stop()
32+
for tick := ticker.C; ; {
33+
select {
34+
case <-ctx.Done():
35+
assert.NoError(t, ctx.Err(), "Eventually timed out")
36+
return false
37+
case <-tick:
38+
assert.NoError(t, ctx.Err(), "Eventually timed out")
39+
if condition(ctx) {
40+
return true
41+
}
42+
}
43+
}
44+
}
45+
46+
// EventuallyShort is a convenience function that runs Eventually with
47+
// IntervalFast and times out after WaitShort.
48+
func EventuallyShort(t testing.TB, condition func(context.Context) bool) bool {
49+
ctx, cancel := context.WithTimeout(context.Background(), WaitShort)
50+
defer cancel()
51+
return Eventually(ctx, t, condition, IntervalFast)
52+
}
53+
54+
// EventuallyMedium is a convenience function that runs Eventually with
55+
// IntervalMedium and times out after WaitMedium.
56+
func EventuallyMedium(t testing.TB, condition func(context.Context) bool) bool {
57+
ctx, cancel := context.WithTimeout(context.Background(), WaitMedium)
58+
defer cancel()
59+
return Eventually(ctx, t, condition, IntervalMedium)
60+
}
61+
62+
// EventuallyLong is a convenience function that runs Eventually with
63+
// IntervalSlow and times out after WaitLong.
64+
func EventuallyLong(t testing.TB, condition func(context.Context) bool) bool {
65+
ctx, cancel := context.WithTimeout(context.Background(), WaitLong)
66+
defer cancel()
67+
return Eventually(ctx, t, condition, IntervalSlow)
68+
}

testutil/eventually_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package testutil_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"go.uber.org/goleak"
9+
10+
"github.com/coder/coder/testutil"
11+
)
12+
13+
func TestMain(m *testing.M) {
14+
goleak.VerifyTestMain(m)
15+
}
16+
17+
func TestEventually(t *testing.T) {
18+
t.Parallel()
19+
t.Run("OK", func(t *testing.T) {
20+
t.Parallel()
21+
state := 0
22+
condition := func(_ context.Context) bool {
23+
defer func() {
24+
state++
25+
}()
26+
return state > 2
27+
}
28+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
29+
defer cancel()
30+
testutil.Eventually(ctx, t, condition, testutil.IntervalFast)
31+
})
32+
33+
t.Run("Timeout", func(t *testing.T) {
34+
t.Parallel()
35+
condition := func(_ context.Context) bool {
36+
return false
37+
}
38+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
39+
defer cancel()
40+
mockT := new(testing.T)
41+
testutil.Eventually(ctx, mockT, condition, testutil.IntervalFast)
42+
assert.True(t, mockT.Failed())
43+
})
44+
45+
t.Run("Panic", func(t *testing.T) {
46+
t.Parallel()
47+
48+
panicky := func() {
49+
mockT := new(testing.T)
50+
condition := func(_ context.Context) bool { return true }
51+
testutil.Eventually(context.Background(), mockT, condition, testutil.IntervalFast)
52+
}
53+
assert.Panics(t, panicky)
54+
})
55+
56+
t.Run("Short", func(t *testing.T) {
57+
t.Parallel()
58+
testutil.EventuallyShort(t, func(_ context.Context) bool { return true })
59+
})
60+
61+
t.Run("Medium", func(t *testing.T) {
62+
t.Parallel()
63+
testutil.EventuallyMedium(t, func(_ context.Context) bool { return true })
64+
})
65+
66+
t.Run("Long", func(t *testing.T) {
67+
t.Parallel()
68+
testutil.EventuallyLong(t, func(_ context.Context) bool { return true })
69+
})
70+
}

0 commit comments

Comments
 (0)