feat: remove excess dependencies leaves (#128)

This mr improves implicit dependencies processing by reducing tree leaves by using the following algorithm:

* remove paths which belong to any base package
* remove packages which are (opt)dependencies of one of the package which provides same path. It also tries to handle circular dependencies by excluding them from being "satisfied"
* remove packages which are already satisfied by any children path
This commit is contained in:
Evgenii Alekseev 2024-08-06 18:00:53 +03:00
parent 4f5166ff25
commit 54b99cacfd
11 changed files with 425 additions and 44 deletions

View File

@ -80,7 +80,7 @@ class Executor(PackageInfo, Cleaner):
# clear changes and update commit hash # clear changes and update commit hash
self.reporter.package_changes_update(single.base, Changes(last_commit_sha)) self.reporter.package_changes_update(single.base, Changes(last_commit_sha))
# update dependencies list # update dependencies list
dependencies = PackageArchive(self.paths.build_directory, single).depends_on() dependencies = PackageArchive(self.paths.build_directory, single, self.pacman).depends_on()
self.reporter.package_dependencies_update(single.base, dependencies) self.reporter.package_dependencies_update(single.base, dependencies)
# update result set # update result set
result.add_updated(single) result.add_updated(single)

View File

@ -57,6 +57,7 @@ class AURPackage:
provides(list[str]): list of packages which this package provides provides(list[str]): list of packages which this package provides
license(list[str]): list of package licenses license(list[str]): list of package licenses
keywords(list[str]): list of package keywords keywords(list[str]): list of package keywords
groups(list[str]): list of package groups
Examples: Examples:
Mainly this class must be used from class methods instead of default :func:`__init__()`:: Mainly this class must be used from class methods instead of default :func:`__init__()`::
@ -100,6 +101,7 @@ class AURPackage:
provides: list[str] = field(default_factory=list) provides: list[str] = field(default_factory=list)
license: list[str] = field(default_factory=list) license: list[str] = field(default_factory=list)
keywords: list[str] = field(default_factory=list) keywords: list[str] = field(default_factory=list)
groups: list[str] = field(default_factory=list)
@classmethod @classmethod
def from_json(cls, dump: dict[str, Any]) -> Self: def from_json(cls, dump: dict[str, Any]) -> Self:
@ -153,6 +155,7 @@ class AURPackage:
provides=package.provides, provides=package.provides,
license=package.licenses, license=package.licenses,
keywords=[], keywords=[],
groups=package.groups,
) )
@classmethod @classmethod
@ -191,6 +194,7 @@ class AURPackage:
provides=dump["provides"], provides=dump["provides"],
license=dump["licenses"], license=dump["licenses"],
keywords=[], keywords=[],
groups=dump["groups"],
) )
@staticmethod @staticmethod

View File

@ -34,6 +34,13 @@ class Dependencies:
paths: dict[str, list[str]] = field(default_factory=dict) paths: dict[str, list[str]] = field(default_factory=dict)
def __post_init__(self) -> None:
"""
remove empty paths
"""
paths = {path: packages for path, packages in self.paths.items() if packages}
object.__setattr__(self, "paths", paths)
@classmethod @classmethod
def from_json(cls, dump: dict[str, Any]) -> Self: def from_json(cls, dump: dict[str, Any]) -> Self:
""" """

View File

