1 # encoding: utf-8 2 """Tests for IPython.utils.path.py""" 3 4 # Copyright (c) IPython Development Team. 5 # Distributed under the terms of the Modified BSD License. 6 7 import os 8 import shutil 9 import sys 10 import tempfile 11 import unittest 12 from contextlib import contextmanager 13 from importlib import reload 14 from os.path import abspath, join 15 from unittest.mock import patch 16 17 import pytest 18 from tempfile import TemporaryDirectory 19 20 import IPython 21 from IPython import paths 22 from IPython.testing import decorators as dec 23 from IPython.testing.decorators import ( 24 onlyif_unicode_paths, 25 skip_if_not_win32, 26 skip_win32, 27 ) 28 from IPython.testing.tools import make_tempfile 29 from IPython.utils import path 30 31 # Platform-dependent imports 32 try: 33 import winreg as wreg 34 except ImportError: 35 #Fake _winreg module on non-windows platforms 36 import types 37 wr_name = "winreg" 38 sys.modules[wr_name] = types.ModuleType(wr_name) 39 try: 40 import winreg as wreg 41 except ImportError: 42 import _winreg as wreg 43 44 #Add entries that needs to be stubbed by the testing code 45 (wreg.OpenKey, wreg.QueryValueEx,) = (None, None) 46 47 #----------------------------------------------------------------------------- 48 # Globals 49 #----------------------------------------------------------------------------- 50 env = os.environ 51 TMP_TEST_DIR = tempfile.mkdtemp() 52 HOME_TEST_DIR = join(TMP_TEST_DIR, "home_test_dir") 53 # 54 # Setup/teardown functions/decorators 55 # 56 57 def setup_module(): 58 """Setup testenvironment for the module: 59 60 - Adds dummy home dir tree 61 """ 62 # Do not mask exceptions here. In particular, catching WindowsError is a 63 # problem because that exception is only defined on Windows... 64 os.makedirs(os.path.join(HOME_TEST_DIR, 'ipython')) 65 66 67 def teardown_module(): 68 """Teardown testenvironment for the module: 69 70 - Remove dummy home dir tree 71 """ 72 # Note: we remove the parent test dir, which is the root of all test 73 # subdirs we may have created. Use shutil instead of os.removedirs, so 74 # that non-empty directories are all recursively removed. 75 shutil.rmtree(TMP_TEST_DIR) 76 77 78 # Build decorator that uses the setup_environment/setup_environment 79 @pytest.fixture 80 def environment(): 81 global oldstuff, platformstuff 82 oldstuff = ( 83 env.copy(), 84 os.name, 85 sys.platform, 86 path.get_home_dir, 87 IPython.__file__, 88 os.getcwd(), 89 ) 90 91 yield 92 93 ( 94 oldenv, 95 os.name, 96 sys.platform, 97 path.get_home_dir, 98 IPython.__file__, 99 old_wd, 100 ) = oldstuff 101 os.chdir(old_wd) 102 reload(path) 103 104 for key in list(env): 105 if key not in oldenv: 106 del env[key] 107 env.update(oldenv) 108 assert not hasattr(sys, "frozen") 109 110 111 with_environment = pytest.mark.usefixtures("environment") 112 113 114 @skip_if_not_win32 115 @with_environment 116 def test_get_home_dir_1(monkeypatch): 117 """Testcase for py2exe logic, un-compressed lib 118 """ 119 unfrozen = path.get_home_dir() 120 monkeypatch.setattr(sys, "frozen", True, raising=False) 121 122 #fake filename for IPython.__init__ 123 IPython.__file__ = abspath(join(HOME_TEST_DIR, "Lib/IPython/__init__.py")) 124 125 home_dir = path.get_home_dir() 126 assert home_dir == unfrozen 127 128 129 @skip_if_not_win32 130 @with_environment 131 def test_get_home_dir_2(monkeypatch): 132 """Testcase for py2exe logic, compressed lib 133 """ 134 unfrozen = path.get_home_dir() 135 monkeypatch.setattr(sys, "frozen", True, raising=False) 136 # fake filename for IPython.__init__ 137 IPython.__file__ = abspath( 138 join(HOME_TEST_DIR, "Library.zip/IPython/__init__.py") 139 ).lower() 140 141 home_dir = path.get_home_dir(True) 142 assert home_dir == unfrozen 143 144 145 @skip_win32 146 @with_environment 147 def test_get_home_dir_3(): 148 """get_home_dir() uses $HOME if set""" 149 env["HOME"] = HOME_TEST_DIR 150 home_dir = path.get_home_dir(True) 151 # get_home_dir expands symlinks 152 assert home_dir == os.path.realpath(env["HOME"]) 153 154 155 @with_environment 156 def test_get_home_dir_4(): 157 """get_home_dir() still works if $HOME is not set""" 158 159 if 'HOME' in env: del env['HOME'] 160 # this should still succeed, but we don't care what the answer is 161 home = path.get_home_dir(False) 162 163 @skip_win32 164 @with_environment 165 def test_get_home_dir_5(monkeypatch): 166 """raise HomeDirError if $HOME is specified, but not a writable dir""" 167 env['HOME'] = abspath(HOME_TEST_DIR+'garbage') 168 # set os.name = posix, to prevent My Documents fallback on Windows 169 monkeypatch.setattr(os, "name", "posix") 170 pytest.raises(path.HomeDirError, path.get_home_dir, True) 171 172 # Should we stub wreg fully so we can run the test on all platforms? 173 @skip_if_not_win32 174 @with_environment 175 def test_get_home_dir_8(monkeypatch): 176 """Using registry hack for 'My Documents', os=='nt' 177 178 HOMESHARE, HOMEDRIVE, HOMEPATH, USERPROFILE and others are missing. 179 """ 180 monkeypatch.setattr(os, "name", "nt") 181 # Remove from stub environment all keys that may be set 182 for key in ['HOME', 'HOMESHARE', 'HOMEDRIVE', 'HOMEPATH', 'USERPROFILE']: 183 env.pop(key, None) 184 185 class key: 186 def __enter__(self): 187 pass 188 def Close(self): 189 pass 190 def __exit__(*args, **kwargs): 191 pass 192 193 with patch.object(wreg, 'OpenKey', return_value=key()), \ 194 patch.object(wreg, 'QueryValueEx', return_value=[abspath(HOME_TEST_DIR)]): 195 home_dir = path.get_home_dir() 196 assert home_dir == abspath(HOME_TEST_DIR) 197 198 @with_environment 199 def test_get_xdg_dir_0(monkeypatch): 200 """test_get_xdg_dir_0, check xdg_dir""" 201 monkeypatch.setattr(path, "_writable_dir", lambda path: True) 202 monkeypatch.setattr(path, "get_home_dir", lambda: "somewhere") 203 monkeypatch.setattr(os, "name", "posix") 204 monkeypatch.setattr(sys, "platform", "linux2") 205 env.pop('IPYTHON_DIR', None) 206 env.pop('IPYTHONDIR', None) 207 env.pop('XDG_CONFIG_HOME', None) 208 209 assert path.get_xdg_dir() == os.path.join("somewhere", ".config") 210 211 212 @with_environment 213 def test_get_xdg_dir_1(monkeypatch): 214 """test_get_xdg_dir_1, check nonexistent xdg_dir""" 215 monkeypatch.setattr(path, "get_home_dir", lambda: HOME_TEST_DIR) 216 monkeypatch.setattr(os, "name", "posix") 217 monkeypatch.setattr(sys, "platform", "linux2") 218 env.pop("IPYTHON_DIR", None) 219 env.pop("IPYTHONDIR", None) 220 env.pop("XDG_CONFIG_HOME", None) 221 assert path.get_xdg_dir() is None 222 223 @with_environment 224 def test_get_xdg_dir_2(monkeypatch): 225 """test_get_xdg_dir_2, check xdg_dir default to ~/.config""" 226 monkeypatch.setattr(path, "get_home_dir", lambda: HOME_TEST_DIR) 227 monkeypatch.setattr(os, "name", "posix") 228 monkeypatch.setattr(sys, "platform", "linux2") 229 env.pop("IPYTHON_DIR", None) 230 env.pop("IPYTHONDIR", None) 231 env.pop("XDG_CONFIG_HOME", None) 232 cfgdir = os.path.join(path.get_home_dir(), ".config") 233 if not os.path.exists(cfgdir): 234 os.makedirs(cfgdir) 235 236 assert path.get_xdg_dir() == cfgdir 237 238 @with_environment 239 def test_get_xdg_dir_3(monkeypatch): 240 """test_get_xdg_dir_3, check xdg_dir not used on non-posix systems""" 241 monkeypatch.setattr(path, "get_home_dir", lambda: HOME_TEST_DIR) 242 monkeypatch.setattr(os, "name", "nt") 243 monkeypatch.setattr(sys, "platform", "win32") 244 env.pop("IPYTHON_DIR", None) 245 env.pop("IPYTHONDIR", None) 246 env.pop("XDG_CONFIG_HOME", None) 247 cfgdir = os.path.join(path.get_home_dir(), ".config") 248 os.makedirs(cfgdir, exist_ok=True) 249 250 assert path.get_xdg_dir() is None 251 252 def test_filefind(): 253 """Various tests for filefind""" 254 f = tempfile.NamedTemporaryFile() 255 # print('fname:',f.name) 256 alt_dirs = paths.get_ipython_dir() 257 t = path.filefind(f.name, alt_dirs) 258 # print('found:',t) 259 260 261 @dec.skip_if_not_win32 262 def test_get_long_path_name_win32(): 263 with TemporaryDirectory() as tmpdir: 264 265 # Make a long path. Expands the path of tmpdir prematurely as it may already have a long 266 # path component, so ensure we include the long form of it 267 long_path = os.path.join(path.get_long_path_name(tmpdir), 'this is my long path name') 268 os.makedirs(long_path) 269 270 # Test to see if the short path evaluates correctly. 271 short_path = os.path.join(tmpdir, 'THISIS~1') 272 evaluated_path = path.get_long_path_name(short_path) 273 assert evaluated_path.lower() == long_path.lower() 274 275 276 @dec.skip_win32 277 def test_get_long_path_name(): 278 p = path.get_long_path_name("/usr/local") 279 assert p == "/usr/local" 280 281 282 @dec.skip_win32 # can't create not-user-writable dir on win 283 @with_environment 284 def test_not_writable_ipdir(): 285 tmpdir = tempfile.mkdtemp() 286 os.name = "posix" 287 env.pop("IPYTHON_DIR", None) 288 env.pop("IPYTHONDIR", None) 289 env.pop("XDG_CONFIG_HOME", None) 290 env["HOME"] = tmpdir 291 ipdir = os.path.join(tmpdir, ".ipython") 292 os.mkdir(ipdir, 0o555) 293 try: 294 open(os.path.join(ipdir, "_foo_"), "w", encoding="utf-8").close() 295 except IOError: 296 pass 297 else: 298 # I can still write to an unwritable dir, 299 # assume I'm root and skip the test 300 pytest.skip("I can't create directories that I can't write to") 301 302 with pytest.warns(UserWarning, match="is not a writable location"): 303 ipdir = paths.get_ipython_dir() 304 env.pop("IPYTHON_DIR", None) 305 306 307 @with_environment 308 def test_get_py_filename(): 309 os.chdir(TMP_TEST_DIR) 310 with make_tempfile("foo.py"): 311 assert path.get_py_filename("foo.py") == "foo.py" 312 assert path.get_py_filename("foo") == "foo.py" 313 with make_tempfile("foo"): 314 assert path.get_py_filename("foo") == "foo" 315 pytest.raises(IOError, path.get_py_filename, "foo.py") 316 pytest.raises(IOError, path.get_py_filename, "foo") 317 pytest.raises(IOError, path.get_py_filename, "foo.py") 318 true_fn = "foo with spaces.py" 319 with make_tempfile(true_fn): 320 assert path.get_py_filename("foo with spaces") == true_fn 321 assert path.get_py_filename("foo with spaces.py") == true_fn 322 pytest.raises(IOError, path.get_py_filename, '"foo with spaces.py"') 323 pytest.raises(IOError, path.get_py_filename, "'foo with spaces.py'") 324 325 @onlyif_unicode_paths 326 def test_unicode_in_filename(): 327 """When a file doesn't exist, the exception raised should be safe to call 328 str() on - i.e. in Python 2 it must only have ASCII characters. 329 330 https://github.com/ipython/ipython/issues/875 331 """ 332 try: 333 # these calls should not throw unicode encode exceptions 334 path.get_py_filename('fooéè.py') 335 except IOError as ex: 336 str(ex) 337 338 339 class TestShellGlob(unittest.TestCase): 340 341 @classmethod 342 def setUpClass(cls): 343 cls.filenames_start_with_a = ['a0', 'a1', 'a2'] 344 cls.filenames_end_with_b = ['0b', '1b', '2b'] 345 cls.filenames = cls.filenames_start_with_a + cls.filenames_end_with_b 346 cls.tempdir = TemporaryDirectory() 347 td = cls.tempdir.name 348 349 with cls.in_tempdir(): 350 # Create empty files 351 for fname in cls.filenames: 352 open(os.path.join(td, fname), "w", encoding="utf-8").close() 353 354 @classmethod 355 def tearDownClass(cls): 356 cls.tempdir.cleanup() 357 358 @classmethod 359 @contextmanager 360 def in_tempdir(cls): 361 save = os.getcwd() 362 try: 363 os.chdir(cls.tempdir.name) 364 yield 365 finally: 366 os.chdir(save) 367 368 def check_match(self, patterns, matches): 369 with self.in_tempdir(): 370 # glob returns unordered list. that's why sorted is required. 371 assert sorted(path.shellglob(patterns)) == sorted(matches) 372 373 def common_cases(self): 374 return [ 375 (['*'], self.filenames), 376 (['a*'], self.filenames_start_with_a), 377 (['*c'], ['*c']), 378 (['*', 'a*', '*b', '*c'], self.filenames 379 + self.filenames_start_with_a 380 + self.filenames_end_with_b 381 + ['*c']), 382 (['a[012]'], self.filenames_start_with_a), 383 ] 384 385 @skip_win32 386 def test_match_posix(self): 387 for (patterns, matches) in self.common_cases() + [ 388 ([r'\*'], ['*']), 389 ([r'a\*', 'a*'], ['a*'] + self.filenames_start_with_a), 390 ([r'a\[012]'], ['a[012]']), 391 ]: 392 self.check_match(patterns, matches) 393 394 @skip_if_not_win32 395 def test_match_windows(self): 396 for (patterns, matches) in self.common_cases() + [ 397 # In windows, backslash is interpreted as path 398 # separator. Therefore, you can't escape glob 399 # using it. 400 ([r'a\*', 'a*'], [r'a\*'] + self.filenames_start_with_a), 401 ([r'a\[012]'], [r'a\[012]']), 402 ]: 403 self.check_match(patterns, matches) 404 405 406 @pytest.mark.parametrize( 407 "globstr, unescaped_globstr", 408 [ 409 (r"\*\[\!\]\?", "*[!]?"), 410 (r"\\*", r"\*"), 411 (r"\\\*", r"\*"), 412 (r"\\a", r"\a"), 413 (r"\a", r"\a"), 414 ], 415 ) 416 def test_unescape_glob(globstr, unescaped_globstr): 417 assert path.unescape_glob(globstr) == unescaped_globstr 418 419 420 @onlyif_unicode_paths 421 def test_ensure_dir_exists(): 422 with TemporaryDirectory() as td: 423 d = os.path.join(td, '∂ir') 424 path.ensure_dir_exists(d) # create it 425 assert os.path.isdir(d) 426 path.ensure_dir_exists(d) # no-op 427 f = os.path.join(td, "ƒile") 428 open(f, "w", encoding="utf-8").close() # touch 429 with pytest.raises(IOError): 430 path.ensure_dir_exists(f) 431 432 class TestLinkOrCopy(unittest.TestCase): 433 def setUp(self): 434 self.tempdir = TemporaryDirectory() 435 self.src = self.dst("src") 436 with open(self.src, "w", encoding="utf-8") as f: 437 f.write("Hello, world!") 438 439 def tearDown(self): 440 self.tempdir.cleanup() 441 442 def dst(self, *args): 443 return os.path.join(self.tempdir.name, *args) 444 445 def assert_inode_not_equal(self, a, b): 446 assert ( 447 os.stat(a).st_ino != os.stat(b).st_ino 448 ), "%r and %r do reference the same indoes" % (a, b) 449 450 def assert_inode_equal(self, a, b): 451 assert ( 452 os.stat(a).st_ino == os.stat(b).st_ino 453 ), "%r and %r do not reference the same indoes" % (a, b) 454 455 def assert_content_equal(self, a, b): 456 with open(a, "rb") as a_f: 457 with open(b, "rb") as b_f: 458 assert a_f.read() == b_f.read() 459 460 @skip_win32 461 def test_link_successful(self): 462 dst = self.dst("target") 463 path.link_or_copy(self.src, dst) 464 self.assert_inode_equal(self.src, dst) 465 466 @skip_win32 467 def test_link_into_dir(self): 468 dst = self.dst("some_dir") 469 os.mkdir(dst) 470 path.link_or_copy(self.src, dst) 471 expected_dst = self.dst("some_dir", os.path.basename(self.src)) 472 self.assert_inode_equal(self.src, expected_dst) 473 474 @skip_win32 475 def test_target_exists(self): 476 dst = self.dst("target") 477 open(dst, "w", encoding="utf-8").close() 478 path.link_or_copy(self.src, dst) 479 self.assert_inode_equal(self.src, dst) 480 481 @skip_win32 482 def test_no_link(self): 483 real_link = os.link 484 try: 485 del os.link 486 dst = self.dst("target") 487 path.link_or_copy(self.src, dst) 488 self.assert_content_equal(self.src, dst) 489 self.assert_inode_not_equal(self.src, dst) 490 finally: 491 os.link = real_link 492 493 @skip_if_not_win32 494 def test_windows(self): 495 dst = self.dst("target") 496 path.link_or_copy(self.src, dst) 497 self.assert_content_equal(self.src, dst) 498 499 def test_link_twice(self): 500 # Linking the same file twice shouldn't leave duplicates around. 501 # See https://github.com/ipython/ipython/issues/6450 502 dst = self.dst('target') 503 path.link_or_copy(self.src, dst) 504 path.link_or_copy(self.src, dst) 505 self.assert_inode_equal(self.src, dst) 506 assert sorted(os.listdir(self.tempdir.name)) == ["src", "target"] 507