implement support of rollback handler

This commit is contained in:
2026-03-16 10:06:15 +02:00
parent 96684d7ddd
commit 7d20d24d86
32 changed files with 834 additions and 246 deletions

View File

@@ -164,6 +164,14 @@ ahriman.application.handlers.restore module
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.rollback module
--------------------------------------------
.. automodule:: ahriman.application.handlers.rollback
:members:
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.run module
---------------------------------------

View File

@@ -252,6 +252,14 @@ ahriman.web.schemas.package\_version\_schema module
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.packager\_schema module
-------------------------------------------
.. automodule:: ahriman.web.schemas.packager_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.pagination\_schema module
---------------------------------------------
@@ -332,6 +340,14 @@ ahriman.web.schemas.repository\_stats\_schema module
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.rollback\_schema module
-------------------------------------------
.. automodule:: ahriman.web.schemas.rollback_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.search\_schema module
-----------------------------------------

View File

@@ -68,6 +68,14 @@ ahriman.web.views.v1.service.request module
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.service.rollback module
--------------------------------------------
.. automodule:: ahriman.web.views.v1.service.rollback
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.service.search module
------------------------------------------

View File

@@ -156,12 +156,15 @@ class ApplicationRepository(ApplicationProperties):
result = Result()
# process already built packages if any
built_packages = self.repository.packages_built()
if built_packages: # speedup a bit
if built_packages := self.repository.packages_built(): # speedup a bit
build_result = self.repository.process_update(built_packages, packagers)
self.on_result(build_result)
result.merge(build_result)
# filter packages which were prebuilt
succeeded = {package.base for package in build_result.success}
updates = [package for package in updates if package.base not in succeeded]
builder = Updater.load(self.repository_id, self.configuration, self.repository)
# ok so for now we split all packages into chunks and process each chunk accordingly

View File

