add ability to specify one-time patch on package addition

This commit is contained in:
Evgenii Alekseev 2023-10-24 04:43:53 +03:00
parent 8524f1eb20
commit 8bc185049c
28 changed files with 382 additions and 86 deletions

View File

@ -200,6 +200,14 @@ Alternatively you can create full-diff patches, which are calculated by using ``
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).
It is also possible to create simple patch during package addition, e.g.:
.. code-block:: shell
sudo -u ahriman ahriman package-add ahriman --variable PKGEXT=.pkg.tar.xz
The ``--variable`` argument accepts variables in shell like format: quotation and lists are supported as usual, but functions are not. This feature is useful in particular in order to override specific makepkg variables during build.
How to build package from official repository
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -276,6 +276,7 @@ def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto)
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
parser.add_argument("-v", "--variable", help="apply specified makepkg variables to the next build", action="append")
parser.set_defaults(handler=handlers.Add)
return parser

View File

@ -23,6 +23,7 @@ from ahriman.application.application import Application
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
from ahriman.models.packagers import Packagers
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_id import RepositoryId
@ -45,7 +46,12 @@ class Add(Handler):
"""
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
application.on_start()
application.add(args.package, args.source, args.username)
patches = [PkgbuildPatch.from_env(patch) for patch in args.variable] if args.variable is not None else []
for package in args.package: # for each requested package insert patch
application.database.patches_insert(package, patches)
if not args.now:
return

View File

@ -162,7 +162,7 @@ class Handler:
if args.repository_id is not None:
separator = "/" if "/" in args.repository_id else "-" # systemd and non-systemd identifiers
# repository parts is optional for backward compatibility
architecture, *repository_parts = args.repository_id.split(separator)
architecture, *repository_parts = args.repository_id.split(separator) # maxsplit isn't used intentionally
args.architecture = architecture
if repository_parts:
args.repository = "-".join(repository_parts) # replace slash with dash

View File

@ -115,7 +115,7 @@ class Patch(Handler):
package_base(str): package base
patch(PkgbuildPatch): patch descriptor
"""
application.database.patches_insert(package_base, patch)
application.database.patches_insert(package_base, [patch])
@staticmethod
def patch_set_list(application: Application, package_base: str | None, variables: list[str] | None,

View File

@ -33,8 +33,8 @@ class PackageOperations(Operations):
package operations
"""
def _package_remove_package_base(self, connection: Connection, package_base: str,
repository_id: RepositoryId) -> None:
@staticmethod
def _package_remove_package_base(connection: Connection, package_base: str, repository_id: RepositoryId) -> None:
"""
remove package base information
@ -50,8 +50,9 @@ class PackageOperations(Operations):
"""delete from package_bases where package_base = :package_base and repository = :repository""",
{"package_base": package_base, "repository": repository_id.id})
def _package_remove_packages(self, connection: Connection, package_base: str,
current_packages: Iterable[str], repository_id: RepositoryId) -> None:
@staticmethod
def _package_remove_packages(connection: Connection, package_base: str, current_packages: Iterable[str],
repository_id: RepositoryId) -> None:
"""
remove packages belong to the package base
@ -74,8 +75,8 @@ class PackageOperations(Operations):
"""delete from packages where package = :package and repository = :repository""",
packages)
def _package_update_insert_base(self, connection: Connection, package: Package,
repository_id: RepositoryId) -> None:
@staticmethod
def _package_update_insert_base(connection: Connection, package: Package, repository_id: RepositoryId) -> None:
"""
insert base package into table
@ -107,8 +108,8 @@ class PackageOperations(Operations):
}
)
def _package_update_insert_packages(self, connection: Connection, package: Package,
repository_id: RepositoryId) -> None:
@staticmethod
def _package_update_insert_packages(connection: Connection, package: Package, repository_id: RepositoryId) -> None:
"""
insert packages into table
@ -149,7 +150,8 @@ class PackageOperations(Operations):
""",
package_list)
def _package_update_insert_status(self, connection: Connection, package_base: str, status: BuildStatus,
@staticmethod
def _package_update_insert_status(connection: Connection, package_base: str, status: BuildStatus,
repository_id: RepositoryId) -> None:
"""
insert base package status into table
@ -176,8 +178,8 @@ class PackageOperations(Operations):
"repository": repository_id.id,
})
def _packages_get_select_package_bases(self, connection: Connection,
repository_id: RepositoryId) -> dict[str, Package]:
@staticmethod
def _packages_get_select_package_bases(connection: Connection, repository_id: RepositoryId) -> dict[str, Package]:
"""
select package bases from the table
@ -201,7 +203,8 @@ class PackageOperations(Operations):
)
}
def _packages_get_select_packages(self, connection: Connection, packages: dict[str, Package],
@staticmethod
def _packages_get_select_packages(connection: Connection, packages: dict[str, Package],
repository_id: RepositoryId) -> dict[str, Package]:
"""
select packages from the table
@ -223,8 +226,8 @@ class PackageOperations(Operations):
packages[row["package_base"]].packages[row["package"]] = PackageDescription.from_json(row)
return packages
def _packages_get_select_statuses(self, connection: Connection,
repository_id: RepositoryId) -> dict[str, BuildStatus]:
@staticmethod
def _packages_get_select_statuses(connection: Connection, repository_id: RepositoryId) -> dict[str, BuildStatus]:
"""
select package build statuses from the table

View File

@ -18,7 +18,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from collections import defaultdict
from sqlite3 import Connection
from ahriman.core.database.operations import Operations
@ -42,16 +41,16 @@ class PatchOperations(Operations):
"""
return self.patches_list(package_base, None).get(package_base, [])
def patches_insert(self, package_base: str, patch: PkgbuildPatch) -> None:
def patches_insert(self, package_base: str, patches: list[PkgbuildPatch]) -> None:
"""
insert or update patch in database
Args:
package_base(str): package base to insert
patch(PkgbuildPatch): patch content
patches(list[PkgbuildPatch]): patch content
"""
def run(connection: Connection) -> None:
connection.execute(
connection.executemany(
"""
insert into patches
(package_base, variable, patch)
@ -60,7 +59,14 @@ class PatchOperations(Operations):
on conflict (package_base, coalesce(variable, '')) do update set
patch = :patch
""",
{"package_base": package_base, "variable": patch.key, "patch": patch.value})
[
{
"package_base": package_base,
"variable": patch.key,
"patch": patch.value,
} for patch in patches
]
)
return self.with_connection(run, commit=True)
@ -89,7 +95,7 @@ class PatchOperations(Operations):
if variables is not None and patch.key not in variables:
continue
patches[package].append(patch)
return dict(patches)
return patches
def patches_remove(self, package_base: str, variables: list[str] | None) -> None:
"""
@ -102,12 +108,21 @@ class PatchOperations(Operations):
def run_many(connection: Connection) -> None:
patches = variables or [] # suppress mypy warning
connection.executemany(
"""delete from patches where package_base = :package_base and variable = :variable""",
[{"package_base": package_base, "variable": variable} for variable in patches])
"""
delete from patches where package_base = :package_base and variable = :variable
""",
[
{
"package_base": package_base,
"variable": variable,
} for variable in patches
])
def run(connection: Connection) -> None:
connection.execute(
"""delete from patches where package_base = :package_base""",
"""
delete from patches where package_base = :package_base
""",
{"package_base": package_base})
if variables is not None:

