From a401818402740042d643e1c26c5c407fe663b60d Mon Sep 17 00:00:00 2001 From: Chao Peng Date: Sat, 13 Dec 2025 01:11:59 -0500 Subject: [PATCH 01/17] Make call_count of Mock thread safe --- .../testmock/testthreadingmock.py | 22 +++++++++++++++++++ Lib/unittest/mock.py | 12 +++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_unittest/testmock/testthreadingmock.py b/Lib/test/test_unittest/testmock/testthreadingmock.py index a02b532ed447cd..2ee90ac1d02952 100644 --- a/Lib/test/test_unittest/testmock/testthreadingmock.py +++ b/Lib/test/test_unittest/testmock/testthreadingmock.py @@ -1,5 +1,6 @@ import time import unittest +import threading import concurrent.futures from test.support import threading_helper @@ -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() diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 0bb6750655380d..a7fad7fec46da0 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -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() @@ -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() @@ -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 @@ -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() @@ -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 From d532d23633eeea55eb5ead7db88c51a1d1ab9fc3 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sat, 13 Dec 2025 06:17:46 +0000 Subject: [PATCH 02/17] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2025-12-13-06-17-44.gh-issue-142651.ZRtBu4.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-12-13-06-17-44.gh-issue-142651.ZRtBu4.rst 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 index 00000000000000..1cca83474d2820 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-13-06-17-44.gh-issue-142651.ZRtBu4.rst @@ -0,0 +1 @@ +Fixed a thread safety issue in :mod:`unittest.mock` where :attr:`~unittest.mock.Mock.call_count` could return inaccurate values when a mock was called concurrently from multiple threads. The attribute now derives its value from the length of :attr:`~unittest.mock.Mock.call_args_list` to ensure consistency. From fe7be86596dcb3cbb3483ceb3175d3e791f3cff7 Mon Sep 17 00:00:00 2001 From: Chao Peng Date: Sat, 13 Dec 2025 01:25:15 -0500 Subject: [PATCH 03/17] update doc --- Doc/library/unittest.mock.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Doc/library/unittest.mock.rst b/Doc/library/unittest.mock.rst index 91f90a0726aa93..86b13f33c42e7b 100644 --- a/Doc/library/unittest.mock.rst +++ b/Doc/library/unittest.mock.rst @@ -532,7 +532,8 @@ the *new_callable* argument to :func:`patch`. .. attribute:: call_count - An integer telling you how many times the mock object has been called: + An integer telling you how many times the mock object has been called, + it is the length of call_args_list: >>> mock = Mock(return_value=None) >>> mock.call_count @@ -541,6 +542,8 @@ the *new_callable* argument to :func:`patch`. >>> mock() >>> mock.call_count 2 + >>> mock.call_count == len(mock.call_args_list) + True .. attribute:: return_value From 29578a94af07fc61fa68ae9e6ba9fa0f37aa531c Mon Sep 17 00:00:00 2001 From: Chao Peng Date: Sat, 13 Dec 2025 02:46:32 -0500 Subject: [PATCH 04/17] calculate call_count after append call_args_list --- .../test_unittest/testmock/testthreadingmock.py | 10 +++++----- Lib/unittest/mock.py | 13 +++++-------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_unittest/testmock/testthreadingmock.py b/Lib/test/test_unittest/testmock/testthreadingmock.py index 2ee90ac1d02952..9672641a47eed4 100644 --- a/Lib/test/test_unittest/testmock/testthreadingmock.py +++ b/Lib/test/test_unittest/testmock/testthreadingmock.py @@ -198,23 +198,23 @@ def test_reset_mock_resets_wait(self): 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}") diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index a7fad7fec46da0..47df724474fd93 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -256,6 +256,7 @@ 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() @@ -489,6 +490,7 @@ 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() @@ -605,17 +607,10 @@ def __class__(self): called = _delegating_property('called') call_args = _delegating_property('call_args') + call_count = _delegating_property('call_count') 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 if delegated is None: @@ -650,6 +645,7 @@ 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() @@ -1190,6 +1186,7 @@ def _increment_mock_call(self, /, *args, **kwargs): _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 From c2e4ca7d882efc7a49314fafd68d5029cbc6e487 Mon Sep 17 00:00:00 2001 From: chaope Date: Sat, 13 Dec 2025 08:34:45 -0500 Subject: [PATCH 05/17] revert change on documentation for call_count. --- Doc/library/unittest.mock.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Doc/library/unittest.mock.rst b/Doc/library/unittest.mock.rst index 86b13f33c42e7b..91f90a0726aa93 100644 --- a/Doc/library/unittest.mock.rst +++ b/Doc/library/unittest.mock.rst @@ -532,8 +532,7 @@ the *new_callable* argument to :func:`patch`. .. attribute:: call_count - An integer telling you how many times the mock object has been called, - it is the length of call_args_list: + An integer telling you how many times the mock object has been called: >>> mock = Mock(return_value=None) >>> mock.call_count @@ -542,8 +541,6 @@ the *new_callable* argument to :func:`patch`. >>> mock() >>> mock.call_count 2 - >>> mock.call_count == len(mock.call_args_list) - True .. attribute:: return_value From e9e6dfa99d568641ea3d0ce52163421edfdb572e Mon Sep 17 00:00:00 2001 From: chaope Date: Sat, 13 Dec 2025 08:38:43 -0500 Subject: [PATCH 06/17] revert unnecessary change. --- Lib/unittest/mock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 47df724474fd93..a70d2219e51116 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -606,8 +606,8 @@ def __class__(self): return self._spec_class called = _delegating_property('called') - call_args = _delegating_property('call_args') 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') From 3a4b590768d37b3fe6a75b4e581c37e6512f7208 Mon Sep 17 00:00:00 2001 From: chaope Date: Sat, 13 Dec 2025 08:39:39 -0500 Subject: [PATCH 07/17] revert blank line deletion --- Lib/unittest/mock.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index a70d2219e51116..34fd49bf56fbb6 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -611,6 +611,7 @@ def __class__(self): call_args_list = _delegating_property('call_args_list') mock_calls = _delegating_property('mock_calls') + def __get_side_effect(self): delegated = self._mock_delegate if delegated is None: From 70e91077ac791fe1ef7785e38e0fb9c6699dcc28 Mon Sep 17 00:00:00 2001 From: Chao Peng Date: Sat, 13 Dec 2025 09:20:38 -0500 Subject: [PATCH 08/17] added TestThreadingMockRaceCondition --- .../testmock/testthreadingmock.py | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_unittest/testmock/testthreadingmock.py b/Lib/test/test_unittest/testmock/testthreadingmock.py index 9672641a47eed4..d2fc8c3636ebc3 100644 --- a/Lib/test/test_unittest/testmock/testthreadingmock.py +++ b/Lib/test/test_unittest/testmock/testthreadingmock.py @@ -1,3 +1,4 @@ +import sys import time import unittest import threading @@ -197,12 +198,34 @@ def test_reset_mock_resets_wait(self): m.wait_until_any_call_with() m.assert_called_once() - def test_call_count_thread_safe(self): +class TestThreadingMockRaceCondition(unittest.TestCase): + """Test that exposes race conditions in ThreadingMock.""" + + def setUp(self): + self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=5) + + # Store and restore original switch interval using addCleanup + self.addCleanup(sys.setswitchinterval, sys.getswitchinterval()) + + # Set switch interval to minimum to force frequent context switches + sys.setswitchinterval(sys.float_info.min) + + def tearDown(self): + self._executor.shutdown() + + def test_call_count_race_condition_with_fast_switching(self): + """Force race condition by maximizing thread context switches. + + This test reduces the thread switch interval to its minimum value, + which maximizes the likelihood of context switches during the critical + section of _increment_mock_call. + """ m = ThreadingMock() - # 3k loops reliably reproduces the issue while keeping runtime ~0.6s - LOOPS = 3_000 + # Use fewer loops but with maximum context switching pressure + # Expected runtime is 0.2 second + LOOPS = 100 THREADS = 10 def test_function(): @@ -215,8 +238,11 @@ def test_function(): for thread in threads: thread.join() + # Without proper locking, this assertion will fail due to race condition self.assertEqual(m.call_count, LOOPS * THREADS, - f"Expected {LOOPS * THREADS}, got {m.call_count}") + f"Race condition detected: expected {LOOPS * THREADS}, " + f"got {m.call_count}. call_args_list has " + f"{len(m.call_args_list)} items.") if __name__ == "__main__": From dc0d804c67e3e9095140be66ecdecedeaa1025b4 Mon Sep 17 00:00:00 2001 From: chaope Date: Sat, 13 Dec 2025 09:50:05 -0500 Subject: [PATCH 09/17] Update Misc/NEWS.d/next/Library/2025-12-13-06-17-44.gh-issue-142651.ZRtBu4.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- .../Library/2025-12-13-06-17-44.gh-issue-142651.ZRtBu4.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 index 1cca83474d2820..236900bac5d6f6 100644 --- 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 @@ -1 +1,3 @@ -Fixed a thread safety issue in :mod:`unittest.mock` where :attr:`~unittest.mock.Mock.call_count` could return inaccurate values when a mock was called concurrently from multiple threads. The attribute now derives its value from the length of :attr:`~unittest.mock.Mock.call_args_list` to ensure consistency. +:mod:`unittest.mock`: fix a thread safety issue where :attr:`Mock.call_count +` may return inaccurate values when the mock +is called concurrently from multiple threads. From f05407ebd7594962f34f13c433481de882d39ce3 Mon Sep 17 00:00:00 2001 From: chaope Date: Sat, 13 Dec 2025 09:50:13 -0500 Subject: [PATCH 10/17] Update Lib/test/test_unittest/testmock/testthreadingmock.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/test/test_unittest/testmock/testthreadingmock.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/test/test_unittest/testmock/testthreadingmock.py b/Lib/test/test_unittest/testmock/testthreadingmock.py index 9672641a47eed4..c0663a4614a53c 100644 --- a/Lib/test/test_unittest/testmock/testthreadingmock.py +++ b/Lib/test/test_unittest/testmock/testthreadingmock.py @@ -215,8 +215,7 @@ def test_function(): for thread in threads: thread.join() - self.assertEqual(m.call_count, LOOPS * THREADS, - f"Expected {LOOPS * THREADS}, got {m.call_count}") + self.assertEqual(m.call_count, LOOPS * THREADS) if __name__ == "__main__": From 3a81b0d9618d9cf894b15f32bfe6685623b7d842 Mon Sep 17 00:00:00 2001 From: Chao Peng Date: Sat, 13 Dec 2025 09:52:06 -0500 Subject: [PATCH 11/17] update test --- Lib/test/test_unittest/testmock/testthreadingmock.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Lib/test/test_unittest/testmock/testthreadingmock.py b/Lib/test/test_unittest/testmock/testthreadingmock.py index d2fc8c3636ebc3..9abcc40b1edeb6 100644 --- a/Lib/test/test_unittest/testmock/testthreadingmock.py +++ b/Lib/test/test_unittest/testmock/testthreadingmock.py @@ -238,11 +238,7 @@ def test_function(): for thread in threads: thread.join() - # Without proper locking, this assertion will fail due to race condition - self.assertEqual(m.call_count, LOOPS * THREADS, - f"Race condition detected: expected {LOOPS * THREADS}, " - f"got {m.call_count}. call_args_list has " - f"{len(m.call_args_list)} items.") + self.assertEqual(m.call_count, LOOPS * THREADS) if __name__ == "__main__": From a76318e79772302eb779eb65e815b69d41e8c2ce Mon Sep 17 00:00:00 2001 From: Chao Peng Date: Sat, 13 Dec 2025 10:05:16 -0500 Subject: [PATCH 12/17] move self._executor.shutdown to addCleanup --- Lib/test/test_unittest/testmock/testthreadingmock.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/test/test_unittest/testmock/testthreadingmock.py b/Lib/test/test_unittest/testmock/testthreadingmock.py index 9abcc40b1edeb6..7f5bb5fb42ef31 100644 --- a/Lib/test/test_unittest/testmock/testthreadingmock.py +++ b/Lib/test/test_unittest/testmock/testthreadingmock.py @@ -204,6 +204,7 @@ class TestThreadingMockRaceCondition(unittest.TestCase): def setUp(self): self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=5) + self.addCleanup(self._executor.shutdown) # Store and restore original switch interval using addCleanup self.addCleanup(sys.setswitchinterval, sys.getswitchinterval()) @@ -211,9 +212,6 @@ def setUp(self): # Set switch interval to minimum to force frequent context switches sys.setswitchinterval(sys.float_info.min) - def tearDown(self): - self._executor.shutdown() - def test_call_count_race_condition_with_fast_switching(self): """Force race condition by maximizing thread context switches. From 8e2348c4a2d8b8c98769476a63fa83a599170c6a Mon Sep 17 00:00:00 2001 From: Chao Peng Date: Sat, 13 Dec 2025 12:27:07 -0500 Subject: [PATCH 13/17] use 3k loops --- .../testmock/testthreadingmock.py | 31 ++----------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/Lib/test/test_unittest/testmock/testthreadingmock.py b/Lib/test/test_unittest/testmock/testthreadingmock.py index 7f5bb5fb42ef31..1222b80cb45b39 100644 --- a/Lib/test/test_unittest/testmock/testthreadingmock.py +++ b/Lib/test/test_unittest/testmock/testthreadingmock.py @@ -1,4 +1,3 @@ -import sys import time import unittest import threading @@ -198,38 +197,14 @@ def test_reset_mock_resets_wait(self): m.wait_until_any_call_with() m.assert_called_once() - -class TestThreadingMockRaceCondition(unittest.TestCase): - """Test that exposes race conditions in ThreadingMock.""" - - def setUp(self): - self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=5) - self.addCleanup(self._executor.shutdown) - - # Store and restore original switch interval using addCleanup - self.addCleanup(sys.setswitchinterval, sys.getswitchinterval()) - - # Set switch interval to minimum to force frequent context switches - sys.setswitchinterval(sys.float_info.min) - - def test_call_count_race_condition_with_fast_switching(self): - """Force race condition by maximizing thread context switches. - - This test reduces the thread switch interval to its minimum value, - which maximizes the likelihood of context switches during the critical - section of _increment_mock_call. - """ + def test_call_count_thread_safe(self): m = ThreadingMock() - - # Use fewer loops but with maximum context switching pressure - # Expected runtime is 0.2 second - LOOPS = 100 + # 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() From e7dcc5752b36b78eac687acdb0f07d2e15fe599f Mon Sep 17 00:00:00 2001 From: Chao Peng Date: Sat, 13 Dec 2025 12:50:40 -0500 Subject: [PATCH 14/17] add @requires_resource('cpu') --- Lib/test/test_unittest/testmock/testthreadingmock.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_unittest/testmock/testthreadingmock.py b/Lib/test/test_unittest/testmock/testthreadingmock.py index 1222b80cb45b39..0a243ba83700ca 100644 --- a/Lib/test/test_unittest/testmock/testthreadingmock.py +++ b/Lib/test/test_unittest/testmock/testthreadingmock.py @@ -3,7 +3,7 @@ import threading import concurrent.futures -from test.support import threading_helper +from test.support import requires_resource, threading_helper from unittest.mock import patch, ThreadingMock @@ -197,6 +197,7 @@ def test_reset_mock_resets_wait(self): m.wait_until_any_call_with() m.assert_called_once() + @requires_resource('cpu') def test_call_count_thread_safe(self): m = ThreadingMock() # 3k loops reliably reproduces the issue while keeping runtime ~0.6s From 851f698a6b6bd80ecaf1947e6293047807e13c1d Mon Sep 17 00:00:00 2001 From: Chao Peng Date: Sat, 13 Dec 2025 15:05:10 -0500 Subject: [PATCH 15/17] update test according to comments --- .../testmock/testthreadingmock.py | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/Lib/test/test_unittest/testmock/testthreadingmock.py b/Lib/test/test_unittest/testmock/testthreadingmock.py index 0a243ba83700ca..6bfdd45b8acce9 100644 --- a/Lib/test/test_unittest/testmock/testthreadingmock.py +++ b/Lib/test/test_unittest/testmock/testthreadingmock.py @@ -1,9 +1,10 @@ +import sys import time import unittest import threading import concurrent.futures -from test.support import requires_resource, threading_helper +from test.support import setswitchinterval, threading_helper from unittest.mock import patch, ThreadingMock @@ -197,20 +198,24 @@ def test_reset_mock_resets_wait(self): m.wait_until_any_call_with() m.assert_called_once() - @requires_resource('cpu') 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() + oldswitchinterval = sys.getswitchinterval() + setswitchinterval(1e-6) + try: + m = ThreadingMock() + # 100 loops with 10 threads reliably reproduces the issue while keeping runtime ~0.2s + LOOPS = 100 + 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() + finally: + sys.setswitchinterval(oldswitchinterval) self.assertEqual(m.call_count, LOOPS * THREADS) From 3b298676c60f57be008fcab45a3ae9e3ede3a189 Mon Sep 17 00:00:00 2001 From: Chao Peng Date: Sat, 13 Dec 2025 15:41:42 -0500 Subject: [PATCH 16/17] update according to comments --- .../test_unittest/testmock/testthreadingmock.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_unittest/testmock/testthreadingmock.py b/Lib/test/test_unittest/testmock/testthreadingmock.py index 6bfdd45b8acce9..e6b04ab1178432 100644 --- a/Lib/test/test_unittest/testmock/testthreadingmock.py +++ b/Lib/test/test_unittest/testmock/testthreadingmock.py @@ -199,16 +199,17 @@ def test_reset_mock_resets_wait(self): 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: - m = ThreadingMock() - # 100 loops with 10 threads reliably reproduces the issue while keeping runtime ~0.2s - LOOPS = 100 - 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() From 16232b89c15a4a1869e174958851bb209aaa55b4 Mon Sep 17 00:00:00 2001 From: Chao Peng Date: Sun, 14 Dec 2025 07:23:32 -0500 Subject: [PATCH 17/17] use threading_helper.start_threads --- Lib/test/test_unittest/testmock/testthreadingmock.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_unittest/testmock/testthreadingmock.py b/Lib/test/test_unittest/testmock/testthreadingmock.py index e6b04ab1178432..3603995b090a6c 100644 --- a/Lib/test/test_unittest/testmock/testthreadingmock.py +++ b/Lib/test/test_unittest/testmock/testthreadingmock.py @@ -211,10 +211,8 @@ def test_function(): setswitchinterval(1e-6) try: threads = [threading.Thread(target=test_function) for _ in range(THREADS)] - for thread in threads: - thread.start() - for thread in threads: - thread.join() + with threading_helper.start_threads(threads): + pass finally: sys.setswitchinterval(oldswitchinterval)