Skip to content

Commit 892b665

Browse files
fix: race condition on Controller.Satisfied (#101)
1 parent bdd3140 commit 892b665

File tree

3 files changed

+56
-0
lines changed

3 files changed

+56
-0
lines changed

gomock/controller.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,8 @@ func (ctrl *Controller) Finish() {
246246
// Satisfied returns whether all expected calls bound to this Controller have been satisfied.
247247
// Calling Finish is then guaranteed to not fail due to missing calls.
248248
func (ctrl *Controller) Satisfied() bool {
249+
ctrl.mu.Lock()
250+
defer ctrl.mu.Unlock()
249251
return ctrl.expectedCalls.Satisfied()
250252
}
251253

sample/concurrent/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Concurrent
2+
3+
This directory contains an example of executing mock calls concurrently.
4+
5+
To run the test,
6+
7+
```bash
8+
go test -race go.uber.org/mock/sample/concurrent
9+
```

sample/concurrent/concurrent_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package concurrent
22

33
import (
44
"context"
5+
"fmt"
56
"testing"
7+
"time"
68

79
"go.uber.org/mock/gomock"
810
mock "go.uber.org/mock/sample/concurrent/mock"
@@ -22,6 +24,26 @@ func call(ctx context.Context, m Math) (int, error) {
2224
}
2325
}
2426

27+
func waitForMocks(ctx context.Context, ctrl *gomock.Controller) error {
28+
ticker := time.NewTicker(1 * time.Millisecond)
29+
defer ticker.Stop()
30+
31+
timeout := time.After(3 * time.Millisecond)
32+
33+
for {
34+
select {
35+
case <-ticker.C:
36+
if ctrl.Satisfied() {
37+
return nil
38+
}
39+
case <-timeout:
40+
return fmt.Errorf("timeout waiting for mocks to be satisfied")
41+
case <-ctx.Done():
42+
return fmt.Errorf("context cancelled")
43+
}
44+
}
45+
}
46+
2547
// TestConcurrentFails is expected to fail (and is disabled). It
2648
// demonstrates how to use gomock.WithContext to interrupt the test
2749
// from a different goroutine.
@@ -42,3 +64,26 @@ func TestConcurrentWorks(t *testing.T) {
4264
t.Error("call failed:", err)
4365
}
4466
}
67+
68+
func TestCancelWhenMocksSatisfied(t *testing.T) {
69+
ctrl, ctx := gomock.WithContext(context.Background(), t)
70+
m := mock.NewMockMath(ctrl)
71+
m.EXPECT().Sum(1, 2).Return(3).MinTimes(1)
72+
73+
// This goroutine calls the mock and then waits for the context to be done.
74+
go func() {
75+
for {
76+
m.Sum(1, 2)
77+
select {
78+
case <-ctx.Done():
79+
return
80+
}
81+
}
82+
}()
83+
84+
// waitForMocks spawns another goroutine which blocks until ctrl.Satisfied() is true.
85+
if err := waitForMocks(ctx, ctrl); err != nil {
86+
t.Error("call failed:", err)
87+
}
88+
ctrl.Finish()
89+
}

0 commit comments

Comments
 (0)