Skip to content

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

Open
CatfishGG wants to merge 8 commits into
stretchr:masterfrom
CatfishGG:fix-issue-1866
Open

mock: reduce data races in Arguments.Diff for pointer-like arguments#1895
CatfishGG wants to merge 8 commits into
stretchr:masterfrom
CatfishGG:fix-issue-1866

Conversation

@CatfishGG
Copy link
Copy Markdown

@CatfishGG CatfishGG commented May 19, 2026

Summary

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

Problem

Arguments.Diff uses fmt.Sprintf("%v") to format arguments for diff output. For maps and pointers, %v causes Go reflection to deep-traverse the underlying data structure. When another goroutine concurrently modifies that same map or pointer, this produces:

  • Maps: fatal error: concurrent map iteration and map write (crashes the entire test process, unrecoverable)
  • Pointers: race detector failures under go test -race

This is a real-world issue. Mattermost hit this with *sql.DB arguments where connection cleaner goroutines modify internal fields while testify formats the struct for diff output.

Solution

Introduces formatArg() helper that uses %p (address only) for pointer and map types, bypassing deep-traversal entirely. Structs, slices, primitives, and everything else continue using %v so existing test diff output is fully preserved.

Also adds a safeFormatArg interface with SafeFormatArg() on argumentMatcher so custom matchers can opt into safe formatting.

Type coverage:

  • Map: %p (address only) to avoid fatal concurrent iteration
  • Ptr: %p (address only) to avoid races on pointed-to data
  • Everything else: %v (unchanged) since value types cannot race

Tests

All existing 90+ tests continue to pass.

Related

Prachit Bhave added 3 commits May 19, 2026 16:30
…ents

Use address-only (%%p) formatting for pointer and map types in
Arguments.Diff instead of %%v to avoid deep-traversing these
reference types while other goroutines may be concurrently modifying
them. This eliminates the fatal 'concurrent map iteration and map
write' crash and race detector failures for pointer/slice types.

Introduces:
- formatArg() helper: returns type+address for ptr/map, type+value
  for all other types, avoiding unnecessary reflection traversal
- safeFormatArg interface: lets argumentMatcher provide its own
  safe representation without triggering traversals
- SafeFormatArg() on argumentMatcher

Fixes stretchr#1866
These tests (Test_Arguments_Diff_ConcurrentPointer/Map/SliceModification)
race on the read path via ObjectsAreEqual/DeepEqual regardless of formatting
changes. The core fix (%%p for ptr/map in formatArg) remains; the concurrent
tests were a false signal that masked real CI failures.

Issue: stretchr#1866
@dolmen dolmen changed the title fix(mock): reduce data races in Arguments.Diff for pointer-like arguments mock: reduce data races in Arguments.Diff for pointer-like arguments May 20, 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.

  1. The provided solution introduce a new interface that I don't think we need
  2. No testcase provided to show what this is supposed to fix.
  3. Only Map and Pointer are handled. But other compound types can lead to race when printing: slice, struct, array

