implement detailed build plan

This commit is contained in:
arcanis committed 2026-11-11 16:47:31 +02:00
1 parent dc4f2be65b
commit f6c94d59cc
18 files changed
+491 -40

No files matched your search

@@ -21,8 +21,9 @@ from collections.abc import Callable, Iterable
from ahriman.application.application.application_packages import ApplicationPackages
from ahriman.application.application.application_repository import ApplicationRepository
from ahriman.core.formatters import UpdatePrinter
from ahriman.core.formatters import BuildPlanPrinter, UpdatePrinter
from ahriman.core.tree import Tree
from ahriman.models.build_plan import BuildPlan
from ahriman.models.package import Package
from ahriman.models.result import Result
@@ -95,6 +96,21 @@ class Application(ApplicationPackages, ApplicationRepository):
"""
self.repository.triggers.on_stop()
def print_plan(self, plan: BuildPlan, *, log_fn: Callable[[str], None], bump_pkgrel: bool = False) -> None:
"""
print the packages and reasons in a build plan, grouped by dependency step
Args:
plan(BuildPlan): preview to display
log_fn(Callable[[str], None]): output function
bump_pkgrel(bool, optional): whether duplicate releases are expected to increment
"""
local_versions = {} if not plan.items else {
package.base: package.version for package in self.repository.packages()
}
BuildPlanPrinter(plan, local_versions, bump_pkgrel=bump_pkgrel)(
verbose=True, log_fn=log_fn, separator=" -> ")
def print_updates(self, packages: list[Package], *, log_fn: Callable[[str], None]) -> None:
"""
print list of packages to be built. This method will build dependency tree and print updates accordingly
@@ -23,6 +23,7 @@ from ahriman.application.application.application_properties import ApplicationPr
from ahriman.application.application.workers import Updater
from ahriman.core.build_tools.sources import Sources
from ahriman.core.exceptions import UnknownPackageError
from ahriman.models.build_plan import BuildPlan
from ahriman.models.package import Package
from ahriman.models.packagers import Packagers
from ahriman.models.result import Result
@@ -33,6 +34,53 @@ class ApplicationRepository(ApplicationProperties):
repository control class
"""
def _collect_updates(self, filter_packages: Iterable[str], *, aur: bool, local: bool, manual: bool, vcs: bool,
check_files: bool) -> BuildPlan:
"""
collect update candidates for execution or preview
Args:
filter_packages(Iterable[str]): restrict update checks to package bases
aur(bool): check AUR and official repository packages
local(bool): check locally sourced packages
manual(bool): include manually queued packages
vcs(bool): check VCS package versions
check_files(bool): check for broken implicit dependencies
Returns:
BuildPlan: selected update candidates and their reasons
"""
plan = BuildPlan()
updates = {}
# always add already built packages, because they will be always added
built = self.repository.load_archives(self.repository.packages_built())
updates.update({package.base: package for package in built})
for package in built:
plan.add(package, "already built")
if aur:
updates.update({package.base: package for package in self.repository.updates_aur(
filter_packages, vcs=vcs, plan=plan)})
if local:
updates.update({package.base: package for package in self.repository.updates_local(vcs=vcs, plan=plan)})
if manual:
updates.update({package.base: package for package in self.repository.updates_manual(plan=plan)})
if check_files:
updates.update({package.base: package for package in self.repository.updates_dependencies(
filter_packages, plan=plan)})
for package in updates.values():
plan.add(package)
selected = plan.packages
for package in selected:
requesters = [
candidate.base for candidate in selected if candidate.base != package.base and set(
package.packages_full).intersection(
candidate.depends_build)]
if requesters:
plan.add_dependency(package, requesters)
return plan
def changes(self, packages: Iterable[Package]) -> None:
"""
generate and update package changes
@@ -182,7 +230,7 @@ class ApplicationRepository(ApplicationProperties):
return result
def updates(self, filter_packages: Iterable[str], *,
aur: bool, local: bool, manual: bool, vcs: bool, check_files: bool) -> list[Package]:
aur: bool, local: bool, manual: bool, vcs: bool, check_files: bool) -> BuildPlan:
"""
get list of packages to run update process
@@ -193,25 +241,8 @@ class ApplicationRepository(ApplicationProperties):
manual(bool): include or exclude manual updates
vcs(bool): enable or disable checking of VCS packages
check_files(bool): check for broken dependencies
Returns:
list[Package]: list of out-of-dated packages
BuildPlan: selected packages and their reasons
"""
updates = {}
# always add already built packages, because they will be always added
updates.update({
package.base: package
for package in self.repository.load_archives(self.repository.packages_built())
})
if aur:
updates.update({package.base: package for package in self.repository.updates_aur(filter_packages, vcs=vcs)})
if local:
updates.update({package.base: package for package in self.repository.updates_local(vcs=vcs)})
if manual:
updates.update({package.base: package for package in self.repository.updates_manual()})
if check_files:
updates.update({package.base: package for package in self.repository.updates_dependencies(filter_packages)})
return [package for _, package in sorted(updates.items())]
return self._collect_updates(filter_packages, aur=aur, local=local, manual=manual, vcs=vcs,
check_files=check_files)
@@ -147,13 +147,16 @@ class Update(Handler):
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)
plan = application.updates(
args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs,
check_files=args.check_files)
packages = plan.packages
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
application.print_plan(plan, log_fn=print, bump_pkgrel=args.increment)
return
packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
@@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from ahriman.core.formatters.aur_printer import AurPrinter
from ahriman.core.formatters.build_plan_printer import BuildPlanPrinter
from ahriman.core.formatters.build_printer import BuildPrinter
from ahriman.core.formatters.changes_printer import ChangesPrinter
from ahriman.core.formatters.configuration_paths_printer import ConfigurationPathsPrinter
@@ -0,0 +1,84 @@
#
# 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 collections.abc import Callable
from ahriman.core.formatters.printer import Printer
from ahriman.core.formatters.update_printer import UpdatePrinter
from ahriman.core.tree import Tree
from ahriman.models.build_plan import BuildPlan
from ahriman.models.package import Package
class BuildPlanPrinter(Printer):
"""
print a build plan with dependency steps and package selection reasons
Attributes:
plan(BuildPlan): plan to display
local_versions(dict[str, str]): published package versions indexed by base
bump_pkgrel(bool): whether duplicate package releases are expected to increment
"""
def __init__(self, plan: BuildPlan, local_versions: dict[str, str], *, bump_pkgrel: bool = False) -> None:
"""
Args:
plan(BuildPlan): plan to display
local_versions(dict[str, str]): published package versions indexed by base
bump_pkgrel(bool, optional): show an expected release increment (Default value = False)
"""
self.plan = plan
self.local_versions = local_versions
self.bump_pkgrel = bump_pkgrel
def _print_package(self, package: Package, *, verbose: bool, log_fn: Callable[[str], None],
separator: str) -> None:
"""
print a package and its selection reasons
Args:
package(Package): package to display
verbose(bool): include package selection reasons
log_fn(Callable[[str], None]): output function
separator(str): version separator
"""
UpdatePrinter(package, self.local_versions.get(package.base), bump_pkgrel=self.bump_pkgrel)(
verbose=True, log_fn=log_fn, separator=separator)
reasons = self.plan.item(package.base).reasons
if verbose and reasons:
log_fn(f"\tReasons: {'; '.join(reasons)}")
def print(self, *, verbose: bool, log_fn: Callable[[str], None] = Printer._print,
separator: str = " -> ") -> None:
"""
print the plan as numbered dependency steps
Args:
verbose(bool): include package selection reasons
log_fn(Callable[[str], None], optional): output function
separator(str, optional): version separator (Default value = " -> ")
"""
if not self.plan.items:
log_fn("No packages selected")
return
for index, level in enumerate(Tree.resolve(self.plan.packages), start=1):
log_fn(f"Step {index}:")
for package in level:
self._print_package(package, verbose=verbose, log_fn=log_fn, separator=separator)
@@ -32,15 +32,17 @@ class UpdatePrinter(StringPrinter):
local_version(str | None): local version of the package if any
"""
def __init__(self, remote: Package, local_version: str | None) -> None:
def __init__(self, remote: Package, local_version: str | None, *, bump_pkgrel: bool = True) -> None:
"""
Args:
remote(Package): remote (new) package object
local_version(str | None): local version of the package if any
bump_pkgrel(bool, optional): show a release increment for duplicates (Default value = True)
"""
StringPrinter.__init__(self, remote.base)
self.package = remote
self.local_version = local_version
self.bump_pkgrel = bump_pkgrel
def properties(self) -> list[Property]:
"""
@@ -49,7 +51,7 @@ class UpdatePrinter(StringPrinter):
Returns:
list[Property]: list of content properties
"""
if (pkgrel := self.package.next_pkgrel(self.local_version)) is not None:
if self.bump_pkgrel and (pkgrel := self.package.next_pkgrel(self.local_version)) is not None:
epoch, pkgver, _ = parse_version(self.package.version)
effective_new_version = full_version(epoch, pkgver, pkgrel)
else:
@@ -24,6 +24,7 @@ from ahriman.core.build_tools.sources import Sources
from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.repository.cleaner import Cleaner
from ahriman.core.repository.package_info import PackageInfo
from ahriman.models.build_plan import BuildPlan
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.event import EventType
from ahriman.models.package import Package
@@ -47,13 +48,14 @@ class UpdateHandler(PackageInfo, Cleaner):
package.base for package, status in self.reporter.package_get(None) if status.is_held
}
def updates_aur(self, filter_packages: Iterable[str], *, vcs: bool) -> list[Package]:
def updates_aur(self, filter_packages: Iterable[str], *, vcs: bool, plan: BuildPlan | None = None) -> list[Package]:
"""
check AUR for updates
Args:
filter_packages(Iterable[str]): do not check every package just specified in the list
vcs(bool): enable or disable checking of VCS packages
plan(BuildPlan | None, optional): record update reasons for a preview
Returns:
list[Package]: list of packages which are out-of-dated
@@ -87,6 +89,10 @@ class UpdateHandler(PackageInfo, Cleaner):
self.reporter.set_pending(local.base)
self.event(local.base, EventType.PackageOutdated, "Remote version is newer than local")
result.append(remote)
if plan is not None:
reason = "official repository update" if local.remote.source == PackageSource.Repository \
else "AUR update"
plan.add(remote, reason)
else:
self.reporter.package_status_update(local.base, BuildStatusEnum.Success)
except Exception:
@@ -95,13 +101,14 @@ class UpdateHandler(PackageInfo, Cleaner):
return result
def updates_dependencies(self, filter_packages: Iterable[str]) -> list[Package]:
def updates_dependencies(self, filter_packages: Iterable[str], *, plan: BuildPlan | None = None) -> list[Package]:
"""
check packages which are required to be rebuilt based on dynamic dependencies
(e.g. linking, modules paths, etc.)
Args:
filter_packages(Iterable[str]): do not check every package just specified in the list
plan(BuildPlan | None, optional): record dependency reasons for a preview
Returns:
list[Package]: list of packages for which there is breaking linking
@@ -138,17 +145,20 @@ class UpdateHandler(PackageInfo, Cleaner):
self.reporter.set_pending(local.base)
self.event(local.base, EventType.PackageOutdated, "Implicit dependencies are broken")
result.append(local)
if plan is not None:
plan.add(local, "broken implicit dependency")
break
return result
def updates_local(self, *, vcs: bool) -> list[Package]:
def updates_local(self, *, vcs: bool, plan: BuildPlan | None = None) -> list[Package]:
"""
check local packages for updates
Args:
vcs(bool): enable or disable checking of VCS packages
plan(BuildPlan | None, optional): record update reasons for a preview
Returns:
list[Package]: list of local packages which are out-of-dated
@@ -188,12 +198,14 @@ class UpdateHandler(PackageInfo, Cleaner):
self.reporter.set_pending(local.base)
self.event(local.base, EventType.PackageOutdated, "Locally pulled sources are outdated")
result.append(remote)
if plan is not None:
plan.add(remote, "local source update")
except Exception:
self.logger.exception("could not process package at %s", cache_dir)
return result
def updates_manual(self) -> list[Package]:
def updates_manual(self, *, plan: BuildPlan | None = None) -> list[Package]:
"""
check for packages for which manual update has been requested
@@ -206,6 +218,8 @@ class UpdateHandler(PackageInfo, Cleaner):
try:
for local in self.database.build_queue_get():
result.append(local)
if plan is not None:
plan.add(local, "queued request")
if local.base not in known_bases:
self.reporter.set_unknown(local)
else:
@@ -0,0 +1,133 @@
#
# 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 collections.abc import Iterator
from dataclasses import dataclass, field
from ahriman.models.package import Package
@dataclass
class BuildPlanItem:
"""
one package and the reasons it appears in a build plan
Attributes:
package(Package): package selected for the plan
reasons(list[str]): reasons why the package was selected
"""
package: Package
reasons: list[str] = field(default_factory=list)
def add_reason(self, reason: str) -> None:
"""
add a reason unless it is already present
Args:
reason(str): reason for selecting the package
"""
if reason not in self.reasons:
self.reasons.append(reason)
class BuildPlan:
"""
in-memory preview of the packages selected for a build operation
Attributes:
_items(dict[str, BuildPlanItem]): plan items indexed by package base
"""
def __init__(self) -> None:
"""
initialize an empty build plan
"""
self._items: dict[str, BuildPlanItem] = {}
@property
def items(self) -> list[BuildPlanItem]:
"""
get plan items in stable package-base order
Returns:
list[BuildPlanItem]: selected plan items
"""
return [self._items[base] for base in sorted(self._items)]
@property
def packages(self) -> list[Package]:
"""
get packages in stable package-base order
Returns:
list[Package]: selected packages
"""
return [item.package for item in self.items]
def add(self, package: Package, reason: str | None = None) -> BuildPlanItem:
"""
add or update a package in the plan
Args:
package(Package): package selected for the plan
reason(str | None, optional): reason for selecting it
Returns:
BuildPlanItem: plan item for the package
"""
item = self._items.setdefault(package.base, BuildPlanItem(package))
item.package = package
if reason is not None:
item.add_reason(reason)
return item
def add_dependency(self, package: Package, requesters: list[str]) -> None:
"""
add a dependency and identify the selected packages that require it
Args:
package(Package): dependency package
requesters(list[str]): package bases requiring this dependency
"""
item = self.add(package)
for requester in sorted(requesters):
item.add_reason(f"required by {requester}")
if not requesters:
item.add_reason("required dependency")
def item(self, package_base: str) -> BuildPlanItem:
"""
get a plan item by package base
Args:
package_base(str): package base to look up
Returns:
BuildPlanItem: matching item
"""
return self._items[package_base]
def __iter__(self) -> Iterator[Package]:
"""Iterate over selected packages in stable package-base order."""
return iter(self.packages)
def __len__(self) -> int:
"""Return the number of selected packages."""
return len(self._items)
@@ -5,6 +5,7 @@ from unittest.mock import MagicMock, call as MockCall
from ahriman.application.application import Application
from ahriman.core.tree import Leaf, Tree
from ahriman.models.build_plan import BuildPlan
from ahriman.models.package import Package
from ahriman.models.result import Result
@@ -63,6 +64,31 @@ def test_print_updates(application: Application, package_ahriman: Package, mocke
print_mock.assert_called_once_with(verbose=True, log_fn=print, separator=" -> ")
def test_print_plan(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
"""A plan preview prints a dependency step, version, and selection reason."""
plan = BuildPlan()
plan.add(package_ahriman, "AUR update")
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman])
mocker.patch("ahriman.core.tree.Tree.resolve", return_value=[[package_ahriman]])
output: list[str] = []
application.print_plan(plan, log_fn=output.append)
assert output == [
"Step 1:", package_ahriman.base, f"\t{package_ahriman.version} -> {package_ahriman.version}",
"\tReasons: AUR update",
]
def test_print_empty_plan(application: Application) -> None:
"""An empty preview explains why it has no package lines."""
output: list[str] = []
application.print_plan(BuildPlan(), log_fn=output.append)
assert output == ["No packages selected"]
def test_with_dependencies(application: Application, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
@@ -7,6 +7,7 @@ from unittest.mock import call as MockCall
from ahriman.application.application.application_repository import ApplicationRepository
from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.tree import Leaf, Tree
from ahriman.models.build_plan import BuildPlan
from ahriman.models.changes import Changes
from ahriman.models.package import Package
from ahriman.models.packagers import Packagers
@@ -270,6 +271,30 @@ def test_updates_all(application_repository: ApplicationRepository, package_ahri
updates_deps_mock.assert_called_once_with([])
def test_updates_returns_plan(application_repository: ApplicationRepository, package_ahriman: Package,
package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""Update discovery records reasons in its build plan."""
mocker.patch("ahriman.core.repository.package_info.PackageInfo.packages_built", return_value=[Path("built")])
mocker.patch(
"ahriman.core.repository.package_info.PackageInfo.load_archives",
return_value=[package_python_schedule])
package_ahriman.packages[package_ahriman.base].make_depends = [package_python_schedule.base]
def collect(_filter: list[str], *, vcs: bool, plan: BuildPlan) -> list[Package]:
assert vcs
plan.add(package_ahriman, "AUR update")
return [package_ahriman]
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur", side_effect=collect)
plan = application_repository.updates([], aur=True, local=False, manual=False, vcs=True, check_files=False)
assert list(plan) == [package_ahriman, package_python_schedule]
assert plan.item(package_ahriman.base).reasons == ["AUR update"]
assert plan.item(package_python_schedule.base).reasons == ["already built", f"required by {package_ahriman.base}"]
def test_updates_disabled(application_repository: ApplicationRepository, mocker: MockerFixture) -> None:
"""
must get updates without anything
@@ -9,6 +9,7 @@ from ahriman.application.handlers.update import Update
from ahriman.core.configuration import Configuration
from ahriman.core.repository import Repository
from ahriman.models.package import Package
from ahriman.models.build_plan import BuildPlan
from ahriman.models.packagers import Packagers
from ahriman.models.result import Result
@@ -91,7 +92,8 @@ def test_perform_action_empty_exception(args: argparse.Namespace, application: A
args = _default_args(args)
args.exit_code = True
args.dry_run = True
mocker.patch("ahriman.application.application.Application.updates", return_value=[])
mocker.patch("ahriman.application.application.Application.updates", return_value=BuildPlan())
mocker.patch("ahriman.application.application.Application.print_plan")
check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status")
Update.perform_action(application, args)
@@ -106,7 +108,9 @@ def test_perform_action_update_empty_exception(args: argparse.Namespace, applica
args = _default_args(args)
args.exit_code = True
mocker.patch("ahriman.application.application.Application.update", return_value=Result())
mocker.patch("ahriman.application.application.Application.updates", return_value=[package_ahriman])
plan = BuildPlan()
plan.add(package_ahriman)
mocker.patch("ahriman.application.application.Application.updates", return_value=plan)
mocker.patch("ahriman.application.application.Application.with_dependencies", return_value=[package_ahriman])
mocker.patch("ahriman.application.application.Application.print_updates")
mocker.patch("ahriman.application.application.Application.changes")
@@ -125,14 +129,17 @@ def test_perform_action_dry_run(args: argparse.Namespace, application: Applicati
args.dry_run = True
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])
plan = BuildPlan()
plan.add(package_ahriman, "AUR update")
updates_mock = mocker.patch("ahriman.application.application.Application.updates", return_value=plan)
changes_mock = mocker.patch("ahriman.application.application.Application.changes")
print_mock = mocker.patch("ahriman.application.application.Application.print_plan")
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)
updates_mock.assert_called_once()
application_mock.assert_not_called()
changes_mock.assert_called_once_with([package_ahriman])
print_mock.assert_called_once_with(plan, log_fn=print, bump_pkgrel=args.increment)
check_mock.assert_called_once_with(False, [package_ahriman])
@@ -145,7 +152,8 @@ def test_perform_action_no_changes(args: argparse.Namespace, application: Applic
args.changes = False
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.updates", return_value=BuildPlan())
mocker.patch("ahriman.application.application.Application.print_plan")
changes_mock = mocker.patch("ahriman.application.application.Application.changes")
Update.perform_action(application, args)
@@ -0,0 +1,38 @@
from ahriman.core.formatters import BuildPlanPrinter
from ahriman.models.build_plan import BuildPlan
from ahriman.models.package import Package
def test_print_plan(package_ahriman: Package) -> None:
"""The plan printer owns step, version, and reason output."""
plan = BuildPlan()
plan.add(package_ahriman, "AUR update")
output: list[str] = []
BuildPlanPrinter(plan, {package_ahriman.base: package_ahriman.version})(
verbose=True, log_fn=output.append, separator=" -> ")
assert output == [
"Step 1:", package_ahriman.base,
f"\t{package_ahriman.version} -> {package_ahriman.version}", "\tReasons: AUR update",
]
def test_print_empty_plan() -> None:
"""The plan printer explains an empty preview."""
output: list[str] = []
BuildPlanPrinter(BuildPlan(), {})(verbose=True, log_fn=output.append)
assert output == ["No packages selected"]
def test_print_plan_without_reasons(package_ahriman: Package) -> None:
"""The concise printer mode omits reasons while retaining package versions."""
plan = BuildPlan()
plan.add(package_ahriman)
output: list[str] = []
BuildPlanPrinter(plan, {})(verbose=False, log_fn=output.append, separator=" -> ")
assert output == ["Step 1:", package_ahriman.base, f"\tN/A -> {package_ahriman.version}"]
@@ -34,6 +34,15 @@ def test_properties_keep_pkgrel(update_printer: UpdatePrinter) -> None:
]
def test_properties_no_increment(update_printer: UpdatePrinter) -> None:
"""The preview honors a disabled release increment."""
update_printer.local_version = update_printer.package.version
update_printer.bump_pkgrel = False
assert update_printer.properties() == [
Property(update_printer.local_version, update_printer.package.version, is_required=True),
]
def test_title(update_printer: UpdatePrinter) -> None:
"""
must return non-empty title
@@ -6,6 +6,7 @@ from typing import Any
from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.repository.update_handler import UpdateHandler
from ahriman.models.build_plan import BuildPlan
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.dependencies import Dependencies
from ahriman.models.event import EventType
@@ -117,8 +118,10 @@ def test_updates_aur_filter(update_handler: UpdateHandler, package_ahriman: Pack
return_value=[package_ahriman])
mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.is_outdated", return_value=True)
package_load_mock = mocker.patch("ahriman.models.package.Package.from_aur", return_value=package_ahriman)
plan = BuildPlan()
assert update_handler.updates_aur([package_ahriman.base], vcs=True) == [package_ahriman]
assert update_handler.updates_aur([package_ahriman.base], vcs=True, plan=plan) == [package_ahriman]
assert plan.item(package_ahriman.base).reasons == ["AUR update"]
packages_mock.assert_called_once_with([package_ahriman.base])
package_load_mock.assert_called_once_with(package_ahriman.base, None)
@@ -192,6 +195,7 @@ def test_updates_dependencies(update_handler: UpdateHandler, package_ahriman: Pa
return_value=[package_ahriman, package_python_schedule])
status_client_mock = mocker.patch("ahriman.core.status.Client.set_pending")
event_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.event")
plan = BuildPlan()
dependencies = {
package_ahriman.base: Dependencies({"usr/lib/python3.11/site-packages": ["python"]}),
package_python_schedule.base: Dependencies({"usr/lib/python3.12/site-packages": ["python"]}),
@@ -201,7 +205,8 @@ def test_updates_dependencies(update_handler: UpdateHandler, package_ahriman: Pa
mocker.patch("ahriman.core.alpm.pacman.Pacman.files",
return_value={"python": {"usr/lib/python3.12/site-packages"}})
assert update_handler.updates_dependencies(["filter"]) == [package_ahriman]
assert update_handler.updates_dependencies(["filter"], plan=plan) == [package_ahriman]
assert plan.item(package_ahriman.base).reasons == ["broken implicit dependency"]
packages_mock.assert_called_once_with(["filter"])
status_client_mock.assert_called_once_with(package_ahriman.base)
event_mock.assert_called_once_with(package_ahriman.base, EventType.PackageOutdated,
@@ -263,8 +268,10 @@ def test_updates_local(update_handler: UpdateHandler, package_ahriman: Package,
event_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.event")
package_is_outdated_mock = mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.is_outdated",
return_value=True)
plan = BuildPlan()
assert update_handler.updates_local(vcs=True) == [package_ahriman]
assert update_handler.updates_local(vcs=True, plan=plan) == [package_ahriman]
assert plan.item(package_ahriman.base).reasons == ["local source update"]
fetch_mock.assert_called_once_with(Path(package_ahriman.base), pytest.helpers.anyvar(int))
package_load_mock.assert_called_once_with(Path(package_ahriman.base), "x86_64", None)
status_client_mock.assert_called_once_with(package_ahriman.base)
@@ -368,8 +375,10 @@ def test_updates_manual_preserves_queue(update_handler: UpdateHandler, package_a
"""
update_handler.database.build_queue_insert(package_ahriman)
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman])
plan = BuildPlan()
assert update_handler.updates_manual() == [package_ahriman]
assert update_handler.updates_manual(plan=plan) == [package_ahriman]
assert plan.item(package_ahriman.base).reasons == ["queued request"]
assert update_handler.database.build_queue_get() == [package_ahriman]
@@ -0,0 +1,34 @@
from ahriman.models.build_plan import BuildPlan
from ahriman.models.package import Package
def test_add_and_merge_plan_items(package_ahriman: Package, package_python_schedule: Package) -> None:
"""A plan keeps one current package per base and accumulates distinct reasons."""
plan = BuildPlan()
item = plan.add(package_ahriman, "AUR update")
plan.add(package_ahriman, "AUR update")
plan.add(package_ahriman, "queued request")
plan.add(package_python_schedule)
assert item.reasons == ["AUR update", "queued request"]
assert plan.packages == sorted([package_ahriman, package_python_schedule], key=lambda package: package.base)
assert plan.item(package_ahriman.base) is item
def test_add_dependency(package_python_schedule: Package) -> None:
"""A plan records which selected package requires a dependency."""
plan = BuildPlan()
plan.add_dependency(package_python_schedule, ["ahriman", "python-ahriman"])
assert plan.item(package_python_schedule.base).reasons == ["required by ahriman", "required by python-ahriman"]
def test_add_dependency_without_requesters(package_python_schedule: Package) -> None:
"""A dependency without a known direct requester still receives a reason."""
plan = BuildPlan()
plan.add_dependency(package_python_schedule, [])
assert plan.item(package_python_schedule.base).reasons == ["required dependency"]