@@ -21,10 +21,10 @@ import argparse
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.application.handlers.update import Update
from ahriman.core.configuration import Configuration
from ahriman.core.utils import enum_values, extract_user
from ahriman.models.package_source import PackageSource
from ahriman.models.packagers import Packagers
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_id import RepositoryId
@@ -48,26 +48,7 @@ class Add(Handler):
"""
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
application.on_start()
application.add(args.package, args.source, args.username)
patches = [PkgbuildPatch.from_env(patch) for patch in args.variable] if args.variable is not None else []
for package in args.package: # for each requested package insert patch
for patch in patches:
application.reporter.package_patches_update(package, patch)
if not args.now:
return
packages = application.updates(args.package, aur=False, local=False, manual=True, vcs=False, check_files=False)
if args.changes: # generate changes if requested
application.changes(packages)
packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
packagers = Packagers(args.username, {package.base: package.packager for package in packages})
application.print_updates(packages, log_fn=application.logger.info)
result = application.update(packages, packagers, bump_pkgrel=args.increment)
Add.check_status(args.exit_code, not result.is_empty)
Add.perform_action(application, args)
@staticmethod
def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
@@ -103,14 +84,34 @@ class Add(Handler):
parser.add_argument("--increment", help="increment package release (pkgrel) version on duplicate",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-n", "--now", help="run update function after", action="store_true")
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
"-yy to force refresh even if up to date",
action="count", default=False)
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto)
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
parser.add_argument("-v", "--variable", help="apply specified makepkg variables to the next build",
action="append")
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
"-yy to force refresh even if up to date",
action="count", default=False)
parser.set_defaults(aur=False, check_files=False, dry_run=False, local=False, manual=True, vcs=False)
return parser
@staticmethod
def perform_action(application: Application, args: argparse.Namespace) -> None:
"""
perform add action
Args:
application(Application): application instance
args(argparse.Namespace): command line args
"""
application.add(args.package, args.source, args.username)
patches = [PkgbuildPatch.from_env(patch) for patch in args.variable] if args.variable is not None else []
for package in args.package: # for each requested package insert patch
for patch in patches:
application.reporter.package_patches_update(package, patch)
if not args.now:
return
Update.perform_action(application, args)
arguments = [_set_package_add_parser]

View File

@@ -0,0 +1,131 @@
#
# Copyright (c) 2021-2026 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/>.
#
import argparse
from dataclasses import replace
from pathlib import Path
from ahriman.application.application import Application
from ahriman.application.handlers.add import Add
from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.utils import extract_user
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
from ahriman.models.repository_id import RepositoryId
class Rollback(Handler):
"""
package rollback handler
"""
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None:
"""
callback for command line
Args:
args(argparse.Namespace): command line args
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
application = Application(repository_id, configuration, report=report)
application.on_start()
package = Rollback.package_load(application, args.package, args.version)
artifacts = Rollback.package_artifacts(application, package)
args.package = [str(artifact) for artifact in artifacts]
Add.perform_action(application, args)
if args.hold:
application.reporter.package_hold_update(package.base, enabled=True)
@staticmethod
def _set_package_rollback_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for package rollback subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("package-rollback", help="rollback package",
description="rollback package to specified version from archives")
parser.add_argument("package", help="package base")
parser.add_argument("version", help="package version")
parser.add_argument("--hold", help="hold package afterwards",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
parser.set_defaults(aur=False, changes=False, check_files=False, dependencies=False, dry_run=False,
exit_code=True, increment=False, now=True, local=False, manual=False, refresh=False,
source=PackageSource.Archive, variable=None, vcs=False)
return parser
@staticmethod
def package_artifacts(application: Application, package: Package) -> list[Path]:
"""
look for requested package artifacts and return paths to them
Args:
application(Application): application instance
package(Package): package descriptor
Returns:
list[Path]: paths to found artifacts
Raises:
UnknownPackageError: if artifacts do not exist
"""
# lookup for built artifacts
artifacts = application.repository.package_archives_lookup(package)
if not artifacts:
raise UnknownPackageError(package.base)
return artifacts
@staticmethod
def package_load(application: Application, package_base: str, version: str) -> Package:
"""
load package from repository, while setting requested version
Args:
application(Application): application instance
package_base(str): package base
version(str): package version
Returns:
Package: loaded package
Raises:
UnknownPackageError: if package does not exist
"""
try:
package, _ = next(iter(application.reporter.package_get(package_base)))
return replace(package, version=version)
except StopIteration:
raise UnknownPackageError(package_base) from None
arguments = [_set_package_rollback_parser]

View File

@@ -48,22 +48,7 @@ class Update(Handler):
"""
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
application.on_start()
packages = application.updates(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs,
check_files=args.check_files)
if args.changes: # generate changes if requested
application.changes(packages)
if args.dry_run: # exit from application if no build requested
Update.check_status(args.exit_code, packages) # status code check
return
packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
packagers = Packagers(args.username, {package.base: package.packager for package in packages})
application.print_updates(packages, log_fn=application.logger.info)
result = application.update(packages, packagers, bump_pkgrel=args.increment)
Update.check_status(args.exit_code, not result.is_empty)
Update.perform_action(application, args)
@staticmethod
def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
@@ -153,6 +138,31 @@ class Update(Handler):
return print(line) if dry_run else application.logger.info(line) # pylint: disable=bad-builtin
return inner
@staticmethod
def perform_action(application: Application, args: argparse.Namespace) -> None:
"""
perform update action
Args:
application(Application): application instance
args(argparse.Namespace): command line args
"""
packages = application.updates(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs,
check_files=args.check_files)
if args.changes: # generate changes if requested
application.changes(packages)
if args.dry_run: # exit from application if no build requested
Update.check_status(args.exit_code, packages) # status code check
return
packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
packagers = Packagers(args.username, {package.base: package.packager for package in packages})
application.print_updates(packages, log_fn=application.logger.info)
result = application.update(packages, packagers, bump_pkgrel=args.increment)
Update.check_status(args.exit_code, not result.is_empty)
arguments = [
_set_repo_check_parser,
_set_repo_update_parser,

View File

@@ -25,7 +25,6 @@ from pathlib import Path
from tempfile import TemporaryDirectory
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.build_tools.package_version import PackageVersion
from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.core.log import LazyLogging
@@ -89,19 +88,21 @@ class PackageInfo(LazyLogging):
return sorted(result)
def load_archives(self, packages: Iterable[Path]) -> list[Package]:
def load_archives(self, packages: Iterable[Path], *, latest_only: bool = True) -> list[Package]:
"""
load packages from list of archives
Args:
packages(Iterable[Path]): paths to package archives
latest_only(bool, optional): filter packages with the same base, keeping only fresh packages installed
(Default value = True)
Returns:
list[Package]: list of read packages
"""
sources = {package.base: package.remote for package, _, in self.reporter.package_get(None)}
result: dict[str, Package] = {}
result: dict[str, dict[str, Package]] = {}
# we are iterating over bases, not single packages
for full_path in packages:
try:
@@ -109,17 +110,23 @@ class PackageInfo(LazyLogging):
if (source := sources.get(local.base)) is not None: # update source with remote
local.remote = source
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 PackageVersion(current).is_outdated(local, self.configuration, calculate_version=False):
current.version = local.version
loaded_versions = result.setdefault(local.base, {})
current = loaded_versions.setdefault(local.version, local)
current.packages.update(local.packages)
except Exception:
self.logger.exception("could not load package from %s", full_path)
return list(result.values())
if latest_only:
comparator: Callable[[Package, Package], int] = lambda left, right: left.vercmp(right.version)
for package_base, versions in result.items():
newest = max(versions.values(), key=cmp_to_key(comparator))
result[package_base] = {newest.version: newest}
return [
package
for versions in result.values()
for package in versions.values()
]
def package_archives(self, package_base: str) -> list[Package]:
"""
@@ -133,16 +140,17 @@ class PackageInfo(LazyLogging):
Returns:
list[Package]: list of packages belonging to this base, sorted by version by ascension
"""
packages: dict[tuple[str, str], Package] = {}
# we can't use here load_archives, because it ignores versions
for full_path in filter(package_like, self.paths.archive_for(package_base).iterdir()):
local = Package.from_archive(full_path)
if not local.supports_architecture(self.repository_id.architecture):
continue
packages.setdefault((local.base, local.version), local).packages.update(local.packages)
archive = self.paths.archive_for(package_base)
if not archive.is_dir():
return []
packages = self.load_archives(filter(package_like, archive.iterdir()), latest_only=False)
comparator: Callable[[Package, Package], int] = lambda left, right: left.vercmp(right.version)
return sorted(packages.values(), key=cmp_to_key(comparator))
return sorted(
(package for package in packages if package.supports_architecture(self.repository_id.architecture)),
key=cmp_to_key(comparator),
)
def package_archives_lookup(self, package: Package) -> list[Path]:
"""
@@ -155,19 +163,11 @@ class PackageInfo(LazyLogging):
list[Path]: list of built packages and signatures if available, empty list otherwise
"""
archive = self.paths.archive_for(package.base)
if not archive.is_dir():
return []
for path in filter(package_like, archive.iterdir()):
# check if package version is the same
built = Package.from_archive(path)
for built in self.package_archives(package.base):
if built.version != package.version:
continue
# all packages must be either any or same architecture
if not built.supports_architecture(self.repository_id.architecture):
continue
return list_flatmap(built.packages.values(), lambda single: archive.glob(f"{single.filename}*"))
return []

View File

@@ -232,6 +232,27 @@ class Spawn(Thread, LazyLogging):
"""
return self._spawn_process(repository_id, "package-remove", *packages)
def packages_rollback(self, repository_id: RepositoryId, package: str, version: str, username: str | None, *,
hold: bool) -> str:
"""
rollback package
Args:
repository_id(RepositoryId): repository unique identifier
package(str): package base to rollback
version(str): package version to rollback
username(str | None): optional override of username for build process
hold(bool): hold package after rollback
Returns:
str: spawned process identifier
"""
kwargs = {
"username": username,
self.boolean_action_argument("hold", hold): "",
}
return self._spawn_process(repository_id, "package-rollback", package, version, **kwargs)
def packages_update(self, repository_id: RepositoryId, username: str | None, *,
aur: bool, local: bool, manual: bool, increment: bool, refresh: bool) -> str:
"""

View File

@@ -48,6 +48,7 @@ from ahriman.web.schemas.package_properties_schema import PackagePropertiesSchem
from ahriman.web.schemas.package_schema import PackageSchema
from ahriman.web.schemas.package_status_schema import PackageStatusSchema, PackageStatusSimplifiedSchema
from ahriman.web.schemas.package_version_schema import PackageVersionSchema
from ahriman.web.schemas.packager_schema import PackagerSchema
from ahriman.web.schemas.pagination_schema import PaginationSchema
from ahriman.web.schemas.patch_name_schema import PatchNameSchema
from ahriman.web.schemas.patch_schema import PatchSchema
@@ -58,6 +59,7 @@ from ahriman.web.schemas.process_schema import ProcessSchema
from ahriman.web.schemas.remote_schema import RemoteSchema
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
from ahriman.web.schemas.repository_stats_schema import RepositoryStatsSchema
from ahriman.web.schemas.rollback_schema import RollbackSchema
from ahriman.web.schemas.search_schema import SearchSchema
from ahriman.web.schemas.status_schema import StatusSchema
from ahriman.web.schemas.update_flags_schema import UpdateFlagsSchema

View File

@@ -17,10 +17,11 @@
# 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 ahriman.web.apispec import Schema, fields
from ahriman.web.apispec import fields
from ahriman.web.schemas.packager_schema import PackagerSchema
class BuildOptionsSchema(Schema):
class BuildOptionsSchema(PackagerSchema):
"""
request build options schema
"""
@@ -28,9 +29,6 @@ class BuildOptionsSchema(Schema):
increment = fields.Boolean(dump_default=True, metadata={
"description": "Increment pkgrel on conflicts",
})
packager = fields.String(metadata={
"description": "Packager identity if applicable",
})
refresh = fields.Boolean(dump_default=True, metadata={
"description": "Refresh pacman database"
})

View File

@@ -0,0 +1,30 @@
#
# Copyright (c) 2021-2026 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 ahriman.web.apispec import Schema, fields
class PackagerSchema(Schema):
"""
request packager schema
"""
packager = fields.String(metadata={
"description": "Packager identity if applicable",
})

View File

@@ -0,0 +1,40 @@
#
# Copyright (c) 2021-2026 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 ahriman import __version__
from ahriman.web.apispec import fields
from ahriman.web.schemas.packager_schema import PackagerSchema
class RollbackSchema(PackagerSchema):
"""
request schema for package rollback
"""
hold = fields.Boolean(dump_default=True, metadata={
"description": "Hold package after rollback",
})
package = fields.String(required=True, metadata={
"description": "Package name",
"example": "ahriman",
})
version = fields.String(required=True, metadata={
"description": "Package version",
"example": __version__,
})

View File

@@ -227,7 +227,7 @@ class BaseView(View, CorsViewMixin):
extract repository from request
Returns:
RepositoryIde: repository if possible to construct and first one otherwise
RepositoryId: repository if possible to construct and first one otherwise
"""
architecture = self.request.query.get("architecture")
name = self.request.query.get("repository")

View File

@@ -44,7 +44,6 @@ class AddView(BaseView):
description="Add new package(s) from AUR",
permission=POST_PERMISSION,
error_400_enabled=True,
error_404_description="Repository is unknown",
schema=ProcessIdSchema,
query_schema=RepositoryIdSchema,
body_schema=PackagePatchSchema,

View File

@@ -43,7 +43,6 @@ class RebuildView(BaseView):
description="Rebuild packages which depend on specified one",
permission=POST_PERMISSION,
error_400_enabled=True,
error_404_description="Repository is unknown",
schema=ProcessIdSchema,
query_schema=RepositoryIdSchema,
body_schema=PackageNamesSchema,

View File

@@ -43,7 +43,6 @@ class RemoveView(BaseView):
description="Remove specified packages from the repository",
permission=POST_PERMISSION,
error_400_enabled=True,
error_404_description="Repository is unknown",
schema=ProcessIdSchema,
query_schema=RepositoryIdSchema,
body_schema=PackageNamesSchema,

View File

@@ -44,7 +44,6 @@ class RequestView(BaseView):
description="Request new package(s) to be added from AUR",
permission=POST_PERMISSION,
error_400_enabled=True,
error_404_description="Repository is unknown",
schema=ProcessIdSchema,
query_schema=RepositoryIdSchema,
body_schema=PackagePatchSchema,

View File

@@ -0,0 +1,77 @@
#
# Copyright (c) 2021-2026 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 aiohttp.web import HTTPBadRequest, Response
from typing import ClassVar
from ahriman.models.user_access import UserAccess
from ahriman.web.apispec.decorators import apidocs
from ahriman.web.schemas import ProcessIdSchema, RepositoryIdSchema, RollbackSchema
from ahriman.web.views.base import BaseView
class RollbackView(BaseView):
"""
package rollback web view
Attributes:
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
POST_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
ROUTES = ["/api/v1/service/rollback"]
@apidocs(
tags=["Actions"],
summary="Rollback package",
description="Rollback package to specified version",
permission=POST_PERMISSION,
error_400_enabled=True,
schema=ProcessIdSchema,
query_schema=RepositoryIdSchema,
body_schema=RollbackSchema,
)
async def post(self) -> Response:
"""
run package rollback
Returns:
Response: 200 with spawned process id
Raises:
HTTPBadRequest: if bad data is supplied
"""
try:
data = await self.request.json()
package = self.get_non_empty(lambda key: data[key], "package")
version = self.get_non_empty(lambda key: data[key], "version")
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
repository_id = self.repository_id()
username = await self.username()
process_id = self.spawner.packages_rollback(
repository_id,
package,
version,
username,
hold=data.get("hold", True),
)
return self.json_response({"process_id": process_id})

View File

@@ -43,14 +43,13 @@ class UpdateView(BaseView):
description="Run repository update process",
permission=POST_PERMISSION,
error_400_enabled=True,
error_404_description="Repository is unknown",
schema=ProcessIdSchema,
query_schema=RepositoryIdSchema,
body_schema=UpdateFlagsSchema,
)
async def post(self) -> Response:
"""
run repository update. No parameters supported here
run repository update
Returns:
Response: 200 with spawned process id

View File

@@ -118,7 +118,6 @@ class UploadView(BaseView):
permission=POST_PERMISSION,
response_code=HTTPCreated,
error_400_enabled=True,
error_404_description="Repository is unknown",
query_schema=RepositoryIdSchema,
body_schema=FileSchema,
body_location="form",

View File

@@ -104,6 +104,7 @@ def _create_watcher(path: Path, repository_id: RepositoryId) -> Watcher:
package_info = PackageInfo()
package_info.configuration = configuration
package_info.paths = configuration.repository_paths
package_info.reporter = client
package_info.repository_id = repository_id
return Watcher(client, package_info)

View File

@@ -190,13 +190,14 @@ def test_update(application_repository: ApplicationRepository, package_ahriman:
"""
paths = [package.filepath for package in package_ahriman.packages.values()]
tree = Tree([Leaf(package_ahriman)])
prebuilt_result = Result()
resolve_mock = mocker.patch("ahriman.application.application.workers.local_updater.LocalUpdater.partition",
return_value=tree.levels())
mocker.patch("ahriman.core.repository.repository.Repository.packages_built", return_value=paths)
build_mock = mocker.patch("ahriman.application.application.workers.local_updater.LocalUpdater.update",
return_value=result)
update_mock = mocker.patch("ahriman.core.repository.Repository.process_update", return_value=result)
update_mock = mocker.patch("ahriman.core.repository.Repository.process_update", return_value=prebuilt_result)
on_result_mock = mocker.patch(
"ahriman.application.application.application_repository.ApplicationRepository.on_result")
@@ -204,7 +205,24 @@ def test_update(application_repository: ApplicationRepository, package_ahriman:
resolve_mock.assert_called_once_with([package_ahriman])
build_mock.assert_called_once_with([package_ahriman], Packagers("username"), bump_pkgrel=True)
update_mock.assert_called_once_with(paths, Packagers("username"))
on_result_mock.assert_has_calls([MockCall(result), MockCall(result)])
on_result_mock.assert_has_calls([MockCall(prebuilt_result), MockCall(result)])
def test_update_prebuilt_filter(application_repository: ApplicationRepository, package_ahriman: Package, result: Result,
mocker: MockerFixture) -> None:
"""
must filter out packages which were successfully prebuilt
"""
paths = [package.filepath for package in package_ahriman.packages.values()]
resolve_mock = mocker.patch("ahriman.application.application.workers.local_updater.LocalUpdater.partition",
return_value=[])
mocker.patch("ahriman.core.repository.repository.Repository.packages_built", return_value=paths)
mocker.patch("ahriman.core.repository.Repository.process_update", return_value=result)
mocker.patch("ahriman.application.application.application_repository.ApplicationRepository.on_result")
application_repository.update([package_ahriman], Packagers("username"), bump_pkgrel=True)
resolve_mock.assert_called_once_with([])
def test_update_empty(application_repository: ApplicationRepository, package_ahriman: Package, result: Result,

View File

@@ -3,14 +3,12 @@ import pytest
from pytest_mock import MockerFixture
from ahriman.application.application import Application
from ahriman.application.handlers.add import Add
from ahriman.core.configuration import Configuration
from ahriman.core.repository import Repository
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
from ahriman.models.packagers import Packagers
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.result import Result
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
@@ -24,13 +22,9 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
argparse.Namespace: generated arguments for these test cases
"""
args.package = ["ahriman"]
args.changes = True
args.exit_code = False
args.increment = True
args.now = False
args.refresh = 0
args.source = PackageSource.Auto
args.dependencies = True
args.username = "username"
args.variable = None
return args
@@ -43,103 +37,49 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
"""
args = _default_args(args)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
application_mock = mocker.patch("ahriman.application.application.Application.add")
dependencies_mock = mocker.patch("ahriman.application.application.Application.with_dependencies")
on_start_mock = mocker.patch("ahriman.application.application.Application.on_start")
perform_mock = mocker.patch("ahriman.application.handlers.add.Add.perform_action")
_, repository_id = configuration.check_loaded()
Add.run(args, repository_id, configuration, report=False)
application_mock.assert_called_once_with(args.package, args.source, args.username)
dependencies_mock.assert_not_called()
on_start_mock.assert_called_once_with()
perform_mock.assert_called_once_with(pytest.helpers.anyvar(int), args)
def test_run_with_patches(args: argparse.Namespace, configuration: Configuration, repository: Repository,
mocker: MockerFixture) -> None:
def test_perform_action(args: argparse.Namespace, application: Application, mocker: MockerFixture) -> None:
"""
must run command and insert temporary patches
must perform add action
"""
args = _default_args(args)
application_mock = mocker.patch("ahriman.application.application.Application.add")
update_mock = mocker.patch("ahriman.application.handlers.update.Update.perform_action")
Add.perform_action(application, args)
application_mock.assert_called_once_with(args.package, args.source, args.username)
update_mock.assert_not_called()
def test_perform_action_with_patches(args: argparse.Namespace, application: Application, mocker: MockerFixture) -> None:
"""
must perform add action and insert temporary patches
"""
args = _default_args(args)
args.variable = ["KEY=VALUE"]
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
mocker.patch("ahriman.application.application.Application.add")
application_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_patches_update")
patches_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_patches_update")
_, repository_id = configuration.check_loaded()
Add.run(args, repository_id, configuration, report=False)
application_mock.assert_called_once_with(args.package[0], PkgbuildPatch("KEY", "VALUE"))
Add.perform_action(application, args)
patches_mock.assert_called_once_with(args.package[0], PkgbuildPatch("KEY", "VALUE"))
def test_run_with_updates(args: argparse.Namespace, configuration: Configuration, repository: Repository,
package_ahriman: Package, mocker: MockerFixture) -> None:
def test_perform_action_with_updates(args: argparse.Namespace, application: Application, mocker: MockerFixture) -> None:
"""
must run command with updates after
must perform add action with updates after
"""
args = _default_args(args)
args.now = True
result = Result()
result.add_updated(package_ahriman)
mocker.patch("ahriman.application.application.Application.add")
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
application_mock = mocker.patch("ahriman.application.application.Application.update", return_value=result)
check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status")
changes_mock = mocker.patch("ahriman.application.application.Application.changes")
updates_mock = mocker.patch("ahriman.application.application.Application.updates", return_value=[package_ahriman])
dependencies_mock = mocker.patch("ahriman.application.application.Application.with_dependencies",
return_value=[package_ahriman])
print_mock = mocker.patch("ahriman.application.application.Application.print_updates")
update_mock = mocker.patch("ahriman.application.handlers.update.Update.perform_action")
_, repository_id = configuration.check_loaded()
Add.run(args, repository_id, configuration, report=False)
updates_mock.assert_called_once_with(args.package,
aur=False, local=False, manual=True, vcs=False, check_files=False)
changes_mock.assert_called_once_with([package_ahriman])
application_mock.assert_called_once_with([package_ahriman],
Packagers(args.username, {package_ahriman.base: "packager"}),
bump_pkgrel=args.increment)
dependencies_mock.assert_called_once_with([package_ahriman], process_dependencies=args.dependencies)
check_mock.assert_called_once_with(False, True)
print_mock.assert_called_once_with([package_ahriman], log_fn=pytest.helpers.anyvar(int))
def test_run_no_changes(args: argparse.Namespace, configuration: Configuration, repository: Repository,
mocker: MockerFixture) -> None:
"""
must skip changes calculation during package addition
"""
args = _default_args(args)
args.now = True
args.changes = False
mocker.patch("ahriman.application.application.Application.add")
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
mocker.patch("ahriman.application.application.Application.update")
mocker.patch("ahriman.application.handlers.handler.Handler.check_status")
mocker.patch("ahriman.application.application.Application.updates")
mocker.patch("ahriman.application.application.Application.with_dependencies")
mocker.patch("ahriman.application.application.Application.print_updates")
changes_mock = mocker.patch("ahriman.application.application.Application.changes")
_, repository_id = configuration.check_loaded()
Add.run(args, repository_id, configuration, report=False)
changes_mock.assert_not_called()
def test_run_empty_exception(args: argparse.Namespace, configuration: Configuration, repository: Repository,
mocker: MockerFixture) -> None:
"""
must raise ExitCode exception on empty result
"""
args = _default_args(args)
args.now = True
args.exit_code = True
mocker.patch("ahriman.application.application.Application.add")
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
mocker.patch("ahriman.application.application.Application.update", return_value=Result())
mocker.patch("ahriman.application.application.Application.with_dependencies")
mocker.patch("ahriman.application.application.Application.updates")
mocker.patch("ahriman.application.application.Application.print_updates")
check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status")
_, repository_id = configuration.check_loaded()
Add.run(args, repository_id, configuration, report=False)
check_mock.assert_called_once_with(True, False)
Add.perform_action(application, args)
update_mock.assert_called_once_with(application, args)

View File

@@ -0,0 +1,113 @@
import argparse
import pytest
from pytest_mock import MockerFixture
from ahriman.application.application import Application
from ahriman.application.handlers.rollback import Rollback
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.repository import Repository
from ahriman.models.package import Package
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
"""
default arguments for these test cases
Args:
args(argparse.Namespace): command line arguments fixture
Returns:
argparse.Namespace: generated arguments for these test cases
"""
args.package = "ahriman"
args.version = "1.0.0-1"
args.hold = False
return args
def test_run(args: argparse.Namespace, configuration: Configuration, repository: Repository, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
artifacts = [package.filepath for package in package_ahriman.packages.values()]
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
on_start_mock = mocker.patch("ahriman.application.application.Application.on_start")
load_mock = mocker.patch("ahriman.application.handlers.rollback.Rollback.package_load",
return_value=package_ahriman)
artifacts_mock = mocker.patch("ahriman.application.handlers.rollback.Rollback.package_artifacts",
return_value=artifacts)
perform_mock = mocker.patch("ahriman.application.handlers.add.Add.perform_action")
hold_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_hold_update")
_, repository_id = configuration.check_loaded()
Rollback.run(args, repository_id, configuration, report=False)
on_start_mock.assert_called_once_with()
load_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman.base, args.version)
artifacts_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman)
perform_mock.assert_called_once_with(pytest.helpers.anyvar(int), args)
hold_mock.assert_not_called()
def test_run_hold(args: argparse.Namespace, configuration: Configuration, repository: Repository,
package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must hold package after rollback
"""
args = _default_args(args)
args.hold = True
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
mocker.patch("ahriman.application.application.Application.on_start")
mocker.patch("ahriman.application.handlers.rollback.Rollback.package_load", return_value=package_ahriman)
mocker.patch("ahriman.application.handlers.rollback.Rollback.package_artifacts", return_value=[])
mocker.patch("ahriman.application.handlers.add.Add.perform_action")
hold_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_hold_update")
_, repository_id = configuration.check_loaded()
Rollback.run(args, repository_id, configuration, report=False)
hold_mock.assert_called_once_with(package_ahriman.base, enabled=True)
def test_package_artifacts(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must return package artifacts
"""
artifacts = [package.filepath for package in package_ahriman.packages.values()]
lookup_mock = mocker.patch("ahriman.core.repository.Repository.package_archives_lookup", return_value=artifacts)
assert Rollback.package_artifacts(application, package_ahriman) == artifacts
lookup_mock.assert_called_once_with(package_ahriman)
def test_package_artifacts_empty(application: Application, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must raise UnknownPackageError if no artifacts found
"""
mocker.patch("ahriman.core.repository.Repository.package_archives_lookup", return_value=[])
with pytest.raises(UnknownPackageError):
Rollback.package_artifacts(application, package_ahriman)
def test_package_load(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must load package from reporter
"""
package_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_get",
return_value=[(package_ahriman, None)])
result = Rollback.package_load(application, package_ahriman.base, "2.0.0-1")
assert result.version == "2.0.0-1"
package_mock.assert_called_once_with(package_ahriman.base)
def test_package_load_unknown(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must raise UnknownPackageError if package not found
"""
mocker.patch("ahriman.core.status.local_client.LocalClient.package_get", return_value=[])
with pytest.raises(UnknownPackageError):
Rollback.package_load(application, package_ahriman.base, package_ahriman.version)

View File

@@ -39,26 +39,39 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
return args
def test_run(args: argparse.Namespace, package_ahriman: Package, configuration: Configuration, repository: Repository,
def test_run(args: argparse.Namespace, configuration: Configuration, repository: Repository,
mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
on_start_mock = mocker.patch("ahriman.application.application.Application.on_start")
perform_mock = mocker.patch("ahriman.application.handlers.update.Update.perform_action")
_, repository_id = configuration.check_loaded()
Update.run(args, repository_id, configuration, report=False)
on_start_mock.assert_called_once_with()
perform_mock.assert_called_once_with(pytest.helpers.anyvar(int), args)
def test_perform_action(args: argparse.Namespace, application: Application, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must perform update action
"""
args = _default_args(args)
result = Result()
result.add_updated(package_ahriman)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
application_mock = mocker.patch("ahriman.application.application.Application.update", return_value=result)
check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status")
dependencies_mock = mocker.patch("ahriman.application.application.Application.with_dependencies",
return_value=[package_ahriman])
updates_mock = mocker.patch("ahriman.application.application.Application.updates", return_value=[package_ahriman])
changes_mock = mocker.patch("ahriman.application.application.Application.changes")
on_start_mock = mocker.patch("ahriman.application.application.Application.on_start")
print_mock = mocker.patch("ahriman.application.application.Application.print_updates")
_, repository_id = configuration.check_loaded()
Update.run(args, repository_id, configuration, report=False)
Update.perform_action(application, args)
application_mock.assert_called_once_with([package_ahriman],
Packagers(args.username, {package_ahriman.base: "packager"}),
bump_pkgrel=args.increment)
@@ -67,35 +80,31 @@ def test_run(args: argparse.Namespace, package_ahriman: Package, configuration:
changes_mock.assert_called_once_with([package_ahriman])
dependencies_mock.assert_called_once_with([package_ahriman], process_dependencies=args.dependencies)
check_mock.assert_called_once_with(False, True)
on_start_mock.assert_called_once_with()
print_mock.assert_called_once_with([package_ahriman], log_fn=pytest.helpers.anyvar(int))
def test_run_empty_exception(args: argparse.Namespace, configuration: Configuration, repository: Repository,
mocker: MockerFixture) -> None:
def test_perform_action_empty_exception(args: argparse.Namespace, application: Application,
mocker: MockerFixture) -> None:
"""
must raise ExitCode exception on empty update list
"""
args = _default_args(args)
args.exit_code = True
args.dry_run = True
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
mocker.patch("ahriman.application.application.Application.updates", return_value=[])
check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status")
_, repository_id = configuration.check_loaded()
Update.run(args, repository_id, configuration, report=False)
Update.perform_action(application, args)
check_mock.assert_called_once_with(True, [])
def test_run_update_empty_exception(args: argparse.Namespace, package_ahriman: Package, configuration: Configuration,
repository: Repository, mocker: MockerFixture) -> None:
def test_perform_action_update_empty_exception(args: argparse.Namespace, application: Application,
package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must raise ExitCode exception on empty build result
"""
args = _default_args(args)
args.exit_code = True
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
mocker.patch("ahriman.application.application.Application.update", return_value=Result())
mocker.patch("ahriman.application.application.Application.updates", return_value=[package_ahriman])
mocker.patch("ahriman.application.application.Application.with_dependencies", return_value=[package_ahriman])
@@ -103,26 +112,23 @@ def test_run_update_empty_exception(args: argparse.Namespace, package_ahriman: P
mocker.patch("ahriman.application.application.Application.changes")
check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status")
_, repository_id = configuration.check_loaded()
Update.run(args, repository_id, configuration, report=False)
Update.perform_action(application, args)
check_mock.assert_called_once_with(True, False)
def test_run_dry_run(args: argparse.Namespace, package_ahriman: Package, configuration: Configuration,
repository: Repository, mocker: MockerFixture) -> None:
def test_perform_action_dry_run(args: argparse.Namespace, application: Application, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must run simplified command
"""
args = _default_args(args)
args.dry_run = True
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
application_mock = mocker.patch("ahriman.application.application.Application.update")
check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status")
updates_mock = mocker.patch("ahriman.application.application.Application.updates", return_value=[package_ahriman])
changes_mock = mocker.patch("ahriman.application.application.Application.changes")
_, repository_id = configuration.check_loaded()
Update.run(args, repository_id, configuration, report=False)
Update.perform_action(application, args)
updates_mock.assert_called_once_with(
args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs, check_files=args.check_files)
application_mock.assert_not_called()
@@ -130,22 +136,19 @@ def test_run_dry_run(args: argparse.Namespace, package_ahriman: Package, configu
check_mock.assert_called_once_with(False, [package_ahriman])
def test_run_no_changes(args: argparse.Namespace, configuration: Configuration, repository: Repository,
mocker: MockerFixture) -> None:
def test_perform_action_no_changes(args: argparse.Namespace, application: Application, mocker: MockerFixture) -> None:
"""
must skip changes calculation
"""
args = _default_args(args)
args.dry_run = True
args.changes = False
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
mocker.patch("ahriman.application.application.Application.update")
mocker.patch("ahriman.application.handlers.handler.Handler.check_status")
mocker.patch("ahriman.application.application.Application.updates")
changes_mock = mocker.patch("ahriman.application.application.Application.changes")
_, repository_id = configuration.check_loaded()
Update.run(args, repository_id, configuration, report=False)
Update.perform_action(application, args)
changes_mock.assert_not_called()

View File

@@ -271,6 +271,18 @@ def test_subparsers_package_add_option_variable_multiple(parser: argparse.Argume
assert args.variable == ["var1", "var2"]
def test_subparsers_package_add_repo_update(parser: argparse.ArgumentParser) -> None:
"""
package-add must have same keys as repo-update
"""
args = parser.parse_args(["package-add", "ahriman"])
reference_args = parser.parse_args(["repo-update"])
del args.now
del args.source
del args.variable
assert dir(args) == dir(reference_args)
def test_subparsers_package_archives(parser: argparse.ArgumentParser) -> None:
"""
package-archives command must imply action, exit code, info, lock, quiet, report and unsafe
@@ -325,6 +337,26 @@ def test_subparsers_package_changes_remove_package_changes(parser: argparse.Argu
assert dir(args) == dir(reference_args)
def test_subparsers_package_copy_option_architecture(parser: argparse.ArgumentParser) -> None:
"""
package-copy command must correctly parse architecture list
"""
args = parser.parse_args(["package-copy", "source", "ahriman"])
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "package-copy", "source", "ahriman"])
assert args.architecture == "x86_64"
def test_subparsers_package_copy_option_repository(parser: argparse.ArgumentParser) -> None:
"""
package-copy command must correctly parse repository list
"""
args = parser.parse_args(["package-copy", "source", "ahriman"])
assert args.repository is None
args = parser.parse_args(["-r", "repo", "package-copy", "source", "ahriman"])
assert args.repository == "repo"
def test_subparsers_package_pkgbuild(parser: argparse.ArgumentParser) -> None:
"""
package-pkgbuild command must imply action, exit code, lock, quiet, report and unsafe
@@ -363,26 +395,6 @@ def test_subparsers_package_pkgbuild_remove_package_pkgbuild(parser: argparse.Ar
assert dir(args) == dir(reference_args)
def test_subparsers_package_copy_option_architecture(parser: argparse.ArgumentParser) -> None:
"""
package-copy command must correctly parse architecture list
"""
args = parser.parse_args(["package-copy", "source", "ahriman"])
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "package-copy", "source", "ahriman"])
assert args.architecture == "x86_64"
def test_subparsers_package_copy_option_repository(parser: argparse.ArgumentParser) -> None:
"""
package-copy command must correctly parse repository list
"""
args = parser.parse_args(["package-copy", "source", "ahriman"])
assert args.repository is None
args = parser.parse_args(["-r", "repo", "package-copy", "source", "ahriman"])
assert args.repository == "repo"
def test_subparsers_package_remove_option_architecture(parser: argparse.ArgumentParser) -> None:
"""
package-remove command must correctly parse architecture list
@@ -403,6 +415,68 @@ def test_subparsers_package_remove_option_repository(parser: argparse.ArgumentPa
assert args.repository == "repo"
def test_subparsers_package_rollback(parser: argparse.ArgumentParser) -> None:
"""
package-rollback command must imply aur, changes, check-files, dependencies, dry-run, exit-code, increment, now,
local, manual, refresh, source, variable and vcs
"""
args = parser.parse_args(["package-rollback", "ahriman", "1.0.0-1"])
assert not args.aur
assert not args.changes
assert not args.check_files
assert not args.dependencies
assert not args.dry_run
assert args.exit_code
assert not args.increment
assert not args.local
assert not args.manual
assert args.now
assert not args.refresh
assert not args.vcs
assert args.variable is None
def test_subparsers_package_rollback_option_architecture(parser: argparse.ArgumentParser) -> None:
"""
package-rollback command must correctly parse architecture list
"""
args = parser.parse_args(["package-rollback", "ahriman", "1.0.0-1"])
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "package-rollback", "ahriman", "1.0.0-1"])
assert args.architecture == "x86_64"
def test_subparsers_package_rollback_option_repository(parser: argparse.ArgumentParser) -> None:
"""
package-rollback command must correctly parse repository list
"""
args = parser.parse_args(["package-rollback", "ahriman", "1.0.0-1"])
assert args.repository is None
args = parser.parse_args(["-r", "repo", "package-rollback", "ahriman", "1.0.0-1"])
assert args.repository == "repo"
def test_subparsers_package_rollback_option_hold(parser: argparse.ArgumentParser) -> None:
"""
package-rollback command must correctly parse hold option
"""
args = parser.parse_args(["package-rollback", "ahriman", "1.0.0-1"])
assert args.hold
args = parser.parse_args(["package-rollback", "ahriman", "1.0.0-1", "--no-hold"])
assert not args.hold
def test_subparsers_package_rollback_package_add(parser: argparse.ArgumentParser) -> None:
"""
package-rollback must have same keys as package-add
"""
args = parser.parse_args(["package-rollback", "ahriman", "1.0.0-1"])
reference_args = parser.parse_args(["package-add", "ahriman"])
del args.hold
del args.version
assert dir(args) == dir(reference_args)
def test_subparsers_package_status(parser: argparse.ArgumentParser) -> None:
"""
package-status command must imply lock, quiet, report and unsafe

View File

@@ -8,7 +8,6 @@ from unittest.mock import MagicMock
from ahriman.core.repository import Repository
from ahriman.models.changes import Changes
from ahriman.models.package import Package
from ahriman.models.repository_id import RepositoryId
def test_full_depends(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
@@ -93,18 +92,41 @@ def test_load_archives_different_version(repository: Repository, package_python_
assert packages[0].version == package_python_schedule.version
def test_load_archives_all_versions(repository: Repository, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must load packages with different versions keeping all when latest_only is False
"""
mocker.patch("ahriman.models.package.Package.from_archive",
side_effect=[package_ahriman, replace(package_ahriman, version="0.0.1-1")])
mocker.patch("ahriman.core.status.local_client.LocalClient.package_get", return_value=[])
packages = repository.load_archives([Path("a.pkg.tar.xz"), Path("b.pkg.tar.xz")], latest_only=False)
assert len(packages) == 2
def test_package_archives(repository: Repository, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must load package archives sorted by version
"""
mocker.patch("ahriman.core.repository.package_info.package_like", return_value=True)
mocker.patch("pathlib.Path.iterdir", return_value=[str(i) for i in range(5)])
mocker.patch("ahriman.models.package.Package.from_archive",
side_effect=lambda version: replace(package_ahriman, version=version))
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("pathlib.Path.iterdir")
load_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives",
return_value=[replace(package_ahriman, version=str(i)) for i in range(5)])
result = repository.package_archives(package_ahriman.base)
assert len(result) == 5
assert [p.version for p in result] == [str(i) for i in range(5)]
load_mock.assert_called_once_with(pytest.helpers.anyvar(int), latest_only=False)
def test_package_archives_no_directory(repository: Repository, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must return empty list if archive directory does not exist
"""
mocker.patch("pathlib.Path.is_dir", return_value=False)
assert repository.package_archives(package_ahriman.base) == []
def test_package_archives_architecture_mismatch(repository: Repository, package_ahriman: Package,
@@ -114,8 +136,10 @@ def test_package_archives_architecture_mismatch(repository: Repository, package_
"""
package_ahriman.packages[package_ahriman.base].architecture = "i686"
mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.packages[package_ahriman.base].filepath])
mocker.patch("ahriman.models.package.Package.from_archive", return_value=package_ahriman)
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("pathlib.Path.iterdir")
mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives",
return_value=[package_ahriman])
result = repository.package_archives(package_ahriman.base)
assert len(result) == 0
@@ -126,13 +150,7 @@ def test_package_archives_lookup(repository: Repository, package_ahriman: Packag
"""
must existing packages which match the version
"""
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("pathlib.Path.iterdir", return_value=[
Path("1.pkg.tar.zst"),
Path("2.pkg.tar.zst"),
Path("3.pkg.tar.zst"),
])
mocker.patch("ahriman.models.package.Package.from_archive", side_effect=[
archives_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives", return_value=[
package_ahriman,
package_python_schedule,
replace(package_ahriman, version="1"),
@@ -140,6 +158,7 @@ def test_package_archives_lookup(repository: Repository, package_ahriman: Packag
glob_mock = mocker.patch("pathlib.Path.glob", return_value=[Path("1.pkg.tar.xz")])
assert repository.package_archives_lookup(package_ahriman) == [Path("1.pkg.tar.xz")]
archives_mock.assert_called_once_with(package_ahriman.base)
glob_mock.assert_called_once_with(f"{package_ahriman.packages[package_ahriman.base].filename}*")
@@ -148,12 +167,8 @@ def test_package_archives_lookup_version_mismatch(repository: Repository, packag
"""
must return nothing if no packages found with the same version
"""
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("pathlib.Path.iterdir", return_value=[
Path("1.pkg.tar.zst"),
])
mocker.patch("ahriman.models.package.Package.from_archive", return_value=replace(package_ahriman, version="1"))
mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives",
return_value=[replace(package_ahriman, version="1")])
assert repository.package_archives_lookup(package_ahriman) == []
@@ -162,14 +177,7 @@ def test_package_archives_lookup_architecture_mismatch(repository: Repository, p
"""
must return nothing if architecture doesn't match
"""
package_ahriman.packages[package_ahriman.base].architecture = "x86_64"
mocker.patch("pathlib.Path.is_dir", return_value=True)
repository.repository_id = RepositoryId("i686", repository.repository_id.name)
mocker.patch("pathlib.Path.iterdir", return_value=[
Path("1.pkg.tar.zst"),
])
mocker.patch("ahriman.models.package.Package.from_archive", return_value=package_ahriman)
mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives", return_value=[])
assert repository.package_archives_lookup(package_ahriman) == []
@@ -178,7 +186,7 @@ def test_package_archives_lookup_no_archive_directory(repository: Repository, pa
"""
must return nothing if no archive directory found
"""
mocker.patch("pathlib.Path.is_dir", return_value=False)
mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives", return_value=[])
assert repository.package_archives_lookup(package_ahriman) == []

View File

@@ -196,6 +196,26 @@ def test_packages_remove(spawner: Spawn, repository_id: RepositoryId, mocker: Mo
spawn_mock.assert_called_once_with(repository_id, "package-remove", "ahriman", "linux")
def test_packages_rollback(spawner: Spawn, repository_id: RepositoryId, mocker: MockerFixture) -> None:
"""
must call package rollback
"""
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process")
assert spawner.packages_rollback(repository_id, "ahriman", "1.0.0-1", "packager", hold=False)
spawn_mock.assert_called_once_with(repository_id, "package-rollback", "ahriman", "1.0.0-1",
**{"username": "packager", "no-hold": ""})
def test_packages_rollback_with_hold(spawner: Spawn, repository_id: RepositoryId, mocker: MockerFixture) -> None:
"""
must call package rollback with hold
"""
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process")
assert spawner.packages_rollback(repository_id, "ahriman", "1.0.0-1", "packager", hold=True)
spawn_mock.assert_called_once_with(repository_id, "package-rollback", "ahriman", "1.0.0-1",
**{"username": "packager", "hold": ""})
def test_packages_update(spawner: Spawn, repository_id: RepositoryId, mocker: MockerFixture) -> None:
"""
must call repo update

View File

@@ -0,0 +1 @@
# schema testing goes in view class tests

View File

@@ -0,0 +1 @@
# schema testing goes in view class tests

View File

@@ -0,0 +1,70 @@
import pytest
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from unittest.mock import AsyncMock
from ahriman.models.repository_id import RepositoryId
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v1.service.rollback import RollbackView
async def test_get_permission() -> None:
"""
must return correct permission for the request
"""
for method in ("POST",):
request = pytest.helpers.request("", "", method)
assert await RollbackView.get_permission(request) == UserAccess.Full
def test_routes() -> None:
"""
must return correct routes
"""
assert RollbackView.ROUTES == ["/api/v1/service/rollback"]
async def test_post(client: TestClient, repository_id: RepositoryId, mocker: MockerFixture) -> None:
"""
must call post request correctly
"""
rollback_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_rollback", return_value="abc")
user_mock = AsyncMock()
user_mock.return_value = "username"
mocker.patch("ahriman.web.views.base.BaseView.username", side_effect=user_mock)
request_schema = pytest.helpers.schema_request(RollbackView.post)
response_schema = pytest.helpers.schema_response(RollbackView.post)
payload = {"package": "ahriman", "version": "version"}
assert not request_schema.validate(payload)
response = await client.post("/api/v1/service/rollback", json=payload)
assert response.ok
rollback_mock.assert_called_once_with(repository_id, "ahriman", "version", "username", hold=True)
json = await response.json()
assert json["process_id"] == "abc"
assert not response_schema.validate(json)
async def test_post_empty(client: TestClient, mocker: MockerFixture) -> None:
"""
must call raise 400 on empty request
"""
rollback_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_rollback")
response_schema = pytest.helpers.schema_response(RollbackView.post, code=400)
response = await client.post("/api/v1/service/rollback", json={"package": "", "version": "version"})
assert response.status == 400
assert not response_schema.validate(await response.json())
rollback_mock.assert_not_called()
response = await client.post("/api/v1/service/rollback", json={"package": "ahriman", "version": ""})
assert response.status == 400
assert not response_schema.validate(await response.json())
rollback_mock.assert_not_called()
response = await client.post("/api/v1/service/rollback", json={})
assert response.status == 400
assert not response_schema.validate(await response.json())
rollback_mock.assert_not_called()