@@ -109,31 +109,66 @@ def test_deletes_dir_with_readonly_files(self, tmp_path):
109
109
sys .platform == "cygwin" ,
110
110
reason = "Cygwin can't set the permissions that make the test meaningful." ,
111
111
)
112
- def test_wraps_perm_error_if_enabled (self , mocker , permission_error_tmpdir ):
113
- """rmtree wraps PermissionError when HIDE_WINDOWS_KNOWN_ERRORS is true."""
112
+ def test_avoids_changing_permissions_outside_tree (self , tmp_path ):
113
+ # Automatically works on Windows, but on Unix requires either special handling
114
+ # or refraining from attempting to fix PermissionError by making chmod calls.
115
+
116
+ dir1 = tmp_path / "dir1"
117
+ dir1 .mkdir ()
118
+ (dir1 / "file" ).write_bytes (b"" )
119
+ (dir1 / "file" ).chmod (stat .S_IRUSR )
120
+ old_mode = (dir1 / "file" ).stat ().st_mode
121
+
122
+ dir2 = tmp_path / "dir2"
123
+ dir2 .mkdir ()
124
+ (dir2 / "symlink" ).symlink_to (dir1 / "file" )
125
+ dir2 .chmod (stat .S_IRUSR | stat .S_IXUSR )
126
+
127
+ try :
128
+ rmtree (dir2 )
129
+ except PermissionError :
130
+ pass # On Unix, dir2 is not writable, so dir2/symlink may not be deleted.
131
+ except SkipTest as ex :
132
+ self .fail (f"rmtree unexpectedly attempts skip: { ex !r} " )
133
+
134
+ new_mode = (dir1 / "file" ).stat ().st_mode
135
+ assert old_mode == new_mode , f"Should stay { old_mode :#o} , became { new_mode :#o} ."
136
+
137
+ def _patch_for_wrapping_test (self , mocker , hide_windows_known_errors ):
114
138
# Access the module through sys.modules so it is unambiguous which module's
115
139
# attribute we patch: the original git.util, not git.index.util even though
116
140
# git.index.util "replaces" git.util and is what "import git.util" gives us.
117
- mocker .patch .object (sys .modules ["git.util" ], "HIDE_WINDOWS_KNOWN_ERRORS" , True )
141
+ mocker .patch .object (sys .modules ["git.util" ], "HIDE_WINDOWS_KNOWN_ERRORS" , hide_windows_known_errors )
118
142
119
- # Disable common chmod functions so the callback can never fix the problem .
143
+ # Disable common chmod functions so the callback can't fix a PermissionError .
120
144
mocker .patch .object (os , "chmod" )
121
145
mocker .patch .object (pathlib .Path , "chmod" )
122
146
123
- # Now we can see how an intractable PermissionError is treated.
147
+ @pytest .mark .skipif (
148
+ os .name != "nt" ,
149
+ reason = "PermissionError is only ever wrapped on Windows" ,
150
+ )
151
+ def test_wraps_perm_error_if_enabled (self , mocker , permission_error_tmpdir ):
152
+ """rmtree wraps PermissionError on Windows when HIDE_WINDOWS_KNOWN_ERRORS is true."""
153
+ self ._patch_for_wrapping_test (mocker , True )
154
+
124
155
with pytest .raises (SkipTest ):
125
156
rmtree (permission_error_tmpdir )
126
157
127
158
@pytest .mark .skipif (
128
159
sys .platform == "cygwin" ,
129
160
reason = "Cygwin can't set the permissions that make the test meaningful." ,
130
161
)
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" )
162
+ @pytest .mark .parametrize (
163
+ "hide_windows_known_errors" ,
164
+ [
165
+ pytest .param (False ),
166
+ pytest .param (True , marks = pytest .mark .skipif (os .name == "nt" , reason = "We would wrap on Windows" )),
167
+ ],
168
+ )
169
+ def test_does_not_wrap_perm_error_unless_enabled (self , mocker , permission_error_tmpdir , hide_windows_known_errors ):
170
+ """rmtree does not wrap PermissionError on non-Windows systems or when HIDE_WINDOWS_KNOWN_ERRORS is false."""
171
+ self ._patch_for_wrapping_test (mocker , hide_windows_known_errors )
137
172
138
173
with pytest .raises (PermissionError ):
139
174
try :
@@ -144,11 +179,7 @@ def test_does_not_wrap_perm_error_unless_enabled(self, mocker, permission_error_
144
179
@pytest .mark .parametrize ("hide_windows_known_errors" , [False , True ])
145
180
def test_does_not_wrap_other_errors (self , tmp_path , mocker , hide_windows_known_errors ):
146
181
file_not_found_tmpdir = tmp_path / "testdir" # It is deliberately never created.
147
-
148
- # See comments in test_wraps_perm_error_if_enabled for details about patching.
149
- mocker .patch .object (sys .modules ["git.util" ], "HIDE_WINDOWS_KNOWN_ERRORS" , hide_windows_known_errors )
150
- mocker .patch .object (os , "chmod" )
151
- mocker .patch .object (pathlib .Path , "chmod" )
182
+ self ._patch_for_wrapping_test (mocker , hide_windows_known_errors )
152
183
153
184
with pytest .raises (FileNotFoundError ):
154
185
try :
0 commit comments