|
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 |
@@ -5350,6 +5352,84 @@ def test_resource_warning(self): |
5350 | 5352 | with self.check_no_resource_warning(): |
5351 | 5353 | del iterator |
5352 | 5354 |
|
| 5355 | + def test_no_resource_warning_when_open_fails(self): |
| 5356 | + # gh-152754: a scandir() call that never opened a directory owns |
| 5357 | + # nothing, and must not report an unclosed iterator. |
| 5358 | + self.create_file("file.txt") |
| 5359 | + missing = os.path.join(self.path, "missing") |
| 5360 | + not_a_dir = os.path.join(self.path, "file.txt") |
| 5361 | + for path in (missing, not_a_dir): |
| 5362 | + with self.subTest(path=path): |
| 5363 | + with self.check_no_resource_warning(): |
| 5364 | + with self.assertRaises(OSError): |
| 5365 | + os.scandir(path) |
| 5366 | + |
| 5367 | + |
| 5368 | +@threading_helper.requires_working_threading() |
| 5369 | +class ScandirThreadingTest(unittest.TestCase): |
| 5370 | + # gh-152754: an os.scandir() iterator shared between threads must not crash. |
| 5371 | + |
| 5372 | + if support.check_sanitizer(thread=True): |
| 5373 | + SCANDIR_NUMITEMS = 200 |
| 5374 | + SCANDIR_N_NEXT = 2 |
| 5375 | + SCANDIR_N_CLOSE = 2 |
| 5376 | + SCANDIR_REPEAT = 10 |
| 5377 | + else: |
| 5378 | + SCANDIR_NUMITEMS = 1000 |
| 5379 | + SCANDIR_N_NEXT = 6 |
| 5380 | + SCANDIR_N_CLOSE = 3 |
| 5381 | + SCANDIR_REPEAT = 20 |
| 5382 | + |
| 5383 | + def setUp(self): |
| 5384 | + self.dir = os.path.realpath(os_helper.TESTFN) |
| 5385 | + self.addCleanup(os_helper.rmtree, self.dir) |
| 5386 | + os.mkdir(self.dir) |
| 5387 | + self.names = set() |
| 5388 | + for i in range(self.SCANDIR_NUMITEMS): |
| 5389 | + name = f"f{i}" |
| 5390 | + create_file(os.path.join(self.dir, name)) |
| 5391 | + self.names.add(name) |
| 5392 | + |
| 5393 | + def test_close_racing_next(self): |
| 5394 | + # One thread's next() racing another's close() must not crash. |
| 5395 | + def nexter(): |
| 5396 | + for _ in self.it: |
| 5397 | + pass |
| 5398 | + |
| 5399 | + def closer(): |
| 5400 | + self.it.close() |
| 5401 | + |
| 5402 | + funcs = [nexter] * self.SCANDIR_N_NEXT + [closer] * self.SCANDIR_N_CLOSE |
| 5403 | + for _ in range(self.SCANDIR_REPEAT): |
| 5404 | + self.it = os.scandir(self.dir) |
| 5405 | + try: |
| 5406 | + threading_helper.run_concurrently(funcs) |
| 5407 | + finally: |
| 5408 | + self.it.close() |
| 5409 | + |
| 5410 | + def test_shared_next(self): |
| 5411 | + # Threads sharing one iterator must not crash or lose entries: every |
| 5412 | + # entry must be handed to exactly one thread. |
| 5413 | + expected = sorted(self.names) |
| 5414 | + nthreads = self.SCANDIR_N_NEXT + self.SCANDIR_N_CLOSE |
| 5415 | + |
| 5416 | + for _ in range(self.SCANDIR_REPEAT): |
| 5417 | + self.it = os.scandir(self.dir) |
| 5418 | + results = [] |
| 5419 | + results_lock = threading.Lock() |
| 5420 | + |
| 5421 | + def worker(): |
| 5422 | + local = [entry.name for entry in self.it] |
| 5423 | + with results_lock: |
| 5424 | + results.extend(local) |
| 5425 | + |
| 5426 | + try: |
| 5427 | + threading_helper.run_concurrently([worker] * nthreads) |
| 5428 | + finally: |
| 5429 | + self.it.close() |
| 5430 | + |
| 5431 | + self.assertEqual(sorted(results), expected) |
| 5432 | + |
5353 | 5433 |
|
5354 | 5434 | class TestPEP519(unittest.TestCase): |
5355 | 5435 |
|
|
0 commit comments