@ -0,0 +1,90 @@
#
# Copyright (c) 2021-2024 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import annotations
from collections.abc import Iterable
from dataclasses import dataclass, field
from pathlib import Path
from ahriman.core.util import trim_package
@dataclass(frozen=True, kw_only=True)
class FilesystemPackage:
"""
class representing a simplified model for the package installed to filesystem
Attributes:
package_name(str): package name
depends(set[str]): list of package dependencies
directories(set[Path]): list of directories this package contains
files(list[Path]): list of files this package contains
opt_depends(set[str]): list of package optional dependencies
"""
package_name: str
depends: set[str]
opt_depends: set[str]
directories: list[Path] = field(default_factory=list)
files: list[Path] = field(default_factory=list)
def __post_init__(self) -> None:
"""
update dependencies list accordingly
"""
object.__setattr__(self, "depends", {trim_package(package) for package in self.depends})
object.__setattr__(self, "opt_depends", {trim_package(package) for package in self.opt_depends})
def depends_on(self, package_name: str, *, include_optional: bool) -> bool:
"""
check if package depends on given package name
Args:
package_name(str): package name to check dependencies
include_optional(bool): include optional dependencies to check
Returns:
bool: ``True`` in case if the given package in the dependencies lists
"""
if package_name in self.depends:
return True
if include_optional and package_name in self.opt_depends:
return True
return False
def is_root_package(self, packages: Iterable[FilesystemPackage], *, include_optional: bool) -> bool:
"""
check if the package is the one of the root packages. This method checks if there are any packages which are
dependency of the package and - to avoid circular dependencies - does not depend on the package. In addition,
if ``include_optional`` is set to ``True``, then it will also check optional dependencies of the package
Args:
packages(Iterable[FilesystemPackage]): list of packages in which we need to search
include_optional(bool): include optional dependencies to check
Returns:
bool: whether this package depends on any other package in the list of packages
"""
return not any(
package
for package in packages
if self.depends_on(package.package_name, include_optional=include_optional)
and not package.depends_on(self.package_name, include_optional=False)
)

View File

