summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHelmut Grohne <helmut@subdivi.de>2024-04-03 09:36:53 +0200
committerHelmut Grohne <helmut@subdivi.de>2024-04-03 09:36:53 +0200
commitbd4edf4167923e83242933cb86402906243fcbe0 (patch)
tree52f5b8f2a57ec95323a93ea0383f076b9e002d77
parentff3d733095f23068056e8e3273a343d10149e87e (diff)
downloadpython-linuxnamespaces-bd4edf4167923e83242933cb86402906243fcbe0.tar.gz
add basic tests for AtLocation
-rw-r--r--pyproject.toml2
-rw-r--r--tests/test_atlocation.py193
2 files changed, 194 insertions, 1 deletions
diff --git a/pyproject.toml b/pyproject.toml
index 9db7f3a..e0abe04 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,7 +12,7 @@ classifiers = [
requires-python = ">=3.9"
[project.optional-dependencies]
-test = ["pytest"]
+test = ["pytest", "pytest-subtests"]
examples = ["dbussy", "zstandard"]
[tool.black]
diff --git a/tests/test_atlocation.py b/tests/test_atlocation.py
new file mode 100644
index 0000000..9836f3b
--- /dev/null
+++ b/tests/test_atlocation.py
@@ -0,0 +1,193 @@
+# Copyright 2024 Helmut Grohne <helmut@subdivi.de>
+# SPDX-License-Identifier: GPL-3
+
+import contextlib
+import functools
+import os
+import pathlib
+import stat
+import tempfile
+import typing
+import unittest
+
+import linuxnamespaces
+from linuxnamespaces import AtLocation
+
+
+class AtLocationTest(unittest.TestCase):
+ def setUp(self) -> None:
+ self.tempdir = pathlib.Path(
+ self.enterContext(tempfile.TemporaryDirectory())
+ )
+ self.counter = 0
+
+ @contextlib.contextmanager
+ def create(
+ self,
+ loctype: typing.Literal["relative", "absolute", "emptypath", "withfd"],
+ filetype: typing.Literal["absent", "file", "directory", "symlink"],
+ linktarget: typing.Optional[str],
+ follow_symlinks: bool = True,
+ ) -> typing.Iterator[AtLocation]:
+ """Create an AtLocation object for testing purposes. The created object
+ can be "absent", a regular "file", a "directory" or a symbolic link
+ ("symlink"). If it is a symlink, a linktarget must be given and
+ follow_symlinks may be used to set AT_SYMLINK_NOFOLLOW. The location
+ can refer to the object in multiple ways. If expressed as a "relative"
+ path, the resulting context manager will temporarily change the working
+ directory. It can also be "absolute" or use an O_PATH file descriptor
+ with an "emptypath" or with a path relative to the file descriptor
+ ("withfd")."""
+ sym = str(self.counter)
+ self.counter += 1
+ abspath = self.tempdir / sym
+ if filetype == "file":
+ abspath.touch()
+ elif filetype == "directory":
+ abspath.mkdir()
+ elif filetype == "symlink":
+ assert linktarget is not None
+ abspath.symlink_to(linktarget)
+ else:
+ self.assertEqual(filetype, "absent")
+ if follow_symlinks:
+ flags = linuxnamespaces.AtFlags.NONE
+ else:
+ flags = linuxnamespaces.AtFlags.AT_SYMLINK_NOFOLLOW
+ if loctype == "relative":
+ origcwd = os.getcwd()
+ os.chdir(self.tempdir)
+ try:
+ yield AtLocation(sym, flags=flags)
+ finally:
+ os.chdir(origcwd)
+ elif loctype == "absolute":
+ yield AtLocation(abspath, flags=flags)
+ elif loctype == "emptypath":
+ if follow_symlinks:
+ fd = os.open(abspath, os.O_PATH | os.O_NOFOLLOW)
+ else:
+ fd = os.open(abspath, os.O_PATH)
+ with AtLocation(fd) as loc:
+ yield loc
+ else:
+ self.assertEqual(loctype, "withfd")
+ with AtLocation(
+ os.open(self.tempdir, os.O_PATH), sym, flags
+ ) as loc:
+ yield loc
+
+ def create_all(
+ self, skip: typing.Container[str] = ()
+ ) -> typing.Iterator[tuple[str, typing.ContextManager[AtLocation]]]:
+ """Create various AtLocation objects referring to fils, directories
+ and other things in various ways.
+ """
+ for loctype in ("relative", "absolute", "emptypath", "withfd"):
+ if loctype in skip:
+ continue
+ for filetype in ("absent", "file", "directory", "symlink"):
+ if filetype in skip:
+ continue
+ if filetype == "absent" and loctype in ("emptypath", "withfd"):
+ continue
+ follow_symlinks_values = [True]
+ if filetype == "symlink" and loctype != "emptypath":
+ follow_symlinks_values.append(False)
+ for follow_symlinks in follow_symlinks_values:
+ # Mypy fails to see that loctype and filetype really are
+ # literals rather than arbitrary strings.
+ atlocctx = self.create(
+ loctype, # type: ignore[arg-type]
+ filetype, # type: ignore[arg-type]
+ "X",
+ follow_symlinks,
+ )
+ yield (filetype, atlocctx)
+
+ @staticmethod
+ def atloc_subtest(
+ skip: typing.Container[str] = (),
+ ) -> typing.Callable[
+ [typing.Callable[["AtLocationTest", str, AtLocation], None]],
+ typing.Callable[["AtLocationTest"], None],
+ ]:
+ """Wrap a test function and invoke it with possible AtLocations in a
+ subTest managed context.
+ """
+
+ def decorator(
+ func: typing.Callable[["AtLocationTest", str, AtLocation], None]
+ ) -> typing.Callable[["AtLocationTest"], None]:
+ @functools.wraps(func)
+ def decorated(self: "AtLocationTest") -> None:
+ for filetype, atlocctx in self.create_all(skip):
+ with atlocctx as atloc, self.subTest(
+ atlocation=repr(atloc), filetype=filetype
+ ):
+ func(self, filetype, atloc)
+
+ return decorated
+
+ return decorator
+
+ @atloc_subtest()
+ def test_access(self, filetype: str, atloc: AtLocation) -> None:
+ should_exist = filetype in ("file", "directory") or (
+ filetype == "symlink"
+ and atloc.flags
+ & (
+ linuxnamespaces.AtFlags.AT_SYMLINK_NOFOLLOW
+ | linuxnamespaces.AtFlags.AT_EMPTY_PATH
+ )
+ != linuxnamespaces.AtFlags.NONE
+ )
+ if not atloc.flags & linuxnamespaces.AtFlags.AT_EMPTY_PATH:
+ self.assertEqual(atloc.access(os.R_OK), should_exist)
+ self.assertEqual(atloc.exists(), should_exist)
+
+ @atloc_subtest(skip=("absent", "file", "symlink"))
+ def test_join_mkdir(self, _: str, atloc: AtLocation) -> None:
+ subdir = atloc / "subdir"
+ self.assertFalse(subdir.exists())
+ subdir.mkdir()
+ self.assertTrue(subdir.is_dir())
+ subdir.rmdir()
+ self.assertFalse(subdir.exists())
+
+ def test_mknod(self) -> None:
+ with AtLocation(os.open(self.tempdir, os.O_PATH)) as rootloc:
+ for loc in [AtLocation(self.tempdir) / "a", rootloc / "b"]:
+ self.assertFalse(loc.exists(), f"{loc} does not exist")
+ loc.mknod(device=stat.S_IFREG)
+ self.assertTrue(loc.is_file(), f"{loc} is a file")
+
+ @atloc_subtest(skip=("absent", "file", "symlink"))
+ def test_walk(self, _: str, atloc: AtLocation) -> None:
+ (atloc / "emptydir").mkdir()
+ (atloc / "dir").mkdir()
+ (atloc / "dir" / "alibi").mknod(device=stat.S_IFREG)
+ (atloc / "deadlink").symlink_to("doesnotexist")
+ (atloc / "symlink").symlink_to("dir/alibi")
+ for dirloc, dirnames, filenames, dirfd in atloc.walk():
+ self.assertEqual(dirloc.fd, atloc.fd)
+ self.assertTrue(
+ dirfd.flags & linuxnamespaces.AtFlags.AT_EMPTY_PATH
+ )
+ for dentry in dirnames:
+ self.assertEqual(dentry.fd, dirfd.fd)
+ self.assertTrue(dentry.location)
+ for fentry in filenames:
+ self.assertEqual(fentry.fd, dirfd.fd)
+ self.assertTrue(fentry.location)
+ thing = str(fentry.location).rsplit("/", 1)[-1]
+ if thing == "alibi":
+ self.assertTrue(fentry.is_file())
+ if thing == "deadlink":
+ self.assertTrue(fentry.is_symlink())
+ self.assertEqual(fentry.readlink(), "doesnotexist")
+ self.assertFalse(fentry.symfollow().exists())
+ if thing == "symlink":
+ self.assertTrue(fentry.is_symlink())
+ self.assertEqual(fentry.readlink(), "dir/alibi")
+ self.assertTrue(fentry.symfollow().exists())