Compare commits

..

11 Commits
1.6.0 ... 1.6.4

Author SHA1 Message Date
3c5bcbd172 Release 1.6.4 2021-11-10 21:29:45 +03:00
042638d40e handle packages which have been removed from the repository (#45)
* handle packages which have been removed from the repository

* manually remove packages which have been removed from the base
2021-11-10 01:37:25 +03:00
e6adb333b2 Release 1.6.3 2021-11-04 21:32:27 +03:00
fa4244d21e take python laziness into account 2021-11-04 21:30:34 +03:00
91de1c2b8a Release 1.6.2 2021-10-28 03:20:52 +03:00
32a4a82603 improve configuration extension
* Allow spaces in lists. This feature has been done in the way as shell
  interprets arguments by using quotation marks
* Clear current content on reload
2021-10-28 03:19:50 +03:00
e8a10c1bb5 add nginx configuration to the faq 2021-10-27 03:35:33 +03:00
d480eb7bc3 Release 1.6.1 2021-10-27 03:16:53 +03:00
8b0f9bfd78 update license headers 2021-10-27 03:14:39 +03:00
a2639f8dbb add update printer which will print current version if any 2021-10-27 03:11:43 +03:00
65ba590ace use PackageSource enum for Package.load method
When using add function it sill tries to load data with invalid source
2021-10-27 02:49:23 +03:00
29 changed files with 2976 additions and 2462 deletions

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 388 KiB

After

Width:  |  Height:  |  Size: 416 KiB

View File

@ -1,6 +1,13 @@
# ahriman configuration
Some groups can be specified for each architecture separately. E.g. if there are `build` and `build:x86_64` groups it will use the option from `build:x86_64` for the `x86_64` architecture and `build` for any other (architecture specific group has higher priority). In case if both groups are presented, architecture specific options will be merged into global ones overriding them.
Some groups can be specified for each architecture separately. E.g. if there are `build` and `build:x86_64` groups it will use the option from `build:x86_64` for the `x86_64` architecture and `build` for any other (architecture specific group has higher priority). In case if both groups are presented, architecture specific options will be merged into global ones overriding them.
Some values have list of strings type. Those values will be read in the same way as shell does:
* By default, it splits value by spaces excluding empty elements.
* In case if quotation mark (`"` or `'`) will be found, any spaces inside will be ignored.
* In order to use quotation mark inside value it is required to put it to another quotation mark, e.g. `wor"'"d "with quote"` will be parsed as `["wor'd", "with quote"]` and vice versa.
* Unclosed quotation mark is not allowed and will rise an exception.
## `settings` group
@ -38,11 +45,11 @@ Authorization mapping. Group name must refer to user access level, i.e. it shoul
Key is always username (case-insensitive), option value depends on authorization provider:
* `OAuth` - by default requires only usernames and ignores values. But in case of direct login method call (via POST request) it will act as `Mapping` authorization method.
* `Mapping` (default) - reads salted password hashes from values, uses SHA512 in order to hash passwords. Password can be set by using `create-user` subcommand.
* `Mapping` (default) - reads salted password hashes from values, uses SHA512 in order to hash passwords. Password can be set by using `user-add` subcommand.
## `build:*` groups
Build related configuration. Group name must refer to architecture, e.g. it should be `build:x86_64` for x86_64 architecture.
Build related configuration. Group name can refer to architecture, e.g. `build:x86_64` can be used for x86_64 architecture specific settings.
* `archbuild_flags` - additional flags passed to `archbuild` command, space separated list of strings, optional.
* `build_command` - default build command, string, required.
@ -59,7 +66,7 @@ Base repository settings.
## `sign:*` groups
Settings for signing packages or repository. Group name must refer to architecture, e.g. it should be `sign:x86_64` for x86_64 architecture.
Settings for signing packages or repository. Group name can refer to architecture, e.g. `sign:x86_64` can be used for x86_64 architecture specific settings.
* `target` - configuration flag to enable signing, space separated list of strings, required. Allowed values are `package` (sign each package separately), `repository` (sign repository database file).
* `key` - default PGP key, string, required. This key will also be used for database signing if enabled.
@ -69,7 +76,7 @@ Settings for signing packages or repository. Group name must refer to architectu
Report generation settings.
* `target` - list of reports to be generated, space separated list of strings, required. It must point to valid section (or to section with architecture), e.g. `somerandomname` must point to existing section, `email` must point to one of `email` of `email:x86_64` (with architecture it has higher priority).
* `target` - list of reports to be generated, space separated list of strings, required. It must point to valid section (or to section with architecture), e.g. `somerandomname` must point to existing section, `email` must point to one of `email` of `email:x86_64` (the one with architecture has higher priority).
Type will be read from several ways:
@ -152,7 +159,7 @@ Requires `boto3` library to be installed. Section name must be either `s3` (plus
## `web:*` groups
Web server settings. If any of `host`/`port` is not set, web integration will be disabled. Group name must refer to architecture, e.g. it should be `web:x86_64` for x86_64 architecture. This feature requires `aiohttp` libraries to be installed.
Web server settings. If any of `host`/`port` is not set, web integration will be disabled. Group name can refer to architecture, e.g. `web:x86_64` can be used for x86_64 architecture specific settings. This feature requires `aiohttp` libraries to be installed.
* `address` - optional address in form `proto://host:port` (`port` can be omitted in case of default `proto` ports), will be used instead of `http://{host}:{port}` in case if set, string, optional. This option is required in case if `OAuth` provider is used.
* `debug` - enable debug toolbar, boolean, optional, default `no`.

View File

@ -163,6 +163,41 @@ Server = file:///var/lib/ahriman/repository/x86_64
(You might need to add `SigLevel` option according to the pacman documentation.)
### I would like to serve the repository
Easy. For example, nginx configuration (without SSL) will look like:
```
server {
listen 80;
server_name repo.example.com;
location / {
autoindex on;
root /var/lib/ahriman/repository;
}
}
```
Example of the status page configuration is the following (status service is using 8080 port):
```
server {
listen 80;
server_name builds.example.com;
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarder-Proto $scheme;
proxy_pass http://127.0.0.1:8080;
}
}
```
## Remote synchronization
### Wait I would like to use the repository from another server

View File

@ -1,7 +1,7 @@
# Maintainer: Evgeniy Alekseev
pkgname='ahriman'
pkgver=1.6.0
pkgver=1.6.4
pkgrel=1
pkgdesc="ArcH Linux ReposItory MANager"
arch=('any')

View File

@ -65,11 +65,10 @@ class Packages(Properties):
:param without_dependencies: if set, dependency check will be disabled
"""
aur_url = self.configuration.get("alpm", "aur_url")
package = Package.load(source, self.repository.pacman, aur_url)
Sources.load(self.repository.paths.manual_for(package.base), package.git_url,
self.repository.paths.patches_for(package.base))
package = Package.load(source, PackageSource.AUR, self.repository.pacman, aur_url)
local_path = self.repository.paths.manual_for(package.base)
Sources.load(local_path, package.git_url, self.repository.paths.patches_for(package.base))
self._process_dependencies(local_path, known_packages, without_dependencies)
def _add_directory(self, source: str, *_: Any) -> None:
@ -88,11 +87,10 @@ class Packages(Properties):
:param known_packages: list of packages which are known by the service
:param without_dependencies: if set, dependency check will be disabled
"""
local_path = Path(source)
aur_url = self.configuration.get("alpm", "aur_url")
package = Package.load(local_path, self.repository.pacman, aur_url)
package = Package.load(source, PackageSource.Local, self.repository.pacman, aur_url)
cache_dir = self.repository.paths.cache_for(package.base)
shutil.copytree(local_path, cache_dir) # copy package to store in caches
shutil.copytree(Path(source), 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
dst = self.repository.paths.manual_for(package.base)

View File

@ -23,9 +23,11 @@ from pathlib import Path
from typing import Callable, Iterable, List
from ahriman.application.application.properties import Properties
from ahriman.application.formatters.update_printer import UpdatePrinter
from ahriman.core.build_tools.sources import Sources
from ahriman.core.tree import Tree
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
class Repository(Properties):
@ -103,27 +105,37 @@ class Repository(Properties):
targets = target or None
self.repository.process_sync(targets, built_packages)
def unknown(self) -> List[Package]:
def unknown(self) -> List[str]:
"""
get packages which were not found in AUR
:return: unknown package list
:return: unknown package archive list
"""
def has_aur(package_base: str, aur_url: str) -> bool:
try:
_ = Package.from_aur(package_base, aur_url)
except Exception:
return False
return True
def has_local(package_base: str) -> bool:
cache_dir = self.repository.paths.cache_for(package_base)
def has_local(probe: Package) -> bool:
cache_dir = self.repository.paths.cache_for(probe.base)
return cache_dir.is_dir() and not Sources.has_remotes(cache_dir)
return [
package
for package in self.repository.packages()
if not has_aur(package.base, package.aur_url) and not has_local(package.base)
]
def unknown_aur(probe: Package) -> List[str]:
packages: List[str] = []
for single in probe.packages:
try:
_ = Package.from_aur(single, probe.aur_url)
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)
packages = set(probe.packages.keys()).difference(local.packages.keys())
return list(packages)
result = []
for package in self.repository.packages():
if has_local(package):
result.extend(unknown_local(package)) # there is local package
else:
result.extend(unknown_aur(package)) # local package not found
return result
def update(self, updates: Iterable[Package]) -> None:
"""
@ -133,7 +145,10 @@ class Repository(Properties):
def process_update(paths: Iterable[Path]) -> None:
if not paths:
return # don't need to process if no update supplied
updated = [Package.load(path, self.repository.pacman, self.repository.aur_url) for path in paths]
updated = [
Package.load(str(path), PackageSource.Archive, self.repository.pacman, self.repository.aur_url)
for path in paths
]
self.repository.process_update(paths)
self._finalize(updated)
@ -166,7 +181,9 @@ class Repository(Properties):
if not no_manual:
updates.extend(self.repository.updates_manual())
local_versions = {package.base: package.version for package in self.repository.packages()}
for package in updates:
log_fn(f"{package.base} = {package.version}")
UpdatePrinter(package, local_versions.get(package.base)).print(
verbose=True, log_fn=log_fn, separator=" -> ")
return updates

View File

@ -17,11 +17,10 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from typing import List, Optional
from typing import Optional
from ahriman.application.formatters.printer import Printer
from ahriman.models.build_status import BuildStatus
from ahriman.models.property import Property
class StatusPrinter(Printer):
@ -36,13 +35,6 @@ class StatusPrinter(Printer):
"""
self.content = status
def properties(self) -> List[Property]:
"""
convert content into printable data
:return: list of content properties
"""
return []
def title(self) -> Optional[str]:
"""
generate entry title from content

View File

@ -0,0 +1,42 @@
#
# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from typing import Optional
from ahriman.application.formatters.printer import Printer
class StringPrinter(Printer):
"""
print content of the random string
"""
def __init__(self, content: str) -> None:
"""
default constructor
:param content: any content string
"""
self.content = content
def title(self) -> Optional[str]:
"""
generate entry title from content
:return: content title if it can be generated and None otherwise
"""
return self.content

View File

@ -0,0 +1,53 @@
#
# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from typing import List, Optional
from ahriman.application.formatters.printer import Printer
from ahriman.models.package import Package
from ahriman.models.property import Property
class UpdatePrinter(Printer):
"""
print content of the package update
"""
def __init__(self, remote: Package, local_version: Optional[str]) -> None:
"""
default constructor
:param remote: remote (new) package object
:param local_version: local version of the package if any
"""
self.content = remote
self.local_version = local_version or "N/A"
def properties(self) -> List[Property]:
"""
convert content into printable data
:return: list of content properties
"""
return [Property(self.local_version, self.content.version, is_required=True)]
def title(self) -> Optional[str]:
"""
generate entry title from content
:return: content title if it can be generated and None otherwise
"""
return self.content.base

View File

@ -29,6 +29,7 @@ from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.models.action import Action
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
class Patch(Handler):
@ -55,23 +56,24 @@ 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, Path(args.package), args.track)
Patch.patch_set_create(application, args.package, args.track)
@staticmethod
def patch_set_create(application: Application, sources_dir: Path, track: List[str]) -> None:
def patch_set_create(application: Application, sources_dir: str, track: List[str]) -> None:
"""
create patch set for the package base
:param application: application instance
:param sources_dir: path to directory with the package sources
:param track: track files which match the glob before creating the patch
"""
package = Package.load(sources_dir, application.repository.pacman, application.repository.aur_url)
package = Package.load(sources_dir, PackageSource.Local, application.repository.pacman,
application.repository.aur_url)
patch_dir = application.repository.paths.patches_for(package.base)
Patch.patch_set_remove(application, package.base) # remove old patches
patch_dir.mkdir(mode=0o755, parents=True)
Sources.patch_create(sources_dir, patch_dir / "00-main.patch", *track)
Sources.patch_create(Path(sources_dir), patch_dir / "00-main.patch", *track)
@staticmethod
def patch_set_list(application: Application, package_base: str) -> None:

View File

@ -22,10 +22,9 @@ import argparse
from typing import Type
from ahriman.application.application import Application
from ahriman.application.formatters.package_printer import PackagePrinter
from ahriman.application.formatters.string_printer import StringPrinter
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
from ahriman.models.build_status import BuildStatus
class RemoveUnknown(Handler):
@ -46,8 +45,8 @@ class RemoveUnknown(Handler):
application = Application(architecture, configuration, no_report)
unknown_packages = application.unknown()
if args.dry_run:
for package in unknown_packages:
PackagePrinter(package, BuildStatus()).print(args.info)
for package in sorted(unknown_packages):
StringPrinter(package).print(args.info)
return
application.remove(package.base for package in unknown_packages)
application.remove(unknown_packages)

View File

@ -24,7 +24,7 @@ import logging
from logging.config import fileConfig
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Type
from typing import Any, Dict, Generator, List, Optional, Tuple, Type
from ahriman.core.exceptions import InitializeException
@ -49,7 +49,7 @@ class Configuration(configparser.RawConfigParser):
default constructor. In the most cases must not be called directly
"""
configparser.RawConfigParser.__init__(self, allow_no_value=True, converters={
"list": lambda value: value.split(),
"list": self.__convert_list,
"path": self.__convert_path,
})
self.architecture: Optional[str] = None
@ -84,6 +84,32 @@ class Configuration(configparser.RawConfigParser):
config.load_logging(quiet)
return config
@staticmethod
def __convert_list(value: str) -> List[str]:
"""
convert string value to list of strings
:param value: string configuration value
:return: list of string from the parsed string
"""
def generator() -> Generator[str, None, None]:
quote_mark = None
word = ""
for char in value:
if char in ("'", "\"") and quote_mark is None: # quoted part started, store quote and do nothing
quote_mark = char
elif char == quote_mark: # quoted part ended, reset quotation
quote_mark = None
elif char == " " and quote_mark is None: # found space outside of the quotation, yield the word
yield word
word = ""
else: # append character to the buffer
word += char
if quote_mark: # there is unmatched quote
raise ValueError(f"unmatched quote in {value}")
yield word # sequence done, return whatever we found
return [word for word in generator() if word]
@staticmethod
def section_name(section: str, suffix: str) -> str:
"""
@ -204,6 +230,8 @@ class Configuration(configparser.RawConfigParser):
"""
if self.path is None or self.architecture is None:
raise InitializeException("Configuration path and/or architecture are not set")
for section in self.sections(): # clear current content
self.remove_section(section)
self.load(self.path)
self.merge_sections(self.architecture)

View File

@ -153,8 +153,12 @@ class UnknownPackage(ValueError):
exception for status watcher which will be thrown on unknown package
"""
def __init__(self, base: str) -> None:
ValueError.__init__(self, f"Package base {base} is unknown")
def __init__(self, package_base: str) -> None:
"""
default constructor
:param package_base: package base name
"""
ValueError.__init__(self, f"Package base {package_base} is unknown")
class UnsafeRun(RuntimeError):
@ -165,9 +169,9 @@ class UnsafeRun(RuntimeError):
def __init__(self, current_uid: int, root_uid: int) -> None:
"""
default constructor
:param current_uid: current user ID
:param root_uid: ID of the owner of root directory
"""
RuntimeError.__init__(
self,
f"""Current UID {current_uid} differs from root owner {root_uid}.
Note that for the most actions it is unsafe to run application as different user.
If you are 100% sure that it must be there try --unsafe option""")
RuntimeError.__init__(self, f"Current UID {current_uid} differs from root owner {root_uid}. "
f"Note that for the most actions it is unsafe to run application as different user."
f" If you are 100% sure that it must be there try --unsafe option")

View File

@ -20,7 +20,7 @@
import shutil
from pathlib import Path
from typing import Dict, Iterable, List, Optional
from typing import Iterable, List, Optional, Set
from ahriman.core.build_tools.task import Task
from ahriman.core.report.report import Report
@ -34,6 +34,14 @@ class Executor(Cleaner):
trait for common repository update processes
"""
def load_archives(self, packages: Iterable[Path]) -> List[Package]:
"""
load packages from list of archives
:param packages: paths to package archives
:return: list of read packages
"""
raise NotImplementedError
def packages(self) -> List[Package]:
"""
generate list of repository packages
@ -151,23 +159,24 @@ class Executor(Cleaner):
package_path = self.paths.repository / name
self.repo.add(package_path)
# we are iterating over bases, not single packages
updates: Dict[str, Package] = {}
for filename in packages:
try:
local = Package.load(filename, self.pacman, self.aur_url)
updates.setdefault(local.base, local).packages.update(local.packages)
except Exception:
self.logger.exception("could not load package from %s", filename)
current_packages = self.packages()
removed_packages: List[str] = [] # list of packages which have been removed from the base
updates = self.load_archives(packages)
for local in updates.values():
for local in updates:
try:
for description in local.packages.values():
update_single(description.filename, local.base)
self.reporter.set_success(local)
current_package_archives: Set[str] = next(
(set(current.packages) for current in current_packages if current.base == local.base), set())
removed_packages.extend(current_package_archives.difference(local.packages))
except Exception:
self.reporter.set_failed(local.base)
self.logger.exception("could not process %s", local.base)
self.clear_packages()
self.process_remove(removed_packages)
return self.repo.repo_path

View File

@ -18,12 +18,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from pathlib import Path
from typing import Dict, List
from typing import Dict, Iterable, List
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):
@ -31,20 +32,35 @@ class Repository(Executor, UpdateHandler):
base repository control class
"""
def load_archives(self, packages: Iterable[Path]) -> List[Package]:
"""
load packages from list of archives
:param packages: paths to package archives
:return: list of read packages
"""
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)
current = result.setdefault(local.base, local)
if current.version != local.version:
# force version to max of them
self.logger.warning("version of %s differs, found %s and %s",
current.base, current.version, local.version)
if current.is_outdated(local, self.paths, calculate_version=False):
current.version = local.version
current.packages.update(local.packages)
except Exception:
self.logger.exception("could not load package from %s", full_path)
return list(result.values())
def packages(self) -> List[Package]:
"""
generate list of repository packages
:return: list of packages properties
"""
result: Dict[str, Package] = {}
for full_path in filter(package_like, self.paths.repository.iterdir()):
try:
local = Package.load(full_path, self.pacman, self.aur_url)
result.setdefault(local.base, local).packages.update(local.packages)
except Exception:
self.logger.exception("could not load package from %s", full_path)
continue
return list(result.values())
return self.load_archives(filter(package_like, self.paths.repository.iterdir()))
def packages_built(self) -> List[Path]:
"""

View File

@ -21,6 +21,7 @@ from typing import Iterable, List
from ahriman.core.repository.cleaner import Cleaner
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
class UpdateHandler(Cleaner):
@ -53,7 +54,7 @@ class UpdateHandler(Cleaner):
continue
try:
remote = Package.load(local.base, self.pacman, self.aur_url)
remote = Package.load(local.base, PackageSource.AUR, self.pacman, self.aur_url)
if local.is_outdated(remote, self.paths):
self.reporter.set_pending(local.base)
result.append(remote)
@ -72,16 +73,16 @@ class UpdateHandler(Cleaner):
result: List[Package] = []
known_bases = {package.base for package in self.packages()}
for filename in self.paths.manual.iterdir():
for dirname in self.paths.manual.iterdir():
try:
local = Package.load(filename, self.pacman, self.aur_url)
local = Package.load(str(dirname), PackageSource.Local, self.pacman, self.aur_url)
result.append(local)
if local.base not in known_bases:
self.reporter.set_unknown(local)
else:
self.reporter.set_pending(local.base)
except Exception:
self.logger.exception("could not add package from %s", filename)
self.logger.exception("could not add package from %s", dirname)
self.clear_manual()
return result

View File

@ -21,7 +21,7 @@ from __future__ import annotations
import datetime
from dataclasses import dataclass, fields
from dataclasses import dataclass, field, fields
from enum import Enum
from typing import Any, Dict, Type
@ -84,7 +84,7 @@ class BuildStatus:
"""
status: BuildStatusEnum = BuildStatusEnum.Unknown
timestamp: int = int(datetime.datetime.utcnow().timestamp())
timestamp: int = field(default_factory=lambda: int(datetime.datetime.utcnow().timestamp()))
def __post_init__(self) -> None:
"""

View File

@ -26,12 +26,13 @@ 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, List, Optional, Set, Type, Union
from typing import Any, Dict, List, Optional, Set, Type
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.exceptions import InvalidPackageInfo
from ahriman.core.util import check_output
from ahriman.models.package_description import PackageDescription
from ahriman.models.package_source import PackageSource
from ahriman.models.repository_paths import RepositoryPaths
@ -164,21 +165,24 @@ class Package:
packages=packages)
@classmethod
def load(cls: Type[Package], path: Union[Path, str], pacman: Pacman, aur_url: str) -> Package:
def load(cls: Type[Package], package: str, source: PackageSource, pacman: Pacman, aur_url: str) -> Package:
"""
package constructor from available sources
:param path: one of path to sources directory, path to archive or package name/base
:param package: one of path to sources directory, path to archive or package name/base
:param source: source of the package required to define the load method
:param pacman: alpm wrapper instance (required to load from archive)
:param aur_url: AUR root url
:return: package properties
"""
try:
maybe_path = Path(path)
if maybe_path.is_dir():
return cls.from_build(maybe_path, aur_url)
if maybe_path.is_file():
return cls.from_archive(maybe_path, pacman, aur_url)
return cls.from_aur(str(path), aur_url)
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)
raise InvalidPackageInfo(f"Unsupported local package source {resolved_source}")
except InvalidPackageInfo:
raise
except Exception as e:
@ -253,14 +257,15 @@ class Package:
return self.version
def is_outdated(self, remote: Package, paths: RepositoryPaths) -> bool:
def is_outdated(self, remote: Package, paths: RepositoryPaths, calculate_version: bool = True) -> bool:
"""
check if package is out-of-dated
:param remote: package properties from remote source
:param paths: repository paths instance. Required for VCS packages cache
:param calculate_version: expand version to actual value (by calculating git versions)
:return: True if the package is out-of-dated and False otherwise
"""
remote_version = remote.actual_version(paths) # either normal version or updated VCS
remote_version = remote.actual_version(paths) if calculate_version else remote.version
result: int = vercmp(self.version, remote_version)
return result < 0

View File

@ -1,3 +1,7 @@
#
# Copyright (c) 2021 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

View File

@ -17,4 +17,4 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
__version__ = "1.6.0"
__version__ = "1.6.4"

View File

@ -147,6 +147,7 @@ def test_unknown_no_aur(application_repository: Repository, package_ahriman: Pac
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman])
mocker.patch("ahriman.models.package.Package.from_aur", side_effect=Exception())
mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman)
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("ahriman.core.build_tools.sources.Sources.has_remotes", return_value=False)
@ -163,7 +164,7 @@ def test_unknown_no_aur_no_local(application_repository: Repository, package_ahr
mocker.patch("pathlib.Path.is_dir", return_value=False)
packages = application_repository.unknown()
assert packages == [package_ahriman]
assert packages == list(package_ahriman.packages.keys())
def test_unknown_no_local(application_repository: Repository, package_ahriman: Package, mocker: MockerFixture) -> None:
@ -201,6 +202,7 @@ def test_updates_all(application_repository: Repository, package_ahriman: Packag
"""
must get updates for all
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[])
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur",
return_value=[package_ahriman])
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
@ -214,6 +216,7 @@ def test_updates_disabled(application_repository: Repository, mocker: MockerFixt
"""
must get updates without anything
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[])
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
@ -226,6 +229,7 @@ def test_updates_no_aur(application_repository: Repository, mocker: MockerFixtur
"""
must get updates without aur
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[])
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
@ -238,6 +242,7 @@ def test_updates_no_manual(application_repository: Repository, mocker: MockerFix
"""
must get updates without manual
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[])
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
@ -250,6 +255,7 @@ def test_updates_no_vcs(application_repository: Repository, mocker: MockerFixtur
"""
must get updates without VCS
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[])
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
@ -262,6 +268,7 @@ def test_updates_with_filter(application_repository: Repository, mocker: MockerF
"""
must get updates without VCS
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[])
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")

View File

@ -5,6 +5,8 @@ from ahriman.application.formatters.aur_printer import AurPrinter
from ahriman.application.formatters.configuration_printer import ConfigurationPrinter
from ahriman.application.formatters.package_printer import PackagePrinter
from ahriman.application.formatters.status_printer import StatusPrinter
from ahriman.application.formatters.string_printer import StringPrinter
from ahriman.application.formatters.update_printer import UpdatePrinter
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
@ -39,9 +41,27 @@ def package_ahriman_printer(package_ahriman: Package) -> PackagePrinter:
@pytest.fixture
def status_printer(package_ahriman: Package) -> StatusPrinter:
def status_printer() -> StatusPrinter:
"""
fixture for build status printer
:return: build status printer test instance
"""
return StatusPrinter(BuildStatus())
@pytest.fixture
def string_printer() -> StringPrinter:
"""
fixture for any string printer
:return: any string printer test instance
"""
return StringPrinter("hello, world")
@pytest.fixture
def update_printer(package_ahriman: Package) -> UpdatePrinter:
"""
fixture for build status printer
:return: build status printer test instance
"""
return UpdatePrinter(package_ahriman, None)

View File

@ -0,0 +1,15 @@
from ahriman.application.formatters.string_printer import StringPrinter
def test_properties(string_printer: StringPrinter) -> None:
"""
must return empty properties list
"""
assert not string_printer.properties()
def test_title(string_printer: StringPrinter) -> None:
"""
must return non empty title
"""
assert string_printer.title() is not None

View File

@ -0,0 +1,15 @@
from ahriman.application.formatters.update_printer import UpdatePrinter
def test_properties(update_printer: UpdatePrinter) -> None:
"""
must return empty properties list
"""
assert update_printer.properties()
def test_title(update_printer: UpdatePrinter) -> None:
"""
must return non empty title
"""
assert update_printer.title() is not None

View File

@ -11,6 +11,14 @@ from ahriman.core.upload.upload import Upload
from ahriman.models.package import Package
def test_load_archives(executor: Executor) -> None:
"""
must raise NotImplemented for missing load_archives method
"""
with pytest.raises(NotImplementedError):
executor.load_archives([])
def test_packages(executor: Executor) -> None:
"""
must raise NotImplemented for missing method
@ -182,11 +190,13 @@ def test_process_update(executor: Executor, package_ahriman: Package, mocker: Mo
"""
must run update process
"""
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
mocker.patch("ahriman.core.repository.executor.Executor.load_archives", return_value=[package_ahriman])
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
move_mock = mocker.patch("shutil.move")
repo_add_mock = mocker.patch("ahriman.core.alpm.repo.Repo.add")
sign_package_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process_sign_package", side_effect=lambda fn, _: [fn])
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_success")
remove_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_remove")
# must return complete
assert executor.process_update([package.filepath for package in package_ahriman.packages.values()])
@ -201,6 +211,8 @@ def test_process_update(executor: Executor, package_ahriman: Package, mocker: Mo
# must clear directory
from ahriman.core.repository.cleaner import Cleaner
Cleaner.clear_packages.assert_called_once()
# clear removed packages
remove_mock.assert_called_once_with([])
def test_process_update_group(executor: Executor, package_python_schedule: Package,
@ -209,9 +221,11 @@ def test_process_update_group(executor: Executor, package_python_schedule: Packa
must group single packages under one base
"""
mocker.patch("shutil.move")
mocker.patch("ahriman.models.package.Package.load", return_value=package_python_schedule)
mocker.patch("ahriman.core.repository.executor.Executor.load_archives", return_value=[package_python_schedule])
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_python_schedule])
repo_add_mock = mocker.patch("ahriman.core.alpm.repo.Repo.add")
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_success")
remove_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_remove")
executor.process_update([package.filepath for package in package_python_schedule.packages.values()])
repo_add_mock.assert_has_calls([
@ -219,6 +233,7 @@ def test_process_update_group(executor: Executor, package_python_schedule: Packa
for package in package_python_schedule.packages.values()
], any_order=True)
status_client_mock.assert_called_once_with(package_python_schedule)
remove_mock.assert_called_once_with([])
def test_process_empty_filename(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
@ -226,7 +241,8 @@ def test_process_empty_filename(executor: Executor, package_ahriman: Package, mo
must skip update for package which does not have path
"""
package_ahriman.packages[package_ahriman.base].filename = None
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
mocker.patch("ahriman.core.repository.executor.Executor.load_archives", return_value=[package_ahriman])
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
executor.process_update([package.filepath for package in package_ahriman.packages.values()])
@ -235,18 +251,27 @@ def test_process_update_failed(executor: Executor, package_ahriman: Package, moc
must process update for failed package
"""
mocker.patch("shutil.move", side_effect=Exception())
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
mocker.patch("ahriman.core.repository.executor.Executor.load_archives", return_value=[package_ahriman])
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_failed")
executor.process_update([package.filepath for package in package_ahriman.packages.values()])
status_client_mock.assert_called_once()
def test_process_update_failed_on_load(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
def test_process_update_removed_package(executor: Executor, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must process update even with failed package load
must remove packages which have been removed from the new base
"""
mocker.patch("shutil.move")
mocker.patch("ahriman.models.package.Package.load", side_effect=Exception())
without_python2 = Package.from_json(package_python_schedule.view())
del without_python2.packages["python2-schedule"]
assert executor.process_update([package.filepath for package in package_ahriman.packages.values()])
mocker.patch("shutil.move")
mocker.patch("ahriman.core.alpm.repo.Repo.add")
mocker.patch("ahriman.core.repository.executor.Executor.load_archives", return_value=[without_python2])
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_python_schedule])
remove_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_remove")
executor.process_update([package.filepath for package in without_python2.packages.values()])
remove_mock.assert_called_once_with(["python2-schedule"])

View File

@ -5,8 +5,8 @@ from ahriman.core.repository import Repository
from ahriman.models.package import Package
def test_packages(package_ahriman: Package, package_python_schedule: Package,
repository: Repository, mocker: MockerFixture) -> None:
def test_load_archives(package_ahriman: Package, package_python_schedule: Package,
repository: Repository, mocker: MockerFixture) -> None:
"""
must return all packages grouped by package base
"""
@ -17,12 +17,9 @@ def test_packages(package_ahriman: Package, package_python_schedule: Package,
packages={package: props})
for package, props in package_python_schedule.packages.items()
] + [package_ahriman]
mocker.patch("pathlib.Path.iterdir",
return_value=[Path("a.pkg.tar.xz"), Path("b.pkg.tar.xz"), Path("c.pkg.tar.xz")])
mocker.patch("ahriman.models.package.Package.load", side_effect=single_packages)
packages = repository.packages()
packages = repository.load_archives([Path("a.pkg.tar.xz"), Path("b.pkg.tar.xz"), Path("c.pkg.tar.xz")])
assert len(packages) == 2
assert {package.base for package in packages} == {package_ahriman.base, package_python_schedule.base}
@ -33,21 +30,48 @@ def test_packages(package_ahriman: Package, package_python_schedule: Package,
assert set(archives) == expected
def test_packages_failed(repository: Repository, mocker: MockerFixture) -> None:
def test_load_archives_failed(repository: Repository, mocker: MockerFixture) -> None:
"""
must skip packages which cannot be loaded
"""
mocker.patch("pathlib.Path.iterdir", return_value=[Path("a.pkg.tar.xz")])
mocker.patch("ahriman.models.package.Package.load", side_effect=Exception())
assert not repository.packages()
assert not repository.load_archives([Path("a.pkg.tar.xz")])
def test_packages_not_package(repository: Repository, mocker: MockerFixture) -> None:
def test_load_archives_not_package(repository: Repository) -> None:
"""
must skip not packages from iteration
"""
mocker.patch("pathlib.Path.iterdir", return_value=[Path("a.tar.xz")])
assert not repository.packages()
assert not repository.load_archives([Path("a.tar.xz")])
def test_load_archives_different_version(repository: Repository, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must load packages with different versions choosing maximal
"""
single_packages = [
Package(base=package_python_schedule.base,
version=package_python_schedule.version,
aur_url=package_python_schedule.aur_url,
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)
packages = repository.load_archives([Path("a.pkg.tar.xz"), Path("b.pkg.tar.xz")])
assert len(packages) == 1
assert packages[0].version == package_python_schedule.version
def test_packages(repository: Repository, mocker: MockerFixture) -> None:
"""
must return repository packages
"""
load_mock = mocker.patch("ahriman.core.repository.repository.Repository.load_archives")
repository.packages()
load_mock.assert_called_once() # it uses filter object so we cannot verity argument list =/
def test_packages_built(repository: Repository, mocker: MockerFixture) -> None:

View File

@ -4,6 +4,7 @@ import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from unittest import mock
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import InitializeException
@ -54,6 +55,64 @@ def test_section_name(configuration: Configuration) -> None:
assert configuration.section_name("build", "x86_64") == "build:x86_64"
def test_getlist(configuration: Configuration) -> None:
"""
must return list of string correctly
"""
configuration.set_option("build", "test_list", "a b c")
assert configuration.getlist("build", "test_list") == ["a", "b", "c"]
def test_getlist_empty(configuration: Configuration) -> None:
"""
must return list of string correctly for non-existing option
"""
assert configuration.getlist("build", "test_list", fallback=[]) == []
configuration.set_option("build", "test_list", "")
assert configuration.getlist("build", "test_list") == []
def test_getlist_single(configuration: Configuration) -> None:
"""
must return list of strings for single string
"""
configuration.set_option("build", "test_list", "a")
assert configuration.getlist("build", "test_list") == ["a"]
assert configuration.getlist("build", "test_list") == ["a"]
def test_getlist_with_spaces(configuration: Configuration) -> None:
"""
must return list of string if there is string with spaces in quotes
"""
configuration.set_option("build", "test_list", """"ahriman is" cool""")
assert configuration.getlist("build", "test_list") == ["""ahriman is""", """cool"""]
configuration.set_option("build", "test_list", """'ahriman is' cool""")
assert configuration.getlist("build", "test_list") == ["""ahriman is""", """cool"""]
def test_getlist_with_quotes(configuration: Configuration) -> None:
"""
must return list of string if there is string with quote inside quote
"""
configuration.set_option("build", "test_list", """"ahriman is" c"'"ool""")
assert configuration.getlist("build", "test_list") == ["""ahriman is""", """c'ool"""]
configuration.set_option("build", "test_list", """'ahriman is' c'"'ool""")
assert configuration.getlist("build", "test_list") == ["""ahriman is""", """c"ool"""]
def test_getlist_unmatched_quote(configuration: Configuration) -> None:
"""
must raise exception on unmatched quote in string value
"""
configuration.set_option("build", "test_list", """ahri"man is cool""")
with pytest.raises(ValueError):
configuration.getlist("build", "test_list")
configuration.set_option("build", "test_list", """ahri'man is cool""")
with pytest.raises(ValueError):
configuration.getlist("build", "test_list")
def test_getpath_absolute_to_absolute(configuration: Configuration) -> None:
"""
must not change path for absolute path in settings
@ -94,32 +153,6 @@ def test_getpath_without_fallback(configuration: Configuration) -> None:
assert configuration.getpath("build", "option")
def test_getlist(configuration: Configuration) -> None:
"""
must return list of string correctly
"""
configuration.set_option("build", "test_list", "a b c")
assert configuration.getlist("build", "test_list") == ["a", "b", "c"]
def test_getlist_empty(configuration: Configuration) -> None:
"""
must return list of string correctly for non-existing option
"""
assert configuration.getlist("build", "test_list", fallback=[]) == []
configuration.set_option("build", "test_list", "")
assert configuration.getlist("build", "test_list") == []
def test_getlist_single(configuration: Configuration) -> None:
"""
must return list of strings for single string
"""
configuration.set_option("build", "test_list", "a")
assert configuration.getlist("build", "test_list") == ["a"]
assert configuration.getlist("build", "test_list") == ["a"]
def test_gettype(configuration: Configuration) -> None:
"""
must extract type from variable
@ -222,6 +255,17 @@ def test_reload(configuration: Configuration, mocker: MockerFixture) -> None:
merge_mock.assert_called_once()
def test_reload_clear(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must clear current settings before configuration reload
"""
clear_mock = mocker.patch("ahriman.core.configuration.Configuration.remove_section")
sections = configuration.sections()
configuration.reload()
clear_mock.assert_has_calls([mock.call(section) for section in sections])
def test_reload_no_architecture(configuration: Configuration) -> None:
"""
must raise exception on reload if no architecture set

View File

@ -1,4 +1,5 @@
import datetime
import time
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
@ -45,6 +46,17 @@ def test_build_status_init_2(build_status_failed: BuildStatus) -> None:
assert status == build_status_failed
def test_build_status_init_empty_timestamp() -> None:
"""
must st current timestamp when not set
"""
first = BuildStatus()
time.sleep(1)
second = BuildStatus()
# well technically it just should increase
assert first.timestamp < second.timestamp
def test_build_status_from_json_view(build_status_failed: BuildStatus) -> None:
"""
must construct same object from json

View File

@ -6,6 +6,7 @@ from unittest.mock import MagicMock, PropertyMock
from ahriman.core.exceptions import InvalidPackageInfo
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
from ahriman.models.repository_paths import RepositoryPaths
@ -156,14 +157,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_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
"""
mocker.patch("pathlib.Path.is_file", return_value=True)
load_mock = mocker.patch("ahriman.models.package.Package.from_archive")
Package.load(Path("path"), pyalpm_handle, package_ahriman.aur_url)
Package.load("path", PackageSource.Archive, pyalpm_handle, package_ahriman.aur_url)
load_mock.assert_called_once()
@ -172,8 +183,7 @@ def test_load_from_aur(package_ahriman: Package, pyalpm_handle: MagicMock, mocke
must load package from AUR
"""
load_mock = mocker.patch("ahriman.models.package.Package.from_aur")
Package.load(Path("path"), pyalpm_handle, package_ahriman.aur_url)
Package.load("path", PackageSource.AUR, pyalpm_handle, package_ahriman.aur_url)
load_mock.assert_called_once()
@ -181,10 +191,8 @@ def test_load_from_build(package_ahriman: Package, pyalpm_handle: MagicMock, moc
"""
must load package from build directory
"""
mocker.patch("pathlib.Path.is_dir", return_value=True)
load_mock = mocker.patch("ahriman.models.package.Package.from_build")
Package.load(Path("path"), pyalpm_handle, package_ahriman.aur_url)
Package.load("path", PackageSource.Local, pyalpm_handle, package_ahriman.aur_url)
load_mock.assert_called_once()
@ -192,13 +200,26 @@ def test_load_failure(package_ahriman: Package, pyalpm_handle: MagicMock, mocker
"""
must raise InvalidPackageInfo on exception
"""
mocker.patch("pathlib.Path.is_dir", side_effect=InvalidPackageInfo("exception!"))
mocker.patch("ahriman.models.package.Package.from_aur", side_effect=InvalidPackageInfo("exception!"))
with pytest.raises(InvalidPackageInfo):
Package.load(Path("path"), pyalpm_handle, package_ahriman.aur_url)
Package.load("path", PackageSource.AUR, pyalpm_handle, package_ahriman.aur_url)
mocker.patch("pathlib.Path.is_dir", side_effect=Exception())
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("path"), pyalpm_handle, package_ahriman.aur_url)
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: