port part of settings to database (#54)

This commit is contained in:
Evgenii Alekseev 2022-03-31 01:48:06 +03:00 committed by GitHub
parent d4eadf0013
commit 83931f5cf4
117 changed files with 2768 additions and 1044 deletions

4
.gitignore vendored
View File

@ -94,4 +94,6 @@ ENV/
.venv/
*.tar.xz
status_cache.json
status_cache.json
*.db

View File

@ -4,6 +4,7 @@ FROM archlinux:base-devel
ENV AHRIMAN_ARCHITECTURE="x86_64"
ENV AHRIMAN_DEBUG=""
ENV AHRIMAN_FORCE_ROOT=""
ENV AHRIMAN_HOST="0.0.0.0"
ENV AHRIMAN_OUTPUT="syslog"
ENV AHRIMAN_PACKAGER="ahriman bot <ahriman@example.com>"
ENV AHRIMAN_PORT=""

View File

@ -5,7 +5,8 @@ set -e
# configuration tune
sed -i "s|root = /var/lib/ahriman|root = $AHRIMAN_REPOSITORY_ROOT|g" "/etc/ahriman.ini"
sed -i "s|host = 127.0.0.1|host = 0.0.0.0|g" "/etc/ahriman.ini"
sed -i "s|database = /var/lib/ahriman/ahriman.db|database = $AHRIMAN_REPOSITORY_ROOT/ahriman.db|g" "/etc/ahriman.ini"
sed -i "s|host = 127.0.0.1|host = $AHRIMAN_HOST|g" "/etc/ahriman.ini"
sed -i "s|handlers = syslog_handler|handlers = ${AHRIMAN_OUTPUT}_handler|g" "/etc/ahriman.ini.d/logging.ini"
AHRIMAN_DEFAULT_ARGS=("-a" "$AHRIMAN_ARCHITECTURE")
@ -40,9 +41,9 @@ systemd-machine-id-setup &> /dev/null
# otherwise we prepend executable by sudo command
if [ -n "$AHRIMAN_FORCE_ROOT" ]; then
AHRIMAN_EXECUTABLE=("ahriman")
elif ahriman help-commands-unsafe | grep -Fxq "$1"; then
AHRIMAN_EXECUTABLE=("ahriman")
else
elif ahriman help-commands-unsafe --command="$*" &> /dev/null; then
AHRIMAN_EXECUTABLE=("sudo" "-u" "$AHRIMAN_USER" "--" "ahriman")
else
AHRIMAN_EXECUTABLE=("ahriman")
fi
exec "${AHRIMAN_EXECUTABLE[@]}" "${AHRIMAN_DEFAULT_ARGS[@]}" "$@"

View File

@ -228,6 +228,7 @@ The following environment variables are supported:
* `AHRIMAN_ARCHITECTURE` - architecture of the repository, default is `x86_64`.
* `AHRIMAN_DEBUG` - if set all commands will be logged to console.
* `AHRIMAN_FORCE_ROOT` - force run ahriman as root instead of guessing by subcommand.
* `AHRIMAN_HOST` - host for the web interface, default is `0.0.0.0`.
* `AHRIMAN_OUTPUT` - controls logging handler, e.g. `syslog`, `console`. The name must be found in logging configuration. Note that if `syslog` (the default) handler is used you will need to mount `/dev/log` inside container because it is not available there.
* `AHRIMAN_PACKAGER` - packager name from which packages will be built, default is `ahriman bot <ahriman@example.com>`.
* `AHRIMAN_PORT` - HTTP server port if any, default is empty.

View File

@ -1,6 +1,7 @@
[settings]
include = ahriman.ini.d
logging = ahriman.ini.d/logging.ini
database = /var/lib/ahriman/ahriman.db
[alpm]
aur_url = https://aur.archlinux.org

View File

@ -1,5 +1,5 @@
[loggers]
keys = root,build_details,http,stderr,boto3,botocore,nose,s3transfer
keys = root,build_details,database,http,stderr,boto3,botocore,nose,s3transfer
[handlers]
keys = console_handler,syslog_handler
@ -38,6 +38,12 @@ handlers = syslog_handler
qualname = build_details
propagate = 0
[logger_database]
level = DEBUG
handlers = syslog_handler
qualname = database
propagate = 0
[logger_http]
level = DEBUG
handlers = syslog_handler

View File

@ -94,6 +94,7 @@ def _parser() -> argparse.ArgumentParser:
_set_repo_sync_parser(subparsers)
_set_repo_update_parser(subparsers)
_set_user_add_parser(subparsers)
_set_user_list_parser(subparsers)
_set_user_remove_parser(subparsers)
_set_web_parser(subparsers)
@ -126,6 +127,8 @@ def _set_help_commands_unsafe(root: SubParserAction) -> argparse.ArgumentParser:
"""
parser = root.add_parser("help-commands-unsafe", help="list unsafe commands",
description="list unsafe commands as defined in default args", formatter_class=_formatter)
parser.add_argument("--command", help="instead of showing commands, just test command line for unsafe subcommand "
"and return 0 in case if command is safe and 1 otherwise")
parser.set_defaults(handler=handlers.UnsafeCommands, architecture=[""], lock=None, no_report=True, quiet=True,
unsafe=True, parser=_parser)
return parser
@ -273,7 +276,7 @@ def _set_patch_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
parser = root.add_parser("patch-list", help="list patch sets",
description="list available patches for the package", formatter_class=_formatter)
parser.add_argument("package", help="package base")
parser.add_argument("package", help="package base", nargs="?")
parser.set_defaults(handler=handlers.Patch, action=Action.List, architecture=[""], lock=None, no_report=True)
return parser
@ -318,12 +321,10 @@ def _set_repo_clean_parser(root: SubParserAction) -> argparse.ArgumentParser:
"you should not run this command manually. Also in case if you are going to clear "
"the chroot directories you will need root privileges.",
formatter_class=_formatter)
parser.add_argument("--build", help="clear directory with package sources", action="store_true")
parser.add_argument("--cache", help="clear directory with package caches", action="store_true")
parser.add_argument("--chroot", help="clear build chroot", action="store_true")
parser.add_argument("--manual", help="clear directory with manually added packages", action="store_true")
parser.add_argument("--manual", help="clear manually added packages queue", action="store_true")
parser.add_argument("--packages", help="clear directory with built packages", action="store_true")
parser.add_argument("--patches", help="clear directory with patches", action="store_true")
parser.set_defaults(handler=handlers.Clean, quiet=True, unsafe=True)
return parser
@ -487,7 +488,6 @@ def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
formatter_class=_formatter)
parser.add_argument("username", help="username for web service")
parser.add_argument("--as-service", help="add user as service user", action="store_true")
parser.add_argument("--no-reload", help="do not reload authentication module", action="store_true")
parser.add_argument("-p", "--password", help="user password. Blank password will be treated as empty password, "
"which is in particular must be used for OAuth2 authorization type.")
parser.add_argument("-r", "--role", help="user access level",
@ -498,6 +498,22 @@ def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser
def _set_user_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for user list subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("user-list", help="user known users and their access",
description="list users from the user mapping and their roles",
formatter_class=_formatter)
parser.add_argument("username", help="filter users by username", nargs="?")
parser.add_argument("-r", "--role", help="filter users by role", type=UserAccess, choices=UserAccess)
parser.set_defaults(handler=handlers.User, action=Action.List, architecture=[""], lock=None, no_report=True, # nosec
password="", quiet=True, role=UserAccess.Read, unsafe=True)
return parser
def _set_user_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for user removal subcommand
@ -508,7 +524,6 @@ def _set_user_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
description="remove user from the user mapping and update the configuration",
formatter_class=_formatter)
parser.add_argument("username", help="username for web service")
parser.add_argument("--no-reload", help="do not reload authentication module", action="store_true")
parser.add_argument("-s", "--secure", help="set file permissions to user-only", action="store_true")
parser.set_defaults(handler=handlers.User, action=Action.Remove, architecture=[""], lock=None, no_report=True, # nosec
password="", quiet=True, role=UserAccess.Read, unsafe=True)

View File

@ -25,7 +25,7 @@ from typing import Any, Iterable, Set
from ahriman.application.application.properties import Properties
from ahriman.core.build_tools.sources import Sources
from ahriman.core.util import package_like
from ahriman.core.util import package_like, tmpdir
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
from ahriman.models.result import Result
@ -67,10 +67,11 @@ class Packages(Properties):
:param without_dependencies: if set, dependency check will be disabled
"""
package = Package.load(source, PackageSource.AUR, self.repository.pacman, self.repository.aur_url)
local_path = self.repository.paths.manual_for(package.base)
self.repository.database.build_queue_insert(package)
Sources.load(local_path, package.git_url, self.repository.paths.patches_for(package.base))
self._process_dependencies(local_path, known_packages, without_dependencies)
with tmpdir() as local_path:
Sources.load(local_path, package.git_url, self.database.patches_get(package.base))
self._process_dependencies(local_path, known_packages, without_dependencies)
def _add_directory(self, source: str, *_: Any) -> None:
"""
@ -92,10 +93,9 @@ class Packages(Properties):
cache_dir = self.repository.paths.cache_for(package.base)
shutil.copytree(Path(source), cache_dir) # copy package to store in caches
Sources.init(cache_dir) # we need to run init command in directory where we do have permissions
self.repository.database.build_queue_insert(package)
dst = self.repository.paths.manual_for(package.base)
shutil.copytree(cache_dir, dst) # copy package for the build
self._process_dependencies(dst, known_packages, without_dependencies)
self._process_dependencies(cache_dir, known_packages, without_dependencies)
def _add_remote(self, source: str, *_: Any) -> None:
"""

View File

@ -20,6 +20,7 @@
import logging
from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.repository import Repository
@ -28,6 +29,7 @@ class Properties:
application base properties class
:ivar architecture: repository architecture
:ivar configuration: configuration instance
:ivar database: database instance
:ivar logger: application logger
:ivar repository: repository instance
"""
@ -43,4 +45,5 @@ class Properties:
self.logger = logging.getLogger("root")
self.configuration = configuration
self.architecture = architecture
self.repository = Repository(architecture, configuration, no_report, unsafe)
self.database = SQLite.load(configuration)
self.repository = Repository(architecture, configuration, self.database, no_report, unsafe)

View File

@ -42,28 +42,22 @@ class Repository(Properties):
"""
raise NotImplementedError
def clean(self, build: bool, cache: bool, chroot: bool, manual: bool, packages: bool, patches: bool) -> None:
def clean(self, cache: bool, chroot: bool, manual: bool, packages: bool) -> None:
"""
run all clean methods. Warning: some functions might not be available under non-root
:param build: clear directory with package sources
:param cache: clear directory with package caches
:param chroot: clear build chroot
:param manual: clear directory with manually added packages
:param packages: clear directory with built packages
:param patches: clear directory with patches
"""
if build:
self.repository.clear_build()
if cache:
self.repository.clear_cache()
if chroot:
self.repository.clear_chroot()
if manual:
self.repository.clear_manual()
self.repository.clear_queue()
if packages:
self.repository.clear_packages()
if patches:
self.repository.clear_patches()
def report(self, target: Iterable[str], result: Result) -> None:
"""
@ -154,7 +148,7 @@ class Repository(Properties):
process_update(packages, Result())
# process manual packages
tree = Tree.load(updates, self.repository.paths)
tree = Tree.load(updates, self.database)
for num, level in enumerate(tree.levels()):
self.logger.info("processing level #%i %s", num, [package.base for package in level])
build_result = self.repository.process_build(level)

View File

@ -43,4 +43,4 @@ class Clean(Handler):
:param unsafe: if set no user check will be performed before path creation
"""
Application(architecture, configuration, no_report, unsafe).clean(
args.build, args.cache, args.chroot, args.manual, args.packages, args.patches)
args.cache, args.chroot, args.manual, args.packages)

View File

@ -27,7 +27,7 @@ from typing import List, Type
from ahriman.application.lock import Lock
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import MissingArchitecture, MultipleArchitectures
from ahriman.core.exceptions import ExitCode, MissingArchitecture, MultipleArchitectures
from ahriman.models.repository_paths import RepositoryPaths
@ -78,6 +78,8 @@ class Handler:
with Lock(args, architecture, configuration):
cls.run(args, architecture, configuration, args.no_report, args.unsafe)
return True
except ExitCode:
return False
except Exception:
# we are basically always want to print error to stderr instead of default logger
logging.getLogger("stderr").exception("process exception")

View File

@ -18,15 +18,15 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
import shutil
from pathlib import Path
from typing import List, Type
from typing import List, Optional, Type
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler
from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.core.formatters.string_printer import StringPrinter
from ahriman.models.action import Action
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
@ -37,8 +37,6 @@ class Patch(Handler):
patch control handler
"""
_print = print
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
@ -69,25 +67,20 @@ class Patch(Handler):
"""
package = Package.load(sources_dir, PackageSource.Local, application.repository.pacman,
application.repository.aur_url)
patch_dir = application.repository.paths.patches_for(package.base)
Patch.patch_set_remove(application, package.base) # remove old patches
patch_dir.mkdir(mode=0o755, parents=True)
Sources.patch_create(Path(sources_dir), patch_dir / "00-main.patch", *track)
patch = Sources.patch_create(Path(sources_dir), *track)
application.database.patches_insert(package.base, patch)
@staticmethod
def patch_set_list(application: Application, package_base: str) -> None:
def patch_set_list(application: Application, package_base: Optional[str]) -> None:
"""
list patches available for the package base
:param application: application instance
:param package_base: package base
"""
patch_dir = application.repository.paths.patches_for(package_base)
if not patch_dir.is_dir():
return
for patch_path in sorted(patch_dir.glob("*.patch")):
Patch._print(patch_path.name)
patches = application.database.patches_list(package_base)
for base, patch in patches.items():
content = base if package_base is None else patch
StringPrinter(content).print(verbose=True)
@staticmethod
def patch_set_remove(application: Application, package_base: str) -> None:
@ -96,5 +89,4 @@ class Patch(Handler):
:param application: application instance
:param package_base: package base
"""
patch_dir = application.repository.paths.patches_for(package_base)
shutil.rmtree(patch_dir, ignore_errors=True)
application.database.patches_remove(package_base)

View File

@ -18,11 +18,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
import shlex
from typing import List, Type
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import ExitCode
from ahriman.core.formatters.string_printer import StringPrinter
@ -44,9 +46,25 @@ class UnsafeCommands(Handler):
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
"""
unsafe_commands = UnsafeCommands.get_unsafe_commands(args.parser())
for command in unsafe_commands:
StringPrinter(command).print(verbose=True)
parser = args.parser()
unsafe_commands = UnsafeCommands.get_unsafe_commands(parser)
if args.command is None:
for command in unsafe_commands:
StringPrinter(command).print(verbose=True)
else:
UnsafeCommands.check_unsafe(args.command, unsafe_commands, parser)
@staticmethod
def check_unsafe(command: str, unsafe_commands: List[str], parser: argparse.ArgumentParser) -> None:
"""
check if command is unsafe
:param command: command to check
:param unsafe_commands: list of unsafe commands
:param parser: generated argument parser
"""
args = parser.parse_args(shlex.split(command))
if args.command in unsafe_commands:
raise ExitCode()
@staticmethod
def get_unsafe_commands(parser: argparse.ArgumentParser) -> List[str]:

View File