@ -23,8 +23,12 @@ from elftools.elf.elffile import ELFFile
from pathlib import Path from pathlib import Path
from typing import IO from typing import IO
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.remote import OfficialSyncdb
from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.util import walk from ahriman.core.util import walk
from ahriman.models.dependencies import Dependencies from ahriman.models.dependencies import Dependencies
from ahriman.models.filesystem_package import FilesystemPackage
from ahriman.models.package import Package from ahriman.models.package import Package
@ -36,10 +40,12 @@ class PackageArchive:
Attributes: Attributes:
package(Package): package descriptor package(Package): package descriptor
root(Path): path to root filesystem root(Path): path to root filesystem
pacman(Pacman): alpm wrapper instance
""" """
root: Path root: Path
package: Package package: Package
pacman: Pacman
@staticmethod @staticmethod
def dynamic_needed(binary_path: Path) -> list[str]: def dynamic_needed(binary_path: Path) -> list[str]:
@ -80,7 +86,7 @@ class PackageArchive:
content(IO[bytes]): content of the file content(IO[bytes]): content of the file
Returns: Returns:
bool: True in case if file has elf header and False otherwise bool: ``True`` in case if file has elf header and ``False`` otherwise
""" """
expected = b"\x7fELF" expected = b"\x7fELF"
length = len(expected) length = len(expected)
@ -90,6 +96,89 @@ class PackageArchive:
return magic_bytes == expected return magic_bytes == expected
def _load_pacman_package(self, path: Path) -> FilesystemPackage:
"""
load pacman package model from path
Args:
path(Path): path to package files database
Returns:
FilesystemPackage: generated pacman package model with empty paths
"""
package_name, *_ = path.parent.name.rsplit("-", 2)
try:
pacman_package = OfficialSyncdb.info(package_name, pacman=self.pacman)
return FilesystemPackage(
package_name=package_name,
depends=set(pacman_package.depends),
opt_depends=set(pacman_package.opt_depends),
)
except UnknownPackageError:
return FilesystemPackage(package_name=package_name, depends=set(), opt_depends=set())
def _raw_dependencies_packages(self) -> dict[Path, list[FilesystemPackage]]:
"""
extract the initial list of packages which contain specific path this package depends on
Returns:
dict[Path, list[FilesystemPackage]]: map of path to packages containing this path
"""
dependencies, roots = self.depends_on_paths()
installed_packages = self.installed_packages()
# build initial map of file path -> packages containing this path
# in fact, keys will contain all libraries the package linked to and all directories it contains
dependencies_per_path: dict[Path, list[FilesystemPackage]] = {}
for package_base, package in installed_packages.items():
if package_base in self.package.packages:
continue # skip package itself
required_by = [directory for directory in package.directories if directory in roots]
required_by.extend(library for library in package.files if library.name in dependencies)
for path in required_by:
dependencies_per_path.setdefault(path, []).append(package)
return dependencies_per_path
def _refine_dependencies(self, source: dict[Path, list[FilesystemPackage]]) -> dict[Path, list[FilesystemPackage]]:
"""
reduce the initial dependency list by removing packages which are already satisfied (e.g. by other path or by
dependency list, or belonging to the base packages)
Args:
source(dict[Path, list[FilesystemPackage]]): the initial map of path to packages containing it
Returns:
dict[Path, list[FilesystemPackage]]: reduced source map of packages
"""
# base packages should be always excluded from checking
base_packages = OfficialSyncdb.info("base", pacman=self.pacman).depends
result: dict[Path, list[FilesystemPackage]] = {}
# sort items from children directories to root
for path, packages in reversed(sorted(source.items())):
# skip if this path belongs to the one of the base packages
if any(package.package_name in base_packages for package in packages):
continue
# remove explicit dependencies
packages = [package for package in packages if package.is_root_package(packages, include_optional=False)]
# remove optional dependencies
packages = [package for package in packages if package.is_root_package(packages, include_optional=True)]
# check if there is already parent of current path in the result and has the same packages
for children_path, children_packages in result.items():
if not children_path.is_relative_to(path):
continue
children_packages_names = {package.package_name for package in children_packages}
packages = [package for package in packages if package.package_name not in children_packages_names]
result[path] = packages
return result
def depends_on(self) -> Dependencies: def depends_on(self) -> Dependencies:
""" """
extract packages and paths which are required for this package extract packages and paths which are required for this package
@ -97,20 +186,14 @@ class PackageArchive:
Returns: Returns:
Dependencies: map of the package name to set of paths used by this package Dependencies: map of the package name to set of paths used by this package
""" """
dependencies, roots = self.depends_on_paths() initial_packages = self._raw_dependencies_packages()
refined_packages = self._refine_dependencies(initial_packages)
result: dict[str, list[str]] = {} paths = {
for package, (directories, files) in self.installed_packages().items(): str(path): [package.package_name for package in packages]
if package in self.package.packages: for path, packages in refined_packages.items()
continue # skip package itself }
return Dependencies(paths)
required_by = [directory for directory in directories if directory in roots]
required_by.extend(library for library in files if library.name in dependencies)
for path in required_by:
result.setdefault(str(path), []).append(package)
return Dependencies(result)
def depends_on_paths(self) -> tuple[set[str], set[Path]]: def depends_on_paths(self) -> tuple[set[str], set[Path]]:
""" """
@ -130,36 +213,35 @@ class PackageArchive:
return dependencies, roots return dependencies, roots
def installed_packages(self) -> dict[str, tuple[list[Path], list[Path]]]: def installed_packages(self) -> dict[str, FilesystemPackage]:
""" """
extract list of the installed packages and their content extract list of the installed packages and their content
Returns: Returns:
dict[str, tuple[list[Path], list[Path]]]; map of package name to list of directories and files contained dict[str, FilesystemPackage]; map of package name to list of directories and files contained
by this package by this package
""" """
result = {} result = {}
pacman_local_files = self.root / "var" / "lib" / "pacman" / "local" pacman_local_files = self.root / "var" / "lib" / "pacman" / "local"
for path in filter(lambda fn: fn.name == "files", walk(pacman_local_files)): for path in filter(lambda fn: fn.name == "files", walk(pacman_local_files)):
package, *_ = path.parent.name.rsplit("-", 2) package = self._load_pacman_package(path)
directories, files = [], [] is_files_section = False
is_files = False
for line in path.read_text(encoding="utf8").splitlines(): for line in path.read_text(encoding="utf8").splitlines():
if not line: # skip empty lines if not line: # skip empty lines
continue continue
if line.startswith("%") and line.endswith("%"): # directive started if line.startswith("%") and line.endswith("%"): # directive started
is_files = line == "%FILES%" is_files_section = line == "%FILES%"
if not is_files: # not a files directive if not is_files_section: # not a files directive
continue continue
entry = Path(line) entry = Path(line)
if line.endswith("/"): # simple check if it is directory if line.endswith("/"): # simple check if it is directory
directories.append(entry) package.directories.append(entry)
else: else:
files.append(entry) package.files.append(entry)
result[package] = directories, files result[package.package_name] = package
return result return result

View File

@ -181,6 +181,7 @@ def aur_package_ahriman() -> AURPackage:
provides=[], provides=[],
license=["GPL3"], license=["GPL3"],
keywords=[], keywords=[],
groups=[],
) )
@ -228,6 +229,7 @@ def aur_package_akonadi() -> AURPackage:
provides=[], provides=[],
license=["LGPL"], license=["LGPL"],
keywords=[], keywords=[],
groups=[],
) )
@ -397,7 +399,8 @@ def package_description_ahriman() -> PackageDescription:
groups=[], groups=[],
installed_size=4200000, installed_size=4200000,
licenses=["GPL3"], licenses=["GPL3"],
url="https://github.com/arcan1s/ahriman") url="https://github.com/arcan1s/ahriman",
)
@pytest.fixture @pytest.fixture
@ -418,7 +421,8 @@ def package_description_python_schedule() -> PackageDescription:
groups=[], groups=[],
installed_size=4200001, installed_size=4200001,
licenses=["MIT"], licenses=["MIT"],
url="https://github.com/dbader/schedule") url="https://github.com/dbader/schedule",
)
@pytest.fixture @pytest.fixture
@ -439,7 +443,8 @@ def package_description_python2_schedule() -> PackageDescription:
groups=[], groups=[],
installed_size=4200002, installed_size=4200002,
licenses=["MIT"], licenses=["MIT"],
url="https://github.com/dbader/schedule") url="https://github.com/dbader/schedule",
)
@pytest.fixture @pytest.fixture

