|
5 | 5 | # the BSD License: https://opensource.org/license/bsd-3-clause/ |
6 | 6 |
|
7 | 7 | import ast |
8 | | -import contextlib |
9 | 8 | from datetime import datetime |
10 | 9 | import os |
11 | 10 | import pathlib |
|
15 | 14 | import sys |
16 | 15 | import tempfile |
17 | 16 | import time |
18 | | -from unittest import SkipTest, mock, skipIf, skipUnless |
| 17 | +from unittest import SkipTest, mock, skipUnless |
19 | 18 |
|
20 | 19 | import ddt |
21 | 20 | import pytest |
|
44 | 43 | from test.lib import TestBase, with_rw_repo |
45 | 44 |
|
46 | 45 |
|
47 | | -class _Member: |
48 | | - """A member of an IterableList.""" |
49 | | - |
50 | | - __slots__ = ("name",) |
51 | | - |
52 | | - def __init__(self, name): |
53 | | - self.name = name |
54 | | - |
55 | | - def __repr__(self): |
56 | | - return f"{type(self).__name__}({self.name!r})" |
57 | | - |
58 | | - |
59 | | -@contextlib.contextmanager |
60 | | -def _tmpdir_to_force_permission_error(): |
61 | | - """Context manager to test permission errors in situations where they are not overcome.""" |
| 46 | +@pytest.fixture |
| 47 | +def permission_error_tmpdir(tmp_path): |
| 48 | + """Fixture to test permissions errors situations where they are not overcome.""" |
62 | 49 | if sys.platform == "cygwin": |
63 | 50 | raise SkipTest("Cygwin can't set the permissions that make the test meaningful.") |
64 | 51 | if sys.version_info < (3, 8): |
65 | 52 | raise SkipTest("In 3.7, TemporaryDirectory doesn't clean up after weird permissions.") |
66 | 53 |
|
67 | | - with tempfile.TemporaryDirectory() as parent: |
68 | | - td = pathlib.Path(parent, "testdir") |
69 | | - td.mkdir() |
70 | | - (td / "x").write_bytes(b"") |
71 | | - (td / "x").chmod(stat.S_IRUSR) # Set up PermissionError on Windows. |
72 | | - td.chmod(stat.S_IRUSR | stat.S_IXUSR) # Set up PermissionError on Unix. |
73 | | - yield td |
| 54 | + td = tmp_path / "testdir" |
| 55 | + td.mkdir() |
| 56 | + (td / "x").write_bytes(b"") |
| 57 | + (td / "x").chmod(stat.S_IRUSR) # Set up PermissionError on Windows. |
| 58 | + td.chmod(stat.S_IRUSR | stat.S_IXUSR) # Set up PermissionError on Unix. |
| 59 | + yield td |
74 | 60 |
|
75 | 61 |
|
76 | | -@contextlib.contextmanager |
77 | | -def _tmpdir_for_file_not_found(): |
78 | | - """Context manager to test errors deleting a directory that are not due to permissions.""" |
79 | | - with tempfile.TemporaryDirectory() as parent: |
80 | | - yield pathlib.Path(parent, "testdir") # It is deliberately never created. |
| 62 | +@pytest.fixture |
| 63 | +def file_not_found_tmpdir(tmp_path): |
| 64 | + """Fixture to test errors deleting a directory that are not due to permissions.""" |
| 65 | + yield tmp_path / "testdir" # It is deliberately never created. |
81 | 66 |
|
82 | 67 |
|
83 | | -@ddt.ddt |
84 | | -class TestUtils(TestBase): |
85 | | - def test_rmtree_deletes_nested_dir_with_files(self): |
86 | | - with tempfile.TemporaryDirectory() as parent: |
87 | | - td = pathlib.Path(parent, "testdir") |
88 | | - for d in td, td / "q", td / "s": |
89 | | - d.mkdir() |
90 | | - for f in ( |
91 | | - td / "p", |
92 | | - td / "q" / "w", |
93 | | - td / "q" / "x", |
94 | | - td / "r", |
95 | | - td / "s" / "y", |
96 | | - td / "s" / "z", |
97 | | - ): |
98 | | - f.write_bytes(b"") |
| 68 | +class TestRmtree: |
| 69 | + """Tests for :func:`git.util.rmtree`.""" |
99 | 70 |
|
100 | | - try: |
101 | | - rmtree(td) |
102 | | - except SkipTest as ex: |
103 | | - self.fail(f"rmtree unexpectedly attempts skip: {ex!r}") |
| 71 | + def test_deletes_nested_dir_with_files(self, tmp_path): |
| 72 | + td = tmp_path / "testdir" |
104 | 73 |
|
105 | | - self.assertFalse(td.exists()) |
| 74 | + for d in td, td / "q", td / "s": |
| 75 | + d.mkdir() |
| 76 | + for f in ( |
| 77 | + td / "p", |
| 78 | + td / "q" / "w", |
| 79 | + td / "q" / "x", |
| 80 | + td / "r", |
| 81 | + td / "s" / "y", |
| 82 | + td / "s" / "z", |
| 83 | + ): |
| 84 | + f.write_bytes(b"") |
106 | 85 |
|
107 | | - @skipIf( |
| 86 | + try: |
| 87 | + rmtree(td) |
| 88 | + except SkipTest as ex: |
| 89 | + pytest.fail(f"rmtree unexpectedly attempts skip: {ex!r}") |
| 90 | + |
| 91 | + assert not td.exists() |
| 92 | + |
| 93 | + @pytest.mark.skipif( |
108 | 94 | sys.platform == "cygwin", |
109 | | - "Cygwin can't set the permissions that make the test meaningful.", |
| 95 | + reason="Cygwin can't set the permissions that make the test meaningful.", |
110 | 96 | ) |
111 | | - def test_rmtree_deletes_dir_with_readonly_files(self): |
| 97 | + def test_deletes_dir_with_readonly_files(self, tmp_path): |
112 | 98 | # Automatically works on Unix, but requires special handling on Windows. |
113 | | - # Not to be confused with what _tmpdir_to_force_permission_error sets up (see below). |
114 | | - with tempfile.TemporaryDirectory() as parent: |
115 | | - td = pathlib.Path(parent, "testdir") |
116 | | - for d in td, td / "sub": |
117 | | - d.mkdir() |
118 | | - for f in td / "x", td / "sub" / "y": |
119 | | - f.write_bytes(b"") |
120 | | - f.chmod(0) |
| 99 | + # Not to be confused with what permission_error_tmpdir sets up (see below). |
| 100 | + |
| 101 | + td = tmp_path / "testdir" |
| 102 | + |
| 103 | + for d in td, td / "sub": |
| 104 | + d.mkdir() |
| 105 | + for f in td / "x", td / "sub" / "y": |
| 106 | + f.write_bytes(b"") |
| 107 | + f.chmod(0) |
| 108 | + |
| 109 | + try: |
| 110 | + rmtree(td) |
| 111 | + except SkipTest as ex: |
| 112 | + self.fail(f"rmtree unexpectedly attempts skip: {ex!r}") |
121 | 113 |
|
| 114 | + assert not td.exists() |
| 115 | + |
| 116 | + def test_wraps_perm_error_if_enabled(self, mocker, permission_error_tmpdir): |
| 117 | + """rmtree wraps PermissionError when HIDE_WINDOWS_KNOWN_ERRORS is true.""" |
| 118 | + # Access the module through sys.modules so it is unambiguous which module's |
| 119 | + # attribute we patch: the original git.util, not git.index.util even though |
| 120 | + # git.index.util "replaces" git.util and is what "import git.util" gives us. |
| 121 | + mocker.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", True) |
| 122 | + |
| 123 | + # Disable common chmod functions so the callback can't fix the problem. |
| 124 | + mocker.patch.object(os, "chmod") |
| 125 | + mocker.patch.object(pathlib.Path, "chmod") |
| 126 | + |
| 127 | + # Now we can see how an intractable PermissionError is treated. |
| 128 | + with pytest.raises(SkipTest): |
| 129 | + rmtree(permission_error_tmpdir) |
| 130 | + |
| 131 | + def test_does_not_wrap_perm_error_unless_enabled(self, mocker, permission_error_tmpdir): |
| 132 | + """rmtree does not wrap PermissionError when HIDE_WINDOWS_KNOWN_ERRORS is false.""" |
| 133 | + # See comments in test_wraps_perm_error_if_enabled for details about patching. |
| 134 | + mocker.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", False) |
| 135 | + mocker.patch.object(os, "chmod") |
| 136 | + mocker.patch.object(pathlib.Path, "chmod") |
| 137 | + |
| 138 | + with pytest.raises(PermissionError): |
| 139 | + try: |
| 140 | + rmtree(permission_error_tmpdir) |
| 141 | + except SkipTest as ex: |
| 142 | + pytest.fail(f"rmtree unexpectedly attempts skip: {ex!r}") |
| 143 | + |
| 144 | + @pytest.mark.parametrize("hide_windows_known_errors", [False, True]) |
| 145 | + def test_does_not_wrap_other_errors(self, mocker, file_not_found_tmpdir, hide_windows_known_errors): |
| 146 | + # See comments in test_wraps_perm_error_if_enabled for details about patching. |
| 147 | + mocker.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", hide_windows_known_errors) |
| 148 | + mocker.patch.object(os, "chmod") |
| 149 | + mocker.patch.object(pathlib.Path, "chmod") |
| 150 | + |
| 151 | + with pytest.raises(FileNotFoundError): |
122 | 152 | try: |
123 | | - rmtree(td) |
| 153 | + rmtree(file_not_found_tmpdir) |
124 | 154 | except SkipTest as ex: |
125 | 155 | self.fail(f"rmtree unexpectedly attempts skip: {ex!r}") |
126 | 156 |
|
127 | | - self.assertFalse(td.exists()) |
128 | 157 |
|
129 | | - def test_rmtree_can_wrap_exceptions(self): |
130 | | - """rmtree wraps PermissionError when HIDE_WINDOWS_KNOWN_ERRORS is true.""" |
131 | | - with _tmpdir_to_force_permission_error() as td: |
132 | | - # Access the module through sys.modules so it is unambiguous which module's |
133 | | - # attribute we patch: the original git.util, not git.index.util even though |
134 | | - # git.index.util "replaces" git.util and is what "import git.util" gives us. |
135 | | - with mock.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", True): |
136 | | - # Disable common chmod functions so the callback can't fix the problem. |
137 | | - with mock.patch.object(os, "chmod"), mock.patch.object(pathlib.Path, "chmod"): |
138 | | - # Now we can see how an intractable PermissionError is treated. |
139 | | - with self.assertRaises(SkipTest): |
140 | | - rmtree(td) |
| 158 | +class _Member: |
| 159 | + """A member of an IterableList.""" |
141 | 160 |
|
142 | | - @ddt.data( |
143 | | - (False, PermissionError, _tmpdir_to_force_permission_error), |
144 | | - (False, FileNotFoundError, _tmpdir_for_file_not_found), |
145 | | - (True, FileNotFoundError, _tmpdir_for_file_not_found), |
146 | | - ) |
147 | | - def test_rmtree_does_not_wrap_unless_called_for(self, case): |
148 | | - """rmtree doesn't wrap non-PermissionError, nor if HIDE_WINDOWS_KNOWN_ERRORS is false.""" |
149 | | - hide_windows_known_errors, exception_type, tmpdir_context_factory = case |
150 | | - |
151 | | - with tmpdir_context_factory() as td: |
152 | | - # See comments in test_rmtree_can_wrap_exceptions regarding the patching done here. |
153 | | - with mock.patch.object( |
154 | | - sys.modules["git.util"], |
155 | | - "HIDE_WINDOWS_KNOWN_ERRORS", |
156 | | - hide_windows_known_errors, |
157 | | - ): |
158 | | - with mock.patch.object(os, "chmod"), mock.patch.object(pathlib.Path, "chmod"): |
159 | | - with self.assertRaises(exception_type): |
160 | | - try: |
161 | | - rmtree(td) |
162 | | - except SkipTest as ex: |
163 | | - self.fail(f"rmtree unexpectedly attempts skip: {ex!r}") |
| 161 | + __slots__ = ("name",) |
| 162 | + |
| 163 | + def __init__(self, name): |
| 164 | + self.name = name |
| 165 | + |
| 166 | + def __repr__(self): |
| 167 | + return f"{type(self).__name__}({self.name!r})" |
| 168 | + |
| 169 | + |
| 170 | +@ddt.ddt |
| 171 | +class TestUtils(TestBase): |
| 172 | + """Tests for utilities in :mod:`git.util` other than :func:`git.util.rmtree`.""" |
164 | 173 |
|
165 | 174 | @ddt.data("HIDE_WINDOWS_KNOWN_ERRORS", "HIDE_WINDOWS_FREEZE_ERRORS") |
166 | 175 | def test_env_vars_for_windows_tests(self, name): |
|
0 commit comments