Skip to content

mock: reduce data races in Arguments.Diff for pointer-like arguments#1866

Open
pavelzeman wants to merge 1 commit intostretchr:masterfrom
pavelzeman:fix/mock-diff-race
Open

mock: reduce data races in Arguments.Diff for pointer-like arguments#1866
pavelzeman wants to merge 1 commit intostretchr:masterfrom
pavelzeman:fix/mock-diff-race

Conversation

@pavelzeman
Copy link
Copy Markdown

Summary

Fixes Arguments.Diff causing data races when mock arguments (pointers, maps, slices) are concurrently modified by other goroutines.

Problem

Arguments.Diff uses fmt.Sprintf("%v") to format arguments for diff output. This traverses the underlying data structure, which races with concurrent writers:

  • Maps: triggers fatal error: concurrent map iteration and map write — crashes the entire test process (non-recoverable by recover())
  • Pointers/slices: causes data races detectable by go test -race

This is a real-world issue. For example, when testing code that passes *sql.DB to mocked methods, the database connection cleaner goroutine modifies internal fields while testify formats the struct for diff output.

Solution

Introduces safeFormatArg() which uses %p (address only) for reference types (Ptr, Map, Slice, Chan) instead of %v, avoiding traversal of the underlying data structure entirely. All other types (structs, primitives, strings) use the full %v representation since they are value types and cannot race.

Why not deep-copy?

Copying the arguments before formatting seems like it would preserve the full %v output, but:

  • Maps: even iterating to copy triggers the fatal concurrent map iteration and map write error
  • Pointers: copying the pointed-to value still triggers the race detector under -race during the copy itself

Why not recover()?

  • Maps: concurrent map iteration and map write is a runtime throw, not a panic — recover() cannot catch it
  • Pointers/slices: while panics are recoverable, the -race detector flags the concurrent access before any panic occurs, causing test failures

Tests

Three new concurrent-modification tests:

  1. Test_Arguments_Diff_ConcurrentPointerModification — pointer struct modified by another goroutine (reproducer from mock: Diff is prone to data races #1597)
  2. Test_Arguments_Diff_ConcurrentMapModification — map modified by another goroutine
  3. Test_Arguments_Diff_ConcurrentSliceModification — slice modified by another goroutine

All three crash or trigger race detector failures without the fix and pass cleanly with it.

Tradeoff

Reference-type arguments now show addresses instead of values in diff output (e.g., ([]int=0xc0000b4030) instead of ([]int=[1 2 3])). The detailed structural diff (from ObjectsAreEqual) still appears above the summary line when arguments don't match, so the diagnostic value loss is minimal.

Fixes #1597

Arguments.Diff uses fmt.Sprintf("%v") to format arguments for diff output.
When arguments contain pointers, maps, slices, or other reference types that
are concurrently modified by other goroutines, this causes data races:

- For maps: a fatal "concurrent map iteration and map write" error that
  crashes the entire test process (non-recoverable)
- For pointers/slices: data races detectable by -race, and potential panics

This commit introduces safeFormatArg() which uses %p (address only) for
reference types (pointers, maps, slices, channels) instead of %v, avoiding
traversal of the underlying data structure entirely. All other types use
the full %v representation since they are value types and cannot race.

Fixes stretchr#1597
@dolmen dolmen added pkg-mock Any issues related to Mock concurrency labels Apr 21, 2026
@dolmen
Copy link
Copy Markdown
Collaborator

dolmen commented Apr 21, 2026

Related commit in project Mattermost's testsuite: mattermost/mattermost@4b8a4ae

Previous closed PR: #1865

@dolmen dolmen added the mock.ArgumentMatcher About matching arguments in mock label Apr 22, 2026
Copy link
Copy Markdown
Collaborator

@dolmen dolmen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The submission claims "Fixes #1594", but this isn't the case: some cases of races are avoided in formatting the output (so this PR would still be an improvement), but races could also happen in the comparison itself.

So the claims in the commit message and the description of this PR must be lowered.

Some test cases are added that show the issue when the diff output is formatted, but they cover just Arguments.Diff, and more tests that cover the full call from Mock would be welcome.

Once my specific issues are fixed (ex: missing testsuite for safeFormatArg, renamming the function...) we will probably be able to merge this, but #1594 will still not be fixed.

Comment thread mock/mock.go
// cannot race, so they use the full %v representation.
//
// See https://github.com/stretchr/testify/issues/1597
func safeFormatArg(v interface{}) string {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a testsuite for this new function.

Also, in its current implementation it is safer but no yet "safe". So the name is misleading.

Comment thread mock/mock.go
return fmt.Sprintf("(%[1]T=%[1]v)", v)
}
switch reflect.TypeOf(v).Kind() {
case reflect.Ptr, reflect.Map, reflect.Slice, reflect.Chan:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace reflect.Ptr (deprecated) with reflect.Pointer.

Other types must also be handled. In particular reflect.Array, reflect.Interface. We definitely need a comprehensive testsuite.

Comment thread mock/mock.go
}
switch reflect.TypeOf(v).Kind() {
case reflect.Ptr, reflect.Map, reflect.Slice, reflect.Chan:
return fmt.Sprintf("(%T=%p)", v, v)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you checked the output of safeFormatArg([]int(nil))?
We need a comprehensive test suite.

Also use [1] like in other paths.

Comment thread mock/mock_test.go
}

// Test_Arguments_Diff_ConcurrentPointerModification verifies that
// Arguments.Diff does not race when a pointer argument is concurrently
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 3 tests have much common code. Please refactor to extract a func(t *testing.T, arg interface{}, accessArg func()).

Also group them as a top level test with 3 subtests.

@dolmen dolmen changed the title mock: fix data race in Arguments.Diff for pointer-like arguments mock: reduce data races in Arguments.Diff for pointer-like arguments Apr 22, 2026
@dolmen dolmen added the bug label Apr 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug concurrency mock.ArgumentMatcher About matching arguments in mock pkg-mock Any issues related to Mock

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mock: Diff is prone to data races

2 participants