mirror of
https://github.com/arcan1s/ahriman.git
synced 2026-11-11 15:30:29 +00:00
implement detailed build plan
This commit is contained in:
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"]
|
||||
Reference in new issue
Block a user