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"]
+8
View File
@@ -4,6 +4,14 @@ ahriman.core.formatters package
Submodules
----------
ahriman.core.formatters.build\_plan\_printer module
----------------------------------------------------
.. automodule:: ahriman.core.formatters.build_plan_printer
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.formatters.aur\_printer module
-------------------------------------------
+8
View File
@@ -28,6 +28,14 @@ ahriman.models.auth\_settings module
:no-undoc-members:
:show-inheritance:
ahriman.models.build\_plan module
---------------------------------
.. automodule:: ahriman.models.build_plan
:members:
:no-undoc-members:
:show-inheritance:
ahriman.models.build\_status module
-----------------------------------
+2
View File
@@ -274,6 +274,8 @@ In this scenario, the update process must be separated into several stages. Firs
sudo -u ahriman ahriman repo-check
The command prints selected packages in dependency steps, with the old and proposed versions and the reason each package was selected. ``repo-update --dry-run`` uses the same preview. Proposed ``pkgrel`` values follow the ``--increment`` option. The preview describes current metadata; it does not freeze sources or guarantee later build inputs.
During the check process, the service will generate changes from the last known commit and will send it to remote service. In order to verify source files changes, the web interface or special subcommand can be used:
.. code-block:: shell