complete core tests

This commit is contained in:
Evgenii Alekseev 2021-03-28 03:26:11 +03:00
parent 46fb33bb9a
commit 0f3741d5f4
11 changed files with 430 additions and 42 deletions

View File

@ -102,7 +102,7 @@ class Application:
known_packages = self._known_packages()
def add_directory(path: Path) -> None:
for full_path in filter(lambda p: package_like(p.name), path.iterdir()):
for full_path in filter(package_like, path.iterdir()):
add_archive(full_path)
def add_manual(name: str) -> Path:
@ -192,8 +192,7 @@ class Application:
process_update(packages)
# process manual packages
tree = Tree()
tree.load(updates)
tree = Tree.load(updates)
for num, level in enumerate(tree.levels()):
self.logger.info(f"processing level #{num} {[package.base for package in level]}")
packages = self.repository.process_build(level)

View File

@ -55,7 +55,8 @@ class Configuration(configparser.RawConfigParser):
"""
:return: path to directory with configuration includes
"""
return Path(self.get("settings", "include"))
value = Path(self.get("settings", "include"))
return self.absolute_path_for(value)
@classmethod
def from_path(cls: Type[Configuration], path: Path, logfile: bool) -> Configuration:
@ -70,6 +71,16 @@ class Configuration(configparser.RawConfigParser):
config.load_logging(logfile)
return config
def absolute_path_for(self, path_part: Path) -> Path:
"""
helper to generate absolute configuration path for relative settings value
:param path_part: path to generate
:return: absolute path according to current path configuration
"""
if self.path is None or path_part.is_absolute():
return path_part
return self.path.parent / path_part
def dump(self, architecture: str) -> Dict[str, Dict[str, str]]:
"""
dump configuration to dictionary
@ -137,8 +148,10 @@ class Configuration(configparser.RawConfigParser):
"""
def file_logger() -> None:
try:
fileConfig(self.get("settings", "logging"))
except PermissionError:
value = Path(self.get("settings", "logging"))
config_path = self.absolute_path_for(value)
fileConfig(config_path)
except (FileNotFoundError, PermissionError):
console_logger()
logging.exception("could not create logfile, fallback to stderr")

View File