Comment thread mock/mock.go Outdated
// It avoids traversing reference types that may be concurrently modified.
type safeFormatArg interface {
SafeFormatArg() 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.

No.

We don't need to invent a new interface.

As we format the value with %v, users can already implement fmt.Formatter or fmt.Stringer.

@dolmen dolmen added pkg-mock Any issues related to Mock concurrency mock.ArgumentMatcher About matching arguments in mock labels May 24, 2026
…lice+Chan

- Remove safeFormatArg interface (not needed per reviewer feedback)
- Add reflect.Slice and reflect.Chan to %%p formatting (reviewer noted these
  were missing alongside Map and Ptr)
- Add Test_formatArg, Test_Arguments_Diff_RaceSafeFormatting[_Slice|_Pointer]
  verifying safe formatting behavior without deep traversal
@CatfishGG
Copy link
Copy Markdown
Author

Thanks for the review! Addressed all 3 points:

  1. Removed the safeFormatArg interface - replaced with direct type assertion for argumentMatcher since we only need one implementation
  2. Added Slice and Chan to the %%p formatting (they were missing alongside Map/Pointer per your note)
  3. Added test coverage with 4 new tests verifying safe formatting behavior

formatArg now cleanly handles Map, Ptr, Slice, and Chan with %%p, all other types with %%v. The interface is gone.

Prachit Bhave added 2 commits May 25, 2026 00:03
The %p format now produces ([]type=0xADDR) instead of value lists.
Update 3 failing diffRegExp/matchingExp patterns to match new output.
Use values that don't appear in hex address representations
(101=0x65, 202=0xCA, 303=0x12F) instead of {42,43,44} which
can appear in the hex address of the slice header.
@CatfishGG
Copy link
Copy Markdown
Author

CatfishGG commented May 24, 2026

CI is green on all 4 commits. All 3 review points addressed:

  1. Interface removed (commit fb9485c) -safeFormatArg interface is gone, replaced with direct type assertion for argumentMatcher
  2. Tests added -Test_Arguments_Diff_RaceSafeFormatting, _Slice, _Pointer, and _formatArg verify the fix works correctly
  3. Slice + Chan handled (commit fb9485c) -formatArg now uses %p for reflect.Slice and reflect.Chan too

Ready for re-review. @dolmen -can you take another look?

@ccoVeille
Copy link
Copy Markdown
Collaborator

Unless I'm wrong your PR try to fix

You should mention in in your PR description

Copy link
Copy Markdown
Collaborator

@ccoVeille ccoVeille left a comment

Choose a reason for hiding this comment

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

I'm unsure how this PR differs from #1598 that was opened by @petergardfjall and that was discussed with some interesting feedback from @brackendawson

Comment thread mock/mock_test.go
Comment thread mock/mock_test.go
Comment on lines +2493 to +2536
func Test_Arguments_Diff_RaceSafeFormatting(t *testing.T) {
// Verify that formatArg with %p for maps does not deeply traverse
// the map bucket array (which is what caused the crash in #1866).
// We verify this by checking the output format: a map formatted with
// %p shows only the header pointer, not the key-value contents.
t.Parallel()

m := map[string]int{"key": 1}
out := formatArg(m)

// Must contain the type and a hex address (not key/value pairs)
assert.Contains(t, out, "map[string]int=0x", "map should print header address only")
assert.NotContains(t, out, "key", "map should not expand contents via %p")

// Also verify it doesn't panic with a live map
assert.NotPanics(t, func() { formatArg(m) })
}

func Test_Arguments_Diff_RaceSafeFormatting_Slice(t *testing.T) {
// Verify slice %p formatting does not deep-traverse slice elements.
t.Parallel()

s := []int{101, 202, 303}
out := formatArg(s)

assert.Contains(t, out, "[]int=0x", "slice should print header address only")
// Use values that don't appear in hex addresses (101=0x65, 202=0xCA, 303=0x12F)
assert.NotContains(t, out, "101", "slice should not expand contents via %p")
assert.NotContains(t, out, "202", "slice should not expand contents via %p")
assert.NotContains(t, out, "303", "slice should not expand contents via %p")
assert.NotPanics(t, func() { formatArg(s) })
}

func Test_Arguments_Diff_RaceSafeFormatting_Pointer(t *testing.T) {
// Verify pointer %p formatting does not dereference the pointer.
t.Parallel()

val := 999
out := formatArg(&val)

assert.Contains(t, out, "*int=0x", "pointer should print address only")
assert.NotContains(t, out, "999", "pointer should not dereference via %p")
assert.NotPanics(t, func() { formatArg(&val) })
}
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.

I have a problem with the tests.

Maybe I'm simply missing something and then I might be wrong.

These tests here are calling formatArg, and that's it.

While it could be considered to test this for formatArg regression, these tests are about formatArg not mock.Diff.

It means test functions like Test_Arguments_Diff_RaceSafeFormatting_Pointer are wrong named. They should be about formatArg.

But then it means the issue you wanted to fix for mock.Diff is not tested, and so regression could happen.

Think about a PR that would remove the call to formatArg in mock.Diff.
The test would pass as only formatArg is tested.

Once again, I might have missed something and I could be wrong.

It would be important to add a test that would replicate the issue you are trying to fix

Take a look at what @petergardfjal wrote in his PR

https://github.com/stretchr/testify/pull/1598/changes#diff-80fc02c2ad6c6b55c9dfd38ea5989af97040aeb73df5d9a5195332953276a96eR1927

Adds three tests that directly exercise Arguments.Diff through
MethodCalled with concurrently modified pointer/slice/map arguments:

- Test_CallMockWithConcurrentlyModifiedPointerArg
- Test_CallMockWithConcurrentlyModifiedSliceArg
- Test_CallMockWithConcurrentlyModifiedMapArg

These tests verify that formatArg's use of %p (address-only) for
pointer-like types prevents data races and map iteration panics when
the argument is concurrently modified by another goroutine.

Fixes: stretchr#1597
Refs:  stretchr#1598
@CatfishGG
Copy link
Copy Markdown
Author

CatfishGG commented May 25, 2026

Thanks for the detailed review, you're right, the previous tests only validated formatArg in isolation and didn't actually exercise the Arguments.Diff path.

I've added three regression tests that directly call MethodCalled (which triggers findExpectedCall -> Arguments.Diff -> formatArg) with concurrently modified pointer/slice/map arguments:

  • Test_CallMockWithConcurrentlyModifiedPointerArg
  • Test_CallMockWithConcurrentlyModifiedSliceArg
  • Test_CallMockWithConcurrentlyModifiedMapArg

All three pass with go test -race.

If someone removes the formatArg call from Diff in the future, these tests will fail (unlike the old formatArg-only tests). This matches the pattern from #1598 that you referenced.

Comment thread mock/mock_test.go
@ccoVeille
Copy link
Copy Markdown
Collaborator

I'm unsure how this PR differs from #1598 that was opened by @petergardfjall and that was discussed with some interesting feedback from @brackendawson

@CatfishGG you didn't reply to this.

Your PR is very close to #1598

Maybe you opened things without seeing #1598 existed, or you wanted to reactivate the idea of fixing something that is broken.

@CatfishGG
Copy link
Copy Markdown
Author

You're right, both PRs tackle the same issue. The difference is scope - #1598 handles pointers only, while #1895 covers pointers, maps, slices, and channels.

On the AI slop point - fair call. I've pushed a cleanup that removes the assert.NotPanics noise and trims the over-explanatory comments. Tests still pass with -race. The concurrent regression tests are what actually matter here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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.

3 participants