diff --git a/mock.go b/mock.go index 7255aff..f302b0f 100644 --- a/mock.go +++ b/mock.go @@ -454,9 +454,10 @@ func (m *Mock) newTrap(fn clockFunction, tags []string) *Trap { if !m.testOver { m.logger.Logf("Mock Clock - Trap %s(..., %v)", fn, tags) } + // Copy tags because the caller may reuse its slice while the trap still matches calls. tr := &Trap{ fn: fn, - tags: tags, + tags: slices.Clone(tags), mock: m, calls: make(chan *apiCall), done: make(chan struct{}), @@ -728,9 +729,11 @@ func withDuration(d time.Duration) callArg { } func newCall(fn clockFunction, tags []string, args ...callArg) *apiCall { + // Copy tags because the caller may reuse its slice after the call returns, while a trapped call + // still exposes them as Call.Tags. c := &apiCall{ fn: fn, - Tags: tags, + Tags: slices.Clone(tags), complete: make(chan struct{}), } for _, a := range args { diff --git a/mock_test.go b/mock_test.go index 7bc822d..17d7ea4 100644 --- a/mock_test.go +++ b/mock_test.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "runtime/pprof" + "slices" "strings" "testing" "time" @@ -414,6 +415,33 @@ func Test_UnreleasedCalls(t *testing.T) { }) } +func TestTags_CallerReusesSlice(t *testing.T) { + t.Parallel() + testCtx, testCancel := context.WithTimeout(context.Background(), 10*time.Second) + defer testCancel() + mClock := quartz.NewMock(t) + + trapTags := []string{"foo"} + trap := mClock.Trap().Now(trapTags...) + defer trap.Close() + trapTags[0] = "reused" + + done := make(chan struct{}) + go func() { + defer close(done) + callTags := []string{"foo"} + mClock.Now(callTags...) + callTags[0] = "reused" + }() + + call := trap.MustWait(testCtx) + call.MustRelease(testCtx) + <-done + if want := []string{"foo"}; !slices.Equal(call.Tags, want) { + t.Fatalf("expected tags %v, got %v", want, call.Tags) + } +} + func Test_WithLogger(t *testing.T) { t.Parallel()