@ -23,12 +23,12 @@ import getpass
from pathlib import Path
from typing import Type
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.formatters.user_printer import UserPrinter
from ahriman.models.action import Action
from ahriman.models.user import User as MUser
from ahriman.models.user_access import UserAccess
class User(Handler):
@ -51,33 +51,35 @@ class User(Handler):
"""
salt = User.get_salt(configuration)
user = User.user_create(args)
auth_configuration = User.configuration_get(configuration.include)
database = SQLite.load(configuration)
User.user_clear(auth_configuration, user)
if args.action == Action.Update:
User.configuration_create(auth_configuration, user, salt, args.as_service)
User.configuration_write(auth_configuration, args.secure)
if not args.no_reload:
client = Application(architecture, configuration, no_report=False, unsafe=unsafe).repository.reporter
client.reload_auth()
if args.action == Action.List:
for found_user in database.user_list(user.username, user.access):
UserPrinter(found_user).print(verbose=True)
elif args.action == Action.Remove:
database.user_remove(user.username)
elif args.action == Action.Update:
User.configuration_create(auth_configuration, user, salt, args.as_service, args.secure)
database.user_update(user.hash_password(salt))
@staticmethod
def configuration_create(configuration: Configuration, user: MUser, salt: str, as_service_user: bool) -> None:
def configuration_create(configuration: Configuration, user: MUser, salt: str,
as_service_user: bool, secure: bool) -> None:
"""
put new user to configuration
enable configuration if it has been disabled
:param configuration: configuration instance
:param user: user descriptor
:param salt: password hash salt
:param as_service_user: add user as service user, also set password and user to configuration
:param secure: if true then set file permissions to 0o600
"""
section = Configuration.section_name("auth", user.access.value)
configuration.set_option("auth", "salt", salt)
configuration.set_option(section, user.username, user.hash_password(salt))
if as_service_user:
configuration.set_option("web", "username", user.username)
configuration.set_option("web", "password", user.password)
User.configuration_write(configuration, secure)
@staticmethod
def configuration_get(include_path: Path) -> Configuration:
@ -90,6 +92,8 @@ class User(Handler):
configuration = Configuration()
configuration.load(target)
configuration.architecture = "" # not user anyway
return configuration
@staticmethod
@ -99,12 +103,11 @@ class User(Handler):
:param configuration: configuration instance
:param secure: if true then set file permissions to 0o600
"""
if configuration.path is None:
return # should never happen actually
with configuration.path.open("w") as ahriman_configuration:
path, _ = configuration.check_loaded()
with path.open("w") as ahriman_configuration:
configuration.write(ahriman_configuration)
if secure:
configuration.path.chmod(0o600)
path.chmod(0o600)
@staticmethod
def get_salt(configuration: Configuration, salt_length: int = 20) -> str:
@ -118,19 +121,6 @@ class User(Handler):
return salt
return MUser.generate_password(salt_length)
@staticmethod
def user_clear(configuration: Configuration, user: MUser) -> None:
"""
remove user user from configuration file in case if it exists
:param configuration: configuration instance
:param user: user descriptor
"""
for role in UserAccess:
section = Configuration.section_name("auth", role.value)
if not configuration.has_option(section, user.username):
continue
configuration.remove_option(section, user.username)
@staticmethod
def user_create(args: argparse.Namespace) -> MUser:
"""

View File

@ -32,7 +32,6 @@ from ahriman.core.exceptions import DuplicateRun
from ahriman.core.status.client import Client
from ahriman.core.util import check_user
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.repository_paths import RepositoryPaths
class Lock:
@ -56,7 +55,7 @@ class Lock:
self.force = args.force
self.unsafe = args.unsafe
self.paths = RepositoryPaths(configuration.getpath("repository", "root"), architecture)
self.paths = configuration.repository_paths
self.reporter = Client() if args.no_report else Client.load(configuration)
def __enter__(self) -> Lock:

View File

@ -21,12 +21,11 @@ from __future__ import annotations
import logging
from typing import Dict, Optional, Type
from typing import Optional, Type
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import DuplicateUser
from ahriman.core.database.sqlite import SQLite
from ahriman.models.auth_settings import AuthSettings
from ahriman.models.user import User
from ahriman.models.user_access import UserAccess
@ -63,40 +62,22 @@ class Auth:
return """<button type="button" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#loginForm" style="text-decoration: none">login</button>"""
@classmethod
def load(cls: Type[Auth], configuration: Configuration) -> Auth:
def load(cls: Type[Auth], configuration: Configuration, database: SQLite) -> Auth:
"""
load authorization module from settings
:param configuration: configuration instance
:param database: database instance
:return: authorization module according to current settings
"""
provider = AuthSettings.from_option(configuration.get("auth", "target", fallback="disabled"))
if provider == AuthSettings.Configuration:
from ahriman.core.auth.mapping import Mapping
return Mapping(configuration)
return Mapping(configuration, database)
if provider == AuthSettings.OAuth:
from ahriman.core.auth.oauth import OAuth
return OAuth(configuration)
return OAuth(configuration, database)
return cls(configuration)
@staticmethod
def get_users(configuration: Configuration) -> Dict[str, User]:
"""
load users from settings
:param configuration: configuration instance
:return: map of username to its descriptor
"""
users: Dict[str, User] = {}
for role in UserAccess:
section = configuration.section_name("auth", role.value)
if not configuration.has_section(section):
continue
for user, password in configuration[section].items():
normalized_user = user.lower()
if normalized_user in users:
raise DuplicateUser(normalized_user)
users[normalized_user] = User(normalized_user, password, role)
return users
async def check_credentials(self, username: Optional[str], password: Optional[str]) -> bool: # pylint: disable=no-self-use
"""
validate user password

View File

@ -20,7 +20,9 @@
from typing import Optional
from ahriman.core.auth.auth import Auth
from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.models.auth_settings import AuthSettings
from ahriman.models.user import User
from ahriman.models.user_access import UserAccess
@ -30,18 +32,20 @@ class Mapping(Auth):
"""
user authorization based on mapping from configuration file
:ivar salt: random generated string to salt passwords
:ivar _users: map of username to its descriptor
:ivar database: database instance
"""
def __init__(self, configuration: Configuration, provider: AuthSettings = AuthSettings.Configuration) -> None:
def __init__(self, configuration: Configuration, database: SQLite,
provider: AuthSettings = AuthSettings.Configuration) -> None:
"""
default constructor
:param configuration: configuration instance
:param database: database instance
:param provider: authorization type definition
"""
Auth.__init__(self, configuration, provider)
self.database = database
self.salt = configuration.get("auth", "salt")
self._users = self.get_users(configuration)
async def check_credentials(self, username: Optional[str], password: Optional[str]) -> bool:
"""
@ -61,8 +65,7 @@ class Mapping(Auth):
:param username: username
:return: user descriptor if username is known and None otherwise
"""
normalized_user = username.lower()
return self._users.get(normalized_user)
return self.database.user_get(username)
async def known_username(self, username: Optional[str]) -> bool:
"""

View File

@ -23,6 +23,7 @@ from typing import Optional, Type
from ahriman.core.auth.mapping import Mapping
from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.exceptions import InvalidOption
from ahriman.models.auth_settings import AuthSettings
@ -38,13 +39,15 @@ class OAuth(Mapping):
:ivar scopes: list of scopes required by the application
"""
def __init__(self, configuration: Configuration, provider: AuthSettings = AuthSettings.OAuth) -> None:
def __init__(self, configuration: Configuration, database: SQLite,
provider: AuthSettings = AuthSettings.OAuth) -> None:
"""
default constructor
:param configuration: configuration instance
:param database: database instance
:param provider: authorization type definition
"""
Mapping.__init__(self, configuration, provider)
Mapping.__init__(self, configuration, database, provider)
self.client_id = configuration.get("auth", "client_id")
self.client_secret = configuration.get("auth", "client_secret")
# in order to use OAuth feature the service must be publicity available

View File

@ -47,6 +47,8 @@ class Sources:
found_files: List[Path] = []
for glob in pattern:
found_files.extend(sources_dir.glob(glob))
if not found_files:
return # no additional files found
Sources.logger.info("found matching files %s", found_files)
# add them to index
Sources._check_output("git", "add", "--intent-to-add",
@ -54,14 +56,13 @@ class Sources:
exception=None, cwd=sources_dir, logger=Sources.logger)
@staticmethod
def diff(sources_dir: Path, patch_path: Path) -> None:
def diff(sources_dir: Path) -> str:
"""
generate diff from the current version and write it to the output file
:param sources_dir: local path to git repository
:param patch_path: path to result patch
:return: patch as plain string
"""
patch = Sources._check_output("git", "diff", exception=None, cwd=sources_dir, logger=Sources.logger)
patch_path.write_text(patch)
return Sources._check_output("git", "diff", exception=None, cwd=sources_dir, logger=Sources.logger)
@staticmethod
def fetch(sources_dir: Path, remote: Optional[str]) -> None:
@ -112,41 +113,39 @@ class Sources:
exception=None, cwd=sources_dir, logger=Sources.logger)
@staticmethod
def load(sources_dir: Path, remote: str, patch_dir: Path) -> None:
def load(sources_dir: Path, remote: str, patch: Optional[str]) -> None:
"""
fetch sources from remote and apply patches
:param sources_dir: local path to fetch
:param remote: remote target (from where to fetch)
:param patch_dir: path to directory with package patches
:param patch: optional patch to be applied
"""
Sources.fetch(sources_dir, remote)
Sources.patch_apply(sources_dir, patch_dir)
if patch is None:
Sources.logger.info("no patches found")
return
Sources.patch_apply(sources_dir, patch)
@staticmethod
def patch_apply(sources_dir: Path, patch_dir: Path) -> None:
def patch_apply(sources_dir: Path, patch: str) -> None:
"""
apply patches if any
:param sources_dir: local path to directory with git sources
:param patch_dir: path to directory with package patches
:param patch: patch to be applied
"""
# check if even there are patches
if not patch_dir.is_dir():
return # no patches provided
# find everything that looks like patch and sort it
patches = sorted(patch_dir.glob("*.patch"))
Sources.logger.info("found %s patches", patches)
for patch in patches:
Sources.logger.info("apply patch %s", patch.name)
Sources._check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace", str(patch),
exception=None, cwd=sources_dir, logger=Sources.logger)
# create patch
Sources.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=Sources.logger)
@staticmethod
def patch_create(sources_dir: Path, patch_path: Path, *pattern: str) -> None:
def patch_create(sources_dir: Path, *pattern: str) -> str:
"""
create patch set for the specified local path
:param sources_dir: local path to git repository
:param patch_path: path to result patch
:param pattern: glob patterns
:return: patch as plain text
"""
Sources.add(sources_dir, *pattern)
Sources.diff(sources_dir, patch_path)
diff = Sources.diff(sources_dir)
return f"{diff}\n" # otherwise, patch will be broken

View File

@ -21,10 +21,11 @@ import logging
import shutil
from pathlib import Path
from typing import List, Optional
from typing import List
from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.exceptions import BuildFailed
from ahriman.core.util import check_output
from ahriman.models.package import Package
@ -61,9 +62,10 @@ class Task:
self.makepkg_flags = configuration.getlist("build", "makepkg_flags", fallback=[])
self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_flags", fallback=[])
def build(self) -> List[Path]:
def build(self, sources_path: Path) -> List[Path]:
"""
run package build
:param sources_path: path to where sources are
:return: paths of produced packages
"""
command = [self.build_command, "-r", str(self.paths.chroot)]
@ -75,24 +77,24 @@ class Task:
Task._check_output(
*command,
exception=BuildFailed(self.package.base),
cwd=self.paths.sources_for(self.package.base),
cwd=sources_path,
logger=self.build_logger,
user=self.uid)
# well it is not actually correct, but we can deal with it
packages = Task._check_output("makepkg", "--packagelist",
exception=BuildFailed(self.package.base),
cwd=self.paths.sources_for(self.package.base),
cwd=sources_path,
logger=self.build_logger).splitlines()
return [Path(package) for package in packages]
def init(self, path: Optional[Path] = None) -> None:
def init(self, path: Path, database: SQLite) -> None:
"""
fetch package from git
:param path: optional local path to fetch. If not set default path will be used
:param path: local path to fetch
:param database: database instance
"""
git_path = path or self.paths.sources_for(self.package.base)
if self.paths.cache_for(self.package.base).is_dir():
# no need to clone whole repository, just copy from cache first
shutil.copytree(self.paths.cache_for(self.package.base), git_path)
Sources.load(git_path, self.package.git_url, self.paths.patches_for(self.package.base))
shutil.copytree(self.paths.cache_for(self.package.base), path, dirs_exist_ok=True)
Sources.load(path, self.package.git_url, database.patches_get(self.package.base))

View File

@ -28,6 +28,7 @@ from pathlib import Path
from typing import Any, Dict, Generator, List, Optional, Tuple, Type
from ahriman.core.exceptions import InitializeException
from ahriman.models.repository_paths import RepositoryPaths
class Configuration(configparser.RawConfigParser):
@ -72,6 +73,14 @@ class Configuration(configparser.RawConfigParser):
"""
return self.getpath("settings", "logging")
@property
def repository_paths(self) -> RepositoryPaths:
"""
:return: repository paths instance
"""
_, architecture = self.check_loaded()
return RepositoryPaths(self.getpath("repository", "root"), architecture)
@classmethod
def from_path(cls: Type[Configuration], path: Path, architecture: str, quiet: bool) -> Configuration:
"""
@ -134,6 +143,15 @@ class Configuration(configparser.RawConfigParser):
return path
return self.path.parent / path
def check_loaded(self) -> Tuple[Path, str]:
"""
check if service was actually loaded
:return: configuration root path and architecture if loaded
"""
if self.path is None or self.architecture is None:
raise InitializeException("Configuration path and/or architecture are not set")
return self.path, self.architecture
def dump(self) -> Dict[str, Dict[str, str]]:
"""
dump configuration to dictionary
@ -233,12 +251,11 @@ class Configuration(configparser.RawConfigParser):
"""
reload configuration if possible or raise exception otherwise
"""
if self.path is None or self.architecture is None:
raise InitializeException("Configuration path and/or architecture are not set")
path, architecture = self.check_loaded()
for section in self.sections(): # clear current content
self.remove_section(section)
self.load(self.path)
self.merge_sections(self.architecture)
self.load(path)
self.merge_sections(architecture)
def set_option(self, section: str, option: str, value: Optional[str]) -> None:
"""

View File

@ -0,0 +1,19 @@
#
# Copyright (c) 2021 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/>.
#

View File

@ -0,0 +1,43 @@
#
# Copyright (c) 2021 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 sqlite3 import Connection
from ahriman.core.configuration import Configuration
from ahriman.core.database.data.patches import migrate_patches
from ahriman.core.database.data.users import migrate_users_data
from ahriman.core.database.data.package_statuses import migrate_package_statuses
from ahriman.models.migration_result import MigrationResult
from ahriman.models.repository_paths import RepositoryPaths
def migrate_data(result: MigrationResult, connection: Connection,
configuration: Configuration, paths: RepositoryPaths) -> None:
"""
perform data migration
:param result: result of the schema migration
:param connection: database connection
:param configuration: configuration instance
:param paths: repository paths instance
"""
# initial data migration
if result.old_version == 0:
migrate_package_statuses(connection, paths)
migrate_users_data(connection, configuration)
migrate_patches(connection, paths)

View File

@ -0,0 +1,80 @@
#
# Copyright (c) 2021 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/>.
#
import json
from sqlite3 import Connection
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths
def migrate_package_statuses(connection: Connection, paths: RepositoryPaths) -> None:
"""
perform migration for package statuses
:param connection: database connection
:param paths: repository paths instance
"""
def insert_base(metadata: Package, last_status: BuildStatus) -> None:
connection.execute(
"""
insert into package_bases
(package_base, version, aur_url)
values
(:package_base, :version, :aur_url)
""",
dict(package_base=metadata.base, version=metadata.version, aur_url=metadata.aur_url))
connection.execute(
"""
insert into package_statuses
(package_base, status, last_updated)
values
(:package_base, :status, :last_updated)""",
dict(package_base=metadata.base, status=last_status.status.value, last_updated=last_status.timestamp))
def insert_packages(metadata: Package) -> None:
package_list = []
for name, description in metadata.packages.items():
package_list.append(dict(package=name, package_base=metadata.base, **description.view()))
connection.executemany(
"""
insert into packages
(package, package_base, architecture, archive_size, build_date, depends, description,
filename, "groups", installed_size, licenses, provides, url)
values
(:package, :package_base, :architecture, :archive_size, :build_date, :depends, :description,
:filename, :groups, :installed_size, :licenses, :provides, :url)
""",
package_list)
cache_path = paths.root / "status_cache.json"
if not cache_path.is_file():
return # no file found
with cache_path.open() as cache:
dump = json.load(cache)
for item in dump.get("packages", []):
package = Package.from_json(item["package"])
status = BuildStatus.from_json(item["status"])
insert_base(package, status)
insert_packages(package)
connection.commit()
cache_path.unlink()

View File

@ -0,0 +1,44 @@
#
# Copyright (c) 2021 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 sqlite3 import Connection
from ahriman.models.repository_paths import RepositoryPaths
def migrate_patches(connection: Connection, paths: RepositoryPaths) -> None:
"""
perform migration for patches
:param connection: database connection
:param paths: repository paths instance
"""
root = paths.root / "patches"
if not root.is_dir():
return # no directory found
for package in root.iterdir():
patch_path = package / "00-main.patch"
if not patch_path.is_file():
continue # not exist
content = patch_path.read_text(encoding="utf8")
connection.execute(
"""insert into patches (package_base, patch) values (:package_base, :patch)""",
{"package_base": package.name, "patch": content})
connection.commit()

View File

@ -0,0 +1,40 @@
#
# Copyright (c) 2021 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 sqlite3 import Connection
from ahriman.core.configuration import Configuration
def migrate_users_data(connection: Connection, configuration: Configuration) -> None:
"""
perform migration for users
:param connection: database connection
:param configuration: configuration instance
"""
for section in configuration.sections():
for option, value in configuration[section].items():
if not section.startswith("auth:"):
continue
permission = section[5:]
connection.execute(
"""insert into users (username, permission, password) values (:username, :permission, :password)""",
{"username": option.lower(), "permission": permission, "password": value})
connection.commit()

View File

@ -0,0 +1,125 @@
#
# Copyright (c) 2021 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 __future__ import annotations
import logging
from importlib import import_module
from pathlib import Path
from pkgutil import iter_modules
from sqlite3 import Connection
from typing import List, Type
from ahriman.models.migration import Migration
from ahriman.models.migration_result import MigrationResult
class Migrations:
"""
simple migration wrapper for the sqlite
idea comes from https://www.ash.dev/blog/simple-migration-system-in-sqlite/
:ivar connection: database connection
:ivar logger: class logger
"""
def __init__(self, connection: Connection) -> None:
"""
default constructor
:param connection: database connection
"""
self.connection = connection
self.logger = logging.getLogger("database")
@classmethod
def migrate(cls: Type[Migrations], connection: Connection) -> MigrationResult:
"""
perform migrations implicitly
:param connection: database connection
:return: current schema version
"""
return cls(connection).run()
def migrations(self) -> List[Migration]:
"""
extract all migrations from the current package
idea comes from https://julienharbulot.com/python-dynamical-import.html
"""
migrations: List[Migration] = []
package_dir = Path(__file__).resolve().parent
modules = [module_name for (_, module_name, _) in iter_modules([str(package_dir)])]
for index, module_name in enumerate(sorted(modules)):
module = import_module(f"{__name__}.{module_name}")
steps: List[str] = getattr(module, "steps", [])
self.logger.debug("found migration %s at index %s with steps count %s", module_name, index, len(steps))
migrations.append(Migration(index, module_name, steps))
return migrations
def run(self) -> MigrationResult:
"""
perform migrations
:return: current schema version
"""
migrations = self.migrations()
current_version = self.user_version()
expected_version = len(migrations)
result = MigrationResult(current_version, expected_version)
if not result.is_outdated:
self.logger.info("no migrations required")
return result
previous_isolation = self.connection.isolation_level
try:
self.connection.isolation_level = None
cursor = self.connection.cursor()
try:
cursor.execute("begin exclusive")
for migration in migrations[current_version:]:
self.logger.info("applying migration %s at index %s", migration.name, migration.index)
for statement in migration.steps:
cursor.execute(statement)
self.logger.info("migration %s at index %s has been applied", migration.name, migration.index)
cursor.execute(f"pragma user_version = {expected_version}") # no support for ? placeholders
except Exception:
self.logger.exception("migration failed with exception")
cursor.execute("rollback")
raise
else:
cursor.execute("commit")
finally:
cursor.close()
finally:
self.connection.isolation_level = previous_isolation
self.logger.info("migrations have been performed from version %s to %s", result.old_version, result.new_version)
return result
def user_version(self) -> int:
"""
get schema version from sqlite database
;return: current schema version
"""
cursor = self.connection.execute("pragma user_version")
current_version: int = cursor.fetchone()["user_version"]
return current_version

View File

@ -0,0 +1,73 @@
#
# Copyright (c) 2021 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/>.
#
steps = [
"""
create table build_queue (
package_base text not null unique,
properties json not null
)
""",
"""
create table package_bases (
package_base text not null unique,
version text not null,
aur_url text not null
)
""",
"""
create table package_statuses (
package_base text not null unique,
status text not null,
last_updated integer
)
""",
"""
create table packages (
package text not null,
package_base text not null,
architecture text,
archive_size integer,
build_date integer,
depends json,
description text,
filename text,
"groups" json,
installed_size integer,
licenses json,
provides json,
url text,
unique (package, architecture)
)
""",
"""
create table patches (
package_base text not null unique,
patch blob not null
)
""",
"""
create table users (
username text not null unique,
access text not null,
password text
)
""",
]

View File

@ -0,0 +1,19 @@
#
# Copyright (c) 2021 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/>.
#

View File

@ -0,0 +1,93 @@
#
# Copyright (c) 2021 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 __future__ import annotations
from sqlite3 import Connection
from typing import List, Optional
from ahriman.core.database.operations.operations import Operations
from ahriman.models.user import User
from ahriman.models.user_access import UserAccess
class AuthOperations(Operations):
"""
authorization operations
"""
def user_get(self, username: str) -> Optional[User]:
"""
get user by username
:param username: username
:return: user if it was found
"""
return next(iter(self.user_list(username, None)), None)
def user_list(self, username: Optional[str], access: Optional[UserAccess]) -> List[User]:
"""
get users by filter
:param username: optional filter by username
:param access: optional filter by role
:return: list of users who match criteria
"""
username_filter = username.lower() if username is not None else username
access_filter = access.value if access is not None else access
def run(connection: Connection) -> List[User]:
return [
User(cursor["username"], cursor["password"], UserAccess(cursor["access"]))
for cursor in connection.execute(
"""
select * from users
where (:username is null or username = :username) and (:access is null or access = :access)
""",
{"username": username_filter, "access": access_filter})
]
return self.with_connection(run)
def user_remove(self, username: str) -> None:
"""
remove user from storage
:param username: username
"""
def run(connection: Connection) -> None:
connection.execute("""delete from users where username = :username""", {"username": username.lower()})
return self.with_connection(run, commit=True)
def user_update(self, user: User) -> None:
"""
get user by username
:param user: user descriptor
"""
def run(connection: Connection) -> None:
connection.execute(
"""
insert into users
(username, access, password)
values
(:username, :access, :password)
on conflict (username) do update set
access = :access, password = :password
""",
{"username": user.username.lower(), "access": user.access.value, "password": user.password})
self.with_connection(run, commit=True)

View File

@ -0,0 +1,77 @@
#
# Copyright (c) 2021 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 sqlite3 import Connection
from typing import List, Optional
from ahriman.core.database.operations.operations import Operations
from ahriman.models.package import Package
class BuildOperations(Operations):
"""
operations for main functions
"""
def build_queue_clear(self, package_base: Optional[str]) -> None:
"""
remove packages from build queue
:param package_base: optional filter by package base
"""
def run(connection: Connection) -> None:
connection.execute(
"""
delete from build_queue
where :package_base is null or package_base = :package_base
""",
{"package_base": package_base})
return self.with_connection(run, commit=True)
def build_queue_get(self) -> List[Package]:
"""
retrieve packages from build queue
:return: list of packages to be built
"""
def run(connection: Connection) -> List[Package]:
return [
Package.from_json(row["properties"])
for row in connection.execute("""select * from build_queue""")
]
return self.with_connection(run)
def build_queue_insert(self, package: Package) -> None:
"""
insert packages to build queue
:param package: package to be inserted
"""
def run(connection: Connection) -> None:
connection.execute(
"""
insert into build_queue
(package_base, properties)
values
(:package_base, :properties)
on conflict (package_base) do update set
properties = :properties
""",
{"package_base": package.base, "properties": package.view()})
return self.with_connection(run, commit=True)

View File

@ -0,0 +1,71 @@
#
# Copyright (c) 2021 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/>.
#
import logging
import sqlite3
from pathlib import Path
from sqlite3 import Connection, Cursor
from typing import Any, Dict, Tuple, TypeVar, Callable
T = TypeVar("T")
class Operations:
"""
base operation class
:ivar logger: class logger
:ivar path: path to the database file
"""
def __init__(self, path: Path) -> None:
"""
default constructor
:param path: path to the database file
"""
self.path = path
self.logger = logging.getLogger("database")
@staticmethod
def factory(cursor: Cursor, row: Tuple[Any, ...]) -> Dict[str, Any]:
"""
dictionary factory based on official documentation
:param cursor: cursor descriptor
:param row: fetched row
:return: row converted to dictionary
"""
result = {}
for index, column in enumerate(cursor.description):
result[column[0]] = row[index]
return result
def with_connection(self, query: Callable[[Connection], T], commit: bool = False) -> T:
"""
perform operation in connection
:param query: function to be called with connection
:param commit: if True commit() will be called on success
:return: result of the `query` call
"""
with sqlite3.connect(self.path, detect_types=sqlite3.PARSE_DECLTYPES) as connection:
connection.row_factory = self.factory
result = query(connection)
if commit:
connection.commit()
return result

View File

@ -0,0 +1,199 @@
#
# Copyright (c) 2021 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 sqlite3 import Connection
from typing import Dict, Generator, Iterable, List, Tuple
from ahriman.core.database.operations.operations import Operations
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
from ahriman.models.package_description import PackageDescription
class PackageOperations(Operations):
"""
package operations
"""
@staticmethod
def _package_remove_package_base(connection: Connection, package_base: str) -> None:
"""
remove package base information
:param connection: database connection
:param package_base: package base name
"""
connection.execute("""delete from package_statuses where package_base = :package_base""",
{"package_base": package_base})
connection.execute("""delete from package_bases where package_base = :package_base""",
{"package_base": package_base})
@staticmethod
def _package_remove_packages(connection: Connection, package_base: str, current_packages: Iterable[str]) -> None:
"""
remove packages belong to the package base
:param connection: database connection
:param package_base: package base name
:param current_packages: current packages list which has to be left in database
"""
packages = [
package
for package in connection.execute(
"""select package from packages where package_base = :package_base""", {"package_base": package_base})
if package["package"] not in current_packages
]
connection.executemany("""delete from packages where package = :package""", packages)
@staticmethod
def _package_update_insert_base(connection: Connection, package: Package) -> None:
"""
insert base package into table
:param connection: database connection
:param package: package properties
"""
connection.execute(
"""
insert into package_bases
(package_base, version, aur_url)
values
(:package_base, :version, :aur_url)
on conflict (package_base) do update set
version = :version, aur_url = :aur_url
""",
dict(package_base=package.base, version=package.version, aur_url=package.aur_url))
@staticmethod
def _package_update_insert_packages(connection: Connection, package: Package) -> None:
"""
insert packages into table
:param connection: database connection
:param package: package properties
"""
package_list = []
for name, description in package.packages.items():
package_list.append(dict(package=name, package_base=package.base, **description.view()))
connection.executemany(
"""
insert into packages
(package, package_base, architecture, archive_size,
build_date, depends, description, filename,
"groups", installed_size, licenses, provides, url)
values
(:package, :package_base, :architecture, :archive_size,
:build_date, :depends, :description, :filename,
:groups, :installed_size, :licenses, :provides, :url)
on conflict (package, architecture) do update set
package_base = :package_base, archive_size = :archive_size,
build_date = :build_date, depends = :depends, description = :description, filename = :filename,
"groups" = :groups, installed_size = :installed_size, licenses = :licenses, provides = :provides, url = :url
""",
package_list)
@staticmethod
def _package_update_insert_status(connection: Connection, package_base: str, status: BuildStatus) -> None:
"""
insert base package status into table
:param connection: database connection
:param package_base: package base name
:param status: new build status
"""
connection.execute(
"""
insert into package_statuses (package_base, status, last_updated)
values
(:package_base, :status, :last_updated)
on conflict (package_base) do update set
status = :status, last_updated = :last_updated
""",
dict(package_base=package_base, status=status.status.value, last_updated=status.timestamp))
@staticmethod
def _packages_get_select_package_bases(connection: Connection) -> Dict[str, Package]:
"""
select package bases from the table
:param connection: database connection
:return: map of the package base to its descriptor (without packages themselves)
"""
return {
row["package_base"]: Package(row["package_base"], row["version"], row["aur_url"], {})
for row in connection.execute("""select * from package_bases""")
}
@staticmethod
def _packages_get_select_packages(connection: Connection, packages: Dict[str, Package]) -> Dict[str, Package]:
"""
select packages from the table
:param connection: database connection
:param packages: packages descriptor map
:return: map of the package base to its descriptor including individual packages
"""
for row in connection.execute("""select * from packages"""):
if row["package_base"] not in packages:
continue # normally must never happen though
packages[row["package_base"]].packages[row["package"]] = PackageDescription.from_json(row)
return packages
@staticmethod
def _packages_get_select_statuses(connection: Connection) -> Dict[str, BuildStatus]:
"""
select package build statuses from the table
:param connection: database connection
:return: map of the package base to its status
"""
return {
row["package_base"]: BuildStatus.from_json({"status": row["status"], "timestamp": row["last_updated"]})
for row in connection.execute("""select * from package_statuses""")
}
def package_remove(self, package_base: str) -> None:
"""
remove package from database
:param package_base: package base name
"""
def run(connection: Connection) -> None:
self._package_remove_packages(connection, package_base, [])
self._package_remove_package_base(connection, package_base)
return self.with_connection(run, commit=True)
def package_update(self, package: Package, status: BuildStatus) -> None:
"""
update package status
:param package: package properties
:param status: new build status
"""
def run(connection: Connection) -> None:
self._package_update_insert_base(connection, package)
self._package_update_insert_status(connection, package.base, status)
self._package_update_insert_packages(connection, package)
self._package_remove_packages(connection, package.base, package.packages.keys())
return self.with_connection(run, commit=True)
def packages_get(self) -> List[Tuple[Package, BuildStatus]]:
"""
get package list and their build statuses from database
:return: list of package properties and their statuses
"""
def run(connection: Connection) -> Generator[Tuple[Package, BuildStatus], None, None]:
packages = self._packages_get_select_package_bases(connection)
statuses = self._packages_get_select_statuses(connection)
for package_base, package in self._packages_get_select_packages(connection, packages).items():
yield package, statuses.get(package_base, BuildStatus())
return self.with_connection(lambda connection: list(run(connection)))

View File

@ -0,0 +1,85 @@
#
# Copyright (c) 2021 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 sqlite3 import Connection
from typing import Dict, Optional
from ahriman.core.database.operations.operations import Operations
class PatchOperations(Operations):
"""
operations for patches
"""
def patches_get(self, package_base: str) -> Optional[str]:
"""
retrieve patches for the package
:param package_base: package base to search for patches
:return: plain text patch for the package
"""
return self.patches_list(package_base).get(package_base)
def patches_insert(self, package_base: str, patch: str) -> None:
"""
insert or update patch in database
:param package_base: package base to insert
:param patch: patch content
"""
def run(connection: Connection) -> None:
connection.execute(
"""
insert into patches
(package_base, patch)
values
(:package_base, :patch)
on conflict (package_base) do update set
patch = :patch
""",
{"package_base": package_base, "patch": patch})
return self.with_connection(run, commit=True)
def patches_list(self, package_base: Optional[str]) -> Dict[str, str]:
"""
extract all patches
:param package_base: optional filter by package base
:return: map of package base to patch content
"""
def run(connection: Connection) -> Dict[str, str]:
return {
cursor["package_base"]: 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)
def patches_remove(self, package_base: str) -> None:
"""
remove patch set
:param package_base: package base to clear patches
"""
def run(connection: Connection) -> None:
connection.execute(
"""delete from patches where package_base = :package_base""",
{"package_base": package_base})
return self.with_connection(run, commit=True)

View File

@ -0,0 +1,70 @@
#
# Copyright (c) 2021 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 __future__ import annotations
import json
import sqlite3
from sqlite3 import Connection
from typing import Type
from ahriman.core.configuration import Configuration
from ahriman.core.database.data import migrate_data
from ahriman.core.database.migrations import Migrations
from ahriman.core.database.operations.auth_operations import AuthOperations
from ahriman.core.database.operations.build_operations import BuildOperations
from ahriman.core.database.operations.package_operations import PackageOperations
from ahriman.core.database.operations.patch_operations import PatchOperations
class SQLite(AuthOperations, BuildOperations, PackageOperations, PatchOperations):
"""
wrapper for sqlite3 database
"""
@classmethod
def load(cls: Type[SQLite], configuration: Configuration) -> SQLite:
"""
construct instance from configuration
:param configuration: configuration instance
:return: fully initialized instance of the database
"""
database = cls(configuration.getpath("settings", "database"))
database.init(configuration)
return database
def init(self, configuration: Configuration) -> None:
"""
perform database migrations
:param 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
def run(connection: Connection) -> None:
result = Migrations.migrate(connection)
migrate_data(result, connection, configuration, paths)
self.with_connection(run)
paths.chown(self.path)

