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

@@ -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"]