11import unittest
2- from itertools import accumulate , batched , chain , combinations_with_replacement , cycle , permutations , zip_longest
2+ from itertools import (
3+ accumulate ,
4+ batched ,
5+ chain ,
6+ combinations_with_replacement ,
7+ cycle ,
8+ permutations ,
9+ tee ,
10+ zip_longest ,
11+ )
312from test .support import threading_helper
413
514
@@ -15,20 +24,23 @@ def work_iterator(it):
1524
1625
1726class ItertoolsThreading (unittest .TestCase ):
18-
1927 @threading_helper .reap_threads
2028 def test_accumulate (self ):
2129 number_of_iterations = 10
2230 for _ in range (number_of_iterations ):
2331 it = accumulate (tuple (range (40 )))
24- threading_helper .run_concurrently (work_iterator , nthreads = 10 , args = [it ])
32+ threading_helper .run_concurrently (
33+ work_iterator , nthreads = 10 , args = [it ]
34+ )
2535
2636 @threading_helper .reap_threads
2737 def test_batched (self ):
2838 number_of_iterations = 10
2939 for _ in range (number_of_iterations ):
3040 it = batched (tuple (range (1000 )), 2 )
31- threading_helper .run_concurrently (work_iterator , nthreads = 10 , args = [it ])
41+ threading_helper .run_concurrently (
42+ work_iterator , nthreads = 10 , args = [it ]
43+ )
3244
3345 @threading_helper .reap_threads
3446 def test_cycle (self ):
@@ -46,28 +58,88 @@ def test_chain(self):
4658 number_of_iterations = 10
4759 for _ in range (number_of_iterations ):
4860 it = chain (* [(1 ,)] * 200 )
49- threading_helper .run_concurrently (work_iterator , nthreads = 6 , args = [it ])
61+ threading_helper .run_concurrently (
62+ work_iterator , nthreads = 6 , args = [it ]
63+ )
5064
5165 @threading_helper .reap_threads
5266 def test_combinations_with_replacement (self ):
5367 number_of_iterations = 6
5468 for _ in range (number_of_iterations ):
5569 it = combinations_with_replacement (tuple (range (2 )), 2 )
56- threading_helper .run_concurrently (work_iterator , nthreads = 6 , args = [it ])
70+ threading_helper .run_concurrently (
71+ work_iterator , nthreads = 6 , args = [it ]
72+ )
5773
5874 @threading_helper .reap_threads
5975 def test_permutations (self ):
6076 number_of_iterations = 6
6177 for _ in range (number_of_iterations ):
6278 it = permutations (tuple (range (4 )), 2 )
63- threading_helper .run_concurrently (work_iterator , nthreads = 6 , args = [it ])
79+ threading_helper .run_concurrently (
80+ work_iterator , nthreads = 6 , args = [it ]
81+ )
6482
6583 @threading_helper .reap_threads
6684 def test_zip_longest (self ):
6785 number_of_iterations = 10
6886 for _ in range (number_of_iterations ):
6987 it = zip_longest (list (range (4 )), list (range (8 )), fillvalue = 0 )
70- threading_helper .run_concurrently (work_iterator , nthreads = 10 , args = [it ])
88+ threading_helper .run_concurrently (
89+ work_iterator , nthreads = 10 , args = [it ]
90+ )
91+
92+
93+ class TestTeeConcurrent (unittest .TestCase ):
94+ # itertools.tee branches share a linked list of internal data cells.
95+ # Concurrent iteration must not corrupt that shared state or crash the
96+ # free-threaded build. A crash shows up as the interpreter dying (not as a
97+ # caught exception); tee is documented as not thread-safe, so a
98+ # ``RuntimeError`` from the re-entrancy guard is an allowed outcome and is
99+ # tolerated here.
100+
101+ def test_same_branch (self ):
102+ # Many threads consume the same tee branch.
103+ errors = []
104+
105+ def consume (it ):
106+ try :
107+ for _ in it :
108+ pass
109+ except RuntimeError :
110+ pass
111+ except Exception as e :
112+ errors .append (e )
113+
114+ for _ in range (100 ):
115+ a , _ = tee (iter (range (2000 )), 2 )
116+ threading_helper .run_concurrently (consume , nthreads = 8 , args = (a ,))
117+
118+ self .assertEqual (errors , [], msg = f"unexpected errors: { errors } " )
119+
120+ def test_sibling_branches (self ):
121+ # Each thread consumes a different sibling branch of the same tee.
122+ errors = []
123+
124+ def make_worker (it ):
125+ def consume ():
126+ try :
127+ for _ in it :
128+ pass
129+ except RuntimeError :
130+ pass
131+ except Exception as e :
132+ errors .append (e )
133+
134+ return consume
135+
136+ for _ in range (100 ):
137+ branches = tee (iter (range (4000 )), 8 )
138+ threading_helper .run_concurrently (
139+ [make_worker (it ) for it in branches ]
140+ )
141+
142+ self .assertEqual (errors , [], msg = f"unexpected errors: { errors } " )
71143
72144
73145if __name__ == "__main__" :
0 commit comments