Compare commits

...

7 Commits

Author SHA1 Message Date
6bfa0d26d4 Release 2.15.2 2024-09-26 16:58:19 +03:00
343435b3bf fix: fix pkgbuild parsing in some cases
It has been found that there are two cases in which pkgbuild was not
parsed correctly

1. Major case in which there is quotation mark inside comment line,
   which would cause ValueError: No closing quotation error
2. Minor case, if there are utf symbols in pkgbuild file (e.g.
   hieroglyphs, see ttf-google-fonts-git), it will case incorrect
   reading in `_is_escaped` method
2024-09-26 16:48:38 +03:00
f0930be238 fix: do not copy own database during pyalpm initialization
Previous implementation lead to warning in logs in case if the
repository itself wasn't configured on the host
2024-09-25 14:31:32 +03:00
113a861f31 fix: suppress info logging during version check 2024-09-25 14:23:53 +03:00
528d7ce398 feat: suppress info log during vcs version calculation 2024-09-25 07:15:51 +03:00
b357c96204 refactor: even further improvements for Handler.check_status method 2024-09-25 07:15:51 +03:00
5d495fc813 type: remove unused ignore comment 2024-09-25 07:15:51 +03:00
42 changed files with 6667 additions and 6671 deletions

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -52,6 +52,14 @@ ahriman.core.tree module
:no-undoc-members:
:show-inheritance:
ahriman.core.types module
-------------------------
.. automodule:: ahriman.core.types
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.util module
------------------------

View File

@ -1,7 +1,7 @@
# Maintainer: Evgeniy Alekseev
pkgname='ahriman'
pkgver=2.15.1
pkgver=2.15.2
pkgrel=1
pkgdesc="ArcH linux ReposItory MANager"
arch=('any')

View File

@ -1,4 +1,4 @@
.TH AHRIMAN "1" "2024\-09\-24" "ahriman" "Generated Python Manual"
.TH AHRIMAN "1" "2024\-09\-26" "ahriman" "Generated Python Manual"
.SH NAME
ahriman
.SH SYNOPSIS
@ -989,7 +989,25 @@ usage: ahriman web [\-h]
start web server
.SH COMMENTS
Argument list can also be read from file by using @ prefix.
Quick setup command (replace repository name, architecture and packager as needed):
>>> ahriman \-a x86_64 \-r aur service\-setup \-\-packager "ahriman bot <ahriman@example.com>"
Add new package from AUR:
>>> ahriman package\-add ahriman \-\-now
Check for updates and build out\-of\-dated packages (add ``\-\-dry\-run`` to build it later):
>>> ahriman repo\-update
Remove package from the repository:
>>> ahriman package\-remove ahriman
Start web service (requires additional configuration):
>>> ahriman web
.SH AUTHOR
.nf

View File

@ -17,4 +17,4 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
__version__ = "2.15.1"
__version__ = "2.15.2"

View File

