Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: race condition on Controller.Satisfied #101

Merged
merged 1 commit into from Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions gomock/controller.go
Expand Up @@ -246,6 +246,8 @@ func (ctrl *Controller) Finish() {
// Satisfied returns whether all expected calls bound to this Controller have been satisfied.
// Calling Finish is then guaranteed to not fail due to missing calls.
func (ctrl *Controller) Satisfied() bool {
ctrl.mu.Lock()
defer ctrl.mu.Unlock()
return ctrl.expectedCalls.Satisfied()
}

Expand Down
9 changes: 9 additions & 0 deletions sample/concurrent/README.md
@@ -0,0 +1,9 @@
# Concurrent

This directory contains an example of executing mock calls concurrently.

To run the test,

```bash
go test -race go.uber.org/mock/sample/concurrent
```
45 changes: 45 additions & 0 deletions sample/concurrent/concurrent_test.go
Expand Up @@ -2,7 +2,9 @@ package concurrent

import (
"context"
"fmt"
"testing"
"time"

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

func waitForMocks(ctx context.Context, ctrl *gomock.Controller) error {
ticker := time.NewTicker(1 * time.Millisecond)
defer ticker.Stop()

timeout := time.After(3 * time.Millisecond)

for {
select {
case <-ticker.C:
if ctrl.Satisfied() {
return nil
}
case <-timeout:
return fmt.Errorf("timeout waiting for mocks to be satisfied")
case <-ctx.Done():
return fmt.Errorf("context cancelled")
}
}
}

// TestConcurrentFails is expected to fail (and is disabled). It
// demonstrates how to use gomock.WithContext to interrupt the test
// from a different goroutine.
Expand All @@ -42,3 +64,26 @@ func TestConcurrentWorks(t *testing.T) {
t.Error("call failed:", err)
}
}

func TestCancelWhenMocksSatisfied(t *testing.T) {
ctrl, ctx := gomock.WithContext(context.Background(), t)
m := mock.NewMockMath(ctrl)
m.EXPECT().Sum(1, 2).Return(3).MinTimes(1)

// This goroutine calls the mock and then waits for the context to be done.
go func() {
for {
m.Sum(1, 2)
select {
case <-ctx.Done():
return
}
}
}()

// waitForMocks spawns another goroutine which blocks until ctrl.Satisfied() is true.
if err := waitForMocks(ctx, ctrl); err != nil {
t.Error("call failed:", err)
}
ctrl.Finish()
}