@@ -109,31 +109,66 @@ def test_deletes_dir_with_readonly_files(self, tmp_path):
109109 sys .platform == "cygwin" ,
110110 reason = "Cygwin can't set the permissions that make the test meaningful." ,
111111 )
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 ):
114138 # Access the module through sys.modules so it is unambiguous which module's
115139 # attribute we patch: the original git.util, not git.index.util even though
116140 # 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 )
118142
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 .
120144 mocker .patch .object (os , "chmod" )
121145 mocker .patch .object (pathlib .Path , "chmod" )
122146
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+
124155 with pytest .raises (SkipTest ):
125156 rmtree (permission_error_tmpdir )
126157
127158 @pytest .mark .skipif (
128159 sys .platform == "cygwin" ,
129160 reason = "Cygwin can't set the permissions that make the test meaningful." ,
130161 )
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 )
137172
138173 with pytest .raises (PermissionError ):
139174 try :
@@ -144,11 +179,7 @@ def test_does_not_wrap_perm_error_unless_enabled(self, mocker, permission_error_
144179 @pytest .mark .parametrize ("hide_windows_known_errors" , [False , True ])
145180 def test_does_not_wrap_other_errors (self , tmp_path , mocker , hide_windows_known_errors ):
146181 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 )
152183
153184 with pytest .raises (FileNotFoundError ):
154185 try :
0 commit comments