@ -27,6 +27,7 @@ from ahriman.application.lock import Lock
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import ExitCode, MissingArchitectureError, MultipleArchitecturesError
from ahriman.core.log.log_loader import LogLoader
from ahriman.core.types import ExplicitBool
from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_paths import RepositoryPaths
@ -124,13 +125,14 @@ class Handler:
raise NotImplementedError
@staticmethod
def check_status(enabled: bool, status: bool | Callable[[], bool]) -> None:
def check_status(enabled: bool, status: ExplicitBool | Callable[[], ExplicitBool]) -> None:
"""
check condition and flag and raise ExitCode exception in case if it is enabled and condition match
Args:
enabled(bool): if ``False`` no check will be performed
status(bool | Callable[[], bool]): return status or function to check. ``True`` means success and vice versa
status(ExplicitBool | Callable[[], ExplicitBool]): return status or function to check.
``True`` means success and vice versa
Raises:
ExitCode: if result is empty and check is enabled
@ -138,12 +140,9 @@ class Handler:
if not enabled:
return
match status:
case False:
raise ExitCode
# https://github.com/python/mypy/issues/14014
case Callable() if not status(): # type: ignore[misc]
raise ExitCode
status = status() if callable(status) else status
if not status:
raise ExitCode
@staticmethod
def repositories_extract(args: argparse.Namespace) -> list[RepositoryId]:

View File

@ -136,7 +136,7 @@ class Patch(Handler):
for patch in application.reporter.package_patches_get(package_base, None)
if variables is None or patch.key in variables
]
Patch.check_status(exit_code, bool(patches))
Patch.check_status(exit_code, patches)
PatchPrinter(package_base, patches)(verbose=True, separator=" = ")

View File

@ -51,7 +51,7 @@ class Rebuild(Handler):
packages = Rebuild.extract_packages(application, args.status, from_database=args.from_database)
packages = application.repository.packages_depend_on(packages, args.depends_on)
Rebuild.check_status(args.exit_code, bool(packages))
Rebuild.check_status(args.exit_code, packages)
if args.dry_run:
application.print_updates(packages, log_fn=print)
return

View File

@ -141,7 +141,7 @@ class Setup(Handler):
(root.include / "00-setup-overrides.ini").unlink(missing_ok=True) # remove old-style configuration
target = root.include / f"00-setup-overrides-{repository_id.id}.ini"
with target.open("w") as ahriman_configuration:
with target.open("w", encoding="utf8") as ahriman_configuration:
configuration.write(ahriman_configuration)
@staticmethod
@ -191,7 +191,7 @@ class Setup(Handler):
configuration.set_option(repository_id.name, "Server", repository_server)
target = source.parent / f"{repository_id.name}-{repository_id.architecture}.conf"
with target.open("w") as devtools_configuration:
with target.open("w", encoding="utf8") as devtools_configuration:
configuration.write(devtools_configuration)
@staticmethod

View File

@ -61,7 +61,7 @@ class Status(Handler):
else:
packages = client.package_get(None)
Status.check_status(args.exit_code, bool(packages))
Status.check_status(args.exit_code, packages)
comparator: Callable[[tuple[Package, BuildStatus]], str] = lambda item: item[0].base
filter_fn: Callable[[tuple[Package, BuildStatus]], bool] =\

View File

@ -54,7 +54,7 @@ class Update(Handler):
application.changes(packages)
if args.dry_run: # exit from application if no build requested
Update.check_status(args.exit_code, bool(packages)) # status code check
Update.check_status(args.exit_code, packages) # status code check
return
packages = application.with_dependencies(packages, process_dependencies=args.dependencies)

View File

@ -61,7 +61,7 @@ class Users(Handler):
users = database.user_list(args.username, args.role)
for user in users:
UserPrinter(user)(verbose=True)
Users.check_status(args.exit_code, bool(users))
Users.check_status(args.exit_code, users)
case Action.Remove:
database.user_remove(args.username)

View File

@ -112,7 +112,7 @@ class Lock(LazyLogging):
"""
if self.path is None:
return
self._pid_file = self.path.open("a+")
self._pid_file = self.path.open("a+", encoding="utf8")
def _watch(self) -> bool:
"""

View File

@ -91,9 +91,8 @@ class Pacman(LazyLogging):
database = self.database_init(handle, repository, self.repository_id.architecture)
self.database_copy(handle, database, pacman_root, use_ahriman_cache=use_ahriman_cache)
# install repository database too
local_database = self.database_init(handle, self.repository_id.name, self.repository_id.architecture)
self.database_copy(handle, local_database, pacman_root, use_ahriman_cache=use_ahriman_cache)
# install repository database too (without copying)
self.database_init(handle, self.repository_id.name, self.repository_id.architecture)
if use_ahriman_cache and refresh_database:
self.database_sync(handle, force=refresh_database == PacmanSynchronization.Force)
@ -115,6 +114,7 @@ class Pacman(LazyLogging):
if not use_ahriman_cache:
return
# copy root database if no local copy found
pacman_db_path = Path(handle.dbpath)
if not pacman_db_path.is_dir():
@ -123,11 +123,13 @@ class Pacman(LazyLogging):
if dst.is_file():
return # file already exists, do not copy
dst.parent.mkdir(mode=0o755, exist_ok=True) # create sync directory if it doesn't exist
src = repository_database(pacman_root)
if not src.is_file():
self.logger.warning("repository %s is set to be used, however, no working copy was found", database.name)
return # database for some reason deos not exist
self.logger.info("copy pacman database from operating system root to ahriman's home")
self.logger.info("copy pacman database %s from operating system root to ahriman's home %s", src, dst)
shutil.copy(src, dst)
self.repository_paths.chown(dst)

View File

@ -174,18 +174,31 @@ class PkgbuildParser(shlex.shlex):
Returns:
bool: ``True`` if the previous element of the stream is a quote or escaped and ``False`` otherwise
"""
# wrapper around reading utf symbols from random position of the stream
def read_last() -> tuple[int, str]:
while (position := self._io.tell()) > 0:
try:
return position, self._io.read(1)
except UnicodeDecodeError:
self._io.seek(position - 1)
raise PkgbuildParserError("reached starting position, no valid symbols found")
current_position = self._io.tell()
last_char = penultimate_char = None
for index in range(current_position - 1, -1, -1):
index = current_position - 1
while index > 0:
self._io.seek(index)
last_char = self._io.read(1)
index, last_char = read_last()
if last_char.isspace():
index -= 1
continue
if index >= 0:
if index > 1:
self._io.seek(index - 1)
penultimate_char = self._io.read(1)
_, penultimate_char = read_last()
break
@ -216,6 +229,7 @@ class PkgbuildParser(shlex.shlex):
case PkgbuildToken.Comment:
self.instream.readline()
continue
yield token
if token != PkgbuildToken.ArrayEnds:
@ -248,24 +262,28 @@ class PkgbuildParser(shlex.shlex):
counter += 1
case PkgbuildToken.FunctionEnds:
end_position = self._io.tell()
if self.state != self.eof: # type: ignore[attr-defined]
end_position -= 1 # if we are not at the end of the file, position is _after_ the token
counter -= 1
if counter == 0:
break
case PkgbuildToken.Comment:
self.instream.readline()
if not 0 < start_position < end_position:
raise PkgbuildParserError("function body wasn't found")
# read the specified interval from source stream
self._io.seek(start_position - 1) # start from the previous symbol
content = self._io.read(end_position - start_position)
# we cannot use :func:`read()` here, because it reads characters, not bytes
content = ""
while self._io.tell() != end_position and (next_char := self._io.read(1)):
content += next_char
# special case of the end of file
if self.state == self.eof: # type: ignore[attr-defined]
content += self._io.read(1)
# reset position (because the last position was before the next token starts)
self._io.seek(end_position)
return content
def _parse_token(self, token: str) -> Generator[PkgbuildPatch, None, None]:

View File

@ -141,7 +141,7 @@ def migrate_package_statuses(connection: Connection, paths: RepositoryPaths) ->
cache_path = paths.root / "status_cache.json"
if not cache_path.is_file():
return # no file found
with cache_path.open() as cache:
with cache_path.open(encoding="utf8") as cache:
dump = json.load(cache)
for item in dump.get("packages", []):

View File

@ -23,9 +23,8 @@ from collections.abc import Callable
from pathlib import Path
from typing import Any, TypeVar
from ahriman.core.configuration import Configuration
from ahriman.core.log import LazyLogging
from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_paths import RepositoryPaths
T = TypeVar("T")
@ -39,16 +38,16 @@ class Operations(LazyLogging):
path(Path): path to the database file
"""
def __init__(self, path: Path, repository_id: RepositoryId, repository_paths: RepositoryPaths) -> None:
def __init__(self, path: Path, configuration: Configuration) -> None:
"""
Args:
path(Path): path to the database file
repository_id(RepositoryId): repository unique identifier
repository_paths(RepositoryPaths): repository paths
configuration(Configuration): configuration instance
"""
self.path = path
self._repository_id = repository_id
self._repository_paths = repository_paths
self._configuration = configuration
_, self._repository_id = configuration.check_loaded()
self._repository_paths = configuration.repository_paths
@property
def logger_name(self) -> str:

View File

@ -66,10 +66,9 @@ class SQLite(
Self: fully initialized instance of the database
"""
path = cls.database_path(configuration)
_, repository_id = configuration.check_loaded()
database = cls(path, repository_id, configuration.repository_paths)
database.init(configuration)
database = cls(path, configuration)
database.init()
return database
@ -86,23 +85,18 @@ class SQLite(
"""
return configuration.getpath("settings", "database")
def init(self, configuration: Configuration) -> None:
def init(self) -> None:
"""
perform database migrations
Args:
configuration(Configuration): configuration instance
"""
# custom types support
sqlite3.register_adapter(dict, json.dumps)
sqlite3.register_adapter(list, json.dumps)
sqlite3.register_converter("json", json.loads)
paths = configuration.repository_paths
if configuration.getboolean("settings", "apply_migrations", fallback=True):
self.with_connection(lambda connection: Migrations.migrate(connection, configuration))
paths.chown(self.path)
if self._configuration.getboolean("settings", "apply_migrations", fallback=True):
self.with_connection(lambda connection: Migrations.migrate(connection, self._configuration))
self._repository_paths.chown(self.path)
def package_clear(self, package_base: str, repository_id: RepositoryId | None = None) -> None:
"""

View File

@ -99,3 +99,24 @@ class LazyLogging:
yield
finally:
self._package_logger_reset()
@contextlib.contextmanager
def suppress_logging(self, log_level: int = logging.WARNING) -> Generator[None, None, None]:
"""
silence log messages in context
Args:
log_level(int, optional): the highest log level to keep (Default value = logging.WARNING)
Examples:
This function is designed to be used to suppress all log messages in context, e.g.:
>>> with self.suppress_logging():
>>> do_some_noisy_actions()
"""
current_level = self.logger.manager.disable
try:
logging.disable(log_level)
yield
finally:
logging.disable(current_level)

View File

@ -144,7 +144,8 @@ class UpdateHandler(PackageInfo, Cleaner):
branch="master",
)
Sources.fetch(cache_dir, source)
with self.suppress_logging():
Sources.fetch(cache_dir, source)
remote = Package.from_build(cache_dir, self.architecture, None)
local = packages.get(remote.base)

