|
25 | 25 | import sysconfig |
26 | 26 | import tempfile |
27 | 27 | import textwrap |
| 28 | +import threading |
28 | 29 | import time |
29 | 30 | import types |
30 | 31 | import unittest |
|
35 | 36 | from test.support import os_helper |
36 | 37 | from test.support import socket_helper |
37 | 38 | from test.support import infinite_recursion |
| 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 |
|
@@ -5613,6 +5615,84 @@ def test_resource_warning(self): |
5613 | 5615 | with self.check_no_resource_warning(): |
5614 | 5616 | del iterator |
5615 | 5617 |
|
| 5618 | + def test_no_resource_warning_when_open_fails(self): |
| 5619 | + # gh-152754: a scandir() call that never opened a directory owns |
| 5620 | + # nothing, and must not report an unclosed iterator. |
| 5621 | + self.create_file("file.txt") |
| 5622 | + missing = os.path.join(self.path, "missing") |
| 5623 | + not_a_dir = os.path.join(self.path, "file.txt") |
| 5624 | + for path in (missing, not_a_dir): |
| 5625 | + with self.subTest(path=path): |
| 5626 | + with self.check_no_resource_warning(): |
| 5627 | + with self.assertRaises(OSError): |
| 5628 | + os.scandir(path) |
| 5629 | + |
| 5630 | + |
| 5631 | +@threading_helper.requires_working_threading() |
| 5632 | +class ScandirThreadingTest(unittest.TestCase): |
| 5633 | + # gh-152754: an os.scandir() iterator shared between threads must not crash. |
| 5634 | + |
| 5635 | + if support.check_sanitizer(thread=True): |
| 5636 | + SCANDIR_NUMITEMS = 200 |
| 5637 | + SCANDIR_N_NEXT = 2 |
| 5638 | + SCANDIR_N_CLOSE = 2 |
| 5639 | + SCANDIR_REPEAT = 10 |
| 5640 | + else: |
| 5641 | + SCANDIR_NUMITEMS = 1000 |
| 5642 | + SCANDIR_N_NEXT = 6 |
| 5643 | + SCANDIR_N_CLOSE = 3 |
| 5644 | + SCANDIR_REPEAT = 20 |
| 5645 | + |
| 5646 | + def setUp(self): |
| 5647 | + self.dir = os.path.realpath(os_helper.TESTFN) |
| 5648 | + self.addCleanup(os_helper.rmtree, self.dir) |
| 5649 | + os.mkdir(self.dir) |
| 5650 | + self.names = set() |
| 5651 | + for i in range(self.SCANDIR_NUMITEMS): |
| 5652 | + name = f"f{i}" |
| 5653 | + create_file(os.path.join(self.dir, name)) |
| 5654 | + self.names.add(name) |
| 5655 | + |
| 5656 | + def test_close_racing_next(self): |
| 5657 | + # One thread's next() racing another's close() must not crash. |
| 5658 | + def nexter(): |
| 5659 | + for _ in self.it: |
| 5660 | + pass |
| 5661 | + |
| 5662 | + def closer(): |
| 5663 | + self.it.close() |
| 5664 | + |
| 5665 | + funcs = [nexter] * self.SCANDIR_N_NEXT + [closer] * self.SCANDIR_N_CLOSE |
| 5666 | + for _ in range(self.SCANDIR_REPEAT): |
| 5667 | + self.it = os.scandir(self.dir) |
| 5668 | + try: |
| 5669 | + threading_helper.run_concurrently(funcs) |
| 5670 | + finally: |
| 5671 | + self.it.close() |
| 5672 | + |
| 5673 | + def test_shared_next(self): |
| 5674 | + # Threads sharing one iterator must not crash or lose entries: every |
| 5675 | + # entry must be handed to exactly one thread. |
| 5676 | + expected = sorted(self.names) |
| 5677 | + nthreads = self.SCANDIR_N_NEXT + self.SCANDIR_N_CLOSE |
| 5678 | + |
| 5679 | + for _ in range(self.SCANDIR_REPEAT): |
| 5680 | + self.it = os.scandir(self.dir) |
| 5681 | + results = [] |
| 5682 | + results_lock = threading.Lock() |
| 5683 | + |
| 5684 | + def worker(): |
| 5685 | + local = [entry.name for entry in self.it] |
| 5686 | + with results_lock: |
| 5687 | + results.extend(local) |
| 5688 | + |
| 5689 | + try: |
| 5690 | + threading_helper.run_concurrently([worker] * nthreads) |
| 5691 | + finally: |
| 5692 | + self.it.close() |
| 5693 | + |
| 5694 | + self.assertEqual(sorted(results), expected) |
| 5695 | + |
5616 | 5696 |
|
5617 | 5697 | class TestPEP519(unittest.TestCase): |
5618 | 5698 |
|
|
0 commit comments