@ -38,7 +38,7 @@ class Repository(Executor, UpdateHandler):
"""
result: Dict[str, Package] = {}
for full_path in self.paths.repository.iterdir():
if not package_like(full_path.name):
if not package_like(full_path):
continue
try:
local = Package.load(full_path, self.pacman, self.aur_url)

View File

@ -23,7 +23,7 @@ import shutil
import tempfile
from pathlib import Path
from typing import Iterable, List, Set
from typing import Iterable, List, Set, Type
from ahriman.core.build_tools.task import Task
from ahriman.models.package import Package
@ -36,13 +36,14 @@ class Leaf:
:ivar package: leaf package properties
"""
def __init__(self, package: Package) -> None:
def __init__(self, package: Package, dependencies: Set[str]) -> None:
"""
default constructor
:param package: package properties
:param dependencies: package dependencies
"""
self.package = package
self.dependencies: Set[str] = set()
self.dependencies = dependencies
@property
def items(self) -> Iterable[str]:
@ -51,6 +52,21 @@ class Leaf:
"""
return self.package.packages.keys()
@classmethod
def load(cls: Type[Leaf], package: Package) -> Leaf:
"""
load leaf from package with dependencies
:param package: package properties
:return: loaded class
"""
clone_dir = Path(tempfile.mkdtemp())
try:
Task.fetch(clone_dir, package.git_url)
dependencies = Package.dependencies(clone_dir)
finally:
shutil.rmtree(clone_dir, ignore_errors=True)
return cls(package, dependencies)
def is_root(self, packages: Iterable[Leaf]) -> bool:
"""
check if package depends on any other package from list of not
@ -62,17 +78,6 @@ class Leaf:
return False
return True
def load_dependencies(self) -> None:
"""
load dependencies for the leaf
"""
clone_dir = Path(tempfile.mkdtemp())
try:
Task.fetch(clone_dir, self.package.git_url)
self.dependencies = Package.dependencies(clone_dir)
finally:
shutil.rmtree(clone_dir, ignore_errors=True)
class Tree:
"""
@ -80,11 +85,21 @@ class Tree:
:ivar leaves: list of tree leaves
"""
def __init__(self) -> None:
def __init__(self, leaves: List[Leaf]) -> None:
"""
default constructor
:param leaves: leaves to build the tree
"""
self.leaves: List[Leaf] = []
self.leaves = leaves
@classmethod
def load(cls: Type[Tree], packages: Iterable[Package]) -> Tree:
"""
load tree from packages
:param packages: packages list
:return: loaded class
"""
return cls([Leaf.load(package) for package in packages])
def levels(self) -> List[List[Package]]:
"""
@ -99,13 +114,3 @@ class Tree:
unprocessed = [leaf for leaf in unprocessed if not leaf.is_root(unprocessed)]
return result
def load(self, packages: Iterable[Package]) -> None:
"""
load tree from packages
:param packages: packages list
"""
for package in packages:
leaf = Leaf(package)
leaf.load_dependencies()
self.leaves.append(leaf)

View File

@ -28,19 +28,17 @@ from ahriman.core.exceptions import InvalidOption
def check_output(*args: str, exception: Optional[Exception],
cwd: Optional[Path] = None, stderr: int = subprocess.STDOUT,
logger: Optional[Logger] = None) -> str:
cwd: Optional[Path] = None, logger: Optional[Logger] = None) -> str:
"""
subprocess wrapper
:param args: command line arguments
:param exception: exception which has to be reraised instead of default subprocess exception
:param cwd: current working directory
:param stderr: standard error output mode
:param logger: logger to log command result if required
:return: command output
"""
try:
result = subprocess.check_output(args, cwd=cwd, stderr=stderr).decode("utf8").strip()
result = subprocess.check_output(args, cwd=cwd, stderr=subprocess.STDOUT).decode("utf8").strip()
if logger is not None:
for line in result.splitlines():
logger.debug(line)
@ -52,13 +50,14 @@ def check_output(*args: str, exception: Optional[Exception],
return result
def package_like(filename: str) -> bool:
def package_like(filename: Path) -> bool:
"""
check if file looks like package
:param filename: name of file to check
:return: True in case if name contains `.pkg.` and not signature, False otherwise
"""
return ".pkg." in filename and not filename.endswith(".sig")
name = filename.name
return ".pkg." in name and not name.endswith(".sig")
def pretty_datetime(timestamp: Optional[int]) -> str:
@ -86,10 +85,10 @@ def pretty_size(size: Optional[float], level: int = 0) -> str:
return "MiB"
if level == 3:
return "GiB"
raise InvalidOption(level) # I hope it will not be more than 1024 GiB
raise InvalidOption(level) # must never happen actually
if size is None:
return ""
if size < 1024:
return f"{round(size, 2)} {str_level()}"
if size < 1024 or level == 3:
return f"{size:.1f} {str_level()}"
return pretty_size(size / 1024, level + 1)

View File

@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
@ -35,3 +36,10 @@ class PackageDescription:
build_date: Optional[int] = None
filename: Optional[str] = None
installed_size: Optional[int] = None
@property
def filepath(self) -> Optional[Path]:
"""
:return: path object for current filename
"""
return Path(self.filename) if self.filename is not None else None

View File

@ -6,6 +6,7 @@ from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.repo import Repo
from ahriman.core.build_tools.task import Task
from ahriman.core.configuration import Configuration
from ahriman.core.tree import Leaf
from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths
@ -16,6 +17,16 @@ def configuration(resource_path_root: Path) -> Configuration:
return Configuration.from_path(path=path, logfile=False)
@pytest.fixture
def leaf_ahriman(package_ahriman: Package) -> Leaf:
return Leaf(package_ahriman, set())
@pytest.fixture
def leaf_python_schedule(package_python_schedule: Package) -> Leaf:
return Leaf(package_python_schedule, set())
@pytest.fixture
def pacman(configuration: Configuration) -> Pacman:
return Pacman(configuration)

View File

@ -0,0 +1,127 @@
from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
def test_from_path(mocker: MockerFixture) -> None:
"""
must load configuration
"""
read_mock = mocker.patch("configparser.RawConfigParser.read")
load_includes_mock = mocker.patch("ahriman.core.configuration.Configuration.load_includes")
load_logging_mock = mocker.patch("ahriman.core.configuration.Configuration.load_logging")
path = Path("path")
config = Configuration.from_path(path, True)
assert config.path == path
read_mock.assert_called_with(path)
load_includes_mock.assert_called_once()
load_logging_mock.assert_called_once()
def test_absolute_path_for_absolute(configuration: Configuration) -> None:
"""
must not change path for absolute path in settings
"""
path = Path("/a/b/c")
assert configuration.absolute_path_for(path) == path
def test_absolute_path_for_relative(configuration: Configuration) -> None:
"""
must prepend root path to relative path
"""
path = Path("a")
result = configuration.absolute_path_for(path)
assert result.is_absolute()
assert result.parent == configuration.path.parent
assert result.name == path.name
def test_dump(configuration: Configuration) -> None:
"""
dump must not be empty
"""
assert configuration.dump("x86_64")
def test_dump_architecture_specific(configuration: Configuration) -> None:
"""
dump must contain architecture specific settings
"""
configuration.add_section("build_x86_64")
configuration.set("build_x86_64", "archbuild_flags", "")
dump = configuration.dump("x86_64")
assert dump
assert "build" not in dump
assert "build_x86_64" in dump
def test_getlist(configuration: Configuration) -> None:
"""
must return list of string correctly
"""
configuration.set("build", "test_list", "a b c")
assert configuration.getlist("build", "test_list") == ["a", "b", "c"]
def test_getlist_empty(configuration: Configuration) -> None:
"""
must return list of string correctly for non-existing option
"""
assert configuration.getlist("build", "test_list") == []
configuration.set("build", "test_list", "")
assert configuration.getlist("build", "test_list") == []
def test_getlist_single(configuration: Configuration) -> None:
"""
must return list of strings for single string
"""
configuration.set("build", "test_list", "a")
assert configuration.getlist("build", "test_list") == ["a"]
def test_get_section_name(configuration: Configuration) -> None:
"""
must return architecture specific group
"""
configuration.add_section("build_x86_64")
configuration.set("build_x86_64", "archbuild_flags", "")
assert configuration.get_section_name("build", "x86_64") == "build_x86_64"
def test_get_section_name_missing(configuration: Configuration) -> None:
"""
must return default group if architecture depending group does not exist
"""
assert configuration.get_section_name("prefix", "suffix") == "prefix"
assert configuration.get_section_name("build", "x86_64") == "build"
def test_load_includes_missing(configuration: Configuration) -> None:
"""
must not fail if not include directory found
"""
configuration.set("settings", "include", "path")
configuration.load_includes()
def test_load_logging_fallback(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must fallback to stderr without errors
"""
mocker.patch("logging.config.fileConfig", side_effect=PermissionError())
configuration.load_logging(True)
def test_load_logging_stderr(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must use stderr if flag set
"""
logging_mock = mocker.patch("logging.config.fileConfig")
configuration.load_logging(False)
logging_mock.assert_not_called()

View File

@ -0,0 +1,78 @@
from pytest_mock import MockerFixture
from ahriman.core.tree import Leaf, Tree
from ahriman.models.package import Package
def test_leaf_is_root_empty(leaf_ahriman: Leaf) -> None:
"""
must be root for empty packages list
"""
assert leaf_ahriman.is_root([])
def test_leaf_is_root_false(leaf_ahriman: Leaf, leaf_python_schedule: Leaf) -> None:
"""
must be root for empty dependencies list or if does not depend on packages
"""
assert leaf_ahriman.is_root([leaf_python_schedule])
leaf_ahriman.dependencies = {"ahriman-dependency"}
assert leaf_ahriman.is_root([leaf_python_schedule])
def test_leaf_is_root_true(leaf_ahriman: Leaf, leaf_python_schedule: Leaf) -> None:
"""
must not be root if depends on packages
"""
leaf_ahriman.dependencies = {"python-schedule"}
assert not leaf_ahriman.is_root([leaf_python_schedule])
leaf_ahriman.dependencies = {"python2-schedule"}
assert not leaf_ahriman.is_root([leaf_python_schedule])
leaf_ahriman.dependencies = set(leaf_python_schedule.package.packages.keys())
assert not leaf_ahriman.is_root([leaf_python_schedule])
def test_leaf_load(package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must load with dependencies
"""
tempdir_mock = mocker.patch("tempfile.mkdtemp")
fetch_mock = mocker.patch("ahriman.core.build_tools.task.Task.fetch")
dependencies_mock = mocker.patch("ahriman.models.package.Package.dependencies", return_value={"ahriman-dependency"})
rmtree_mock = mocker.patch("shutil.rmtree")
leaf = Leaf.load(package_ahriman)
assert leaf.package == package_ahriman
assert leaf.dependencies == {"ahriman-dependency"}
tempdir_mock.assert_called_once()
fetch_mock.assert_called_once()
dependencies_mock.assert_called_once()
rmtree_mock.assert_called_once()
def test_tree_levels(leaf_ahriman: Leaf, leaf_python_schedule: Leaf, mocker: MockerFixture) -> None:
"""
must generate correct levels in the simples case
"""
leaf_ahriman.dependencies = set(leaf_python_schedule.package.packages.keys())
tree = Tree([leaf_ahriman, leaf_python_schedule])
assert len(tree.levels()) == 2
first, second = tree.levels()
assert first == [leaf_python_schedule.package]
assert second == [leaf_ahriman.package]
def test_tree_load(package_ahriman: Package, package_python_schedule: Package, mocker: MockerFixture) -> None:
"""
must package list
"""
mocker.patch("tempfile.mkdtemp")
mocker.patch("ahriman.core.build_tools.task.Task.fetch")
mocker.patch("ahriman.models.package.Package.dependencies")
mocker.patch("shutil.rmtree")
tree = Tree.load([package_ahriman, package_python_schedule])
assert len(tree.leaves) == 2

View File

@ -0,0 +1,131 @@
import logging
import pytest
import subprocess
from pytest_mock import MockerFixture
from ahriman.core.util import check_output, package_like, pretty_datetime, pretty_size
from ahriman.models.package import Package
def test_check_output(mocker: MockerFixture) -> None:
"""
must run command and log result
"""
logger_mock = mocker.patch("logging.Logger.debug")
assert check_output("echo", "hello", exception=None) == "hello"
logger_mock.assert_not_called()
assert check_output("echo", "hello", exception=None, logger=logging.getLogger("")) == "hello"
logger_mock.assert_called_once()
def test_check_output_failure(mocker: MockerFixture) -> None:
"""
must process exception correctly
"""
logger_mock = mocker.patch("logging.Logger.debug")
mocker.patch("subprocess.check_output", side_effect=subprocess.CalledProcessError(1, "echo"))
with pytest.raises(subprocess.CalledProcessError):
check_output("echo", "hello", exception=None)
logger_mock.assert_not_called()
with pytest.raises(subprocess.CalledProcessError):
check_output("echo", "hello", exception=None, logger=logging.getLogger(""))
logger_mock.assert_not_called()
def test_check_output_failure_log(mocker: MockerFixture) -> None:
"""
must process exception correctly and log it
"""
logger_mock = mocker.patch("logging.Logger.debug")
mocker.patch("subprocess.check_output", side_effect=subprocess.CalledProcessError(1, "echo", output=b"result"))
with pytest.raises(subprocess.CalledProcessError):
check_output("echo", "hello", exception=None, logger=logging.getLogger(""))
logger_mock.assert_called_once()
def test_package_like(package_ahriman: Package) -> None:
"""
package_like must return true for archives
"""
assert package_like(package_ahriman.packages[package_ahriman.base].filepath)
def test_package_like_sig(package_ahriman: Package) -> None:
"""
package_like must return false for signature files
"""
package_file = package_ahriman.packages[package_ahriman.base].filepath
sig_file = package_file.parent / f"{package_file.name}.sig"
assert not package_like(sig_file)
def test_pretty_datetime() -> None:
"""
must generate string from timestamp value
"""
assert pretty_datetime(0) == "1970-01-01 00:00:00"
def test_pretty_datetime_empty() -> None:
"""
must generate empty string from None timestamp
"""
assert pretty_datetime(None) == ""
def test_pretty_size_bytes() -> None:
"""
must generate bytes string for bytes value
"""
value, abbrev = pretty_size(42).split()
assert value == "42.0"
assert abbrev == "B"
def test_pretty_size_kbytes() -> None:
"""
must generate kibibytes string for kibibytes value
"""
value, abbrev = pretty_size(42 * 1024).split()
assert value == "42.0"
assert abbrev == "KiB"
def test_pretty_size_mbytes() -> None:
"""
must generate mebibytes string for mebibytes value
"""
value, abbrev = pretty_size(42 * 1024 * 1024).split()
assert value == "42.0"
assert abbrev == "MiB"
def test_pretty_size_gbytes() -> None:
"""
must generate gibibytes string for gibibytes value
"""
value, abbrev = pretty_size(42 * 1024 * 1024 * 1024).split()
assert value == "42.0"
assert abbrev == "GiB"
def test_pretty_size_pbytes() -> None:
"""
must generate pebibytes string for pebibytes value
"""
value, abbrev = pretty_size(42 * 1024 * 1024 * 1024 * 1024).split()
assert value == "43008.0"
assert abbrev == "GiB"
def test_pretty_size_empty() -> None:
"""
must generate empty string for None value
"""
assert pretty_size(None) == ""

View File

@ -0,0 +1,17 @@
from ahriman.models.package_desciption import PackageDescription
def test_filepath(package_description_ahriman: PackageDescription) -> None:
"""
must generate correct filepath if set
"""
assert package_description_ahriman.filepath is not None
assert package_description_ahriman.filepath.name == package_description_ahriman.filename
def test_filepath_empty(package_description_ahriman: PackageDescription) -> None:
"""
must return None for missing filename
"""
package_description_ahriman.filename = None
assert package_description_ahriman.filepath is None