]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.13] gh-142651: make `Mock.call_count` thread-safe (GH-142656) (#142744)
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Mon, 15 Dec 2025 12:52:11 +0000 (13:52 +0100)
committerGitHub <noreply@github.com>
Mon, 15 Dec 2025 12:52:11 +0000 (18:22 +0530)
gh-142651: make `Mock.call_count` thread-safe (GH-142656)
(cherry picked from commit 850f95f6f64a55920cbb91b022b70b736bd20ed8)

Co-authored-by: chaope <pengchaoandy@gmail.com>
Lib/test/test_unittest/testmock/testthreadingmock.py
Lib/unittest/mock.py
Misc/NEWS.d/next/Library/2025-12-13-06-17-44.gh-issue-142651.ZRtBu4.rst [new file with mode: 0644]

index a02b532ed447cd572c4d5648d609817b7e603fe6..3603995b090a6c9d48977e101352f35d4b0b48fa 100644 (file)
@@ -1,8 +1,10 @@
+import sys
 import time
 import unittest
+import threading
 import concurrent.futures
 
-from test.support import threading_helper
+from test.support import setswitchinterval, threading_helper
 from unittest.mock import patch, ThreadingMock
 
 
@@ -196,6 +198,26 @@ class TestThreadingMock(unittest.TestCase):
         m.wait_until_any_call_with()
         m.assert_called_once()
 
+    def test_call_count_thread_safe(self):
+        # See https://github.com/python/cpython/issues/142651.
+        m = ThreadingMock()
+        LOOPS = 100
+        THREADS = 10
+        def test_function():
+            for _ in range(LOOPS):
+                m()
+
+        oldswitchinterval = sys.getswitchinterval()
+        setswitchinterval(1e-6)
+        try:
+            threads = [threading.Thread(target=test_function) for _ in range(THREADS)]
+            with threading_helper.start_threads(threads):
+                pass
+        finally:
+            sys.setswitchinterval(oldswitchinterval)
+
+        self.assertEqual(m.call_count, LOOPS * THREADS)
+
 
 if __name__ == "__main__":
     unittest.main()
index 6cec61ff35ccd84c7c27f34d0de8058bf81369bb..1dd7735d150ebebe7a73172b1f4e1364de0032e9 100644 (file)
@@ -1174,7 +1174,6 @@ class CallableMixin(Base):
 
     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
@@ -1182,6 +1181,7 @@ class CallableMixin(Base):
         _call = _Call((args, kwargs), two=True)
         self.call_args = _call
         self.call_args_list.append(_call)
+        self.call_count = len(self.call_args_list)
 
         # initial stuff for method_calls:
         do_method_calls = self._mock_parent is not None
diff --git a/Misc/NEWS.d/next/Library/2025-12-13-06-17-44.gh-issue-142651.ZRtBu4.rst b/Misc/NEWS.d/next/Library/2025-12-13-06-17-44.gh-issue-142651.ZRtBu4.rst
new file mode 100644 (file)
index 0000000..236900b
--- /dev/null
@@ -0,0 +1,3 @@
+:mod:`unittest.mock`: fix a thread safety issue where :attr:`Mock.call_count
+<unittest.mock.Mock.call_count>` may return inaccurate values when the mock
+is called concurrently from multiple threads.