diff --git a/docs/ahriman.1 b/docs/ahriman.1 index ba13df92..7b489aab 100644 --- a/docs/ahriman.1 +++ b/docs/ahriman.1 @@ -3,7 +3,7 @@ ahriman .SH SYNOPSIS .B ahriman -[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-report] [-q] [--unsafe] [-V] {aur-search,search,help,help-commands-unsafe,key-import,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,repo-backup,repo-check,check,repo-clean,clean,repo-config,config,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-sync,sync,repo-triggers,repo-update,update,shell,user-add,user-list,user-remove,version,web} ... +[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-report] [-q] [--unsafe] [-V] {aur-search,search,help,help-commands-unsafe,key-import,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,patch-set-add,repo-backup,repo-check,check,repo-clean,clean,repo-config,config,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-sync,sync,repo-triggers,repo-update,update,shell,user-add,user-list,user-remove,version,web} ... .SH DESCRIPTION ArcH linux ReposItory MANager @@ -71,7 +71,7 @@ remove package status update package status .TP \fBahriman\fR \fI\,patch-add\/\fR -add patch set +add patch for PKGBUILD function .TP \fBahriman\fR \fI\,patch-list\/\fR list patch sets @@ -79,6 +79,9 @@ list patch sets \fBahriman\fR \fI\,patch-remove\/\fR remove patch set .TP +\fBahriman\fR \fI\,patch-set-add\/\fR +add patch set +.TP \fBahriman\fR \fI\,repo-backup\/\fR backup repository data .TP @@ -284,21 +287,25 @@ set status for specified packages. If no packages supplied, service status will new status .SH COMMAND \fI\,'ahriman patch-add'\/\fR -usage: ahriman patch-add [-h] [-t TRACK] package +usage: ahriman patch-add [-h] [-p PATCH] package variable -create or update source patches +create or update patched PKGBUILD function or variable .TP \fBpackage\fR -path to directory with changed files for patch addition/update +package base + +.TP +\fBvariable\fR +PKGBUILD variable or function name. If variable is a function, it must end with () .SH OPTIONS \fI\,'ahriman patch-add'\/\fR .TP -\fB\-t\fR \fI\,TRACK\/\fR, \fB\-\-track\fR \fI\,TRACK\/\fR -files which has to be tracked +\fB\-p\fR \fI\,PATCH\/\fR, \fB\-\-patch\fR \fI\,PATCH\/\fR +path to file which contains function or variable value. If not set, the value will be read from stdin .SH COMMAND \fI\,'ahriman patch-list'\/\fR -usage: ahriman patch-list [-h] [-e] [package] +usage: ahriman patch-list [-h] [-e] [-v VARIABLE] [package] list available patches for the package @@ -311,8 +318,12 @@ package base \fB\-e\fR, \fB\-\-exit\-code\fR return non\-zero exit status if result is empty +.TP +\fB\-v\fR \fI\,VARIABLE\/\fR, \fB\-\-variable\fR \fI\,VARIABLE\/\fR +if set, show only patches for specified PKGBUILD variables + .SH COMMAND \fI\,'ahriman patch-remove'\/\fR -usage: ahriman patch-remove [-h] package +usage: ahriman patch-remove [-h] [-v VARIABLE] package remove patches for the package @@ -320,6 +331,26 @@ remove patches for the package \fBpackage\fR package base +.SH OPTIONS \fI\,'ahriman patch-remove'\/\fR +.TP +\fB\-v\fR \fI\,VARIABLE\/\fR, \fB\-\-variable\fR \fI\,VARIABLE\/\fR +should be used for single\-function patches in case if you wold like to remove only specified PKGBUILD variables. In case +if not set, it will remove all patches related to the package + +.SH COMMAND \fI\,'ahriman patch-set-add'\/\fR +usage: ahriman patch-set-add [-h] [-t TRACK] package + +create or update source patches + +.TP +\fBpackage\fR +path to directory with changed files for patch addition/update + +.SH OPTIONS \fI\,'ahriman patch-set-add'\/\fR +.TP +\fB\-t\fR \fI\,TRACK\/\fR, \fB\-\-track\fR \fI\,TRACK\/\fR +files which has to be tracked + .SH COMMAND \fI\,'ahriman repo-backup'\/\fR usage: ahriman repo-backup [-h] path diff --git a/docs/ahriman.core.database.migrations.rst b/docs/ahriman.core.database.migrations.rst index 08dc66fb..9fd425af 100644 --- a/docs/ahriman.core.database.migrations.rst +++ b/docs/ahriman.core.database.migrations.rst @@ -28,6 +28,14 @@ ahriman.core.database.migrations.m002\_user\_access module :no-undoc-members: :show-inheritance: +ahriman.core.database.migrations.m003\_patch\_variables module +-------------------------------------------------------------- + +.. automodule:: ahriman.core.database.migrations.m003_patch_variables + :members: + :no-undoc-members: + :show-inheritance: + Module contents --------------- diff --git a/docs/ahriman.core.formatters.rst b/docs/ahriman.core.formatters.rst index c3ecda6d..d3b2a849 100644 --- a/docs/ahriman.core.formatters.rst +++ b/docs/ahriman.core.formatters.rst @@ -36,6 +36,14 @@ ahriman.core.formatters.package\_printer module :no-undoc-members: :show-inheritance: +ahriman.core.formatters.patch\_printer module +--------------------------------------------- + +.. automodule:: ahriman.core.formatters.patch_printer + :members: + :no-undoc-members: + :show-inheritance: + ahriman.core.formatters.printer module -------------------------------------- diff --git a/docs/faq.rst b/docs/faq.rst index 49503eff..99af90d8 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -162,11 +162,13 @@ Unlike ``RemotePullTrigger`` trigger, the ``RemotePushTrigger`` more likely will But I just wanted to change PKGBUILD from AUR a bit! ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Well it is supported also. +Well it is supported also. The recommended way is to patch specific function, e.g. by running ``sudo -u ahriman ahriman patch-add ahriman version``. This command will prompt for new value of the PKGBUILD variable ``version``. You can also write it to file and read from it ``sudo -u ahriman ahriman patch-add ahriman version version.patch``. + +Alternatively you can create full-diff patches, which are calculated by using ``git diff`` from current PKGBUILD master branch: #. Clone sources from AUR. #. Make changes you would like to (e.g. edit ``PKGBUILD``, add external patches). -#. Run ``sudo -u ahriman ahriman patch-add /path/to/local/directory/with/PKGBUILD``. +#. Run ``sudo -u ahriman ahriman patch-set-add /path/to/local/directory/with/PKGBUILD``. The last command will calculate diff from current tree to the ``HEAD`` and will store it locally. Patches will be applied on any package actions (e.g. it can be used for dependency management). diff --git a/src/ahriman/application/ahriman.py b/src/ahriman/application/ahriman.py index 03d79372..7861c971 100644 --- a/src/ahriman/application/ahriman.py +++ b/src/ahriman/application/ahriman.py @@ -93,6 +93,7 @@ def _parser() -> argparse.ArgumentParser: _set_patch_add_parser(subparsers) _set_patch_list_parser(subparsers) _set_patch_remove_parser(subparsers) + _set_patch_set_add_parser(subparsers) _set_repo_backup_parser(subparsers) _set_repo_check_parser(subparsers) _set_repo_clean_parser(subparsers) @@ -320,7 +321,7 @@ def _set_package_status_update_parser(root: SubParserAction) -> argparse.Argumen def _set_patch_add_parser(root: SubParserAction) -> argparse.ArgumentParser: """ - add parser for new patch subcommand + add parser for new single-function patch subcommand Args: root(SubParserAction): subparsers for the commands @@ -328,16 +329,18 @@ def _set_patch_add_parser(root: SubParserAction) -> argparse.ArgumentParser: Returns: argparse.ArgumentParser: created argument parser """ - parser = root.add_parser("patch-add", help="add patch set", description="create or update source patches", - epilog="In order to add a patch set for the package you will need to clone " - "the AUR package manually, add required changes (e.g. external patches, " - "edit PKGBUILD) and run command, e.g. ``ahriman patch path/to/directory``. " - "By default it tracks *.patch and *.diff files, but this behavior can be changed " - "by using --track option", + parser = root.add_parser("patch-add", help="add patch for PKGBUILD function", + description="create or update patched PKGBUILD function or variable", + epilog="Unlike ``patch-set-add``, this function allows to patch only one PKGBUILD f" + "unction, e.g. typing ``ahriman patch-add ahriman version`` it will change the " + "``version`` inside PKGBUILD, typing ``ahriman patch-add ahriman build()`` " + "it will change ``build()`` function inside PKGBUILD", formatter_class=_formatter) - parser.add_argument("package", help="path to directory with changed files for patch addition/update") - parser.add_argument("-t", "--track", help="files which has to be tracked", action="append", - default=["*.diff", "*.patch"]) + parser.add_argument("package", help="package base") + parser.add_argument("variable", help="PKGBUILD variable or function name. If variable is a function, " + "it must end with ()") + parser.add_argument("patch", help="path to file which contains function or variable value. If not set, " + "the value will be read from stdin", type=Path, nargs="?") parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture=[""], lock=None, no_report=True) return parser @@ -356,6 +359,8 @@ def _set_patch_list_parser(root: SubParserAction) -> argparse.ArgumentParser: description="list available patches for the package", formatter_class=_formatter) parser.add_argument("package", help="package base", nargs="?") parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true") + parser.add_argument("-v", "--variable", help="if set, show only patches for specified PKGBUILD variables", + action="append") parser.set_defaults(handler=handlers.Patch, action=Action.List, architecture=[""], lock=None, no_report=True) return parser @@ -373,10 +378,39 @@ def _set_patch_remove_parser(root: SubParserAction) -> argparse.ArgumentParser: parser = root.add_parser("patch-remove", help="remove patch set", description="remove patches for the package", formatter_class=_formatter) parser.add_argument("package", help="package base") + parser.add_argument("-v", "--variable", help="should be used for single-function patches in case if you wold like " + "to remove only specified PKGBUILD variables. In case if not set, " + "it will remove all patches related to the package", + action="append") parser.set_defaults(handler=handlers.Patch, action=Action.Remove, architecture=[""], lock=None, no_report=True) return parser +def _set_patch_set_add_parser(root: SubParserAction) -> argparse.ArgumentParser: + """ + add parser for new full-diff patch subcommand + + Args: + root(SubParserAction): subparsers for the commands + + Returns: + argparse.ArgumentParser: created argument parser + """ + parser = root.add_parser("patch-set-add", help="add patch set", description="create or update source patches", + epilog="In order to add a patch set for the package you will need to clone " + "the AUR package manually, add required changes (e.g. external patches, " + "edit PKGBUILD) and run command, e.g. ``ahriman patch-set-add path/to/directory``. " + "By default it tracks *.patch and *.diff files, but this behavior can be changed " + "by using --track option", + formatter_class=_formatter) + parser.add_argument("package", help="path to directory with changed files for patch addition/update", type=Path) + parser.add_argument("-t", "--track", help="files which has to be tracked", action="append", + default=["*.diff", "*.patch"]) + parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture=[""], lock=None, no_report=True, + variable=None) + return parser + + def _set_repo_backup_parser(root: SubParserAction) -> argparse.ArgumentParser: """ add parser for repository backup subcommand diff --git a/src/ahriman/application/handlers/patch.py b/src/ahriman/application/handlers/patch.py index cd9cc432..8d1f34d1 100644 --- a/src/ahriman/application/handlers/patch.py +++ b/src/ahriman/application/handlers/patch.py @@ -18,17 +18,19 @@ # along with this program. If not, see . # import argparse +import sys from pathlib import Path -from typing import List, Optional, Type +from typing import List, Optional, Tuple, Type from ahriman.application.application import Application from ahriman.application.handlers import Handler from ahriman.core.build_tools.sources import Sources from ahriman.core.configuration import Configuration -from ahriman.core.formatters import StringPrinter +from ahriman.core.formatters import PatchPrinter from ahriman.models.action import Action from ahriman.models.package import Package +from ahriman.models.pkgbuild_patch import PkgbuildPatch class Patch(Handler): @@ -52,51 +54,93 @@ class Patch(Handler): application = Application(architecture, configuration, no_report, unsafe) application.on_start() - if args.action == Action.List: - Patch.patch_set_list(application, args.package, args.exit_code) + if args.action == Action.Update and args.variable is not None: + patch = Patch.patch_create_from_function(args.variable, args.patch) + Patch.patch_set_create(application, args.package, patch) + elif args.action == Action.Update and args.variable is None: + package_base, patch = Patch.patch_create_from_diff(args.package, args.track) + Patch.patch_set_create(application, package_base, patch) + elif args.action == Action.List: + Patch.patch_set_list(application, args.package, args.variable, args.exit_code) elif args.action == Action.Remove: - Patch.patch_set_remove(application, args.package) - elif args.action == Action.Update: - Patch.patch_set_create(application, Path(args.package), args.track) + Patch.patch_set_remove(application, args.package, args.variable) @staticmethod - def patch_set_create(application: Application, sources_dir: Path, track: List[str]) -> None: + def patch_create_from_diff(sources_dir: Path, track: List[str]) -> Tuple[str, PkgbuildPatch]: + """ + create PKGBUILD plain diff patches from sources directory + + Args: + sources_dir(Path): path to directory with the package sources + track(List[str]): track files which match the glob before creating the patch + + Returns: + Tuple[str, PkgbuildPatch]: package base and created PKGBUILD patch based on the diff from master HEAD + to current files + """ + package = Package.from_build(sources_dir) + patch = Sources.patch_create(sources_dir, *track) + return package.base, PkgbuildPatch(None, patch) + + @staticmethod + def patch_create_from_function(variable: str, patch_path: Optional[Path]) -> PkgbuildPatch: + """ + create single-function patch set for the package base + + Args: + variable(str): function or variable name inside PKGBUILD + patch_path(Path): optional path to patch content. If not set, it will be read from stdin + + Returns: + PkgbuildPatch: created patch for the PKGBUILD function + """ + if patch_path is None: + print("Post new function or variable value below. Press Ctrl-D to finish:", file=sys.stderr) + patch = "".join(list(sys.stdin)) + else: + patch = patch_path.read_text(encoding="utf8") + patch = patch.strip() # remove spaces around the patch + return PkgbuildPatch(variable, patch) + + @staticmethod + def patch_set_create(application: Application, package_base: str, patch: PkgbuildPatch) -> None: """ create patch set for the package base Args: application(Application): application instance - sources_dir(Path): path to directory with the package sources - track(List[str]): track files which match the glob before creating the patch + package_base(str): package base + patch(PkgbuildPatch): patch descriptor """ - package = Package.from_build(sources_dir) - patch = Sources.patch_create(sources_dir, *track) - application.database.patches_insert(package.base, patch) + application.database.patches_insert(package_base, patch) @staticmethod - def patch_set_list(application: Application, package_base: Optional[str], exit_code: bool) -> None: + def patch_set_list(application: Application, package_base: Optional[str], variables: List[str], + exit_code: bool) -> None: """ list patches available for the package base Args: application(Application): application instance package_base(Optional[str]): package base + variables(List[str]): extract patches only for specified PKGBUILD variables exit_code(bool): exit with error on empty search result + : """ - patches = application.database.patches_list(package_base) + patches = application.database.patches_list(package_base, variables) Patch.check_if_empty(exit_code, not patches) for base, patch in patches.items(): - content = base if package_base is None else patch - StringPrinter(content).print(verbose=True) + PatchPrinter(base, patch).print(verbose=True, separator=" = ") @staticmethod - def patch_set_remove(application: Application, package_base: str) -> None: + def patch_set_remove(application: Application, package_base: str, variables: List[str]) -> None: """ remove patch set for the package base Args: application(Application): application instance package_base(str): package base + variables(List[str]): remove patches only for specified PKGBUILD variables """ - application.database.patches_remove(package_base) + application.database.patches_remove(package_base, variables) diff --git a/src/ahriman/core/build_tools/sources.py b/src/ahriman/core/build_tools/sources.py index 77df3b71..8c597d64 100644 --- a/src/ahriman/core/build_tools/sources.py +++ b/src/ahriman/core/build_tools/sources.py @@ -45,24 +45,22 @@ class Sources(LazyLogging): _check_output = check_output @staticmethod - def extend_architectures(sources_dir: Path, architecture: str) -> None: + def extend_architectures(sources_dir: Path, architecture: str) -> List[PkgbuildPatch]: """ extend existing PKGBUILD with repository architecture Args: sources_dir(Path): local path to directory with source files architecture(str): repository architecture - """ - pkgbuild_path = sources_dir / "PKGBUILD" - if not pkgbuild_path.is_file(): - return + Returns: + List[PkgbuildPatch]: generated patch for PKGBUILD architectures if required + """ architectures = Package.supported_architectures(sources_dir) if "any" in architectures: # makepkg does not like when there is any other arch except for any - return + return [] architectures.add(architecture) - patch = PkgbuildPatch("arch", list(architectures)) - patch.write(pkgbuild_path) + return [PkgbuildPatch("arch", list(architectures))] @staticmethod def fetch(sources_dir: Path, remote: Optional[RemoteSource]) -> None: @@ -134,14 +132,14 @@ class Sources(LazyLogging): exception=None, cwd=sources_dir, logger=instance.logger) @staticmethod - def load(sources_dir: Path, package: Package, patch: Optional[str], paths: RepositoryPaths) -> None: + def load(sources_dir: Path, package: Package, patches: List[PkgbuildPatch], paths: RepositoryPaths) -> None: """ fetch sources from remote and apply patches Args: sources_dir(Path): local path to fetch package(Package): package definitions - patch(Optional[str]): optional patch to be applied + patches(List[PkgbuildPatch]): optional patch to be applied paths(RepositoryPaths): repository paths instance """ instance = Sources() @@ -150,9 +148,9 @@ class Sources(LazyLogging): shutil.copytree(cache_dir, sources_dir, dirs_exist_ok=True) instance.fetch(sources_dir, package.remote) - if patch is not None: + patches.extend(instance.extend_architectures(sources_dir, paths.architecture)) + for patch in patches: instance.patch_apply(sources_dir, patch) - instance.extend_architectures(sources_dir, paths.architecture) @staticmethod def patch_create(sources_dir: Path, *pattern: str) -> str: @@ -248,15 +246,18 @@ class Sources(LazyLogging): dst = sources_dir / src.relative_to(pkgbuild_dir) shutil.move(src, dst) - def patch_apply(self, sources_dir: Path, patch: str) -> None: + def patch_apply(self, sources_dir: Path, patch: PkgbuildPatch) -> None: """ apply patches if any Args: sources_dir(Path): local path to directory with git sources - patch(str): patch to be applied + patch(PkgbuildPatch): patch to be applied """ # create patch - self.logger.info("apply patch from database") - Sources._check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace", - exception=None, cwd=sources_dir, input_data=patch, logger=self.logger) + self.logger.info("apply patch %s from database at %s", patch.key, sources_dir) + if patch.is_plain_diff: + Sources._check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace", + exception=None, cwd=sources_dir, input_data=patch.serialize(), logger=self.logger) + else: + patch.write(sources_dir / "PKGBUILD") diff --git a/src/ahriman/core/database/migrations/m003_patch_variables.py b/src/ahriman/core/database/migrations/m003_patch_variables.py new file mode 100644 index 00000000..b454bd85 --- /dev/null +++ b/src/ahriman/core/database/migrations/m003_patch_variables.py @@ -0,0 +1,43 @@ +# +# Copyright (c) 2021-2022 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 . +# +__all__ = ["steps"] + + +steps = [ + """ + alter table patches rename to patches_ + """, + """ + create table patches ( + package_base text not null, + variable text, + patch blob not null + ) + """, + """ + create unique index patches_package_base_variable on patches (package_base, coalesce(variable, '')) + """, + """ + insert into patches (package_base, patch) select package_base, patch from patches_ + """, + """ + drop table patches_ + """, +] diff --git a/src/ahriman/core/database/operations/patch_operations.py b/src/ahriman/core/database/operations/patch_operations.py index 9dcebfe4..97b483d5 100644 --- a/src/ahriman/core/database/operations/patch_operations.py +++ b/src/ahriman/core/database/operations/patch_operations.py @@ -17,10 +17,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +from collections import defaultdict + from sqlite3 import Connection -from typing import Dict, Optional +from typing import Dict, List, Optional, Tuple from ahriman.core.database.operations import Operations +from ahriman.models.pkgbuild_patch import PkgbuildPatch class PatchOperations(Operations): @@ -28,7 +31,7 @@ class PatchOperations(Operations): operations for patches """ - def patches_get(self, package_base: str) -> Optional[str]: + def patches_get(self, package_base: str) -> List[PkgbuildPatch]: """ retrieve patches for the package @@ -36,62 +39,77 @@ class PatchOperations(Operations): package_base(str): package base to search for patches Returns: - Optional[str]: plain text patch for the package + List[PkgbuildPatch]: plain text patch for the package """ - return self.patches_list(package_base).get(package_base) + return self.patches_list(package_base, []).get(package_base, []) - def patches_insert(self, package_base: str, patch: str) -> None: + def patches_insert(self, package_base: str, patch: PkgbuildPatch) -> None: """ insert or update patch in database Args: package_base(str): package base to insert - patch(str): patch content + patch(PkgbuildPatch): patch content """ def run(connection: Connection) -> None: connection.execute( """ insert into patches - (package_base, patch) + (package_base, variable, patch) values - (:package_base, :patch) - on conflict (package_base) do update set + (:package_base, :variable, :patch) + on conflict (package_base, coalesce(variable, '')) do update set patch = :patch """, - {"package_base": package_base, "patch": patch}) + {"package_base": package_base, "variable": patch.key, "patch": patch.value}) return self.with_connection(run, commit=True) - def patches_list(self, package_base: Optional[str]) -> Dict[str, str]: + def patches_list(self, package_base: Optional[str], variables: List[str]) -> Dict[str, List[PkgbuildPatch]]: """ extract all patches Args: package_base(Optional[str]): optional filter by package base + variables(List[str]): extract patches only for specified PKGBUILD variables Returns: - Dict[str, str]: map of package base to patch content + Dict[str, List[PkgbuildPatch]]: map of package base to patch content """ - def run(connection: Connection) -> Dict[str, str]: - return { - cursor["package_base"]: cursor["patch"] + def run(connection: Connection) -> List[Tuple[str, PkgbuildPatch]]: + return [ + (cursor["package_base"], PkgbuildPatch(cursor["variable"], cursor["patch"])) for cursor in connection.execute( """select * from patches where :package_base is null or package_base = :package_base""", {"package_base": package_base}) - } + ] - return self.with_connection(run) + # we could use itertools & operator but why? + patches: Dict[str, List[PkgbuildPatch]] = defaultdict(list) + for package, patch in self.with_connection(run): + if variables and patch.key not in variables: + continue + patches[package].append(patch) + return dict(patches) - def patches_remove(self, package_base: str) -> None: + def patches_remove(self, package_base: str, variables: List[str]) -> None: """ remove patch set Args: package_base(str): package base to clear patches + variables(List[str]): remove patches only for specified PKGBUILD variables """ + def run_many(connection: Connection) -> None: + connection.executemany( + """delete from patches where package_base = :package_base and variable = :variable""", + [{"package_base": package_base, "variable": variable} for variable in variables]) + def run(connection: Connection) -> None: connection.execute( """delete from patches where package_base = :package_base""", {"package_base": package_base}) + if variables: + return self.with_connection(run_many, commit=True) return self.with_connection(run, commit=True) diff --git a/src/ahriman/core/formatters/__init__.py b/src/ahriman/core/formatters/__init__.py index 931e99d5..c6cffb1c 100644 --- a/src/ahriman/core/formatters/__init__.py +++ b/src/ahriman/core/formatters/__init__.py @@ -24,6 +24,7 @@ from ahriman.core.formatters.aur_printer import AurPrinter from ahriman.core.formatters.build_printer import BuildPrinter from ahriman.core.formatters.configuration_printer import ConfigurationPrinter from ahriman.core.formatters.package_printer import PackagePrinter +from ahriman.core.formatters.patch_printer import PatchPrinter from ahriman.core.formatters.status_printer import StatusPrinter from ahriman.core.formatters.update_printer import UpdatePrinter from ahriman.core.formatters.user_printer import UserPrinter diff --git a/src/ahriman/core/formatters/patch_printer.py b/src/ahriman/core/formatters/patch_printer.py new file mode 100644 index 00000000..ab9568d6 --- /dev/null +++ b/src/ahriman/core/formatters/patch_printer.py @@ -0,0 +1,56 @@ +# +# Copyright (c) 2021-2022 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 . +# +from typing import List + +from ahriman.core.formatters import StringPrinter +from ahriman.models.pkgbuild_patch import PkgbuildPatch +from ahriman.models.property import Property + + +class PatchPrinter(StringPrinter): + """ + print content of the PKGBUILD patch + + Attributes: + patches(List[PkgbuildPatch]): PKGBUILD patch object + """ + + def __init__(self, package_base: str, patches: List[PkgbuildPatch]) -> None: + """ + default constructor + + Args: + package_base(str): package base + patches(List[PkgbuildPatch]): PKGBUILD patch object + """ + StringPrinter.__init__(self, package_base) + self.patches = patches + + def properties(self) -> List[Property]: + """ + convert content into printable data + + Returns: + List[Property]: list of content properties + """ + return [ + Property(patch.key or "Full source diff", patch.value, is_required=True) + for patch in self.patches + ] diff --git a/src/ahriman/core/repository/executor.py b/src/ahriman/core/repository/executor.py index 46f847ea..48ed817f 100644 --- a/src/ahriman/core/repository/executor.py +++ b/src/ahriman/core/repository/executor.py @@ -109,7 +109,7 @@ class Executor(Cleaner): try: self.paths.tree_clear(package_base) # remove all internal files self.database.build_queue_clear(package_base) - self.database.patches_remove(package_base) + self.database.patches_remove(package_base, []) self.reporter.remove(package_base) # we only update status page in case of base removal except Exception: self.logger.exception("could not remove base %s", package_base) diff --git a/src/ahriman/models/package.py b/src/ahriman/models/package.py index 96183717..447a8e5c 100644 --- a/src/ahriman/models/package.py +++ b/src/ahriman/models/package.py @@ -305,7 +305,7 @@ class Package(LazyLogging): from ahriman.core.build_tools.sources import Sources - Sources.load(paths.cache_for(self.base), self, None, paths) + Sources.load(paths.cache_for(self.base), self, [], paths) try: # update pkgver first diff --git a/src/ahriman/models/pkgbuild_patch.py b/src/ahriman/models/pkgbuild_patch.py index 64c7a5dc..ca6a4b7e 100644 --- a/src/ahriman/models/pkgbuild_patch.py +++ b/src/ahriman/models/pkgbuild_patch.py @@ -21,7 +21,7 @@ import shlex from dataclasses import dataclass, field from pathlib import Path -from typing import List, Union +from typing import List, Optional, Union @dataclass(frozen=True) @@ -30,16 +30,23 @@ class PkgbuildPatch: wrapper for patching PKBGUILDs Attributes: - key(str): name of the property in PKGBUILD, e.g. version, url etc + key(Optional[str]): name of the property in PKGBUILD, e.g. version, url etc. If not set, patch will be + considered as full PKGBUILD diffs value(Union[str, List[str]]): value of the stored PKGBUILD property. It must be either string or list of string values unsafe(bool): if set, value will be not quoted, might break PKGBUILD """ - key: str + key: Optional[str] value: Union[str, List[str]] unsafe: bool = field(default=False, kw_only=True) + def __post_init__(self) -> None: + """ + remove empty key + """ + object.__setattr__(self, "key", self.key or None) + @property def is_function(self) -> bool: """ @@ -48,7 +55,17 @@ class PkgbuildPatch: Returns: bool: True in case if key ends with parentheses and False otherwise """ - return self.key.endswith("()") + return self.key is not None and self.key.endswith("()") + + @property + def is_plain_diff(self) -> bool: + """ + check if patch is full diff one or just single-variable patch + + Returns: + bool: True in case key set and False otherwise + """ + return self.key is None def quote(self, value: str) -> str: """ @@ -74,6 +91,8 @@ class PkgbuildPatch: if isinstance(self.value, list): # list like value = " ".join(map(self.quote, self.value)) return f"""{self.key}=({value})""" + if self.is_plain_diff: # no additional logic for plain diffs + return self.value # we suppose that function values are only supported in string-like values if self.is_function: return f"{self.key} {self.value}" # no quoting enabled here diff --git a/tests/ahriman/application/handlers/test_handler_patch.py b/tests/ahriman/application/handlers/test_handler_patch.py index 3960aeed..71d8aae3 100644 --- a/tests/ahriman/application/handlers/test_handler_patch.py +++ b/tests/ahriman/application/handlers/test_handler_patch.py @@ -1,5 +1,6 @@ import argparse import pytest +import sys from pathlib import Path from pytest_mock import MockerFixture @@ -9,6 +10,7 @@ from ahriman.application.handlers import Patch from ahriman.core.configuration import Configuration from ahriman.models.action import Action from ahriman.models.package import Package +from ahriman.models.pkgbuild_patch import PkgbuildPatch def _default_args(args: argparse.Namespace) -> argparse.Namespace: @@ -25,6 +27,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace: args.exit_code = False args.remove = False args.track = ["*.diff", "*.patch"] + args.variable = None return args @@ -35,12 +38,31 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc args = _default_args(args) args.action = Action.Update mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") + patch_mock = mocker.patch("ahriman.application.handlers.Patch.patch_create_from_diff", + return_value=(args.package, PkgbuildPatch(None, "patch"))) application_mock = mocker.patch("ahriman.application.handlers.Patch.patch_set_create") - on_start_mock = mocker.patch("ahriman.application.application.Application.on_start") Patch.run(args, "x86_64", configuration, True, False) - application_mock.assert_called_once_with(pytest.helpers.anyvar(int), Path(args.package), args.track) - on_start_mock.assert_called_once_with() + patch_mock.assert_called_once_with(args.package, args.track) + application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package, PkgbuildPatch(None, "patch")) + + +def test_run_function(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: + """ + must run command with patch function flag + """ + args = _default_args(args) + args.action = Action.Update + args.patch = "patch" + args.variable = "version" + patch = PkgbuildPatch(args.variable, args.patch) + mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") + patch_mock = mocker.patch("ahriman.application.handlers.Patch.patch_create_from_function", return_value=patch) + application_mock = mocker.patch("ahriman.application.handlers.Patch.patch_set_create") + + Patch.run(args, "x86_64", configuration, True, False) + patch_mock.assert_called_once_with(args.variable, args.patch) + application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package, patch) def test_run_list(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: @@ -49,11 +71,12 @@ def test_run_list(args: argparse.Namespace, configuration: Configuration, mocker """ args = _default_args(args) args.action = Action.List + args.variable = ["version"] mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") application_mock = mocker.patch("ahriman.application.handlers.Patch.patch_set_list") Patch.run(args, "x86_64", configuration, True, False) - application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package, False) + application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package, ["version"], False) def test_run_remove(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: @@ -62,24 +85,71 @@ def test_run_remove(args: argparse.Namespace, configuration: Configuration, mock """ args = _default_args(args) args.action = Action.Remove + args.variable = ["version"] mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") application_mock = mocker.patch("ahriman.application.handlers.Patch.patch_set_remove") Patch.run(args, "x86_64", configuration, True, False) - application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package) + application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package, ["version"]) + + +def test_patch_create_from_diff(package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must create patch from directory tree diff + """ + patch = PkgbuildPatch(None, "patch") + path = Path("local") + mocker.patch("pathlib.Path.mkdir") + package_mock = mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman) + sources_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_create", return_value=patch.value) + + assert Patch.patch_create_from_diff(path, ["*.diff"]) == (package_ahriman.base, patch) + package_mock.assert_called_once_with(path) + sources_mock.assert_called_once_with(path, "*.diff") + + +def test_patch_create_from_function(mocker: MockerFixture) -> None: + """ + must create function patch from file + """ + path = Path("local") + patch = PkgbuildPatch("version", "patch") + read_mock = mocker.patch("pathlib.Path.read_text", return_value=patch.value) + + assert Patch.patch_create_from_function(patch.key, path) == patch + read_mock.assert_called_once_with(encoding="utf8") + + +def test_patch_create_from_function_stdin(mocker: MockerFixture) -> None: + """ + must create function patch from stdin + """ + patch = PkgbuildPatch("version", "This is a patch") + mocker.patch.object(sys, "stdin", patch.value.splitlines()) + assert Patch.patch_create_from_function(patch.key, None) == patch + + +def test_patch_create_from_function_strip(mocker: MockerFixture) -> None: + """ + must remove spaces at the beginning and at the end of the line + """ + patch = PkgbuildPatch("version", "This is a patch") + mocker.patch.object(sys, "stdin", ["\n"] + patch.value.splitlines() + ["\n"]) + assert Patch.patch_create_from_function(patch.key, None) == patch def test_patch_set_list(application: Application, mocker: MockerFixture) -> None: """ must list available patches for the command """ - get_mock = mocker.patch("ahriman.core.database.SQLite.patches_list", return_value={"ahriman": "patch"}) + get_mock = mocker.patch("ahriman.core.database.SQLite.patches_list", + return_value={"ahriman": PkgbuildPatch(None, "patch")}) print_mock = mocker.patch("ahriman.core.formatters.Printer.print") check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") - Patch.patch_set_list(application, "ahriman", False) - get_mock.assert_called_once_with("ahriman") - print_mock.assert_called_once_with(verbose=True) + Patch.patch_set_list(application, "ahriman", ["version"], False) + get_mock.assert_called_once_with("ahriman", ["version"]) + print_mock.assert_called_once_with(verbose=True, separator=" = ") check_mock.assert_called_once_with(False, False) @@ -90,7 +160,7 @@ def test_patch_set_list_empty_exception(application: Application, mocker: Mocker mocker.patch("ahriman.core.database.SQLite.patches_list", return_value={}) check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") - Patch.patch_set_list(application, "ahriman", True) + Patch.patch_set_list(application, "ahriman", [], True) check_mock.assert_called_once_with(True, True) @@ -98,13 +168,9 @@ def test_patch_set_create(application: Application, package_ahriman: Package, mo """ must create patch set for the package """ - mocker.patch("pathlib.Path.mkdir") - mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman) - mocker.patch("ahriman.core.build_tools.sources.Sources.patch_create", return_value="patch") create_mock = mocker.patch("ahriman.core.database.SQLite.patches_insert") - - Patch.patch_set_create(application, Path("path"), ["*.patch"]) - create_mock.assert_called_once_with(package_ahriman.base, "patch") + Patch.patch_set_create(application, package_ahriman.base, PkgbuildPatch("version", package_ahriman.version)) + create_mock.assert_called_once_with(package_ahriman.base, PkgbuildPatch("version", package_ahriman.version)) def test_patch_set_remove(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None: @@ -112,5 +178,5 @@ def test_patch_set_remove(application: Application, package_ahriman: Package, mo must remove patch set for the package """ remove_mock = mocker.patch("ahriman.core.database.SQLite.patches_remove") - Patch.patch_set_remove(application, package_ahriman.base) - remove_mock.assert_called_once_with(package_ahriman.base) + Patch.patch_set_remove(application, package_ahriman.base, ["version"]) + remove_mock.assert_called_once_with(package_ahriman.base, ["version"]) diff --git a/tests/ahriman/application/test_ahriman.py b/tests/ahriman/application/test_ahriman.py index 8d04c14e..e2d607bd 100644 --- a/tests/ahriman/application/test_ahriman.py +++ b/tests/ahriman/application/test_ahriman.py @@ -197,7 +197,7 @@ def test_subparsers_patch_add(parser: argparse.ArgumentParser) -> None: """ patch-add command must imply action, architecture list, lock and no-report """ - args = parser.parse_args(["patch-add", "ahriman"]) + args = parser.parse_args(["patch-add", "ahriman", "version"]) assert args.action == Action.Update assert args.architecture == [""] assert args.lock is None @@ -208,18 +208,10 @@ def test_subparsers_patch_add_architecture(parser: argparse.ArgumentParser) -> N """ patch-add command must correctly parse architecture list """ - args = parser.parse_args(["-a", "x86_64", "patch-add", "ahriman"]) + args = parser.parse_args(["-a", "x86_64", "patch-add", "ahriman", "version"]) assert args.architecture == [""] -def test_subparsers_patch_add_track(parser: argparse.ArgumentParser) -> None: - """ - patch-add command must correctly parse track files patterns - """ - args = parser.parse_args(["patch-add", "-t", "*.py", "ahriman"]) - assert args.track == ["*.diff", "*.patch", "*.py"] - - def test_subparsers_patch_list(parser: argparse.ArgumentParser) -> None: """ patch-list command must imply action, architecture list, lock and no-report @@ -258,6 +250,42 @@ def test_subparsers_patch_remove_architecture(parser: argparse.ArgumentParser) - assert args.architecture == [""] +def test_subparsers_patch_set_add(parser: argparse.ArgumentParser) -> None: + """ + patch-set-add command must imply action, architecture list, lock, no-report and variable + """ + args = parser.parse_args(["patch-set-add", "ahriman"]) + assert args.action == Action.Update + assert args.architecture == [""] + assert args.lock is None + assert args.no_report + assert args.variable is None + + +def test_subparsers_patch_set_add_architecture(parser: argparse.ArgumentParser) -> None: + """ + patch-set-add command must correctly parse architecture list + """ + args = parser.parse_args(["-a", "x86_64", "patch-set-add", "ahriman"]) + assert args.architecture == [""] + + +def test_subparsers_patch_set_add_option_package(parser: argparse.ArgumentParser) -> None: + """ + patch-set-add command must convert package option to path instance + """ + args = parser.parse_args(["patch-set-add", "ahriman"]) + assert isinstance(args.package, Path) + + +def test_subparsers_patch_set_add_option_track(parser: argparse.ArgumentParser) -> None: + """ + patch-set-add command must correctly parse track files patterns + """ + args = parser.parse_args(["patch-set-add", "-t", "*.py", "ahriman"]) + assert args.track == ["*.diff", "*.patch", "*.py"] + + def test_subparsers_repo_backup(parser: argparse.ArgumentParser) -> None: """ repo-backup command must imply architecture list, lock, no-report and unsafe diff --git a/tests/ahriman/core/build_tools/test_sources.py b/tests/ahriman/core/build_tools/test_sources.py index 8499405f..a78b82f6 100644 --- a/tests/ahriman/core/build_tools/test_sources.py +++ b/tests/ahriman/core/build_tools/test_sources.py @@ -6,6 +6,7 @@ from unittest import mock from ahriman.core.build_tools.sources import Sources from ahriman.models.package import Package +from ahriman.models.pkgbuild_patch import PkgbuildPatch from ahriman.models.remote_source import RemoteSource from ahriman.models.repository_paths import RepositoryPaths @@ -16,11 +17,9 @@ def test_extend_architectures(mocker: MockerFixture) -> None: """ mocker.patch("pathlib.Path.is_file", return_value=True) archs_mock = mocker.patch("ahriman.models.package.Package.supported_architectures", return_value={"x86_64"}) - write_mock = mocker.patch("ahriman.models.pkgbuild_patch.PkgbuildPatch.write") - Sources.extend_architectures(Path("local"), "i686") + assert Sources.extend_architectures(Path("local"), "i686") == [PkgbuildPatch("arch", list({"x86_64", "i686"}))] archs_mock.assert_called_once_with(Path("local")) - write_mock.assert_called_once_with(Path("local") / "PKGBUILD") def test_extend_architectures_any(mocker: MockerFixture) -> None: @@ -29,21 +28,7 @@ def test_extend_architectures_any(mocker: MockerFixture) -> None: """ mocker.patch("pathlib.Path.is_file", return_value=True) mocker.patch("ahriman.models.package.Package.supported_architectures", return_value={"any"}) - write_mock = mocker.patch("ahriman.models.pkgbuild_patch.PkgbuildPatch.write") - - Sources.extend_architectures(Path("local"), "i686") - write_mock.assert_not_called() - - -def test_extend_architectures_skip(mocker: MockerFixture) -> None: - """ - must skip extending list of the architectures in case if no PKGBUILD file found - """ - mocker.patch("pathlib.Path.is_file", return_value=False) - write_mock = mocker.patch("ahriman.models.pkgbuild_patch.PkgbuildPatch.write") - - Sources.extend_architectures(Path("local"), "i686") - write_mock.assert_not_called() + assert Sources.extend_architectures(Path("local"), "i686") == [] def test_fetch_empty(remote_source: RemoteSource, mocker: MockerFixture) -> None: @@ -167,15 +152,16 @@ def test_load(package_ahriman: Package, repository_paths: RepositoryPaths, mocke """ must load packages sources correctly """ - mocker.patch("pathlib.Path.is_dir", return_value=False) + patch = PkgbuildPatch(None, "patch") + path = Path("local") fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch") patch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_apply") - architectures_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.extend_architectures") + architectures_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.extend_architectures", return_value=[]) - Sources.load(Path("local"), package_ahriman, "patch", repository_paths) - fetch_mock.assert_called_once_with(Path("local"), package_ahriman.remote) - patch_mock.assert_called_once_with(Path("local"), "patch") - architectures_mock.assert_called_once_with(Path("local"), repository_paths.architecture) + Sources.load(path, package_ahriman, [patch], repository_paths) + fetch_mock.assert_called_once_with(path, package_ahriman.remote) + patch_mock.assert_called_once_with(path, patch) + architectures_mock.assert_called_once_with(path, repository_paths.architecture) def test_load_no_patch(package_ahriman: Package, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: @@ -184,9 +170,10 @@ def test_load_no_patch(package_ahriman: Package, repository_paths: RepositoryPat """ mocker.patch("pathlib.Path.is_dir", return_value=False) mocker.patch("ahriman.core.build_tools.sources.Sources.fetch") + mocker.patch("ahriman.core.build_tools.sources.Sources.extend_architectures", return_value=[]) patch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_apply") - Sources.load(Path("local"), package_ahriman, None, repository_paths) + Sources.load(Path("local"), package_ahriman, [], repository_paths) patch_mock.assert_not_called() @@ -197,8 +184,9 @@ def test_load_with_cache(package_ahriman: Package, repository_paths: RepositoryP mocker.patch("pathlib.Path.is_dir", return_value=True) copytree_mock = mocker.patch("shutil.copytree") mocker.patch("ahriman.core.build_tools.sources.Sources.fetch") + mocker.patch("ahriman.core.build_tools.sources.Sources.extend_architectures", return_value=[]) - Sources.load(Path("local"), package_ahriman, None, repository_paths) + Sources.load(Path("local"), package_ahriman, [], repository_paths) copytree_mock.assert_called_once() # we do not check full command here, sorry @@ -331,11 +319,24 @@ def test_patch_apply(sources: Sources, mocker: MockerFixture) -> None: """ must apply patches if any """ + patch = PkgbuildPatch(None, "patch") check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") local = Path("local") - sources.patch_apply(local, "patches") + sources.patch_apply(local, patch) check_output_mock.assert_called_once_with( "git", "apply", "--ignore-space-change", "--ignore-whitespace", - exception=None, cwd=local, input_data="patches", logger=pytest.helpers.anyvar(int) + exception=None, cwd=local, input_data=patch.value, logger=pytest.helpers.anyvar(int) ) + + +def test_patch_apply_function(sources: Sources, mocker: MockerFixture) -> None: + """ + must apply single-function patches + """ + patch = PkgbuildPatch("version", "42") + local = Path("local") + write_mock = mocker.patch("ahriman.models.pkgbuild_patch.PkgbuildPatch.write") + + sources.patch_apply(local, patch) + write_mock.assert_called_once_with(local / "PKGBUILD") diff --git a/tests/ahriman/core/build_tools/test_task.py b/tests/ahriman/core/build_tools/test_task.py index 11652bb7..44b5669b 100644 --- a/tests/ahriman/core/build_tools/test_task.py +++ b/tests/ahriman/core/build_tools/test_task.py @@ -20,4 +20,4 @@ def test_init(task_ahriman: Task, database: SQLite, mocker: MockerFixture) -> No """ load_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.load") task_ahriman.init(Path("ahriman"), database) - load_mock.assert_called_once_with(Path("ahriman"), task_ahriman.package, None, task_ahriman.paths) + load_mock.assert_called_once_with(Path("ahriman"), task_ahriman.package, [], task_ahriman.paths) diff --git a/tests/ahriman/core/database/migrations/test_m003_patch_variables.py b/tests/ahriman/core/database/migrations/test_m003_patch_variables.py new file mode 100644 index 00000000..f03b786c --- /dev/null +++ b/tests/ahriman/core/database/migrations/test_m003_patch_variables.py @@ -0,0 +1,8 @@ +from ahriman.core.database.migrations.m003_patch_variables import steps + + +def test_migration_package_source() -> None: + """ + migration must not be empty + """ + assert steps diff --git a/tests/ahriman/core/database/operations/test_patch_operations.py b/tests/ahriman/core/database/operations/test_patch_operations.py index adc14e3e..4c13e862 100644 --- a/tests/ahriman/core/database/operations/test_patch_operations.py +++ b/tests/ahriman/core/database/operations/test_patch_operations.py @@ -1,55 +1,96 @@ from ahriman.core.database import SQLite from ahriman.models.package import Package +from ahriman.models.pkgbuild_patch import PkgbuildPatch def test_patches_get_insert(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None: """ must insert patch to database """ - database.patches_insert(package_ahriman.base, "patch_1") - database.patches_insert(package_python_schedule.base, "patch_2") - assert database.patches_get(package_ahriman.base) == "patch_1" - assert not database.build_queue_get() + database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch_1")) + database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch_3")) + database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch_2")) + assert database.patches_get(package_ahriman.base) == [ + PkgbuildPatch(None, "patch_1"), PkgbuildPatch("key", "patch_3") + ] def test_patches_list(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None: """ must list all patches """ - database.patches_insert(package_ahriman.base, "patch1") - database.patches_insert(package_python_schedule.base, "patch2") - assert database.patches_list(None) == {package_ahriman.base: "patch1", package_python_schedule.base: "patch2"} + database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1")) + database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch3")) + database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2")) + assert database.patches_list(None, []) == { + package_ahriman.base: [PkgbuildPatch(None, "patch1"), PkgbuildPatch("key", "patch3")], + package_python_schedule.base: [PkgbuildPatch(None, "patch2")], + } def test_patches_list_filter(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None: """ must list all patches filtered by package name (same as get) """ - database.patches_insert(package_ahriman.base, "patch1") - database.patches_insert(package_python_schedule.base, "patch2") + database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1")) + database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2")) - assert database.patches_list(package_ahriman.base) == {package_ahriman.base: "patch1"} - assert database.patches_list(package_python_schedule.base) == {package_python_schedule.base: "patch2"} + assert database.patches_list(package_ahriman.base, []) == {package_ahriman.base: [PkgbuildPatch(None, "patch1")]} + assert database.patches_list(package_python_schedule.base, []) == { + package_python_schedule.base: [PkgbuildPatch(None, "patch2")], + } + + +def test_patches_list_filter_by_variable(database: SQLite, package_ahriman: Package, + package_python_schedule: Package) -> None: + """ + must list all patches filtered by package name (same as get) + """ + database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1")) + database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch2")) + database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch3")) + + assert database.patches_list(None, []) == { + package_ahriman.base: [PkgbuildPatch(None, "patch1"), PkgbuildPatch("key", "patch2")], + package_python_schedule.base: [PkgbuildPatch(None, "patch3")], + } + assert database.patches_list(None, ["key"]) == { + package_ahriman.base: [PkgbuildPatch("key", "patch2")], + } def test_patches_insert_remove(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None: """ must remove patch from database """ - database.patches_insert(package_ahriman.base, "patch_1") - database.patches_insert(package_python_schedule.base, "patch_2") - database.patches_remove(package_ahriman.base) + database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1")) + database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2")) + database.patches_remove(package_ahriman.base, []) - assert database.patches_get(package_ahriman.base) is None - database.patches_insert(package_python_schedule.base, "patch_2") + assert database.patches_get(package_ahriman.base) == [] + assert database.patches_get(package_python_schedule.base) == [PkgbuildPatch(None, "patch2")] + + +def test_patches_insert_remove_by_variable(database: SQLite, package_ahriman: Package, + package_python_schedule: Package) -> None: + """ + must remove patch from database by variable + """ + database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1")) + database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch3")) + database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2")) + database.patches_remove(package_ahriman.base, ["key"]) + + assert database.patches_get(package_ahriman.base) == [PkgbuildPatch(None, "patch1")] + assert database.patches_get(package_python_schedule.base) == [PkgbuildPatch(None, "patch2")] def test_patches_insert_insert(database: SQLite, package_ahriman: Package) -> None: """ must update patch in database """ - database.patches_insert(package_ahriman.base, "patch_1") - assert database.patches_get(package_ahriman.base) == "patch_1" + database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1")) + assert database.patches_get(package_ahriman.base) == [PkgbuildPatch(None, "patch1")] - database.patches_insert(package_ahriman.base, "patch_2") - assert database.patches_get(package_ahriman.base) == "patch_2" + database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch2")) + assert database.patches_get(package_ahriman.base) == [PkgbuildPatch(None, "patch2")] diff --git a/tests/ahriman/core/formatters/conftest.py b/tests/ahriman/core/formatters/conftest.py index 7246d195..a274db52 100644 --- a/tests/ahriman/core/formatters/conftest.py +++ b/tests/ahriman/core/formatters/conftest.py @@ -1,10 +1,11 @@ import pytest -from ahriman.core.formatters import AurPrinter, ConfigurationPrinter, PackagePrinter, StatusPrinter, StringPrinter, \ - UpdatePrinter, UserPrinter, VersionPrinter +from ahriman.core.formatters import AurPrinter, ConfigurationPrinter, PackagePrinter, PatchPrinter, StatusPrinter, \ + StringPrinter, UpdatePrinter, UserPrinter, VersionPrinter from ahriman.models.aur_package import AURPackage from ahriman.models.build_status import BuildStatus from ahriman.models.package import Package +from ahriman.models.pkgbuild_patch import PkgbuildPatch from ahriman.models.user import User @@ -47,6 +48,20 @@ def package_ahriman_printer(package_ahriman: Package) -> PackagePrinter: return PackagePrinter(package_ahriman, BuildStatus()) +@pytest.fixture +def patch_printer(package_ahriman: Package) -> PatchPrinter: + """ + fixture for patch printer + + Args: + package_ahriman(Package): package fixture + + Returns: + PatchPrinter: patch printer test instance + """ + return PatchPrinter(package_ahriman.base, [PkgbuildPatch("key", "value")]) + + @pytest.fixture def status_printer() -> StatusPrinter: """ diff --git a/tests/ahriman/core/formatters/test_patch_printer.py b/tests/ahriman/core/formatters/test_patch_printer.py new file mode 100644 index 00000000..059de51f --- /dev/null +++ b/tests/ahriman/core/formatters/test_patch_printer.py @@ -0,0 +1,22 @@ +from ahriman.core.formatters import PatchPrinter + + +def test_properties(patch_printer: PatchPrinter) -> None: + """ + must return non empty properties list + """ + assert patch_printer.properties() + + +def test_properties_required(patch_printer: PatchPrinter) -> None: + """ + must return all properties as required + """ + assert all(prop.is_required for prop in patch_printer.properties()) + + +def test_title(patch_printer: PatchPrinter) -> None: + """ + must return non empty title + """ + assert patch_printer.title() == "ahriman" diff --git a/tests/ahriman/core/repository/test_executor.py b/tests/ahriman/core/repository/test_executor.py index beb46415..9cd55ab4 100644 --- a/tests/ahriman/core/repository/test_executor.py +++ b/tests/ahriman/core/repository/test_executor.py @@ -72,7 +72,7 @@ def test_process_remove_base(executor: Executor, package_ahriman: Package, mocke # must update status and remove package files tree_clear_mock.assert_called_once_with(package_ahriman.base) build_queue_mock.assert_called_once_with(package_ahriman.base) - patches_mock.assert_called_once_with(package_ahriman.base) + patches_mock.assert_called_once_with(package_ahriman.base, []) status_client_mock.assert_called_once_with(package_ahriman.base) diff --git a/tests/ahriman/core/test_tree.py b/tests/ahriman/core/test_tree.py index 402cab1a..ef5a7e86 100644 --- a/tests/ahriman/core/test_tree.py +++ b/tests/ahriman/core/test_tree.py @@ -49,8 +49,7 @@ def test_leaf_load(package_ahriman: Package, repository_paths: RepositoryPaths, leaf = Leaf.load(package_ahriman, repository_paths, database) assert leaf.package == package_ahriman assert leaf.dependencies == {"ahriman-dependency"} - load_mock.assert_called_once_with( - pytest.helpers.anyvar(int), package_ahriman, None, repository_paths) + load_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman, [], repository_paths) dependencies_mock.assert_called_once_with(pytest.helpers.anyvar(int)) diff --git a/tests/ahriman/models/test_pkgbuild_patch.py b/tests/ahriman/models/test_pkgbuild_patch.py index 9063ee06..0d40134d 100644 --- a/tests/ahriman/models/test_pkgbuild_patch.py +++ b/tests/ahriman/models/test_pkgbuild_patch.py @@ -5,6 +5,15 @@ from unittest.mock import MagicMock, call from ahriman.models.pkgbuild_patch import PkgbuildPatch +def test_post_init() -> None: + """ + must remove empty keys + """ + assert PkgbuildPatch("", "value").key is None + assert PkgbuildPatch(None, "value").key is None + assert PkgbuildPatch("key", "value").key == "key" + + def test_is_function() -> None: """ must correctly define key as function @@ -13,6 +22,14 @@ def test_is_function() -> None: assert PkgbuildPatch("key()", "value").is_function +def test_is_plain_diff() -> None: + """ + must correctly define key as function + """ + assert not PkgbuildPatch("key", "value").is_plain_diff + assert PkgbuildPatch(None, "value").is_plain_diff + + def test_quote() -> None: """ must quote strings if unsafe flag is not set @@ -32,6 +49,13 @@ def test_serialize() -> None: assert PkgbuildPatch("key", "4'2", unsafe=True).serialize() == "key=4'2" +def test_serialize_plain_diff() -> None: + """ + must correctly serialize function values + """ + assert PkgbuildPatch(None, "{ value }").serialize() == "{ value }" + + def test_serialize_function() -> None: """ must correctly serialize function values