View File

@ -28,6 +28,7 @@ from multiprocessing import Process, Queue
from threading import Lock, Thread
from ahriman.core.log import LazyLogging
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.process_status import ProcessStatus
from ahriman.models.repository_id import RepositoryId
@ -96,7 +97,8 @@ class Spawn(Thread, LazyLogging):
queue.put(ProcessStatus(process_id, result, consumed_time))
def _spawn_process(self, repository_id: RepositoryId, command: str, *args: str, **kwargs: str | None) -> str:
def _spawn_process(self, repository_id: RepositoryId, command: str, *args: str,
**kwargs: str | list[str] | None) -> str:
"""
spawn external ahriman process with supplied arguments
@ -104,7 +106,7 @@ class Spawn(Thread, LazyLogging):
repository_id(RepositoryId): repository unique identifier
command(str): subcommand to run
*args(str): positional command arguments
**kwargs(str): named command arguments
**kwargs(str | list[str] | None): named command arguments
Returns:
str: spawned process identifier
@ -118,9 +120,13 @@ class Spawn(Thread, LazyLogging):
for argument, value in kwargs.items():
if value is None:
continue # skip null values
arguments.append(f"--{argument}")
if value:
arguments.append(value)
flag = f"--{argument}"
if isinstance(value, list):
arguments.extend(list(sum(((flag, v) for v in value), ())))
elif value:
arguments.extend([flag, value])
else:
arguments.append(flag) # boolean argument
process_id = str(uuid.uuid4())
self.logger.info("full command line arguments of %s are %s using repository %s",
@ -167,7 +173,7 @@ class Spawn(Thread, LazyLogging):
return self._spawn_process(repository_id, "service-key-import", key, **kwargs)
def packages_add(self, repository_id: RepositoryId, packages: Iterable[str], username: str | None, *,
now: bool) -> str:
patches: list[PkgbuildPatch], now: bool) -> str:
"""
add packages
@ -175,14 +181,18 @@ class Spawn(Thread, LazyLogging):
repository_id(RepositoryId): repository unique identifier
packages(Iterable[str]): packages list to add
username(str | None): optional override of username for build process
patches(list[PkgbuildPatch]): list of patches to be passed
now(bool): build packages now
Returns:
str: spawned process identifier
"""
kwargs = {"username": username}
kwargs: dict[str, str | list[str] | None] = {"username": username}
if now:
kwargs["now"] = ""
if patches:
kwargs["variable"] = [patch.serialize() for patch in patches]
return self._spawn_process(repository_id, "package-add", *packages, **kwargs)
def packages_rebuild(self, repository_id: RepositoryId, depends_on: str, username: str | None) -> str:

View File

@ -56,6 +56,7 @@ __all__ = [
"srcinfo_property",
"srcinfo_property_list",
"trim_package",
"unquote",
"utcnow",
"walk",
]
@ -465,6 +466,38 @@ def trim_package(package_name: str) -> str:
return package_name
def unquote(source: str) -> str:
"""
like ``shlex.quote``, but opposite
Args:
source(str): source string to remove quotes
Returns:
str: string with quotes removed
Raises:
ValueError: if no closing quotation
"""
def generator() -> Generator[str, None, None]:
token = None
for char in source:
if token is not None:
if char == token:
token = None # closed quote
else:
yield char # character inside quotes
elif char in ("'", "\""):
token = char # first quote found
else:
yield char # normal character
if token is not None:
raise ValueError("No closing quotation")
return "".join(generator())
def utcnow() -> datetime.datetime:
"""
get current time

