diff --git a/docs/architecture.md b/docs/architecture.md index 490671e1..759c4219 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -22,7 +22,7 @@ This package contains application (aka executable) related classes and everythin This package contains everything which is required for any time of application run and separated to several packages: -* `ahriman.core.alpm` package controls pacman related functions. It provides wrappers for `pyalpm` library and safe calls for repository tools (`repo-add` and `repo-remove`). +* `ahriman.core.alpm` package controls pacman related functions. It provides wrappers for `pyalpm` library and safe calls for repository tools (`repo-add` and `repo-remove`). Also this package contains `ahriman.core.alpm.remote` package which provides wrapper for remote sources (e.g. AUR RPC and official repositories RPC). * `ahriman.core.auth` package provides classes for authorization methods used by web mostly. Base class is `ahriman.core.auth.auth.Auth` which must be called by `load` method. * `ahriman.core.build_tools` is a package which provides wrapper for `devtools` commands. * `ahriman.core.database` is everything including data and schema migrations for database. diff --git a/docs/configuration.md b/docs/configuration.md index d3af5ed0..d6308a6a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -23,7 +23,6 @@ Base configuration settings. libalpm and AUR related configuration. -* `aur_url` - base url for AUR, string, required. * `database` - path to pacman local database cache, string, required. * `repositories` - list of pacman repositories, space separated list of strings, required. * `root` - root for alpm library, string, required. diff --git a/docs/faq.md b/docs/faq.md index 22ec6852..e4f34e54 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -113,6 +113,14 @@ Well it is supported also. The last command will calculate diff from current tree to the `HEAD` and will store it locally. Patches will be applied on any package actions (e.g. it can be used for dependency management). +### Hey, I would like to rebuild the official repository package + +So it is the same as adding any other package, but due to restrictions you must specify source explicitly, e.g.: + +```shell +sudo -u ahriman ahriman package-add pacmann -s repository +``` + ### Package build fails because it cannot validate PGP signature of source files TL;DR diff --git a/package/share/ahriman/settings/ahriman.ini b/package/share/ahriman/settings/ahriman.ini index 4805f947..a96c2749 100644 --- a/package/share/ahriman/settings/ahriman.ini +++ b/package/share/ahriman/settings/ahriman.ini @@ -4,7 +4,6 @@ logging = ahriman.ini.d/logging.ini database = /var/lib/ahriman/ahriman.db [alpm] -aur_url = https://aur.archlinux.org database = /var/lib/pacman repositories = core extra community multilib root = / diff --git a/package/share/ahriman/templates/build-status.jinja2 b/package/share/ahriman/templates/build-status.jinja2 index 6aa15815..bfbc582f 100644 --- a/package/share/ahriman/templates/build-status.jinja2 +++ b/package/share/ahriman/templates/build-status.jinja2 @@ -74,7 +74,7 @@ {% for package in packages %} - {{ package.base }} + {% if package.web_url is not none %}{{ package.base }}{% else %}{{ package.base }}{% endif %} {{ package.version }} {{ package.packages|join("
"|safe) }} {{ package.groups|join("
"|safe) }} diff --git a/src/ahriman/application/application/application_packages.py b/src/ahriman/application/application/application_packages.py index 17f9ef0e..c4184d7c 100644 --- a/src/ahriman/application/application/application_packages.py +++ b/src/ahriman/application/application/application_packages.py @@ -80,12 +80,13 @@ class ApplicationPackages(ApplicationProperties): known_packages(Set[str]): list of packages which are known by the service without_dependencies(bool): if set, dependency check will be disabled """ - package = Package.load(source, PackageSource.AUR, self.repository.pacman, self.repository.aur_url) + package = Package.from_aur(source, self.repository.pacman) self.database.build_queue_insert(package) + self.database.remote_update(package) with tmpdir() as local_path: - Sources.load(local_path, package.git_url, self.database.patches_get(package.base)) + Sources.load(local_path, package.remote, self.database.patches_get(package.base)) self._process_dependencies(local_path, known_packages, without_dependencies) def _add_directory(self, source: str, *_: Any) -> None: @@ -108,9 +109,10 @@ class ApplicationPackages(ApplicationProperties): known_packages(Set[str]): list of packages which are known by the service without_dependencies(bool): if set, dependency check will be disabled """ - package = Package.load(source, PackageSource.Local, self.repository.pacman, self.repository.aur_url) + source_dir = Path(source) + package = Package.from_build(source_dir) cache_dir = self.repository.paths.cache_for(package.base) - shutil.copytree(Path(source), cache_dir) # copy package to store in caches + shutil.copytree(source_dir, cache_dir) # copy package to store in caches Sources.init(cache_dir) # we need to run init command in directory where we do have permissions self.database.build_queue_insert(package) @@ -132,6 +134,18 @@ class ApplicationPackages(ApplicationProperties): for chunk in response.iter_content(chunk_size=1024): local_file.write(chunk) + def _add_repository(self, source: str, *_: Any) -> None: + """ + add package from official repository + + Args: + source(str): package base name + """ + package = Package.from_official(source, self.repository.pacman) + self.database.build_queue_insert(package) + self.database.remote_update(package) + # repository packages must not depend on unknown packages, thus we are not going to process dependencies + def _process_dependencies(self, local_path: Path, known_packages: Set[str], without_dependencies: bool) -> None: """ process package dependencies diff --git a/src/ahriman/application/application/application_repository.py b/src/ahriman/application/application/application_repository.py index d8bc8f23..32187a55 100644 --- a/src/ahriman/application/application/application_repository.py +++ b/src/ahriman/application/application/application_repository.py @@ -128,14 +128,14 @@ class ApplicationRepository(ApplicationProperties): packages: List[str] = [] for single in probe.packages: try: - _ = Package.from_aur(single, probe.aur_url) + _ = Package.from_aur(single, self.repository.pacman) except Exception: packages.append(single) return packages def unknown_local(probe: Package) -> List[str]: cache_dir = self.repository.paths.cache_for(probe.base) - local = Package.from_build(cache_dir, probe.aur_url) + local = Package.from_build(cache_dir) packages = set(probe.packages.keys()).difference(local.packages.keys()) return list(packages) diff --git a/src/ahriman/application/handlers/patch.py b/src/ahriman/application/handlers/patch.py index 919891d2..73d5f1e0 100644 --- a/src/ahriman/application/handlers/patch.py +++ b/src/ahriman/application/handlers/patch.py @@ -29,7 +29,6 @@ from ahriman.core.configuration import Configuration from ahriman.core.formatters.string_printer import StringPrinter from ahriman.models.action import Action from ahriman.models.package import Package -from ahriman.models.package_source import PackageSource class Patch(Handler): @@ -57,21 +56,20 @@ class Patch(Handler): elif args.action == Action.Remove: Patch.patch_set_remove(application, args.package) elif args.action == Action.Update: - Patch.patch_set_create(application, args.package, args.track) + Patch.patch_set_create(application, Path(args.package), args.track) @staticmethod - def patch_set_create(application: Application, sources_dir: str, track: List[str]) -> None: + def patch_set_create(application: Application, sources_dir: Path, track: List[str]) -> None: """ create patch set for the package base Args: application(Application): application instance - sources_dir(str): path to directory with the package sources + sources_dir(Path): path to directory with the package sources track(List[str]): track files which match the glob before creating the patch """ - package = Package.load(sources_dir, PackageSource.Local, application.repository.pacman, - application.repository.aur_url) - patch = Sources.patch_create(Path(sources_dir), *track) + package = Package.from_build(sources_dir) + patch = Sources.patch_create(sources_dir, *track) application.database.patches_insert(package.base, patch) @staticmethod diff --git a/src/ahriman/application/handlers/search.py b/src/ahriman/application/handlers/search.py index 0d5e7c61..8c42cf6b 100644 --- a/src/ahriman/application/handlers/search.py +++ b/src/ahriman/application/handlers/search.py @@ -22,6 +22,7 @@ import argparse from dataclasses import fields from typing import Callable, Iterable, List, Tuple, Type +from ahriman.application.application import Application from ahriman.application.handlers.handler import Handler from ahriman.core.alpm.remote.aur import AUR from ahriman.core.alpm.remote.official import Official @@ -55,8 +56,10 @@ class Search(Handler): no_report(bool): force disable reporting unsafe(bool): if set no user check will be performed before path creation """ - official_packages_list = Official.multisearch(*args.search) - aur_packages_list = AUR.multisearch(*args.search) + application = Application(architecture, configuration, no_report, unsafe) + + official_packages_list = Official.multisearch(*args.search, pacman=application.repository.pacman) + aur_packages_list = AUR.multisearch(*args.search, pacman=application.repository.pacman) Search.check_if_empty(args.exit_code, not official_packages_list and not aur_packages_list) for packages_list in (official_packages_list, aur_packages_list): diff --git a/src/ahriman/core/alpm/pacman.py b/src/ahriman/core/alpm/pacman.py index cca3ce37..4dffa666 100644 --- a/src/ahriman/core/alpm/pacman.py +++ b/src/ahriman/core/alpm/pacman.py @@ -17,8 +17,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from pyalpm import Handle # type: ignore -from typing import Set +from pyalpm import Handle, Package, SIG_PACKAGE # type: ignore +from typing import Generator, Set from ahriman.core.configuration import Configuration @@ -42,7 +42,7 @@ class Pacman: pacman_root = configuration.getpath("alpm", "database") self.handle = Handle(root, str(pacman_root)) for repository in configuration.getlist("alpm", "repositories"): - self.handle.register_syncdb(repository, 0) # 0 is pgp_level + self.handle.register_syncdb(repository, SIG_PACKAGE) def all_packages(self) -> Set[str]: """ @@ -58,3 +58,19 @@ class Pacman: result.update(package.provides) # provides list for meta-packages return result + + def get(self, package_name: str) -> Generator[Package, None, None]: + """ + retrieve list of the packages from the repository by name + + Args: + package_name(str): package name to search + + Yields: + Package: list of packages which were returned by the query + """ + for database in self.handle.get_syncdbs(): + package = database.get_pkg(package_name) + if package is None: + continue + yield package diff --git a/src/ahriman/core/alpm/remote/aur.py b/src/ahriman/core/alpm/remote/aur.py index 34a6c225..6f4e1a36 100644 --- a/src/ahriman/core/alpm/remote/aur.py +++ b/src/ahriman/core/alpm/remote/aur.py @@ -19,8 +19,9 @@ # import requests -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List +from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.remote.remote import Remote from ahriman.core.exceptions import InvalidPackageInfo from ahriman.core.util import exception_response_text @@ -32,27 +33,15 @@ class AUR(Remote): AUR RPC wrapper Attributes: + DEFAULT_AUR_URL(str): (class attribute) default AUR url DEFAULT_RPC_URL(str): (class attribute) default AUR RPC url DEFAULT_RPC_VERSION(str): (class attribute) default AUR RPC version - rpc_url(str): AUR RPC url - rpc_version(str): AUR RPC version """ - DEFAULT_RPC_URL = "https://aur.archlinux.org/rpc" + DEFAULT_AUR_URL = "https://aur.archlinux.org" + DEFAULT_RPC_URL = f"{DEFAULT_AUR_URL}/rpc" DEFAULT_RPC_VERSION = "5" - def __init__(self, rpc_url: Optional[str] = None, rpc_version: Optional[str] = None) -> None: - """ - default constructor - - Args: - rpc_url(Optional[str], optional): AUR RPC url (Default value = None) - rpc_version(Optional[str], optional): AUR RPC version (Default value = None) - """ - Remote.__init__(self) - self.rpc_url = rpc_url or self.DEFAULT_RPC_URL - self.rpc_version = rpc_version or self.DEFAULT_RPC_VERSION - @staticmethod def parse_response(response: Dict[str, Any]) -> List[AURPackage]: """ @@ -73,6 +62,33 @@ class AUR(Remote): raise InvalidPackageInfo(error_details) return [AURPackage.from_json(package) for package in response["results"]] + @staticmethod + def remote_git_url(package_base: str, repository: str) -> str: + """ + generate remote git url from the package base + + Args + package_base(str): package base + repository(str): repository name + + Returns: + str: git url for the specific base + """ + return f"{AUR.DEFAULT_AUR_URL}/{package_base}.git" + + @staticmethod + def remote_web_url(package_base: str) -> str: + """ + generate remote web url from the package base + + Args + package_base(str): package base + + Returns: + str: web url for the specific base + """ + return f"{AUR.DEFAULT_AUR_URL}/packages/{package_base}" + def make_request(self, request_type: str, *args: str, **kwargs: str) -> List[AURPackage]: """ perform request to AUR RPC @@ -87,7 +103,7 @@ class AUR(Remote): """ query: Dict[str, Any] = { "type": request_type, - "v": self.rpc_version + "v": self.DEFAULT_RPC_VERSION } arg_query = "arg[]" if len(args) > 1 else "arg" @@ -97,7 +113,7 @@ class AUR(Remote): query[key] = value try: - response = requests.get(self.rpc_url, params=query) + response = requests.get(self.DEFAULT_RPC_URL, params=query) response.raise_for_status() return self.parse_response(response.json()) except requests.HTTPError as e: @@ -110,12 +126,13 @@ class AUR(Remote): self.logger.exception("could not perform request by using type %s", request_type) raise - def package_info(self, package_name: str) -> AURPackage: + def package_info(self, package_name: str, *, pacman: Pacman) -> AURPackage: """ get package info by its name Args: package_name(str): package name to search + pacman(Pacman): alpm wrapper instance Returns: AURPackage: package which match the package name @@ -123,12 +140,13 @@ class AUR(Remote): packages = self.make_request("info", package_name) return next(package for package in packages if package.name == package_name) - def package_search(self, *keywords: str) -> List[AURPackage]: + def package_search(self, *keywords: str, pacman: Pacman) -> List[AURPackage]: """ search package in AUR web Args: *keywords(str): keywords to search + pacman(Pacman): alpm wrapper instance Returns: List[AURPackage]: list of packages which match the criteria diff --git a/src/ahriman/core/alpm/remote/official.py b/src/ahriman/core/alpm/remote/official.py index 7430154c..afd544a4 100644 --- a/src/ahriman/core/alpm/remote/official.py +++ b/src/ahriman/core/alpm/remote/official.py @@ -19,8 +19,9 @@ # import requests -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List +from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.remote.remote import Remote from ahriman.core.exceptions import InvalidPackageInfo from ahriman.core.util import exception_response_text @@ -32,22 +33,15 @@ class Official(Remote): official repository RPC wrapper Attributes: - DEFAULT_RPC_URL(str): (class attribute) default AUR RPC url - rpc_url(str): AUR RPC url + DEFAULT_ARCHLINUX_URL(str): (class attribute) default archlinux url + DEFAULT_SEARCH_REPOSITORIES(List[str]): (class attribute) default list of repositories to search + DEFAULT_RPC_URL(str): (class attribute) default archlinux repositories RPC url """ + DEFAULT_ARCHLINUX_URL = "https://archlinux.org" + DEFAULT_SEARCH_REPOSITORIES = ["Core", "Extra", "Multilib", "Community"] DEFAULT_RPC_URL = "https://archlinux.org/packages/search/json" - def __init__(self, rpc_url: Optional[str] = None) -> None: - """ - default constructor - - Args: - rpc_url(Optional[str], optional): AUR RPC url (Default value = None) - """ - Remote.__init__(self) - self.rpc_url = rpc_url or self.DEFAULT_RPC_URL - @staticmethod def parse_response(response: Dict[str, Any]) -> List[AURPackage]: """ @@ -66,6 +60,35 @@ class Official(Remote): raise InvalidPackageInfo("API validation error") return [AURPackage.from_repo(package) for package in response["results"]] + @staticmethod + def remote_git_url(package_base: str, repository: str) -> str: + """ + generate remote git url from the package base + + Args + package_base(str): package base + repository(str): repository name + + Returns: + str: git url for the specific base + """ + if repository.lower() in ("core", "extra", "testing", "kde-unstable"): + return "https://github.com/archlinux/svntogit-packages.git" # hardcoded, ok + return "https://github.com/archlinux/svntogit-community.git" + + @staticmethod + def remote_web_url(package_base: str) -> str: + """ + generate remote web url from the package base + + Args + package_base(str): package base + + Returns: + str: web url for the specific base + """ + return f"{Official.DEFAULT_ARCHLINUX_URL}/packages/{package_base}" + def make_request(self, *args: str, by: str) -> List[AURPackage]: """ perform request to official repositories RPC @@ -78,7 +101,7 @@ class Official(Remote): List[AURPackage]: response parsed to package list """ try: - response = requests.get(self.rpc_url, params={by: args}) + response = requests.get(self.DEFAULT_RPC_URL, params={by: args, "repo": self.DEFAULT_SEARCH_REPOSITORIES}) response.raise_for_status() return self.parse_response(response.json()) except requests.HTTPError as e: @@ -88,12 +111,13 @@ class Official(Remote): self.logger.exception("could not perform request") raise - def package_info(self, package_name: str) -> AURPackage: + def package_info(self, package_name: str, *, pacman: Pacman) -> AURPackage: """ get package info by its name Args: package_name(str): package name to search + pacman(Pacman): alpm wrapper instance Returns: AURPackage: package which match the package name @@ -101,12 +125,13 @@ class Official(Remote): packages = self.make_request(package_name, by="name") return next(package for package in packages if package.name == package_name) - def package_search(self, *keywords: str) -> List[AURPackage]: + def package_search(self, *keywords: str, pacman: Pacman) -> List[AURPackage]: """ search package in AUR web Args: *keywords(str): keywords to search + pacman(Pacman): alpm wrapper instance Returns: List[AURPackage]: list of packages which match the criteria diff --git a/src/ahriman/core/alpm/remote/official_syncdb.py b/src/ahriman/core/alpm/remote/official_syncdb.py new file mode 100644 index 00000000..99f8b471 --- /dev/null +++ b/src/ahriman/core/alpm/remote/official_syncdb.py @@ -0,0 +1,51 @@ +# +# Copyright (c) 2021-2022 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 . +# +from ahriman.core.alpm.pacman import Pacman +from ahriman.core.alpm.remote.official import Official +from ahriman.models.aur_package import AURPackage + + +class OfficialSyncdb(Official): + """ + official repository wrapper based on synchronized databases. + + Despite the fact that official repository provides an API for the interaction according to the comment in issue + https://github.com/arcan1s/ahriman/pull/59#issuecomment-1106412297 we might face rate limits while requesting + updates. + + This approach also has limitations, because we don't require superuser rights (neither going to download database + separately), the database file might be outdated and must be handled manually (or kind of). This behaviour might be + changed in the future. + + Still we leave search function based on the official repositories RPC. + """ + + def package_info(self, package_name: str, *, pacman: Pacman) -> AURPackage: + """ + get package info by its name + + Args: + package_name(str): package name to search + pacman(Pacman): alpm wrapper instance + + Returns: + AURPackage: package which match the package name + """ + return next(AURPackage.from_pacman(package) for package in pacman.get(package_name)) diff --git a/src/ahriman/core/alpm/remote/remote.py b/src/ahriman/core/alpm/remote/remote.py index 57665f7e..360d3d83 100644 --- a/src/ahriman/core/alpm/remote/remote.py +++ b/src/ahriman/core/alpm/remote/remote.py @@ -23,6 +23,7 @@ import logging from typing import Dict, List, Type +from ahriman.core.alpm.pacman import Pacman from ahriman.models.aur_package import AURPackage @@ -41,26 +42,28 @@ class Remote: self.logger = logging.getLogger("build_details") @classmethod - def info(cls: Type[Remote], package_name: str) -> AURPackage: + def info(cls: Type[Remote], package_name: str, *, pacman: Pacman) -> AURPackage: """ get package info by its name Args: package_name(str): package name to search + pacman(Pacman): alpm wrapper instance Returns: AURPackage: package which match the package name """ - return cls().package_info(package_name) + return cls().package_info(package_name, pacman=pacman) @classmethod - def multisearch(cls: Type[Remote], *keywords: str) -> List[AURPackage]: + def multisearch(cls: Type[Remote], *keywords: str, pacman: Pacman) -> List[AURPackage]: """ search in remote repository by using API with multiple words. This method is required in order to handle https://bugs.archlinux.org/task/49133. In addition, short words will be dropped Args: *keywords(str): search terms, e.g. "ahriman", "is", "cool" + pacman(Pacman): alpm wrapper instance Returns: List[AURPackage]: list of packages each of them matches all search terms @@ -68,7 +71,7 @@ class Remote: instance = cls() packages: Dict[str, AURPackage] = {} for term in filter(lambda word: len(word) > 3, keywords): - portion = instance.search(term) + portion = instance.search(term, pacman=pacman) packages = { package.name: package # not mistake to group them by name for package in portion @@ -76,25 +79,60 @@ class Remote: } return list(packages.values()) + @staticmethod + def remote_git_url(package_base: str, repository: str) -> str: + """ + generate remote git url from the package base + + Args + package_base(str): package base + repository(str): repository name + + Returns: + str: git url for the specific base + + Raises: + NotImplementedError: not implemented method + """ + raise NotImplementedError + + @staticmethod + def remote_web_url(package_base: str) -> str: + """ + generate remote web url from the package base + + Args + package_base(str): package base + + Returns: + str: web url for the specific base + + Raises: + NotImplementedError: not implemented method + """ + raise NotImplementedError + @classmethod - def search(cls: Type[Remote], *keywords: str) -> List[AURPackage]: + def search(cls: Type[Remote], *keywords: str, pacman: Pacman) -> List[AURPackage]: """ search package in AUR web Args: *keywords(str): search terms, e.g. "ahriman", "is", "cool" + pacman(Pacman): alpm wrapper instance Returns: List[AURPackage]: list of packages which match the criteria """ - return cls().package_search(*keywords) + return cls().package_search(*keywords, pacman=pacman) - def package_info(self, package_name: str) -> AURPackage: + def package_info(self, package_name: str, *, pacman: Pacman) -> AURPackage: """ get package info by its name Args: package_name(str): package name to search + pacman(Pacman): alpm wrapper instance Returns: AURPackage: package which match the package name @@ -104,12 +142,13 @@ class Remote: """ raise NotImplementedError - def package_search(self, *keywords: str) -> List[AURPackage]: + def package_search(self, *keywords: str, pacman: Pacman) -> List[AURPackage]: """ search package in AUR web Args: *keywords(str): keywords to search + pacman(Pacman): alpm wrapper instance Returns: List[AURPackage]: list of packages which match the criteria diff --git a/src/ahriman/core/build_tools/sources.py b/src/ahriman/core/build_tools/sources.py index 8261e176..6e419958 100644 --- a/src/ahriman/core/build_tools/sources.py +++ b/src/ahriman/core/build_tools/sources.py @@ -18,11 +18,13 @@ # along with this program. If not, see . # import logging +import shutil from pathlib import Path from typing import List, Optional -from ahriman.core.util import check_output +from ahriman.core.util import check_output, walk +from ahriman.models.remote_source import RemoteSource class Sources: @@ -30,12 +32,14 @@ class Sources: helper to download package sources (PKGBUILD etc) Attributes: + DEFAULT_BRANCH(str): (class attribute) default branch to process git repositories. + Must be used only for local stored repositories, use RemoteSource descriptor instead for real packages logger(logging.Logger): (class attribute) class logger """ + DEFAULT_BRANCH = "master" # default fallback branch logger = logging.getLogger("build_details") - _branch = "master" # in case if BLM would like to change it _check_output = check_output @staticmethod @@ -73,13 +77,13 @@ class Sources: return Sources._check_output("git", "diff", exception=None, cwd=sources_dir, logger=Sources.logger) @staticmethod - def fetch(sources_dir: Path, remote: Optional[str]) -> None: + def fetch(sources_dir: Path, remote: Optional[RemoteSource]) -> None: """ either clone repository or update it to origin/`branch` Args: sources_dir(Path): local path to fetch - remote(Optional[str]): remote target (from where to fetch) + remote(Optional[RemoteSource]): remote target (from where to fetch) """ # local directory exists and there is .git directory is_initialized_git = (sources_dir / ".git").is_dir() @@ -88,22 +92,30 @@ class Sources: Sources.logger.info("skip update at %s because there are no branches configured", sources_dir) return + branch = remote.branch if remote is not None else Sources.DEFAULT_BRANCH if is_initialized_git: - Sources.logger.info("update HEAD to remote at %s", sources_dir) - Sources._check_output("git", "fetch", "origin", Sources._branch, + Sources.logger.info("update HEAD to remote at %s using branch %s", sources_dir, branch) + Sources._check_output("git", "fetch", "origin", branch, + exception=None, cwd=sources_dir, logger=Sources.logger) + elif remote is not None: + Sources.logger.info("clone remote %s to %s using branch %s", remote.git_url, sources_dir, branch) + Sources._check_output("git", "clone", "--branch", branch, "--single-branch", + remote.git_url, str(sources_dir), exception=None, cwd=sources_dir, logger=Sources.logger) - elif remote is None: - Sources.logger.warning("%s is not initialized, but no remote provided", sources_dir) else: - Sources.logger.info("clone remote %s to %s", remote, sources_dir) - Sources._check_output("git", "clone", remote, str(sources_dir), - exception=None, cwd=sources_dir, logger=Sources.logger) + Sources.logger.warning("%s is not initialized, but no remote provided", sources_dir) + # and now force reset to our branch - Sources._check_output("git", "checkout", "--force", Sources._branch, + Sources._check_output("git", "checkout", "--force", branch, exception=None, cwd=sources_dir, logger=Sources.logger) - Sources._check_output("git", "reset", "--hard", f"origin/{Sources._branch}", + Sources._check_output("git", "reset", "--hard", f"origin/{branch}", exception=None, cwd=sources_dir, logger=Sources.logger) + # move content if required + # we are using full path to source directory in order to make append possible + pkgbuild_dir = remote.pkgbuild_dir if remote is not None else sources_dir.resolve() + Sources.move((sources_dir / pkgbuild_dir).resolve(), sources_dir) + @staticmethod def has_remotes(sources_dir: Path) -> bool: """ @@ -126,17 +138,17 @@ class Sources: Args: sources_dir(Path): local path to sources """ - Sources._check_output("git", "init", "--initial-branch", Sources._branch, + Sources._check_output("git", "init", "--initial-branch", Sources.DEFAULT_BRANCH, exception=None, cwd=sources_dir, logger=Sources.logger) @staticmethod - def load(sources_dir: Path, remote: str, patch: Optional[str]) -> None: + def load(sources_dir: Path, remote: Optional[RemoteSource], patch: Optional[str]) -> None: """ fetch sources from remote and apply patches Args: sources_dir(Path): local path to fetch - remote(str): remote target (from where to fetch) + remote(Optional[RemoteSource]): remote target (from where to fetch) patch(Optional[str]): optional patch to be applied """ Sources.fetch(sources_dir, remote) @@ -145,6 +157,21 @@ class Sources: return Sources.patch_apply(sources_dir, patch) + @staticmethod + def move(pkgbuild_dir: Path, sources_dir: Path) -> None: + """ + move content from pkgbuild_dir to sources_dir + + Args: + pkgbuild_dir(Path): path to directory with pkgbuild from which need to move + sources_dir(Path): path to target directory + """ + if pkgbuild_dir == sources_dir: + return # directories are the same, no need to move + for src in walk(pkgbuild_dir): + dst = sources_dir / src.relative_to(pkgbuild_dir) + shutil.move(src, dst) + @staticmethod def patch_apply(sources_dir: Path, patch: str) -> None: """ diff --git a/src/ahriman/core/build_tools/task.py b/src/ahriman/core/build_tools/task.py index 45625d29..1ce93f20 100644 --- a/src/ahriman/core/build_tools/task.py +++ b/src/ahriman/core/build_tools/task.py @@ -107,4 +107,4 @@ class Task: if self.paths.cache_for(self.package.base).is_dir(): # no need to clone whole repository, just copy from cache first shutil.copytree(self.paths.cache_for(self.package.base), path, dirs_exist_ok=True) - Sources.load(path, self.package.git_url, database.patches_get(self.package.base)) + Sources.load(path, self.package.remote, database.patches_get(self.package.base)) diff --git a/src/ahriman/core/database/data/__init__.py b/src/ahriman/core/database/data/__init__.py index 8624672a..fc7597be 100644 --- a/src/ahriman/core/database/data/__init__.py +++ b/src/ahriman/core/database/data/__init__.py @@ -20,6 +20,7 @@ from sqlite3 import Connection from ahriman.core.configuration import Configuration +from ahriman.core.database.data.package_remotes import migrate_package_remotes from ahriman.core.database.data.package_statuses import migrate_package_statuses from ahriman.core.database.data.patches import migrate_patches from ahriman.core.database.data.users import migrate_users_data @@ -43,3 +44,5 @@ def migrate_data( migrate_package_statuses(connection, repository_paths) migrate_patches(connection, repository_paths) migrate_users_data(connection, configuration) + if result.old_version <= 1: + migrate_package_remotes(connection, repository_paths) diff --git a/src/ahriman/core/database/data/package_remotes.py b/src/ahriman/core/database/data/package_remotes.py new file mode 100644 index 00000000..5624159c --- /dev/null +++ b/src/ahriman/core/database/data/package_remotes.py @@ -0,0 +1,61 @@ +# +# Copyright (c) 2021-2022 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 . +# +from sqlite3 import Connection + +from ahriman.models.package_source import PackageSource +from ahriman.models.remote_source import RemoteSource +from ahriman.models.repository_paths import RepositoryPaths + + +# pylint: disable=protected-access +def migrate_package_remotes(connection: Connection, paths: RepositoryPaths) -> None: + """ + perform migration for package remote sources + + Args: + connection(Connection): database connection + paths(RepositoryPaths): repository paths instance + """ + from ahriman.core.database.operations.package_operations import PackageOperations + + def insert_remote(base: str, remote: RemoteSource) -> None: + connection.execute( + """ + update package_bases set + branch = :branch, git_url = :git_url, path = :path, + web_url = :web_url, source = :source + where package_base = :package_base + """, + dict( + package_base=base, + branch=remote.branch, git_url=remote.git_url, path=remote.path, + web_url=remote.web_url, source=remote.source + ) + ) + + packages = PackageOperations._packages_get_select_package_bases(connection) + for package_base, package in packages.items(): + local_cache = paths.cache_for(package_base) + if local_cache.exists() and not package.is_vcs: + continue # skip packages which are not VCS and with local cache + remote_source = RemoteSource.from_remote(PackageSource.AUR, package_base, "aur") + if remote_source is None: + continue # should never happen + insert_remote(package_base, remote_source) diff --git a/src/ahriman/core/database/data/package_statuses.py b/src/ahriman/core/database/data/package_statuses.py index 98f3bd44..27777f31 100644 --- a/src/ahriman/core/database/data/package_statuses.py +++ b/src/ahriman/core/database/data/package_statuses.py @@ -42,7 +42,7 @@ def migrate_package_statuses(connection: Connection, paths: RepositoryPaths) -> values (:package_base, :version, :aur_url) """, - dict(package_base=metadata.base, version=metadata.version, aur_url=metadata.aur_url)) + dict(package_base=metadata.base, version=metadata.version, aur_url="")) connection.execute( """ insert into package_statuses diff --git a/src/ahriman/core/database/migrations/m001_package_source.py b/src/ahriman/core/database/migrations/m001_package_source.py new file mode 100644 index 00000000..6c37f7a5 --- /dev/null +++ b/src/ahriman/core/database/migrations/m001_package_source.py @@ -0,0 +1,40 @@ +# +# Copyright (c) 2021-2022 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 . +# + +steps = [ + """ + alter table package_bases add column branch text + """, + """ + alter table package_bases add column git_url text + """, + """ + alter table package_bases add column path text + """, + """ + alter table package_bases add column web_url text + """, + """ + alter table package_bases add column source text + """, + """ + alter table package_bases drop column aur_url + """, +] diff --git a/src/ahriman/core/database/operations/package_operations.py b/src/ahriman/core/database/operations/package_operations.py index 421bba3e..5d52f254 100644 --- a/src/ahriman/core/database/operations/package_operations.py +++ b/src/ahriman/core/database/operations/package_operations.py @@ -24,6 +24,7 @@ from ahriman.core.database.operations.operations import Operations from ahriman.models.build_status import BuildStatus from ahriman.models.package import Package from ahriman.models.package_description import PackageDescription +from ahriman.models.remote_source import RemoteSource class PackageOperations(Operations): @@ -75,13 +76,22 @@ class PackageOperations(Operations): connection.execute( """ insert into package_bases - (package_base, version, aur_url) + (package_base, version, source, branch, git_url, path, web_url) values - (:package_base, :version, :aur_url) + (:package_base, :version, :source, :branch, :git_url, :path, :web_url) on conflict (package_base) do update set - version = :version, aur_url = :aur_url + version = :version, branch = :branch, git_url = :git_url, path = :path, web_url = :web_url, source = :source """, - dict(package_base=package.base, version=package.version, aur_url=package.aur_url)) + dict( + package_base=package.base, + version=package.version, + branch=package.remote.branch if package.remote is not None else None, + git_url=package.remote.git_url if package.remote is not None else None, + path=package.remote.path if package.remote is not None else None, + web_url=package.remote.web_url if package.remote is not None else None, + source=package.remote.source.value if package.remote is not None else None, + ) + ) @staticmethod def _package_update_insert_packages(connection: Connection, package: Package) -> None: @@ -144,7 +154,7 @@ class PackageOperations(Operations): Dict[str, Package]: map of the package base to its descriptor (without packages themselves) """ return { - row["package_base"]: Package(row["package_base"], row["version"], row["aur_url"], {}) + row["package_base"]: Package(row["package_base"], row["version"], RemoteSource.from_json(row), {}) for row in connection.execute("""select * from package_bases""") } @@ -225,3 +235,28 @@ class PackageOperations(Operations): yield package, statuses.get(package_base, BuildStatus()) return self.with_connection(lambda connection: list(run(connection))) + + def remote_update(self, package: Package) -> None: + """ + update package remote source + + Args: + package(Package): package properties + """ + return self.with_connection( + lambda connection: self._package_update_insert_base(connection, package), + commit=True) + + def remotes_get(self) -> Dict[str, RemoteSource]: + """ + get packages remotes based on current settings + + Returns: + Dict[str, RemoteSource]: map of package base to its remote sources + """ + packages = self.with_connection(self._packages_get_select_package_bases) + return { + package_base: package.remote + for package_base, package in packages.items() + if package.remote is not None + } diff --git a/src/ahriman/core/database/sqlite.py b/src/ahriman/core/database/sqlite.py index ebe61e1b..d08a60a4 100644 --- a/src/ahriman/core/database/sqlite.py +++ b/src/ahriman/core/database/sqlite.py @@ -81,5 +81,5 @@ class SQLite(AuthOperations, BuildOperations, PackageOperations, PatchOperations paths = configuration.repository_paths - self.with_connection(lambda conn: Migrations.migrate(conn, configuration)) + self.with_connection(lambda connection: Migrations.migrate(connection, configuration)) paths.chown(self.path) diff --git a/src/ahriman/core/repository/repository.py b/src/ahriman/core/repository/repository.py index 8b64dcde..b77a349f 100644 --- a/src/ahriman/core/repository/repository.py +++ b/src/ahriman/core/repository/repository.py @@ -24,7 +24,6 @@ from ahriman.core.repository.executor import Executor from ahriman.core.repository.update_handler import UpdateHandler from ahriman.core.util import package_like from ahriman.models.package import Package -from ahriman.models.package_source import PackageSource class Repository(Executor, UpdateHandler): @@ -42,11 +41,15 @@ class Repository(Executor, UpdateHandler): Returns: List[Package]: list of read packages """ + sources = self.database.remotes_get() + result: Dict[str, Package] = {} # we are iterating over bases, not single packages for full_path in packages: try: - local = Package.load(str(full_path), PackageSource.Archive, self.pacman, self.aur_url) + local = Package.from_archive(full_path, self.pacman, None) + local.remote = sources.get(local.base) + current = result.setdefault(local.base, local) if current.version != local.version: # force version to max of them @@ -95,5 +98,5 @@ class Repository(Executor, UpdateHandler): return [ package for package in packages - if depends_on is None or depends_on.intersection(package.full_depends(self.pacman, packages)) + if depends_on.intersection(package.full_depends(self.pacman, packages)) ] diff --git a/src/ahriman/core/repository/repository_properties.py b/src/ahriman/core/repository/repository_properties.py index 119091c6..b712729d 100644 --- a/src/ahriman/core/repository/repository_properties.py +++ b/src/ahriman/core/repository/repository_properties.py @@ -35,7 +35,6 @@ class RepositoryProperties: Attributes: architecture(str): repository architecture - aur_url(str): base AUR url configuration(Configuration): configuration instance database(SQLite): database instance ignore_list(List[str]): package bases which will be ignored during auto updates @@ -65,7 +64,6 @@ class RepositoryProperties: self.configuration = configuration self.database = database - self.aur_url = configuration.get("alpm", "aur_url") self.name = configuration.get("repository", "name") self.paths = configuration.repository_paths diff --git a/src/ahriman/core/repository/update_handler.py b/src/ahriman/core/repository/update_handler.py index c9280d50..3f635ff1 100644 --- a/src/ahriman/core/repository/update_handler.py +++ b/src/ahriman/core/repository/update_handler.py @@ -62,13 +62,18 @@ class UpdateHandler(Cleaner): continue if filter_packages and local.base not in filter_packages: continue + source = local.remote.source if local.remote is not None else None try: - remote = Package.load(local.base, PackageSource.AUR, self.pacman, self.aur_url) + if source == PackageSource.Repository: + remote = Package.from_official(local.base, self.pacman) + else: + remote = Package.from_aur(local.base, self.pacman) if local.is_outdated(remote, self.paths): self.reporter.set_pending(local.base) result.append(remote) - self.reporter.set_success(local) + else: + self.reporter.set_success(local) except Exception: self.reporter.set_failed(local.base) self.logger.exception("could not load remote package %s", local.base) @@ -89,7 +94,7 @@ class UpdateHandler(Cleaner): for dirname in self.paths.cache.iterdir(): try: Sources.fetch(dirname, remote=None) - remote = Package.load(str(dirname), PackageSource.Local, self.pacman, self.aur_url) + remote = Package.from_build(dirname) local = packages.get(remote.base) if local is None: diff --git a/src/ahriman/core/tree.py b/src/ahriman/core/tree.py index eb2c5fcb..75eaad4f 100644 --- a/src/ahriman/core/tree.py +++ b/src/ahriman/core/tree.py @@ -70,7 +70,7 @@ class Leaf: Leaf: loaded class """ with tmpdir() as clone_dir: - Sources.load(clone_dir, package.git_url, database.patches_get(package.base)) + Sources.load(clone_dir, package.remote, database.patches_get(package.base)) dependencies = Package.dependencies(clone_dir) return cls(package, dependencies) diff --git a/src/ahriman/models/action.py b/src/ahriman/models/action.py index cc4cff28..1b66e60a 100644 --- a/src/ahriman/models/action.py +++ b/src/ahriman/models/action.py @@ -20,7 +20,7 @@ from enum import Enum -class Action(Enum): +class Action(str, Enum): """ base action enumeration diff --git a/src/ahriman/models/aur_package.py b/src/ahriman/models/aur_package.py index 5cd01d3b..aabab4ab 100644 --- a/src/ahriman/models/aur_package.py +++ b/src/ahriman/models/aur_package.py @@ -23,6 +23,7 @@ import datetime import inflection from dataclasses import dataclass, field, fields +from pyalpm import Package # type: ignore from typing import Any, Callable, Dict, List, Optional, Type from ahriman.core.util import filter_json, full_version @@ -47,6 +48,7 @@ class AURPackage: first_submitted(datetime.datetime): timestamp of the first package submission last_modified(datetime.datetime): timestamp of the last package submission url_path(str): AUR package path + repository(str): repository name of the package depends(List[str]): list of package dependencies make_depends(List[str]): list of package make dependencies opt_depends(List[str]): list of package optional dependencies @@ -70,6 +72,7 @@ class AURPackage: url: Optional[str] = None out_of_date: Optional[datetime.datetime] = None maintainer: Optional[str] = None + repository: str = "aur" depends: List[str] = field(default_factory=list) make_depends: List[str] = field(default_factory=list) opt_depends: List[str] = field(default_factory=list) @@ -94,6 +97,42 @@ class AURPackage: properties = cls.convert(dump) return cls(**filter_json(properties, known_fields)) + @classmethod + def from_pacman(cls: Type[AURPackage], package: Package) -> AURPackage: + """ + construct package descriptor from official repository wrapper + + Args: + package(Package): pyalpm package descriptor + + Returns: + AURPackage: AUR package descriptor + """ + return cls( + id=0, + name=package.name, + package_base_id=0, + package_base=package.base, + version=package.version, + description=package.desc, + num_votes=0, + popularity=0.0, + first_submitted=datetime.datetime.utcfromtimestamp(0), + last_modified=datetime.datetime.utcfromtimestamp(package.builddate), + url_path="", + url=package.url, + out_of_date=None, + maintainer=None, + repository=package.db.name, + depends=package.depends, + make_depends=package.makedepends, + opt_depends=package.optdepends, + conflicts=package.conflicts, + provides=package.provides, + license=package.licenses, + keywords=[], + ) + @classmethod def from_repo(cls: Type[AURPackage], dump: Dict[str, Any]) -> AURPackage: """ @@ -122,6 +161,7 @@ class AURPackage: dump["flag_date"], "%Y-%m-%dT%H:%M:%S.%fZ") if dump["flag_date"] is not None else None, maintainer=next(iter(dump["maintainers"]), None), + repository=dump["repo"], depends=dump["depends"], make_depends=dump["makedepends"], opt_depends=dump["optdepends"], diff --git a/src/ahriman/models/auth_settings.py b/src/ahriman/models/auth_settings.py index d698b91e..a0892aea 100644 --- a/src/ahriman/models/auth_settings.py +++ b/src/ahriman/models/auth_settings.py @@ -23,7 +23,7 @@ from enum import Enum from typing import Type -class AuthSettings(Enum): +class AuthSettings(str, Enum): """ web authorization type diff --git a/src/ahriman/models/build_status.py b/src/ahriman/models/build_status.py index dad122a9..b04a694f 100644 --- a/src/ahriman/models/build_status.py +++ b/src/ahriman/models/build_status.py @@ -28,7 +28,7 @@ from typing import Any, Dict, Type from ahriman.core.util import filter_json, pretty_datetime -class BuildStatusEnum(Enum): +class BuildStatusEnum(str, Enum): """ build status enumeration diff --git a/src/ahriman/models/package.py b/src/ahriman/models/package.py index 0e80ed50..8a0b28fe 100644 --- a/src/ahriman/models/package.py +++ b/src/ahriman/models/package.py @@ -26,15 +26,17 @@ from dataclasses import asdict, dataclass from pathlib import Path from pyalpm import vercmp # type: ignore from srcinfo.parse import parse_srcinfo # type: ignore -from typing import Any, Dict, Iterable, List, Set, Type +from typing import Any, Dict, Iterable, List, Optional, Set, Type from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.remote.aur import AUR from ahriman.core.alpm.remote.official import Official +from ahriman.core.alpm.remote.official_syncdb import OfficialSyncdb from ahriman.core.exceptions import InvalidPackageInfo from ahriman.core.util import check_output, full_version from ahriman.models.package_description import PackageDescription from ahriman.models.package_source import PackageSource +from ahriman.models.remote_source import RemoteSource from ahriman.models.repository_paths import RepositoryPaths @@ -44,15 +46,16 @@ class Package: package properties representation Attributes: - aur_url(str): AUR root url base(str): package base name - packages(Dict[str, PackageDescription): map of package names to their properties. Filled only on load from archive + packages(Dict[str, PackageDescription): map of package names to their properties. + Filled only on load from archive + remote(Optional[RemoteSource]): package remote source if applicable version(str): package full version """ base: str version: str - aur_url: str + remote: Optional[RemoteSource] packages: Dict[str, PackageDescription] _check_output = check_output @@ -67,16 +70,6 @@ class Package: """ return sorted(set(sum([package.depends for package in self.packages.values()], start=[]))) - @property - def git_url(self) -> str: - """ - get git clone url - - Returns: - str: package git url to clone - """ - return f"{self.aur_url}/{self.base}.git" - @property def groups(self) -> List[str]: """ @@ -122,56 +115,46 @@ class Package: """ return sorted(set(sum([package.licenses for package in self.packages.values()], start=[]))) - @property - def web_url(self) -> str: - """ - get package url which can be used to see package in web - - Returns: - str: package AUR url - """ - return f"{self.aur_url}/packages/{self.base}" - @classmethod - def from_archive(cls: Type[Package], path: Path, pacman: Pacman, aur_url: str) -> Package: + def from_archive(cls: Type[Package], path: Path, pacman: Pacman, remote: Optional[RemoteSource]) -> Package: """ construct package properties from package archive Args: path(Path): path to package archive pacman(Pacman): alpm wrapper instance - aur_url(str): AUR root url + remote(RemoteSource): package remote source if applicable Returns: Package: package properties """ package = pacman.handle.load_pkg(str(path)) - return cls(package.base, package.version, aur_url, - {package.name: PackageDescription.from_package(package, path)}) + description = PackageDescription.from_package(package, path) + return cls(package.base, package.version, remote, {package.name: description}) @classmethod - def from_aur(cls: Type[Package], name: str, aur_url: str) -> Package: + def from_aur(cls: Type[Package], name: str, pacman: Pacman) -> Package: """ construct package properties from AUR page Args: name(str): package name (either base or normal name) - aur_url(str): AUR root url + pacman(Pacman): alpm wrapper instance Returns: Package: package properties """ - package = AUR.info(name) - return cls(package.package_base, package.version, aur_url, {package.name: PackageDescription()}) + package = AUR.info(name, pacman=pacman) + remote = RemoteSource.from_remote(PackageSource.AUR, package.package_base, package.repository) + return cls(package.package_base, package.version, remote, {package.name: PackageDescription()}) @classmethod - def from_build(cls: Type[Package], path: Path, aur_url: str) -> Package: + def from_build(cls: Type[Package], path: Path) -> Package: """ construct package properties from sources directory Args: path(Path): path to package sources directory - aur_url(str): AUR root url Returns: Package: package properties @@ -179,13 +162,14 @@ class Package: Raises: InvalidPackageInfo: if there are parsing errors """ - srcinfo, errors = parse_srcinfo((path / ".SRCINFO").read_text()) + srcinfo_source = Package._check_output("makepkg", "--printsrcinfo", exception=None, cwd=path) + srcinfo, errors = parse_srcinfo(srcinfo_source) if errors: raise InvalidPackageInfo(errors) packages = {key: PackageDescription() for key in srcinfo["packages"]} version = full_version(srcinfo.get("epoch"), srcinfo["pkgver"], srcinfo["pkgrel"]) - return cls(srcinfo["pkgbase"], version, aur_url, packages) + return cls(srcinfo["pkgbase"], version, None, packages) @classmethod def from_json(cls: Type[Package], dump: Dict[str, Any]) -> Package: @@ -202,59 +186,29 @@ class Package: key: PackageDescription.from_json(value) for key, value in dump.get("packages", {}).items() } - return Package( + remote = dump.get("remote", {}) + return cls( base=dump["base"], version=dump["version"], - aur_url=dump["aur_url"], + remote=RemoteSource.from_json(remote), packages=packages) @classmethod - def from_official(cls: Type[Package], name: str, aur_url: str) -> Package: + def from_official(cls: Type[Package], name: str, pacman: Pacman, use_syncdb: bool = True) -> Package: """ construct package properties from official repository page Args: name(str): package name (either base or normal name) - aur_url(str): AUR root url + pacman(Pacman): alpm wrapper instance + use_syncdb(bool): use pacman databases instead of official repositories RPC (Default value = True) Returns: Package: package properties """ - package = Official.info(name) - return cls(package.package_base, package.version, aur_url, {package.name: PackageDescription()}) - - @classmethod - def load(cls: Type[Package], package: str, source: PackageSource, pacman: Pacman, aur_url: str) -> Package: - """ - package constructor from available sources - - Args: - package(str): one of path to sources directory, path to archive or package name/base - source(PackageSource): source of the package required to define the load method - pacman(Pacman): alpm wrapper instance (required to load from archive) - aur_url(str): AUR root url - - Returns: - Package: package properties - - Raises: - InvalidPackageInfo: if supplied package source is not valid - """ - try: - resolved_source = source.resolve(package) - if resolved_source == PackageSource.Archive: - return cls.from_archive(Path(package), pacman, aur_url) - if resolved_source == PackageSource.AUR: - return cls.from_aur(package, aur_url) - if resolved_source == PackageSource.Local: - return cls.from_build(Path(package), aur_url) - if resolved_source == PackageSource.Repository: - return cls.from_official(package, aur_url) - raise InvalidPackageInfo(f"Unsupported local package source {resolved_source}") - except InvalidPackageInfo: - raise - except Exception as e: - raise InvalidPackageInfo(str(e)) + package = OfficialSyncdb.info(name, pacman=pacman) if use_syncdb else Official.info(name, pacman=pacman) + remote = RemoteSource.from_remote(PackageSource.Repository, package.package_base, package.repository) + return cls(package.package_base, package.version, remote, {package.name: PackageDescription()}) @staticmethod def dependencies(path: Path) -> Set[str]: @@ -279,7 +233,8 @@ class Package: package_name = package_name.split(symbol)[0] return package_name - srcinfo, errors = parse_srcinfo((path / ".SRCINFO").read_text()) + srcinfo_source = Package._check_output("makepkg", "--printsrcinfo", exception=None, cwd=path) + srcinfo, errors = parse_srcinfo(srcinfo_source) if errors: raise InvalidPackageInfo(errors) makedepends = extract_packages(srcinfo.get("makedepends", [])) @@ -310,7 +265,7 @@ class Package: from ahriman.core.build_tools.sources import Sources logger = logging.getLogger("build_details") - Sources.load(paths.cache_for(self.base), self.git_url, None) + Sources.load(paths.cache_for(self.base), self.remote, None) try: # update pkgver first diff --git a/src/ahriman/models/package_source.py b/src/ahriman/models/package_source.py index 4b27adb0..71a025f0 100644 --- a/src/ahriman/models/package_source.py +++ b/src/ahriman/models/package_source.py @@ -26,7 +26,7 @@ from urllib.parse import urlparse from ahriman.core.util import package_like -class PackageSource(Enum): +class PackageSource(str, Enum): """ package source for addition enumeration diff --git a/src/ahriman/models/remote_source.py b/src/ahriman/models/remote_source.py new file mode 100644 index 00000000..a72f4374 --- /dev/null +++ b/src/ahriman/models/remote_source.py @@ -0,0 +1,124 @@ +# +# Copyright (c) 2021-2022 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 . +# +from __future__ import annotations + +from dataclasses import asdict, dataclass, fields +from pathlib import Path +from typing import Any, Dict, Optional, Type + +from ahriman.core.util import filter_json +from ahriman.models.package_source import PackageSource + + +@dataclass +class RemoteSource: + """ + remote package source properties + + Attributes: + branch(str): branch of the git repository + git_url(str): url of the git repository + path(str): path to directory with PKGBUILD inside the git repository + source(PackageSource): package source pointer used by some parsers + web_url(str): url of the package in the web interface + """ + + git_url: str + web_url: str + path: str + branch: str + source: PackageSource + + def __post_init__(self) -> None: + """ + convert source to enum type + """ + self.source = PackageSource(self.source) + + @property + def pkgbuild_dir(self) -> Path: + """ + get path to directory with package sources (PKGBUILD etc) + + Returns: + Path: path to directory with package sources based on settings + """ + return Path(self.path) + + @classmethod + def from_json(cls: Type[RemoteSource], dump: Dict[str, Any]) -> Optional[RemoteSource]: + """ + construct remote source from the json dump (or database row) + + Args: + dump(Dict[str, Any]): json dump body + + Returns: + Optional[RemoteSource]: remote source + """ + # filter to only known fields + known_fields = [pair.name for pair in fields(cls)] + dump = filter_json(dump, known_fields) + if dump: + return cls(**dump) + return None + + @classmethod + def from_remote(cls: Type[RemoteSource], source: PackageSource, package_base: str, + repository: str) -> Optional[RemoteSource]: + """ + generate remote source from the package base + + Args: + source(PackageSource): source of the package + package_base(str): package base + repository(str): repository name + + Returns: + Optional[RemoteSource]: generated remote source if any, None otherwise + """ + if source == PackageSource.AUR: + from ahriman.core.alpm.remote.aur import AUR + return RemoteSource( + git_url=AUR.remote_git_url(package_base, repository), + web_url=AUR.remote_web_url(package_base), + path=".", + branch="master", + source=source, + ) + if source == PackageSource.Repository: + from ahriman.core.alpm.remote.official import Official + return RemoteSource( + git_url=Official.remote_git_url(package_base, repository), + web_url=Official.remote_web_url(package_base), + path="trunk", + branch=f"packages/{package_base}", + source=source, + ) + return None + + def view(self) -> Dict[str, Any]: + """ + generate json package remote view + + Returns: + Dict[str, Any]: json-friendly dictionary + """ + return asdict(self) diff --git a/src/ahriman/models/report_settings.py b/src/ahriman/models/report_settings.py index e06e11f0..181e8764 100644 --- a/src/ahriman/models/report_settings.py +++ b/src/ahriman/models/report_settings.py @@ -23,7 +23,7 @@ from enum import Enum from typing import Type -class ReportSettings(Enum): +class ReportSettings(str, Enum): """ report targets enumeration diff --git a/src/ahriman/models/sign_settings.py b/src/ahriman/models/sign_settings.py index 93a55537..a09791d4 100644 --- a/src/ahriman/models/sign_settings.py +++ b/src/ahriman/models/sign_settings.py @@ -23,7 +23,7 @@ from enum import Enum from typing import Type -class SignSettings(Enum): +class SignSettings(str, Enum): """ sign targets enumeration diff --git a/src/ahriman/models/smtp_ssl_settings.py b/src/ahriman/models/smtp_ssl_settings.py index fb700495..eeba8f46 100644 --- a/src/ahriman/models/smtp_ssl_settings.py +++ b/src/ahriman/models/smtp_ssl_settings.py @@ -23,7 +23,7 @@ from enum import Enum from typing import Type -class SmtpSSLSettings(Enum): +class SmtpSSLSettings(str, Enum): """ SMTP SSL mode enumeration diff --git a/src/ahriman/models/upload_settings.py b/src/ahriman/models/upload_settings.py index 408e391d..3913f274 100644 --- a/src/ahriman/models/upload_settings.py +++ b/src/ahriman/models/upload_settings.py @@ -23,7 +23,7 @@ from enum import Enum from typing import Type -class UploadSettings(Enum): +class UploadSettings(str, Enum): """ remote synchronization targets enumeration diff --git a/src/ahriman/models/user_access.py b/src/ahriman/models/user_access.py index 7e013f27..cd58970f 100644 --- a/src/ahriman/models/user_access.py +++ b/src/ahriman/models/user_access.py @@ -20,7 +20,7 @@ from enum import Enum -class UserAccess(Enum): +class UserAccess(str, Enum): """ web user access enumeration diff --git a/src/ahriman/web/views/index.py b/src/ahriman/web/views/index.py index ddb6ab4b..7792bf53 100644 --- a/src/ahriman/web/views/index.py +++ b/src/ahriman/web/views/index.py @@ -86,7 +86,7 @@ class IndexView(BaseView): "status_color": status.status.bootstrap_color(), "timestamp": pretty_datetime(status.timestamp), "version": package.version, - "web_url": package.web_url, + "web_url": package.remote.web_url if package.remote is not None else None, } for package, status in sorted(self.service.packages, key=lambda item: item[0].base) ] service = { diff --git a/src/ahriman/web/views/service/search.py b/src/ahriman/web/views/service/search.py index dbb5fe62..ad6fb82c 100644 --- a/src/ahriman/web/views/service/search.py +++ b/src/ahriman/web/views/service/search.py @@ -50,7 +50,7 @@ class SearchView(BaseView): HTTPNotFound: if no packages found """ search: List[str] = self.request.query.getall("for", default=[]) - packages = AUR.multisearch(*search) + packages = AUR.multisearch(*search, pacman=self.service.repository.pacman) if not packages: raise HTTPNotFound(reason=f"No packages found for terms: {search}") diff --git a/tests/ahriman/application/application/test_application_packages.py b/tests/ahriman/application/application/test_application_packages.py index 8454de10..d41769d4 100644 --- a/tests/ahriman/application/application/test_application_packages.py +++ b/tests/ahriman/application/application/test_application_packages.py @@ -44,19 +44,21 @@ def test_add_aur(application_packages: ApplicationPackages, package_ahriman: Pac """ must add package from AUR """ - mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) + mocker.patch("ahriman.models.package.Package.from_aur", return_value=package_ahriman) load_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.load") dependencies_mock = mocker.patch( "ahriman.application.application.application_packages.ApplicationPackages._process_dependencies") build_queue_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.build_queue_insert") + update_remote_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.remote_update") application_packages._add_aur(package_ahriman.base, set(), False) load_mock.assert_called_once_with( pytest.helpers.anyvar(int), - package_ahriman.git_url, + package_ahriman.remote, pytest.helpers.anyvar(int)) dependencies_mock.assert_called_once_with(pytest.helpers.anyvar(int), set(), False) build_queue_mock.assert_called_once_with(package_ahriman) + update_remote_mock.assert_called_once_with(package_ahriman) def test_add_directory( @@ -80,7 +82,7 @@ def test_add_local(application_packages: ApplicationPackages, package_ahriman: P """ must add package from local sources """ - mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) + mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman) init_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.init") copytree_mock = mocker.patch("shutil.copytree") dependencies_mock = mocker.patch( @@ -112,6 +114,20 @@ def test_add_remote(application_packages: ApplicationPackages, package_descripti response_mock.raise_for_status.assert_called_once_with() +def test_add_repository(application_packages: ApplicationPackages, package_ahriman: Package, + mocker: MockerFixture) -> None: + """ + must add package from official repository + """ + mocker.patch("ahriman.models.package.Package.from_official", return_value=package_ahriman) + build_queue_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.build_queue_insert") + update_remote_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.remote_update") + + application_packages._add_repository(package_ahriman.base) + build_queue_mock.assert_called_once_with(package_ahriman) + update_remote_mock.assert_called_once_with(package_ahriman) + + def test_process_dependencies(application_packages: ApplicationPackages, mocker: MockerFixture) -> None: """ must process dependencies addition diff --git a/tests/ahriman/application/application/test_application_repository.py b/tests/ahriman/application/application/test_application_repository.py index 0231ccff..0132ae4a 100644 --- a/tests/ahriman/application/application/test_application_repository.py +++ b/tests/ahriman/application/application/test_application_repository.py @@ -14,7 +14,7 @@ def test_finalize(application_repository: ApplicationRepository) -> None: must raise NotImplemented for missing finalize method """ with pytest.raises(NotImplementedError): - application_repository._finalize([]) + application_repository._finalize(Result()) def test_clean_cache(application_repository: ApplicationRepository, mocker: MockerFixture) -> None: @@ -58,8 +58,8 @@ def test_report(application_repository: ApplicationRepository, mocker: MockerFix must generate report """ executor_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_report") - application_repository.report(["a"], []) - executor_mock.assert_called_once_with(["a"], []) + application_repository.report(["a"], Result()) + executor_mock.assert_called_once_with(["a"], Result()) def test_sign(application_repository: ApplicationRepository, package_ahriman: Package, package_python_schedule: Package, @@ -179,7 +179,6 @@ def test_update(application_repository: ApplicationRepository, package_ahriman: mocker.patch("ahriman.core.tree.Tree.load", return_value=tree) mocker.patch("ahriman.core.repository.repository.Repository.packages_built", return_value=paths) - mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) build_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_build", return_value=result) update_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_update", return_value=result) finalize_mock = mocker.patch( @@ -201,7 +200,6 @@ def test_update_empty(application_repository: ApplicationRepository, package_ahr mocker.patch("ahriman.core.tree.Tree.load", return_value=tree) mocker.patch("ahriman.core.repository.repository.Repository.packages_built", return_value=[]) - mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) mocker.patch("ahriman.core.repository.executor.Executor.process_build") update_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_update") diff --git a/tests/ahriman/application/handlers/test_handler_patch.py b/tests/ahriman/application/handlers/test_handler_patch.py index 5a100817..80b62f4b 100644 --- a/tests/ahriman/application/handlers/test_handler_patch.py +++ b/tests/ahriman/application/handlers/test_handler_patch.py @@ -1,6 +1,7 @@ import argparse import pytest +from pathlib import Path from pytest_mock import MockerFixture from ahriman.application.application import Application @@ -37,7 +38,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc application_mock = mocker.patch("ahriman.application.handlers.patch.Patch.patch_set_create") Patch.run(args, "x86_64", configuration, True, False) - application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package, args.track) + application_mock.assert_called_once_with(pytest.helpers.anyvar(int), Path(args.package), args.track) def test_run_list(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: @@ -96,11 +97,11 @@ def test_patch_set_create(application: Application, package_ahriman: Package, mo must create patch set for the package """ mocker.patch("pathlib.Path.mkdir") - mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) + mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman) mocker.patch("ahriman.core.build_tools.sources.Sources.patch_create", return_value="patch") create_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.patches_insert") - Patch.patch_set_create(application, "path", ["*.patch"]) + Patch.patch_set_create(application, Path("path"), ["*.patch"]) create_mock.assert_called_once_with(package_ahriman.base, "patch") diff --git a/tests/ahriman/application/handlers/test_handler_search.py b/tests/ahriman/application/handlers/test_handler_search.py index ed70a5c9..43976b70 100644 --- a/tests/ahriman/application/handlers/test_handler_search.py +++ b/tests/ahriman/application/handlers/test_handler_search.py @@ -34,6 +34,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, aur_package must run command """ args = _default_args(args) + mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") aur_search_mock = mocker.patch("ahriman.core.alpm.remote.aur.AUR.multisearch", return_value=[aur_package_ahriman]) official_search_mock = mocker.patch("ahriman.core.alpm.remote.official.Official.multisearch", return_value=[aur_package_ahriman]) @@ -41,8 +42,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, aur_package print_mock = mocker.patch("ahriman.core.formatters.printer.Printer.print") Search.run(args, "x86_64", configuration, True, False) - aur_search_mock.assert_called_once_with("ahriman") - official_search_mock.assert_called_once_with("ahriman") + aur_search_mock.assert_called_once_with("ahriman", pacman=pytest.helpers.anyvar(int)) + official_search_mock.assert_called_once_with("ahriman", pacman=pytest.helpers.anyvar(int)) check_mock.assert_called_once_with(False, False) print_mock.assert_has_calls([mock.call(False), mock.call(False)]) @@ -56,6 +57,7 @@ def test_run_empty_exception(args: argparse.Namespace, configuration: Configurat mocker.patch("ahriman.core.alpm.remote.aur.AUR.multisearch", return_value=[]) mocker.patch("ahriman.core.alpm.remote.official.Official.multisearch", return_value=[]) mocker.patch("ahriman.core.formatters.printer.Printer.print") + mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_if_empty") Search.run(args, "x86_64", configuration, True, False) @@ -70,6 +72,7 @@ def test_run_sort(args: argparse.Namespace, configuration: Configuration, aur_pa args = _default_args(args) mocker.patch("ahriman.core.alpm.remote.aur.AUR.multisearch", return_value=[aur_package_ahriman]) mocker.patch("ahriman.core.alpm.remote.official.Official.multisearch", return_value=[]) + mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") sort_mock = mocker.patch("ahriman.application.handlers.search.Search.sort") Search.run(args, "x86_64", configuration, True, False) @@ -88,6 +91,7 @@ def test_run_sort_by(args: argparse.Namespace, configuration: Configuration, aur args.sort_by = "field" mocker.patch("ahriman.core.alpm.remote.aur.AUR.multisearch", return_value=[aur_package_ahriman]) mocker.patch("ahriman.core.alpm.remote.official.Official.multisearch", return_value=[]) + mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") sort_mock = mocker.patch("ahriman.application.handlers.search.Search.sort") Search.run(args, "x86_64", configuration, True, False) diff --git a/tests/ahriman/conftest.py b/tests/ahriman/conftest.py index 8e0891a2..213f2c88 100644 --- a/tests/ahriman/conftest.py +++ b/tests/ahriman/conftest.py @@ -6,6 +6,7 @@ from pytest_mock import MockerFixture from typing import Any, Dict, Type, TypeVar from unittest.mock import MagicMock +from ahriman.core.alpm.pacman import Pacman from ahriman.core.auth.auth import Auth from ahriman.core.configuration import Configuration from ahriman.core.database.sqlite import SQLite @@ -15,6 +16,8 @@ from ahriman.models.aur_package import AURPackage from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.package import Package from ahriman.models.package_description import PackageDescription +from ahriman.models.package_source import PackageSource +from ahriman.models.remote_source import RemoteSource from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.result import Result from ahriman.models.user import User @@ -101,8 +104,8 @@ def aur_package_ahriman() -> AURPackage: description="ArcH Linux ReposItory MANager", num_votes=0, popularity=0, - first_submitted=datetime.datetime(2021, 4, 9, 22, 44, 45), - last_modified=datetime.datetime(2021, 12, 25, 23, 11, 11), + first_submitted=datetime.datetime.utcfromtimestamp(1618008285), + last_modified=datetime.datetime.utcfromtimestamp(1640473871), url_path="/cgit/aur.git/snapshot/ahriman.tar.gz", url="https://github.com/arcan1s/ahriman", out_of_date=None, @@ -155,13 +158,14 @@ def aur_package_akonadi() -> AURPackage: version="21.12.3-2", description="PIM layer, which provides an asynchronous API to access all kind of PIM data", num_votes=0, - popularity=0, - first_submitted=datetime.datetime(1970, 1, 1, 0, 0, 0), - last_modified=datetime.datetime(2022, 3, 6, 8, 39, 50, 610000), + popularity=0.0, + first_submitted=datetime.datetime.utcfromtimestamp(0), + last_modified=datetime.datetime.utcfromtimestamp(1646555990.610), url_path="", url="https://kontact.kde.org", out_of_date=None, maintainer="felixonmars", + repository="extra", depends=[ "libakonadi", "mariadb", @@ -245,7 +249,7 @@ def package_ahriman(package_description_ahriman: PackageDescription) -> Package: return Package( base="ahriman", version="1.7.0-1", - aur_url="https://aur.archlinux.org", + remote=RemoteSource.from_remote(PackageSource.AUR, "ahriman", "aur"), packages=packages) @@ -270,7 +274,7 @@ def package_python_schedule( return Package( base="python-schedule", version="1.0.0-2", - aur_url="https://aur.archlinux.org", + remote=RemoteSource.from_remote(PackageSource.AUR, "python-schedule", "aur"), packages=packages) @@ -344,6 +348,34 @@ def package_description_python2_schedule() -> PackageDescription: url="https://github.com/dbader/schedule") +@pytest.fixture +def pacman(configuration: Configuration) -> Pacman: + """ + fixture for pacman wrapper + + Args: + configuration(Configuration): configuration fixture + + Returns: + Pacman: pacman wrapper test instance + """ + return Pacman(configuration) + + +@pytest.fixture +def remote_source(package_ahriman: Package) -> RemoteSource: + """ + remote source fixture + + Args: + package_ahriman(Package): package fixture + + Returns: + RemoteSource: remote source test instance + """ + return RemoteSource.from_remote(PackageSource.AUR, "ahriman", "aur") + + @pytest.fixture def repository_paths(configuration: Configuration) -> RepositoryPaths: """ diff --git a/tests/ahriman/core/alpm/remote/conftest.py b/tests/ahriman/core/alpm/remote/conftest.py index 866bcaa5..072a6307 100644 --- a/tests/ahriman/core/alpm/remote/conftest.py +++ b/tests/ahriman/core/alpm/remote/conftest.py @@ -2,6 +2,7 @@ import pytest from ahriman.core.alpm.remote.aur import AUR from ahriman.core.alpm.remote.official import Official +from ahriman.core.alpm.remote.official_syncdb import OfficialSyncdb from ahriman.core.alpm.remote.remote import Remote @@ -27,6 +28,17 @@ def official() -> Official: return Official() +@pytest.fixture +def official_syncdb() -> OfficialSyncdb: + """ + official repository fixture with database processing + + Returns: + OfficialSyncdb: official repository with database processing helper instance + """ + return OfficialSyncdb() + + @pytest.fixture def remote() -> Remote: """ diff --git a/tests/ahriman/core/alpm/remote/test_aur.py b/tests/ahriman/core/alpm/remote/test_aur.py index 4eab4873..fa87a027 100644 --- a/tests/ahriman/core/alpm/remote/test_aur.py +++ b/tests/ahriman/core/alpm/remote/test_aur.py @@ -6,6 +6,7 @@ from pathlib import Path from pytest_mock import MockerFixture from unittest.mock import MagicMock +from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.remote.aur import AUR from ahriman.core.exceptions import InvalidPackageInfo from ahriman.models.aur_package import AURPackage @@ -49,6 +50,23 @@ def test_parse_response_unknown_error() -> None: AUR.parse_response({"type": "error"}) +def test_remote_git_url(aur_package_ahriman: AURPackage) -> None: + """ + must generate package git url + """ + git_url = AUR.remote_git_url(aur_package_ahriman.package_base, aur_package_ahriman.repository) + assert git_url.endswith(".git") + assert git_url.startswith(AUR.DEFAULT_AUR_URL) + + +def test_remote_web_url(aur_package_ahriman: AURPackage) -> None: + """ + must generate package git url + """ + web_url = AUR.remote_web_url(aur_package_ahriman.package_base) + assert web_url.startswith(AUR.DEFAULT_AUR_URL) + + def test_make_request(aur: AUR, aur_package_ahriman: AURPackage, mocker: MockerFixture, resource_path_root: Path) -> None: """ @@ -109,19 +127,19 @@ def test_make_request_failed_http_error(aur: AUR, mocker: MockerFixture) -> None aur.make_request("info", "ahriman") -def test_package_info(aur: AUR, aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None: +def test_package_info(aur: AUR, aur_package_ahriman: AURPackage, pacman: Pacman, mocker: MockerFixture) -> None: """ must make request for info """ request_mock = mocker.patch("ahriman.core.alpm.remote.aur.AUR.make_request", return_value=[aur_package_ahriman]) - assert aur.package_info(aur_package_ahriman.name) == aur_package_ahriman + assert aur.package_info(aur_package_ahriman.name, pacman=pacman) == aur_package_ahriman request_mock.assert_called_once_with("info", aur_package_ahriman.name) -def test_package_search(aur: AUR, aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None: +def test_package_search(aur: AUR, aur_package_ahriman: AURPackage, pacman: Pacman, mocker: MockerFixture) -> None: """ must make request for search """ request_mock = mocker.patch("ahriman.core.alpm.remote.aur.AUR.make_request", return_value=[aur_package_ahriman]) - assert aur.package_search(aur_package_ahriman.name) == [aur_package_ahriman] + assert aur.package_search(aur_package_ahriman.name, pacman=pacman) == [aur_package_ahriman] request_mock.assert_called_once_with("search", aur_package_ahriman.name, by="name-desc") diff --git a/tests/ahriman/core/alpm/remote/test_official.py b/tests/ahriman/core/alpm/remote/test_official.py index c3a6fcb4..b453d91d 100644 --- a/tests/ahriman/core/alpm/remote/test_official.py +++ b/tests/ahriman/core/alpm/remote/test_official.py @@ -6,6 +6,7 @@ from pathlib import Path from pytest_mock import MockerFixture from unittest.mock import MagicMock +from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.remote.official import Official from ahriman.core.exceptions import InvalidPackageInfo from ahriman.models.aur_package import AURPackage @@ -41,6 +42,38 @@ def test_parse_response_unknown_error(resource_path_root: Path) -> None: Official.parse_response(json.loads(response)) +def test_remote_git_url(aur_package_akonadi: AURPackage) -> None: + """ + must generate package git url for core packages + """ + git_urls = [ + Official.remote_git_url(aur_package_akonadi.package_base, repository) + for repository in ("core", "extra", "Core", "Extra") + ] + assert all(git_url.endswith("svntogit-packages.git") for git_url in git_urls) + assert len(set(git_urls)) == 1 + + +def test_remote_git_url_community(aur_package_akonadi: AURPackage) -> None: + """ + must generate package git url for core packages + """ + git_urls = [ + Official.remote_git_url(aur_package_akonadi.package_base, repository) + for repository in ("community", "multilib", "Community", "Multilib") + ] + assert all(git_url.endswith("svntogit-community.git") for git_url in git_urls) + assert len(set(git_urls)) == 1 + + +def test_remote_web_url(aur_package_akonadi: AURPackage) -> None: + """ + must generate package git url + """ + web_url = Official.remote_web_url(aur_package_akonadi.package_base) + assert web_url.startswith(Official.DEFAULT_ARCHLINUX_URL) + + def test_make_request(official: Official, aur_package_akonadi: AURPackage, mocker: MockerFixture, resource_path_root: Path) -> None: """ @@ -51,7 +84,8 @@ def test_make_request(official: Official, aur_package_akonadi: AURPackage, request_mock = mocker.patch("requests.get", return_value=response_mock) assert official.make_request("akonadi", by="q") == [aur_package_akonadi] - request_mock.assert_called_once_with("https://archlinux.org/packages/search/json", params={"q": ("akonadi",)}) + request_mock.assert_called_once_with("https://archlinux.org/packages/search/json", + params={"q": ("akonadi",), "repo": Official.DEFAULT_SEARCH_REPOSITORIES}) def test_make_request_failed(official: Official, mocker: MockerFixture) -> None: @@ -72,21 +106,23 @@ def test_make_request_failed_http_error(official: Official, mocker: MockerFixtur official.make_request("akonadi", by="q") -def test_package_info(official: Official, aur_package_akonadi: AURPackage, mocker: MockerFixture) -> None: +def test_package_info(official: Official, aur_package_akonadi: AURPackage, pacman: Pacman, + mocker: MockerFixture) -> None: """ must make request for info """ request_mock = mocker.patch("ahriman.core.alpm.remote.official.Official.make_request", return_value=[aur_package_akonadi]) - assert official.package_info(aur_package_akonadi.name) == aur_package_akonadi + assert official.package_info(aur_package_akonadi.name, pacman=pacman) == aur_package_akonadi request_mock.assert_called_once_with(aur_package_akonadi.name, by="name") -def test_package_search(official: Official, aur_package_akonadi: AURPackage, mocker: MockerFixture) -> None: +def test_package_search(official: Official, aur_package_akonadi: AURPackage, pacman: Pacman, + mocker: MockerFixture) -> None: """ must make request for search """ request_mock = mocker.patch("ahriman.core.alpm.remote.official.Official.make_request", return_value=[aur_package_akonadi]) - assert official.package_search(aur_package_akonadi.name) == [aur_package_akonadi] + assert official.package_search(aur_package_akonadi.name, pacman=pacman) == [aur_package_akonadi] request_mock.assert_called_once_with(aur_package_akonadi.name, by="q") diff --git a/tests/ahriman/core/alpm/remote/test_official_syncdb.py b/tests/ahriman/core/alpm/remote/test_official_syncdb.py new file mode 100644 index 00000000..0c00b66f --- /dev/null +++ b/tests/ahriman/core/alpm/remote/test_official_syncdb.py @@ -0,0 +1,18 @@ +from pytest_mock import MockerFixture + +from ahriman.core.alpm.pacman import Pacman +from ahriman.core.alpm.remote.official_syncdb import OfficialSyncdb +from ahriman.models.aur_package import AURPackage + + +def test_package_info(official_syncdb: OfficialSyncdb, aur_package_akonadi: AURPackage, + pacman: Pacman, mocker: MockerFixture) -> None: + """ + must return package info from the database + """ + mocker.patch("ahriman.models.aur_package.AURPackage.from_pacman", return_value=aur_package_akonadi) + get_mock = mocker.patch("ahriman.core.alpm.pacman.Pacman.get", return_value=[aur_package_akonadi]) + + package = official_syncdb.package_info(aur_package_akonadi.name, pacman=pacman) + get_mock.assert_called_once_with(aur_package_akonadi.name) + assert package == aur_package_akonadi diff --git a/tests/ahriman/core/alpm/remote/test_remote.py b/tests/ahriman/core/alpm/remote/test_remote.py index a23e84d7..e2a26fa0 100644 --- a/tests/ahriman/core/alpm/remote/test_remote.py +++ b/tests/ahriman/core/alpm/remote/test_remote.py @@ -3,70 +3,87 @@ import pytest from pytest_mock import MockerFixture from unittest import mock +from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.remote.remote import Remote from ahriman.models.aur_package import AURPackage -def test_info(mocker: MockerFixture) -> None: +def test_info(pacman: Pacman, mocker: MockerFixture) -> None: """ must call info method """ info_mock = mocker.patch("ahriman.core.alpm.remote.remote.Remote.package_info") - Remote.info("ahriman") - info_mock.assert_called_once_with("ahriman") + Remote.info("ahriman", pacman=pacman) + info_mock.assert_called_once_with("ahriman", pacman=pacman) -def test_multisearch(aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None: +def test_multisearch(aur_package_ahriman: AURPackage, pacman: Pacman, mocker: MockerFixture) -> None: """ must search in AUR with multiple words """ terms = ["ahriman", "is", "cool"] search_mock = mocker.patch("ahriman.core.alpm.remote.remote.Remote.search", return_value=[aur_package_ahriman]) - assert Remote.multisearch(*terms) == [aur_package_ahriman] - search_mock.assert_has_calls([mock.call("ahriman"), mock.call("cool")]) + assert Remote.multisearch(*terms, pacman=pacman) == [aur_package_ahriman] + search_mock.assert_has_calls([mock.call("ahriman", pacman=pacman), mock.call("cool", pacman=pacman)]) -def test_multisearch_empty(mocker: MockerFixture) -> None: +def test_multisearch_empty(pacman: Pacman, mocker: MockerFixture) -> None: """ must return empty list if no long terms supplied """ terms = ["it", "is"] search_mock = mocker.patch("ahriman.core.alpm.remote.remote.Remote.search") - assert Remote.multisearch(*terms) == [] + assert Remote.multisearch(*terms, pacman=pacman) == [] search_mock.assert_not_called() -def test_multisearch_single(aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None: +def test_multisearch_single(aur_package_ahriman: AURPackage, pacman: Pacman, mocker: MockerFixture) -> None: """ must search in AUR with one word """ search_mock = mocker.patch("ahriman.core.alpm.remote.remote.Remote.search", return_value=[aur_package_ahriman]) - assert Remote.multisearch("ahriman") == [aur_package_ahriman] - search_mock.assert_called_once_with("ahriman") + assert Remote.multisearch("ahriman", pacman=pacman) == [aur_package_ahriman] + search_mock.assert_called_once_with("ahriman", pacman=pacman) -def test_search(mocker: MockerFixture) -> None: +def test_remote_git_url(remote: Remote, pacman: Pacman) -> None: + """ + must raise NotImplemented for missing remote git url + """ + with pytest.raises(NotImplementedError): + remote.remote_git_url("package", "repositorys") + + +def test_remote_web_url(remote: Remote, pacman: Pacman) -> None: + """ + must raise NotImplemented for missing remote web url + """ + with pytest.raises(NotImplementedError): + remote.remote_web_url("package") + + +def test_search(pacman: Pacman, mocker: MockerFixture) -> None: """ must call search method """ search_mock = mocker.patch("ahriman.core.alpm.remote.remote.Remote.package_search") - Remote.search("ahriman") - search_mock.assert_called_once_with("ahriman") + Remote.search("ahriman", pacman=pacman) + search_mock.assert_called_once_with("ahriman", pacman=pacman) -def test_package_info(remote: Remote) -> None: +def test_package_info(remote: Remote, pacman: Pacman) -> None: """ must raise NotImplemented for missing package info method """ with pytest.raises(NotImplementedError): - remote.package_info("package") + remote.package_info("package", pacman=pacman) -def test_package_search(remote: Remote) -> None: +def test_package_search(remote: Remote, pacman: Pacman) -> None: """ must raise NotImplemented for missing package search method """ with pytest.raises(NotImplementedError): - remote.package_search("package") + remote.package_search("package", pacman=pacman) diff --git a/tests/ahriman/core/alpm/test_pacman.py b/tests/ahriman/core/alpm/test_pacman.py index 3823e3af..e2df983c 100644 --- a/tests/ahriman/core/alpm/test_pacman.py +++ b/tests/ahriman/core/alpm/test_pacman.py @@ -15,3 +15,17 @@ def test_all_packages_with_provides(pacman: Pacman) -> None: package list must contain provides packages """ assert "sh" in pacman.all_packages() + + +def test_get(pacman: Pacman) -> None: + """ + must retrieve package + """ + assert list(pacman.get("pacman")) + + +def test_get_empty(pacman: Pacman) -> None: + """ + must return empty packages list without exception + """ + assert not list(pacman.get("some-random-name")) diff --git a/tests/ahriman/core/build_tools/test_sources.py b/tests/ahriman/core/build_tools/test_sources.py index b7f559f2..ee14e57f 100644 --- a/tests/ahriman/core/build_tools/test_sources.py +++ b/tests/ahriman/core/build_tools/test_sources.py @@ -5,6 +5,7 @@ from pytest_mock import MockerFixture from unittest import mock from ahriman.core.build_tools.sources import Sources +from ahriman.models.remote_source import RemoteSource def test_add(mocker: MockerFixture) -> None: @@ -45,7 +46,7 @@ def test_diff(mocker: MockerFixture) -> None: exception=None, cwd=local, logger=pytest.helpers.anyvar(int)) -def test_fetch_empty(mocker: MockerFixture) -> None: +def test_fetch_empty(remote_source: RemoteSource, mocker: MockerFixture) -> None: """ must do nothing in case if no branches available """ @@ -53,46 +54,51 @@ def test_fetch_empty(mocker: MockerFixture) -> None: mocker.patch("ahriman.core.build_tools.sources.Sources.has_remotes", return_value=False) check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") - Sources.fetch(Path("local"), "remote") + Sources.fetch(Path("local"), remote_source) check_output_mock.assert_not_called() -def test_fetch_existing(mocker: MockerFixture) -> None: +def test_fetch_existing(remote_source: RemoteSource, mocker: MockerFixture) -> None: """ must fetch new package via fetch command """ mocker.patch("pathlib.Path.is_dir", return_value=True) mocker.patch("ahriman.core.build_tools.sources.Sources.has_remotes", return_value=True) check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") + move_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.move") local = Path("local") - Sources.fetch(local, "remote") + Sources.fetch(local, remote_source) check_output_mock.assert_has_calls([ - mock.call("git", "fetch", "origin", Sources._branch, + mock.call("git", "fetch", "origin", remote_source.branch, exception=None, cwd=local, logger=pytest.helpers.anyvar(int)), - mock.call("git", "checkout", "--force", Sources._branch, + mock.call("git", "checkout", "--force", remote_source.branch, exception=None, cwd=local, logger=pytest.helpers.anyvar(int)), - mock.call("git", "reset", "--hard", f"origin/{Sources._branch}", + mock.call("git", "reset", "--hard", f"origin/{remote_source.branch}", exception=None, cwd=local, logger=pytest.helpers.anyvar(int)) ]) + move_mock.assert_called_once_with(local.resolve(), local) -def test_fetch_new(mocker: MockerFixture) -> None: +def test_fetch_new(remote_source: RemoteSource, mocker: MockerFixture) -> None: """ must fetch new package via clone command """ mocker.patch("pathlib.Path.is_dir", return_value=False) check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") + move_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.move") local = Path("local") - Sources.fetch(local, "remote") + Sources.fetch(local, remote_source) check_output_mock.assert_has_calls([ - mock.call("git", "clone", "remote", str(local), exception=None, cwd=local, logger=pytest.helpers.anyvar(int)), - mock.call("git", "checkout", "--force", Sources._branch, + mock.call("git", "clone", "--branch", remote_source.branch, "--single-branch", + remote_source.git_url, str(local), exception=None, cwd=local, logger=pytest.helpers.anyvar(int)), + mock.call("git", "checkout", "--force", remote_source.branch, exception=None, cwd=local, logger=pytest.helpers.anyvar(int)), - mock.call("git", "reset", "--hard", f"origin/{Sources._branch}", + mock.call("git", "reset", "--hard", f"origin/{remote_source.branch}", exception=None, cwd=local, logger=pytest.helpers.anyvar(int)) ]) + move_mock.assert_called_once_with(local.resolve(), local) def test_fetch_new_without_remote(mocker: MockerFixture) -> None: @@ -101,15 +107,28 @@ def test_fetch_new_without_remote(mocker: MockerFixture) -> None: """ mocker.patch("pathlib.Path.is_dir", return_value=False) check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") + move_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.move") local = Path("local") Sources.fetch(local, None) check_output_mock.assert_has_calls([ - mock.call("git", "checkout", "--force", Sources._branch, + mock.call("git", "checkout", "--force", Sources.DEFAULT_BRANCH, exception=None, cwd=local, logger=pytest.helpers.anyvar(int)), - mock.call("git", "reset", "--hard", f"origin/{Sources._branch}", + mock.call("git", "reset", "--hard", f"origin/{Sources.DEFAULT_BRANCH}", exception=None, cwd=local, logger=pytest.helpers.anyvar(int)) ]) + move_mock.assert_called_once_with(local.resolve(), local) + + +def test_fetch_relative(remote_source: RemoteSource, mocker: MockerFixture) -> None: + """ + must process move correctly on relative directory + """ + mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") + move_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.move") + + Sources.fetch(Path("path"), remote_source) + move_mock.assert_called_once_with(Path("path").resolve(), Path("path")) def test_has_remotes(mocker: MockerFixture) -> None: @@ -140,33 +159,53 @@ def test_init(mocker: MockerFixture) -> None: local = Path("local") Sources.init(local) - check_output_mock.assert_called_once_with("git", "init", "--initial-branch", Sources._branch, + check_output_mock.assert_called_once_with("git", "init", "--initial-branch", Sources.DEFAULT_BRANCH, exception=None, cwd=local, logger=pytest.helpers.anyvar(int)) -def test_load(mocker: MockerFixture) -> None: +def test_load(remote_source: RemoteSource, mocker: MockerFixture) -> None: """ must load packages sources correctly """ fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch") patch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_apply") - Sources.load(Path("local"), "remote", "patch") - fetch_mock.assert_called_once_with(Path("local"), "remote") + Sources.load(Path("local"), remote_source, "patch") + fetch_mock.assert_called_once_with(Path("local"), remote_source) patch_mock.assert_called_once_with(Path("local"), "patch") -def test_load_no_patch(mocker: MockerFixture) -> None: +def test_load_no_patch(remote_source: RemoteSource, mocker: MockerFixture) -> None: """ must load packages sources correctly without patches """ mocker.patch("ahriman.core.build_tools.sources.Sources.fetch") patch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_apply") - Sources.load(Path("local"), "remote", None) + Sources.load(Path("local"), remote_source, None) patch_mock.assert_not_called() +def test_move(mocker: MockerFixture) -> None: + """ + must move content between directories + """ + mocker.patch("ahriman.core.build_tools.sources.walk", return_value=[Path("/source/path")]) + move_mock = mocker.patch("shutil.move") + + Sources.move(Path("/source"), Path("/destination")) + move_mock.assert_called_once_with(Path("/source/path"), Path("/destination/path")) + + +def test_move_same(mocker: MockerFixture) -> None: + """ + must not do anything in case if directories are the same + """ + walk_mock = mocker.patch("ahriman.core.build_tools.sources.walk") + Sources.move(Path("/same"), Path("/same")) + walk_mock.assert_not_called() + + def test_patch_apply(mocker: MockerFixture) -> None: """ must apply patches if any diff --git a/tests/ahriman/core/conftest.py b/tests/ahriman/core/conftest.py index abaac578..22c9b4f0 100644 --- a/tests/ahriman/core/conftest.py +++ b/tests/ahriman/core/conftest.py @@ -1,6 +1,5 @@ import pytest -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 @@ -37,20 +36,6 @@ def leaf_python_schedule(package_python_schedule: Package) -> Leaf: return Leaf(package_python_schedule, set()) -@pytest.fixture -def pacman(configuration: Configuration) -> Pacman: - """ - fixture for pacman wrapper - - Args: - configuration(Configuration): configuration fixture - - Returns: - Pacman: pacman wrapper test instance - """ - return Pacman(configuration) - - @pytest.fixture def repo(configuration: Configuration, repository_paths: RepositoryPaths) -> Repo: """ diff --git a/tests/ahriman/core/database/data/test_data_init.py b/tests/ahriman/core/database/data/test_data_init.py index a44e57fd..44016e8c 100644 --- a/tests/ahriman/core/database/data/test_data_init.py +++ b/tests/ahriman/core/database/data/test_data_init.py @@ -15,11 +15,24 @@ def test_migrate_data_initial(connection: Connection, configuration: Configurati packages = mocker.patch("ahriman.core.database.data.migrate_package_statuses") patches = mocker.patch("ahriman.core.database.data.migrate_patches") users = mocker.patch("ahriman.core.database.data.migrate_users_data") + remotes = mocker.patch("ahriman.core.database.data.migrate_package_remotes") migrate_data(MigrationResult(old_version=0, new_version=900), connection, configuration) packages.assert_called_once_with(connection, repository_paths) patches.assert_called_once_with(connection, repository_paths) users.assert_called_once_with(connection, configuration) + remotes.assert_called_once_with(connection, repository_paths) + + +def test_migrate_data_remotes(connection: Connection, configuration: Configuration, + repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: + """ + must perform initial migration + """ + remotes = mocker.patch("ahriman.core.database.data.migrate_package_remotes") + + migrate_data(MigrationResult(old_version=1, new_version=900), connection, configuration) + remotes.assert_called_once_with(connection, repository_paths) def test_migrate_data_skip(connection: Connection, configuration: Configuration, mocker: MockerFixture) -> None: diff --git a/tests/ahriman/core/database/data/test_package_remotes.py b/tests/ahriman/core/database/data/test_package_remotes.py new file mode 100644 index 00000000..fc4e473a --- /dev/null +++ b/tests/ahriman/core/database/data/test_package_remotes.py @@ -0,0 +1,66 @@ +import pytest + +from pytest_mock import MockerFixture +from sqlite3 import Connection + +from ahriman.core.database.data import migrate_package_remotes +from ahriman.models.package import Package +from ahriman.models.repository_paths import RepositoryPaths + + +def test_migrate_package_remotes(package_ahriman: Package, connection: Connection, repository_paths: RepositoryPaths, + mocker: MockerFixture) -> None: + """ + must put package remotes to database + """ + mocker.patch( + "ahriman.core.database.operations.package_operations.PackageOperations._packages_get_select_package_bases", + return_value={package_ahriman.base: package_ahriman}) + mocker.patch("pathlib.Path.exists", return_value=False) + + migrate_package_remotes(connection, repository_paths) + connection.execute.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(int)) + + +def test_migrate_package_remotes_has_local(package_ahriman: Package, connection: Connection, + repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: + """ + must skip processing for packages which have local cache + """ + mocker.patch( + "ahriman.core.database.operations.package_operations.PackageOperations._packages_get_select_package_bases", + return_value={package_ahriman.base: package_ahriman}) + mocker.patch("pathlib.Path.exists", return_value=True) + + migrate_package_remotes(connection, repository_paths) + connection.execute.assert_not_called() + + +def test_migrate_package_remotes_vcs(package_ahriman: Package, connection: Connection, + repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: + """ + must process VCS packages with local cache + """ + mocker.patch( + "ahriman.core.database.operations.package_operations.PackageOperations._packages_get_select_package_bases", + return_value={package_ahriman.base: package_ahriman}) + mocker.patch("pathlib.Path.exists", return_value=True) + mocker.patch.object(Package, "is_vcs", True) + + migrate_package_remotes(connection, repository_paths) + connection.execute.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(int)) + + +def test_migrate_package_remotes_no_remotes(package_ahriman: Package, connection: Connection, + repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: + """ + must skip processing in case if no remotes generated (should never happen) + """ + mocker.patch( + "ahriman.core.database.operations.package_operations.PackageOperations._packages_get_select_package_bases", + return_value={package_ahriman.base: package_ahriman}) + mocker.patch("pathlib.Path.exists", return_value=False) + mocker.patch("ahriman.models.remote_source.RemoteSource.from_remote", return_value=None) + + migrate_package_remotes(connection, repository_paths) + connection.execute.assert_not_called() diff --git a/tests/ahriman/core/database/data/test_users.py b/tests/ahriman/core/database/data/test_users.py index 4bd69849..795144fc 100644 --- a/tests/ahriman/core/database/data/test_users.py +++ b/tests/ahriman/core/database/data/test_users.py @@ -9,7 +9,7 @@ from ahriman.core.database.data import migrate_users_data def test_migrate_users_data(connection: Connection, configuration: Configuration) -> None: """ - must users to database + must put users to database """ configuration.set_option("auth:read", "user1", "password1") configuration.set_option("auth:write", "user2", "password2") diff --git a/tests/ahriman/core/database/migrations/test_m001_package_source.py b/tests/ahriman/core/database/migrations/test_m001_package_source.py new file mode 100644 index 00000000..a227db36 --- /dev/null +++ b/tests/ahriman/core/database/migrations/test_m001_package_source.py @@ -0,0 +1,8 @@ +from ahriman.core.database.migrations.m001_package_source import steps + + +def test_migration_package_source() -> None: + """ + migration must not be empty + """ + assert steps diff --git a/tests/ahriman/core/database/operations/test_package_operations.py b/tests/ahriman/core/database/operations/test_package_operations.py index c916f844..2eea8fe5 100644 --- a/tests/ahriman/core/database/operations/test_package_operations.py +++ b/tests/ahriman/core/database/operations/test_package_operations.py @@ -7,6 +7,8 @@ from unittest import mock from ahriman.core.database.sqlite import SQLite from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.package import Package +from ahriman.models.package_source import PackageSource +from ahriman.models.remote_source import RemoteSource def test_package_remove_package_base(database: SQLite, connection: Connection) -> None: @@ -166,3 +168,23 @@ def test_package_update_update(database: SQLite, package_ahriman: Package) -> No assert next(db_status.status for db_package, db_status in database.packages_get() if db_package.base == package_ahriman.base) == BuildStatusEnum.Failed + + +def test_remote_update_get(database: SQLite, package_ahriman: Package) -> None: + """ + must insert and retrieve package remote + """ + database.remote_update(package_ahriman) + assert database.remotes_get()[package_ahriman.base] == package_ahriman.remote + + +def test_remote_update_update(database: SQLite, package_ahriman: Package) -> None: + """ + must perform package remote update for existing package + """ + database.remote_update(package_ahriman) + remote_source = RemoteSource.from_remote(PackageSource.Repository, package_ahriman.base, "community") + package_ahriman.remote = remote_source + + database.remote_update(package_ahriman) + assert database.remotes_get()[package_ahriman.base] == remote_source diff --git a/tests/ahriman/core/repository/test_repository.py b/tests/ahriman/core/repository/test_repository.py index 1a40f9e0..45c44b78 100644 --- a/tests/ahriman/core/repository/test_repository.py +++ b/tests/ahriman/core/repository/test_repository.py @@ -15,11 +15,11 @@ def test_load_archives(package_ahriman: Package, package_python_schedule: Packag single_packages = [ Package(base=package_python_schedule.base, version=package_python_schedule.version, - aur_url=package_python_schedule.aur_url, + remote=package_python_schedule.remote, packages={package: props}) for package, props in package_python_schedule.packages.items() ] + [package_ahriman] - mocker.patch("ahriman.models.package.Package.load", side_effect=single_packages) + mocker.patch("ahriman.models.package.Package.from_archive", side_effect=single_packages) packages = repository.load_archives([Path("a.pkg.tar.xz"), Path("b.pkg.tar.xz"), Path("c.pkg.tar.xz")]) assert len(packages) == 2 @@ -36,7 +36,7 @@ def test_load_archives_failed(repository: Repository, mocker: MockerFixture) -> """ must skip packages which cannot be loaded """ - mocker.patch("ahriman.models.package.Package.load", side_effect=Exception()) + mocker.patch("ahriman.models.package.Package.from_archive", side_effect=Exception()) assert not repository.load_archives([Path("a.pkg.tar.xz")]) @@ -55,12 +55,12 @@ def test_load_archives_different_version(repository: Repository, package_python_ single_packages = [ Package(base=package_python_schedule.base, version=package_python_schedule.version, - aur_url=package_python_schedule.aur_url, + remote=package_python_schedule.remote, packages={package: props}) for package, props in package_python_schedule.packages.items() ] single_packages[0].version = "0.0.1-1" - mocker.patch("ahriman.models.package.Package.load", side_effect=single_packages) + mocker.patch("ahriman.models.package.Package.from_archive", side_effect=single_packages) packages = repository.load_archives([Path("a.pkg.tar.xz"), Path("b.pkg.tar.xz")]) assert len(packages) == 1 diff --git a/tests/ahriman/core/repository/test_update_handler.py b/tests/ahriman/core/repository/test_update_handler.py index f064ffc1..db999212 100644 --- a/tests/ahriman/core/repository/test_update_handler.py +++ b/tests/ahriman/core/repository/test_update_handler.py @@ -5,6 +5,7 @@ from pytest_mock import MockerFixture from ahriman.core.repository.update_handler import UpdateHandler from ahriman.models.package import Package from ahriman.models.package_source import PackageSource +from ahriman.models.remote_source import RemoteSource def test_packages(update_handler: UpdateHandler) -> None: @@ -22,7 +23,22 @@ def test_updates_aur(update_handler: UpdateHandler, package_ahriman: Package, """ mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman]) mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True) - mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) + mocker.patch("ahriman.models.package.Package.from_aur", return_value=package_ahriman) + status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_pending") + + assert update_handler.updates_aur([], False) == [package_ahriman] + status_client_mock.assert_called_once_with(package_ahriman.base) + + +def test_updates_aur_official(update_handler: UpdateHandler, package_ahriman: Package, + mocker: MockerFixture) -> None: + """ + must provide updates based on repository data + """ + package_ahriman.remote = RemoteSource.from_remote(PackageSource.Repository, package_ahriman.base, "community") + mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman]) + mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True) + mocker.patch("ahriman.models.package.Package.from_official", return_value=package_ahriman) status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_pending") assert update_handler.updates_aur([], False) == [package_ahriman] @@ -36,7 +52,7 @@ def test_updates_aur_success(update_handler: UpdateHandler, package_ahriman: Pac """ mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman]) mocker.patch("ahriman.models.package.Package.is_outdated", return_value=False) - mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) + mocker.patch("ahriman.models.package.Package.from_aur", return_value=package_ahriman) status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_success") assert not update_handler.updates_aur([], False) @@ -49,7 +65,7 @@ def test_updates_aur_failed(update_handler: UpdateHandler, package_ahriman: Pack must update status via client for failed load """ mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman]) - mocker.patch("ahriman.models.package.Package.load", side_effect=Exception()) + mocker.patch("ahriman.models.package.Package.from_aur", side_effect=Exception()) status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_failed") update_handler.updates_aur([], False) @@ -64,11 +80,10 @@ def test_updates_aur_filter(update_handler: UpdateHandler, package_ahriman: Pack mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman, package_python_schedule]) mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True) - package_load_mock = mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) + package_load_mock = mocker.patch("ahriman.models.package.Package.from_aur", return_value=package_ahriman) assert update_handler.updates_aur([package_ahriman.base], False) == [package_ahriman] - package_load_mock.assert_called_once_with(package_ahriman.base, PackageSource.AUR, - update_handler.pacman, update_handler.aur_url) + package_load_mock.assert_called_once_with(package_ahriman.base, update_handler.pacman) def test_updates_aur_ignore(update_handler: UpdateHandler, package_ahriman: Package, @@ -78,7 +93,7 @@ def test_updates_aur_ignore(update_handler: UpdateHandler, package_ahriman: Pack """ update_handler.ignore_list = [package_ahriman.base] mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman]) - package_load_mock = mocker.patch("ahriman.models.package.Package.load") + package_load_mock = mocker.patch("ahriman.models.package.Package.from_aur") update_handler.updates_aur([], False) package_load_mock.assert_not_called() @@ -105,13 +120,12 @@ def test_updates_local(update_handler: UpdateHandler, package_ahriman: Package, mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.base]) mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True) fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch") - package_load_mock = mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) + package_load_mock = mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman) status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_pending") assert update_handler.updates_local() == [package_ahriman] fetch_mock.assert_called_once_with(package_ahriman.base, remote=None) - package_load_mock.assert_called_once_with( - package_ahriman.base, PackageSource.Local, update_handler.pacman, update_handler.aur_url) + package_load_mock.assert_called_once_with(package_ahriman.base) status_client_mock.assert_called_once_with(package_ahriman.base) @@ -123,7 +137,7 @@ def test_updates_local_unknown(update_handler: UpdateHandler, package_ahriman: P mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.base]) mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True) mocker.patch("ahriman.core.build_tools.sources.Sources.fetch") - mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) + mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman) status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_unknown") assert update_handler.updates_local() == [package_ahriman] @@ -151,7 +165,7 @@ def test_updates_local_success(update_handler: UpdateHandler, package_ahriman: P mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.base]) mocker.patch("ahriman.models.package.Package.is_outdated", return_value=False) mocker.patch("ahriman.core.build_tools.sources.Sources.fetch") - mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) + mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman) status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_success") assert not update_handler.updates_local() diff --git a/tests/ahriman/core/test_tree.py b/tests/ahriman/core/test_tree.py index 6e44855c..219215a6 100644 --- a/tests/ahriman/core/test_tree.py +++ b/tests/ahriman/core/test_tree.py @@ -51,7 +51,7 @@ def test_leaf_load(package_ahriman: Package, database: SQLite, mocker: MockerFix assert leaf.dependencies == {"ahriman-dependency"} tempdir_mock.assert_called_once_with() load_mock.assert_called_once_with( - pytest.helpers.anyvar(int), package_ahriman.git_url, database.patches_get(package_ahriman.base)) + pytest.helpers.anyvar(int), package_ahriman.remote, database.patches_get(package_ahriman.base)) dependencies_mock.assert_called_once_with(pytest.helpers.anyvar(int)) rmtree_mock.assert_called_once_with(pytest.helpers.anyvar(int), ignore_errors=True) diff --git a/tests/ahriman/models/conftest.py b/tests/ahriman/models/conftest.py index 8cc65cef..fafb2252 100644 --- a/tests/ahriman/models/conftest.py +++ b/tests/ahriman/models/conftest.py @@ -1,14 +1,18 @@ +import datetime import pytest import time from unittest.mock import MagicMock, PropertyMock from ahriman import version +from ahriman.models.aur_package import AURPackage from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.counters import Counters from ahriman.models.internal_status import InternalStatus from ahriman.models.package import Package from ahriman.models.package_description import PackageDescription +from ahriman.models.package_source import PackageSource +from ahriman.models.remote_source import RemoteSource from ahriman.models.user_identity import UserIdentity @@ -67,7 +71,7 @@ def package_tpacpi_bat_git() -> Package: return Package( base="tpacpi-bat-git", version="3.1.r12.g4959b52-1", - aur_url="https://aur.archlinux.org", + remote=RemoteSource.from_remote(PackageSource.AUR, "tpacpi-bat-git", "aur"), packages={"tpacpi-bat-git": PackageDescription()}) @@ -88,22 +92,34 @@ def pyalpm_handle(pyalpm_package_ahriman: MagicMock) -> MagicMock: @pytest.fixture -def pyalpm_package_ahriman(package_ahriman: Package) -> MagicMock: +def pyalpm_package_ahriman(aur_package_ahriman: AURPackage) -> MagicMock: """ mock object for pyalpm package Args: - package_ahriman(Package): package fixture + aur_package_ahriman(AURPackage): package fixture Returns: MagicMock: pyalpm package mock """ mock = MagicMock() - type(mock).base = PropertyMock(return_value=package_ahriman.base) - type(mock).depends = PropertyMock(return_value=["python-aur"]) - type(mock).name = PropertyMock(return_value=package_ahriman.base) - type(mock).provides = PropertyMock(return_value=["python-ahriman"]) - type(mock).version = PropertyMock(return_value=package_ahriman.version) + db = type(mock).db = MagicMock() + + type(mock).base = PropertyMock(return_value=aur_package_ahriman.package_base) + type(mock).builddate = PropertyMock( + return_value=aur_package_ahriman.last_modified.replace(tzinfo=datetime.timezone.utc).timestamp()) + type(mock).conflicts = PropertyMock(return_value=aur_package_ahriman.conflicts) + type(db).name = PropertyMock(return_value="aur") + type(mock).depends = PropertyMock(return_value=aur_package_ahriman.depends) + type(mock).desc = PropertyMock(return_value=aur_package_ahriman.description) + type(mock).licenses = PropertyMock(return_value=aur_package_ahriman.license) + type(mock).makedepends = PropertyMock(return_value=aur_package_ahriman.make_depends) + type(mock).name = PropertyMock(return_value=aur_package_ahriman.name) + type(mock).optdepends = PropertyMock(return_value=aur_package_ahriman.opt_depends) + type(mock).provides = PropertyMock(return_value=aur_package_ahriman.provides) + type(mock).version = PropertyMock(return_value=aur_package_ahriman.version) + type(mock).url = PropertyMock(return_value=aur_package_ahriman.url) + return mock diff --git a/tests/ahriman/models/test_aur_package.py b/tests/ahriman/models/test_aur_package.py index 00a34896..255e1d12 100644 --- a/tests/ahriman/models/test_aur_package.py +++ b/tests/ahriman/models/test_aur_package.py @@ -1,5 +1,6 @@ import datetime import json +import pyalpm # typing: ignore from dataclasses import asdict, fields from pathlib import Path @@ -53,6 +54,22 @@ def test_from_json_2(aur_package_ahriman: AURPackage, mocker: MockerFixture) -> assert AURPackage.from_json(asdict(aur_package_ahriman)) == aur_package_ahriman +def test_from_pacman(pyalpm_package_ahriman: pyalpm.Package, aur_package_ahriman: AURPackage, + resource_path_root: Path) -> None: + """ + must load package from repository database + """ + model = AURPackage.from_pacman(pyalpm_package_ahriman) + # some fields are missing so we are changing them + model.id = aur_package_ahriman.id + model.package_base_id = aur_package_ahriman.package_base_id + model.first_submitted = aur_package_ahriman.first_submitted + model.url_path = aur_package_ahriman.url_path + model.maintainer = aur_package_ahriman.maintainer + + assert model == aur_package_ahriman + + def test_from_repo(aur_package_akonadi: AURPackage, resource_path_root: Path) -> None: """ must load package from repository api json diff --git a/tests/ahriman/models/test_package.py b/tests/ahriman/models/test_package.py index 7998aa2c..0bf89816 100644 --- a/tests/ahriman/models/test_package.py +++ b/tests/ahriman/models/test_package.py @@ -4,10 +4,10 @@ from pathlib import Path from pytest_mock import MockerFixture from unittest.mock import MagicMock +from ahriman.core.alpm.pacman import Pacman from ahriman.core.exceptions import InvalidPackageInfo from ahriman.models.aur_package import AURPackage from ahriman.models.package import Package -from ahriman.models.package_source import PackageSource from ahriman.models.repository_paths import RepositoryPaths @@ -21,15 +21,6 @@ def test_depends(package_python_schedule: Package) -> None: ) -def test_git_url(package_ahriman: Package) -> None: - """ - must generate valid git url - """ - assert package_ahriman.git_url.endswith(".git") - assert package_ahriman.git_url.startswith(package_ahriman.aur_url) - assert package_ahriman.base in package_ahriman.git_url - - def test_groups(package_ahriman: Package) -> None: """ must return list of groups for each package @@ -80,30 +71,23 @@ def test_licenses(package_ahriman: Package) -> None: assert sorted(package_ahriman.licenses) == package_ahriman.licenses -def test_web_url(package_ahriman: Package) -> None: - """ - must generate valid web url - """ - assert package_ahriman.web_url.startswith(package_ahriman.aur_url) - assert package_ahriman.base in package_ahriman.web_url - - def test_from_archive(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None: """ must construct package from alpm library """ mocker.patch("ahriman.models.package_description.PackageDescription.from_package", return_value=package_ahriman.packages[package_ahriman.base]) - assert Package.from_archive(Path("path"), pyalpm_handle, package_ahriman.aur_url) == package_ahriman + assert Package.from_archive(Path("path"), pyalpm_handle, package_ahriman.remote) == package_ahriman -def test_from_aur(package_ahriman: Package, aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None: +def test_from_aur(package_ahriman: Package, aur_package_ahriman: AURPackage, pacman: Pacman, + mocker: MockerFixture) -> None: """ must construct package from aur """ mocker.patch("ahriman.core.alpm.remote.aur.AUR.info", return_value=aur_package_ahriman) - package = Package.from_aur(package_ahriman.base, package_ahriman.aur_url) + package = Package.from_aur(package_ahriman.base, pacman) assert package_ahriman.base == package.base assert package_ahriman.version == package.version assert package_ahriman.packages.keys() == package.packages.keys() @@ -114,11 +98,12 @@ def test_from_build(package_ahriman: Package, mocker: MockerFixture, resource_pa must construct package from srcinfo """ srcinfo = (resource_path_root / "models" / "package_ahriman_srcinfo").read_text() - mocker.patch("pathlib.Path.read_text", return_value=srcinfo) + mocker.patch("ahriman.models.package.Package._check_output", return_value=srcinfo) - package = Package.from_build(Path("path"), package_ahriman.aur_url) + package = Package.from_build(Path("path")) assert package_ahriman.packages.keys() == package.packages.keys() package_ahriman.packages = package.packages # we are not going to test PackageDescription here + package_ahriman.remote = None assert package_ahriman == package @@ -126,11 +111,11 @@ def test_from_build_failed(package_ahriman: Package, mocker: MockerFixture) -> N """ must raise exception if there are errors during srcinfo load """ - mocker.patch("pathlib.Path.read_text", return_value="") + mocker.patch("ahriman.models.package.Package._check_output", return_value="") mocker.patch("ahriman.models.package.parse_srcinfo", return_value=({"packages": {}}, ["an error"])) with pytest.raises(InvalidPackageInfo): - Package.from_build(Path("path"), package_ahriman.aur_url) + Package.from_build(Path("path")) def test_from_json_view_1(package_ahriman: Package) -> None: @@ -154,97 +139,24 @@ def test_from_json_view_3(package_tpacpi_bat_git: Package) -> None: assert Package.from_json(package_tpacpi_bat_git.view()) == package_tpacpi_bat_git -def test_from_official(package_ahriman: Package, aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None: +def test_from_official(package_ahriman: Package, aur_package_ahriman: AURPackage, pacman: Pacman, + mocker: MockerFixture) -> None: """ must construct package from official repository """ mocker.patch("ahriman.core.alpm.remote.official.Official.info", return_value=aur_package_ahriman) - package = Package.from_official(package_ahriman.base, package_ahriman.aur_url) + package = Package.from_official(package_ahriman.base, pacman) assert package_ahriman.base == package.base assert package_ahriman.version == package.version assert package_ahriman.packages.keys() == package.packages.keys() -def test_load_resolve(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None: - """ - must resolve source before package loading - """ - resolve_mock = mocker.patch("ahriman.models.package_source.PackageSource.resolve", - return_value=PackageSource.Archive) - mocker.patch("ahriman.models.package.Package.from_archive") - - Package.load("path", PackageSource.Archive, pyalpm_handle, package_ahriman.aur_url) - resolve_mock.assert_called_once_with("path") - - -def test_load_from_archive(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None: - """ - must load package from package archive - """ - load_mock = mocker.patch("ahriman.models.package.Package.from_archive") - Package.load("path", PackageSource.Archive, pyalpm_handle, package_ahriman.aur_url) - load_mock.assert_called_once_with(Path("path"), pyalpm_handle, package_ahriman.aur_url) - - -def test_load_from_aur(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None: - """ - must load package from AUR - """ - load_mock = mocker.patch("ahriman.models.package.Package.from_aur") - Package.load("path", PackageSource.AUR, pyalpm_handle, package_ahriman.aur_url) - load_mock.assert_called_once_with("path", package_ahriman.aur_url) - - -def test_load_from_build(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None: - """ - must load package from build directory - """ - load_mock = mocker.patch("ahriman.models.package.Package.from_build") - Package.load("path", PackageSource.Local, pyalpm_handle, package_ahriman.aur_url) - load_mock.assert_called_once_with(Path("path"), package_ahriman.aur_url) - - -def test_load_from_official(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None: - """ - must load package from AUR - """ - load_mock = mocker.patch("ahriman.models.package.Package.from_official") - Package.load("path", PackageSource.Repository, pyalpm_handle, package_ahriman.aur_url) - load_mock.assert_called_once_with("path", package_ahriman.aur_url) - - -def test_load_failure(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None: - """ - must raise InvalidPackageInfo on exception - """ - mocker.patch("ahriman.models.package.Package.from_aur", side_effect=InvalidPackageInfo("exception!")) - with pytest.raises(InvalidPackageInfo): - Package.load("path", PackageSource.AUR, pyalpm_handle, package_ahriman.aur_url) - - -def test_load_failure_exception(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None: - """ - must raise InvalidPackageInfo on random eexception - """ - mocker.patch("ahriman.models.package.Package.from_aur", side_effect=Exception()) - with pytest.raises(InvalidPackageInfo): - Package.load("path", PackageSource.AUR, pyalpm_handle, package_ahriman.aur_url) - - -def test_load_invalid_source(package_ahriman: Package, pyalpm_handle: MagicMock) -> None: - """ - must raise InvalidPackageInfo on unsupported source - """ - with pytest.raises(InvalidPackageInfo): - Package.load("path", PackageSource.Remote, pyalpm_handle, package_ahriman.aur_url) - - def test_dependencies_failed(mocker: MockerFixture) -> None: """ must raise exception if there are errors during srcinfo load """ - mocker.patch("pathlib.Path.read_text", return_value="") + mocker.patch("ahriman.models.package.Package._check_output", return_value="") mocker.patch("ahriman.models.package.parse_srcinfo", return_value=({"packages": {}}, ["an error"])) with pytest.raises(InvalidPackageInfo): @@ -256,7 +168,7 @@ def test_dependencies_with_version(mocker: MockerFixture, resource_path_root: Pa must load correct list of dependencies with version """ srcinfo = (resource_path_root / "models" / "package_yay_srcinfo").read_text() - mocker.patch("pathlib.Path.read_text", return_value=srcinfo) + mocker.patch("ahriman.models.package.Package._check_output", return_value=srcinfo) assert Package.dependencies(Path("path")) == {"git", "go", "pacman"} @@ -266,7 +178,7 @@ def test_dependencies_with_version_and_overlap(mocker: MockerFixture, resource_p must load correct list of dependencies with version """ srcinfo = (resource_path_root / "models" / "package_gcc10_srcinfo").read_text() - mocker.patch("pathlib.Path.read_text", return_value=srcinfo) + mocker.patch("ahriman.models.package.Package._check_output", return_value=srcinfo) assert Package.dependencies(Path("path")) == {"glibc", "doxygen", "binutils", "git", "libmpc", "python", "zstd"} @@ -328,7 +240,7 @@ def test_full_depends(package_ahriman: Package, package_python_schedule: Package assert package_ahriman.full_depends(pyalpm_handle, [package_python_schedule]) == package_ahriman.depends package_python_schedule.packages[package_python_schedule.base].depends = [package_ahriman.base] - expected = sorted(set(package_python_schedule.depends + ["python-aur"])) + expected = sorted(set(package_python_schedule.depends + package_ahriman.depends)) assert package_python_schedule.full_depends(pyalpm_handle, [package_python_schedule]) == expected diff --git a/tests/ahriman/models/test_package_description.py b/tests/ahriman/models/test_package_description.py index 6b158e56..3a84c020 100644 --- a/tests/ahriman/models/test_package_description.py +++ b/tests/ahriman/models/test_package_description.py @@ -1,4 +1,3 @@ -from dataclasses import asdict from unittest.mock import MagicMock from ahriman.models.package_description import PackageDescription @@ -24,14 +23,14 @@ def test_from_json(package_description_ahriman: PackageDescription) -> None: """ must construct description from json object """ - assert PackageDescription.from_json(asdict(package_description_ahriman)) == package_description_ahriman + assert PackageDescription.from_json(package_description_ahriman.view()) == package_description_ahriman def test_from_json_with_unknown_fields(package_description_ahriman: PackageDescription) -> None: """ must construct description from json object containing unknown fields """ - dump = asdict(package_description_ahriman) + dump = package_description_ahriman.view() dump.update(unknown_field="value") assert PackageDescription.from_json(dump) == package_description_ahriman diff --git a/tests/ahriman/models/test_remote_source.py b/tests/ahriman/models/test_remote_source.py new file mode 100644 index 00000000..62d6451e --- /dev/null +++ b/tests/ahriman/models/test_remote_source.py @@ -0,0 +1,82 @@ +from pathlib import Path +from pytest_mock import MockerFixture + +from ahriman.models.package import Package +from ahriman.models.package_source import PackageSource +from ahriman.models.remote_source import RemoteSource + + +def test_post_init(remote_source: RemoteSource) -> None: + """ + must convert string source to enum + """ + remote = RemoteSource( + git_url=remote_source.git_url, + web_url=remote_source.web_url, + path=remote_source.path, + branch=remote_source.branch, + source=remote_source.source.value, + ) + assert remote == remote_source + + +def test_pkgbuild_dir(remote_source: RemoteSource) -> None: + """ + must return path as is in `path` property + """ + assert isinstance(remote_source.pkgbuild_dir, Path) + assert remote_source.pkgbuild_dir.name == "" + + +def test_from_json(remote_source: RemoteSource) -> None: + """ + must construct remote source from json + """ + assert RemoteSource.from_json(remote_source.view()) == remote_source + + +def test_from_json_empty() -> None: + """ + must return None in case of empty dictionary, which is required by the database wrapper + """ + assert RemoteSource.from_json({}) is None + + +def test_from_remote_aur(package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must construct remote from AUR source + """ + remote_git_url_mock = mocker.patch("ahriman.core.alpm.remote.aur.AUR.remote_git_url") + remote_web_url_mock = mocker.patch("ahriman.core.alpm.remote.aur.AUR.remote_web_url") + + remote = RemoteSource.from_remote(PackageSource.AUR, package_ahriman.base, "aur") + remote_git_url_mock.assert_called_once_with(package_ahriman.base, "aur") + remote_web_url_mock.assert_called_once_with(package_ahriman.base) + assert remote.pkgbuild_dir == Path(".") + assert remote.branch == "master" + assert remote.source == PackageSource.AUR + + +def test_from_remote_official(package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must construct remote from official repository source + """ + remote_git_url_mock = mocker.patch("ahriman.core.alpm.remote.official.Official.remote_git_url") + remote_web_url_mock = mocker.patch("ahriman.core.alpm.remote.official.Official.remote_web_url") + + remote = RemoteSource.from_remote(PackageSource.Repository, package_ahriman.base, "community") + remote_git_url_mock.assert_called_once_with(package_ahriman.base, "community") + remote_web_url_mock.assert_called_once_with(package_ahriman.base) + assert remote.pkgbuild_dir == Path("trunk") + assert remote.branch.endswith(package_ahriman.base) + assert remote.source == PackageSource.Repository + + +def test_from_remote_other() -> None: + """ + must return None in case if source is not one of AUR or Repository + """ + assert RemoteSource.from_remote(PackageSource.Archive, "package", "repository") is None + assert RemoteSource.from_remote(PackageSource.Directory, "package", "repository") is None + assert RemoteSource.from_remote(PackageSource.Local, "package", "repository") is None + assert RemoteSource.from_remote(PackageSource.Remote, "package", "repository") is None diff --git a/tests/ahriman/web/views/service/test_views_service_search.py b/tests/ahriman/web/views/service/test_views_service_search.py index 13eefa50..ff3d1b3e 100644 --- a/tests/ahriman/web/views/service/test_views_service_search.py +++ b/tests/ahriman/web/views/service/test_views_service_search.py @@ -37,7 +37,7 @@ async def test_get_exception(client: TestClient, mocker: MockerFixture) -> None: response = await client.get("/service-api/v1/search") assert response.status == 404 - search_mock.assert_called_once_with() + search_mock.assert_called_once_with(pacman=pytest.helpers.anyvar(int)) async def test_get_join(client: TestClient, mocker: MockerFixture) -> None: @@ -48,4 +48,4 @@ async def test_get_join(client: TestClient, mocker: MockerFixture) -> None: response = await client.get("/service-api/v1/search", params=[("for", "ahriman"), ("for", "maybe")]) assert response.ok - search_mock.assert_called_once_with("ahriman", "maybe") + search_mock.assert_called_once_with("ahriman", "maybe", pacman=pytest.helpers.anyvar(int)) diff --git a/tests/testresources/core/ahriman.ini b/tests/testresources/core/ahriman.ini index 9fdda429..6f018915 100644 --- a/tests/testresources/core/ahriman.ini +++ b/tests/testresources/core/ahriman.ini @@ -4,7 +4,6 @@ logging = logging.ini database = ../../../ahriman-test.db [alpm] -aur_url = https://aur.archlinux.org database = /var/lib/pacman repositories = core extra community multilib root = /