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