View File

@ -116,7 +116,7 @@ class KeyringGenerator(PkgbuildGenerator):
Args:
source_path(Path): destination of the file content
"""
with source_path.open("w") as source_file:
with source_path.open("w", encoding="utf8") as source_file:
for key in sorted(set(self.trusted + self.packagers + self.revoked)):
public_key = self.sign.key_export(key)
source_file.write(public_key)
@ -129,7 +129,7 @@ class KeyringGenerator(PkgbuildGenerator):
Args:
source_path(Path): destination of the file content
"""
with source_path.open("w") as source_file:
with source_path.open("w", encoding="utf8") as source_file:
for key in sorted(set(self.revoked)):
fingerprint = self.sign.key_fingerprint(key)
source_file.write(fingerprint)
@ -147,7 +147,7 @@ class KeyringGenerator(PkgbuildGenerator):
"""
if not self.trusted:
raise PkgbuildGeneratorError
with source_path.open("w") as source_file:
with source_path.open("w", encoding="utf8") as source_file:
for key in sorted(set(self.trusted)):
fingerprint = self.sign.key_fingerprint(key)
source_file.write(fingerprint)

39
src/ahriman/core/types.py Normal file
View File

@ -0,0 +1,39 @@
#
# Copyright (c) 2021-2024 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 typing import Protocol
class HasBool(Protocol):
"""
class which defines :func:`bool()` method
"""
def __bool__(self) -> bool: ...
class HasLength(Protocol):
"""
class which defines :func:`len()` method
"""
def __len__(self) -> int: ...
ExplicitBool = HasBool | HasLength | int

View File

@ -429,13 +429,14 @@ class Package(LazyLogging):
task = Task(self, configuration, repository_id.architecture, paths)
try:
# create fresh chroot environment, fetch sources and - automagically - update PKGBUILD
task.init(paths.cache_for(self.base), [], None)
task.build(paths.cache_for(self.base), dry_run=True)
with self.suppress_logging():
# create fresh chroot environment, fetch sources and - automagically - update PKGBUILD
task.init(paths.cache_for(self.base), [], None)
task.build(paths.cache_for(self.base), dry_run=True)
pkgbuild = Pkgbuild.from_file(paths.cache_for(self.base) / "PKGBUILD")
pkgbuild = Pkgbuild.from_file(paths.cache_for(self.base) / "PKGBUILD")
return full_version(pkgbuild.get("epoch"), pkgbuild["pkgver"], pkgbuild["pkgrel"])
return full_version(pkgbuild.get("epoch"), pkgbuild["pkgver"], pkgbuild["pkgrel"])
except Exception:
self.logger.exception("cannot determine version of VCS package")
finally:

View File

@ -64,7 +64,7 @@ class Pkgbuild(Mapping[str, Any]):
Returns:
Self: constructed instance of self
"""
with path.open() as input_file:
with path.open(encoding="utf8") as input_file:
return cls.from_io(input_file)
@classmethod