View File

@ -26,12 +26,12 @@ class BuildFailed(RuntimeError):
base exception for failed builds
"""
def __init__(self, package: str) -> None:
def __init__(self, package_base: str) -> None:
"""
default constructor
:param package: package base raised exception
:param package_base: package base raised exception
"""
RuntimeError.__init__(self, f"Package {package} build failed, check logs for details")
RuntimeError.__init__(self, f"Package {package_base} build failed, check logs for details")
class DuplicateRun(RuntimeError):
@ -47,18 +47,11 @@ class DuplicateRun(RuntimeError):
self, "Another application instance is run. This error can be suppressed by using --force flag.")
class DuplicateUser(ValueError):
class ExitCode(RuntimeError):
"""
exception which will be thrown in case if there are two users with different settings
special exception which has to be thrown to return non-zero status without error message
"""
def __init__(self, username: str) -> None:
"""
default constructor
:param username: username with duplicates
"""
ValueError.__init__(self, f"Found duplicate user with username {username}")
class InitializeException(RuntimeError):
"""
@ -113,6 +106,19 @@ class InvalidPackageInfo(RuntimeError):
RuntimeError.__init__(self, f"There are errors during reading package information: `{details}`")
class MigrationError(RuntimeError):
"""
exception which will be raised on migration error
"""
def __init__(self, details: str) -> None:
"""
default constructor
:param details: error details
"""
RuntimeError.__init__(self, details)
class MissingArchitecture(ValueError):
"""
exception which will be raised if architecture is required, but missing

View File

@ -0,0 +1,46 @@
#
# Copyright (c) 2021 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 List
from ahriman.core.formatters.string_printer import StringPrinter
from ahriman.models.property import Property
from ahriman.models.user import User
class UserPrinter(StringPrinter):
"""
print properties of user
:ivar user: stored user
"""
def __init__(self, user: User) -> None:
"""
default constructor
:param user: user to print
"""
StringPrinter.__init__(self, user.username)
self.user = user
def properties(self) -> List[Property]:
"""
convert content into printable data
:return: list of content properties
"""
return [Property("role", self.user.access.value, is_required=True)]

View File

@ -37,14 +37,6 @@ class Cleaner(Properties):
"""
raise NotImplementedError
def clear_build(self) -> None:
"""
clear sources directory
"""
self.logger.info("clear package sources directory")
for package in self.paths.sources.iterdir():
shutil.rmtree(package)
def clear_cache(self) -> None:
"""
clear cache directory
@ -61,14 +53,6 @@ class Cleaner(Properties):
for chroot in self.paths.chroot.iterdir():
shutil.rmtree(chroot)
def clear_manual(self) -> None:
"""
clear directory with manual package updates
"""
self.logger.info("clear manual packages")
for package in self.paths.manual.iterdir():
shutil.rmtree(package)
def clear_packages(self) -> None:
"""
clear directory with built packages (NOT repository itself)
@ -77,10 +61,9 @@ class Cleaner(Properties):
for package in self.packages_built():
package.unlink()
def clear_patches(self) -> None:
def clear_queue(self) -> None:
"""
clear directory with patches
clear packages which were queued for the update
"""
self.logger.info("clear patches directory")
for package in self.paths.patches.iterdir():
shutil.rmtree(package)
self.logger.info("clear build queue")
self.database.build_queue_clear(None)

View File

