Skip to content

Commit a401818

Browse files
committed
Make call_count of Mock thread safe
1 parent 3e36d37 commit a401818

File tree

2 files changed

+29
-5
lines changed

2 files changed

+29
-5
lines changed

Lib/test/test_unittest/testmock/testthreadingmock.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import time
22
import unittest
3+
import threading
34
import concurrent.futures
45

56
from test.support import threading_helper
@@ -196,6 +197,27 @@ def test_reset_mock_resets_wait(self):
196197
m.wait_until_any_call_with()
197198
m.assert_called_once()
198199

200+
def test_call_count_thread_safe(self):
201+
202+
m = ThreadingMock()
203+
204+
# 3k loops reliably reproduces the issue while keeping runtime ~0.6s
205+
LOOPS = 3_000
206+
THREADS = 10
207+
208+
def test_function():
209+
for _ in range(LOOPS):
210+
m()
211+
212+
threads = [threading.Thread(target=test_function) for _ in range(THREADS)]
213+
for thread in threads:
214+
thread.start()
215+
for thread in threads:
216+
thread.join()
217+
218+
self.assertEqual(m.call_count, LOOPS * THREADS,
219+
f"Expected {LOOPS * THREADS}, got {m.call_count}")
220+
199221

200222
if __name__ == "__main__":
201223
unittest.main()

Lib/unittest/mock.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,6 @@ def reset_mock():
256256
ret.reset_mock()
257257

258258
funcopy.called = False
259-
funcopy.call_count = 0
260259
funcopy.call_args = None
261260
funcopy.call_args_list = _CallList()
262261
funcopy.method_calls = _CallList()
@@ -490,7 +489,6 @@ def __init__(
490489

491490
__dict__['_mock_called'] = False
492491
__dict__['_mock_call_args'] = None
493-
__dict__['_mock_call_count'] = 0
494492
__dict__['_mock_call_args_list'] = _CallList()
495493
__dict__['_mock_mock_calls'] = _CallList()
496494

@@ -606,11 +604,17 @@ def __class__(self):
606604
return self._spec_class
607605

608606
called = _delegating_property('called')
609-
call_count = _delegating_property('call_count')
610607
call_args = _delegating_property('call_args')
611608
call_args_list = _delegating_property('call_args_list')
612609
mock_calls = _delegating_property('mock_calls')
613610

611+
@property
612+
def call_count(self):
613+
sig = self._mock_delegate
614+
if sig is None:
615+
return len(self._mock_call_args_list)
616+
return len(sig.call_args_list)
617+
614618

615619
def __get_side_effect(self):
616620
delegated = self._mock_delegate
@@ -646,7 +650,6 @@ def reset_mock(self, visited=None, *,
646650

647651
self.called = False
648652
self.call_args = None
649-
self.call_count = 0
650653
self.mock_calls = _CallList()
651654
self.call_args_list = _CallList()
652655
self.method_calls = _CallList()
@@ -1180,7 +1183,6 @@ def _mock_call(self, /, *args, **kwargs):
11801183

11811184
def _increment_mock_call(self, /, *args, **kwargs):
11821185
self.called = True
1183-
self.call_count += 1
11841186

11851187
# handle call_args
11861188
# needs to be set here so assertions on call arguments pass before

0 commit comments

Comments
 (0)