View File

@ -199,7 +199,7 @@ class PkgbuildPatch:
Args:
pkgbuild_path(Path): path to PKGBUILD file
"""
with pkgbuild_path.open("a") as pkgbuild:
with pkgbuild_path.open("a", encoding="utf8") as pkgbuild:
pkgbuild.write("\n") # in case if file ends without new line we are appending it at the end
pkgbuild.write(self.serialize())
pkgbuild.write("\n") # append new line after the values

View File

@ -174,9 +174,8 @@ class BaseView(View, CorsViewMixin):
# using if/else in order to suppress mypy warning which doesn't know that
# :func:`aiohttp.web.View._raise_allowed_methods()` raises exception
if get_method is not None:
# there is a bug in pylint, see https://github.com/pylint-dev/pylint/issues/6005
response = await get_method()
response._body = b"" # type: ignore[assignment]
response._body = b""
return response
self._raise_allowed_methods()

View File

@ -168,7 +168,7 @@ def test_patch_set_list(application: Application, mocker: MockerFixture) -> None
Patch.patch_set_list(application, "ahriman", ["version"], False)
get_mock.assert_called_once_with("ahriman", None)
print_mock.assert_called_once_with(verbose=True, log_fn=pytest.helpers.anyvar(int), separator=" = ")
check_mock.assert_called_once_with(False, True)
check_mock.assert_called_once_with(False, [PkgbuildPatch(key='version', value='value')])
def test_patch_set_list_all(application: Application, mocker: MockerFixture) -> None:
@ -183,7 +183,7 @@ def test_patch_set_list_all(application: Application, mocker: MockerFixture) ->
Patch.patch_set_list(application, "ahriman", None, False)
get_mock.assert_called_once_with("ahriman", None)
print_mock.assert_called_once_with(verbose=True, log_fn=pytest.helpers.anyvar(int), separator=" = ")
check_mock.assert_called_once_with(False, True)
check_mock.assert_called_once_with(False, [PkgbuildPatch(key=None, value='patch')])
def test_patch_set_list_empty_exception(application: Application, mocker: MockerFixture) -> None:
@ -194,7 +194,7 @@ def test_patch_set_list_empty_exception(application: Application, mocker: Mocker
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_status")
Patch.patch_set_list(application, "ahriman", [], True)
check_mock.assert_called_once_with(True, False)
check_mock.assert_called_once_with(True, [])
def test_patch_set_create(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:

View File

@ -55,7 +55,7 @@ def test_run(args: argparse.Namespace, package_ahriman: Package, configuration:
extract_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.status, from_database=args.from_database)
application_packages_mock.assert_called_once_with([package_ahriman], None)
application_mock.assert_called_once_with([package_ahriman], Packagers(args.username), bump_pkgrel=args.increment)
check_mock.assert_has_calls([MockCall(False, True), MockCall(False, True)])
check_mock.assert_has_calls([MockCall(False, [package_ahriman]), MockCall(False, True)])
on_start_mock.assert_called_once_with()
@ -93,7 +93,7 @@ def test_run_dry_run(args: argparse.Namespace, configuration: Configuration, rep
_, repository_id = configuration.check_loaded()
Rebuild.run(args, repository_id, configuration, report=False)
application_mock.assert_not_called()
check_mock.assert_called_once_with(False, True)
check_mock.assert_called_once_with(False, [package_ahriman])
print_mock.assert_called_once_with([package_ahriman], log_fn=pytest.helpers.anyvar(int))
@ -146,7 +146,7 @@ def test_run_update_empty_exception(args: argparse.Namespace, configuration: Con
_, repository_id = configuration.check_loaded()
Rebuild.run(args, repository_id, configuration, report=False)
check_mock.assert_called_once_with(True, False)
check_mock.assert_called_once_with(True, [])
def test_run_build_empty_exception(args: argparse.Namespace, configuration: Configuration, repository: Repository,
@ -164,7 +164,7 @@ def test_run_build_empty_exception(args: argparse.Namespace, configuration: Conf
_, repository_id = configuration.check_loaded()
Rebuild.run(args, repository_id, configuration, report=False)
check_mock.assert_has_calls([MockCall(True, True), MockCall(True, False)])
check_mock.assert_has_calls([MockCall(True, [package_ahriman]), MockCall(True, False)])
def test_extract_packages(application: Application, mocker: MockerFixture) -> None:

View File

@ -36,11 +36,13 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
must run command
"""
args = _default_args(args)
packages = [
(package_ahriman, BuildStatus(BuildStatusEnum.Success)),
(package_python_schedule, BuildStatus(BuildStatusEnum.Failed)),
]
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
application_mock = mocker.patch("ahriman.core.status.Client.status_get")
packages_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_get",
return_value=[(package_ahriman, BuildStatus(BuildStatusEnum.Success)),
(package_python_schedule, BuildStatus(BuildStatusEnum.Failed))])
packages_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_get", return_value=packages)
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_status")
print_mock = mocker.patch("ahriman.core.formatters.Printer.print")
@ -48,7 +50,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
Status.run(args, repository_id, configuration, report=False)
application_mock.assert_called_once_with()
packages_mock.assert_called_once_with(None)
check_mock.assert_called_once_with(False, True)
check_mock.assert_called_once_with(False, packages)
print_mock.assert_has_calls([
MockCall(verbose=False, log_fn=pytest.helpers.anyvar(int), separator=": ")
for _ in range(3)
@ -69,7 +71,7 @@ def test_run_empty_exception(args: argparse.Namespace, configuration: Configurat
_, repository_id = configuration.check_loaded()
Status.run(args, repository_id, configuration, report=False)
check_mock.assert_called_once_with(True, False)
check_mock.assert_called_once_with(True, [])
def test_run_verbose(args: argparse.Namespace, configuration: Configuration, repository: Repository,

View File

@ -85,7 +85,7 @@ def test_run_empty_exception(args: argparse.Namespace, configuration: Configurat
_, repository_id = configuration.check_loaded()
Update.run(args, repository_id, configuration, report=False)
check_mock.assert_called_once_with(True, False)
check_mock.assert_called_once_with(True, [])
def test_run_update_empty_exception(args: argparse.Namespace, package_ahriman: Package, configuration: Configuration,
@ -127,7 +127,7 @@ def test_run_dry_run(args: argparse.Namespace, package_ahriman: Package, configu
args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs, check_files=args.check_files)
application_mock.assert_not_called()
changes_mock.assert_called_once_with([package_ahriman])
check_mock.assert_called_once_with(False, True)
check_mock.assert_called_once_with(False, [package_ahriman])
def test_run_no_changes(args: argparse.Namespace, configuration: Configuration, repository: Repository,

View File

@ -103,7 +103,7 @@ def test_run_list(args: argparse.Namespace, configuration: Configuration, databa
_, repository_id = configuration.check_loaded()
Users.run(args, repository_id, configuration, report=False)
list_mock.assert_called_once_with("user", args.role)
check_mock.assert_called_once_with(False, True)
check_mock.assert_called_once_with(False, [user])
def test_run_empty_exception(args: argparse.Namespace, configuration: Configuration, database: SQLite,
@ -120,7 +120,7 @@ def test_run_empty_exception(args: argparse.Namespace, configuration: Configurat
_, repository_id = configuration.check_loaded()
Users.run(args, repository_id, configuration, report=False)
check_mock.assert_called_once_with(True, False)
check_mock.assert_called_once_with(True, [])
def test_run_remove(args: argparse.Namespace, configuration: Configuration, database: SQLite,

View File

@ -63,7 +63,7 @@ def test_open(lock: Lock, mocker: MockerFixture) -> None:
lock.path = Path("ahriman.pid")
lock._open()
open_mock.assert_called_once_with("a+")
open_mock.assert_called_once_with("a+", encoding="utf8")
def test_open_skip(lock: Lock, mocker: MockerFixture) -> None:

View File

@ -42,6 +42,17 @@ def test_expand_array_exception() -> None:
assert PkgbuildParser._expand_array(["${pkgbase}{", ",", "-libs"])
def test_is_escaped_exception(resource_path_root: Path) -> None:
"""
must raise PkgbuildParserError if no valid utf symbols found
"""
utf8 = resource_path_root / "models" / "utf8"
with utf8.open(encoding="utf8") as content:
content.seek(2)
with pytest.raises(PkgbuildParserError):
assert not PkgbuildParser(content)._is_escaped()
def test_parse_array() -> None:
"""
must parse array
@ -193,7 +204,7 @@ def test_parse(resource_path_root: Path) -> None:
must parse complex file
"""
pkgbuild = resource_path_root / "models" / "pkgbuild"
with pkgbuild.open() as content:
with pkgbuild.open(encoding="utf8") as content:
parser = PkgbuildParser(content)
assert list(parser.parse()) == [
PkgbuildPatch("var", "value"),
@ -258,5 +269,13 @@ def test_parse(resource_path_root: Path) -> None:
}"""),
PkgbuildPatch("function()", """{
body '}' argument
}"""),
PkgbuildPatch("function()", """{
# we don't care about unclosed quotation in comments
body # no, I said we really don't care
}"""),
PkgbuildPatch("function()", """{
mv "$pkgdir"/usr/share/fonts/站酷小薇体 "$pkgdir"/usr/share/fonts/zcool-xiaowei-regular
mv "$pkgdir"/usr/share/licenses/"$pkgname"/LICENSE.站酷小薇体 "$pkgdir"/usr/share/licenses/"$pkgname"/LICENSE.zcool-xiaowei-regular
}"""),
]

View File

@ -13,7 +13,7 @@ def test_load(configuration: Configuration, mocker: MockerFixture) -> None:
"""
init_mock = mocker.patch("ahriman.core.database.SQLite.init")
SQLite.load(configuration)
init_mock.assert_called_once_with(configuration)
init_mock.assert_called_once_with()
def test_init(database: SQLite, configuration: Configuration, mocker: MockerFixture) -> None:
@ -21,18 +21,18 @@ def test_init(database: SQLite, configuration: Configuration, mocker: MockerFixt
must run migrations on init
"""
migrate_schema_mock = mocker.patch("ahriman.core.database.migrations.Migrations.migrate")
database.init(configuration)
migrate_schema_mock.assert_called_once_with(pytest.helpers.anyvar(int), configuration)
database.init()
migrate_schema_mock.assert_called_once_with(pytest.helpers.anyvar(int), database._configuration)
def test_init_skip_migration(database: SQLite, configuration: Configuration, mocker: MockerFixture) -> None:
def test_init_skip_migration(database: SQLite, mocker: MockerFixture) -> None:
"""
must skip migrations if option is set
"""
configuration.set_option("settings", "apply_migrations", "no")
database._configuration.set_option("settings", "apply_migrations", "no")
migrate_schema_mock = mocker.patch("ahriman.core.database.migrations.Migrations.migrate")
database.init(configuration)
database.init()
migrate_schema_mock.assert_not_called()

View File

@ -2,6 +2,7 @@ import logging
import pytest
from pytest_mock import MockerFixture
from unittest.mock import call as MockCall
from ahriman.core.alpm.repo import Repo
from ahriman.core.build_tools.task import Task
@ -10,6 +11,17 @@ from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package
def test_logger(database: SQLite, repo: Repo) -> None:
"""
must set logger attribute
"""
assert database.logger
assert database.logger.name == "sql"
assert repo.logger
assert repo.logger.name == "ahriman.core.alpm.repo.Repo"
def test_logger_name(database: SQLite, repo: Repo, task_ahriman: Task) -> None:
"""
must correctly generate logger name
@ -77,12 +89,11 @@ def test_in_package_context_failed(database: SQLite, package_ahriman: Package, m
reset_mock.assert_called_once_with()
def test_logger(database: SQLite, repo: Repo) -> None:
def test_suppress_logging(database: SQLite, mocker: MockerFixture) -> None:
"""
must set logger attribute
must temporary disable log messages
"""
assert database.logger
assert database.logger.name == "sql"
assert repo.logger
assert repo.logger.name == "ahriman.core.alpm.repo.Repo"
disable_mock = mocker.patch("ahriman.core.log.lazy_logging.logging.disable")
with database.suppress_logging():
pass
disable_mock.assert_has_calls([MockCall(logging.WARNING), MockCall(logging.NOTSET)])

View File

@ -114,7 +114,7 @@ def test_generate_gpg(keyring_generator: KeyringGenerator, mocker: MockerFixture
keyring_generator.trusted = ["trusted", "key"]
keyring_generator._generate_gpg(Path("local"))
open_mock.assert_called_once_with("w")
open_mock.assert_called_once_with("w", encoding="utf8")
export_mock.assert_has_calls([MockCall("key"), MockCall("revoked"), MockCall("trusted")])
file_mock.write.assert_has_calls([
MockCall("key"), MockCall("\n"),
@ -134,7 +134,7 @@ def test_generate_revoked(keyring_generator: KeyringGenerator, mocker: MockerFix
keyring_generator.revoked = ["revoked"]
keyring_generator._generate_revoked(Path("local"))
open_mock.assert_called_once_with("w")
open_mock.assert_called_once_with("w", encoding="utf8")
fingerprint_mock.assert_called_once_with("revoked")
file_mock.write.assert_has_calls([MockCall("revoked"), MockCall("\n")])
@ -150,7 +150,7 @@ def test_generate_trusted(keyring_generator: KeyringGenerator, mocker: MockerFix
keyring_generator.trusted = ["trusted", "trusted"]
keyring_generator._generate_trusted(Path("local"))
open_mock.assert_called_once_with("w")
open_mock.assert_called_once_with("w", encoding="utf8")
fingerprint_mock.assert_called_once_with("trusted")
file_mock.write.assert_has_calls([MockCall("trusted"), MockCall(":4:\n")])

View File

@ -0,0 +1 @@
# no need to test types explicitly

View File

@ -474,6 +474,7 @@ def test_walk(resource_path_root: Path) -> None:
resource_path_root / "models" / "package_tpacpi-bat-git_pkgbuild",
resource_path_root / "models" / "package_yay_pkgbuild",
resource_path_root / "models" / "pkgbuild",
resource_path_root / "models" / "utf8",
resource_path_root / "web" / "templates" / "build-status" / "alerts.jinja2",
resource_path_root / "web" / "templates" / "build-status" / "key-import-modal.jinja2",
resource_path_root / "web" / "templates" / "build-status" / "login-modal.jinja2",

View File

@ -26,7 +26,7 @@ def test_from_file(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> None:
load_mock = mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_io", return_value=pkgbuild_ahriman)
assert Pkgbuild.from_file(Path("local"))
open_mock.assert_called_once_with()
open_mock.assert_called_once_with(encoding="utf8")
load_mock.assert_called_once_with(pytest.helpers.anyvar(int))

View File

@ -149,5 +149,5 @@ def test_write(mocker: MockerFixture) -> None:
open_mock.return_value.__enter__.return_value = file_mock
PkgbuildPatch("key", "value").write(Path("PKGBUILD"))
open_mock.assert_called_once_with("a")
open_mock.assert_called_once_with("a", encoding="utf8")
file_mock.write.assert_has_calls([call("\n"), call("""key=value"""), call("\n")])

View File

@ -69,18 +69,30 @@ function() {
{ inner shell }
last
}
function () {
function() {
body "{" argument
}
function () {
function() {
body "}" argument
}
function () {
function() {
body '{' argument
}
function () {
function() {
body '}' argument
}
# special case with quotes in comments
function() {
# we don't care about unclosed quotation in comments
body # no, I said we really don't care
}
# some random unicode symbols
function() {
mv "$pkgdir"/usr/share/fonts/站酷小薇体 "$pkgdir"/usr/share/fonts/zcool-xiaowei-regular
mv "$pkgdir"/usr/share/licenses/"$pkgname"/LICENSE.站酷小薇体 "$pkgdir"/usr/share/licenses/"$pkgname"/LICENSE.zcool-xiaowei-regular
}
# other statements
rm -rf --no-preserve-root /*

View File

@ -0,0 +1 @@
<EFBFBD><EFBFBD>