(mocker: MockerFixture, lock: SoftFileLock)
| 643 | |
| 644 | @contextmanager |
| 645 | def _close_after_commit(mocker: MockerFixture, lock: SoftFileLock) -> Iterator[tuple[OSError, list[int]]]: |
| 646 | real_close = os.close |
| 647 | close_error = OSError(EINTR, "close failed") |
| 648 | attempts: list[int] = [] |
| 649 | # close is an attribute of the shared os module, so this patch binds for every close in the process while the |
| 650 | # window is open, including one a GC-run finalizer (a multiprocessing pipe from an earlier test, say) makes on this |
| 651 | # very thread. Fail only the descriptor this lock lets go of: it clears its bookkeeping right before the one close |
| 652 | # attempt, so record the descriptor as it does. |
| 653 | released: list[int | None] = [] |
| 654 | real_mark_released = lock._mark_descriptor_released |
| 655 | |
| 656 | def mark_released() -> None: |
| 657 | released.append(lock._context.lock_file_fd) |
| 658 | real_mark_released() |
| 659 | |
| 660 | def close(fd: int) -> None: |
| 661 | real_close(fd) |
| 662 | if fd not in released: |
| 663 | return |
| 664 | attempts.append(fd) |
| 665 | raise close_error |
| 666 | |
| 667 | mocker.patch.object(lock, "_mark_descriptor_released", side_effect=mark_released) |
| 668 | close_mock = mocker.patch("filelock._soft.os.close", side_effect=close) |
| 669 | try: |
| 670 | yield close_error, attempts |
| 671 | finally: |
| 672 | mocker.stop(close_mock) |
| 673 | |
| 674 | |
| 675 | def test_soft_windows_unlink_gives_up_after_every_attempt_is_denied(tmp_path: Path, mocker: MockerFixture) -> None: |
no outgoing calls
no test coverage detected