@ -26,6 +26,7 @@ from ahriman.core.build_tools.task import Task
from ahriman.core.report.report import Report
from ahriman.core.repository.cleaner import Cleaner
from ahriman.core.upload.upload import Upload
from ahriman.core.util import tmpdir
from ahriman.models.package import Package
from ahriman.models.result import Result
@ -56,25 +57,25 @@ class Executor(Cleaner):
:param updates: list of packages properties to build
:return: `packages_built`
"""
def build_single(package: Package) -> None:
def build_single(package: Package, local_path: Path) -> None:
self.reporter.set_building(package.base)
task = Task(package, self.configuration, self.paths)
task.init()
built = task.build()
task.init(local_path, self.database)
built = task.build(local_path)
for src in built:
dst = self.paths.packages / src.name
shutil.move(src, dst)
result = Result()
for single in updates:
try:
build_single(single)
result.add_success(single)
except Exception:
self.reporter.set_failed(single.base)
result.add_failed(single)
self.logger.exception("%s (%s) build exception", single.base, self.architecture)
self.clear_build()
with tmpdir() as build_dir:
try:
build_single(single, build_dir)
result.add_success(single)
except Exception:
self.reporter.set_failed(single.base)
result.add_failed(single)
self.logger.exception("%s (%s) build exception", single.base, self.architecture)
return result
@ -87,6 +88,8 @@ class Executor(Cleaner):
def remove_base(package_base: str) -> None:
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.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)

View File

@ -22,11 +22,11 @@ import logging
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.repo import Repo
from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.exceptions import UnsafeRun
from ahriman.core.sign.gpg import GPG
from ahriman.core.status.client import Client
from ahriman.core.util import check_user
from ahriman.models.repository_paths import RepositoryPaths
class Properties:
@ -35,6 +35,7 @@ class Properties:
:ivar architecture: repository architecture
:ivar aur_url: base AUR url
:ivar configuration: configuration instance
:ivar database: database instance
:ivar ignore_list: package bases which will be ignored during auto updates
:ivar logger: class logger
:ivar name: repository name
@ -45,22 +46,25 @@ class Properties:
:ivar sign: GPG wrapper instance
"""
def __init__(self, architecture: str, configuration: Configuration, no_report: bool, unsafe: bool) -> None:
def __init__(self, architecture: str, configuration: Configuration, database: SQLite,
no_report: bool, unsafe: bool) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
:param database: database instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
"""
self.logger = logging.getLogger("root")
self.architecture = architecture
self.configuration = configuration
self.database = database
self.aur_url = configuration.get("alpm", "aur_url")
self.name = configuration.get("repository", "name")
self.paths = RepositoryPaths(configuration.getpath("repository", "root"), architecture)
self.paths = configuration.repository_paths
try:
check_user(self.paths, unsafe)
self.paths.tree_create()

View File

@ -90,7 +90,7 @@ class UpdateHandler(Cleaner):
else:
self.reporter.set_success(local)
except Exception:
self.logger.exception("could not procees package at %s", dirname)
self.logger.exception("could not process package at %s", dirname)
return result
@ -102,16 +102,15 @@ class UpdateHandler(Cleaner):
result: List[Package] = []
known_bases = {package.base for package in self.packages()}
for dirname in self.paths.manual.iterdir():
try:
local = Package.load(str(dirname), PackageSource.Local, self.pacman, self.aur_url)
try:
for local in self.database.build_queue_get():
result.append(local)
if local.base not in known_bases:
self.reporter.set_unknown(local)
else:
self.reporter.set_pending(local.base)
except Exception:
self.logger.exception("could not add package from %s", dirname)
self.clear_manual()
except Exception:
self.logger.exception("could not load packages from database")
self.clear_queue()
return result

View File

@ -77,11 +77,6 @@ class Client:
"""
return BuildStatus()
def reload_auth(self) -> None:
"""
reload authentication module call
"""
def remove(self, base: str) -> None:
"""
remove packages from watcher

View File

@ -17,13 +17,12 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import json
import logging
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from typing import Dict, List, Optional, Tuple
from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.exceptions import UnknownPackage
from ahriman.core.repository import Repository
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
@ -34,33 +33,29 @@ class Watcher:
"""
package status watcher
:ivar architecture: repository architecture
:ivar database: database instance
:ivar known: list of known packages. For the most cases `packages` should be used instead
:ivar logger: class logger
:ivar repository: repository object
:ivar status: daemon status
"""
def __init__(self, architecture: str, configuration: Configuration) -> None:
def __init__(self, architecture: str, configuration: Configuration, database: SQLite) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
:param database: database instance
"""
self.logger = logging.getLogger("http")
self.architecture = architecture
self.repository = Repository(architecture, configuration, no_report=True, unsafe=False)
self.database = database
self.repository = Repository(architecture, configuration, database, no_report=True, unsafe=False)
self.known: Dict[str, Tuple[Package, BuildStatus]] = {}
self.status = BuildStatus()
@property
def cache_path(self) -> Path:
"""
:return: path to dump with json cache
"""
return self.repository.paths.root / "status_cache.json"
@property
def packages(self) -> List[Tuple[Package, BuildStatus]]:
"""
@ -68,48 +63,6 @@ class Watcher:
"""
return list(self.known.values())
def _cache_load(self) -> None:
"""
update current state from cache
"""
def parse_single(properties: Dict[str, Any]) -> None:
package = Package.from_json(properties["package"])
status = BuildStatus.from_json(properties["status"])
if package.base in self.known:
self.known[package.base] = (package, status)
if not self.cache_path.is_file():
return
with self.cache_path.open() as cache:
try:
dump = json.load(cache)
except Exception:
self.logger.exception("cannot parse json from file")
dump = {}
for item in dump.get("packages", []):
try:
parse_single(item)
except Exception:
self.logger.exception("cannot parse item %s to package", item)
def _cache_save(self) -> None:
"""
dump current cache to filesystem
"""
dump = {
"packages": [
{
"package": package.view(),
"status": status.view()
} for package, status in self.packages
]
}
try:
with self.cache_path.open("w") as cache:
json.dump(dump, cache)
except Exception:
self.logger.exception("cannot dump cache")
def get(self, base: str) -> Tuple[Package, BuildStatus]:
"""
get current package base build status
@ -131,31 +84,34 @@ class Watcher:
else:
status = BuildStatus()
self.known[package.base] = (package, status)
self._cache_load()
def remove(self, base: str) -> None:
for package, status in self.database.packages_get():
if package.base in self.known:
self.known[package.base] = (package, status)
def remove(self, package_base: str) -> None:
"""
remove package base from known list if any
:param base: package base
:param package_base: package base
"""
self.known.pop(base, None)
self._cache_save()
self.known.pop(package_base, None)
self.database.package_remove(package_base)
def update(self, base: str, status: BuildStatusEnum, package: Optional[Package]) -> None:
def update(self, package_base: str, status: BuildStatusEnum, package: Optional[Package]) -> None:
"""
update package status and description
:param base: package base to update
:param package_base: package base to update
:param status: new build status
:param package: optional new package description. In case if not set current properties will be used
"""
if package is None:
try:
package, _ = self.known[base]
package, _ = self.known[package_base]
except KeyError:
raise UnknownPackage(base)
raise UnknownPackage(package_base)
full_status = BuildStatus(status)
self.known[base] = (package, full_status)
self._cache_save()
self.known[package_base] = (package, full_status)
self.database.package_update(package, full_status)
def update_self(self, status: BuildStatusEnum) -> None:
"""

View File

@ -67,13 +67,6 @@ class WebClient(Client):
"""
return f"{self.address}/user-api/v1/login"
@property
def _reload_auth_url(self) -> str:
"""
:return: full url for web service to reload authentication module
"""
return f"{self.address}/service-api/v1/reload-auth"
@property
def _status_url(self) -> str:
"""
@ -198,18 +191,6 @@ class WebClient(Client):
self.logger.exception("could not get service status")
return BuildStatus()
def reload_auth(self) -> None:
"""
reload authentication module call
"""
try:
response = self.__session.post(self._reload_auth_url)
response.raise_for_status()
except requests.HTTPError as e:
self.logger.exception("could not reload auth module: %s", exception_response_text(e))
except Exception:
self.logger.exception("could not reload auth module")
def remove(self, base: str) -> None:
"""
remove packages from watcher

View File

@ -26,8 +26,8 @@ from pathlib import Path
from typing import Iterable, List, Set, Type
from ahriman.core.build_tools.sources import Sources
from ahriman.core.database.sqlite import SQLite
from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths
class Leaf:
@ -54,16 +54,16 @@ class Leaf:
return self.package.packages.keys()
@classmethod
def load(cls: Type[Leaf], package: Package, paths: RepositoryPaths) -> Leaf:
def load(cls: Type[Leaf], package: Package, database: SQLite) -> Leaf:
"""
load leaf from package with dependencies
:param package: package properties
:param paths: repository paths instance
:param database: database instance
:return: loaded class
"""
clone_dir = Path(tempfile.mkdtemp())
try:
Sources.load(clone_dir, package.git_url, paths.patches_for(package.base))
Sources.load(clone_dir, package.git_url, database.patches_get(package.base))
dependencies = Package.dependencies(clone_dir)
finally:
shutil.rmtree(clone_dir, ignore_errors=True)
@ -95,14 +95,14 @@ class Tree:
self.leaves = leaves
@classmethod
def load(cls: Type[Tree], packages: Iterable[Package], paths: RepositoryPaths) -> Tree:
def load(cls: Type[Tree], packages: Iterable[Package], database: SQLite) -> Tree:
"""
load tree from packages
:param packages: packages list
:param paths: repository paths instance
:param database: database instance
:return: loaded class
"""
return cls([Leaf.load(package, paths) for package in packages])
return cls([Leaf.load(package, database) for package in packages])
def levels(self) -> List[List[Package]]:
"""

View File

@ -19,9 +19,12 @@
#
import datetime
import os
import subprocess
import requests
import shutil
import subprocess
import tempfile
from contextlib import contextmanager
from logging import Logger
from pathlib import Path
from typing import Any, Dict, Generator, Iterable, Optional, Union
@ -143,6 +146,19 @@ def pretty_size(size: Optional[float], level: int = 0) -> str:
return pretty_size(size / 1024, level + 1)
@contextmanager
def tmpdir() -> Generator[Path, None, None]:
"""
wrapper for tempfile to remove directory after all
:return: path to the created directory
"""
path = Path(tempfile.mkdtemp())
try:
yield path
finally:
shutil.rmtree(path, ignore_errors=True)
def walk(directory_path: Path) -> Generator[Path, None, None]:
"""
list all file paths in given directory

View File

@ -0,0 +1,35 @@
#
# Copyright (c) 2021 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 dataclasses import dataclass
from typing import List
@dataclass
class Migration:
"""
migration implementation
:ivar index: migration position
:ivar name: migration name
:ivar steps: migration steps
"""
index: int
name: str
steps: List[str]

View File

@ -0,0 +1,50 @@
#
# Copyright (c) 2021 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 dataclasses import dataclass
from ahriman.core.exceptions import MigrationError
@dataclass
class MigrationResult:
"""
migration result implementation model
:ivar old_version: old schema version before migrations
:ivar new_version: new schema version after migrations
"""
old_version: int
new_version: int
@property
def is_outdated(self) -> bool:
"""
:return: True in case if it requires migrations and False otherwise
"""
self.validate()
return self.new_version > self.old_version
def validate(self) -> None:
"""
perform version validation
"""
if self.old_version < 0 or self.old_version > self.new_version:
raise MigrationError(f"Invalid current schema version, expected less or equal to {self.new_version}, "
f"got {self.old_version}")

View File

@ -239,7 +239,7 @@ class Package:
from ahriman.core.build_tools.sources import Sources
logger = logging.getLogger("build_details")
Sources.load(paths.cache_for(self.base), self.git_url, paths.patches_for(self.base))
Sources.load(paths.cache_for(self.base), self.git_url, None)
try:
# update pkgver first

View File

@ -19,7 +19,7 @@
#
from __future__ import annotations
from dataclasses import dataclass, field, fields
from dataclasses import asdict, dataclass, field, fields
from pathlib import Path
from pyalpm import Package # type: ignore
from typing import Any, Dict, List, Optional, Type
@ -94,3 +94,10 @@ class PackageDescription:
licenses=package.licenses,
provides=package.provides,
url=package.url)
def view(self) -> Dict[str, Any]:
"""
generate json package view
:return: json-friendly dictionary
"""
return asdict(self)

View File

@ -52,16 +52,9 @@ class RepositoryPaths:
"""
:return: directory for devtools chroot
"""
# for the chroot directory devtools will create own tree and we don"t have to specify architecture here
# for the chroot directory devtools will create own tree, and we don"t have to specify architecture here
return self.root / "chroot"
@property
def manual(self) -> Path:
"""
:return: directory for manual updates (i.e. from add command)
"""
return self.root / "manual" / self.architecture
@property
def packages(self) -> Path:
"""
@ -69,13 +62,6 @@ class RepositoryPaths:
"""
return self.root / "packages" / self.architecture
@property
def patches(self) -> Path:
"""
:return: directory for source patches
"""
return self.root / "patches"
@property
def repository(self) -> Path:
"""
@ -90,13 +76,6 @@ class RepositoryPaths:
"""
return self.owner(self.root)
@property
def sources(self) -> Path:
"""
:return: directory for downloaded PKGBUILDs for current build
"""
return self.root / "sources" / self.architecture
@classmethod
def known_architectures(cls: Type[RepositoryPaths], root: Path) -> Set[str]:
"""
@ -151,30 +130,6 @@ class RepositoryPaths:
set_owner(path)
path = path.parent
def manual_for(self, package_base: str) -> Path:
"""
get manual path for specific package base
:param package_base: package base name
:return: full path to directory for specified package base manual updates
"""
return self.manual / package_base
def patches_for(self, package_base: str) -> Path:
"""
get patches path for specific package base
:param package_base: package base name
:return: full path to directory for specified package base patches
"""
return self.patches / package_base
def sources_for(self, package_base: str) -> Path:
"""
get path to directory from where build will start for the package base
:param package_base: package base name
:return: full path to directory for specified package base sources
"""
return self.sources / package_base
def tree_clear(self, package_base: str) -> None:
"""
clear package specific files
@ -182,9 +137,7 @@ class RepositoryPaths:
"""
for directory in (
self.cache_for(package_base),
self.manual_for(package_base),
self.patches_for(package_base),
self.sources_for(package_base)):
):
shutil.rmtree(directory, ignore_errors=True)
def tree_create(self) -> None:
@ -194,10 +147,8 @@ class RepositoryPaths:
for directory in (
self.cache,
self.chroot,
self.manual,
self.packages,
self.patches,
self.repository,
self.sources):
):
directory.mkdir(mode=0o755, parents=True, exist_ok=True)
self.chown(directory)

View File

@ -79,18 +79,18 @@ class User:
verified = False # the absence of evidence is not the evidence of absence (c) Gin Rummy
return verified
def hash_password(self, salt: str) -> str:
def hash_password(self, salt: str) -> User:
"""
generate hashed password from plain text
:param salt: salt for hashed password
:return: hashed string to store in configuration
:return: user with hashed password to store in configuration
"""
if not self.password:
# in case of empty password we leave it empty. This feature is used by any external (like OAuth) provider
# when we do not store any password here
return ""
return self
password_hash: str = self._HASHER.hash(self.password + salt)
return password_hash
return User(self.username, password_hash, self.access)
def verify_access(self, required: UserAccess) -> bool:
"""

View File

@ -22,7 +22,6 @@ from pathlib import Path
from ahriman.web.views.index import IndexView
from ahriman.web.views.service.add import AddView
from ahriman.web.views.service.reload_auth import ReloadAuthView
from ahriman.web.views.service.remove import RemoveView
from ahriman.web.views.service.request import RequestView
from ahriman.web.views.service.search import SearchView
@ -45,8 +44,6 @@ def setup_routes(application: Application, static_path: Path) -> None:
POST /service-api/v1/add add new packages to repository
POST /service-api/v1/reload-auth reload authentication module
POST /service-api/v1/remove remove existing package from repository
POST /service-api/v1/request request to add new packages to repository
@ -81,8 +78,6 @@ def setup_routes(application: Application, static_path: Path) -> None:
application.router.add_post("/service-api/v1/add", AddView)
application.router.add_post("/service-api/v1/reload-auth", ReloadAuthView)
application.router.add_post("/service-api/v1/remove", RemoveView)
application.router.add_post("/service-api/v1/request", RequestView)

View File

@ -24,6 +24,7 @@ from typing import Any, Dict, List, Optional, Type
from ahriman.core.auth.auth import Auth
from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.spawn import Spawn
from ahriman.core.status.watcher import Watcher
from ahriman.models.user_access import UserAccess
@ -42,6 +43,14 @@ class BaseView(View):
configuration: Configuration = self.request.app["configuration"]
return configuration
@property
def database(self) -> SQLite:
"""
:return: database instance
"""
database: SQLite = self.request.app["database"]
return database
@property
def service(self) -> Watcher:
"""

View File

@ -1,51 +0,0 @@
#
# Copyright (c) 2021 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 aiohttp.web import HTTPNoContent, Response
from ahriman.core.auth.auth import Auth
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
class ReloadAuthView(BaseView):
"""
reload authentication module web view
:cvar POST_PERMISSION: post permissions of self
"""
POST_PERMISSION = UserAccess.Write
async def post(self) -> Response:
"""
reload authentication module. No parameters supported here
:return: 204 on success
"""
self.configuration.reload()
try:
import aiohttp_security # type: ignore
self.request.app[aiohttp_security.api.AUTZ_KEY].validator =\
self.request.app["validator"] =\
Auth.load(self.configuration)
except (ImportError, KeyError):
self.request.app.logger.warning("could not update authentication module validator", exc_info=True)
raise
raise HTTPNoContent()

View File

@ -58,7 +58,7 @@ class LoginView(BaseView):
identity = UserIdentity.from_username(username, self.validator.max_age)
if identity is not None and await self.validator.known_username(username):
await remember(self.request, response, identity.to_identity())
return response
raise response
raise HTTPUnauthorized()
@ -81,6 +81,6 @@ class LoginView(BaseView):
identity = UserIdentity.from_username(username, self.validator.max_age)
if identity is not None and await self.validator.check_credentials(username, data.get("password")):
await remember(self.request, response, identity.to_identity())
return response
raise response
raise HTTPUnauthorized()

View File

