Skip to content

Commit

Permalink
pathlib tests: annotate tests needing symlinks with decorator (python…
Browse files Browse the repository at this point in the history
…#114625)

Add `@needs_symlinks` decorator for tests that require symlink support in
the path class.

Also add `@needs_windows` and `@needs_posix` decorators for tests that
require a specific a specific path flavour. These aren't much used yet, but
will be later.
  • Loading branch information
barneygale authored Jan 26, 2024
1 parent b5c7c84 commit 7a9727e
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 48 deletions.
17 changes: 6 additions & 11 deletions Lib/test/test_pathlib/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,14 @@
from test.support import os_helper
from test.support.os_helper import TESTFN, FakePath
from test.test_pathlib import test_pathlib_abc
from test.test_pathlib.test_pathlib_abc import needs_posix, needs_windows, needs_symlinks

try:
import grp, pwd
except ImportError:
grp = pwd = None


only_nt = unittest.skipIf(os.name != 'nt',
'test requires a Windows-compatible system')
only_posix = unittest.skipIf(os.name == 'nt',
'test requires a POSIX-compatible system')

root_in_posix = False
if hasattr(os, 'geteuid'):
root_in_posix = (os.geteuid() == 0)
Expand Down Expand Up @@ -1268,7 +1264,7 @@ def test_chmod(self):
self.assertEqual(p.stat().st_mode, new_mode)

# On Windows, os.chmod does not follow symlinks (issue #15411)
@only_posix
@needs_posix
@os_helper.skip_unless_working_chmod
def test_chmod_follow_symlinks_true(self):
p = self.cls(self.base) / 'linkA'
Expand Down Expand Up @@ -1537,7 +1533,7 @@ def test_mkdir_exist_ok_root(self):
self.cls('/').resolve().mkdir(exist_ok=True)
self.cls('/').resolve().mkdir(parents=True, exist_ok=True)

@only_nt # XXX: not sure how to test this on POSIX.
@needs_windows # XXX: not sure how to test this on POSIX.
def test_mkdir_with_unknown_drive(self):
for d in 'ZYXWVUTSRQPONMLKJIHGFEDCBA':
p = self.cls(d + ':\\')
Expand Down Expand Up @@ -1602,9 +1598,8 @@ def my_mkdir(path, mode=0o777):
self.assertNotIn(str(p12), concurrently_created)
self.assertTrue(p.exists())

@needs_symlinks
def test_symlink_to(self):
if not self.can_symlink:
self.skipTest("symlinks required")
P = self.cls(self.base)
target = P / 'fileA'
# Symlinking a path target.
Expand Down Expand Up @@ -1848,7 +1843,7 @@ def test_rglob_pathlike(self):
self.assertEqual(expect, set(p.rglob(FakePath(pattern))))


@only_posix
@unittest.skipIf(os.name == 'nt', 'test requires a POSIX-compatible system')
class PosixPathTest(PathTest, PurePosixPathTest):
cls = pathlib.PosixPath

Expand Down Expand Up @@ -2024,7 +2019,7 @@ def test_from_uri_pathname2url(self):
self.assertEqual(P.from_uri('file:' + pathname2url('//foo/bar')), P('//foo/bar'))


@only_nt
@unittest.skipIf(os.name != 'nt', 'test requires a Windows-compatible system')
class WindowsPathTest(PathTest, PureWindowsPathTest):
cls = pathlib.WindowsPath

Expand Down
86 changes: 49 additions & 37 deletions Lib/test/test_pathlib/test_pathlib_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,27 @@
from test.support.os_helper import TESTFN


_tests_needing_posix = set()
_tests_needing_windows = set()
_tests_needing_symlinks = set()


def needs_posix(fn):
"""Decorator that marks a test as requiring a POSIX-flavoured path class."""
_tests_needing_posix.add(fn.__name__)
return fn

def needs_windows(fn):
"""Decorator that marks a test as requiring a Windows-flavoured path class."""
_tests_needing_windows.add(fn.__name__)
return fn

def needs_symlinks(fn):
"""Decorator that marks a test as requiring a path class that supports symlinks."""
_tests_needing_symlinks.add(fn.__name__)
return fn


class UnsupportedOperationTest(unittest.TestCase):
def test_is_notimplemented(self):
self.assertTrue(issubclass(UnsupportedOperation, NotImplementedError))
Expand Down Expand Up @@ -115,6 +136,11 @@ class DummyPurePathTest(unittest.TestCase):
base = f'/this/path/kills/fascists/{TESTFN}'

def setUp(self):
name = self.id().split('.')[-1]
if name in _tests_needing_posix and self.cls.pathmod is not posixpath:
self.skipTest('requires POSIX-flavoured path class')
if name in _tests_needing_windows and self.cls.pathmod is posixpath:
self.skipTest('requires Windows-flavoured path class')
p = self.cls('a')
self.pathmod = p.pathmod
self.sep = self.pathmod.sep
Expand Down Expand Up @@ -888,6 +914,9 @@ class DummyPathTest(DummyPurePathTest):

def setUp(self):
super().setUp()
name = self.id().split('.')[-1]
if name in _tests_needing_symlinks and not self.can_symlink:
self.skipTest('requires symlinks')
pathmod = self.cls.pathmod
p = self.cls(self.base)
p.mkdir(parents=True)
Expand Down Expand Up @@ -1045,9 +1074,8 @@ def test_iterdir(self):
expected += ['linkA', 'linkB', 'brokenLink', 'brokenLinkLoop']
self.assertEqual(paths, { P(self.base, q) for q in expected })

@needs_symlinks
def test_iterdir_symlink(self):
if not self.can_symlink:
self.skipTest("symlinks required")
# __iter__ on a symlink to a directory.
P = self.cls
p = P(self.base, 'linkB')
Expand Down Expand Up @@ -1116,9 +1144,8 @@ def _check(path, pattern, case_sensitive, expected):
_check(path, "dirb/file*", True, [])
_check(path, "dirb/file*", False, ["dirB/fileB"])

@needs_symlinks
def test_glob_follow_symlinks_common(self):
if not self.can_symlink:
self.skipTest("symlinks required")
def _check(path, glob, expected):
actual = {path for path in path.glob(glob, follow_symlinks=True)
if path.parts.count("linkD") <= 1} # exclude symlink loop.
Expand All @@ -1144,9 +1171,8 @@ def _check(path, glob, expected):
_check(p, "dir*/*/../dirD/**/", ["dirC/dirD/../dirD/"])
_check(p, "*/dirD/**/", ["dirC/dirD/"])

@needs_symlinks
def test_glob_no_follow_symlinks_common(self):
if not self.can_symlink:
self.skipTest("symlinks required")
def _check(path, glob, expected):
actual = {path for path in path.glob(glob, follow_symlinks=False)}
self.assertEqual(actual, { P(self.base, q) for q in expected })
Expand Down Expand Up @@ -1210,9 +1236,8 @@ def _check(glob, expected):
_check(p.rglob("*.txt"), ["dirC/novel.txt"])
_check(p.rglob("*.*"), ["dirC/novel.txt"])

@needs_symlinks
def test_rglob_follow_symlinks_common(self):
if not self.can_symlink:
self.skipTest("symlinks required")
def _check(path, glob, expected):
actual = {path for path in path.rglob(glob, follow_symlinks=True)
if path.parts.count("linkD") <= 1} # exclude symlink loop.
Expand Down Expand Up @@ -1243,9 +1268,8 @@ def _check(path, glob, expected):
_check(p, "*.txt", ["dirC/novel.txt"])
_check(p, "*.*", ["dirC/novel.txt"])

@needs_symlinks
def test_rglob_no_follow_symlinks_common(self):
if not self.can_symlink:
self.skipTest("symlinks required")
def _check(path, glob, expected):
actual = {path for path in path.rglob(glob, follow_symlinks=False)}
self.assertEqual(actual, { P(self.base, q) for q in expected })
Expand All @@ -1269,10 +1293,9 @@ def _check(path, glob, expected):
_check(p, "*.txt", ["dirC/novel.txt"])
_check(p, "*.*", ["dirC/novel.txt"])

@needs_symlinks
def test_rglob_symlink_loop(self):
# Don't get fooled by symlink loops (Issue #26012).
if not self.can_symlink:
self.skipTest("symlinks required")
P = self.cls
p = P(self.base)
given = set(p.rglob('*'))
Expand Down Expand Up @@ -1302,10 +1325,9 @@ def test_glob_dotdot(self):
self.assertEqual(set(p.glob("xyzzy/..")), set())
self.assertEqual(set(p.glob("/".join([".."] * 50))), { P(self.base, *[".."] * 50)})

@needs_symlinks
def test_glob_permissions(self):
# See bpo-38894
if not self.can_symlink:
self.skipTest("symlinks required")
P = self.cls
base = P(self.base) / 'permissions'
base.mkdir()
Expand All @@ -1322,19 +1344,17 @@ def test_glob_permissions(self):
self.assertEqual(len(set(base.glob("*/fileC"))), 50)
self.assertEqual(len(set(base.glob("*/file*"))), 50)

@needs_symlinks
def test_glob_long_symlink(self):
# See gh-87695
if not self.can_symlink:
self.skipTest("symlinks required")
base = self.cls(self.base) / 'long_symlink'
base.mkdir()
bad_link = base / 'bad_link'
bad_link.symlink_to("bad" * 200)
self.assertEqual(sorted(base.glob('**/*')), [bad_link])

@needs_symlinks
def test_readlink(self):
if not self.can_symlink:
self.skipTest("symlinks required")
P = self.cls(self.base)
self.assertEqual((P / 'linkA').readlink(), self.cls('fileA'))
self.assertEqual((P / 'brokenLink').readlink(),
Expand All @@ -1358,9 +1378,8 @@ def _check_resolve(self, p, expected, strict=True):
# This can be used to check both relative and absolute resolutions.
_check_resolve_relative = _check_resolve_absolute = _check_resolve

@needs_symlinks
def test_resolve_common(self):
if not self.can_symlink:
self.skipTest("symlinks required")
P = self.cls
p = P(self.base, 'foo')
with self.assertRaises(OSError) as cm:
Expand Down Expand Up @@ -1419,10 +1438,9 @@ def test_resolve_common(self):
# resolves to 'dirB/..' first before resolving to parent of dirB.
self._check_resolve_relative(p, P(self.base, 'foo', 'in', 'spam'), False)

@needs_symlinks
def test_resolve_dot(self):
# See http://web.archive.org/web/20200623062557/https://bitbucket.org/pitrou/pathlib/issues/9/
if not self.can_symlink:
self.skipTest("symlinks required")
pathmod = self.pathmod
p = self.cls(self.base)
p.joinpath('0').symlink_to('.', target_is_directory=True)
Expand All @@ -1441,11 +1459,9 @@ def _check_symlink_loop(self, *args):
path.resolve(strict=True)
self.assertEqual(cm.exception.errno, errno.ELOOP)

@needs_posix
@needs_symlinks
def test_resolve_loop(self):
if not self.can_symlink:
self.skipTest("symlinks required")
if self.cls.pathmod is not posixpath:
self.skipTest("symlink loops work differently with concrete Windows paths")
# Loops with relative symlinks.
self.cls(self.base, 'linkX').symlink_to('linkX/inside')
self._check_symlink_loop(self.base, 'linkX')
Expand Down Expand Up @@ -1487,9 +1503,8 @@ def test_stat(self):
self.assertEqual(statA.st_dev, statC.st_dev)
# other attributes not used by pathlib.

@needs_symlinks
def test_stat_no_follow_symlinks(self):
if not self.can_symlink:
self.skipTest("symlinks required")
p = self.cls(self.base) / 'linkA'
st = p.stat()
self.assertNotEqual(st, p.stat(follow_symlinks=False))
Expand All @@ -1499,9 +1514,8 @@ def test_stat_no_follow_symlinks_nosymlink(self):
st = p.stat()
self.assertEqual(st, p.stat(follow_symlinks=False))

@needs_symlinks
def test_lstat(self):
if not self.can_symlink:
self.skipTest("symlinks required")
p = self.cls(self.base)/ 'linkA'
st = p.stat()
self.assertNotEqual(st, p.lstat())
Expand Down Expand Up @@ -1634,9 +1648,6 @@ def test_is_char_device_false(self):
self.assertIs((P / 'fileA\x00').is_char_device(), False)

def _check_complex_symlinks(self, link0_target):
if not self.can_symlink:
self.skipTest("symlinks required")

# Test solving a non-looping chain of symlinks (issue #19887).
pathmod = self.pathmod
P = self.cls(self.base)
Expand Down Expand Up @@ -1682,12 +1693,15 @@ def _check_complex_symlinks(self, link0_target):
finally:
os.chdir(old_path)

@needs_symlinks
def test_complex_symlinks_absolute(self):
self._check_complex_symlinks(self.base)

@needs_symlinks
def test_complex_symlinks_relative(self):
self._check_complex_symlinks('.')

@needs_symlinks
def test_complex_symlinks_relative_dot_dot(self):
self._check_complex_symlinks(self.pathmod.join('dirA', '..'))

Expand Down Expand Up @@ -1803,9 +1817,8 @@ def test_walk_bottom_up(self):
raise AssertionError(f"Unexpected path: {path}")
self.assertTrue(seen_testfn)

@needs_symlinks
def test_walk_follow_symlinks(self):
if not self.can_symlink:
self.skipTest("symlinks required")
self.setUpWalk()
walk_it = self.walk_path.walk(follow_symlinks=True)
for root, dirs, files in walk_it:
Expand All @@ -1816,9 +1829,8 @@ def test_walk_follow_symlinks(self):
else:
self.fail("Didn't follow symlink with follow_symlinks=True")

@needs_symlinks
def test_walk_symlink_location(self):
if not self.can_symlink:
self.skipTest("symlinks required")
self.setUpWalk()
# Tests whether symlinks end up in filenames or dirnames depending
# on the `follow_symlinks` argument.
Expand Down

0 comments on commit 7a9727e

Please sign in to comment.