View File

@ -30,7 +30,7 @@ def test_package_logger_set_reset(database: SQLite) -> None:
database._package_logger_reset() database._package_logger_reset()
record = logging.makeLogRecord({}) record = logging.makeLogRecord({})
with pytest.raises(AttributeError): with pytest.raises(AttributeError):
record.package_id assert record.package_id
def test_in_package_context(database: SQLite, package_ahriman: Package, mocker: MockerFixture) -> None: def test_in_package_context(database: SQLite, package_ahriman: Package, mocker: MockerFixture) -> None:

View File

@ -6,10 +6,12 @@ from unittest.mock import MagicMock, PropertyMock
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from ahriman import __version__ from ahriman import __version__
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.remote import AUR from ahriman.core.alpm.remote import AUR
from ahriman.models.aur_package import AURPackage from ahriman.models.aur_package import AURPackage
from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.counters import Counters from ahriman.models.counters import Counters
from ahriman.models.filesystem_package import FilesystemPackage
from ahriman.models.internal_status import InternalStatus from ahriman.models.internal_status import InternalStatus
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.package_archive import PackageArchive from ahriman.models.package_archive import PackageArchive
@ -46,6 +48,17 @@ def counters() -> Counters:
success=0) success=0)
@pytest.fixture
def filesystem_package() -> FilesystemPackage:
"""
filesystem_package fixture
Returns:
FilesystemPackage: filesystem package test instance
"""
return FilesystemPackage(package_name="package", depends={"dependency"}, opt_depends={"optional"})
@pytest.fixture @pytest.fixture
def internal_status(counters: Counters) -> InternalStatus: def internal_status(counters: Counters) -> InternalStatus:
""" """
@ -65,7 +78,7 @@ def internal_status(counters: Counters) -> InternalStatus:
@pytest.fixture @pytest.fixture
def package_archive_ahriman(package_ahriman: Package, repository_paths: RepositoryPaths, def package_archive_ahriman(package_ahriman: Package, repository_paths: RepositoryPaths, pacman: Pacman,
passwd: Any, mocker: MockerFixture) -> PackageArchive: passwd: Any, mocker: MockerFixture) -> PackageArchive:
""" """
package archive fixture package archive fixture
@ -73,6 +86,7 @@ def package_archive_ahriman(package_ahriman: Package, repository_paths: Reposito
Args: Args:
package_ahriman(Package): package test instance package_ahriman(Package): package test instance
repository_paths(RepositoryPaths): repository paths test instance repository_paths(RepositoryPaths): repository paths test instance
pacman(Pacman): pacman test instance
passwd(Any): passwd structure test instance passwd(Any): passwd structure test instance
mocker(MockerFixture): mocker object mocker(MockerFixture): mocker object
@ -80,7 +94,7 @@ def package_archive_ahriman(package_ahriman: Package, repository_paths: Reposito
PackageArchive: package archive test instance PackageArchive: package archive test instance
""" """
mocker.patch("ahriman.models.repository_paths.getpwuid", return_value=passwd) mocker.patch("ahriman.models.repository_paths.getpwuid", return_value=passwd)
return PackageArchive(repository_paths.build_directory, package_ahriman) return PackageArchive(repository_paths.build_directory, package_ahriman, pacman)
@pytest.fixture @pytest.fixture
@ -150,6 +164,7 @@ def pyalpm_package_ahriman(aur_package_ahriman: AURPackage) -> MagicMock:
type(mock).provides = PropertyMock(return_value=aur_package_ahriman.provides) type(mock).provides = PropertyMock(return_value=aur_package_ahriman.provides)
type(mock).version = PropertyMock(return_value=aur_package_ahriman.version) type(mock).version = PropertyMock(return_value=aur_package_ahriman.version)
type(mock).url = PropertyMock(return_value=aur_package_ahriman.url) type(mock).url = PropertyMock(return_value=aur_package_ahriman.url)
type(mock).groups = PropertyMock(return_value=aur_package_ahriman.groups)
return mock return mock

View File

@ -1,6 +1,13 @@
from ahriman.models.dependencies import Dependencies from ahriman.models.dependencies import Dependencies
def test_post_init() -> None:
"""
must remove empty leaves
"""
assert Dependencies({"path": ["package"], "empty": []}) == Dependencies({"path": ["package"]})
def test_from_json_view() -> None: def test_from_json_view() -> None:
""" """
must construct and serialize dependencies to json must construct and serialize dependencies to json

View File

@ -0,0 +1,60 @@
from ahriman.models.filesystem_package import FilesystemPackage
def test_post_init() -> None:
"""
must trim versions and descriptions from dependencies list
"""
assert FilesystemPackage(package_name="p", depends={"a=1"}, opt_depends={"c: a description"}) == \
FilesystemPackage(package_name="p", depends={"a"}, opt_depends={"c"})
def test_depends_on(filesystem_package: FilesystemPackage) -> None:
"""
must correctly check package dependencies
"""
assert filesystem_package.depends_on("dependency", include_optional=False)
assert not filesystem_package.depends_on("random", include_optional=False)
def test_depends_on_optional(filesystem_package: FilesystemPackage) -> None:
"""
must correctly check optional dependencies
"""
assert filesystem_package.depends_on("optional", include_optional=True)
assert not filesystem_package.depends_on("optional", include_optional=False)
assert not filesystem_package.depends_on("random", include_optional=True)
def test_is_root_package() -> None:
"""
must correctly identify root packages
"""
package = FilesystemPackage(package_name="package", depends={"dependency"}, opt_depends={"optional"})
dependency = FilesystemPackage(package_name="dependency", depends=set(), opt_depends=set())
optional = FilesystemPackage(package_name="optional", depends=set(), opt_depends={"package"})
packages = [package, dependency, optional]
assert not package.is_root_package(packages, include_optional=True)
assert not package.is_root_package(packages, include_optional=False)
assert dependency.is_root_package(packages, include_optional=True)
assert dependency.is_root_package(packages, include_optional=False)
assert not optional.is_root_package(packages, include_optional=True)
assert optional.is_root_package(packages, include_optional=False)
def test_is_root_package_circular() -> None:
"""
must correctly identify root packages with circular dependencies
"""
package1 = FilesystemPackage(package_name="package1", depends={"package2"}, opt_depends=set())
package2 = FilesystemPackage(package_name="package2", depends={"package1"}, opt_depends=set())
assert package1.is_root_package([package1, package2], include_optional=False)
assert package2.is_root_package([package1, package2], include_optional=False)
package1 = FilesystemPackage(package_name="package1", depends=set(), opt_depends={"package2"})
package2 = FilesystemPackage(package_name="package2", depends=set(), opt_depends={"package1"})
assert not package1.is_root_package([package1, package2], include_optional=True)
assert package1.is_root_package([package1, package2], include_optional=False)

View File

@ -1,7 +1,10 @@
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from unittest.mock import MagicMock, PropertyMock
from ahriman.core.exceptions import UnknownPackageError
from ahriman.models.filesystem_package import FilesystemPackage
from ahriman.models.package_archive import PackageArchive from ahriman.models.package_archive import PackageArchive
@ -44,27 +47,135 @@ def test_is_elf() -> None:
assert PackageArchive.is_elf(BytesIO(b"\x7fELF\nrandom string")) assert PackageArchive.is_elf(BytesIO(b"\x7fELF\nrandom string"))
def test_depends_on(package_archive_ahriman: PackageArchive, mocker: MockerFixture) -> None: def test_load_pacman_package(package_archive_ahriman: PackageArchive, mocker: MockerFixture) -> None:
""" """
must extract packages and files which are dependencies for the package must correctly load filesystem package from pacman
""" """
mocker.patch("ahriman.models.package_archive.PackageArchive.installed_packages", return_value={ directory = f"{package_archive_ahriman.package.base}-{package_archive_ahriman.package.version}"
package_archive_ahriman.package.base: ([Path("usr") / "dir2"], [Path("file1")]), path = Path("/") / "var" / "lib" / "pacman" / "local" / directory / "files"
"package1": ( package = MagicMock()
[Path("package1") / "dir1", Path("usr") / "dir2"], type(package).depends = PropertyMock(return_value=["depends"])
[Path("package1") / "file1", Path("package1") / "file2"], type(package).opt_depends = PropertyMock(return_value=["opt_depends"])
info_mock = mocker.patch("ahriman.core.alpm.remote.OfficialSyncdb.info", return_value=package)
assert package_archive_ahriman._load_pacman_package(path) == FilesystemPackage(
package_name=package_archive_ahriman.package.base,
depends={"depends"},
opt_depends={"opt_depends"},
)
info_mock.assert_called_once_with(package_archive_ahriman.package.base, pacman=package_archive_ahriman.pacman)
def test_load_pacman_package_exception(package_archive_ahriman: PackageArchive, mocker: MockerFixture) -> None:
"""
must return empty package if no package found
"""
directory = f"{package_archive_ahriman.package.base}-{package_archive_ahriman.package.version}"
path = Path("/") / "var" / "lib" / "pacman" / "local" / directory / "files"
mocker.patch("ahriman.core.alpm.remote.OfficialSyncdb.info",
side_effect=UnknownPackageError(package_archive_ahriman.package.base))
assert package_archive_ahriman._load_pacman_package(path) == FilesystemPackage(
package_name=package_archive_ahriman.package.base,
depends=set(),
opt_depends=set(),
)
def test_raw_dependencies_packages(package_archive_ahriman: PackageArchive, mocker: MockerFixture) -> None:
"""
must correctly extract raw dependencies list
"""
packages = {
package_archive_ahriman.package.base: FilesystemPackage(
package_name=package_archive_ahriman.package.base,
depends=set(),
opt_depends=set(),
directories=[Path("usr") / "dir2"],
files=[Path("file1")],
), ),
"package2": ( "package1": FilesystemPackage(
[Path("usr") / "dir2", Path("package2") / "dir3", Path("package2") / "dir4"], package_name="package1",
[Path("package2") / "file4", Path("package2") / "file3"], depends=set(),
opt_depends=set(),
directories=[Path("package1") / "dir1", Path("usr") / "dir2"],
files=[Path("package1") / "file1", Path("package1") / "file2"],
), ),
}) "package2": FilesystemPackage(
package_name="package2",
depends=set(),
opt_depends=set(),
directories=[Path("usr") / "dir2", Path("package2") / "dir3", Path("package2") / "dir4"],
files=[Path("package2") / "file4", Path("package2") / "file3"],
),
}
mocker.patch("ahriman.models.package_archive.PackageArchive.installed_packages", return_value=packages)
mocker.patch("ahriman.models.package_archive.PackageArchive.depends_on_paths", return_value=( mocker.patch("ahriman.models.package_archive.PackageArchive.depends_on_paths", return_value=(
{"file1", "file3"}, {"file1", "file3"},
{Path("usr") / "dir2", Path("dir3"), Path("package2") / "dir4"}, {Path("usr") / "dir2", Path("dir3"), Path("package2") / "dir4"},
)) ))
result = package_archive_ahriman._raw_dependencies_packages()
assert result == {
Path("package1") / "file1": [packages["package1"]],
Path("package2") / "file3": [packages["package2"]],
Path("package2") / "dir4": [packages["package2"]],
Path("usr") / "dir2": [packages["package1"], packages["package2"]],
}
def test_refine_dependencies(package_archive_ahriman: PackageArchive, mocker: MockerFixture) -> None:
"""
must correctly refine dependencies list
"""
base_package = MagicMock()
type(base_package).depends = PropertyMock(return_value=["base"])
info_mock = mocker.patch("ahriman.core.alpm.remote.OfficialSyncdb.info", return_value=base_package)
path1 = Path("usr") / "lib" / "python3.12"
path2 = path1 / "site-packages"
path3 = Path("etc")
path4 = Path("var") / "lib" / "whatever"
package1 = FilesystemPackage(package_name="package1", depends={"package5"}, opt_depends={"package2"})
package2 = FilesystemPackage(package_name="package2", depends={"package1"}, opt_depends=set())
package3 = FilesystemPackage(package_name="package3", depends=set(), opt_depends={"package1"})
package4 = FilesystemPackage(package_name="base", depends=set(), opt_depends=set())
package5 = FilesystemPackage(package_name="package5", depends={"package1"}, opt_depends=set())
package6 = FilesystemPackage(package_name="package6", depends=set(), opt_depends=set())
assert package_archive_ahriman._refine_dependencies({
path1: [package1, package2, package3, package5, package6],
path2: [package1, package2, package3, package5],
path3: [package1, package4],
path4: [package1],
}) == {
path1: [package6],
path2: [package1, package5],
path4: [package1],
}
info_mock.assert_called_once_with("base", pacman=package_archive_ahriman.pacman)
def test_depends_on(package_archive_ahriman: PackageArchive, mocker: MockerFixture) -> None:
"""
must extract packages and files which are dependencies for the package
"""
raw_mock = mocker.patch("ahriman.models.package_archive.PackageArchive._raw_dependencies_packages",
return_value="1")
refined_mock = mocker.patch("ahriman.models.package_archive.PackageArchive._refine_dependencies", return_value={
Path("package1") / "file1": [FilesystemPackage(package_name="package1", depends=set(), opt_depends=set())],
Path("package2") / "file3": [FilesystemPackage(package_name="package2", depends=set(), opt_depends=set())],
Path("package2") / "dir4": [FilesystemPackage(package_name="package2", depends=set(), opt_depends=set())],
Path("usr") / "dir2": [
FilesystemPackage(package_name="package1", depends=set(), opt_depends=set()),
FilesystemPackage(package_name="package2", depends=set(), opt_depends=set()),
],
})
result = package_archive_ahriman.depends_on() result = package_archive_ahriman.depends_on()
raw_mock.assert_called_once_with()
refined_mock.assert_called_once_with("1")
assert result.paths == { assert result.paths == {
"package1/file1": ["package1"], "package1/file1": ["package1"],
"package2/file3": ["package2"], "package2/file3": ["package2"],
@ -106,7 +217,7 @@ def test_installed_packages(package_archive_ahriman: PackageArchive, mocker: Moc
result = package_archive_ahriman.installed_packages() result = package_archive_ahriman.installed_packages()
assert result assert result
assert Path("usr") in result[package_archive_ahriman.package.base][0] assert Path("usr") in result[package_archive_ahriman.package.base].directories
assert Path("usr/bin/ahriman") in result[package_archive_ahriman.package.base][1] assert Path("usr/bin/ahriman") in result[package_archive_ahriman.package.base].files
walk_mock.assert_called_once_with(package_archive_ahriman.root / "var" / "lib" / "pacman" / "local") walk_mock.assert_called_once_with(package_archive_ahriman.root / "var" / "lib" / "pacman" / "local")
read_mock.assert_called_once_with(encoding="utf8") read_mock.assert_called_once_with(encoding="utf8")