@ -25,6 +25,7 @@ from aiohttp import web
from ahriman.core.auth.auth import Auth
from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.exceptions import InitializeException
from ahriman.core.spawn import Spawn
from ahriman.core.status.watcher import Watcher
@ -93,8 +94,11 @@ def setup_service(architecture: str, configuration: Configuration, spawner: Spaw
application.logger.info("setup configuration")
application["configuration"] = configuration
application.logger.info("setup database and perform migrations")
database = application["database"] = SQLite.load(configuration)
application.logger.info("setup watcher")
application["watcher"] = Watcher(architecture, configuration)
application["watcher"] = Watcher(architecture, configuration, database)
application.logger.info("setup process spawner")
application["spawn"] = spawner
@ -108,7 +112,7 @@ def setup_service(architecture: str, configuration: Configuration, spawner: Spaw
check_host=configuration.getboolean("web", "debug_check_host", fallback=False))
application.logger.info("setup authorization")
validator = application["validator"] = Auth.load(configuration)
validator = application["validator"] = Auth.load(configuration, database)
if validator.enabled:
from ahriman.web.middlewares.auth_handler import setup_auth
setup_auth(application, validator)

View File

@ -6,39 +6,46 @@ from ahriman.application.application.packages import Packages
from ahriman.application.application.properties import Properties
from ahriman.application.application.repository import Repository
from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
@pytest.fixture
def application_packages(configuration: Configuration, mocker: MockerFixture) -> Packages:
def application_packages(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> Packages:
"""
fixture for application with package functions
:param configuration: configuration fixture
:param database: database fixture
:param mocker: mocker object
:return: application test instance
"""
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
mocker.patch("ahriman.core.database.sqlite.SQLite.load", return_value=database)
return Packages("x86_64", configuration, no_report=True, unsafe=False)
@pytest.fixture
def application_properties(configuration: Configuration, mocker: MockerFixture) -> Properties:
def application_properties(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> Properties:
"""
fixture for application with properties only
:param configuration: configuration fixture
:param database: database fixture
:param mocker: mocker object
:return: application test instance
"""
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
mocker.patch("ahriman.core.database.sqlite.SQLite.load", return_value=database)
return Properties("x86_64", configuration, no_report=True, unsafe=False)
@pytest.fixture
def application_repository(configuration: Configuration, mocker: MockerFixture) -> Repository:
def application_repository(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> Repository:
"""
fixture for application with repository functions
:param configuration: configuration fixture
:param database: database fixture
:param mocker: mocker object
:return: application test instance
"""
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
mocker.patch("ahriman.core.database.sqlite.SQLite.load", return_value=database)
return Repository("x86_64", configuration, no_report=True, unsafe=False)

View File

@ -2,7 +2,6 @@ import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from unittest import mock
from unittest.mock import MagicMock
from ahriman.application.application.packages import Packages
@ -43,16 +42,17 @@ def test_add_aur(application_packages: Packages, package_ahriman: Package, mocke
must add package from AUR
"""
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
insert_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.build_queue_insert")
load_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.load")
dependencies_mock = mocker.patch("ahriman.application.application.packages.Packages._process_dependencies")
application_packages._add_aur(package_ahriman.base, set(), False)
insert_mock.assert_called_once_with(package_ahriman)
load_mock.assert_called_once_with(
application_packages.repository.paths.manual_for(package_ahriman.base),
pytest.helpers.anyvar(int),
package_ahriman.git_url,
application_packages.repository.paths.patches_for(package_ahriman.base))
dependencies_mock.assert_called_once_with(
application_packages.repository.paths.manual_for(package_ahriman.base), set(), False)
pytest.helpers.anyvar(int))
dependencies_mock.assert_called_once_with(pytest.helpers.anyvar(int), set(), False)
def test_add_directory(application_packages: Packages, package_ahriman: Package, mocker: MockerFixture) -> None:
@ -75,18 +75,16 @@ def test_add_local(application_packages: Packages, package_ahriman: Package, moc
"""
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
init_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.init")
insert_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.build_queue_insert")
copytree_mock = mocker.patch("shutil.copytree")
dependencies_mock = mocker.patch("ahriman.application.application.packages.Packages._process_dependencies")
application_packages._add_local(package_ahriman.base, set(), False)
copytree_mock.assert_called_once_with(
Path(package_ahriman.base), application_packages.repository.paths.cache_for(package_ahriman.base))
init_mock.assert_called_once_with(application_packages.repository.paths.cache_for(package_ahriman.base))
copytree_mock.assert_has_calls([
mock.call(Path(package_ahriman.base), application_packages.repository.paths.cache_for(package_ahriman.base)),
mock.call(application_packages.repository.paths.cache_for(package_ahriman.base),
application_packages.repository.paths.manual_for(package_ahriman.base)),
])
dependencies_mock.assert_called_once_with(
application_packages.repository.paths.manual_for(package_ahriman.base), set(), False)
insert_mock.assert_called_once_with(package_ahriman)
dependencies_mock.assert_called_once_with(pytest.helpers.anyvar(int), set(), False)
def test_add_remote(application_packages: Packages, package_description_ahriman: PackageDescription,

View File

@ -17,21 +17,12 @@ def test_finalize(application_repository: Repository) -> None:
application_repository._finalize([])
def test_clean_build(application_repository: Repository, mocker: MockerFixture) -> None:
"""
must clean build directory
"""
clear_mock = mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_build")
application_repository.clean(True, False, False, False, False, False)
clear_mock.assert_called_once_with()
def test_clean_cache(application_repository: Repository, mocker: MockerFixture) -> None:
"""
must clean cache directory
"""
clear_mock = mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_cache")
application_repository.clean(False, True, False, False, False, False)
application_repository.clean(True, False, False, False)
clear_mock.assert_called_once_with()
@ -40,7 +31,7 @@ def test_clean_chroot(application_repository: Repository, mocker: MockerFixture)
must clean chroot directory
"""
clear_mock = mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_chroot")
application_repository.clean(False, False, True, False, False, False)
application_repository.clean(False, True, False, False)
clear_mock.assert_called_once_with()
@ -48,8 +39,8 @@ def test_clean_manual(application_repository: Repository, mocker: MockerFixture)
"""
must clean manual directory
"""
clear_mock = mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_manual")
application_repository.clean(False, False, False, True, False, False)
clear_mock = mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_queue")
application_repository.clean(False, False, True, False)
clear_mock.assert_called_once_with()
@ -58,16 +49,7 @@ def test_clean_packages(application_repository: Repository, mocker: MockerFixtur
must clean packages directory
"""
clear_mock = mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_packages")
application_repository.clean(False, False, False, False, True, False)
clear_mock.assert_called_once_with()
def test_clean_patches(application_repository: Repository, mocker: MockerFixture) -> None:
"""
must clean packages directory
"""
clear_mock = mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_patches")
application_repository.clean(False, False, False, False, False, True)
application_repository.clean(False, False, False, True)
clear_mock.assert_called_once_with()

View File

@ -7,17 +7,20 @@ from ahriman.application.ahriman import _parser
from ahriman.application.application import Application
from ahriman.application.lock import Lock
from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
@pytest.fixture
def application(configuration: Configuration, mocker: MockerFixture) -> Application:
def application(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> Application:
"""
fixture for application
:param configuration: configuration fixture
:param database: database fixture
:param mocker: mocker object
:return: application test instance
"""
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
mocker.patch("ahriman.core.database.sqlite.SQLite.load", return_value=database)
return Application("x86_64", configuration, no_report=True, unsafe=False)

View File

@ -6,7 +6,7 @@ from pytest_mock import MockerFixture
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import MissingArchitecture, MultipleArchitectures
from ahriman.core.exceptions import ExitCode, MissingArchitecture, MultipleArchitectures
def test_architectures_extract(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
@ -71,8 +71,26 @@ def test_call_exception(args: argparse.Namespace, mocker: MockerFixture) -> None
"""
must process exception
"""
mocker.patch("ahriman.application.lock.Lock.__enter__", side_effect=Exception())
args.configuration = Path("")
args.quiet = False
mocker.patch("ahriman.core.configuration.Configuration.from_path", side_effect=Exception())
logging_mock = mocker.patch("logging.Logger.exception")
assert not Handler.call(args, "x86_64")
logging_mock.assert_called_once_with(pytest.helpers.anyvar(str, strict=True))
def test_call_exit_code(args: argparse.Namespace, mocker: MockerFixture) -> None:
"""
must process exitcode exception
"""
args.configuration = Path("")
args.quiet = False
mocker.patch("ahriman.core.configuration.Configuration.from_path", side_effect=ExitCode())
logging_mock = mocker.patch("logging.Logger.exception")
assert not Handler.call(args, "x86_64")
logging_mock.assert_not_called()
def test_execute(args: argparse.Namespace, mocker: MockerFixture) -> None:
@ -98,11 +116,14 @@ def test_execute_multiple_not_supported(args: argparse.Namespace, mocker: Mocker
Handler.execute(args)
def test_execute_single(args: argparse.Namespace, mocker: MockerFixture) -> None:
def test_execute_single(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run execution in current process if only one architecture supplied
"""
args.architecture = ["x86_64"]
args.configuration = Path("")
args.quiet = False
mocker.patch("ahriman.core.configuration.Configuration.from_path", return_value=configuration)
starmap_mock = mocker.patch("multiprocessing.pool.Pool.starmap")
Handler.execute(args)

View File

@ -12,12 +12,10 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
:param args: command line arguments fixture
:return: generated arguments for these test cases
"""
args.build = False
args.cache = False
args.chroot = False
args.manual = False
args.packages = False
args.patches = False
return args
@ -30,4 +28,4 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
application_mock = mocker.patch("ahriman.application.application.Application.clean")
Clean.run(args, "x86_64", configuration, True, False)
application_mock.assert_called_once_with(False, False, False, False, False, False)
application_mock.assert_called_once_with(False, False, False, False)

View File

@ -67,24 +67,23 @@ def test_patch_set_list(application: Application, mocker: MockerFixture) -> None
must list available patches for the command
"""
mocker.patch("pathlib.Path.is_dir", return_value=True)
glob_mock = mocker.patch("pathlib.Path.glob", return_value=[Path("local")])
print_mock = mocker.patch("ahriman.application.handlers.patch.Patch._print")
get_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.patches_list", return_value={"ahriman": "patch"})
print_mock = mocker.patch("ahriman.core.formatters.printer.Printer.print")
Patch.patch_set_list(application, "ahriman")
glob_mock.assert_called_once_with("*.patch")
print_mock.assert_called()
get_mock.assert_called_once_with("ahriman")
print_mock.assert_called_once_with(verbose=True)
def test_patch_set_list_no_dir(application: Application, mocker: MockerFixture) -> None:
def test_patch_set_list_no_patches(application: Application, mocker: MockerFixture) -> None:
"""
must not fail if no patches directory found
"""
mocker.patch("pathlib.Path.is_dir", return_value=False)
glob_mock = mocker.patch("pathlib.Path.glob")
print_mock = mocker.patch("ahriman.application.handlers.patch.Patch._print")
mocker.patch("ahriman.core.database.sqlite.SQLite.patches_get", return_value=None)
print_mock = mocker.patch("ahriman.core.formatters.printer.Printer.print")
Patch.patch_set_list(application, "ahriman")
glob_mock.assert_not_called()
print_mock.assert_not_called()
@ -94,21 +93,17 @@ def test_patch_set_create(application: Application, package_ahriman: Package, mo
"""
mocker.patch("pathlib.Path.mkdir")
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
remove_mock = mocker.patch("ahriman.application.handlers.patch.Patch.patch_set_remove")
create_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_create")
patch_dir = application.repository.paths.patches_for(package_ahriman.base)
mocker.patch("ahriman.core.build_tools.sources.Sources.patch_create", return_value="patch")
create_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.patches_insert")
Patch.patch_set_create(application, Path("path"), ["*.patch"])
remove_mock.assert_called_once_with(application, package_ahriman.base)
create_mock.assert_called_once_with(Path("path"), patch_dir / "00-main.patch", "*.patch")
Patch.patch_set_create(application, "path", ["*.patch"])
create_mock.assert_called_once_with(package_ahriman.base, "patch")
def test_patch_set_remove(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must remove patch set for the package
"""
remove_mock = mocker.patch("shutil.rmtree")
patch_dir = application.repository.paths.patches_for(package_ahriman.base)
remove_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.patches_remove")
Patch.patch_set_remove(application, package_ahriman.base)
remove_mock.assert_called_once_with(patch_dir, ignore_errors=True)
remove_mock.assert_called_once_with(package_ahriman.base)

View File

@ -6,13 +6,25 @@ from pytest_mock import MockerFixture
from ahriman.application.ahriman import _parser
from ahriman.application.handlers import UnsafeCommands
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import ExitCode
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
"""
default arguments for these test cases
:param args: command line arguments fixture
:return: generated arguments for these test cases
"""
args.parser = _parser
args.command = None
return args
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command
"""
args.parser = _parser
args = _default_args(args)
commands_mock = mocker.patch("ahriman.application.handlers.UnsafeCommands.get_unsafe_commands",
return_value=["command"])
print_mock = mocker.patch("ahriman.core.formatters.printer.Printer.print")
@ -22,6 +34,36 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
print_mock.assert_called_once_with(verbose=True)
def test_run_check(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command and check if command is unsafe
"""
args = _default_args(args)
args.command = "clean"
commands_mock = mocker.patch("ahriman.application.handlers.UnsafeCommands.get_unsafe_commands",
return_value=["command"])
check_mock = mocker.patch("ahriman.application.handlers.UnsafeCommands.check_unsafe")
UnsafeCommands.run(args, "x86_64", configuration, True, False)
commands_mock.assert_called_once_with(pytest.helpers.anyvar(int))
check_mock.assert_called_once_with("clean", ["command"], pytest.helpers.anyvar(int))
def test_check_unsafe() -> None:
"""
must check if command is unsafe
"""
with pytest.raises(ExitCode):
UnsafeCommands.check_unsafe("repo-clean", ["repo-clean"], _parser())
def test_check_unsafe_safe() -> None:
"""
must check if command is unsafe
"""
UnsafeCommands.check_unsafe("package-status", ["repo-clean"], _parser())
def test_get_unsafe_commands() -> None:
"""
must return unsafe commands

View File

@ -3,10 +3,11 @@ import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from unittest import mock
from ahriman.application.handlers import User
from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.exceptions import InitializeException
from ahriman.models.action import Action
from ahriman.models.user import User as MUser
from ahriman.models.user_access import UserAccess
@ -21,96 +22,78 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.username = "user"
args.action = Action.Update
args.as_service = False
args.no_reload = False
args.password = "pa55w0rd"
args.role = UserAccess.Read
args.secure = False
return args
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
def test_run(args: argparse.Namespace, configuration: Configuration, database: SQLite, mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
user = MUser(args.username, args.password, args.role)
mocker.patch("ahriman.core.database.sqlite.SQLite.load", return_value=database)
mocker.patch("ahriman.models.user.User.hash_password", return_value=user)
get_auth_configuration_mock = mocker.patch("ahriman.application.handlers.User.configuration_get")
create_configuration_mock = mocker.patch("ahriman.application.handlers.User.configuration_create")
write_configuration_mock = mocker.patch("ahriman.application.handlers.User.configuration_write")
create_user_mock = mocker.patch("ahriman.application.handlers.User.user_create")
get_salt_mock = mocker.patch("ahriman.application.handlers.User.get_salt")
reload_mock = mocker.patch("ahriman.core.status.client.Client.reload_auth")
create_user_mock = mocker.patch("ahriman.application.handlers.User.user_create", return_value=user)
get_salt_mock = mocker.patch("ahriman.application.handlers.User.get_salt", return_value="salt")
update_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.user_update")
User.run(args, "x86_64", configuration, True, False)
get_auth_configuration_mock.assert_called_once_with(configuration.include)
create_configuration_mock.assert_called_once_with(
pytest.helpers.anyvar(int), pytest.helpers.anyvar(int), pytest.helpers.anyvar(int), args.as_service)
create_configuration_mock.assert_called_once_with(pytest.helpers.anyvar(int), pytest.helpers.anyvar(int),
pytest.helpers.anyvar(int), args.as_service, args.secure)
create_user_mock.assert_called_once_with(args)
get_salt_mock.assert_called_once_with(configuration)
write_configuration_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.secure)
reload_mock.assert_called_once_with()
update_mock.assert_called_once_with(user)
def test_run_remove(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
def test_run_list(args: argparse.Namespace, configuration: Configuration, database: SQLite, user: User,
mocker: MockerFixture) -> None:
"""
must list avaiable users
"""
args = _default_args(args)
args.action = Action.List
mocker.patch("ahriman.core.database.sqlite.SQLite.load", return_value=database)
get_auth_configuration_mock = mocker.patch("ahriman.application.handlers.User.configuration_get")
list_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.user_list", return_value=[user])
User.run(args, "x86_64", configuration, True, False)
get_auth_configuration_mock.assert_called_once_with(configuration.include)
list_mock.assert_called_once_with("user", UserAccess.Read)
def test_run_remove(args: argparse.Namespace, configuration: Configuration, database: SQLite,
mocker: MockerFixture) -> None:
"""
must remove user if remove flag supplied
"""
args = _default_args(args)
args.action = Action.Remove
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
mocker.patch("ahriman.core.database.sqlite.SQLite.load", return_value=database)
get_auth_configuration_mock = mocker.patch("ahriman.application.handlers.User.configuration_get")
create_configuration_mock = mocker.patch("ahriman.application.handlers.User.configuration_create")
write_configuration_mock = mocker.patch("ahriman.application.handlers.User.configuration_write")
reload_mock = mocker.patch("ahriman.core.status.client.Client.reload_auth")
remove_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.user_remove")
User.run(args, "x86_64", configuration, True, False)
get_auth_configuration_mock.assert_called_once_with(configuration.include)
create_configuration_mock.assert_not_called()
write_configuration_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.secure)
reload_mock.assert_called_once_with()
def test_run_no_reload(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command with no reload
"""
args = _default_args(args)
args.no_reload = True
mocker.patch("ahriman.application.handlers.User.configuration_get")
mocker.patch("ahriman.application.handlers.User.configuration_create")
mocker.patch("ahriman.application.handlers.User.configuration_write")
reload_mock = mocker.patch("ahriman.core.status.client.Client.reload_auth")
User.run(args, "x86_64", configuration, True, False)
reload_mock.assert_not_called()
remove_mock.assert_called_once_with(args.username)
def test_configuration_create(configuration: Configuration, user: MUser, mocker: MockerFixture) -> None:
"""
must correctly create configuration file
"""
section = Configuration.section_name("auth", user.access.value)
mocker.patch("pathlib.Path.open")
set_mock = mocker.patch("ahriman.core.configuration.Configuration.set_option")
write_mock = mocker.patch("ahriman.application.handlers.User.configuration_write")
User.configuration_create(configuration, user, "salt", False)
set_mock.assert_has_calls([
mock.call("auth", "salt", pytest.helpers.anyvar(int)),
mock.call(section, user.username, pytest.helpers.anyvar(int))
])
def test_configuration_create_user_exists(configuration: Configuration, user: MUser, mocker: MockerFixture) -> None:
"""
must correctly update configuration file if user already exists
"""
section = Configuration.section_name("auth", user.access.value)
configuration.set_option(section, user.username, "")
mocker.patch("pathlib.Path.open")
User.configuration_create(configuration, user, "salt", False)
generated = MUser.from_option(user.username, configuration.get(section, user.username))
assert generated.check_credentials(user.password, configuration.get("auth", "salt"))
User.configuration_create(configuration, user, "salt", False, False)
set_mock.assert_called_once_with("auth", "salt", pytest.helpers.anyvar(int))
write_mock.assert_called_once_with(configuration, False)
def test_configuration_create_with_plain_password(
@ -120,12 +103,11 @@ def test_configuration_create_with_plain_password(
"""
must set plain text password and user for the service
"""
section = Configuration.section_name("auth", user.access.value)
mocker.patch("pathlib.Path.open")
User.configuration_create(configuration, user, "salt", True)
User.configuration_create(configuration, user, "salt", True, False)
generated = MUser.from_option(user.username, configuration.get(section, user.username))
generated = MUser.from_option(user.username, user.password).hash_password("salt")
service = MUser.from_option(configuration.get("web", "username"), configuration.get("web", "password"))
assert generated.username == service.username
assert generated.check_credentials(service.password, configuration.get("auth", "salt"))
@ -174,12 +156,9 @@ def test_configuration_write_not_loaded(configuration: Configuration, mocker: Mo
"""
configuration.path = None
mocker.patch("pathlib.Path.open")
write_mock = mocker.patch("ahriman.core.configuration.Configuration.write")
chmod_mock = mocker.patch("pathlib.Path.chmod")
User.configuration_write(configuration, secure=True)
write_mock.assert_not_called()
chmod_mock.assert_not_called()
with pytest.raises(InitializeException):
User.configuration_write(configuration, secure=True)
def test_get_salt_read(configuration: Configuration) -> None:
@ -200,31 +179,6 @@ def test_get_salt_generate(configuration: Configuration) -> None:
assert len(salt) == 16
def test_user_clear(configuration: Configuration, user: MUser) -> None:
"""
must clear user from configuration
"""
section = Configuration.section_name("auth", user.access.value)
configuration.set_option(section, user.username, user.password)
User.user_clear(configuration, user)
assert configuration.get(section, user.username, fallback=None) is None
def test_user_clear_multiple_sections(configuration: Configuration, user: MUser) -> None:
"""
must clear user from configuration from all sections
"""
for role in UserAccess:
section = Configuration.section_name("auth", role.value)
configuration.set_option(section, user.username, user.password)
User.user_clear(configuration, user)
for role in UserAccess:
section = Configuration.section_name("auth", role.value)
assert configuration.get(section, user.username, fallback=None) is None
def test_user_create(args: argparse.Namespace, user: MUser) -> None:
"""
must create user

View File

@ -438,6 +438,38 @@ def test_subparsers_user_add_option_role(parser: argparse.ArgumentParser) -> Non
assert isinstance(args.role, UserAccess)
def test_subparsers_user_list(parser: argparse.ArgumentParser) -> None:
"""
user-list command must imply action, architecture, lock, no-report, password, quiet and unsafe
"""
args = parser.parse_args(["user-list"])
assert args.action == Action.List
assert args.architecture == [""]
assert args.lock is None
assert args.no_report
assert args.password is not None
assert args.quiet
assert args.unsafe
def test_subparsers_user_list_architecture(parser: argparse.ArgumentParser) -> None:
"""
user-list command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "user-list"])
assert args.architecture == [""]
def test_subparsers_user_list_option_role(parser: argparse.ArgumentParser) -> None:
"""
user-list command must convert role option to useraccess instance
"""
args = parser.parse_args(["user-list"])
assert isinstance(args.role, UserAccess)
args = parser.parse_args(["user-list", "--role", "write"])
assert isinstance(args.role, UserAccess)
def test_subparsers_user_remove(parser: argparse.ArgumentParser) -> None:
"""
user-remove command must imply action, architecture, lock, no-report, password, quiet, role and unsafe

View File

@ -3,14 +3,16 @@ import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from typing import Any, Type, TypeVar
from typing import Any, Dict, Type, TypeVar
from unittest.mock import MagicMock
from ahriman.core.auth.auth import Auth
from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.spawn import Spawn
from ahriman.core.status.watcher import Watcher
from ahriman.models.aur_package import AURPackage
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.package import Package
from ahriman.models.package_description import PackageDescription
from ahriman.models.repository_paths import RepositoryPaths
@ -48,6 +50,26 @@ def anyvar(cls: Type[T], strict: bool = False) -> T:
return AnyVar()
@pytest.helpers.register
def get_package_status(package: Package) -> Dict[str, Any]:
"""
helper to extract package status from package
:param package: package object
:return: simplified package status map (with only status and view)
"""
return {"status": BuildStatusEnum.Unknown.value, "package": package.view()}
@pytest.helpers.register
def get_package_status_extended(package: Package) -> Dict[str, Any]:
"""
helper to extract package status from package
:param package: package object
:return: full package status map (with timestamped build status and view)
"""
return {"status": BuildStatus().view(), "package": package.view()}
# generic fixtures
@pytest.fixture
def aur_package_ahriman() -> AURPackage:
@ -123,6 +145,18 @@ def configuration(resource_path_root: Path) -> Configuration:
return Configuration.from_path(path=path, architecture="x86_64", quiet=False)
@pytest.fixture
def database(configuration: Configuration) -> SQLite:
"""
database fixture
:param: configuration: configuration fixture
:return: database test instance
"""
database = SQLite.load(configuration)
yield database
database.path.unlink()
@pytest.fixture
def package_ahriman(package_description_ahriman: PackageDescription) -> Package:
"""
@ -267,12 +301,13 @@ def user() -> User:
@pytest.fixture
def watcher(configuration: Configuration, mocker: MockerFixture) -> Watcher:
def watcher(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> Watcher:
"""
package status watcher fixture
:param configuration: configuration fixture
:param database: database fixture
:param mocker: mocker object
:return: package status watcher test instance
"""
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
return Watcher("x86_64", configuration)
return Watcher("x86_64", configuration, database)

View File

@ -3,24 +3,27 @@ import pytest
from ahriman.core.auth.mapping import Mapping
from ahriman.core.auth.oauth import OAuth
from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
@pytest.fixture
def mapping(configuration: Configuration) -> Mapping:
def mapping(configuration: Configuration, database: SQLite) -> Mapping:
"""
auth provider fixture
:param configuration: configuration fixture
:param database: database fixture
:return: auth service instance
"""
return Mapping(configuration)
return Mapping(configuration, database)
@pytest.fixture
def oauth(configuration: Configuration) -> OAuth:
def oauth(configuration: Configuration, database: SQLite) -> OAuth:
"""
OAuth provider fixture
:param configuration: configuration fixture
:param database: database fixture
:return: OAuth2 service instance
"""
configuration.set("web", "address", "https://example.com")
return OAuth(configuration)
return OAuth(configuration, database)

View File

@ -1,10 +1,8 @@
import pytest
from ahriman.core.auth.auth import Auth
from ahriman.core.auth.mapping import Mapping
from ahriman.core.auth.oauth import OAuth
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import DuplicateUser
from ahriman.core.database.sqlite import SQLite
from ahriman.models.user import User
from ahriman.models.user_access import UserAccess
@ -17,88 +15,42 @@ def test_auth_control(auth: Auth) -> None:
assert "button" in auth.auth_control # I think it should be button
def test_load_dummy(configuration: Configuration) -> None:
def test_load_dummy(configuration: Configuration, database: SQLite) -> None:
"""
must load dummy validator if authorization is not enabled
"""
configuration.set_option("auth", "target", "disabled")
auth = Auth.load(configuration)
auth = Auth.load(configuration, database)
assert isinstance(auth, Auth)
def test_load_dummy_empty(configuration: Configuration) -> None:
def test_load_dummy_empty(configuration: Configuration, database: SQLite) -> None:
"""
must load dummy validator if no option set
"""
auth = Auth.load(configuration)
auth = Auth.load(configuration, database)
assert isinstance(auth, Auth)
def test_load_mapping(configuration: Configuration) -> None:
def test_load_mapping(configuration: Configuration, database: SQLite) -> None:
"""
must load mapping validator if option set
"""
configuration.set_option("auth", "target", "configuration")
auth = Auth.load(configuration)
auth = Auth.load(configuration, database)
assert isinstance(auth, Mapping)
def test_load_oauth(configuration: Configuration) -> None:
def test_load_oauth(configuration: Configuration, database: SQLite) -> None:
"""
must load OAuth2 validator if option set
"""
configuration.set_option("auth", "target", "oauth")
configuration.set_option("web", "address", "https://example.com")
auth = Auth.load(configuration)
auth = Auth.load(configuration, database)
assert isinstance(auth, OAuth)
def test_get_users(mapping: Auth, configuration: Configuration) -> None:
"""
must return valid user list
"""
user_write = User("user_write", "pwd_write", UserAccess.Write)
write_section = Configuration.section_name("auth", user_write.access.value)
configuration.set_option(write_section, user_write.username, user_write.password)
user_read = User("user_read", "pwd_read", UserAccess.Read)
read_section = Configuration.section_name("auth", user_read.access.value)
configuration.set_option(read_section, user_read.username, user_read.password)
user_read = User("user_read", "pwd_read", UserAccess.Read)
read_section = Configuration.section_name("auth", user_read.access.value)
configuration.set_option(read_section, user_read.username, user_read.password)
users = mapping.get_users(configuration)
expected = {user_write.username: user_write, user_read.username: user_read}
assert users == expected
def test_get_users_normalized(mapping: Auth, configuration: Configuration) -> None:
"""
must return user list with normalized usernames in keys
"""
user = User("UsEr", "pwd_read", UserAccess.Read)
read_section = Configuration.section_name("auth", user.access.value)
configuration.set_option(read_section, user.username, user.password)
users = mapping.get_users(configuration)
expected = user.username.lower()
assert expected in users
assert users[expected].username == expected
def test_get_users_duplicate(mapping: Auth, configuration: Configuration, user: User) -> None:
"""
must raise exception on duplicate username
"""
write_section = Configuration.section_name("auth", UserAccess.Write.value)
configuration.set_option(write_section, user.username, user.password)
read_section = Configuration.section_name("auth", UserAccess.Read.value)
configuration.set_option(read_section, user.username, user.password)
with pytest.raises(DuplicateUser):
mapping.get_users(configuration)
async def test_check_credentials(auth: Auth, user: User) -> None:
"""
must pass any credentials

View File

@ -1,15 +1,17 @@
from pytest_mock import MockerFixture
from ahriman.core.auth.mapping import Mapping
from ahriman.models.user import User
from ahriman.models.user_access import UserAccess
async def test_check_credentials(mapping: Mapping, user: User) -> None:
async def test_check_credentials(mapping: Mapping, user: User, mocker: MockerFixture) -> None:
"""
must return true for valid credentials
"""
current_password = user.password
user.password = user.hash_password(mapping.salt)
mapping._users[user.username] = user
user = user.hash_password(mapping.salt)
mocker.patch("ahriman.core.database.sqlite.SQLite.user_get", return_value=user)
assert await mapping.check_credentials(user.username, current_password)
# here password is hashed so it is invalid
assert not await mapping.check_credentials(user.username, user.password)
@ -31,19 +33,19 @@ async def test_check_credentials_unknown(mapping: Mapping, user: User) -> None:
assert not await mapping.check_credentials(user.username, user.password)
def test_get_user(mapping: Mapping, user: User) -> None:
def test_get_user(mapping: Mapping, user: User, mocker: MockerFixture) -> None:
"""
must return user from storage by username
"""
mapping._users[user.username] = user
mocker.patch("ahriman.core.database.sqlite.SQLite.user_get", return_value=user)
assert mapping.get_user(user.username) == user
def test_get_user_normalized(mapping: Mapping, user: User) -> None:
def test_get_user_normalized(mapping: Mapping, user: User, mocker: MockerFixture) -> None:
"""
must return user from storage by username case-insensitive
"""
mapping._users[user.username] = user
mocker.patch("ahriman.core.database.sqlite.SQLite.user_get", return_value=user)
assert mapping.get_user(user.username.upper()) == user
@ -54,20 +56,27 @@ def test_get_user_unknown(mapping: Mapping, user: User) -> None:
assert mapping.get_user(user.username) is None
async def test_known_username(mapping: Mapping, user: User) -> None:
async def test_known_username(mapping: Mapping, user: User, mocker: MockerFixture) -> None:
"""
must allow only known users
"""
mapping._users[user.username] = user
mocker.patch("ahriman.core.database.sqlite.SQLite.user_get", return_value=user)
assert await mapping.known_username(user.username)
async def test_known_username_unknown(mapping: Mapping, user: User, mocker: MockerFixture) -> None:
"""
must not allow only known users
"""
assert not await mapping.known_username(None)
mocker.patch("ahriman.core.database.sqlite.SQLite.user_get", return_value=None)
assert not await mapping.known_username(user.password)
async def test_verify_access(mapping: Mapping, user: User) -> None:
async def test_verify_access(mapping: Mapping, user: User, mocker: MockerFixture) -> None:
"""
must verify user access
"""
mapping._users[user.username] = user
mocker.patch("ahriman.core.database.sqlite.SQLite.user_get", return_value=user)
assert await mapping.verify_access(user.username, user.access, None)
assert not await mapping.verify_access(user.username, UserAccess.Write, None)

View File

@ -22,16 +22,25 @@ def test_add(mocker: MockerFixture) -> None:
exception=None, cwd=local, logger=pytest.helpers.anyvar(int))
def test_add_skip(mocker: MockerFixture) -> None:
"""
must skip addition of files to index if no fiels found
"""
mocker.patch("pathlib.Path.glob", return_value=[])
check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output")
Sources.add(Path("local"), "pattern1")
check_output_mock.assert_not_called()
def test_diff(mocker: MockerFixture) -> None:
"""
must calculate diff
"""
write_mock = mocker.patch("pathlib.Path.write_text")
check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output")
local = Path("local")
Sources.diff(local, Path("patch"))
write_mock.assert_called_once_with(pytest.helpers.anyvar(int))
assert Sources.diff(local)
check_output_mock.assert_called_once_with("git", "diff",
exception=None, cwd=local, logger=pytest.helpers.anyvar(int))
@ -142,51 +151,34 @@ def test_load(mocker: MockerFixture) -> None:
fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
patch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_apply")
Sources.load(Path("local"), "remote", Path("patches"))
Sources.load(Path("local"), "remote", "patch")
fetch_mock.assert_called_once_with(Path("local"), "remote")
patch_mock.assert_called_once_with(Path("local"), Path("patches"))
patch_mock.assert_called_once_with(Path("local"), "patch")
def test_load_no_patch(mocker: MockerFixture) -> None:
"""
must load packages sources correctly without patches
"""
mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
patch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_apply")
Sources.load(Path("local"), "remote", None)
patch_mock.assert_not_called()
def test_patch_apply(mocker: MockerFixture) -> None:
"""
must apply patches if any
"""
mocker.patch("pathlib.Path.is_dir", return_value=True)
glob_mock = mocker.patch("pathlib.Path.glob", return_value=[Path("01.patch"), Path("02.patch")])
check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output")
local = Path("local")
Sources.patch_apply(local, Path("patches"))
glob_mock.assert_called_once_with("*.patch")
check_output_mock.assert_has_calls([
mock.call("git", "apply", "--ignore-space-change", "--ignore-whitespace", "01.patch",
exception=None, cwd=local, logger=pytest.helpers.anyvar(int)),
mock.call("git", "apply", "--ignore-space-change", "--ignore-whitespace", "02.patch",
exception=None, cwd=local, logger=pytest.helpers.anyvar(int)),
])
def test_patch_apply_no_dir(mocker: MockerFixture) -> None:
"""
must not fail if no patches directory exists
"""
mocker.patch("pathlib.Path.is_dir", return_value=False)
glob_mock = mocker.patch("pathlib.Path.glob")
Sources.patch_apply(Path("local"), Path("patches"))
glob_mock.assert_not_called()
def test_patch_apply_no_patches(mocker: MockerFixture) -> None:
"""
must not fail if no patches exist
"""
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("pathlib.Path.glob", return_value=[])
check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output")
Sources.patch_apply(Path("local"), Path("patches"))
check_output_mock.assert_not_called()
Sources.patch_apply(local, "patches")
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)
)
def test_patch_create(mocker: MockerFixture) -> None:
@ -196,6 +188,15 @@ def test_patch_create(mocker: MockerFixture) -> None:
add_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.add")
diff_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.diff")
Sources.patch_create(Path("local"), Path("patch"), "glob")
Sources.patch_create(Path("local"), "glob")
add_mock.assert_called_once_with(Path("local"), "glob")
diff_mock.assert_called_once_with(Path("local"), Path("patch"))
diff_mock.assert_called_once_with(Path("local"))
def test_patch_create_with_newline(mocker: MockerFixture) -> None:
"""
created patch must have new line at the end
"""
mocker.patch("ahriman.core.build_tools.sources.Sources.add")
mocker.patch("ahriman.core.build_tools.sources.Sources.diff", return_value="diff")
assert Sources.patch_create(Path("local"), "glob").endswith("\n")

View File

@ -1,6 +1,8 @@
from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.core.build_tools.task import Task
from ahriman.core.database.sqlite import SQLite
def test_build(task_ahriman: Task, mocker: MockerFixture) -> None:
@ -8,11 +10,11 @@ def test_build(task_ahriman: Task, mocker: MockerFixture) -> None:
must build package
"""
check_output_mock = mocker.patch("ahriman.core.build_tools.task.Task._check_output")
task_ahriman.build()
task_ahriman.build(Path("ahriman"))
check_output_mock.assert_called()
def test_init_with_cache(task_ahriman: Task, mocker: MockerFixture) -> None:
def test_init_with_cache(task_ahriman: Task, database: SQLite, mocker: MockerFixture) -> None:
"""
must copy tree instead of fetch
"""
@ -20,5 +22,5 @@ def test_init_with_cache(task_ahriman: Task, mocker: MockerFixture) -> None:
mocker.patch("ahriman.core.build_tools.sources.Sources.load")
copytree_mock = mocker.patch("shutil.copytree")
task_ahriman.init(None)
task_ahriman.init(Path("ahriman"), database)
copytree_mock.assert_called_once() # we do not check full command here, sorry

View File

@ -0,0 +1,13 @@
import pytest
from sqlite3 import Connection
from unittest.mock import MagicMock
@pytest.fixture
def connection() -> Connection:
"""
mock object for sqlite3 connection
:return: sqlite3 connection test instance
"""
return MagicMock()

View File

@ -0,0 +1,33 @@
from pytest_mock import MockerFixture
from sqlite3 import Connection
from ahriman.core.configuration import Configuration
from ahriman.core.database.data import migrate_data
from ahriman.models.migration_result import MigrationResult
from ahriman.models.repository_paths import RepositoryPaths
def test_migrate_data_initial(connection: Connection, configuration: Configuration,
repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
"""
must perform initial migration
"""
packages = mocker.patch("ahriman.core.database.data.migrate_package_statuses")
users = mocker.patch("ahriman.core.database.data.migrate_users_data")
migrate_data(MigrationResult(old_version=0, new_version=900), connection, configuration, repository_paths)
packages.assert_called_once_with(connection, repository_paths)
users.assert_called_once_with(connection, configuration)
def test_migrate_data_skip(connection: Connection, configuration: Configuration,
repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
"""
must not migrate data if version is up-to-date
"""
packages = mocker.patch("ahriman.core.database.data.migrate_package_statuses")
users = mocker.patch("ahriman.core.database.data.migrate_users_data")
migrate_data(MigrationResult(old_version=900, new_version=900), connection, configuration, repository_paths)
packages.assert_not_called()
users.assert_not_called()

View File

@ -0,0 +1,43 @@
import pytest
from pytest_mock import MockerFixture
from sqlite3 import Connection
from unittest import mock
from ahriman.core.database.data import migrate_package_statuses
from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths
def test_migrate_package_statuses(connection: Connection, package_ahriman: Package, repository_paths: RepositoryPaths,
mocker: MockerFixture) -> None:
"""
must migrate packages to database
"""
response = {"packages": [pytest.helpers.get_package_status_extended(package_ahriman)]}
mocker.patch("pathlib.Path.is_file", return_value=True)
mocker.patch("pathlib.Path.open")
mocker.patch("json.load", return_value=response)
unlink_mock = mocker.patch("pathlib.Path.unlink")
migrate_package_statuses(connection, repository_paths)
unlink_mock.assert_called_once_with()
connection.execute.assert_has_calls([
mock.call(pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(int)),
mock.call(pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(int)),
])
connection.executemany.assert_has_calls([
mock.call(pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(int)),
])
connection.commit.assert_called_once_with()
def test_migrate_package_statuses_skip(connection: Connection, repository_paths: RepositoryPaths,
mocker: MockerFixture) -> None:
"""
must skip packages migration if no cache file found
"""
mocker.patch("pathlib.Path.is_file", return_value=False)
migrate_package_statuses(connection, repository_paths)
connection.commit.assert_not_called()

View File

@ -0,0 +1,53 @@
import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from sqlite3 import Connection
from ahriman.core.database.data import migrate_patches
from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths
def test_migrate_patches(connection: Connection, repository_paths: RepositoryPaths,
package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must perform migration for patches
"""
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("pathlib.Path.is_file", return_value=True)
iterdir_mock = mocker.patch("pathlib.Path.iterdir", return_value=[Path(package_ahriman.base)])
read_mock = mocker.patch("pathlib.Path.read_text", return_value="patch")
migrate_patches(connection, repository_paths)
iterdir_mock.assert_called_once_with()
read_mock.assert_called_once_with(encoding="utf8")
connection.execute.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(int))
connection.commit.assert_called_once_with()
def test_migrate_patches_skip(connection: Connection, repository_paths: RepositoryPaths,
mocker: MockerFixture) -> None:
"""
must skip patches migration in case if no root found
"""
mocker.patch("pathlib.Path.is_dir", return_value=False)
iterdir_mock = mocker.patch("pathlib.Path.iterdir")
migrate_patches(connection, repository_paths)
iterdir_mock.assert_not_called()
def test_migrate_patches_no_patch(connection: Connection, repository_paths: RepositoryPaths,
package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must skip package if no match found
"""
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("pathlib.Path.is_file", return_value=False)
iterdir_mock = mocker.patch("pathlib.Path.iterdir", return_value=[Path(package_ahriman.base)])
read_mock = mocker.patch("pathlib.Path.read_text")
migrate_patches(connection, repository_paths)
iterdir_mock.assert_called_once_with()
read_mock.assert_not_called()

View File

@ -0,0 +1,22 @@
import pytest
from sqlite3 import Connection
from unittest import mock
from ahriman.core.configuration import Configuration
from ahriman.core.database.data import migrate_users_data
def test_migrate_users_data(connection: Connection, configuration: Configuration) -> None:
"""
must users to database
"""
configuration.set_option("auth:read", "user1", "password1")
configuration.set_option("auth:write", "user2", "password2")
migrate_users_data(connection, configuration)
connection.execute.assert_has_calls([
mock.call(pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(int)),
mock.call(pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(int)),
])
connection.commit.assert_called_once_with()

View File

@ -0,0 +1,15 @@
import pytest
from sqlite3 import Connection
from ahriman.core.database.migrations import Migrations
@pytest.fixture
def migrations(connection: Connection) -> Migrations:
"""
fixture for migrations object
:param connection: sqlite connection fixture
:return: migrations test instance
"""
return Migrations(connection)

View File

@ -0,0 +1,8 @@
from ahriman.core.database.migrations.m000_initial import steps
def test_migration_initial() -> None:
"""
migration must not be empty
"""
assert steps

View File

@ -0,0 +1,109 @@
import pytest
from pytest_mock import MockerFixture
from sqlite3 import Connection
from unittest import mock
from unittest.mock import MagicMock
from ahriman.core.database.migrations import Migrations
from ahriman.models.migration import Migration
from ahriman.models.migration_result import MigrationResult
def test_migrate(connection: Connection, mocker: MockerFixture) -> None:
"""
must perform migrations
"""
run_mock = mocker.patch("ahriman.core.database.migrations.Migrations.run")
Migrations.migrate(connection)
run_mock.assert_called_once_with()
def test_migrations(migrations: Migrations) -> None:
"""
must retrieve migrations
"""
assert migrations.migrations()
def test_run_skip(migrations: Migrations, mocker: MockerFixture) -> None:
"""
must skip migration if version is the same
"""
mocker.patch.object(MigrationResult, "is_outdated", False)
migrations.run()
migrations.connection.cursor.assert_not_called()
def test_run(migrations: Migrations, mocker: MockerFixture) -> None:
"""
must run migration
"""
cursor = MagicMock()
mocker.patch("ahriman.core.database.migrations.Migrations.user_version", return_value=0)
mocker.patch("ahriman.core.database.migrations.Migrations.migrations",
return_value=[Migration(0, "test", ["select 1"])])
migrations.connection.cursor.return_value = cursor
validate_mock = mocker.patch("ahriman.models.migration_result.MigrationResult.validate")
migrations.run()
validate_mock.assert_called_once_with()
cursor.execute.assert_has_calls([
mock.call("begin exclusive"),
mock.call("select 1"),
mock.call("pragma user_version = 1"),
mock.call("commit"),
])
cursor.close.assert_called_once_with()
def test_run_migration_exception(migrations: Migrations, mocker: MockerFixture) -> None:
"""
must rollback and close cursor on exception during migration
"""
cursor = MagicMock()
mocker.patch("logging.Logger.info", side_effect=Exception())
mocker.patch("ahriman.core.database.migrations.Migrations.user_version", return_value=0)
mocker.patch("ahriman.core.database.migrations.Migrations.migrations",
return_value=[Migration(0, "test", ["select 1"])])
mocker.patch("ahriman.models.migration_result.MigrationResult.validate")
migrations.connection.cursor.return_value = cursor
with pytest.raises(Exception):
migrations.run()
cursor.execute.assert_has_calls([
mock.call("begin exclusive"),
mock.call("rollback"),
])
cursor.close.assert_called_once_with()
def test_run_sql_exception(migrations: Migrations, mocker: MockerFixture) -> None:
"""
must close cursor on general migration error
"""
cursor = MagicMock()
cursor.execute.side_effect = Exception()
mocker.patch("ahriman.core.database.migrations.Migrations.user_version", return_value=0)
mocker.patch("ahriman.core.database.migrations.Migrations.migrations",
return_value=[Migration(0, "test", ["select 1"])])
mocker.patch("ahriman.models.migration_result.MigrationResult.validate")
migrations.connection.cursor.return_value = cursor
with pytest.raises(Exception):
migrations.run()
cursor.close.assert_called_once_with()
def test_user_version(migrations: Migrations) -> None:
"""
must correctly extract current migration version
"""
cursor = MagicMock()
cursor.fetchone.return_value = {"user_version": 42}
migrations.connection.execute.return_value = cursor
version = migrations.user_version()
migrations.connection.execute.assert_called_once_with("pragma user_version")
assert version == 42

View File

@ -0,0 +1,97 @@
from ahriman.core.database.sqlite import SQLite
from ahriman.models.user import User
from ahriman.models.user_access import UserAccess
def test_user_get_update(database: SQLite, user: User) -> None:
"""
must retrieve user from the database
"""
database.user_update(user)
assert database.user_get(user.username) == user
def test_user_list(database: SQLite, user: User) -> None:
"""
must return all users
"""
database.user_update(user)
database.user_update(User(user.password, user.username, user.access))
users = database.user_list(None, None)
assert len(users) == 2
assert user in users
assert User(user.password, user.username, user.access) in users
def test_user_list_filter_by_username(database: SQLite) -> None:
"""
must return users filtered by its id
"""
first = User("1", "", UserAccess.Read)
second = User("2", "", UserAccess.Write)
third = User("3", "", UserAccess.Read)
database.user_update(first)
database.user_update(second)
database.user_update(third)
assert database.user_list("1", None) == [first]
assert database.user_list("2", None) == [second]
assert database.user_list("3", None) == [third]
def test_user_list_filter_by_access(database: SQLite) -> None:
"""
must return users filtered by its access
"""
first = User("1", "", UserAccess.Read)
second = User("2", "", UserAccess.Write)
third = User("3", "", UserAccess.Read)
database.user_update(first)
database.user_update(second)
database.user_update(third)
users = database.user_list(None, UserAccess.Read)
assert len(users) == 2
assert first in users
assert third in users
def test_user_list_filter_by_username_access(database: SQLite) -> None:
"""
must return users filtered by its access and username
"""
first = User("1", "", UserAccess.Read)
second = User("2", "", UserAccess.Write)
third = User("3", "", UserAccess.Read)
database.user_update(first)
database.user_update(second)
database.user_update(third)
assert database.user_list("1", UserAccess.Read) == [first]
assert not database.user_list("1", UserAccess.Write)
def test_user_remove_update(database: SQLite, user: User) -> None:
"""
must remove user from the database
"""
database.user_update(user)
database.user_remove(user.username)
assert database.user_get(user.username) is None
def test_user_update(database: SQLite, user: User) -> None:
"""
must update user in the database
"""
database.user_update(user)
assert database.user_get(user.username) == user
new_user = user.hash_password("salt")
new_user.access = UserAccess.Write
database.user_update(new_user)
assert database.user_get(new_user.username) == new_user

View File

@ -0,0 +1,45 @@
from ahriman.core.database.sqlite import SQLite
from ahriman.models.package import Package
def test_build_queue_insert_clear(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must clear all packages from queue
"""
database.build_queue_insert(package_ahriman)
database.build_queue_insert(package_python_schedule)
database.build_queue_clear(None)
assert not database.build_queue_get()
def test_build_queue_insert_clear_specific(database: SQLite, package_ahriman: Package,
package_python_schedule: Package) -> None:
"""
must remove only specified package from the queue
"""
database.build_queue_insert(package_ahriman)
database.build_queue_insert(package_python_schedule)
database.build_queue_clear(package_python_schedule.base)
assert database.build_queue_get() == [package_ahriman]
def test_build_queue_insert_get(database: SQLite, package_ahriman: Package) -> None:
"""
must insert and get package from the database
"""
database.build_queue_insert(package_ahriman)
assert database.build_queue_get() == [package_ahriman]
def test_build_queue_insert(database: SQLite, package_ahriman: Package) -> None:
"""
must update user in the database
"""
database.build_queue_insert(package_ahriman)
assert database.build_queue_get() == [package_ahriman]
package_ahriman.version = "42"
database.build_queue_insert(package_ahriman)
assert database.build_queue_get() == [package_ahriman]

View File

@ -0,0 +1,39 @@
import sqlite3
from pytest_mock import MockerFixture
from unittest.mock import MagicMock
from ahriman.core.database.sqlite import SQLite
def test_factory(database: SQLite) -> None:
"""
must convert response to dictionary
"""
result = database.with_connection(lambda conn: conn.execute("select 1 as result").fetchone())
assert isinstance(result, dict)
assert result["result"] == 1
def test_with_connection(database: SQLite, mocker: MockerFixture) -> None:
"""
must run query inside connection
"""
connection_mock = MagicMock()
connect_mock = mocker.patch("sqlite3.connect", return_value=connection_mock)
database.with_connection(lambda conn: conn.execute("select 1"))
connect_mock.assert_called_once_with(database.path, detect_types=sqlite3.PARSE_DECLTYPES)
connection_mock.__enter__().commit.assert_not_called()
def test_with_connection_with_commit(database: SQLite, mocker: MockerFixture) -> None:
"""
must run query inside connection and commit after
"""
connection_mock = MagicMock()
connection_mock.commit.return_value = 42
mocker.patch("sqlite3.connect", return_value=connection_mock)
database.with_connection(lambda conn: conn.execute("select 1"), commit=True)
connection_mock.__enter__().commit.assert_called_once_with()

View File

@ -0,0 +1,168 @@
import pytest
from pytest_mock import MockerFixture
from sqlite3 import Connection
from unittest import mock
from ahriman.core.database.sqlite import SQLite
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.package import Package
def test_package_remove_package_base(database: SQLite, connection: Connection) -> None:
"""
must remove package base
"""
database._package_remove_package_base(connection, "package")
connection.execute.assert_has_calls([
mock.call(pytest.helpers.anyvar(str, strict=True), {"package_base": "package"}),
mock.call(pytest.helpers.anyvar(str, strict=True), {"package_base": "package"}),
])
def test_package_remove_packages(database: SQLite, connection: Connection, package_ahriman: Package) -> None:
"""
must remove packages belong to base
"""
database._package_remove_packages(connection, package_ahriman.base, package_ahriman.packages.keys())
connection.execute.assert_called_once_with(
pytest.helpers.anyvar(str, strict=True), {"package_base": package_ahriman.base})
connection.executemany.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), [])
def test_package_update_insert_base(database: SQLite, connection: Connection, package_ahriman: Package) -> None:
"""
must insert base package
"""
database._package_update_insert_base(connection, package_ahriman)
connection.execute.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(int))
def test_package_update_insert_packages(database: SQLite, connection: Connection, package_ahriman: Package) -> None:
"""
must insert single packages
"""
database._package_update_insert_packages(connection, package_ahriman)
connection.executemany(pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(int))
def test_package_update_insert_status(database: SQLite, connection: Connection, package_ahriman: Package) -> None:
"""
must insert single package status
"""
database._package_update_insert_status(connection, package_ahriman.base, BuildStatus())
connection.execute(pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(int))
def test_packages_get_select_package_bases(database: SQLite, connection: Connection) -> None:
"""
must select all bases
"""
database._packages_get_select_package_bases(connection)
connection.execute(pytest.helpers.anyvar(str, strict=True))
def test_packages_get_select_packages(database: SQLite, connection: Connection, package_ahriman: Package) -> None:
"""
must select all packages
"""
database._packages_get_select_packages(connection, {package_ahriman.base: package_ahriman})
connection.execute(pytest.helpers.anyvar(str, strict=True))
def test_packages_get_select_packages_skip(database: SQLite, connection: Connection, package_ahriman: Package) -> None:
"""
must skip unknown packages
"""
view = {"package_base": package_ahriman.base}
for package, properties in package_ahriman.packages.items():
view.update({"package": package})
view.update(properties.view())
connection.execute.return_value = [{"package_base": "random name"}, view]
database._packages_get_select_packages(connection, {package_ahriman.base: package_ahriman})
def test_packages_get_select_statuses(database: SQLite, connection: Connection) -> None:
"""
must select all statuses
"""
database._packages_get_select_statuses(connection)
connection.execute(pytest.helpers.anyvar(str, strict=True))
def test_package_remove(database: SQLite, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must totally remove package from the database
"""
remove_package_mock = mocker.patch("ahriman.core.database.sqlite.SQLite._package_remove_package_base")
remove_packages_mock = mocker.patch("ahriman.core.database.sqlite.SQLite._package_remove_packages")
database.package_remove(package_ahriman.base)
remove_package_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman.base)
remove_packages_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman.base, [])
def test_package_update(database: SQLite, package_ahriman: Package, mocker: MockerFixture):
"""
must update package status
"""
status = BuildStatus()
insert_base_mock = mocker.patch("ahriman.core.database.sqlite.SQLite._package_update_insert_base")
insert_status_mock = mocker.patch("ahriman.core.database.sqlite.SQLite._package_update_insert_status")
insert_packages_mock = mocker.patch("ahriman.core.database.sqlite.SQLite._package_update_insert_packages")
remove_packages_mock = mocker.patch("ahriman.core.database.sqlite.SQLite._package_remove_packages")
database.package_update(package_ahriman, status)
insert_base_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman)
insert_status_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman.base, status)
insert_packages_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman)
remove_packages_mock.assert_called_once_with(
pytest.helpers.anyvar(int), package_ahriman.base, package_ahriman.packages.keys())
def test_packages_get(database: SQLite, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must return all packages
"""
select_bases_mock = mocker.patch("ahriman.core.database.sqlite.SQLite._packages_get_select_package_bases",
return_value={package_ahriman.base: package_ahriman})
select_packages_mock = mocker.patch("ahriman.core.database.sqlite.SQLite._packages_get_select_packages")
select_statuses_mock = mocker.patch("ahriman.core.database.sqlite.SQLite._packages_get_select_statuses")
database.packages_get()
select_bases_mock.assert_called_once_with(pytest.helpers.anyvar(int))
select_statuses_mock.assert_called_once_with(pytest.helpers.anyvar(int))
select_packages_mock.assert_called_once_with(pytest.helpers.anyvar(int), {package_ahriman.base: package_ahriman})
def test_package_update_get(database: SQLite, package_ahriman: Package) -> None:
"""
must insert and retrieve package
"""
status = BuildStatus()
database.package_update(package_ahriman, status)
assert next((db_package, db_status)
for db_package, db_status in database.packages_get()
if db_package.base == package_ahriman.base) == (package_ahriman, status)
def test_package_update_remove_get(database: SQLite, package_ahriman: Package) -> None:
"""
must insert, remove and retrieve package
"""
status = BuildStatus()
database.package_update(package_ahriman, status)
database.package_remove(package_ahriman.base)
assert not database.packages_get()
def test_package_update_update(database: SQLite, package_ahriman: Package) -> None:
"""
must perform update for existing package
"""
database.package_update(package_ahriman, BuildStatus())
database.package_update(package_ahriman, BuildStatus(BuildStatusEnum.Failed))
assert next(db_status.status
for db_package, db_status in database.packages_get()
if db_package.base == package_ahriman.base) == BuildStatusEnum.Failed

View File

@ -0,0 +1,55 @@
from ahriman.core.database.sqlite import SQLite
from ahriman.models.package import Package
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()
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"}
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")
assert database.patches_list(package_ahriman.base) == {package_ahriman.base: "patch1"}
assert database.patches_list(package_python_schedule.base) == {package_python_schedule.base: "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)
assert database.patches_get(package_ahriman.base) is None
database.patches_insert(package_python_schedule.base, "patch_2")
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, "patch_2")
assert database.patches_get(package_ahriman.base) == "patch_2"

View File

@ -0,0 +1,28 @@
import pytest
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
def test_load(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must correctly load instance
"""
init_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.init")
SQLite.load(configuration)
init_mock.assert_called_once_with(configuration)
def test_init(database: SQLite, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run migrations on init
"""
migrate_schema_mock = mocker.patch("ahriman.core.database.migrations.Migrations.migrate")
migrate_data_mock = mocker.patch("ahriman.core.database.sqlite.migrate_data")
database.init(configuration)
migrate_schema_mock.assert_called_once_with(pytest.helpers.anyvar(int))
migrate_data_mock.assert_called_once_with(
pytest.helpers.anyvar(int), pytest.helpers.anyvar(int), configuration, configuration.repository_paths)

View File

@ -6,9 +6,11 @@ from ahriman.core.formatters.package_printer import PackagePrinter
from ahriman.core.formatters.status_printer import StatusPrinter
from ahriman.core.formatters.string_printer import StringPrinter
from ahriman.core.formatters.update_printer import UpdatePrinter
from ahriman.core.formatters.user_printer import UserPrinter
from ahriman.models.aur_package import AURPackage
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
from ahriman.models.user import User
@pytest.fixture
@ -65,3 +67,13 @@ def update_printer(package_ahriman: Package) -> UpdatePrinter:
:return: build status printer test instance
"""
return UpdatePrinter(package_ahriman, None)
@pytest.fixture
def user_printer(user: User) -> UserPrinter:
"""
fixture for user printer
:param user: user fixture
:return: user printer test instance
"""
return UserPrinter(user)

View File

@ -0,0 +1,15 @@
from ahriman.core.formatters.user_printer import UserPrinter
def test_properties(user_printer: UserPrinter) -> None:
"""
must return non empty properties list
"""
assert user_printer.properties()
def test_title(user_printer: UserPrinter) -> None:
"""
must return non empty title
"""
assert user_printer.title() is not None

View File

@ -3,6 +3,7 @@ import pytest
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.repository import Repository
from ahriman.core.repository.cleaner import Cleaner
from ahriman.core.repository.executor import Executor
@ -11,68 +12,71 @@ from ahriman.core.repository.update_handler import UpdateHandler
@pytest.fixture
def cleaner(configuration: Configuration, mocker: MockerFixture) -> Cleaner:
def cleaner(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> Cleaner:
"""
fixture for cleaner
:param configuration: configuration fixture
:param database: database fixture
:param mocker: mocker object
:return: cleaner test instance
"""
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
return Cleaner("x86_64", configuration, no_report=True, unsafe=False)
return Cleaner("x86_64", configuration, database, no_report=True, unsafe=False)
@pytest.fixture
def executor(configuration: Configuration, mocker: MockerFixture) -> Executor:
def executor(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> Executor:
"""
fixture for executor
:param configuration: configuration fixture
:param database: database fixture
:param mocker: mocker object
:return: executor test instance
"""
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_build")
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_cache")
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_chroot")
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_manual")
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_packages")
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_queue")
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
return Executor("x86_64", configuration, no_report=True, unsafe=False)
return Executor("x86_64", configuration, database, no_report=True, unsafe=False)
@pytest.fixture
def repository(configuration: Configuration, mocker: MockerFixture) -> Repository:
def repository(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> Repository:
"""
fixture for repository
:param configuration: configuration fixture
:param database: database fixture
:param mocker: mocker object
:return: repository test instance
"""
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
return Repository("x86_64", configuration, no_report=True, unsafe=False)
return Repository("x86_64", configuration, database, no_report=True, unsafe=False)
@pytest.fixture
def properties(configuration: Configuration) -> Properties:
def properties(configuration: Configuration, database: SQLite) -> Properties:
"""
fixture for properties
:param configuration: configuration fixture
:param database: database fixture
:return: properties test instance
"""
return Properties("x86_64", configuration, no_report=True, unsafe=False)
return Properties("x86_64", configuration, database, no_report=True, unsafe=False)
@pytest.fixture
def update_handler(configuration: Configuration, mocker: MockerFixture) -> UpdateHandler:
def update_handler(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> UpdateHandler:
"""
fixture for update handler
:param configuration: configuration fixture
:param database: database fixture
:param mocker: mocker object
:return: update handler test instance
"""
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_build")
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_cache")
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_chroot")
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_manual")
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_packages")
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_queue")
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
return UpdateHandler("x86_64", configuration, no_report=True, unsafe=False)
return UpdateHandler("x86_64", configuration, database, no_report=True, unsafe=False)

View File

@ -36,15 +36,6 @@ def test_packages_built(cleaner: Cleaner) -> None:
cleaner.packages_built()
def test_clear_build(cleaner: Cleaner, mocker: MockerFixture) -> None:
"""
must remove directories with sources
"""
_mock_clear(mocker)
cleaner.clear_build()
_mock_clear_check()
def test_clear_cache(cleaner: Cleaner, mocker: MockerFixture) -> None:
"""
must remove every cached sources
@ -63,15 +54,6 @@ def test_clear_chroot(cleaner: Cleaner, mocker: MockerFixture) -> None:
_mock_clear_check()
def test_clear_manual(cleaner: Cleaner, mocker: MockerFixture) -> None:
"""
must clear directory with manual packages
"""
_mock_clear(mocker)
cleaner.clear_manual()
_mock_clear_check()
def test_clear_packages(cleaner: Cleaner, mocker: MockerFixture) -> None:
"""
must delete built packages
@ -84,10 +66,10 @@ def test_clear_packages(cleaner: Cleaner, mocker: MockerFixture) -> None:
Path.unlink.assert_has_calls([mock.call(), mock.call(), mock.call()])
def test_clear_patches(cleaner: Cleaner, mocker: MockerFixture) -> None:
def test_clear_queue(cleaner: Cleaner, mocker: MockerFixture) -> None:
"""
must clear directory with patches
must clear queued packages from the database
"""
_mock_clear(mocker)
cleaner.clear_patches()
_mock_clear_check()
clear_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.build_queue_clear")
cleaner.clear_queue()
clear_mock.assert_called_once_with(None)

View File

@ -41,9 +41,6 @@ def test_process_build(executor: Executor, package_ahriman: Package, mocker: Moc
move_mock.assert_called_once_with(Path(package_ahriman.base), executor.paths.packages / package_ahriman.base)
# must update status
status_client_mock.assert_called_once_with(package_ahriman.base)
# must clear directory
from ahriman.core.repository.cleaner import Cleaner
Cleaner.clear_build.assert_called_once_with()
def test_process_build_failure(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:

View File

@ -1,51 +1,52 @@
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.exceptions import UnsafeRun
from ahriman.core.repository.properties import Properties
from ahriman.core.status.web_client import WebClient
def test_create_tree_on_load(configuration: Configuration, mocker: MockerFixture) -> None:
def test_create_tree_on_load(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> None:
"""
must create tree on load
"""
mocker.patch("ahriman.core.repository.properties.check_user")
tree_create_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
Properties("x86_64", configuration, True, False)
Properties("x86_64", configuration, database, True, False)
tree_create_mock.assert_called_once_with()
def test_create_tree_on_load_unsafe(configuration: Configuration, mocker: MockerFixture) -> None:
def test_create_tree_on_load_unsafe(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> None:
"""
must not create tree on load in case if user differs from the root owner
"""
mocker.patch("ahriman.core.repository.properties.check_user", side_effect=UnsafeRun(0, 1))
tree_create_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
Properties("x86_64", configuration, True, False)
Properties("x86_64", configuration, database, True, False)
tree_create_mock.assert_not_called()
def test_create_dummy_report_client(configuration: Configuration, mocker: MockerFixture) -> None:
def test_create_dummy_report_client(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> None:
"""
must create dummy report client if report is disabled
"""
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
load_mock = mocker.patch("ahriman.core.status.client.Client.load")
properties = Properties("x86_64", configuration, True, False)
properties = Properties("x86_64", configuration, database, True, False)
load_mock.assert_not_called()
assert not isinstance(properties.reporter, WebClient)
def test_create_full_report_client(configuration: Configuration, mocker: MockerFixture) -> None:
def test_create_full_report_client(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> None:
"""
must create load report client if report is enabled
"""
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
load_mock = mocker.patch("ahriman.core.status.client.Client.load")
Properties("x86_64", configuration, False, False)
Properties("x86_64", configuration, database, False, False)
load_mock.assert_called_once_with(configuration)

View File

@ -168,7 +168,7 @@ def test_updates_manual_clear(update_handler: UpdateHandler, mocker: MockerFixtu
update_handler.updates_manual()
from ahriman.core.repository.cleaner import Cleaner
Cleaner.clear_manual.assert_called_once_with()
Cleaner.clear_queue.assert_called_once_with()
def test_updates_manual_status_known(update_handler: UpdateHandler, package_ahriman: Package,
@ -176,9 +176,8 @@ def test_updates_manual_status_known(update_handler: UpdateHandler, package_ahri
"""
must create record for known package via reporter
"""
mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.base])
mocker.patch("ahriman.core.database.sqlite.SQLite.build_queue_get", return_value=[package_ahriman])
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman])
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_pending")
update_handler.updates_manual()
@ -190,9 +189,8 @@ def test_updates_manual_status_unknown(update_handler: UpdateHandler, package_ah
"""
must create record for unknown package via reporter
"""
mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.base])
mocker.patch("ahriman.core.database.sqlite.SQLite.build_queue_get", return_value=[package_ahriman])
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[])
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_unknown")
update_handler.updates_manual()
@ -204,8 +202,6 @@ def test_updates_manual_with_failures(update_handler: UpdateHandler, package_ahr
"""
must process manual through the packages with failure
"""
mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.base])
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[])
mocker.patch("ahriman.models.package.Package.load", side_effect=Exception())
mocker.patch("ahriman.core.database.sqlite.SQLite.build_queue_get", side_effect=Exception())
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman])
assert update_handler.updates_manual() == []

View File

@ -1,33 +1,8 @@
import pytest
from typing import Any, Dict
from ahriman.core.configuration import Configuration
from ahriman.core.status.client import Client
from ahriman.core.status.web_client import WebClient
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.package import Package
# helpers
@pytest.helpers.register
def get_package_status(package: Package) -> Dict[str, Any]:
"""
helper to extract package status from package
:param package: package object
:return: simplified package status map (with only status and view)
"""
return {"status": BuildStatusEnum.Unknown.value, "package": package.view()}
@pytest.helpers.register
def get_package_status_extended(package: Package) -> Dict[str, Any]:
"""
helper to extract package status from package
:param package: package object
:return: full package status map (with timestamped build status and view)
"""
return {"status": BuildStatus().view(), "package": package.view()}
# fixtures
@ -47,5 +22,5 @@ def web_client(configuration: Configuration) -> WebClient:
:param configuration: configuration fixture
:return: web client test instance
"""
configuration.set("web", "port", 8080)
configuration.set("web", "port", "8080")
return WebClient(configuration)

View File

@ -61,13 +61,6 @@ def test_get_self(client: Client) -> None:
assert client.get_self().status == BuildStatusEnum.Unknown
def test_reload_auth(client: Client) -> None:
"""
must process auth reload without errors
"""
client.reload_auth()
def test_remove(client: Client, package_ahriman: Package) -> None:
"""
must process remove without errors

View File

@ -6,6 +6,7 @@ from pytest_mock import MockerFixture
from unittest.mock import PropertyMock
from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.exceptions import UnknownPackage
from ahriman.core.status.watcher import Watcher
from ahriman.core.status.web_client import WebClient
@ -13,7 +14,7 @@ from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.package import Package
def test_force_no_report(configuration: Configuration, mocker: MockerFixture) -> None:
def test_force_no_report(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> None:
"""
must force dummy report client
"""
@ -21,122 +22,12 @@ def test_force_no_report(configuration: Configuration, mocker: MockerFixture) ->
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
load_mock = mocker.patch("ahriman.core.status.client.Client.load")
watcher = Watcher("x86_64", configuration)
watcher = Watcher("x86_64", configuration, database)
load_mock.assert_not_called()
assert not isinstance(watcher.repository.reporter, WebClient)
def test_cache_load(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must load state from cache
"""
response = {"packages": [pytest.helpers.get_package_status_extended(package_ahriman)]}
mocker.patch("pathlib.Path.is_file", return_value=True)
mocker.patch("pathlib.Path.open")
mocker.patch("json.load", return_value=response)
watcher.known = {package_ahriman.base: (None, None)}
watcher._cache_load()
package, status = watcher.known[package_ahriman.base]
assert package == package_ahriman
assert status.status == BuildStatusEnum.Unknown
def test_cache_load_json_error(watcher: Watcher, mocker: MockerFixture) -> None:
"""
must not fail on json errors
"""
mocker.patch("pathlib.Path.is_file", return_value=True)
mocker.patch("pathlib.Path.open")
mocker.patch("json.load", side_effect=Exception())
watcher._cache_load()
assert not watcher.known
def test_cache_load_no_file(watcher: Watcher, mocker: MockerFixture) -> None:
"""
must not fail on missing file
"""
mocker.patch("pathlib.Path.is_file", return_value=False)
watcher._cache_load()
assert not watcher.known
def test_cache_load_package_load_error(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must not fail on json errors
"""
response = {"packages": [pytest.helpers.get_package_status_extended(package_ahriman)]}
mocker.patch("pathlib.Path.is_file", return_value=True)
mocker.patch("pathlib.Path.open")
mocker.patch("ahriman.models.package.Package.from_json", side_effect=Exception())
mocker.patch("json.load", return_value=response)
watcher._cache_load()
assert not watcher.known
def test_cache_load_unknown(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must not load unknown package
"""
response = {"packages": [pytest.helpers.get_package_status_extended(package_ahriman)]}
mocker.patch("pathlib.Path.is_file", return_value=True)
mocker.patch("pathlib.Path.open")
mocker.patch("json.load", return_value=response)
watcher._cache_load()
assert not watcher.known
def test_cache_save(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must save state to cache
"""
mocker.patch("pathlib.Path.open")
json_mock = mocker.patch("json.dump")
watcher.known = {package_ahriman.base: (package_ahriman, BuildStatus())}
watcher._cache_save()
json_mock.assert_called_once_with(pytest.helpers.anyvar(int), pytest.helpers.anyvar(int))
def test_cache_save_failed(watcher: Watcher, mocker: MockerFixture) -> None:
"""
must not fail on dumping packages
"""
mocker.patch("pathlib.Path.open")
mocker.patch("json.dump", side_effect=Exception())
watcher._cache_save()
def test_cache_save_load(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must save state to cache which can be loaded later
"""
dump_file = Path(tempfile.mktemp()) # nosec
mocker.patch("ahriman.core.status.watcher.Watcher.cache_path",
new_callable=PropertyMock, return_value=dump_file)
known_current = {package_ahriman.base: (package_ahriman, BuildStatus())}
watcher.known = known_current
watcher._cache_save()
watcher.known = {package_ahriman.base: (None, None)}
watcher._cache_load()
assert watcher.known == known_current
dump_file.unlink()
def test_get(watcher: Watcher, package_ahriman: Package) -> None:
"""
must return package status
@ -160,7 +51,7 @@ def test_load(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture)
must correctly load packages
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman])
cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_load")
cache_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.packages_get")
watcher.load()
cache_mock.assert_called_once_with()
@ -173,9 +64,10 @@ def test_load_known(watcher: Watcher, package_ahriman: Package, mocker: MockerFi
"""
must correctly load packages with known statuses
"""
status = BuildStatus(BuildStatusEnum.Success)
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman])
mocker.patch("ahriman.core.status.watcher.Watcher._cache_load")
watcher.known = {package_ahriman.base: (package_ahriman, BuildStatus(BuildStatusEnum.Success))}
mocker.patch("ahriman.core.database.sqlite.SQLite.packages_get", return_value=[(package_ahriman, status)])
watcher.known = {package_ahriman.base: (package_ahriman, status)}
watcher.load()
_, status = watcher.known[package_ahriman.base]
@ -186,32 +78,32 @@ def test_remove(watcher: Watcher, package_ahriman: Package, mocker: MockerFixtur
"""
must remove package base
"""
cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_save")
cache_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.package_remove")
watcher.known = {package_ahriman.base: (package_ahriman, BuildStatus())}
watcher.remove(package_ahriman.base)
assert not watcher.known
cache_mock.assert_called_once_with()
cache_mock.assert_called_once_with(package_ahriman.base)
def test_remove_unknown(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must not fail on unknown base removal
"""
cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_save")
cache_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.package_remove")
watcher.remove(package_ahriman.base)
cache_mock.assert_called_once_with()
cache_mock.assert_called_once_with(package_ahriman.base)
def test_update(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must update package status
"""
cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_save")
cache_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.package_update")
watcher.update(package_ahriman.base, BuildStatusEnum.Unknown, package_ahriman)
cache_mock.assert_called_once_with()
cache_mock.assert_called_once_with(package_ahriman, pytest.helpers.anyvar(int))
package, status = watcher.known[package_ahriman.base]
assert package == package_ahriman
assert status.status == BuildStatusEnum.Unknown
@ -221,25 +113,22 @@ def test_update_ping(watcher: Watcher, package_ahriman: Package, mocker: MockerF
"""
must update package status only for known package
"""
cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_save")
cache_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.package_update")
watcher.known = {package_ahriman.base: (package_ahriman, BuildStatus())}
watcher.update(package_ahriman.base, BuildStatusEnum.Success, None)
cache_mock.assert_called_once_with()
cache_mock.assert_called_once_with(package_ahriman, pytest.helpers.anyvar(int))
package, status = watcher.known[package_ahriman.base]
assert package == package_ahriman
assert status.status == BuildStatusEnum.Success
def test_update_unknown(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
def test_update_unknown(watcher: Watcher, package_ahriman: Package) -> None:
"""
must fail on unknown package status update only
"""
cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_save")
with pytest.raises(UnknownPackage):
watcher.update(package_ahriman.base, BuildStatusEnum.Unknown, None)
cache_mock.assert_called_once_with()
def test_update_self(watcher: Watcher) -> None:

View File

@ -230,32 +230,6 @@ def test_get_self_failed_http_error(web_client: WebClient, mocker: MockerFixture
assert web_client.get_self().status == BuildStatusEnum.Unknown
def test_reload_auth(web_client: WebClient, mocker: MockerFixture) -> None:
"""
must process auth reload
"""
requests_mock = mocker.patch("requests.Session.post")
web_client.reload_auth()
requests_mock.assert_called_once_with(pytest.helpers.anyvar(str, True))
def test_reload_auth_failed(web_client: WebClient, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during auth reload
"""
mocker.patch("requests.Session.post", side_effect=Exception())
web_client.reload_auth()
def test_reload_auth_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during removal
"""
mocker.patch("requests.Session.post", side_effect=requests.exceptions.HTTPError())
web_client.reload_auth()
def test_remove(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must process package removal

View File

@ -8,6 +8,14 @@ from unittest import mock
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import InitializeException
from ahriman.models.repository_paths import RepositoryPaths
def test_repository_paths(configuration: Configuration, repository_paths: RepositoryPaths) -> None:
"""
must return repository paths
"""
assert configuration.repository_paths == repository_paths
def test_from_path(mocker: MockerFixture) -> None:
@ -40,6 +48,33 @@ def test_from_path_file_missing(mocker: MockerFixture) -> None:
read_mock.assert_called_once_with(configuration.SYSTEM_CONFIGURATION_PATH)
def test_check_loaded(configuration: Configuration) -> None:
"""
must return valid path and architecture
"""
path, architecture = configuration.check_loaded()
assert path == configuration.path
assert architecture == configuration.architecture
def test_check_loaded_path(configuration: Configuration) -> None:
"""
must raise exception if path is none
"""
configuration.path = None
with pytest.raises(InitializeException):
configuration.check_loaded()
def test_check_loaded_architecture(configuration: Configuration) -> None:
"""
must raise exception if architecture is none
"""
configuration.architecture = None
with pytest.raises(InitializeException):
configuration.check_loaded()
def test_dump(configuration: Configuration) -> None:
"""
dump must not be empty

Some files were not shown because too many files have changed in this diff Show More