|
4 | 4 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ |
5 | 5 |
|
6 | 6 | import ast |
7 | | -from datetime import datetime |
| 7 | +import errno |
8 | 8 | import os |
9 | 9 | import pathlib |
10 | 10 | import pickle |
|
14 | 14 | import tempfile |
15 | 15 | import threading |
16 | 16 | import time |
| 17 | +from datetime import datetime |
17 | 18 | from unittest import SkipTest, mock |
18 | 19 |
|
19 | 20 | import ddt |
|
37 | 38 | LockFile, |
38 | 39 | cygpath, |
39 | 40 | decygpath, |
40 | | - is_cygwin_git, |
41 | 41 | get_user_id, |
| 42 | + is_cygwin_git, |
42 | 43 | remove_password_if_present, |
43 | 44 | rmtree, |
44 | 45 | ) |
45 | | - |
46 | 46 | from test.lib import TestBase, requires_symlinks, with_rw_repo |
47 | 47 |
|
48 | 48 |
|
@@ -882,3 +882,102 @@ def test_iterable_list_name_lookup_ignores_list_attributes(name, prefix): |
882 | 882 | assert name not in values |
883 | 883 | with pytest.raises(IndexError, match="No item found with id"): |
884 | 884 | values[name] |
| 885 | + |
| 886 | + |
| 887 | +@pytest.mark.parametrize("path_type", [str, pathlib.Path]) |
| 888 | +@pytest.mark.parametrize("relative", [True, False]) |
| 889 | +def test_blocking_lock_waits_for_existing_holder(tmp_path, monkeypatch, path_type, relative): |
| 890 | + monkeypatch.chdir(tmp_path) |
| 891 | + path = path_type("resource" if relative else tmp_path / "resource") |
| 892 | + owner = LockFile(path) |
| 893 | + waiter = BlockingLockFile(path, check_interval_s=0.01, max_block_time_s=1) |
| 894 | + owner._obtain_lock() |
| 895 | + try: |
| 896 | + with mock.patch.object(time, "sleep", side_effect=lambda _: owner._release_lock()) as sleep: |
| 897 | + waiter._obtain_lock() |
| 898 | + sleep.assert_called_once_with(0.01) |
| 899 | + assert waiter._has_lock() |
| 900 | + finally: |
| 901 | + owner._release_lock() |
| 902 | + waiter._release_lock() |
| 903 | + |
| 904 | + |
| 905 | +@pytest.mark.parametrize("path_type", [str, pathlib.Path]) |
| 906 | +def test_blocking_relative_lock_reports_timeout(tmp_path, monkeypatch, path_type): |
| 907 | + monkeypatch.chdir(tmp_path) |
| 908 | + owner = LockFile(path_type("resource")) |
| 909 | + waiter = BlockingLockFile(path_type("resource"), max_block_time_s=0) |
| 910 | + owner._obtain_lock() |
| 911 | + try: |
| 912 | + with pytest.raises(OSError, match="Waited .* seconds for lock"): |
| 913 | + waiter._obtain_lock() |
| 914 | + finally: |
| 915 | + owner._release_lock() |
| 916 | + |
| 917 | + |
| 918 | +def test_blocking_lock_still_rejects_missing_directory(tmp_path): |
| 919 | + waiter = BlockingLockFile(tmp_path / "missing" / "resource") |
| 920 | + with mock.patch.object(time, "sleep", side_effect=AssertionError("Must not retry")) as sleep: |
| 921 | + with pytest.raises(OSError) as exc_info: |
| 922 | + waiter._obtain_lock() |
| 923 | + sleep.assert_not_called() |
| 924 | + assert isinstance(exc_info.value.__cause__, FileNotFoundError) |
| 925 | + assert not waiter._has_lock() |
| 926 | + |
| 927 | + |
| 928 | +@pytest.mark.skipif(sys.platform == "win32", reason="Lock creation uses CreateFileW on Windows") |
| 929 | +@pytest.mark.parametrize( |
| 930 | + "error_number", [errno.EACCES, errno.ENOENT, errno.ENOTDIR, errno.EROFS, errno.ENOSPC, errno.EIO] |
| 931 | +) |
| 932 | +@pytest.mark.parametrize("path_type", [str, pathlib.Path]) |
| 933 | +@pytest.mark.parametrize("relative", [True, False]) |
| 934 | +def test_blocking_lock_propagates_non_contention_errors(tmp_path, monkeypatch, error_number, path_type, relative): |
| 935 | + monkeypatch.chdir(tmp_path) |
| 936 | + path = path_type("resource" if relative else tmp_path / "resource") |
| 937 | + waiter = BlockingLockFile(path) |
| 938 | + error = OSError(error_number, os.strerror(error_number), waiter._lock_file_path()) |
| 939 | + with mock.patch.object(os, "open", side_effect=error) as open_lock: |
| 940 | + with mock.patch.object(time, "sleep", side_effect=AssertionError("Must not retry")) as sleep: |
| 941 | + with pytest.raises(OSError) as exc_info: |
| 942 | + waiter._obtain_lock() |
| 943 | + sleep.assert_not_called() |
| 944 | + open_lock.assert_called_once() |
| 945 | + assert exc_info.value.__cause__ is error |
| 946 | + assert str(exc_info.value) == str(error) |
| 947 | + assert not waiter._has_lock() |
| 948 | + assert not os.path.lexists(waiter._lock_file_path()) |
| 949 | + |
| 950 | + |
| 951 | +@pytest.mark.skipif(os.name != "posix", reason="Directory write permissions require POSIX") |
| 952 | +@pytest.mark.parametrize("path_type", [str, pathlib.Path]) |
| 953 | +def test_blocking_relative_lock_rejects_unwritable_cwd(tmp_path, monkeypatch, path_type): |
| 954 | + monkeypatch.chdir(tmp_path) |
| 955 | + original_mode = tmp_path.stat().st_mode |
| 956 | + tmp_path.chmod(stat.S_IRUSR | stat.S_IXUSR) |
| 957 | + try: |
| 958 | + if os.access(".", os.W_OK): |
| 959 | + pytest.skip("Current user can write to read-only directories") |
| 960 | + assert os.path.isdir(".") |
| 961 | + waiter = BlockingLockFile(path_type("resource")) |
| 962 | + with mock.patch.object(time, "sleep", side_effect=AssertionError("Must not retry")) as sleep: |
| 963 | + with pytest.raises(OSError) as exc_info: |
| 964 | + waiter._obtain_lock() |
| 965 | + sleep.assert_not_called() |
| 966 | + assert isinstance(exc_info.value.__cause__, PermissionError) |
| 967 | + assert not waiter._has_lock() |
| 968 | + finally: |
| 969 | + tmp_path.chmod(original_mode) |
| 970 | + |
| 971 | + |
| 972 | +@pytest.mark.skipif(os.name != "posix", reason="Removing the current directory requires POSIX") |
| 973 | +@pytest.mark.parametrize("path_type", [str, pathlib.Path]) |
| 974 | +def test_blocking_relative_lock_rejects_removed_cwd(tmp_path, monkeypatch, path_type): |
| 975 | + monkeypatch.chdir(tmp_path) |
| 976 | + tmp_path.rmdir() |
| 977 | + waiter = BlockingLockFile(path_type("resource")) |
| 978 | + with mock.patch.object(time, "sleep", side_effect=AssertionError("Must not retry")) as sleep: |
| 979 | + with pytest.raises(OSError) as exc_info: |
| 980 | + waiter._obtain_lock() |
| 981 | + sleep.assert_not_called() |
| 982 | + assert isinstance(exc_info.value.__cause__, FileNotFoundError) |
| 983 | + assert not waiter._has_lock() |
0 commit comments