View File

@ -19,8 +19,11 @@
#
import shlex
from dataclasses import dataclass, field
from dataclasses import dataclass
from pathlib import Path
from typing import Self
from ahriman.core.util import unquote
@dataclass(frozen=True)
@ -33,12 +36,12 @@ class PkgbuildPatch:
considered as full PKGBUILD diffs
value(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 | None
value: str | list[str]
unsafe: bool = field(default=False, kw_only=True)
quote = shlex.quote
def __post_init__(self) -> None:
"""
@ -66,17 +69,26 @@ class PkgbuildPatch:
"""
return self.key is None
def quote(self, value: str) -> str:
@classmethod
def from_env(cls, variable: str) -> Self:
"""
quote value according to the unsafe flag
construct patch from environment variable. Functions are not supported
Args:
value(str): value to be quoted
variable(str): variable in bash form, i.e. KEY=VALUE
Returns:
str: quoted string in case if unsafe is False and as is otherwise
Self: package properties
"""
return value if self.unsafe else shlex.quote(value)
key, *value_parts = variable.split("=", maxsplit=1)
raw_value = next(iter(value_parts), "") # extract raw value
if raw_value.startswith("(") and raw_value.endswith(")"):
value: str | list[str] = shlex.split(raw_value[1:-1]) # arrays for poor
else:
value = unquote(raw_value)
return cls(key, value)
def serialize(self) -> str:
"""
@ -88,14 +100,14 @@ class PkgbuildPatch:
str: serialized key-value pair, print-friendly
"""
if isinstance(self.value, list): # list like
value = " ".join(map(self.quote, self.value))
value = " ".join(map(PkgbuildPatch.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
return f"""{self.key}={self.quote(self.value)}"""
return f"""{self.key}={PkgbuildPatch.quote(self.value)}"""
def write(self, pkgbuild_path: Path) -> None:
"""

View File

@ -52,6 +52,8 @@ class RepositoryId:
Returns:
str: unique id for this repository
"""
if self.is_empty:
return ""
return f"{self.architecture}-{self.name}" # basically the same as used for command line
def query(self) -> list[tuple[str, str]]:

View File

@ -33,6 +33,7 @@ from ahriman.web.schemas.package_properties_schema import PackagePropertiesSchem
from ahriman.web.schemas.package_schema import PackageSchema
from ahriman.web.schemas.package_status_schema import PackageStatusSimplifiedSchema, PackageStatusSchema
from ahriman.web.schemas.pagination_schema import PaginationSchema
from ahriman.web.schemas.patch_schema import PackagePatchSchema, PatchSchema
from ahriman.web.schemas.pgp_key_id_schema import PGPKeyIdSchema
from ahriman.web.schemas.pgp_key_schema import PGPKeySchema
from ahriman.web.schemas.process_id_schema import ProcessIdSchema

View File

@ -0,0 +1,45 @@
#
# Copyright (c) 2021-2023 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 marshmallow import Schema, fields
from ahriman.web.schemas.package_names_schema import PackageNamesSchema
class PatchSchema(Schema):
"""
request patch schema
"""
key = fields.String(required=True, metadata={
"description": "environment variable name",
})
value = fields.String(metadata={
"description": "environment variable value",
})
class PackagePatchSchema(PackageNamesSchema):
"""
request schema with packages and patches
"""
patches = fields.Nested(PatchSchema(many=True), metadata={
"description": "optional environment variables to be applied as patches"
})

View File

@ -21,8 +21,9 @@ import aiohttp_apispec # type: ignore[import-untyped]
from aiohttp.web import HTTPBadRequest, Response, json_response
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema, RepositoryIdSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackagePatchSchema, ProcessIdSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
@ -53,7 +54,7 @@ class AddView(BaseView):
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
@aiohttp_apispec.json_schema(PackageNamesSchema)
@aiohttp_apispec.json_schema(PackagePatchSchema)
async def post(self) -> Response:
"""
add new package
@ -65,13 +66,14 @@ class AddView(BaseView):
HTTPBadRequest: if bad data is supplied
"""
try:
data = await self.extract_data(["packages"])
data = await self.extract_data(["packages", "patches"])
packages = self.get_non_empty(lambda key: [package for package in data[key] if package], "packages")
patches = [PkgbuildPatch(patch["key"], patch.get("value", "")) for patch in data.get("patches", [])]
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
repository_id = self.repository_id()
username = await self.username()
process_id = self.spawner.packages_add(repository_id, packages, username, now=True)
process_id = self.spawner.packages_add(repository_id, packages, username, patches=patches, now=True)
return json_response({"process_id": process_id})

View File

@ -21,8 +21,9 @@ import aiohttp_apispec # type: ignore[import-untyped]
from aiohttp.web import HTTPBadRequest, Response, json_response
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema, RepositoryIdSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackagePatchSchema, ProcessIdSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
@ -53,7 +54,7 @@ class RequestView(BaseView):
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
@aiohttp_apispec.json_schema(PackageNamesSchema)
@aiohttp_apispec.json_schema(PackagePatchSchema)
async def post(self) -> Response:
"""
request to add new package
@ -65,13 +66,14 @@ class RequestView(BaseView):
HTTPBadRequest: if bad data is supplied
"""
try:
data = await self.extract_data(["packages"])
data = await self.extract_data(["packages", "patches"])
packages = self.get_non_empty(lambda key: [package for package in data[key] if package], "packages")
patches = [PkgbuildPatch(patch["key"], patch.get("value", "")) for patch in data.get("patches", [])]
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
username = await self.username()
repository_id = self.repository_id()
process_id = self.spawner.packages_add(repository_id, packages, username, now=False)
process_id = self.spawner.packages_add(repository_id, packages, username, patches=patches, now=False)
return json_response({"process_id": process_id})

View File

@ -9,6 +9,7 @@ from ahriman.core.repository import Repository
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
from ahriman.models.packagers import Packagers
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.result import Result
@ -22,7 +23,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
Returns:
argparse.Namespace: generated arguments for these test cases
"""
args.package = []
args.package = ["ahriman"]
args.exit_code = False
args.increment = True
args.now = False
@ -30,6 +31,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.source = PackageSource.Auto
args.dependencies = True
args.username = "username"
args.variable = None
return args
@ -51,6 +53,22 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
on_start_mock.assert_called_once_with()
def test_run_with_patches(args: argparse.Namespace, configuration: Configuration, repository: Repository,
mocker: MockerFixture) -> None:
"""
must run command and insert temporary patches
"""
args = _default_args(args)
args.variable = ["KEY=VALUE"]
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
mocker.patch("ahriman.application.application.Application.add")
application_mock = mocker.patch("ahriman.core.database.SQLite.patches_insert")
_, repository_id = configuration.check_loaded()
Add.run(args, repository_id, configuration, report=False)
application_mock.assert_called_once_with(args.package[0], [PkgbuildPatch("KEY", "VALUE")])
def test_run_with_updates(args: argparse.Namespace, configuration: Configuration, repository: Repository,
package_ahriman: Package, mocker: MockerFixture) -> None:
"""

View File

@ -180,7 +180,7 @@ def test_patch_set_create(application: Application, package_ahriman: Package, mo
"""
create_mock = mocker.patch("ahriman.core.database.SQLite.patches_insert")
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))
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:

View File

@ -254,6 +254,22 @@ def test_subparsers_package_add_option_refresh(parser: argparse.ArgumentParser)
assert args.refresh == 2
def test_subparsers_package_add_option_variable_empty(parser: argparse.ArgumentParser) -> None:
"""
package-add command must accept empty variable list as None
"""
args = parser.parse_args(["package-add", "ahriman"])
assert args.variable is None
def test_subparsers_package_add_option_variable_multiple(parser: argparse.ArgumentParser) -> None:
"""
repo-rebuild command must accept multiple depends-on
"""
args = parser.parse_args(["package-add", "ahriman", "-v", "var1", "-v", "var2"])
assert args.variable == ["var1", "var2"]
def test_subparsers_package_remove_option_architecture(parser: argparse.ArgumentParser) -> None:
"""
package-remove command must correctly parse architecture list

View File

@ -20,6 +20,7 @@ def test_init(task_ahriman: Task, database: SQLite, mocker: MockerFixture) -> No
"""
mocker.patch("ahriman.models.package.Package.from_build", return_value=task_ahriman.package)
load_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.load")
task_ahriman.init(Path("ahriman"), database, None)
load_mock.assert_called_once_with(Path("ahriman"), task_ahriman.package, [], task_ahriman.paths)

View File

@ -8,7 +8,7 @@ from ahriman.core.configuration import Configuration
from ahriman.core.database.migrations.m011_repository_name import migrate_data, migrate_package_repository, steps
def test_migration_check_depends() -> None:
def test_migration_repository_name() -> None:
"""
migration must not be empty
"""

View File

@ -7,9 +7,9 @@ def test_patches_get_insert(database: SQLite, package_ahriman: Package, package_
"""
must insert patch to database
"""
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"))
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")
]
@ -19,9 +19,9 @@ def test_patches_list(database: SQLite, package_ahriman: Package, package_python
"""
must list all patches
"""
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_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, None) == {
package_ahriman.base: [PkgbuildPatch(None, "patch1"), PkgbuildPatch("key", "patch3")],
package_python_schedule.base: [PkgbuildPatch(None, "patch2")],
@ -32,8 +32,8 @@ def test_patches_list_filter(database: SQLite, package_ahriman: Package, package
"""
must list all patches filtered by package name (same as get)
"""
database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "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, None) == {package_ahriman.base: [PkgbuildPatch(None, "patch1")]}
assert database.patches_list(package_python_schedule.base, None) == {
@ -46,9 +46,9 @@ def test_patches_list_filter_by_variable(database: SQLite, package_ahriman: Pack
"""
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"))
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, None) == {
package_ahriman.base: [PkgbuildPatch(None, "patch1"), PkgbuildPatch("key", "patch2")],
@ -63,8 +63,8 @@ def test_patches_insert_remove(database: SQLite, package_ahriman: Package, packa
"""
must remove patch from database
"""
database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2"))
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, None)
assert database.patches_get(package_ahriman.base) == []
@ -76,9 +76,9 @@ def test_patches_insert_remove_by_variable(database: SQLite, package_ahriman: Pa
"""
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_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")]
@ -89,8 +89,14 @@ def test_patches_insert_insert(database: SQLite, package_ahriman: Package) -> No
"""
must update patch in database
"""
database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
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, PkgbuildPatch(None, "patch2"))
database.patches_insert(package_ahriman.base, [PkgbuildPatch(None, "patch2")])
assert database.patches_get(package_ahriman.base) == [PkgbuildPatch(None, "patch2")]
database.patches_insert(package_ahriman.base, [PkgbuildPatch(None, "patch3"), PkgbuildPatch("key", "patch4")])
assert database.patches_get(package_ahriman.base) == [
PkgbuildPatch(None, "patch3"),
PkgbuildPatch("key", "patch4"),
]

View File

@ -4,6 +4,7 @@ from pytest_mock import MockerFixture
from unittest.mock import MagicMock
from ahriman.core.spawn import Spawn
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.process_status import ProcessStatus
from ahriman.models.repository_id import RepositoryId
@ -56,11 +57,12 @@ def test_spawn_process(spawner: Spawn, repository_id: RepositoryId, mocker: Mock
"""
start_mock = mocker.patch("multiprocessing.Process.start")
assert spawner._spawn_process(repository_id, "add", "ahriman", now="", maybe="?", none=None)
assert spawner._spawn_process(repository_id, "command", "argument",
empty="", string="v", list=["a", "b"], empty_list=[], none=None)
start_mock.assert_called_once_with()
spawner.args_parser.parse_args.assert_called_once_with(
spawner.command_arguments + [
"add", "ahriman", "--now", "--maybe", "?"
"command", "argument", "--empty", "--string", "v", "--list", "a", "--list", "b",
]
)
@ -99,7 +101,7 @@ def test_packages_add(spawner: Spawn, repository_id: RepositoryId, mocker: Mocke
must call package addition
"""
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process")
assert spawner.packages_add(repository_id, ["ahriman", "linux"], None, now=False)
assert spawner.packages_add(repository_id, ["ahriman", "linux"], None, patches=[], now=False)
spawn_mock.assert_called_once_with(repository_id, "package-add", "ahriman", "linux", username=None)
@ -108,7 +110,7 @@ def test_packages_add_with_build(spawner: Spawn, repository_id: RepositoryId, mo
must call package addition with update
"""
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process")
assert spawner.packages_add(repository_id, ["ahriman", "linux"], None, now=True)
assert spawner.packages_add(repository_id, ["ahriman", "linux"], None, patches=[], now=True)
spawn_mock.assert_called_once_with(repository_id, "package-add", "ahriman", "linux", username=None, now="")
@ -117,10 +119,21 @@ def test_packages_add_with_username(spawner: Spawn, repository_id: RepositoryId,
must call package addition with username
"""
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process")
assert spawner.packages_add(repository_id, ["ahriman", "linux"], "username", now=False)
assert spawner.packages_add(repository_id, ["ahriman", "linux"], "username", patches=[], now=False)
spawn_mock.assert_called_once_with(repository_id, "package-add", "ahriman", "linux", username="username")
def test_packages_add_with_patches(spawner: Spawn, repository_id: RepositoryId, mocker: MockerFixture) -> None:
"""
must call package addition with patches
"""
patches = [PkgbuildPatch("key", "value"), PkgbuildPatch("key", "value")]
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process")
assert spawner.packages_add(repository_id, ["ahriman", "linux"], None, patches=patches, now=False)
spawn_mock.assert_called_once_with(repository_id, "package-add", "ahriman", "linux", username=None,
variable=[patch.serialize() for patch in patches])
def test_packages_rebuild(spawner: Spawn, repository_id: RepositoryId, mocker: MockerFixture) -> None:
"""
must call package rebuild

View File

@ -2,6 +2,7 @@ import datetime
import logging
import os
import pytest
import shlex
from pathlib import Path
from pytest_mock import MockerFixture
@ -11,7 +12,7 @@ from unittest.mock import call as MockCall
from ahriman.core.exceptions import BuildError, CalledProcessError, OptionError, UnsafeRunError
from ahriman.core.util import check_output, check_user, dataclass_view, enum_values, extract_user, filter_json, \
full_version, minmax, package_like, parse_version, partition, pretty_datetime, pretty_size, safe_filename, \
srcinfo_property, srcinfo_property_list, trim_package, utcnow, walk
srcinfo_property, srcinfo_property_list, trim_package, unquote, utcnow, walk
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
from ahriman.models.repository_paths import RepositoryPaths
@ -443,6 +444,26 @@ def test_trim_package() -> None:
assert trim_package("package: a description") == "package"
def test_unquote() -> None:
"""
must remove quotation marks
"""
for source in (
"abc",
"ab'c",
"ab\"c",
):
assert unquote(shlex.quote(source)) == source
def test_unquote_error() -> None:
"""
must raise value error on invalid quotation
"""
with pytest.raises(ValueError):
unquote("ab'c")
def test_utcnow() -> None:
"""
must generate correct timestamp

View File

@ -34,9 +34,18 @@ def test_quote() -> None:
"""
must quote strings if unsafe flag is not set
"""
assert PkgbuildPatch("key", "value").quote("value") == """value"""
assert PkgbuildPatch("key", "va'lue").quote("va'lue") == """'va'"'"'lue'"""
assert PkgbuildPatch("key", "va'lue", unsafe=True).quote("va'lue") == """va'lue"""
assert PkgbuildPatch.quote("value") == """value"""
assert PkgbuildPatch.quote("va'lue") == """'va'"'"'lue'"""
def test_from_env() -> None:
"""
must construct patch from environment variable
"""
assert PkgbuildPatch.from_env("KEY=VALUE") == PkgbuildPatch("KEY", "VALUE")
assert PkgbuildPatch.from_env("KEY=VA=LUE") == PkgbuildPatch("KEY", "VA=LUE")
assert PkgbuildPatch.from_env("KEY=") == PkgbuildPatch("KEY", "")
assert PkgbuildPatch.from_env("KEY") == PkgbuildPatch("KEY", "")
def test_serialize() -> None:
@ -46,7 +55,19 @@ def test_serialize() -> None:
assert PkgbuildPatch("key", "value").serialize() == "key=value"
assert PkgbuildPatch("key", "42").serialize() == "key=42"
assert PkgbuildPatch("key", "4'2").serialize() == """key='4'"'"'2'"""
assert PkgbuildPatch("key", "4'2", unsafe=True).serialize() == "key=4'2"
def test_from_env_serialize() -> None:
"""
must serialize and parse back
"""
for patch in (
PkgbuildPatch("key", "value"),
PkgbuildPatch("key", "4'2"),
PkgbuildPatch("arch", ["i686", "x86_64"]),
PkgbuildPatch("key", ["val'ue", "val\"ue2"]),
):
assert PkgbuildPatch.from_env(patch.serialize()) == patch
def test_serialize_plain_diff() -> None:
@ -60,7 +81,7 @@ def test_serialize_function() -> None:
"""
must correctly serialize function values
"""
assert PkgbuildPatch("key()", "{ value }", unsafe=True).serialize() == "key() { value }"
assert PkgbuildPatch("key()", "{ value }").serialize() == "key() { value }"
def test_serialize_list() -> None:
@ -69,7 +90,6 @@ def test_serialize_list() -> None:
"""
assert PkgbuildPatch("arch", ["i686", "x86_64"]).serialize() == """arch=(i686 x86_64)"""
assert PkgbuildPatch("key", ["val'ue", "val\"ue2"]).serialize() == """key=('val'"'"'ue' 'val"ue2')"""
assert PkgbuildPatch("key", ["val'ue", "val\"ue2"], unsafe=True).serialize() == """key=(val'ue val"ue2)"""
def test_write(mocker: MockerFixture) -> None:

View File

@ -17,7 +17,7 @@ def test_id() -> None:
"""
must correctly generate id
"""
assert RepositoryId("", "").id == "-"
assert RepositoryId("", "").id == ""
assert RepositoryId("arch", "repo").id == "arch-repo"

View File

@ -0,0 +1 @@
# schema testing goes in view class tests

View File

@ -4,6 +4,7 @@ from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from unittest.mock import AsyncMock
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_id import RepositoryId
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v1.service.add import AddView
@ -40,13 +41,42 @@ async def test_post(client: TestClient, repository_id: RepositoryId, mocker: Moc
assert not request_schema.validate(payload)
response = await client.post("/api/v1/service/add", json=payload)
assert response.ok
add_mock.assert_called_once_with(repository_id, ["ahriman"], "username", now=True)
add_mock.assert_called_once_with(repository_id, ["ahriman"], "username", patches=[], now=True)
json = await response.json()
assert json["process_id"] == "abc"
assert not response_schema.validate(json)
async def test_post_patches(client: TestClient, repository_id: RepositoryId, mocker: MockerFixture) -> None:
"""
must call post request with patches correctly
"""
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add", return_value="abc")
user_mock = AsyncMock()
user_mock.return_value = "username"
mocker.patch("ahriman.web.views.base.BaseView.username", side_effect=user_mock)
request_schema = pytest.helpers.schema_request(AddView.post)
payload = {
"packages": ["ahriman"],
"patches": [
{
"key": "k",
"value": "v",
},
{
"key": "k2",
},
]
}
assert not request_schema.validate(payload)
response = await client.post("/api/v1/service/add", json=payload)
assert response.ok
add_mock.assert_called_once_with(repository_id, ["ahriman"], "username",
patches=[PkgbuildPatch("k", "v"), PkgbuildPatch("k2", "")], now=True)
async def test_post_empty(client: TestClient, mocker: MockerFixture) -> None:
"""
must call raise 400 on empty request

View File

@ -4,6 +4,7 @@ from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from unittest.mock import AsyncMock
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_id import RepositoryId
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v1.service.request import RequestView
@ -40,13 +41,42 @@ async def test_post(client: TestClient, repository_id: RepositoryId, mocker: Moc
assert not request_schema.validate(payload)
response = await client.post("/api/v1/service/request", json=payload)
assert response.ok
add_mock.assert_called_once_with(repository_id, ["ahriman"], "username", now=False)
add_mock.assert_called_once_with(repository_id, ["ahriman"], "username", patches=[], now=False)
json = await response.json()
assert json["process_id"] == "abc"
assert not response_schema.validate(json)
async def test_post_patches(client: TestClient, repository_id: RepositoryId, mocker: MockerFixture) -> None:
"""
must call post request with patches correctly
"""
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add", return_value="abc")
user_mock = AsyncMock()
user_mock.return_value = "username"
mocker.patch("ahriman.web.views.base.BaseView.username", side_effect=user_mock)
request_schema = pytest.helpers.schema_request(RequestView.post)
payload = {
"packages": ["ahriman"],
"patches": [
{
"key": "k",
"value": "v",
},
{
"key": "k2",
},
]
}
assert not request_schema.validate(payload)
response = await client.post("/api/v1/service/request", json=payload)
assert response.ok
add_mock.assert_called_once_with(repository_id, ["ahriman"], "username",
patches=[PkgbuildPatch("k", "v"), PkgbuildPatch("k2", "")], now=False)
async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None:
"""
must raise exception on missing packages payload