|
24 | 24 | import sysconfig |
25 | 25 | import tempfile |
26 | 26 | import textwrap |
| 27 | +import threading |
27 | 28 | import time |
28 | 29 | import types |
29 | 30 | import unittest |
|
35 | 36 | from test.support import infinite_recursion |
36 | 37 | from test.support import requires_root_user |
37 | 38 | from test.support import requires_non_root_user |
| 39 | +from test.support import threading_helper |
38 | 40 | from test.support import warnings_helper |
39 | 41 | from platform import win32_is_iot |
40 | 42 | from .utils import create_file |
@@ -5292,6 +5294,84 @@ def test_resource_warning(self): |
5292 | 5294 | with self.check_no_resource_warning(): |
5293 | 5295 | del iterator |
5294 | 5296 |
|
| 5297 | + def test_no_resource_warning_when_open_fails(self): |
| 5298 | + # gh-152754: a scandir() call that never opened a directory owns |
| 5299 | + # nothing, and must not report an unclosed iterator. |
| 5300 | + self.create_file("file.txt") |
| 5301 | + missing = os.path.join(self.path, "missing") |
| 5302 | + not_a_dir = os.path.join(self.path, "file.txt") |
| 5303 | + for path in (missing, not_a_dir): |
| 5304 | + with self.subTest(path=path): |
| 5305 | + with self.check_no_resource_warning(): |
| 5306 | + with self.assertRaises(OSError): |
| 5307 | + os.scandir(path) |
| 5308 | + |
| 5309 | + |
| 5310 | +@threading_helper.requires_working_threading() |
| 5311 | +class ScandirThreadingTest(unittest.TestCase): |
| 5312 | + # gh-152754: an os.scandir() iterator shared between threads must not crash. |
| 5313 | + |
| 5314 | + if support.check_sanitizer(thread=True): |
| 5315 | + SCANDIR_NUMITEMS = 200 |
| 5316 | + SCANDIR_N_NEXT = 2 |
| 5317 | + SCANDIR_N_CLOSE = 2 |
| 5318 | + SCANDIR_REPEAT = 10 |
| 5319 | + else: |
| 5320 | + SCANDIR_NUMITEMS = 1000 |
| 5321 | + SCANDIR_N_NEXT = 6 |
| 5322 | + SCANDIR_N_CLOSE = 3 |
| 5323 | + SCANDIR_REPEAT = 20 |
| 5324 | + |
| 5325 | + def setUp(self): |
| 5326 | + self.dir = os.path.realpath(os_helper.TESTFN) |
| 5327 | + self.addCleanup(os_helper.rmtree, self.dir) |
| 5328 | + os.mkdir(self.dir) |
| 5329 | + self.names = set() |
| 5330 | + for i in range(self.SCANDIR_NUMITEMS): |
| 5331 | + name = f"f{i}" |
| 5332 | + create_file(os.path.join(self.dir, name)) |
| 5333 | + self.names.add(name) |
| 5334 | + |
| 5335 | + def test_close_racing_next(self): |
| 5336 | + # One thread's next() racing another's close() must not crash. |
| 5337 | + def nexter(): |
| 5338 | + for _ in self.it: |
| 5339 | + pass |
| 5340 | + |
| 5341 | + def closer(): |
| 5342 | + self.it.close() |
| 5343 | + |
| 5344 | + funcs = [nexter] * self.SCANDIR_N_NEXT + [closer] * self.SCANDIR_N_CLOSE |
| 5345 | + for _ in range(self.SCANDIR_REPEAT): |
| 5346 | + self.it = os.scandir(self.dir) |
| 5347 | + try: |
| 5348 | + threading_helper.run_concurrently(funcs) |
| 5349 | + finally: |
| 5350 | + self.it.close() |
| 5351 | + |
| 5352 | + def test_shared_next(self): |
| 5353 | + # Threads sharing one iterator must not crash or lose entries: every |
| 5354 | + # entry must be handed to exactly one thread. |
| 5355 | + expected = sorted(self.names) |
| 5356 | + nthreads = self.SCANDIR_N_NEXT + self.SCANDIR_N_CLOSE |
| 5357 | + |
| 5358 | + for _ in range(self.SCANDIR_REPEAT): |
| 5359 | + self.it = os.scandir(self.dir) |
| 5360 | + results = [] |
| 5361 | + results_lock = threading.Lock() |
| 5362 | + |
| 5363 | + def worker(): |
| 5364 | + local = [entry.name for entry in self.it] |
| 5365 | + with results_lock: |
| 5366 | + results.extend(local) |
| 5367 | + |
| 5368 | + try: |
| 5369 | + threading_helper.run_concurrently([worker] * nthreads) |
| 5370 | + finally: |
| 5371 | + self.it.close() |
| 5372 | + |
| 5373 | + self.assertEqual(sorted(results), expected) |
| 5374 | + |
5295 | 5375 |
|
5296 | 5376 | class TestPEP519(unittest.TestCase): |
5297 | 5377 |
|
|
0 commit comments