Initial implementation and tests for packagemanager module
This commit is contained in:
parent
dd4f55c1fe
commit
57f484f0dd
131
rbackup/package_managers/packagemanager.py
Normal file
131
rbackup/package_managers/packagemanager.py
Normal file
@ -0,0 +1,131 @@
|
||||
"""
|
||||
.. author:: Eric Torres
|
||||
.. module:: rbackup.packagemanager
|
||||
:synopsis: Module for package manager plugins
|
||||
"""
|
||||
import logging
|
||||
import subprocess
|
||||
import tarfile
|
||||
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
# ========== Constants ==========
|
||||
|
||||
|
||||
# ========== Logging Setup ==========
|
||||
syslog = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ========== Classes ==========
|
||||
class PackageManager:
|
||||
def __init__(self, cachedir, db_path, pkglist_cmd):
|
||||
"""Default constructor for the PackageManager class.
|
||||
|
||||
:param cachedir: path to the package manager cache directory
|
||||
:type cachedir: str or path-like object
|
||||
:param db_path: path to the package manager database
|
||||
:type db_path: str or path-like object
|
||||
:param pkglist_cmd: command to list installed packages to stdout
|
||||
:type pkglist_cmd: list
|
||||
"""
|
||||
if not isinstance(pkglist_cmd, Iterable) or isinstance(pkglist_cmd, dict):
|
||||
raise TypeError("pkglist_cmd is the wrong type")
|
||||
elif not pkglist_cmd:
|
||||
raise ValueError(f"Package list command is empty: {pkglist_cmd}")
|
||||
elif any(not isinstance(arg, str) for arg in pkglist_cmd):
|
||||
raise TypeError(f"{pkglist_cmd} contains a non-str value")
|
||||
elif any(not bool(arg) for arg in pkglist_cmd):
|
||||
raise ValueError(f"{pkglist_cmd} contains an empty str value")
|
||||
|
||||
self._cachedir = Path(cachedir)
|
||||
self._db_path = Path(db_path)
|
||||
self._pkglist_cmd = pkglist_cmd
|
||||
|
||||
def __init_subclass__(cls, cachedir, db_path, pkglist_cmd):
|
||||
"""Default constructor for all child classes."""
|
||||
super().__init_subclass(cachedir, db_path, pkglist_cmd)
|
||||
|
||||
def _gen_pkglist(self):
|
||||
"""Generate a text file listing installed packages
|
||||
on the system and return the path to that file.
|
||||
|
||||
If there is an error in the package listing command, then
|
||||
it is to be assumed that no file was created, therefore there
|
||||
is no file to cleanup.
|
||||
|
||||
Note that this method is internal and is
|
||||
meant to be called from a subclass in a separate module.
|
||||
|
||||
:returns: path to temporary file
|
||||
:rtype: path-like object
|
||||
"""
|
||||
syslog.info("Creating a package list")
|
||||
|
||||
try:
|
||||
process = subprocess.run(self._pkglist_cmd, capture_output=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
syslog.error(e)
|
||||
else:
|
||||
with NamedTemporaryFile(mode="wb", delete=False) as pkglist:
|
||||
pkglist.write(process.stdout)
|
||||
|
||||
syslog.info("Package list generation complete")
|
||||
return Path(pkglist.name)
|
||||
|
||||
def _gen_db_archive(self, *args, **kwargs):
|
||||
"""Generate a database archive for this package manager.
|
||||
|
||||
Note that this method is internal and is
|
||||
meant to be called from a subclass in a separate module.
|
||||
|
||||
All arguments and keyword-only arguments are passed directly
|
||||
to the packagemanagerk
|
||||
|
||||
:returns: the path to the created file
|
||||
:rtype: path-like object
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
syslog.info("Creating a database archive")
|
||||
|
||||
with NamedTemporaryFile(delete=False) as db_archive:
|
||||
archive = tarfile.open(*args, **kwargs)
|
||||
|
||||
archive.add(cachedir)
|
||||
|
||||
syslog.info("Database archive generation complete")
|
||||
|
||||
return Path(db_archive.name)
|
||||
|
||||
@property
|
||||
def cache_directory(self):
|
||||
"""Return the cache directory of this package manager.
|
||||
|
||||
:rtype: path-like object
|
||||
"""
|
||||
return self._cachedir
|
||||
|
||||
@property
|
||||
def database_path(self):
|
||||
"""Return the database path of this package manager.
|
||||
|
||||
:rtype: path-like object
|
||||
"""
|
||||
return self._db_path
|
||||
|
||||
@property
|
||||
def pkglist_cmd(self):
|
||||
"""Return the package listing command of this package manager.
|
||||
|
||||
:rtype: iterable or str
|
||||
"""
|
||||
return self._pkglist_cmd
|
||||
|
||||
|
||||
# ========== Functions ==========
|
||||
if __name__ == "__main__":
|
||||
import doctest
|
||||
|
||||
doctest.testmod()
|
122
rbackup/tests/test_packagemanager.py
Normal file
122
rbackup/tests/test_packagemanager.py
Normal file
@ -0,0 +1,122 @@
|
||||
"""
|
||||
.. author:: Eric Torres
|
||||
:synopsis: Unit tests for the PackageManager module.
|
||||
"""
|
||||
import doctest
|
||||
import subprocess
|
||||
import unittest
|
||||
|
||||
from hypothesis import given, note
|
||||
from hypothesis.strategies import (
|
||||
booleans,
|
||||
dictionaries,
|
||||
integers,
|
||||
iterables,
|
||||
lists,
|
||||
one_of,
|
||||
none,
|
||||
text,
|
||||
)
|
||||
from pathlib import Path
|
||||
from rbackup.package_managers.packagemanager import PackageManager
|
||||
from unittest.mock import patch
|
||||
|
||||
# ========== Constants ==========
|
||||
TESTING_MODULE = "rbackup.package_managers.packagemanager"
|
||||
|
||||
|
||||
# ========== Functions ==========
|
||||
def load_tests(loader, tests, ignore):
|
||||
tests.addTests(doctest.DocTestSuite(TESTING_MODULE))
|
||||
return tests
|
||||
|
||||
|
||||
# ========== Test Cases ==========
|
||||
class TestCreatePackageManager(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.patched_path = patch(f"{TESTING_MODULE}.Path", autospec=Path)
|
||||
self.patched_subprocess = patch(f"{TESTING_MODULE}.subprocess.run")
|
||||
self.patched_tarfile = patch(f"{TESTING_MODULE}.tarfile.open")
|
||||
self.patched_tempfile = patch(f"{TESTING_MODULE}.NamedTemporaryFile")
|
||||
|
||||
self.mocked_path = self.patched_path.start()
|
||||
self.mocked_run = self.patched_subprocess.start()
|
||||
self.mocked_tarfile = self.patched_tarfile.start()
|
||||
self.mocked_tempfile = self.patched_tempfile.start()
|
||||
|
||||
self.cachedir = "/var/cache/pacman/"
|
||||
self.db_path = "/var/lib/pacman"
|
||||
self.pkglist_cmd = ["pacman", "-Qqe"]
|
||||
self.p = PackageManager(self.cachedir, self.db_path, self.pkglist_cmd)
|
||||
|
||||
@given(one_of(text(min_size=1), iterables(text(min_size=1), min_size=1)))
|
||||
def test_create_with_valid_values(self, l):
|
||||
PackageManager("nothing", "nothing", l)
|
||||
|
||||
@given(one_of(none(), booleans(), integers(), dictionaries(text(), text())))
|
||||
def test_incorrect_cmd_type(self, cmd):
|
||||
with self.assertRaises(TypeError):
|
||||
PackageManager("nothing", "nothing", cmd)
|
||||
|
||||
def test_empty_cmd(self):
|
||||
with self.assertRaises(ValueError):
|
||||
PackageManager("nothing", "nothing", [])
|
||||
PackageManager("nothing", "nothing", set())
|
||||
PackageManager("nothing", "nothing", '')
|
||||
|
||||
@given(iterables(one_of(none(), booleans(), integers()), min_size=1))
|
||||
def test_wrong_iterable_element_type(self, cmd):
|
||||
with self.assertRaises(TypeError):
|
||||
PackageManager("nothing", "nothing", cmd)
|
||||
|
||||
def test_empty_str_in_iterable(self):
|
||||
with self.assertRaises(ValueError):
|
||||
PackageManager("nothing", "nothing", [''])
|
||||
PackageManager("nothing", "nothing", ['pacman', ''])
|
||||
|
||||
def tearDown(self):
|
||||
self.patched_path.stop()
|
||||
self.patched_subprocess.stop()
|
||||
self.patched_tarfile.stop()
|
||||
self.patched_tempfile.stop()
|
||||
|
||||
|
||||
class TestPackageManagerMethods(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.patched_path = patch(f"{TESTING_MODULE}.Path", autospec=Path)
|
||||
self.patched_subprocess = patch(f"{TESTING_MODULE}.subprocess.run")
|
||||
self.patched_tarfile = patch(f"{TESTING_MODULE}.tarfile.open")
|
||||
self.patched_tempfile = patch(f"{TESTING_MODULE}.NamedTemporaryFile")
|
||||
|
||||
self.mocked_path = self.patched_path.start()
|
||||
self.mocked_run = self.patched_subprocess.start()
|
||||
self.mocked_tarfile = self.patched_tarfile.start()
|
||||
self.mocked_tempfile = self.patched_tempfile.start()
|
||||
|
||||
self.cachedir = "/var/cache/pacman/"
|
||||
self.db_path = "/var/lib/pacman"
|
||||
self.pkglist_cmd = ["pacman", "-Qqe"]
|
||||
self.p = PackageManager(self.cachedir, self.db_path, self.pkglist_cmd)
|
||||
|
||||
def test_pkglist(self):
|
||||
self.mocked_run.return_value.stdout = "packages"
|
||||
self.mocked_tempfile.return_value.name = "tempfile"
|
||||
|
||||
pkglist = self.p._gen_pkglist()
|
||||
|
||||
self.mocked_tempfile.return_value.__enter__.return_value.write.assert_called_with(
|
||||
"packages"
|
||||
)
|
||||
self.assertIsInstance(pkglist, Path)
|
||||
|
||||
def test_pkglist_subprocess_error(self):
|
||||
self.mocked_run.side_effect = subprocess.CalledProcessError(1, self.pkglist_cmd)
|
||||
|
||||
self.p._gen_pkglist()
|
||||
self.mocked_tempfile.assert_not_called()
|
||||
|
||||
def tearDown(self):
|
||||
self.patched_path.stop()
|
||||
self.patched_subprocess.stop()
|
||||
self.patched_tarfile.stop()
|
||||
self.patched_tempfile.stop()
|
Loading…
x
Reference in New Issue
Block a user