Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Make call_count of Mock thread safe
  • Loading branch information
chaope committed Dec 13, 2025
commit a401818402740042d643e1c26c5c407fe663b60d
22 changes: 22 additions & 0 deletions Lib/test/test_unittest/testmock/testthreadingmock.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import time
import unittest
import threading
import concurrent.futures

from test.support import threading_helper
Expand Down Expand Up @@ -196,6 +197,27 @@ def test_reset_mock_resets_wait(self):
m.wait_until_any_call_with()
m.assert_called_once()

def test_call_count_thread_safe(self):

m = ThreadingMock()

# 3k loops reliably reproduces the issue while keeping runtime ~0.6s
LOOPS = 3_000
THREADS = 10

def test_function():
for _ in range(LOOPS):
m()

threads = [threading.Thread(target=test_function) for _ in range(THREADS)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()

self.assertEqual(m.call_count, LOOPS * THREADS,
f"Expected {LOOPS * THREADS}, got {m.call_count}")


if __name__ == "__main__":
unittest.main()
12 changes: 7 additions & 5 deletions Lib/unittest/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,6 @@ def reset_mock():
ret.reset_mock()

funcopy.called = False
funcopy.call_count = 0
funcopy.call_args = None
funcopy.call_args_list = _CallList()
funcopy.method_calls = _CallList()
Expand Down Expand Up @@ -490,7 +489,6 @@ def __init__(

__dict__['_mock_called'] = False
__dict__['_mock_call_args'] = None
__dict__['_mock_call_count'] = 0
__dict__['_mock_call_args_list'] = _CallList()
__dict__['_mock_mock_calls'] = _CallList()

Expand Down Expand Up @@ -606,11 +604,17 @@ def __class__(self):
return self._spec_class

called = _delegating_property('called')
call_count = _delegating_property('call_count')
call_args = _delegating_property('call_args')
call_args_list = _delegating_property('call_args_list')
mock_calls = _delegating_property('mock_calls')

@property
def call_count(self):
sig = self._mock_delegate
if sig is None:
return len(self._mock_call_args_list)
return len(sig.call_args_list)


def __get_side_effect(self):
delegated = self._mock_delegate
Expand Down Expand Up @@ -646,7 +650,6 @@ def reset_mock(self, visited=None, *,

self.called = False
self.call_args = None
self.call_count = 0
self.mock_calls = _CallList()
self.call_args_list = _CallList()
self.method_calls = _CallList()
Expand Down Expand Up @@ -1180,7 +1183,6 @@ def _mock_call(self, /, *args, **kwargs):

def _increment_mock_call(self, /, *args, **kwargs):
self.called = True
self.call_count += 1

# handle call_args
# needs to be set here so assertions on call arguments pass before
Expand Down
Loading