From efde0b2e86b7673c05718733bc63ac14c4692dca Mon Sep 17 00:00:00 2001 From: Evgenii Alekseev Date: Fri, 8 Sep 2023 03:42:28 +0300 Subject: [PATCH] feat: allow to use one application for multiple repositories (#111) * allow to use one application for multiple repositories * update tests * handle None append argument everywhere * rewrite repository definition logic * drop optional flags from docs * support of new schema in systemd units * add migration docs and ability to migrate tree automatically * use repostory id instead * verbose multiarchitectureerror * object path support for s3 sync * fix tests after rebase --- .github/workflows/setup.sh | 14 +- docker/entrypoint.sh | 8 +- docs/ahriman.application.handlers.rst | 8 + docs/ahriman.core.database.migrations.rst | 8 + docs/ahriman.core.log.rst | 6 +- docs/ahriman.models.rst | 8 + docs/architecture.rst | 49 +- docs/configuration.rst | 24 +- docs/faq.rst | 99 +-- docs/index.rst | 1 + docs/migration.rst | 55 ++ docs/setup.rst | 26 +- package/archlinux/ahriman.install | 18 +- .../lib/systemd/system/ahriman-web@.service | 4 +- package/lib/systemd/system/ahriman@.service | 4 +- package/lib/systemd/system/ahriman@.timer | 2 +- package/share/ahriman/settings/ahriman.ini | 2 - .../bash-completion/completions/_ahriman | 31 +- package/share/man/man1/ahriman.1 | 45 +- package/share/zsh/site-functions/_ahriman | 23 +- src/ahriman/application/ahriman.py | 96 +-- .../application/application/application.py | 3 +- .../application/application_properties.py | 21 +- src/ahriman/application/handlers/__init__.py | 1 + src/ahriman/application/handlers/add.py | 8 +- src/ahriman/application/handlers/backup.py | 8 +- src/ahriman/application/handlers/clean.py | 8 +- src/ahriman/application/handlers/daemon.py | 10 +- src/ahriman/application/handlers/dump.py | 8 +- src/ahriman/application/handlers/handler.py | 120 ++-- src/ahriman/application/handlers/help.py | 8 +- .../application/handlers/key_import.py | 10 +- src/ahriman/application/handlers/patch.py | 20 +- src/ahriman/application/handlers/rebuild.py | 10 +- src/ahriman/application/handlers/remove.py | 8 +- .../application/handlers/remove_unknown.py | 8 +- src/ahriman/application/handlers/restore.py | 8 +- src/ahriman/application/handlers/search.py | 10 +- .../application/handlers/service_updates.py | 10 +- src/ahriman/application/handlers/setup.py | 84 +-- src/ahriman/application/handlers/shell.py | 16 +- src/ahriman/application/handlers/sign.py | 8 +- src/ahriman/application/handlers/status.py | 10 +- .../application/handlers/status_update.py | 10 +- src/ahriman/application/handlers/structure.py | 10 +- .../application/handlers/tree_migrate.py | 68 +++ src/ahriman/application/handlers/triggers.py | 10 +- .../application/handlers/unsafe_commands.py | 8 +- src/ahriman/application/handlers/update.py | 8 +- src/ahriman/application/handlers/users.py | 8 +- src/ahriman/application/handlers/validate.py | 18 +- src/ahriman/application/handlers/versions.py | 8 +- src/ahriman/application/handlers/web.py | 20 +- src/ahriman/application/lock.py | 12 +- src/ahriman/core/alpm/pacman.py | 23 +- src/ahriman/core/build_tools/sources.py | 2 +- .../core/configuration/configuration.py | 113 ++-- src/ahriman/core/configuration/schema.py | 1 - .../migrations/m001_package_source.py | 16 +- .../migrations/m005_make_opt_depends.py | 4 +- .../m006_packages_architecture_required.py | 4 +- .../database/migrations/m007_check_depends.py | 4 +- .../database/migrations/m008_packagers.py | 4 +- .../migrations/m011_repository_name.py | 211 +++++++ .../database/operations/build_operations.py | 24 +- .../database/operations/logs_operations.py | 19 +- .../core/database/operations/operations.py | 6 +- .../database/operations/package_operations.py | 95 +-- .../database/operations/patch_operations.py | 17 +- src/ahriman/core/database/sqlite.py | 5 +- src/ahriman/core/exceptions.py | 12 +- src/ahriman/core/gitremote/remote_pull.py | 7 +- .../core/gitremote/remote_pull_trigger.py | 11 +- .../core/gitremote/remote_push_trigger.py | 9 +- src/ahriman/core/http/sync_http_client.py | 4 +- src/ahriman/core/log/__init__.py | 1 - .../core/log/{log.py => log_loader.py} | 7 +- src/ahriman/core/report/console.py | 7 +- src/ahriman/core/report/email.py | 9 +- src/ahriman/core/report/html.py | 9 +- src/ahriman/core/report/jinja_template.py | 8 +- src/ahriman/core/report/remote_call.py | 7 +- src/ahriman/core/report/report.py | 31 +- src/ahriman/core/report/report_trigger.py | 9 +- src/ahriman/core/report/telegram.py | 11 +- src/ahriman/core/repository/repository.py | 9 +- .../core/repository/repository_properties.py | 35 +- src/ahriman/core/spawn.py | 20 +- src/ahriman/core/status/watcher.py | 11 +- src/ahriman/core/status/web_client.py | 2 +- src/ahriman/core/support/keyring_trigger.py | 9 +- .../core/support/mirrorlist_trigger.py | 9 +- src/ahriman/core/support/package_creator.py | 4 +- .../support/pkgbuild/keyring_generator.py | 7 +- .../support/pkgbuild/mirrorlist_generator.py | 13 +- src/ahriman/core/tree.py | 14 +- src/ahriman/core/triggers/trigger.py | 28 +- src/ahriman/core/triggers/trigger_loader.py | 15 +- src/ahriman/core/upload/github.py | 27 +- src/ahriman/core/upload/http_upload.py | 16 +- src/ahriman/core/upload/remote_service.py | 11 +- src/ahriman/core/upload/rsync.py | 7 +- src/ahriman/core/upload/s3.py | 21 +- src/ahriman/core/upload/upload.py | 31 +- src/ahriman/core/upload/upload_trigger.py | 17 +- src/ahriman/models/aur_package.py | 3 +- src/ahriman/models/package_description.py | 3 +- src/ahriman/models/repository_id.py | 73 +++ src/ahriman/models/repository_paths.py | 104 +++- src/ahriman/web/views/v1/status/status.py | 4 +- src/ahriman/web/web.py | 7 +- .../application/application/conftest.py | 9 +- .../test_application_properties.py | 7 + tests/ahriman/application/conftest.py | 9 +- .../application/handlers/test_handler.py | 195 ++++-- .../application/handlers/test_handler_add.py | 9 +- .../handlers/test_handler_backup.py | 7 +- .../handlers/test_handler_clean.py | 3 +- .../handlers/test_handler_daemon.py | 5 +- .../application/handlers/test_handler_dump.py | 7 +- .../application/handlers/test_handler_help.py | 10 +- .../handlers/test_handler_key_import.py | 7 +- .../handlers/test_handler_patch.py | 28 +- .../handlers/test_handler_rebuild.py | 23 +- .../handlers/test_handler_remove.py | 3 +- .../handlers/test_handler_remove_unknown.py | 6 +- .../handlers/test_handler_restore.py | 7 +- .../handlers/test_handler_search.py | 16 +- .../handlers/test_handler_service_updates.py | 13 +- .../handlers/test_handler_setup.py | 125 ++-- .../handlers/test_handler_shell.py | 16 +- .../application/handlers/test_handler_sign.py | 3 +- .../handlers/test_handler_status.py | 24 +- .../handlers/test_handler_status_update.py | 18 +- .../handlers/test_handler_structure.py | 7 +- .../handlers/test_handler_tree_migrate.py | 42 ++ .../handlers/test_handler_triggers.py | 6 +- .../handlers/test_handler_unsafe_commands.py | 10 +- .../handlers/test_handler_update.py | 12 +- .../handlers/test_handler_users.py | 22 +- .../handlers/test_handler_validate.py | 18 +- .../handlers/test_handler_versions.py | 10 +- .../application/handlers/test_handler_web.py | 34 +- tests/ahriman/application/test_ahriman.py | 575 +++++++++++++++--- tests/ahriman/application/test_lock.py | 8 +- tests/ahriman/conftest.py | 34 +- tests/ahriman/core/alpm/test_pacman.py | 12 +- .../ahriman/core/build_tools/test_sources.py | 2 +- .../core/configuration/test_configuration.py | 104 +++- .../migrations/test_m001_package_source.py | 38 +- .../migrations/test_m011_repository_name.py | 39 ++ .../operations/test_build_operations.py | 41 ++ .../operations/test_logs_operations.py | 32 +- .../operations/test_package_operations.py | 13 +- .../operations/test_patch_operations.py | 10 +- .../core/gitremote/test_remote_pull.py | 15 +- .../gitremote/test_remote_pull_trigger.py | 3 +- .../gitremote/test_remote_push_trigger.py | 3 +- .../core/http/test_sync_http_client.py | 2 +- .../log/{test_log.py => test_log_loader.py} | 20 +- tests/ahriman/core/report/conftest.py | 35 +- tests/ahriman/core/report/test_console.py | 3 +- tests/ahriman/core/report/test_email.py | 71 ++- tests/ahriman/core/report/test_html.py | 3 +- .../core/report/test_jinja_template.py | 3 +- tests/ahriman/core/report/test_report.py | 26 +- .../core/report/test_report_trigger.py | 3 +- tests/ahriman/core/report/test_telegram.py | 45 +- tests/ahriman/core/repository/conftest.py | 9 +- .../core/repository/test_repository.py | 7 +- .../repository/test_repository_properties.py | 14 + tests/ahriman/core/status/test_watcher.py | 5 +- tests/ahriman/core/support/conftest.py | 3 +- .../ahriman/core/support/pkgbuild/conftest.py | 3 +- .../pkgbuild/test_keyring_generator.py | 43 +- .../pkgbuild/test_mirrorlist_generator.py | 32 +- .../core/support/test_keyring_trigger.py | 3 +- .../core/support/test_mirrorlist_trigger.py | 3 +- .../core/support/test_package_creator.py | 4 +- tests/ahriman/core/test_spawn.py | 6 +- tests/ahriman/core/triggers/conftest.py | 6 +- tests/ahriman/core/triggers/test_trigger.py | 22 +- .../core/triggers/test_trigger_loader.py | 10 +- tests/ahriman/core/upload/conftest.py | 29 +- tests/ahriman/core/upload/test_github.py | 121 ++-- tests/ahriman/core/upload/test_s3.py | 37 +- tests/ahriman/core/upload/test_upload.py | 25 +- .../core/upload/test_upload_trigger.py | 3 +- tests/ahriman/models/test_repository_id.py | 40 ++ tests/ahriman/models/test_repository_paths.py | 175 +++++- tests/ahriman/web/conftest.py | 11 +- 191 files changed, 3441 insertions(+), 1319 deletions(-) create mode 100644 docs/migration.rst create mode 100644 src/ahriman/application/handlers/tree_migrate.py create mode 100644 src/ahriman/core/database/migrations/m011_repository_name.py rename src/ahriman/core/log/{log.py => log_loader.py} (95%) create mode 100644 src/ahriman/models/repository_id.py create mode 100644 tests/ahriman/application/handlers/test_handler_tree_migrate.py create mode 100644 tests/ahriman/core/database/migrations/test_m011_repository_name.py rename tests/ahriman/core/log/{test_log.py => test_log_loader.py} (72%) create mode 100644 tests/ahriman/models/test_repository_id.py diff --git a/.github/workflows/setup.sh b/.github/workflows/setup.sh index 792566a5..c15e1f13 100755 --- a/.github/workflows/setup.sh +++ b/.github/workflows/setup.sh @@ -36,21 +36,21 @@ systemd-machine-id-setup # initial setup command as root [[ -z $MINIMAL_INSTALL ]] && WEB_ARGS=("--web-port" "8080") -ahriman -a x86_64 service-setup --packager "ahriman bot " --repository "github" "${WEB_ARGS[@]}" +ahriman -a x86_64 -r "github" service-setup --packager "ahriman bot " "${WEB_ARGS[@]}" # validate configuration -ahriman -a x86_64 service-config-validate --exit-code +ahriman service-config-validate --exit-code # enable services -systemctl enable ahriman-web@x86_64 -systemctl enable ahriman@x86_64.timer +systemctl enable ahriman-web@x86_64-github +systemctl enable ahriman@x86_64-github.timer if [[ -z $MINIMAL_INSTALL ]]; then # run web service (detached) - sudo -u ahriman -- ahriman -a x86_64 web & + sudo -u ahriman -- ahriman web & WEB_PID=$! fi # add the first package -sudo -u ahriman -- ahriman package-add --now yay +sudo -u ahriman -- ahriman package-add --now ahriman # check if package was actually installed -test -n "$(find "/var/lib/ahriman/repository/x86_64" -name "yay*pkg*")" +test -n "$(find "/var/lib/ahriman/repository/github/x86_64" -name "ahriman*pkg*")" # run package check sudo -u ahriman -- ahriman repo-update # stop web service lol diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 371989d9..76d10e1f 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -17,6 +17,7 @@ host = $AHRIMAN_HOST EOF AHRIMAN_DEFAULT_ARGS=("--architecture" "$AHRIMAN_ARCHITECTURE") +AHRIMAN_DEFAULT_ARGS+=("--repository" "$AHRIMAN_REPOSITORY") if [ -n "$AHRIMAN_OUTPUT" ]; then AHRIMAN_DEFAULT_ARGS+=("--log-handler" "$AHRIMAN_OUTPUT") fi @@ -33,19 +34,18 @@ chown "$AHRIMAN_USER":"$AHRIMAN_USER" "$AHRIMAN_GNUPG_HOME" # run built-in setup command AHRIMAN_SETUP_ARGS=("--build-as-user" "$AHRIMAN_USER") AHRIMAN_SETUP_ARGS+=("--packager" "$AHRIMAN_PACKAGER") -AHRIMAN_SETUP_ARGS+=("--repository" "$AHRIMAN_REPOSITORY") if [ -z "$AHRIMAN_MULTILIB" ]; then AHRIMAN_SETUP_ARGS+=("--no-multilib") fi if [ -n "$AHRIMAN_PACMAN_MIRROR" ]; then AHRIMAN_SETUP_ARGS+=("--mirror" "$AHRIMAN_PACMAN_MIRROR") fi -if [ -n "$AHRIMAN_PORT" ]; then - AHRIMAN_SETUP_ARGS+=("--web-port" "$AHRIMAN_PORT") -fi if [ -n "$AHRIMAN_REPOSITORY_SERVER" ]; then AHRIMAN_SETUP_ARGS+=("--server" "$AHRIMAN_REPOSITORY_SERVER") fi +if [ -n "$AHRIMAN_PORT" ]; then + AHRIMAN_SETUP_ARGS+=("--web-port" "$AHRIMAN_PORT") +fi if [ -n "$AHRIMAN_UNIX_SOCKET" ]; then AHRIMAN_SETUP_ARGS+=("--web-unix-socket" "$AHRIMAN_UNIX_SOCKET") fi diff --git a/docs/ahriman.application.handlers.rst b/docs/ahriman.application.handlers.rst index 7f6b617f..9f11f690 100644 --- a/docs/ahriman.application.handlers.rst +++ b/docs/ahriman.application.handlers.rst @@ -172,6 +172,14 @@ ahriman.application.handlers.structure module :no-undoc-members: :show-inheritance: +ahriman.application.handlers.tree\_migrate module +------------------------------------------------- + +.. automodule:: ahriman.application.handlers.tree_migrate + :members: + :no-undoc-members: + :show-inheritance: + ahriman.application.handlers.triggers module -------------------------------------------- diff --git a/docs/ahriman.core.database.migrations.rst b/docs/ahriman.core.database.migrations.rst index a144ed6a..88a69538 100644 --- a/docs/ahriman.core.database.migrations.rst +++ b/docs/ahriman.core.database.migrations.rst @@ -92,6 +92,14 @@ ahriman.core.database.migrations.m010\_version\_based\_logs\_removal module :no-undoc-members: :show-inheritance: +ahriman.core.database.migrations.m011\_repository\_name module +-------------------------------------------------------------- + +.. automodule:: ahriman.core.database.migrations.m011_repository_name + :members: + :no-undoc-members: + :show-inheritance: + Module contents --------------- diff --git a/docs/ahriman.core.log.rst b/docs/ahriman.core.log.rst index 19624dec..ca69d75b 100644 --- a/docs/ahriman.core.log.rst +++ b/docs/ahriman.core.log.rst @@ -36,10 +36,10 @@ ahriman.core.log.lazy\_logging module :no-undoc-members: :show-inheritance: -ahriman.core.log.log module ---------------------------- +ahriman.core.log.log\_loader module +----------------------------------- -.. automodule:: ahriman.core.log.log +.. automodule:: ahriman.core.log.log_loader :members: :no-undoc-members: :show-inheritance: diff --git a/docs/ahriman.models.rst b/docs/ahriman.models.rst index 253e2e45..742fb4f3 100644 --- a/docs/ahriman.models.rst +++ b/docs/ahriman.models.rst @@ -164,6 +164,14 @@ ahriman.models.report\_settings module :no-undoc-members: :show-inheritance: +ahriman.models.repository\_id module +------------------------------------ + +.. automodule:: ahriman.models.repository_id + :members: + :no-undoc-members: + :show-inheritance: + ahriman.models.repository\_paths module --------------------------------------- diff --git a/docs/architecture.rst b/docs/architecture.rst index 6c6077a9..96207671 100644 --- a/docs/architecture.rst +++ b/docs/architecture.rst @@ -102,6 +102,53 @@ All subcommands are divided into several groups depending on the role they are d For historical reasons and in order to keep backward compatibility some subcommands have aliases to their shorter forms or even other groups, but the service doesn't guarantee that they will remain unchanged. +Filesystem tree +--------------- + +The application supports two types of trees, one is for legacy configuration (when there were no repository name explicit configuration available) and another one is for new-style tree. This document describes only new-style tree in order to avoid deprecated structure. + +Having default root as ``/var/lib/ahriman`` (differs from container though), the directory structure is the following: + +.. code-block:: + + /var/lib/ahriman/ + ├── ahriman.db + ├── cache + ├── chroot + │ └── aur-clone + ├── packages + │ └── aur-clone + │ └── x86_64 + ├── pacman + │ └── aur-clone + │ └── x86_64 + │ ├── local + │ │ └── ALPM_DB_VERSION + │ └── sync + │ ├── core.db + │ ├── extra.db + │ └── multilib.db + │ + └── repository + └── aur-clone + └── x86_64 + ├── aur-clone.db -> aur-clone.db.tar.gz + ├── aur-clone.db.tar.gz + ├── aur-clone.files -> aur-clone.files.tar.gz + └── aur-clone.files.tar.gz + +There are multiple subdirectories, some of them are commons for any repository, but some of them are not. + +* ``cache`` is a directory with locally stored PKGBUILD's and VCS packages. It is common for all repositories and architectures. +* ``chroot/{repository}`` is a chroot directory for ``devtools``. It is specific for each repository, but shared for different architectures inside (the ``devtools`` handles architectures automatically). +* ``packages/{repository}/{architecture}`` is a directory with prebuilt packages. When package is built, first it will be uploaded to this directory and later will be handled by update process. It is architecture and repository specific. +* ``pacman/{repository}/{architecture}`` is repository and architecture specific caches for pacman's databases. +* ``repository/{repository}/{architecture}`` is a repository packages directory. + +Normally you should avoid direct interaction with the application tree. + +For tree migration process refer to the :doc:`migration notes `. + Database -------- @@ -274,7 +321,7 @@ There are several supported synchronization providers, currently they are ``rsyn ``rsync`` provider does not have any specific logic except for running external rsync application with configured arguments. The service does not handle SSH configuration, thus it has to be configured before running application manually. -``s3`` provider uses ``boto3`` package and implements sync feature. The files are stored in architecture directory (e.g. if bucket is ``repository``, packages will be stored in ``repository/x86_64`` for the ``x86_64`` architecture), bucket must be created before any action and API key must have permissions to write to the bucket. No external configuration required. In order to upload only changed files the service compares calculated hashes with the Amazon ETags, used realization is described `here `_. +``s3`` provider uses ``boto3`` package and implements sync feature. The files are stored in architecture directory (e.g. if bucket is ``repository``, packages will be stored in ``repository/aur-clone/x86_64`` for the ``aur-clone`` repository ``x86_64`` architecture), bucket must be created before any action and API key must have permissions to write to the bucket. No external configuration required. In order to upload only changed files the service compares calculated hashes with the Amazon ETags, used realization is described `here `_. ``github`` provider authenticates through basic auth, API key with repository write permissions is required. There will be created a release with the name of the architecture in case if it does not exist; files will be uploaded to the release assets. It also stores array of files and their MD5 checksums in release body in order to upload only changed ones. According to the Github API in case if there is already uploaded asset with the same name (e.g. database files), asset will be removed first. diff --git a/docs/configuration.rst b/docs/configuration.rst index deaea442..e8fb141c 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -1,7 +1,12 @@ Configuration ============= -Some groups can be specified for each architecture separately. E.g. if there are ``build`` and ``build:x86_64`` groups it will use an option from ``build:x86_64`` for the ``x86_64`` architecture and ``build`` for any other (architecture specific group has higher priority). In case if both groups are presented, architecture specific options will be merged into global ones overriding them. +Some groups can be specified for each architecture and/or repository separately. E.g. if there are ``build`` and ``build:x86_64`` groups it will use an option from ``build:x86_64`` for the ``x86_64`` architecture and ``build`` for any other (architecture specific group has higher priority). In case if both groups are presented, architecture specific options will be merged into global ones overriding them. The order which will be used for option resolution is the following: + +#. Repository and architecture specific, e.g. ``build:aur-clone:x86_64``. +#. Repository specific, e.g. ``build:aur-clone``. +#. Architecture specific, e.g. ``build:x86_64``. +#. Default section, e.g. ``build``. There are two variable types which have been added to default ones, they are paths and lists. List values will be read in the same way as shell does: @@ -25,7 +30,7 @@ There is also additional subcommand which will allow to validate configuration a .. code-block:: shell - ahriman -a x86_64 service-config-validate + ahriman service-config-validate It will check current settings on common errors and compare configuration with known schema. @@ -87,7 +92,6 @@ Build related configuration. Group name can refer to architecture, e.g. ``build: Base repository settings. -* ``name`` - repository name, string, required. * ``root`` - root path for application, string, required. ``sign:*`` groups @@ -292,20 +296,21 @@ Type will be read from several sources: ``github`` type ^^^^^^^^^^^^^^^ -This feature requires Github key creation (see below). Section name must be either ``github`` (plus optional architecture name, e.g. ``github:x86_64``) or random name with ``type`` set. +This feature requires GitHub key creation (see below). Section name must be either ``github`` (plus optional architecture name, e.g. ``github:x86_64``) or random name with ``type`` set. * ``type`` - type of the upload, string, optional, must be set to ``github`` if exists. -* ``owner`` - Github repository owner, string, required. -* ``password`` - created Github API key. In order to create it do the following: +* ``owner`` - GitHub repository owner, string, required. +* ``password`` - created GitHub API key. In order to create it do the following: #. Go to `settings page `_. #. Switch to `developers settings `_. #. Switch to `personal access tokens `_. #. Generate new token. Required scope is ``public_repo`` (or ``repo`` for private repository support). -* ``repository`` - Github repository name, string, required. Repository must be created before any action and must have active branch (e.g. with readme). +* ``repository`` - GitHub repository name, string, required. Repository must be created before any action and must have active branch (e.g. with readme). * ``timeout`` - HTTP request timeout in seconds, int, optional, default is ``30``. -* ``username`` - Github authorization user, string, required. Basically the same as ``owner``. +* ``use_full_release_name`` - if set to ``yes``, the release will contain both repository name and architecture, and only architecture otherwise, boolean, optional, default ``no`` (legacy behavior). +* ``username`` - GitHub authorization user, string, required. Basically the same as ``owner``. ``remote-service`` type ^^^^^^^^^^^^^^^^^^^^^^^ @@ -329,9 +334,10 @@ Requires ``rsync`` package to be installed. Do not forget to configure ssh for u Requires ``boto3`` library to be installed. Section name must be either ``s3`` (plus optional architecture name, e.g. ``s3:x86_64``) or random name with ``type`` set. -* ``type`` - type of the upload, string, optional, must be set to ``github`` if exists. +* ``type`` - type of the upload, string, optional, must be set to ``s3`` if exists. * ``access_key`` - AWS access key ID, string, required. * ``bucket`` - bucket name (e.g. ``bucket``), string, required. * ``chunk_size`` - chunk size for calculating entity tags, int, optional, default 8 * 1024 * 1024. +* ``object_path`` - path prefix for stored objects, string, optional. If none set, the prefix as in repository tree will be used. * ``region`` - bucket region (e.g. ``eu-central-1``), string, required. * ``secret_key`` - AWS secret access key, string, required. \ No newline at end of file diff --git a/docs/faq.rst b/docs/faq.rst index 62c3da76..43dc424e 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -17,8 +17,8 @@ TL;DR .. code-block:: shell yay -S ahriman - ahriman -a x86_64 service-setup --packager "ahriman bot " --repository "repository" - systemctl enable --now ahriman@x86_64.timer + ahriman -a x86_64 -r aur-clone service-setup --packager "ahriman bot " + systemctl enable --now ahriman@x86_64-aur-clone.timer Long answer """"""""""" @@ -32,7 +32,7 @@ There is special command which can be used in order to validate current configur .. code-block:: shell - ahriman -a x86_64 service-config-validate --exit-code + ahriman service-config-validate --exit-code This command will print found errors, based on `cerberus `_, e.g.: @@ -71,7 +71,7 @@ states that default build command is ``extra-x86_64-build``. But if there is sec [build:i686] build_command = extra-i686-build -the ``extra-i686-build`` command will be used for ``i686`` architecture. +the ``extra-i686-build`` command will be used for ``i686`` architecture. You can also override settings for different repositories and architectures; in this case section names will be ``build:aur-clone`` (repository name only) and ``build:aur-clone:i686`` (both repository name and architecture). How to generate build reports ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -121,7 +121,7 @@ How do I add new package sudo -u ahriman ahriman package-add ahriman --now -``--now`` flag is totally optional and just run ``repo-update`` subcommand after the registering the new package, Thus the extended flow is the following: +``--now`` flag is totally optional and just run ``repo-update`` subcommand after the registering the new package. Thus the extended flow is the following: .. code-block:: shell @@ -209,7 +209,7 @@ So it is the same as adding any other package, but due to restrictions you must sudo -u ahriman ahriman package-add pacman -s repository -This feature is heavily depends on local pacman cache. In order to use this feature it is recommended to either run ``pacman -Sy`` before the interaction or configure timer for this. +This feature is heavily depends on local pacman cache. In order to use this feature it is recommended to either run ``pacman -Sy`` before the interaction or use internal application cache with ``--refresh`` flag. Package build fails because it cannot validate PGP signature of source files ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -317,7 +317,7 @@ Add the following lines to your ``pacman.conf``: .. code-block:: ini [repository] - Server = file:///var/lib/ahriman/repository/x86_64 + Server = file:///var/lib/ahriman/repository/$repo/$arch (You might need to add ``SigLevel`` option according to the pacman documentation.) @@ -359,7 +359,14 @@ Example of the status page configuration is the following (status service is usi Docker image ------------ -We provide official images which can be found under ``arcan1s/ahriman`` repository. Docker image is being updated on each commit to master as well as on each version. If you would like to use last (probably unstable) build you can use ``edge`` tag or ``latest`` for any tagged versions; otherwise you can use any version tag available. +We provide official images which can be found under: + +* docker registry ``arcan1s/ahriman``; +* ghcr.io registry ``ghcr.io/arcan1s/ahriman``; + +These images are totally identical. + +Docker image is being updated on each commit to master as well as on each version. If you would like to use last (probably unstable) build you can use ``edge`` tag or ``latest`` for any tagged versions; otherwise you can use any version tag available. The default action (in case if no arguments provided) is ``repo-update``. Basically the idea is to run container, e.g.: @@ -456,22 +463,22 @@ Physical server setup In this example we are going to use files and packages which are provided by official repositories of the used architecture. Note, that versions might be different, thus you need to find correct versions on the distribution web site, e.g. `archlinux32 packages `_. #. - First, considering having base Arch Linux system, we need to install keyring for the specified repositories: + First, considering having base Arch Linux system, we need to install keyring for the specified repositories, e.g.: .. code-block:: shell - wget http://pool.mirror.archlinux32.org/i686/core/archlinux32-keyring-20220927-1.0-any.pkg.tar.zst - pacman -U archlinux32-keyring-20220927-1.0-any.pkg.tar.zst + wget http://pool.mirror.archlinux32.org/i686/core/archlinux32-keyring-20230705-1.0-any.pkg.tar.zst + pacman -U archlinux32-keyring-20230705-1.0-any.pkg.tar.zst #. - In order to run ``devtools`` scripts for custom architecture they also need specific ``makepkg`` configuration, it can be retrieved by installing the ``devtools`` package of the distribution: + In order to run ``devtools`` scripts for custom architecture they also need specific ``makepkg`` configuration, it can be retrieved by installing the ``devtools`` package of the distribution, e.g.: .. code-block:: shell - wget http://pool.mirror.archlinux32.org/i686/extra/devtools-20221208-1.0-any.pkg.tar.zst - pacman -U devtools-20221208-1.0-any.pkg.tar.zst + wget http://pool.mirror.archlinux32.org/i686/extra/devtools-20221208-1.2-any.pkg.tar.zst + pacman -U devtools-20221208-1.2-any.pkg.tar.zst - Alternatively, you can create your own ``makepkg`` configuration and save it as ``/usr/share/devtools/makepkg-i686.conf``. + Alternatively, you can create your own ``makepkg`` configuration and save it as ``/usr/share/devtools/makepkg.conf.d/i686.conf``. #. Setup repository as usual: @@ -485,6 +492,9 @@ In this example we are going to use files and packages which are provided by off * ``--mirror`` - link to the mirrors which will be used instead of official repositories. * ``--no-multilib`` - in the example we are using i686 architecture for which multilib repository doesn't exist. +#. + That's all Folks! + Docker container setup ^^^^^^^^^^^^^^^^^^^^^^ @@ -510,8 +520,8 @@ There are two possible ways to achieve same setup, by using docker container. Th .. code-block:: dockerfile RUN pacman --noconfirm -Sy wget - RUN wget http://pool.mirror.archlinux32.org/i686/extra/devtools-20221208-1.0-any.pkg.tar.zst && pacman --noconfirm -U devtools-20221208-1.0-any.pkg.tar.zst - RUN wget http://pool.mirror.archlinux32.org/i686/core/archlinux32-keyring-20220927-1.0-any.pkg.tar.zst && pacman --noconfirm -U archlinux32-keyring-20220927-1.0-any.pkg.tar.zst + RUN wget http://pool.mirror.archlinux32.org/i686/extra/devtools-20221208-1.2-any.pkg.tar.zst && pacman --noconfirm -U devtools-20221208-1.2-any.pkg.tar.zst + RUN wget http://pool.mirror.archlinux32.org/i686/core/archlinux32-keyring-20230705-1.0-any.pkg.tar.zst && pacman --noconfirm -U archlinux32-keyring-20230705-1.0-any.pkg.tar.zst #. At that point you should have full ``Dockerfile`` like: @@ -523,8 +533,8 @@ There are two possible ways to achieve same setup, by using docker container. Th RUN pacman-key --init RUN pacman --noconfirm -Sy wget - RUN wget http://pool.mirror.archlinux32.org/i686/extra/devtools-20221208-1.0-any.pkg.tar.zst && pacman --noconfirm -U devtools-20221208-1.0-any.pkg.tar.zst - RUN wget http://pool.mirror.archlinux32.org/i686/core/archlinux32-keyring-20220927-1.0-any.pkg.tar.zst && pacman --noconfirm -U archlinux32-keyring-20220927-1.0-any.pkg.tar.zst + RUN wget http://pool.mirror.archlinux32.org/i686/extra/devtools-20221208-1.2-any.pkg.tar.zst && pacman --noconfirm -U devtools-20221208-1.2-any.pkg.tar.zst + RUN wget http://pool.mirror.archlinux32.org/i686/core/archlinux32-keyring-20230705-1.0-any.pkg.tar.zst && pacman --noconfirm -U archlinux32-keyring-20230705-1.0-any.pkg.tar.zst #. After that you can build you own container, e.g.: @@ -554,8 +564,8 @@ There are several choices: .. code-block:: server { - location /x86_64 { - root /var/lib/ahriman/repository/x86_64; + location / { + root /var/lib/ahriman/repository/; autoindex on; } } @@ -571,7 +581,7 @@ There are several choices: [rsync] remote = 192.168.0.1:/srv/repo - After that just add ``/srv/repo`` to the ``pacman.conf`` as usual. You can also upload to S3 (e.g. ``Server = https://s3.eu-central-1.amazonaws.com/repository/x86_64``) or to Github (e.g. ``Server = https://github.com/ahriman/repository/releases/download/x86_64``). + After that just add ``/srv/repo`` to the ``pacman.conf`` as usual. You can also upload to S3 (``Server = https://s3.eu-central-1.amazonaws.com/repository/aur-clone/x86_64``) or to Github (``Server = https://github.com/ahriman/repository/releases/download/aur-clone-x86_64``). How to sync to S3 ^^^^^^^^^^^^^^^^^ @@ -632,6 +642,23 @@ How to sync to S3 region = eu-central-1 secret_key = ... +S3 with SSL +""""""""""" + +In order to configure S3 on custom domain with SSL (and some other features, like redirects), the CloudFront should be used. + +#. Configure S3 as described above. +#. In bucket properties, enable static website hosting with hosting type "Host a static website". +#. Go to AWS Certificate Manager and create public ceritificate on your domain. Validate domain as suggested. +#. Go to CloudFront and create distribution. The following settings are required: + + * Origin domain choose S3 bucket. + * Tick use website endpoint. + * Disable caching. + * Select issued certificate. + +#. Point DNS record to CloudFront address. + How to sync to Github releases ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -676,7 +703,7 @@ How to report by email [email] host = smtp.example.com - link_path = http://example.com/x86_64 + link_path = http://example.com/aur-clone/x86_64 password = ... port = 465 receivers = me@example.com @@ -702,10 +729,10 @@ How to generate index page for S3 target = html [html] - path = /var/lib/ahriman/repository/x86_64/index.html - link_path = http://example.com/x86_64 + path = /var/lib/ahriman/repository/aur-clone/x86_64/index.html + link_path = http://example.com/aur-clone/x86_64 -After these steps ``index.html`` file will be automatically synced to S3 +After these steps ``index.html`` file will be automatically synced to S3. How to post build report to telegram ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -741,7 +768,7 @@ How to post build report to telegram [telegram] api_key = aaAAbbBBccCC chat_id = @ahriman - link_path = http://example.com/x86_64 + link_path = http://example.com/aur-clone/x86_64 ``api_key`` is the one sent by `@BotFather `_, ``chat_id`` is the value retrieved from previous step. @@ -756,7 +783,7 @@ If you did everything fine you should receive the message with the next update. Distributed builds ------------------ -The service allows to run build on multiple machines and collect packages on main node. There are multiple ways to achieve it, this section describes officially supported methods. +The service allows to run build on multiple machines and collect packages on main node. There are several ways to achieve it, this section describes officially supported methods. Remote synchronization and remote server call ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -933,7 +960,7 @@ Command to run worker node: .. code-block:: shell - docker run --privileged -v worker.ini:/etc/ahriman.ini.d/overrides.ini -it arcan1s/ahriman:latest package-add arhiman --now + docker run --privileged -v worker.ini:/etc/ahriman.ini.d/overrides.ini -it arcan1s/ahriman:latest package-add ahriman --now The command above will successfully build ``ahriman`` package, upload it on master node and, finally, will update master node repository. @@ -1046,7 +1073,7 @@ How to setup web service port = 8080 #. - Start the web service ``systemctl enable --now ahriman-web@x86_64``. + Start the web service ``systemctl enable --now ahriman-web@x86_64-aur-clone``. How to enable basic authorization ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1059,7 +1086,7 @@ How to enable basic authorization yay -S --asdeps python-aiohttp-security python-aiohttp-session python-cryptography #. - Configure the service to enable authorization (``salt`` can be generated as any random string): + Configure the service to enable authorization (``salt`` can be generated as any random string and optional): .. code-block:: ini @@ -1087,7 +1114,7 @@ How to enable basic authorization sudo -u ahriman ahriman user-add -r full api - This command will ask for the password, just type it in stdin; *do not* leave the field blank, user will not be able to authorize, and finally configure the application: + This command will ask for the password, just type it in stdin; **do not** leave the field blank, user will not be able to authorize, and finally configure the application: .. code-block:: ini @@ -1103,7 +1130,7 @@ How to enable basic authorization sudo -u ahriman ahriman user-add -r full my-first-user #. - Restart web service ``systemctl restart ahriman-web@x86_64``. + Restart web service ``systemctl restart ahriman-web@x86_64-aur-clone``. How to enable OAuth authorization ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1149,7 +1176,7 @@ How to enable OAuth authorization When it will ask for the password leave it blank. #. - Restart web service ``systemctl restart ahriman-web@x86_64``. + Restart web service ``systemctl restart ahriman-web@x86_64-aur-clone``. How to implement own interface ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1265,7 +1292,9 @@ You can also ask to forward logs to ``stderr``, just set ``--log-handler`` flag, ahriman --log-handler console ... -You can even configure logging as you wish, but kindly refer to python ``logging`` module `configuration `_. The application uses java concept to log messages, e.g. class ``Application`` imported from ``ahriman.application.application`` package will have logger called ``ahriman.application.application.Application``. In order to e.g. change logger name for whole application package it is possible to change values for ``ahriman.application`` package; thus editing ``ahriman`` logger configuration will change logging for whole application (unless there are overrides for another logger). +You can even configure logging as you wish, but kindly refer to python ``logging`` module `configuration `_. + +The application uses java concept to log messages, e.g. class ``Application`` imported from ``ahriman.application.application`` package will have logger called ``ahriman.application.application.Application``. In order to e.g. change logger name for whole application package it is possible to change values for ``ahriman.application`` package; thus editing ``ahriman`` logger configuration will change logging for whole application (unless there are overrides for another logger). Html customization ^^^^^^^^^^^^^^^^^^ diff --git a/docs/index.rst b/docs/index.rst index e6183aa3..adb3996b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -34,6 +34,7 @@ Contents configuration command-line faq + migration architecture advanced-usage triggers diff --git a/docs/migration.rst b/docs/migration.rst new file mode 100644 index 00000000..4f69e683 --- /dev/null +++ b/docs/migration.rst @@ -0,0 +1,55 @@ +Manual migrations +================= + +Normally most of migrations are handled automatically after application start. However, some upgrades require manual interventions; this document describes them. + +Upgrades to breakpoints +----------------------- + +To 2.9.0 +^^^^^^^^ + +This release includes major upgrade for the newest devtools and archlinux repository structure. In order to upgrade package need to: + +#. Upgrade to the latest major release of python (3.11) (required by other changes). +#. Upgrade devtools to the latest release. +#. Backup your settings, ``/etc/ahriman.ini.d/00-setup-overrides.ini`` by default. +#. Run setup command (i.e. ``ahriman service-setup``) again with the same arguments as you used before. This step can be done manually by moving ``devtools`` configuration (something like ``/usr/share/devtools/pacman-ahriman*.conf``) to new location ``/usr/share/devtools/pacman.conf.d/`` under name ``ahriman.conf``. After that make sure to remove any ``community`` mentions from configurations (e.g. ``/usr/share/devtools/pacman.conf.d/ahriman.conf``, ``/etc/ahriman.ini``) if there were any. The only thing which will change is ``devtools`` configuration. +#. Remove build chroot as it is incompatible, e.g. ``sudo ahriman service-clean --chroot``. +#. Run ``sudo -u ahriman ahriman update --no-aur --no-local --no-manual -yy`` in order to update local databases. + +To 2.12.0 +^^^^^^^^^ + +This release includes paths migration. Unlike usual case, no automatic migration is performed because it might break user configuration. The following noticeable changes have been made: + +* Path to pre-built packages now includes repository name, i.e. it has been changed from ``/var/lib/ahriman/packages/x86_64`` to ``/var/lib/ahriman/packages/aur-clone/x86_64``. +* Path to pacman databases now includes repository name too, it has been changed from ``/var/lib/ahriman/pacman/x86_64`` to ``/var/lib/ahriman/pacman/aur-clone/x86_64``. +* Path to repository itself also includes repository name, from ``/var/lib/ahriman/repository/x86_64`` to ``/var/lib/ahriman/repository/aur-clone/x86_64``. + +In order to migrate to new filesystem tree the following actions are required: + +#. + Stop and disable all services, e.g. timer and web service: + + .. code-block:: shell + + sudo systemctl disable --now ahriman@x86_64.timer + sudo systemctl disable --now ahriman-web@x86_64 + +#. + Create directory tree. It can be done by running ``ahriman service-tree-migrate`` subcommand. It performs copying between the old repository tree and the new one. Alternatively you can copy directories by hands. + +#. + Edit configuration in case if anything is pointing to the old path, e.g. HTML report generation, in the way in which it will be pointed to directory inside repository specific one, e.g. ``/var/lib/ahriman/repository/x86_64`` to ``/var/lib/ahriman/repository/aur-clone/x86_64``. + +#. + Make sure to update remote synchronization services if any. Almost all of them rely on current repository tree by default, so you need to setup either redirects or configure to synchronize to the old locations (e.g. ``object_path`` option for S3 synchronization). + +#. + Enable and start services again. Unit template parameter should include both repository architecture and name, dash separated, e.g. ``x86_64-aur-clone``: + + .. code-block:: shell + + sudo systemctl enable --now ahriman@x86_64-aur-clone.timer + sudo systemctl enable --now ahriman-web@x86_64-aur-clone diff --git a/docs/setup.rst b/docs/setup.rst index 9b1a673c..34227cbd 100644 --- a/docs/setup.rst +++ b/docs/setup.rst @@ -10,7 +10,7 @@ Initial setup .. code-block:: shell - sudo ahriman -a x86_64 service-setup ... + sudo ahriman -a x86_64 -r aur-clone service-setup ... ``service-setup`` literally does the following steps: @@ -29,26 +29,26 @@ Initial setup .. code-block:: shell - ln -s /usr/bin/archbuild /usr/local/bin/ahriman-x86_64-build + ln -s /usr/bin/archbuild /usr/local/bin/aur-clone-x86_64-build #. Create configuration file (same as previous ``{name}.conf``): .. code-block:: shell - cp /usr/share/devtools/pacman.conf.d/{extra,ahriman}.conf + cp /usr/share/devtools/pacman.conf.d/{extra,aur-clone}.conf #. Change configuration file, add your own repository, add multilib repository etc: .. code-block:: shell - echo '[multilib]' | tee -a /usr/share/devtools/pacman-ahriman.conf - echo 'Include = /etc/pacman.d/mirrorlist' | tee -a /usr/share/devtools/pacman.conf.d/ahriman.conf + echo '[multilib]' | tee -a /usr/share/devtools/pacman.conf.d/aur-clone-x86_64.conf + echo 'Include = /etc/pacman.d/mirrorlist' | tee -a /usr/share/devtools/pacman.conf.d/aur-clone-x86_64.conf - echo '[aur-clone]' | tee -a /usr/share/devtools/pacman-ahriman.conf - echo 'SigLevel = Optional TrustAll' | tee -a /usr/share/devtools/pacman.conf.d/ahriman.conf - echo 'Server = file:///var/lib/ahriman/repository/$arch' | tee -a /usr/share/devtools/pacman.conf.d/ahriman.conf + echo '[aur-clone]' | tee -a /usr/share/devtools/pacman.conf.d/aur-clone-x86_64.conf + echo 'SigLevel = Optional TrustAll' | tee -a /usr/share/devtools/pacman.conf.d/aur-clone-x86_64.conf + echo 'Server = file:///var/lib/ahriman/repository/$repo/$arch' | tee -a /usr/share/devtools/pacman.conf.d/aur-clone-x86_64.conf #. Set ``build_command`` option to point to your command: @@ -56,14 +56,14 @@ Initial setup .. code-block:: shell echo '[build]' | tee -a /etc/ahriman.ini.d/build.ini - echo 'build_command = ahriman-x86_64-build' | tee -a /etc/ahriman.ini.d/build.ini + echo 'build_command = aur-clone-x86_64-build' | tee -a /etc/ahriman.ini.d/build.ini #. Configure ``/etc/sudoers.d/ahriman`` to allow running command without a password: .. code-block:: shell - echo 'Cmnd_Alias CARCHBUILD_CMD = /usr/local/bin/ahriman-x86_64-build *' | tee -a /etc/sudoers.d/ahriman + echo 'Cmnd_Alias CARCHBUILD_CMD = /usr/local/bin/aur-clone-x86_64-build *' | tee -a /etc/sudoers.d/ahriman echo 'ahriman ALL=(ALL) NOPASSWD:SETENV: CARCHBUILD_CMD' | tee -a /etc/sudoers.d/ahriman chmod 400 /etc/sudoers.d/ahriman @@ -74,20 +74,20 @@ Initial setup .. code-block:: shell - systemctl enable --now ahriman@x86_64.timer + systemctl enable --now ahriman@x86_64-aur-clone.timer #. Start and enable status page: .. code-block:: shell - systemctl enable --now ahriman-web@x86_64 + systemctl enable --now ahriman-web@x86_64-aur-clone #. Add packages by using ``ahriman package-add {package}`` command: .. code-block:: shell - sudo -u ahriman ahriman -a x86_64 package-add ahriman --now --refresh + sudo -u ahriman ahriman package-add ahriman --now --refresh The ``--refresh`` flag is required in order to handle local database update. diff --git a/package/archlinux/ahriman.install b/package/archlinux/ahriman.install index e1a3813a..7481d85a 100644 --- a/package/archlinux/ahriman.install +++ b/package/archlinux/ahriman.install @@ -1,6 +1,7 @@ post_upgrade() { local breakpoints=( 2.9.0-1 + 2.12.0-1 ) for v in "${breakpoints[@]}"; do @@ -20,6 +21,21 @@ It was found that you were upgrading from old-devtools package to the new one, w * remove build chroot: sudo rm -r /var/lib/ahriman/chroot/ahriman-x86_64/; * update local databases: sudo -u ahriman ahriman update --no-aur --no-local --no-manual -yy; -For more information kindly refer to changelog https://github.com/arcan1s/ahriman/releases/tag/2.9.0 +For more information kindly refer to migration notes https://ahriman.readthedocs.io/en/stable/migration.html +EOF +} + +_2_12_0_1_changes() { + cat << EOF +Whereas old tree is still supported it is highly recommended to migrate to the new one: + +* stop and disable all services; +* run service-tree-migrate as ahriman user; +* edit configuration to avoid pointing to the old paths; +* update synchronization services in order to support new paths (or setup redirects); +* enable web and timer services again by using x86_64-aur-clone suffix, where x86_64 is your architecture and + aur-clone is repository name; + +For more information kindly refer to migration notes https://ahriman.readthedocs.io/en/stable/migration.html EOF } diff --git a/package/lib/systemd/system/ahriman-web@.service b/package/lib/systemd/system/ahriman-web@.service index f9c8c052..7b137599 100644 --- a/package/lib/systemd/system/ahriman-web@.service +++ b/package/lib/systemd/system/ahriman-web@.service @@ -1,10 +1,10 @@ [Unit] -Description=ArcH linux ReposItory MANager web server (%I architecture) +Description=ArcH linux ReposItory MANager web server (%i) After=network.target [Service] Type=simple -ExecStart=/usr/bin/ahriman --architecture %i web +ExecStart=/usr/bin/ahriman --repository-id "%I" web User=ahriman Group=ahriman diff --git a/package/lib/systemd/system/ahriman@.service b/package/lib/systemd/system/ahriman@.service index 0fc74ed4..10b95123 100644 --- a/package/lib/systemd/system/ahriman@.service +++ b/package/lib/systemd/system/ahriman@.service @@ -1,7 +1,7 @@ [Unit] -Description=ArcH linux ReposItory MANager (%I architecture) +Description=ArcH linux ReposItory MANager (%i) [Service] -ExecStart=/usr/bin/ahriman --architecture %i repo-update --refresh +ExecStart=/usr/bin/ahriman --repository-id "%I" repo-update --refresh User=ahriman Group=ahriman \ No newline at end of file diff --git a/package/lib/systemd/system/ahriman@.timer b/package/lib/systemd/system/ahriman@.timer index 61164a83..03731030 100644 --- a/package/lib/systemd/system/ahriman@.timer +++ b/package/lib/systemd/system/ahriman@.timer @@ -1,5 +1,5 @@ [Unit] -Description=ArcH linux ReposItory MANager timer (%I architecture) +Description=ArcH linux ReposItory MANager timer (%i) [Timer] OnCalendar=daily diff --git a/package/share/ahriman/settings/ahriman.ini b/package/share/ahriman/settings/ahriman.ini index 56f96743..440660eb 100644 --- a/package/share/ahriman/settings/ahriman.ini +++ b/package/share/ahriman/settings/ahriman.ini @@ -21,7 +21,6 @@ allow_read_only = yes [build] archbuild_flags = -build_command = extra-x86_64-build ignore_packages = makechrootpkg_flags = makepkg_flags = --nocolor --ignorearch @@ -30,7 +29,6 @@ triggers_known = ahriman.core.gitremote.RemotePullTrigger ahriman.core.gitremote vcs_allowed_age = 604800 [repository] -name = aur-clone root = /var/lib/ahriman [sign] diff --git a/package/share/bash-completion/completions/_ahriman b/package/share/bash-completion/completions/_ahriman index 5fedf474..19797cb5 100644 --- a/package/share/bash-completion/completions/_ahriman +++ b/package/share/bash-completion/completions/_ahriman @@ -1,8 +1,8 @@ # AUTOMATICALLY GENERATED by `shtab` -_shtab_ahriman_subparsers=('aur-search' 'search' 'help' 'help-commands-unsafe' 'help-updates' 'help-version' 'version' 'package-add' 'add' 'package-update' 'package-remove' 'remove' 'package-status' 'status' 'package-status-remove' 'package-status-update' 'status-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'repo-backup' 'repo-check' 'check' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'daemon' 'repo-rebuild' 'rebuild' 'repo-remove-unknown' 'remove-unknown' 'repo-report' 'report' 'repo-restore' 'repo-sign' 'sign' 'repo-status-update' 'repo-sync' 'sync' 'repo-tree' 'repo-triggers' 'repo-update' 'update' 'service-clean' 'clean' 'repo-clean' 'service-config' 'config' 'repo-config' 'service-config-validate' 'config-validate' 'repo-config-validate' 'service-key-import' 'key-import' 'service-setup' 'init' 'repo-init' 'repo-setup' 'setup' 'service-shell' 'shell' 'user-add' 'user-list' 'user-remove' 'web') +_shtab_ahriman_subparsers=('aur-search' 'search' 'help' 'help-commands-unsafe' 'help-updates' 'help-version' 'version' 'package-add' 'add' 'package-update' 'package-remove' 'remove' 'package-status' 'status' 'package-status-remove' 'package-status-update' 'status-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'repo-backup' 'repo-check' 'check' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'daemon' 'repo-rebuild' 'rebuild' 'repo-remove-unknown' 'remove-unknown' 'repo-report' 'report' 'repo-restore' 'repo-sign' 'sign' 'repo-status-update' 'repo-sync' 'sync' 'repo-tree' 'repo-triggers' 'repo-update' 'update' 'service-clean' 'clean' 'repo-clean' 'service-config' 'config' 'repo-config' 'service-config-validate' 'config-validate' 'repo-config-validate' 'service-key-import' 'key-import' 'service-setup' 'init' 'repo-init' 'repo-setup' 'setup' 'service-shell' 'shell' 'service-tree-migrate' 'user-add' 'user-list' 'user-remove' 'web') -_shtab_ahriman_option_strings=('-h' '--help' '-a' '--architecture' '-c' '--configuration' '--force' '-l' '--lock' '--log-handler' '--report' '--no-report' '-q' '--quiet' '--unsafe' '--wait-timeout' '-V' '--version') +_shtab_ahriman_option_strings=('-h' '--help' '-a' '--architecture' '-c' '--configuration' '--force' '-l' '--lock' '--log-handler' '-q' '--quiet' '--report' '--no-report' '-r' '--repository' '--unsafe' '--wait-timeout' '-V' '--version') _shtab_ahriman_aur_search_option_strings=('-h' '--help' '-e' '--exit-code' '--info' '--no-info' '--sort-by') _shtab_ahriman_search_option_strings=('-h' '--help' '-e' '--exit-code' '--info' '--no-info' '--sort-by') _shtab_ahriman_help_option_strings=('-h' '--help') @@ -58,21 +58,22 @@ _shtab_ahriman_config_validate_option_strings=('-h' '--help' '-e' '--exit-code') _shtab_ahriman_repo_config_validate_option_strings=('-h' '--help' '-e' '--exit-code') _shtab_ahriman_service_key_import_option_strings=('-h' '--help' '--key-server') _shtab_ahriman_key_import_option_strings=('-h' '--help' '--key-server') -_shtab_ahriman_service_setup_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket') -_shtab_ahriman_init_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket') -_shtab_ahriman_repo_init_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket') -_shtab_ahriman_repo_setup_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket') -_shtab_ahriman_setup_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket') +_shtab_ahriman_service_setup_option_strings=('-h' '--help' '--build-as-user' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket') +_shtab_ahriman_init_option_strings=('-h' '--help' '--build-as-user' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket') +_shtab_ahriman_repo_init_option_strings=('-h' '--help' '--build-as-user' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket') +_shtab_ahriman_repo_setup_option_strings=('-h' '--help' '--build-as-user' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket') +_shtab_ahriman_setup_option_strings=('-h' '--help' '--build-as-user' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket') _shtab_ahriman_service_shell_option_strings=('-h' '--help') _shtab_ahriman_shell_option_strings=('-h' '--help') -_shtab_ahriman_user_add_option_strings=('-h' '--help' '--key' '--packager' '-p' '--password' '-r' '--role') -_shtab_ahriman_user_list_option_strings=('-h' '--help' '-e' '--exit-code' '-r' '--role') +_shtab_ahriman_service_tree_migrate_option_strings=('-h' '--help') +_shtab_ahriman_user_add_option_strings=('-h' '--help' '--key' '--packager' '-p' '--password' '-R' '--role') +_shtab_ahriman_user_list_option_strings=('-h' '--help' '-e' '--exit-code' '-R' '--role') _shtab_ahriman_user_remove_option_strings=('-h' '--help') _shtab_ahriman_web_option_strings=('-h' '--help') -_shtab_ahriman_pos_0_choices=('aur-search' 'search' 'help' 'help-commands-unsafe' 'help-updates' 'help-version' 'version' 'package-add' 'add' 'package-update' 'package-remove' 'remove' 'package-status' 'status' 'package-status-remove' 'package-status-update' 'status-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'repo-backup' 'repo-check' 'check' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'daemon' 'repo-rebuild' 'rebuild' 'repo-remove-unknown' 'remove-unknown' 'repo-report' 'report' 'repo-restore' 'repo-sign' 'sign' 'repo-status-update' 'repo-sync' 'sync' 'repo-tree' 'repo-triggers' 'repo-update' 'update' 'service-clean' 'clean' 'repo-clean' 'service-config' 'config' 'repo-config' 'service-config-validate' 'config-validate' 'repo-config-validate' 'service-key-import' 'key-import' 'service-setup' 'init' 'repo-init' 'repo-setup' 'setup' 'service-shell' 'shell' 'user-add' 'user-list' 'user-remove' 'web') +_shtab_ahriman_pos_0_choices=('aur-search' 'search' 'help' 'help-commands-unsafe' 'help-updates' 'help-version' 'version' 'package-add' 'add' 'package-update' 'package-remove' 'remove' 'package-status' 'status' 'package-status-remove' 'package-status-update' 'status-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'repo-backup' 'repo-check' 'check' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'daemon' 'repo-rebuild' 'rebuild' 'repo-remove-unknown' 'remove-unknown' 'repo-report' 'report' 'repo-restore' 'repo-sign' 'sign' 'repo-status-update' 'repo-sync' 'sync' 'repo-tree' 'repo-triggers' 'repo-update' 'update' 'service-clean' 'clean' 'repo-clean' 'service-config' 'config' 'repo-config' 'service-config-validate' 'config-validate' 'repo-config-validate' 'service-key-import' 'key-import' 'service-setup' 'init' 'repo-init' 'repo-setup' 'setup' 'service-shell' 'shell' 'service-tree-migrate' 'user-add' 'user-list' 'user-remove' 'web') _shtab_ahriman___log_handler_choices=('console' 'syslog' 'journald') _shtab_ahriman_aur_search___sort_by_choices=('description' 'first_submitted' 'id' 'last_modified' 'maintainer' 'name' 'num_votes' 'out_of_date' 'package_base' 'package_base_id' 'popularity' 'repository' 'submitter' 'url' 'url_path' 'version') _shtab_ahriman_search___sort_by_choices=('description' 'first_submitted' 'id' 'last_modified' 'maintainer' 'name' 'num_votes' 'out_of_date' 'package_base' 'package_base_id' 'popularity' 'repository' 'submitter' 'url' 'url_path' 'version') @@ -101,19 +102,19 @@ _shtab_ahriman_init___sign_target_choices=('disabled' 'packages' 'repository') _shtab_ahriman_repo_init___sign_target_choices=('disabled' 'packages' 'repository') _shtab_ahriman_repo_setup___sign_target_choices=('disabled' 'packages' 'repository') _shtab_ahriman_setup___sign_target_choices=('disabled' 'packages' 'repository') -_shtab_ahriman_user_add__r_choices=('unauthorized' 'read' 'reporter' 'full') +_shtab_ahriman_user_add__R_choices=('unauthorized' 'read' 'reporter' 'full') _shtab_ahriman_user_add___role_choices=('unauthorized' 'read' 'reporter' 'full') -_shtab_ahriman_user_list__r_choices=('unauthorized' 'read' 'reporter' 'full') +_shtab_ahriman_user_list__R_choices=('unauthorized' 'read' 'reporter' 'full') _shtab_ahriman_user_list___role_choices=('unauthorized' 'read' 'reporter' 'full') _shtab_ahriman_pos_0_nargs=A... _shtab_ahriman__h_nargs=0 _shtab_ahriman___help_nargs=0 _shtab_ahriman___force_nargs=0 -_shtab_ahriman___report_nargs=0 -_shtab_ahriman___no_report_nargs=0 _shtab_ahriman__q_nargs=0 _shtab_ahriman___quiet_nargs=0 +_shtab_ahriman___report_nargs=0 +_shtab_ahriman___no_report_nargs=0 _shtab_ahriman___unsafe_nargs=0 _shtab_ahriman__V_nargs=0 _shtab_ahriman___version_nargs=0 @@ -473,6 +474,8 @@ _shtab_ahriman_shell__h_nargs=0 _shtab_ahriman_shell___help_nargs=0 _shtab_ahriman_shell__v_nargs=0 _shtab_ahriman_shell___verbose_nargs=0 +_shtab_ahriman_service_tree_migrate__h_nargs=0 +_shtab_ahriman_service_tree_migrate___help_nargs=0 _shtab_ahriman_user_add__h_nargs=0 _shtab_ahriman_user_add___help_nargs=0 _shtab_ahriman_user_list__h_nargs=0 diff --git a/package/share/man/man1/ahriman.1 b/package/share/man/man1/ahriman.1 index cab20277..6115d6bd 100644 --- a/package/share/man/man1/ahriman.1 +++ b/package/share/man/man1/ahriman.1 @@ -1,9 +1,9 @@ -.TH AHRIMAN "1" "2023\-08\-26" "ahriman" "Generated Python Manual" +.TH AHRIMAN "1" "2023\-09\-02" "ahriman" "Generated Python Manual" .SH NAME ahriman .SH SYNOPSIS .B ahriman -[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--log-handler {console,syslog,journald}] [--report | --no-report] [-q] [--unsafe] [--wait-timeout WAIT_TIMEOUT] [-V] {aur-search,search,help,help-commands-unsafe,help-updates,help-version,version,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,patch-set-add,repo-backup,repo-check,check,repo-create-keyring,repo-create-mirrorlist,repo-daemon,daemon,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-sign,sign,repo-status-update,repo-sync,sync,repo-tree,repo-triggers,repo-update,update,service-clean,clean,repo-clean,service-config,config,repo-config,service-config-validate,config-validate,repo-config-validate,service-key-import,key-import,service-setup,init,repo-init,repo-setup,setup,service-shell,shell,user-add,user-list,user-remove,web} ... +[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--log-handler {console,syslog,journald}] [-q] [--report | --no-report] [-r REPOSITORY] [--unsafe] [--wait-timeout WAIT_TIMEOUT] [-V] {aur-search,search,help,help-commands-unsafe,help-updates,help-version,version,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,patch-set-add,repo-backup,repo-check,check,repo-create-keyring,repo-create-mirrorlist,repo-daemon,daemon,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-sign,sign,repo-status-update,repo-sync,sync,repo-tree,repo-triggers,repo-update,update,service-clean,clean,repo-clean,service-config,config,repo-config,service-config-validate,config-validate,repo-config-validate,service-key-import,key-import,service-setup,init,repo-init,repo-setup,setup,service-shell,shell,service-tree-migrate,user-add,user-list,user-remove,web} ... .SH DESCRIPTION ArcH linux ReposItory MANager @@ -28,13 +28,17 @@ lock file \fB\-\-log\-handler\fR \fI\,{console,syslog,journald}\/\fR explicit log handler specification. If none set, the handler will be guessed from environment +.TP +\fB\-q\fR, \fB\-\-quiet\fR +force disable any logging + .TP \fB\-\-report\fR, \fB\-\-no\-report\fR force enable or disable reporting to web service .TP -\fB\-q\fR, \fB\-\-quiet\fR -force disable any logging +\fB\-r\fR \fI\,REPOSITORY\/\fR, \fB\-\-repository\fR \fI\,REPOSITORY\/\fR +target repository. For several subcommands it can be used multiple times .TP \fB\-\-unsafe\fR @@ -157,6 +161,9 @@ initial service configuration \fBahriman\fR \fI\,service\-shell\/\fR invoke python shell .TP +\fBahriman\fR \fI\,service\-tree\-migrate\/\fR +migrate repository tree +.TP \fBahriman\fR \fI\,user\-add\/\fR create or update user .TP @@ -691,11 +698,10 @@ PGP key to import from public server key server for key import .SH COMMAND \fI\,'ahriman service\-setup'\/\fR -usage: ahriman service\-setup [\-h] [\-\-build\-as\-user BUILD_AS_USER] [\-\-build\-command BUILD_COMMAND] - [\-\-from\-configuration FROM_CONFIGURATION] [\-\-generate\-salt | \-\-no\-generate\-salt] - [\-\-makeflags\-jobs | \-\-no\-makeflags\-jobs] [\-\-mirror MIRROR] [\-\-multilib | \-\-no\-multilib] - \-\-packager PACKAGER \-\-repository REPOSITORY [\-\-server SERVER] [\-\-sign\-key SIGN_KEY] - [\-\-sign\-target {disabled,packages,repository}] [\-\-web\-port WEB_PORT] +usage: ahriman service\-setup [\-h] [\-\-build\-as\-user BUILD_AS_USER] [\-\-from\-configuration FROM_CONFIGURATION] + [\-\-generate\-salt | \-\-no\-generate\-salt] [\-\-makeflags\-jobs | \-\-no\-makeflags\-jobs] + [\-\-mirror MIRROR] [\-\-multilib | \-\-no\-multilib] \-\-packager PACKAGER [\-\-server SERVER] + [\-\-sign\-key SIGN_KEY] [\-\-sign\-target {disabled,packages,repository}] [\-\-web\-port WEB_PORT] [\-\-web\-unix\-socket WEB_UNIX_SOCKET] create initial service configuration, requires root @@ -705,10 +711,6 @@ create initial service configuration, requires root \fB\-\-build\-as\-user\fR \fI\,BUILD_AS_USER\/\fR force makepkg user to the specific one -.TP -\fB\-\-build\-command\fR \fI\,BUILD_COMMAND\/\fR -build command prefix - .TP \fB\-\-from\-configuration\fR \fI\,FROM_CONFIGURATION\/\fR path to default devtools pacman configuration @@ -733,10 +735,6 @@ add or do not multilib repository \fB\-\-packager\fR \fI\,PACKAGER\/\fR packager name and email -.TP -\fB\-\-repository\fR \fI\,REPOSITORY\/\fR -repository name - .TP \fB\-\-server\fR \fI\,SERVER\/\fR server to be used for devtools. If none set, local files will be used @@ -766,8 +764,13 @@ drop into python shell \fBcode\fR instead of dropping into shell, just execute the specified code +.SH COMMAND \fI\,'ahriman service\-tree\-migrate'\/\fR +usage: ahriman service\-tree\-migrate [\-h] + +migrate repository tree between versions + .SH COMMAND \fI\,'ahriman user\-add'\/\fR -usage: ahriman user\-add [\-h] [\-\-key KEY] [\-\-packager PACKAGER] [\-p PASSWORD] [\-r {unauthorized,read,reporter,full}] +usage: ahriman user\-add [\-h] [\-\-key KEY] [\-\-packager PACKAGER] [\-p PASSWORD] [\-R {unauthorized,read,reporter,full}] username update user for web services with the given password and role. In case if password was not entered it will be asked interactively @@ -791,11 +794,11 @@ user password. Blank password will be treated as empty password, which is in par authorization type. .TP -\fB\-r\fR \fI\,{unauthorized,read,reporter,full}\/\fR, \fB\-\-role\fR \fI\,{unauthorized,read,reporter,full}\/\fR +\fB\-R\fR \fI\,{unauthorized,read,reporter,full}\/\fR, \fB\-\-role\fR \fI\,{unauthorized,read,reporter,full}\/\fR user access level .SH COMMAND \fI\,'ahriman user\-list'\/\fR -usage: ahriman user\-list [\-h] [\-e] [\-r {unauthorized,read,reporter,full}] [username] +usage: ahriman user\-list [\-h] [\-e] [\-R {unauthorized,read,reporter,full}] [username] list users from the user mapping and their roles @@ -809,7 +812,7 @@ filter users by username return non\-zero exit status if result is empty .TP -\fB\-r\fR \fI\,{unauthorized,read,reporter,full}\/\fR, \fB\-\-role\fR \fI\,{unauthorized,read,reporter,full}\/\fR +\fB\-R\fR \fI\,{unauthorized,read,reporter,full}\/\fR, \fB\-\-role\fR \fI\,{unauthorized,read,reporter,full}\/\fR filter users by role .SH COMMAND \fI\,'ahriman user\-remove'\/\fR diff --git a/package/share/zsh/site-functions/_ahriman b/package/share/zsh/site-functions/_ahriman index a6649f6f..d964add6 100644 --- a/package/share/zsh/site-functions/_ahriman +++ b/package/share/zsh/site-functions/_ahriman @@ -59,6 +59,7 @@ _shtab_ahriman_commands() { "service-key-import:import PGP key from public sources to the repository user" "service-setup:create initial service configuration, requires root" "service-shell:drop into python shell" + "service-tree-migrate:migrate repository tree between versions" "setup:create initial service configuration, requires root" "shell:drop into python shell" "sign:(re-)sign packages and repository database according to current settings" @@ -82,8 +83,9 @@ _shtab_ahriman_options=( "--force[force run, remove file lock (default\: False)]" {-l,--lock}"[lock file (default\: \/tmp\/ahriman.lock)]:lock:" "--log-handler[explicit log handler specification. If none set, the handler will be guessed from environment (default\: None)]:log_handler:(console syslog journald)" - {--report,--no-report}"[force enable or disable reporting to web service (default\: True)]:report:" {-q,--quiet}"[force disable any logging (default\: False)]" + {--report,--no-report}"[force enable or disable reporting to web service (default\: True)]:report:" + "*"{-r,--repository}"[target repository. For several subcommands it can be used multiple times (default\: None)]:repository:" "--unsafe[allow to run ahriman as non-ahriman user. Some actions might be unavailable (default\: False)]" "--wait-timeout[wait for lock to be free. Negative value will lead to immediate application run even if there is lock file. In case of zero value, the application will wait infinitely (default\: -1)]:wait_timeout:" "(- : *)"{-V,--version}"[show program\'s version number and exit]" @@ -169,14 +171,12 @@ _shtab_ahriman_help_version_options=( _shtab_ahriman_init_options=( "(- : *)"{-h,--help}"[show this help message and exit]" "--build-as-user[force makepkg user to the specific one (default\: None)]:build_as_user:" - "--build-command[build command prefix (default\: ahriman)]:build_command:" "--from-configuration[path to default devtools pacman configuration (default\: \/usr\/share\/devtools\/pacman.conf.d\/extra.conf)]:from_configuration:" {--generate-salt,--no-generate-salt}"[generate salt for user passwords (default\: False)]:generate_salt:" {--makeflags-jobs,--no-makeflags-jobs}"[append MAKEFLAGS variable with parallelism set to number of cores (default\: True)]:makeflags_jobs:" "--mirror[use the specified explicitly mirror instead of including mirrorlist (default\: None)]:mirror:" {--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:" "--packager[packager name and email (default\: None)]:packager:" - "--repository[repository name (default\: None)]:repository:" "--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:" "--sign-key[sign key id (default\: None)]:sign_key:" "*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)" @@ -340,14 +340,12 @@ _shtab_ahriman_repo_daemon_options=( _shtab_ahriman_repo_init_options=( "(- : *)"{-h,--help}"[show this help message and exit]" "--build-as-user[force makepkg user to the specific one (default\: None)]:build_as_user:" - "--build-command[build command prefix (default\: ahriman)]:build_command:" "--from-configuration[path to default devtools pacman configuration (default\: \/usr\/share\/devtools\/pacman.conf.d\/extra.conf)]:from_configuration:" {--generate-salt,--no-generate-salt}"[generate salt for user passwords (default\: False)]:generate_salt:" {--makeflags-jobs,--no-makeflags-jobs}"[append MAKEFLAGS variable with parallelism set to number of cores (default\: True)]:makeflags_jobs:" "--mirror[use the specified explicitly mirror instead of including mirrorlist (default\: None)]:mirror:" {--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:" "--packager[packager name and email (default\: None)]:packager:" - "--repository[repository name (default\: None)]:repository:" "--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:" "--sign-key[sign key id (default\: None)]:sign_key:" "*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)" @@ -384,14 +382,12 @@ _shtab_ahriman_repo_restore_options=( _shtab_ahriman_repo_setup_options=( "(- : *)"{-h,--help}"[show this help message and exit]" "--build-as-user[force makepkg user to the specific one (default\: None)]:build_as_user:" - "--build-command[build command prefix (default\: ahriman)]:build_command:" "--from-configuration[path to default devtools pacman configuration (default\: \/usr\/share\/devtools\/pacman.conf.d\/extra.conf)]:from_configuration:" {--generate-salt,--no-generate-salt}"[generate salt for user passwords (default\: False)]:generate_salt:" {--makeflags-jobs,--no-makeflags-jobs}"[append MAKEFLAGS variable with parallelism set to number of cores (default\: True)]:makeflags_jobs:" "--mirror[use the specified explicitly mirror instead of including mirrorlist (default\: None)]:mirror:" {--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:" "--packager[packager name and email (default\: None)]:packager:" - "--repository[repository name (default\: None)]:repository:" "--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:" "--sign-key[sign key id (default\: None)]:sign_key:" "*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)" @@ -478,14 +474,12 @@ _shtab_ahriman_service_key_import_options=( _shtab_ahriman_service_setup_options=( "(- : *)"{-h,--help}"[show this help message and exit]" "--build-as-user[force makepkg user to the specific one (default\: None)]:build_as_user:" - "--build-command[build command prefix (default\: ahriman)]:build_command:" "--from-configuration[path to default devtools pacman configuration (default\: \/usr\/share\/devtools\/pacman.conf.d\/extra.conf)]:from_configuration:" {--generate-salt,--no-generate-salt}"[generate salt for user passwords (default\: False)]:generate_salt:" {--makeflags-jobs,--no-makeflags-jobs}"[append MAKEFLAGS variable with parallelism set to number of cores (default\: True)]:makeflags_jobs:" "--mirror[use the specified explicitly mirror instead of including mirrorlist (default\: None)]:mirror:" {--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:" "--packager[packager name and email (default\: None)]:packager:" - "--repository[repository name (default\: None)]:repository:" "--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:" "--sign-key[sign key id (default\: None)]:sign_key:" "*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)" @@ -498,17 +492,19 @@ _shtab_ahriman_service_shell_options=( ":instead of dropping into shell, just execute the specified code (default\: None):" ) +_shtab_ahriman_service_tree_migrate_options=( + "(- : *)"{-h,--help}"[show this help message and exit]" +) + _shtab_ahriman_setup_options=( "(- : *)"{-h,--help}"[show this help message and exit]" "--build-as-user[force makepkg user to the specific one (default\: None)]:build_as_user:" - "--build-command[build command prefix (default\: ahriman)]:build_command:" "--from-configuration[path to default devtools pacman configuration (default\: \/usr\/share\/devtools\/pacman.conf.d\/extra.conf)]:from_configuration:" {--generate-salt,--no-generate-salt}"[generate salt for user passwords (default\: False)]:generate_salt:" {--makeflags-jobs,--no-makeflags-jobs}"[append MAKEFLAGS variable with parallelism set to number of cores (default\: True)]:makeflags_jobs:" "--mirror[use the specified explicitly mirror instead of including mirrorlist (default\: None)]:mirror:" {--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:" "--packager[packager name and email (default\: None)]:packager:" - "--repository[repository name (default\: None)]:repository:" "--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:" "--sign-key[sign key id (default\: None)]:sign_key:" "*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)" @@ -565,14 +561,14 @@ _shtab_ahriman_user_add_options=( "--key[optional PGP key used by this user. The private key must be imported (default\: None)]:key:" "--packager[optional packager id used for build process in form of \`Name Surname \\` (default\: None)]:packager:" {-p,--password}"[user password. Blank password will be treated as empty password, which is in particular must be used for OAuth2 authorization type. (default\: None)]:password:" - {-r,--role}"[user access level (default\: UserAccess.Read)]:role:(unauthorized read reporter full)" + {-R,--role}"[user access level (default\: UserAccess.Read)]:role:(unauthorized read reporter full)" ":username for web service:" ) _shtab_ahriman_user_list_options=( "(- : *)"{-h,--help}"[show this help message and exit]" {-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]" - {-r,--role}"[filter users by role (default\: None)]:role:(unauthorized read reporter full)" + {-R,--role}"[filter users by role (default\: None)]:role:(unauthorized read reporter full)" ":filter users by username (default\: None):" ) @@ -658,6 +654,7 @@ _shtab_ahriman() { service-key-import) _arguments -C -s $_shtab_ahriman_service_key_import_options ;; service-setup) _arguments -C -s $_shtab_ahriman_service_setup_options ;; service-shell) _arguments -C -s $_shtab_ahriman_service_shell_options ;; + service-tree-migrate) _arguments -C -s $_shtab_ahriman_service_tree_migrate_options ;; setup) _arguments -C -s $_shtab_ahriman_setup_options ;; shell) _arguments -C -s $_shtab_ahriman_shell_options ;; sign) _arguments -C -s $_shtab_ahriman_sign_options ;; diff --git a/src/ahriman/application/ahriman.py b/src/ahriman/application/ahriman.py index bca4c4d6..bf85efa3 100644 --- a/src/ahriman/application/ahriman.py +++ b/src/ahriman/application/ahriman.py @@ -79,9 +79,14 @@ def _parser() -> argparse.ArgumentParser: parser.add_argument("--log-handler", help="explicit log handler specification. If none set, the handler will be " "guessed from environment", type=LogHandler, choices=enum_values(LogHandler)) + parser.add_argument("-q", "--quiet", help="force disable any logging", action="store_true") parser.add_argument("--report", help="force enable or disable reporting to web service", action=argparse.BooleanOptionalAction, default=True) - parser.add_argument("-q", "--quiet", help="force disable any logging", action="store_true") + parser.add_argument("-r", "--repository", help="target repository. For several subcommands it can be used " + "multiple times", action="append") + # special secret argument for systemd unit. The issue is that systemd doesn't allow multiple arguments to template + # name. This parameter accepts [[arch]-repo] in order to keep backward compatibility + parser.add_argument("--repository-id", help=argparse.SUPPRESS) parser.add_argument("--unsafe", help="allow to run ahriman as non-ahriman user. Some actions might be unavailable", action="store_true") parser.add_argument("--wait-timeout", help="wait for lock to be free. Negative value will lead to " @@ -127,6 +132,7 @@ def _parser() -> argparse.ArgumentParser: _set_service_key_import_parser(subparsers) _set_service_setup_parser(subparsers) _set_service_shell_parser(subparsers) + _set_service_tree_migrate_parser(subparsers) _set_user_add_parser(subparsers) _set_user_list_parser(subparsers) _set_user_remove_parser(subparsers) @@ -155,7 +161,8 @@ def _set_aur_search_parser(root: SubParserAction) -> argparse.ArgumentParser: parser.add_argument("--sort-by", help="sort field by this field. In case if two packages have the same value of " "the specified field, they will be always sorted by name", default="name", choices=sorted(handlers.Search.SORT_FIELDS)) - parser.set_defaults(handler=handlers.Search, architecture=[""], lock=None, report=False, quiet=True, unsafe=True) + parser.set_defaults(handler=handlers.Search, architecture=[""], lock=None, quiet=True, report=False, + repository=[""], unsafe=True) return parser @@ -173,8 +180,8 @@ def _set_help_parser(root: SubParserAction) -> argparse.ArgumentParser: description="show help message for application or command and exit", formatter_class=_formatter) parser.add_argument("command", help="show help message for specific command", nargs="?") - parser.set_defaults(handler=handlers.Help, architecture=[""], lock=None, report=False, quiet=True, unsafe=True, - parser=_parser) + parser.set_defaults(handler=handlers.Help, architecture=[""], lock=None, quiet=True, report=False, repository=[""], + unsafe=True, parser=_parser) return parser @@ -192,8 +199,8 @@ def _set_help_commands_unsafe_parser(root: SubParserAction) -> argparse.Argument 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", nargs="*") - parser.set_defaults(handler=handlers.UnsafeCommands, architecture=[""], lock=None, report=False, quiet=True, - unsafe=True, parser=_parser) + parser.set_defaults(handler=handlers.UnsafeCommands, architecture=[""], lock=None, quiet=True, report=False, + repository=[""], unsafe=True, parser=_parser) return parser @@ -211,8 +218,8 @@ def _set_help_updates_parser(root: SubParserAction) -> argparse.ArgumentParser: description="request AUR for current version and compare with current service version", formatter_class=_formatter) parser.add_argument("-e", "--exit-code", help="return non-zero exit code if updates available", action="store_true") - parser.set_defaults(handler=handlers.ServiceUpdates, architecture=[""], lock=None, report=False, quiet=True, - unsafe=True) + parser.set_defaults(handler=handlers.ServiceUpdates, architecture=[""], lock=None, quiet=True, report=False, + repository=[""], unsafe=True) return parser @@ -228,7 +235,8 @@ def _set_help_version_parser(root: SubParserAction) -> argparse.ArgumentParser: """ parser = root.add_parser("help-version", aliases=["version"], help="application version", description="print application and its dependencies versions", formatter_class=_formatter) - parser.set_defaults(handler=handlers.Versions, architecture=[""], lock=None, report=False, quiet=True, unsafe=True) + parser.set_defaults(handler=handlers.Versions, architecture=[""], lock=None, quiet=True, report=False, + repository=[""], unsafe=True) return parser @@ -310,7 +318,7 @@ def _set_package_status_parser(root: SubParserAction) -> argparse.ArgumentParser action=argparse.BooleanOptionalAction, default=False) parser.add_argument("-s", "--status", help="filter packages by status", type=BuildStatusEnum, choices=enum_values(BuildStatusEnum)) - parser.set_defaults(handler=handlers.Status, lock=None, report=False, quiet=True, unsafe=True) + parser.set_defaults(handler=handlers.Status, lock=None, quiet=True, report=False, unsafe=True) return parser @@ -330,7 +338,7 @@ def _set_package_status_remove_parser(root: SubParserAction) -> argparse.Argumen "clears the status page.", formatter_class=_formatter) parser.add_argument("package", help="remove specified packages from status page", nargs="+") - parser.set_defaults(handler=handlers.StatusUpdate, action=Action.Remove, lock=None, report=False, quiet=True, + parser.set_defaults(handler=handlers.StatusUpdate, action=Action.Remove, lock=None, quiet=True, report=False, unsafe=True) return parser @@ -352,7 +360,7 @@ def _set_package_status_update_parser(root: SubParserAction) -> argparse.Argumen nargs="*") parser.add_argument("-s", "--status", help="new package build status", type=BuildStatusEnum, choices=enum_values(BuildStatusEnum), default=BuildStatusEnum.Success) - parser.set_defaults(handler=handlers.StatusUpdate, action=Action.Update, lock=None, report=False, quiet=True, + parser.set_defaults(handler=handlers.StatusUpdate, action=Action.Update, lock=None, quiet=True, report=False, unsafe=True) return parser @@ -379,7 +387,8 @@ def _set_patch_add_parser(root: SubParserAction) -> argparse.ArgumentParser: "it must end with ()") parser.add_argument("patch", help="path to file which contains function or variable value. If not set, " "the value will be read from stdin", type=Path, nargs="?") - parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture=[""], lock=None, report=False) + parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture=[""], lock=None, report=False, + repository=[""]) return parser @@ -400,7 +409,7 @@ def _set_patch_list_parser(root: SubParserAction) -> argparse.ArgumentParser: parser.add_argument("-v", "--variable", help="if set, show only patches for specified PKGBUILD variables", action="append") parser.set_defaults(handler=handlers.Patch, action=Action.List, architecture=[""], lock=None, report=False, - unsafe=True) + repository=[""], unsafe=True) return parser @@ -421,7 +430,8 @@ def _set_patch_remove_parser(root: SubParserAction) -> argparse.ArgumentParser: "to remove only specified PKGBUILD variables. In case if not set, " "it will remove all patches related to the package", action="append") - parser.set_defaults(handler=handlers.Patch, action=Action.Remove, architecture=[""], lock=None, report=False) + parser.set_defaults(handler=handlers.Patch, action=Action.Remove, architecture=[""], lock=None, report=False, + repository=[""]) return parser @@ -446,7 +456,7 @@ def _set_patch_set_add_parser(root: SubParserAction) -> argparse.ArgumentParser: parser.add_argument("-t", "--track", help="files which has to be tracked", action="append", default=["*.diff", "*.patch"]) parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture=[""], lock=None, report=False, - variable=None) + repository=[""], variable=None) return parser @@ -463,7 +473,8 @@ def _set_repo_backup_parser(root: SubParserAction) -> argparse.ArgumentParser: parser = root.add_parser("repo-backup", help="backup repository data", description="backup repository settings and database", formatter_class=_formatter) parser.add_argument("path", help="path of the output archive", type=Path) - parser.set_defaults(handler=handlers.Backup, architecture=[""], lock=None, report=False, unsafe=True) + parser.set_defaults(handler=handlers.Backup, architecture=[""], lock=None, report=False, repository=[""], + unsafe=True) return parser @@ -642,7 +653,8 @@ def _set_repo_restore_parser(root: SubParserAction) -> argparse.ArgumentParser: description="restore settings and database", formatter_class=_formatter) parser.add_argument("path", help="path of the input archive", type=Path) parser.add_argument("-o", "--output", help="root path of the extracted files", type=Path, default=Path("/")) - parser.set_defaults(handler=handlers.Restore, architecture=[""], lock=None, report=False, unsafe=True) + parser.set_defaults(handler=handlers.Restore, architecture=[""], lock=None, report=False, repository=[""], + unsafe=True) return parser @@ -679,8 +691,8 @@ def _set_repo_status_update_parser(root: SubParserAction) -> argparse.ArgumentPa description="update repository status on the status page", formatter_class=_formatter) parser.add_argument("-s", "--status", help="new status", type=BuildStatusEnum, choices=enum_values(BuildStatusEnum), default=BuildStatusEnum.Success) - parser.set_defaults(handler=handlers.StatusUpdate, action=Action.Update, lock=None, report=False, package=[], - quiet=True, unsafe=True) + parser.set_defaults(handler=handlers.StatusUpdate, action=Action.Update, lock=None, package=[], quiet=True, + report=False, unsafe=True) return parser @@ -717,7 +729,7 @@ def _set_repo_tree_parser(root: SubParserAction) -> argparse.ArgumentParser: formatter_class=_formatter) parser.add_argument("-p", "--partitions", help="also divide packages by independent partitions", type=int, default=1) - parser.set_defaults(handler=handlers.Structure, lock=None, report=False, quiet=True, unsafe=True) + parser.set_defaults(handler=handlers.Structure, lock=None, quiet=True, report=False, unsafe=True) return parser @@ -820,7 +832,7 @@ def _set_service_config_parser(root: SubParserAction) -> argparse.ArgumentParser formatter_class=_formatter) parser.add_argument("--secure", help="hide passwords and secrets from output", action=argparse.BooleanOptionalAction, default=True) - parser.set_defaults(handler=handlers.Dump, lock=None, report=False, quiet=True, unsafe=True) + parser.set_defaults(handler=handlers.Dump, lock=None, quiet=True, report=False, unsafe=True) return parser @@ -840,7 +852,7 @@ def _set_service_config_validate_parser(root: SubParserAction) -> argparse.Argum formatter_class=_formatter) parser.add_argument("-e", "--exit-code", help="return non-zero exit status if configuration is invalid", action="store_true") - parser.set_defaults(handler=handlers.Validate, lock=None, report=False, quiet=True, unsafe=True) + parser.set_defaults(handler=handlers.Validate, lock=None, quiet=True, report=False, unsafe=True) return parser @@ -863,7 +875,7 @@ def _set_service_key_import_parser(root: SubParserAction) -> argparse.ArgumentPa formatter_class=_formatter) parser.add_argument("--key-server", help="key server for key import", default="keyserver.ubuntu.com") parser.add_argument("key", help="PGP key to import from public server") - parser.set_defaults(handler=handlers.KeyImport, architecture=[""], lock=None, report=False) + parser.set_defaults(handler=handlers.KeyImport, architecture=[""], lock=None, report=False, repository=[""]) return parser @@ -883,7 +895,6 @@ def _set_service_setup_parser(root: SubParserAction) -> argparse.ArgumentParser: epilog="Create _minimal_ configuration for the service according to provided options.", formatter_class=_formatter) parser.add_argument("--build-as-user", help="force makepkg user to the specific one") - parser.add_argument("--build-command", help="build command prefix", default="ahriman") parser.add_argument("--from-configuration", help="path to default devtools pacman configuration", type=Path, default=Path("/usr") / "share" / "devtools" / "pacman.conf.d" / "extra.conf") parser.add_argument("--generate-salt", help="generate salt for user passwords", @@ -894,14 +905,13 @@ def _set_service_setup_parser(root: SubParserAction) -> argparse.ArgumentParser: parser.add_argument("--multilib", help="add or do not multilib repository", action=argparse.BooleanOptionalAction, default=True) parser.add_argument("--packager", help="packager name and email", required=True) - parser.add_argument("--repository", help="repository name", required=True) parser.add_argument("--server", help="server to be used for devtools. If none set, local files will be used") parser.add_argument("--sign-key", help="sign key id") parser.add_argument("--sign-target", help="sign options", action="append", type=SignSettings.from_option, choices=enum_values(SignSettings)) parser.add_argument("--web-port", help="port of the web service", type=int) parser.add_argument("--web-unix-socket", help="path to unix socket used for interprocess communications", type=Path) - parser.set_defaults(handler=handlers.Setup, lock=None, report=False, quiet=True, unsafe=True) + parser.set_defaults(handler=handlers.Setup, lock=None, quiet=True, report=False, unsafe=True) return parser @@ -923,6 +933,22 @@ def _set_service_shell_parser(root: SubParserAction) -> argparse.ArgumentParser: return parser +def _set_service_tree_migrate_parser(root: SubParserAction) -> argparse.ArgumentParser: + """ + add parser for tree migration subcommand + + Args: + root(SubParserAction): subparsers for the commands + + Returns: + argparse.ArgumentParser: created argument parser + """ + parser = root.add_parser("service-tree-migrate", help="migrate repository tree", + description="migrate repository tree between versions", formatter_class=_formatter) + parser.set_defaults(handler=handlers.TreeMigrate, lock=None, quiet=True, report=False) + return parser + + def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser: """ add parser for create user subcommand @@ -943,10 +969,10 @@ def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser: "`Name Surname `") 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", + parser.add_argument("-R", "--role", help="user access level", type=UserAccess, choices=enum_values(UserAccess), default=UserAccess.Read) - parser.set_defaults(handler=handlers.Users, action=Action.Update, architecture=[""], lock=None, report=False, - quiet=True) + parser.set_defaults(handler=handlers.Users, action=Action.Update, architecture=[""], lock=None, quiet=True, + report=False, repository=[""]) return parser @@ -965,9 +991,9 @@ def _set_user_list_parser(root: SubParserAction) -> argparse.ArgumentParser: formatter_class=_formatter) parser.add_argument("username", help="filter users by username", nargs="?") parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true") - parser.add_argument("-r", "--role", help="filter users by role", type=UserAccess, choices=enum_values(UserAccess)) - parser.set_defaults(handler=handlers.Users, action=Action.List, architecture=[""], lock=None, report=False, - quiet=True, unsafe=True) + parser.add_argument("-R", "--role", help="filter users by role", type=UserAccess, choices=enum_values(UserAccess)) + parser.set_defaults(handler=handlers.Users, action=Action.List, architecture=[""], lock=None, quiet=True, + report=False, repository=[""], unsafe=True) return parser @@ -985,8 +1011,8 @@ 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.set_defaults(handler=handlers.Users, action=Action.Remove, architecture=[""], lock=None, report=False, - quiet=True) + parser.set_defaults(handler=handlers.Users, action=Action.Remove, architecture=[""], lock=None, quiet=True, + report=False, repository=[""]) return parser diff --git a/src/ahriman/application/application/application.py b/src/ahriman/application/application/application.py index a17bc3a6..2a2baed8 100644 --- a/src/ahriman/application/application/application.py +++ b/src/ahriman/application/application/application.py @@ -37,9 +37,10 @@ class Application(ApplicationPackages, ApplicationRepository): >>> from ahriman.core.configuration import Configuration >>> from ahriman.models.package_source import PackageSource + >>> from ahriman.models.repository_id import RepositoryId >>> >>> configuration = Configuration() - >>> application = Application("x86_64", configuration, report=True) + >>> application = Application(RepositoryId("x86_64", "x86_64"), configuration, report=True) >>> # add packages to build queue >>> application.add(["ahriman"], PackageSource.AUR) >>> diff --git a/src/ahriman/application/application/application_properties.py b/src/ahriman/application/application/application_properties.py index 380b39fa..f5aa0229 100644 --- a/src/ahriman/application/application/application_properties.py +++ b/src/ahriman/application/application/application_properties.py @@ -22,6 +22,7 @@ from ahriman.core.database import SQLite from ahriman.core.log import LazyLogging from ahriman.core.repository import Repository from ahriman.models.pacman_synchronization import PacmanSynchronization +from ahriman.models.repository_id import RepositoryId class ApplicationProperties(LazyLogging): @@ -29,26 +30,36 @@ class ApplicationProperties(LazyLogging): application base properties class Attributes: - architecture(str): repository architecture configuration(Configuration): configuration instance database(SQLite): database instance repository(Repository): repository instance + repository_id(RepositoryId): repository unique identifier """ - def __init__(self, architecture: str, configuration: Configuration, *, report: bool, + def __init__(self, repository_id: RepositoryId, configuration: Configuration, *, report: bool, refresh_pacman_database: PacmanSynchronization = PacmanSynchronization.Disabled) -> None: """ default constructor Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting refresh_pacman_database(PacmanSynchronization, optional): pacman database synchronization level (Default value = PacmanSynchronization.Disabled) """ self.configuration = configuration - self.architecture = architecture + self.repository_id = repository_id self.database = SQLite.load(configuration) - self.repository = Repository.load(architecture, configuration, self.database, report=report, + self.repository = Repository.load(repository_id, configuration, self.database, report=report, refresh_pacman_database=refresh_pacman_database) + + @property + def architecture(self) -> str: + """ + repository architecture for backward compatibility + + Returns: + str: repository architecture + """ + return self.repository_id.architecture diff --git a/src/ahriman/application/handlers/__init__.py b/src/ahriman/application/handlers/__init__.py index 980c1d1c..e53de728 100644 --- a/src/ahriman/application/handlers/__init__.py +++ b/src/ahriman/application/handlers/__init__.py @@ -39,6 +39,7 @@ from ahriman.application.handlers.sign import Sign from ahriman.application.handlers.status import Status from ahriman.application.handlers.status_update import StatusUpdate from ahriman.application.handlers.structure import Structure +from ahriman.application.handlers.tree_migrate import TreeMigrate from ahriman.application.handlers.triggers import Triggers from ahriman.application.handlers.unsafe_commands import UnsafeCommands from ahriman.application.handlers.update import Update diff --git a/src/ahriman/application/handlers/add.py b/src/ahriman/application/handlers/add.py index b42e1ee4..2892f004 100644 --- a/src/ahriman/application/handlers/add.py +++ b/src/ahriman/application/handlers/add.py @@ -23,6 +23,7 @@ from ahriman.application.application import Application from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration from ahriman.models.packagers import Packagers +from ahriman.models.repository_id import RepositoryId class Add(Handler): @@ -31,17 +32,18 @@ class Add(Handler): """ @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ - application = Application(architecture, configuration, report=report, refresh_pacman_database=args.refresh) + application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh) application.on_start() application.add(args.package, args.source, args.username) if not args.now: diff --git a/src/ahriman/application/handlers/backup.py b/src/ahriman/application/handlers/backup.py index 4bac517d..c01ac485 100644 --- a/src/ahriman/application/handlers/backup.py +++ b/src/ahriman/application/handlers/backup.py @@ -26,6 +26,7 @@ from tarfile import TarFile from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration from ahriman.core.database import SQLite +from ahriman.models.repository_id import RepositoryId class Backup(Handler): @@ -33,16 +34,17 @@ class Backup(Handler): backup packages handler """ - ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture" + ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ diff --git a/src/ahriman/application/handlers/clean.py b/src/ahriman/application/handlers/clean.py index 3487d96b..34443e3e 100644 --- a/src/ahriman/application/handlers/clean.py +++ b/src/ahriman/application/handlers/clean.py @@ -22,6 +22,7 @@ import argparse from ahriman.application.application import Application from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration +from ahriman.models.repository_id import RepositoryId class Clean(Handler): @@ -30,17 +31,18 @@ class Clean(Handler): """ @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ - application = Application(architecture, configuration, report=report) + application = Application(repository_id, configuration, report=report) application.on_start() application.clean(cache=args.cache, chroot=args.chroot, manual=args.manual, packages=args.packages, pacman=args.pacman) diff --git a/src/ahriman/application/handlers/daemon.py b/src/ahriman/application/handlers/daemon.py index 3878ee06..33c00bf7 100644 --- a/src/ahriman/application/handlers/daemon.py +++ b/src/ahriman/application/handlers/daemon.py @@ -22,6 +22,7 @@ import threading from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration +from ahriman.models.repository_id import RepositoryId class Daemon(Handler): @@ -30,19 +31,20 @@ class Daemon(Handler): """ @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ from ahriman.application.handlers import Update - Update.run(args, architecture, configuration, report=report) - timer = threading.Timer(args.interval, Daemon.run, args=[args, architecture, configuration], + Update.run(args, repository_id, configuration, report=report) + timer = threading.Timer(args.interval, Daemon.run, args=[args, repository_id, configuration], kwargs={"report": report}) timer.start() timer.join() diff --git a/src/ahriman/application/handlers/dump.py b/src/ahriman/application/handlers/dump.py index d212b36b..15533059 100644 --- a/src/ahriman/application/handlers/dump.py +++ b/src/ahriman/application/handlers/dump.py @@ -22,6 +22,7 @@ import argparse from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration from ahriman.core.formatters import ConfigurationPathsPrinter, ConfigurationPrinter, StringPrinter +from ahriman.models.repository_id import RepositoryId class Dump(Handler): @@ -29,16 +30,17 @@ class Dump(Handler): dump configuration handler """ - ALLOW_AUTO_ARCHITECTURE_RUN = False + ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ diff --git a/src/ahriman/application/handlers/handler.py b/src/ahriman/application/handlers/handler.py index c5c61be2..3e5dfdb2 100644 --- a/src/ahriman/application/handlers/handler.py +++ b/src/ahriman/application/handlers/handler.py @@ -25,7 +25,8 @@ from multiprocessing import Pool from ahriman.application.lock import Lock from ahriman.core.configuration import Configuration from ahriman.core.exceptions import ExitCode, MissingArchitectureError, MultipleArchitecturesError -from ahriman.core.log import Log +from ahriman.core.log.log_loader import LogLoader +from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_paths import RepositoryPaths @@ -34,7 +35,6 @@ class Handler: base handler class for command callbacks Attributes: - ALLOW_AUTO_ARCHITECTURE_RUN(bool): (class attribute) allow defining architecture from existing repositories ALLOW_MULTI_ARCHITECTURE_RUN(bool): (class attribute) allow running with multiple architectures Examples: @@ -46,60 +46,28 @@ class Handler: >>> Add.execute(args) """ - ALLOW_AUTO_ARCHITECTURE_RUN = True ALLOW_MULTI_ARCHITECTURE_RUN = True @classmethod - def architectures_extract(cls, args: argparse.Namespace) -> list[str]: - """ - get known architectures - - Args: - args(argparse.Namespace): command line args - - Returns: - list[str]: list of architectures for which tree is created - - Raises: - MissingArchitectureError: if no architecture set and automatic detection is not allowed or failed - """ - if not cls.ALLOW_AUTO_ARCHITECTURE_RUN and args.architecture is None: - # for some parsers (e.g. config) we need to run with specific architecture - # for those cases architecture must be set explicitly - raise MissingArchitectureError(args.command) - if args.architecture: # architecture is specified explicitly - return sorted(set(args.architecture)) - - configuration = Configuration() - configuration.load(args.configuration) - # wtf??? - root = configuration.getpath("repository", "root") # pylint: disable=assignment-from-no-return - architectures = RepositoryPaths.known_architectures(root) - - if not architectures: # well we did not find anything - raise MissingArchitectureError(args.command) - return sorted(architectures) - - @classmethod - def call(cls, args: argparse.Namespace, architecture: str) -> bool: + def call(cls, args: argparse.Namespace, repository_id: RepositoryId) -> bool: """ additional function to wrap all calls for multiprocessing library Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier Returns: bool: True on success, False otherwise """ try: - configuration = Configuration.from_path(args.configuration, architecture) + configuration = Configuration.from_path(args.configuration, repository_id) - log_handler = Log.handler(args.log_handler) - Log.load(configuration, log_handler, quiet=args.quiet, report=args.report) + log_handler = LogLoader.handler(args.log_handler) + LogLoader.load(configuration, log_handler, quiet=args.quiet, report=args.report) - with Lock(args, architecture, configuration): - cls.run(args, architecture, configuration, report=args.report) + with Lock(args, repository_id, configuration): + cls.run(args, repository_id, configuration, report=args.report) return True except ExitCode: @@ -123,28 +91,82 @@ class Handler: Raises: MultipleArchitecturesError: if more than one architecture supplied and no multi architecture supported """ - architectures = cls.architectures_extract(args) + repositories = cls.repositories_extract(args) # actually we do not have to spawn another process if it is single-process application, do we? - if len(architectures) > 1: + if len(repositories) > 1: if not cls.ALLOW_MULTI_ARCHITECTURE_RUN: - raise MultipleArchitecturesError(args.command) + raise MultipleArchitecturesError(args.command, repositories) - with Pool(len(architectures)) as pool: - result = pool.starmap(cls.call, [(args, architecture) for architecture in architectures]) + with Pool(len(repositories)) as pool: + result = pool.starmap(cls.call, [(args, repository_id) for repository_id in repositories]) else: - result = [cls.call(args, architectures.pop())] + result = [cls.call(args, repositories.pop())] return 0 if all(result) else 1 @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def repositories_extract(cls, args: argparse.Namespace) -> list[RepositoryId]: + """ + get known architectures + + Args: + args(argparse.Namespace): command line args + + Returns: + list[RepositoryId]: list of repository names and architectures for which tree is created + + Raises: + MissingArchitectureError: if no architecture set and automatic detection is not allowed or failed + """ + configuration = Configuration() + configuration.load(args.configuration) + # pylint, wtf??? + root = configuration.getpath("repository", "root") # pylint: disable=assignment-from-no-return + + # preparse systemd repository-id argument + # we are using unescaped values, so / is not allowed here, because it is impossible to separate if from dashes + if args.repository_id is not None: + # repository parts is optional for backward compatibility + architecture, *repository_parts = args.repository_id.split("/") + args.architecture = [architecture] + if repository_parts: + args.repository = ["-".join(repository_parts)] # replace slash with dash + + # extract repository names first + names = args.repository + if names is None: # try to read file system first + names = RepositoryPaths.known_repositories(root) + if not names: # try to read configuration now + names = [configuration.get("repository", "name")] + + # extract architecture names + if (architectures := args.architecture) is not None: + repositories = set( + RepositoryId(architecture, name) + for name in names + for architecture in architectures + ) + else: # try to read from file system + repositories = set( + RepositoryId(architecture, name) + for name in names + for architecture in RepositoryPaths.known_architectures(root, name) + ) + + if not repositories: + raise MissingArchitectureError(args.command) + return sorted(repositories) + + @classmethod + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting diff --git a/src/ahriman/application/handlers/help.py b/src/ahriman/application/handlers/help.py index b7f7aeed..9be37636 100644 --- a/src/ahriman/application/handlers/help.py +++ b/src/ahriman/application/handlers/help.py @@ -21,6 +21,7 @@ import argparse from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration +from ahriman.models.repository_id import RepositoryId class Help(Handler): @@ -28,16 +29,17 @@ class Help(Handler): help handler """ - ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture" + ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ diff --git a/src/ahriman/application/handlers/key_import.py b/src/ahriman/application/handlers/key_import.py index 4424d910..4dac06fb 100644 --- a/src/ahriman/application/handlers/key_import.py +++ b/src/ahriman/application/handlers/key_import.py @@ -22,6 +22,7 @@ import argparse from ahriman.application.application import Application from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration +from ahriman.models.repository_id import RepositoryId class KeyImport(Handler): @@ -29,18 +30,19 @@ class KeyImport(Handler): key import packages handler """ - ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture" + ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ - application = Application(architecture, configuration, report=report) + application = Application(repository_id, configuration, report=report) application.repository.sign.key_import(args.key_server, args.key) diff --git a/src/ahriman/application/handlers/patch.py b/src/ahriman/application/handlers/patch.py index 30c2ec13..9121a236 100644 --- a/src/ahriman/application/handlers/patch.py +++ b/src/ahriman/application/handlers/patch.py @@ -30,6 +30,7 @@ from ahriman.core.formatters import PatchPrinter from ahriman.models.action import Action from ahriman.models.package import Package from ahriman.models.pkgbuild_patch import PkgbuildPatch +from ahriman.models.repository_id import RepositoryId class Patch(Handler): @@ -37,18 +38,21 @@ class Patch(Handler): patch control handler """ + ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action + @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ - application = Application(architecture, configuration, report=report) + application = Application(repository_id, configuration, report=report) application.on_start() match args.action: @@ -56,7 +60,7 @@ class Patch(Handler): patch = Patch.patch_create_from_function(args.variable, args.patch) Patch.patch_set_create(application, args.package, patch) case Action.Update: - package_base, patch = Patch.patch_create_from_diff(args.package, architecture, args.track) + package_base, patch = Patch.patch_create_from_diff(args.package, repository_id.architecture, args.track) Patch.patch_set_create(application, package_base, patch) case Action.List: Patch.patch_set_list(application, args.package, args.variable, args.exit_code) @@ -114,7 +118,7 @@ class Patch(Handler): application.database.patches_insert(package_base, patch) @staticmethod - def patch_set_list(application: Application, package_base: str | None, variables: list[str], + def patch_set_list(application: Application, package_base: str | None, variables: list[str] | None, exit_code: bool) -> None: """ list patches available for the package base @@ -122,7 +126,7 @@ class Patch(Handler): Args: application(Application): application instance package_base(str | None): package base - variables(list[str]): extract patches only for specified PKGBUILD variables + variables(list[str] | None): extract patches only for specified PKGBUILD variables exit_code(bool): exit with error on empty search result """ patches = application.database.patches_list(package_base, variables) @@ -132,13 +136,13 @@ class Patch(Handler): PatchPrinter(base, patch).print(verbose=True, separator=" = ") @staticmethod - def patch_set_remove(application: Application, package_base: str, variables: list[str]) -> None: + def patch_set_remove(application: Application, package_base: str, variables: list[str] | None) -> None: """ remove patch set for the package base Args: application(Application): application instance package_base(str): package base - variables(list[str]): remove patches only for specified PKGBUILD variables + variables(list[str] | None): remove patches only for specified PKGBUILD variables """ application.database.patches_remove(package_base, variables) diff --git a/src/ahriman/application/handlers/rebuild.py b/src/ahriman/application/handlers/rebuild.py index 595ef8ae..2b776e25 100644 --- a/src/ahriman/application/handlers/rebuild.py +++ b/src/ahriman/application/handlers/rebuild.py @@ -24,6 +24,7 @@ from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration from ahriman.models.build_status import BuildStatusEnum from ahriman.models.package import Package +from ahriman.models.repository_id import RepositoryId class Rebuild(Handler): @@ -32,21 +33,22 @@ class Rebuild(Handler): """ @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ - application = Application(architecture, configuration, report=report) + application = Application(repository_id, configuration, report=report) application.on_start() packages = Rebuild.extract_packages(application, args.status, from_database=args.from_database) - updates = application.repository.packages_depend_on(packages, args.depends_on or None) + updates = application.repository.packages_depend_on(packages, args.depends_on) Rebuild.check_if_empty(args.exit_code, not updates) if args.dry_run: diff --git a/src/ahriman/application/handlers/remove.py b/src/ahriman/application/handlers/remove.py index 1c65cb85..349075ba 100644 --- a/src/ahriman/application/handlers/remove.py +++ b/src/ahriman/application/handlers/remove.py @@ -22,6 +22,7 @@ import argparse from ahriman.application.application import Application from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration +from ahriman.models.repository_id import RepositoryId class Remove(Handler): @@ -30,16 +31,17 @@ class Remove(Handler): """ @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ - application = Application(architecture, configuration, report=report) + application = Application(repository_id, configuration, report=report) application.on_start() application.remove(args.package) diff --git a/src/ahriman/application/handlers/remove_unknown.py b/src/ahriman/application/handlers/remove_unknown.py index 2fda7576..f6f01f36 100644 --- a/src/ahriman/application/handlers/remove_unknown.py +++ b/src/ahriman/application/handlers/remove_unknown.py @@ -23,6 +23,7 @@ from ahriman.application.application import Application from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration from ahriman.core.formatters import StringPrinter +from ahriman.models.repository_id import RepositoryId class RemoveUnknown(Handler): @@ -31,17 +32,18 @@ class RemoveUnknown(Handler): """ @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ - application = Application(architecture, configuration, report=report) + application = Application(repository_id, configuration, report=report) application.on_start() unknown_packages = application.unknown() diff --git a/src/ahriman/application/handlers/restore.py b/src/ahriman/application/handlers/restore.py index 966b43ce..e06cefa7 100644 --- a/src/ahriman/application/handlers/restore.py +++ b/src/ahriman/application/handlers/restore.py @@ -23,6 +23,7 @@ from tarfile import TarFile from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration +from ahriman.models.repository_id import RepositoryId class Restore(Handler): @@ -30,16 +31,17 @@ class Restore(Handler): restore packages handler """ - ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture" + ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ diff --git a/src/ahriman/application/handlers/search.py b/src/ahriman/application/handlers/search.py index 6fabcb89..fd1df635 100644 --- a/src/ahriman/application/handlers/search.py +++ b/src/ahriman/application/handlers/search.py @@ -29,6 +29,7 @@ from ahriman.core.configuration import Configuration from ahriman.core.exceptions import OptionError from ahriman.core.formatters import AurPrinter from ahriman.models.aur_package import AURPackage +from ahriman.models.repository_id import RepositoryId class Search(Handler): @@ -39,7 +40,7 @@ class Search(Handler): SORT_FIELDS(set[str]): (class attribute) allowed fields to sort the package list """ - ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture" + ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action SORT_FIELDS = { field.name for field in fields(AURPackage) @@ -47,17 +48,18 @@ class Search(Handler): } @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ - application = Application(architecture, configuration, report=report) + application = Application(repository_id, configuration, report=report) official_packages_list = Official.multisearch(*args.search, pacman=application.repository.pacman) aur_packages_list = AUR.multisearch(*args.search, pacman=application.repository.pacman) diff --git a/src/ahriman/application/handlers/service_updates.py b/src/ahriman/application/handlers/service_updates.py index 96e2f550..f45b7599 100644 --- a/src/ahriman/application/handlers/service_updates.py +++ b/src/ahriman/application/handlers/service_updates.py @@ -25,6 +25,7 @@ from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration from ahriman.core.formatters import UpdatePrinter from ahriman.models.package import Package +from ahriman.models.repository_id import RepositoryId class ServiceUpdates(Handler): @@ -32,20 +33,21 @@ class ServiceUpdates(Handler): service updates handler """ - ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture" + ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ - application = Application(architecture, configuration, report=report) + application = Application(repository_id, configuration, report=report) remote = Package.from_aur("ahriman", application.repository.pacman, None) _, release = remote.version.rsplit("-", 1) # we don't store pkgrel locally, so we just append it diff --git a/src/ahriman/application/handlers/setup.py b/src/ahriman/application/handlers/setup.py index 3515d9cf..d2790053 100644 --- a/src/ahriman/application/handlers/setup.py +++ b/src/ahriman/application/handlers/setup.py @@ -25,6 +25,8 @@ from pwd import getpwuid from ahriman.application.application import Application from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration +from ahriman.core.exceptions import MissingArchitectureError +from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.user import User @@ -39,88 +41,92 @@ class Setup(Handler): SUDOERS_DIR_PATH(Path): (class attribute) path to sudoers.d includes directory """ - ALLOW_AUTO_ARCHITECTURE_RUN = False + ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io ARCHBUILD_COMMAND_PATH = Path("/usr") / "bin" / "archbuild" MIRRORLIST_PATH = Path("/etc") / "pacman.d" / "mirrorlist" SUDOERS_DIR_PATH = Path("/etc") / "sudoers.d" @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ - Setup.configuration_create_ahriman(args, architecture, args.repository, configuration) + # special check for args to avoid auto definition for setup command + if args.architecture is None or args.repository is None: + raise MissingArchitectureError(args.command) + + Setup.configuration_create_ahriman(args, repository_id, configuration) configuration.reload() - application = Application(architecture, configuration, report=report) + application = Application(repository_id, configuration, report=report) Setup.configuration_create_makepkg(args.packager, args.makeflags_jobs, application.repository.paths) - Setup.executable_create(application.repository.paths, args.build_command, architecture) + Setup.executable_create(application.repository.paths, repository_id) repository_server = f"file://{application.repository.paths.repository}" if args.server is None else args.server - Setup.configuration_create_devtools(args.build_command, architecture, args.from_configuration, args.mirror, - args.multilib, args.repository, repository_server) - Setup.configuration_create_sudo(application.repository.paths, args.build_command, architecture) + Setup.configuration_create_devtools( + repository_id, args.from_configuration, args.mirror, args.multilib, repository_server) + Setup.configuration_create_sudo(application.repository.paths, repository_id) application.repository.repo.init() # lazy database sync application.repository.pacman.handle # pylint: disable=pointless-statement @staticmethod - def build_command(root: Path, prefix: str, architecture: str) -> Path: + def build_command(root: Path, repository_id: RepositoryId) -> Path: """ generate build command name Args: root(Path): root directory for the build command (must be root of the repository) - prefix(str): command prefix in {prefix}-{architecture}-build - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier Returns: Path: valid devtools command name """ - return root / f"{prefix}-{architecture}-build" + return root / f"{repository_id.name}-{repository_id.architecture}-build" @staticmethod - def configuration_create_ahriman(args: argparse.Namespace, architecture: str, repository: str, + def configuration_create_ahriman(args: argparse.Namespace, repository_id: RepositoryId, root: Configuration) -> None: """ create service specific configuration Args: args(argparse.Namespace): command line args - architecture(str): repository architecture - repository(str): repository name + repository_id(RepositoryId): repository unique identifier root(Configuration): root configuration instance """ configuration = Configuration() - section = Configuration.section_name("build", architecture) - build_command = Setup.build_command(root.repository_paths.root, args.build_command, architecture) + section = Configuration.section_name("build", repository_id.name, repository_id.architecture) + build_command = Setup.build_command(root.repository_paths.root, repository_id) configuration.set_option(section, "build_command", str(build_command)) - configuration.set_option("repository", "name", repository) + configuration.set_option("repository", "name", repository_id.name) # backward compatibility for docker if args.build_as_user is not None: configuration.set_option(section, "makechrootpkg_flags", f"-U {args.build_as_user}") - section = Configuration.section_name("alpm", architecture) + section = Configuration.section_name("alpm", repository_id.name, repository_id.architecture) if args.mirror is not None: configuration.set_option(section, "mirror", args.mirror) if not args.multilib: repositories = filter(lambda r: r != "multilib", root.getlist("alpm", "repositories")) configuration.set_option(section, "repositories", " ".join(repositories)) - section = Configuration.section_name("sign", architecture) + section = Configuration.section_name("sign", repository_id.name, repository_id.architecture) if args.sign_key is not None: - configuration.set_option(section, "target", " ".join([target.name.lower() for target in args.sign_target])) + sign_targets = args.sign_target or [] + configuration.set_option(section, "target", " ".join([target.name.lower() for target in sign_targets])) configuration.set_option(section, "key", args.sign_key) - section = Configuration.section_name("web", architecture) + section = Configuration.section_name("web", repository_id.name, repository_id.architecture) if args.web_port is not None: configuration.set_option(section, "port", str(args.web_port)) if args.web_unix_socket is not None: @@ -134,8 +140,8 @@ class Setup(Handler): configuration.write(ahriman_configuration) @staticmethod - def configuration_create_devtools(prefix: str, architecture: str, source: Path, mirror: str | None, - multilib: bool, repository: str, repository_server: str) -> None: + def configuration_create_devtools(repository_id: RepositoryId, source: Path, mirror: str | None, + multilib: bool, repository_server: str) -> None: """ create configuration for devtools based on ``source`` configuration @@ -143,12 +149,10 @@ class Setup(Handler): devtools does not allow to specify the pacman configuration, thus we still have to use configuration in /usr Args: - prefix(str): command prefix in {prefix}-{architecture}-build - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier source(Path): path to source configuration file mirror(str | None): link to package server mirror multilib(bool): add or do not multilib repository to the configuration - repository(str): repository name repository_server(str): url of the repository """ # allow_no_value=True is required because pacman uses boolean configuration in which just keys present @@ -163,7 +167,7 @@ class Setup(Handler): configuration.read(source) # set our architecture now - configuration.set_option("options", "Architecture", architecture) + configuration.set_option("options", "Architecture", repository_id.architecture) # add multilib if multilib: @@ -178,10 +182,10 @@ class Setup(Handler): configuration.set_option(section, "Server", mirror) # add repository itself - configuration.set_option(repository, "SigLevel", "Never") # we don't care - configuration.set_option(repository, "Server", repository_server) + configuration.set_option(repository_id.name, "SigLevel", "Never") # we don't care + configuration.set_option(repository_id.name, "Server", repository_server) - target = source.parent / f"{prefix}-{architecture}.conf" + target = source.parent / f"{repository_id.name}-{repository_id.architecture}.conf" with target.open("w") as devtools_configuration: configuration.write(devtools_configuration) @@ -205,31 +209,29 @@ class Setup(Handler): (home_dir / ".makepkg.conf").write_text(content, encoding="utf8") @staticmethod - def configuration_create_sudo(paths: RepositoryPaths, prefix: str, architecture: str) -> None: + def configuration_create_sudo(paths: RepositoryPaths, repository_id: RepositoryId) -> None: """ create configuration to run build command with sudo without password Args: paths(RepositoryPaths): repository paths instance - prefix(str): command prefix in {prefix}-{architecture}-build - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier """ - command = Setup.build_command(paths.root, prefix, architecture) - sudoers_file = Setup.build_command(Setup.SUDOERS_DIR_PATH, prefix, architecture) + command = Setup.build_command(paths.root, repository_id) + sudoers_file = Setup.build_command(Setup.SUDOERS_DIR_PATH, repository_id) sudoers_file.write_text(f"ahriman ALL=(ALL) NOPASSWD:SETENV: {command} *\n", encoding="utf8") sudoers_file.chmod(0o400) # security! @staticmethod - def executable_create(paths: RepositoryPaths, prefix: str, architecture: str) -> None: + def executable_create(paths: RepositoryPaths, repository_id: RepositoryId) -> None: """ create executable for the service Args: paths(RepositoryPaths): repository paths instance - prefix(str): command prefix in {prefix}-{architecture}-build - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier """ - command = Setup.build_command(paths.root, prefix, architecture) + command = Setup.build_command(paths.root, repository_id) command.unlink(missing_ok=True) command.symlink_to(Setup.ARCHBUILD_COMMAND_PATH) paths.chown(command) # we would like to keep owner inside ahriman's home diff --git a/src/ahriman/application/handlers/shell.py b/src/ahriman/application/handlers/shell.py index ef9fb531..d3d026fb 100644 --- a/src/ahriman/application/handlers/shell.py +++ b/src/ahriman/application/handlers/shell.py @@ -26,6 +26,7 @@ from pathlib import Path from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration from ahriman.core.formatters import StringPrinter +from ahriman.models.repository_id import RepositoryId class Shell(Handler): @@ -33,16 +34,17 @@ class Shell(Handler): python shell handler """ - ALLOW_MULTI_ARCHITECTURE_RUN = False + ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ @@ -50,7 +52,13 @@ class Shell(Handler): # licensed by https://creativecommons.org/licenses/by-sa/3.0 path = Path(sys.prefix) / "share" / "ahriman" / "templates" / "shell" StringPrinter(path.read_text(encoding="utf8")).print(verbose=False) - local_variables = {"architecture": architecture, "configuration": configuration} + + local_variables = { + "architecture": repository_id.architecture, + "configuration": configuration, + "repository_id": repository_id, + } + if args.code is None: code.interact(local=local_variables) else: diff --git a/src/ahriman/application/handlers/sign.py b/src/ahriman/application/handlers/sign.py index e572d55b..4d1ac575 100644 --- a/src/ahriman/application/handlers/sign.py +++ b/src/ahriman/application/handlers/sign.py @@ -22,6 +22,7 @@ import argparse from ahriman.application.application import Application from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration +from ahriman.models.repository_id import RepositoryId class Sign(Handler): @@ -30,14 +31,15 @@ class Sign(Handler): """ @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ - Application(architecture, configuration, report=report).sign(args.package) + Application(repository_id, configuration, report=report).sign(args.package) diff --git a/src/ahriman/application/handlers/status.py b/src/ahriman/application/handlers/status.py index 477d0a3f..6700ca36 100644 --- a/src/ahriman/application/handlers/status.py +++ b/src/ahriman/application/handlers/status.py @@ -27,6 +27,7 @@ from ahriman.core.configuration import Configuration from ahriman.core.formatters import PackagePrinter, StatusPrinter from ahriman.models.build_status import BuildStatus from ahriman.models.package import Package +from ahriman.models.repository_id import RepositoryId class Status(Handler): @@ -34,21 +35,22 @@ class Status(Handler): package status handler """ - ALLOW_AUTO_ARCHITECTURE_RUN = False + ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ # we are using reporter here - client = Application(architecture, configuration, report=True).repository.reporter + client = Application(repository_id, configuration, report=True).repository.reporter if args.ahriman: service_status = client.status_get() StatusPrinter(service_status.status).print(verbose=args.info) diff --git a/src/ahriman/application/handlers/status_update.py b/src/ahriman/application/handlers/status_update.py index 803ca8e5..ff1f63f4 100644 --- a/src/ahriman/application/handlers/status_update.py +++ b/src/ahriman/application/handlers/status_update.py @@ -23,6 +23,7 @@ from ahriman.application.application import Application from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration from ahriman.models.action import Action +from ahriman.models.repository_id import RepositoryId class StatusUpdate(Handler): @@ -30,21 +31,22 @@ class StatusUpdate(Handler): status update handler """ - ALLOW_AUTO_ARCHITECTURE_RUN = False + ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ # we are using reporter here - client = Application(architecture, configuration, report=True).repository.reporter + client = Application(repository_id, configuration, report=True).repository.reporter match args.action: case Action.Update if args.package: diff --git a/src/ahriman/application/handlers/structure.py b/src/ahriman/application/handlers/structure.py index 596a0344..cc7d5bb4 100644 --- a/src/ahriman/application/handlers/structure.py +++ b/src/ahriman/application/handlers/structure.py @@ -24,6 +24,7 @@ from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration from ahriman.core.formatters import StringPrinter, TreePrinter from ahriman.core.tree import Tree +from ahriman.models.repository_id import RepositoryId class Structure(Handler): @@ -31,20 +32,21 @@ class Structure(Handler): dump repository structure handler """ - ALLOW_AUTO_ARCHITECTURE_RUN = False + ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ - application = Application(architecture, configuration, report=report) + application = Application(repository_id, configuration, report=report) partitions = Tree.partition(application.repository.packages(), count=args.partitions) for partition_id, partition in enumerate(partitions): diff --git a/src/ahriman/application/handlers/tree_migrate.py b/src/ahriman/application/handlers/tree_migrate.py new file mode 100644 index 00000000..511b4967 --- /dev/null +++ b/src/ahriman/application/handlers/tree_migrate.py @@ -0,0 +1,68 @@ +# +# Copyright (c) 2021-2023 ahriman team. +# +# This file is part of ahriman +# (see https://github.com/arcan1s/ahriman). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +import argparse + +from ahriman.application.handlers import Handler +from ahriman.core.configuration import Configuration +from ahriman.models.repository_id import RepositoryId +from ahriman.models.repository_paths import RepositoryPaths + + +class TreeMigrate(Handler): + """ + tree migration handler + """ + + @classmethod + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: + """ + callback for command line + + Args: + args(argparse.Namespace): command line args + repository_id(RepositoryId): repository unique identifier + configuration(Configuration): configuration instance + report(bool): force enable or disable reporting + """ + current_tree = configuration.repository_paths + target_tree = RepositoryPaths(current_tree.root, current_tree.repository_id, _force_current_tree=True) + + # create repository tree + target_tree.tree_create() + # perform migration + TreeMigrate.tree_move(current_tree, target_tree) + + @staticmethod + def tree_move(from_tree: RepositoryPaths, to_tree: RepositoryPaths) -> None: + """ + move files between trees. Trees must be created in advance + + Args: + from_tree(RepositoryPaths): old repository tree + to_tree(RepositoryPaths): new repository tree + """ + # we don't care about devtools chroot + for attribute in ( + RepositoryPaths.packages, + RepositoryPaths.pacman, + RepositoryPaths.repository, + ): + attribute.fget(from_tree).rename(attribute.fget(to_tree)) # type: ignore[attr-defined] diff --git a/src/ahriman/application/handlers/triggers.py b/src/ahriman/application/handlers/triggers.py index f47b4081..23d63034 100644 --- a/src/ahriman/application/handlers/triggers.py +++ b/src/ahriman/application/handlers/triggers.py @@ -22,6 +22,7 @@ import argparse from ahriman.application.application import Application from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration +from ahriman.models.repository_id import RepositoryId from ahriman.models.result import Result @@ -31,19 +32,20 @@ class Triggers(Handler): """ @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ - application = Application(architecture, configuration, report=report) + application = Application(repository_id, configuration, report=report) if args.trigger: loader = application.repository.triggers - loader.triggers = [loader.load_trigger(trigger, architecture, configuration) for trigger in args.trigger] + loader.triggers = [loader.load_trigger(trigger, repository_id, configuration) for trigger in args.trigger] application.on_start() application.on_result(Result()) diff --git a/src/ahriman/application/handlers/unsafe_commands.py b/src/ahriman/application/handlers/unsafe_commands.py index a40e9eec..68c85c85 100644 --- a/src/ahriman/application/handlers/unsafe_commands.py +++ b/src/ahriman/application/handlers/unsafe_commands.py @@ -22,6 +22,7 @@ import argparse from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration from ahriman.core.formatters import StringPrinter +from ahriman.models.repository_id import RepositoryId class UnsafeCommands(Handler): @@ -29,16 +30,17 @@ class UnsafeCommands(Handler): unsafe command help parser """ - ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture" + ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ diff --git a/src/ahriman/application/handlers/update.py b/src/ahriman/application/handlers/update.py index f2fa9efd..9824fda9 100644 --- a/src/ahriman/application/handlers/update.py +++ b/src/ahriman/application/handlers/update.py @@ -25,6 +25,7 @@ from ahriman.application.application import Application from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration from ahriman.models.packagers import Packagers +from ahriman.models.repository_id import RepositoryId class Update(Handler): @@ -33,17 +34,18 @@ class Update(Handler): """ @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ - application = Application(architecture, configuration, report=report, refresh_pacman_database=args.refresh) + application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh) application.on_start() packages = application.updates(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs) Update.check_if_empty(args.exit_code, not packages) diff --git a/src/ahriman/application/handlers/users.py b/src/ahriman/application/handlers/users.py index 92a33b70..17ef9876 100644 --- a/src/ahriman/application/handlers/users.py +++ b/src/ahriman/application/handlers/users.py @@ -26,6 +26,7 @@ from ahriman.core.database import SQLite from ahriman.core.exceptions import PasswordError from ahriman.core.formatters import UserPrinter from ahriman.models.action import Action +from ahriman.models.repository_id import RepositoryId from ahriman.models.user import User @@ -34,16 +35,17 @@ class Users(Handler): user management handler """ - ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture" + ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ diff --git a/src/ahriman/application/handlers/validate.py b/src/ahriman/application/handlers/validate.py index 64214dff..bc983bcd 100644 --- a/src/ahriman/application/handlers/validate.py +++ b/src/ahriman/application/handlers/validate.py @@ -29,6 +29,7 @@ from ahriman.core.configuration.validator import Validator from ahriman.core.exceptions import ExtensionError from ahriman.core.formatters import ValidationPrinter from ahriman.core.triggers import TriggerLoader +from ahriman.models.repository_id import RepositoryId class Validate(Handler): @@ -36,20 +37,21 @@ class Validate(Handler): configuration validator handler """ - ALLOW_AUTO_ARCHITECTURE_RUN = False + ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ - schema = Validate.schema(architecture, configuration) + schema = Validate.schema(repository_id, configuration) validator = Validator(configuration=configuration, schema=schema) if validator.validate(configuration.dump()): @@ -61,12 +63,12 @@ class Validate(Handler): Validate.check_if_empty(args.exit_code, True) @staticmethod - def schema(architecture: str, configuration: Configuration) -> ConfigurationSchema: + def schema(repository_id: RepositoryId, configuration: Configuration) -> ConfigurationSchema: """ get schema with triggers Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance Returns: @@ -85,12 +87,12 @@ class Validate(Handler): continue # default settings if any - for schema_name, schema in trigger_class.configuration_schema(architecture, None).items(): + for schema_name, schema in trigger_class.configuration_schema(repository_id, None).items(): erased = Validate.schema_erase_required(copy.deepcopy(schema)) root[schema_name] = Validate.schema_merge(root.get(schema_name, {}), erased) # settings according to enabled triggers - for schema_name, schema in trigger_class.configuration_schema(architecture, configuration).items(): + for schema_name, schema in trigger_class.configuration_schema(repository_id, configuration).items(): root[schema_name] = Validate.schema_merge(root.get(schema_name, {}), copy.deepcopy(schema)) return root diff --git a/src/ahriman/application/handlers/versions.py b/src/ahriman/application/handlers/versions.py index dbde2dbb..3c1d8157 100644 --- a/src/ahriman/application/handlers/versions.py +++ b/src/ahriman/application/handlers/versions.py @@ -28,6 +28,7 @@ from ahriman import __version__ from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration from ahriman.core.formatters import VersionPrinter +from ahriman.models.repository_id import RepositoryId class Versions(Handler): @@ -38,17 +39,18 @@ class Versions(Handler): PEP423_PACKAGE_NAME(str): (class attribute) special regex for valid PEP423 package name """ - ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture" + ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action PEP423_PACKAGE_NAME = re.compile(r"^[A-Za-z0-9._-]+") @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ diff --git a/src/ahriman/application/handlers/web.py b/src/ahriman/application/handlers/web.py index 6cac79b5..7868c392 100644 --- a/src/ahriman/application/handlers/web.py +++ b/src/ahriman/application/handlers/web.py @@ -24,6 +24,7 @@ from collections.abc import Generator from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration from ahriman.core.spawn import Spawn +from ahriman.models.repository_id import RepositoryId class Web(Handler): @@ -31,28 +32,28 @@ class Web(Handler): web server handler """ - ALLOW_AUTO_ARCHITECTURE_RUN = False ALLOW_MULTI_ARCHITECTURE_RUN = False # required to be able to spawn external processes @classmethod - def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None: + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: """ callback for command line Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance report(bool): force enable or disable reporting """ # we are using local import for optional dependencies from ahriman.web.web import run_server, setup_service - spawner_args = Web.extract_arguments(args, architecture, configuration) - spawner = Spawn(args.parser(), architecture, list(spawner_args)) + spawner_args = Web.extract_arguments(args, repository_id, configuration) + spawner = Spawn(args.parser(), repository_id, list(spawner_args)) spawner.start() - application = setup_service(architecture, configuration, spawner) + application = setup_service(repository_id, configuration, spawner) run_server(application) # terminate spawn process at the last @@ -60,21 +61,22 @@ class Web(Handler): spawner.join() @staticmethod - def extract_arguments(args: argparse.Namespace, architecture: str, + def extract_arguments(args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration) -> Generator[str, None, None]: """ extract list of arguments used for current command, except for command specific ones Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance Returns: Generator[str, None, None]: command line arguments which were used for this specific command """ # read architecture from the same argument list - yield from ["--architecture", architecture] + yield from ["--architecture", repository_id.architecture] + yield from ["--repository", repository_id.name] # read configuration path from current settings if (configuration_path := configuration.path) is not None: yield from ["--configuration", str(configuration_path)] diff --git a/src/ahriman/application/lock.py b/src/ahriman/application/lock.py index a2724673..084ef4ef 100644 --- a/src/ahriman/application/lock.py +++ b/src/ahriman/application/lock.py @@ -30,6 +30,7 @@ from ahriman.core.log import LazyLogging 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_id import RepositoryId from ahriman.models.waiter import Waiter @@ -50,26 +51,29 @@ class Lock(LazyLogging): The common flow is to create instance in ``with`` block and handle exceptions after all:: >>> from ahriman.core.configuration import Configuration + >>> from ahriman.models.repository_id import RepositoryId >>> >>> configuration = Configuration() >>> try: - >>> with Lock(args, "x86_64", configuration): + >>> with Lock(args, RepositoryId("x86_64", "aur-clone"), configuration): >>> perform_actions() >>> except Exception as exception: >>> handle_exceptions(exception) """ - def __init__(self, args: argparse.Namespace, architecture: str, configuration: Configuration) -> None: + def __init__(self, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration) -> None: """ default constructor Args: args(argparse.Namespace): command line args - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance """ + lock_suffix = f"{repository_id.name}_{repository_id.architecture}" self.path: Path | None = \ - args.lock.with_stem(f"{args.lock.stem}_{architecture}") if args.lock is not None else None + args.lock.with_stem(f"{args.lock.stem}_{lock_suffix}") if args.lock is not None else None + self.force: bool = args.force self.unsafe: bool = args.unsafe self.wait_timeout: int = args.wait_timeout diff --git a/src/ahriman/core/alpm/pacman.py b/src/ahriman/core/alpm/pacman.py index 6fb56540..0a891862 100644 --- a/src/ahriman/core/alpm/pacman.py +++ b/src/ahriman/core/alpm/pacman.py @@ -23,11 +23,13 @@ from collections.abc import Callable, Generator from functools import cached_property from pathlib import Path from pyalpm import DB, Handle, Package, SIG_PACKAGE, error as PyalpmError # type: ignore[import] +from string import Template from ahriman.core.configuration import Configuration from ahriman.core.log import LazyLogging from ahriman.core.util import trim_package from ahriman.models.pacman_synchronization import PacmanSynchronization +from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_paths import RepositoryPaths @@ -36,18 +38,18 @@ class Pacman(LazyLogging): alpm wrapper """ - def __init__(self, architecture: str, configuration: Configuration, *, + def __init__(self, repository_id: RepositoryId, configuration: Configuration, *, refresh_database: PacmanSynchronization) -> None: """ default constructor Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance refresh_database(PacmanSynchronization): synchronize local cache to remote """ self.__create_handle_fn: Callable[[], Handle] = lambda: self.__create_handle( - architecture, configuration, refresh_database=refresh_database) + repository_id, configuration, refresh_database=refresh_database) @cached_property def handle(self) -> Handle: @@ -59,13 +61,13 @@ class Pacman(LazyLogging): """ return self.__create_handle_fn() - def __create_handle(self, architecture: str, configuration: Configuration, *, + def __create_handle(self, repository_id: RepositoryId, configuration: Configuration, *, refresh_database: PacmanSynchronization) -> Handle: """ create lazy handle function Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance refresh_database(PacmanSynchronization): synchronize local cache to remote @@ -81,7 +83,7 @@ class Pacman(LazyLogging): handle = Handle(str(root), str(database_path)) for repository in configuration.getlist("alpm", "repositories"): - database = self.database_init(handle, repository, mirror, architecture) + database = self.database_init(handle, repository, mirror, repository_id.architecture) self.database_copy(handle, database, pacman_root, paths, use_ahriman_cache=use_ahriman_cache) if use_ahriman_cache and refresh_database: @@ -113,6 +115,7 @@ class Pacman(LazyLogging): dst = repository_database(pacman_db_path) if dst.is_file(): return # file already exists, do not copy + dst.parent.mkdir(mode=0o755, exist_ok=True) # create sync directory if it doesn't exist src = repository_database(pacman_root) if not src.is_file(): self.logger.warning("repository %s is set to be used, however, no working copy was found", database.name) @@ -136,8 +139,14 @@ class Pacman(LazyLogging): """ self.logger.info("loading pacman database %s", repository) database: DB = handle.register_syncdb(repository, SIG_PACKAGE) + # replace variables in mirror address - database.servers = [mirror.replace("$repo", repository).replace("$arch", architecture)] + variables = { + "arch": architecture, + "repo": repository, + } + database.servers = [Template(mirror).safe_substitute(variables)] + return database def database_sync(self, handle: Handle, *, force: bool) -> None: diff --git a/src/ahriman/core/build_tools/sources.py b/src/ahriman/core/build_tools/sources.py index ea7291dd..49d0b195 100644 --- a/src/ahriman/core/build_tools/sources.py +++ b/src/ahriman/core/build_tools/sources.py @@ -154,7 +154,7 @@ class Sources(LazyLogging): shutil.copytree(cache_dir, sources_dir, dirs_exist_ok=True) instance.fetch(sources_dir, package.remote) - patches.extend(instance.extend_architectures(sources_dir, paths.architecture)) + patches.extend(instance.extend_architectures(sources_dir, paths.repository_id.architecture)) for patch in patches: instance.patch_apply(sources_dir, patch) diff --git a/src/ahriman/core/configuration/configuration.py b/src/ahriman/core/configuration/configuration.py index 85ad33df..4089cb5b 100644 --- a/src/ahriman/core/configuration/configuration.py +++ b/src/ahriman/core/configuration/configuration.py @@ -27,6 +27,7 @@ from typing import Any, Self from ahriman.core.configuration.shell_interpolator import ShellInterpolator from ahriman.core.exceptions import InitializeError +from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_paths import RepositoryPaths @@ -38,9 +39,9 @@ class Configuration(configparser.RawConfigParser): ARCHITECTURE_SPECIFIC_SECTIONS(list[str]): (class attribute) known sections which can be architecture specific. Required by dump and merging functions SYSTEM_CONFIGURATION_PATH(Path): (class attribute) default system configuration path distributed by package - architecture(str | None): repository architecture includes(list[Path]): list of includes which were read path(Path | None): path to root configuration file + repository_id(RepositoryId | None): repository unique identifier Examples: Configuration class provides additional method in order to handle application configuration. Since this class is @@ -49,7 +50,7 @@ class Configuration(configparser.RawConfigParser): >>> from pathlib import Path >>> - >>> configuration = Configuration.from_path(Path("/etc/ahriman.ini"), "x86_64") + >>> configuration = Configuration.from_path(Path("/etc/ahriman.ini"), RepositoryId("x86_64", "aur-clone")) >>> repository_name = configuration.get("repository", "name") >>> makepkg_flags = configuration.getlist("build", "makepkg_flags") @@ -59,7 +60,7 @@ class Configuration(configparser.RawConfigParser): In order to get current settings, the ``check_loaded`` method can be used. This method will raise an ``InitializeError`` in case if configuration was not yet loaded:: - >>> path, architecture = configuration.check_loaded() + >>> path, repository_id = configuration.check_loaded() """ ARCHITECTURE_SPECIFIC_SECTIONS = ["alpm", "build", "sign", "web"] @@ -84,10 +85,21 @@ class Configuration(configparser.RawConfigParser): } ) - self.architecture: str | None = None + self.repository_id: RepositoryId | None = None self.path: Path | None = None self.includes: list[Path] = [] + @property + def architecture(self) -> str: + """ + repository architecture for backward compatibility + + Returns: + str: repository architecture + """ + _, repository_id = self.check_loaded() + return repository_id.architecture + @property def include(self) -> Path: """ @@ -111,12 +123,13 @@ class Configuration(configparser.RawConfigParser): @property def repository_name(self) -> str: """ - repository name as defined by configuration + repository name for backward compatibility Returns: - str: repository name from configuration + str: repository name """ - return self.get("repository", "name") + _, repository_id = self.check_loaded() + return repository_id.name @property def repository_paths(self) -> RepositoryPaths: @@ -126,39 +139,60 @@ class Configuration(configparser.RawConfigParser): Returns: RepositoryPaths: repository paths instance """ - _, architecture = self.check_loaded() - return RepositoryPaths(self.getpath("repository", "root"), architecture) + _, repository_id = self.check_loaded() + return RepositoryPaths(self.getpath("repository", "root"), repository_id) @classmethod - def from_path(cls, path: Path, architecture: str) -> Self: + def from_path(cls, path: Path, repository_id: RepositoryId) -> Self: """ constructor with full object initialization Args: path(Path): path to root configuration file - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier Returns: Self: configuration instance """ configuration = cls() configuration.load(path) - configuration.merge_sections(architecture) + configuration.merge_sections(repository_id) return configuration @staticmethod - def section_name(section: str, suffix: str) -> str: + def override_sections(section: str, repository_id: RepositoryId) -> list[str]: + """ + extract override sections + + Args: + section(str): section name + repository_id(RepositoryId): repository unique identifier + + Returns: + list[str]: architecture and repository specific sections in correct order + """ + # the valid order is global < per architecture < per repository < per repository and architecture + return [ + Configuration.section_name(section, repository_id.architecture), # architecture specific override + Configuration.section_name(section, repository_id.name), # override with repository name + Configuration.section_name(section, repository_id.name, repository_id.architecture), # both + ] + + @staticmethod + def section_name(section: str, *suffixes: str | None) -> str: """ generate section name for sections which depends on context Args: section(str): section name - suffix(str): session suffix, e.g. repository architecture + *suffixes(str | None): session suffix, e.g. repository architecture Returns: str: correct section name for repository specific section """ - return f"{section}:{suffix}" + for suffix in filter(bool, suffixes): + section = f"{section}:{suffix}" + return section def _convert_path(self, value: str) -> Path: """ @@ -175,19 +209,19 @@ class Configuration(configparser.RawConfigParser): return path return self.path.parent / path - def check_loaded(self) -> tuple[Path, str]: + def check_loaded(self) -> tuple[Path, RepositoryId]: """ check if service was actually loaded Returns: - tuple[Path, str]: configuration root path and architecture if loaded + tuple[Path, RepositoryId]: configuration root path and architecture if loaded Raises: InitializeError: in case if architecture and/or path are not set """ - if self.path is None or self.architecture is None: - raise InitializeError("Configuration path and/or architecture are not set") - return self.path, self.architecture + if self.path is None or self.repository_id is None: + raise InitializeError("Configuration path and/or repository id are not set") + return self.path, self.repository_id def dump(self) -> dict[str, dict[str, str]]: """ @@ -207,14 +241,14 @@ class Configuration(configparser.RawConfigParser): def getpath(self, *args: Any, **kwargs: Any) -> Path: ... # type: ignore[empty-body] - def gettype(self, section: str, architecture: str, *, fallback: str | None = None) -> tuple[str, str]: + def gettype(self, section: str, repository_id: RepositoryId, *, fallback: str | None = None) -> tuple[str, str]: """ get type variable with fallback to old logic. Despite the fact that it has same semantics as other get* methods, but it has different argument list Args: section(str): section name - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier fallback(str | None, optional): optional fallback type if any. If set, second element of the tuple will be always set to this value (Default value = None) @@ -227,9 +261,9 @@ class Configuration(configparser.RawConfigParser): if (group_type := self.get(section, "type", fallback=fallback)) is not None: return section, group_type # new-style logic # okay lets check for the section with architecture name - full_section = self.section_name(section, architecture) - if self.has_section(full_section): - return full_section, section + for specific in self.override_sections(section, repository_id): + if self.has_section(specific): + return specific, section # okay lets just use section as type if self.has_section(section): return section, section @@ -262,23 +296,24 @@ class Configuration(configparser.RawConfigParser): except (FileNotFoundError, configparser.NoOptionError, configparser.NoSectionError): pass - def merge_sections(self, architecture: str) -> None: + def merge_sections(self, repository_id: RepositoryId) -> None: """ - merge architecture specific sections into main configuration + merge architecture and repository specific sections into main configuration Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier """ - self.architecture = architecture + self.repository_id = repository_id + for section in self.ARCHITECTURE_SPECIFIC_SECTIONS: - # get overrides - specific = self.section_name(section, architecture) - if self.has_section(specific): - # if there is no such section it means that there is no overrides for this arch, - # but we anyway will have to delete sections for others architectures - for key, value in self[specific].items(): - self.set_option(section, key, value) - # remove any arch specific section + for specific in self.override_sections(section, repository_id): + if self.has_section(specific): + # if there is no such section it means that there is no overrides for this arch, + # but we anyway will have to delete sections for others architectures + for key, value in self[specific].items(): + self.set_option(section, key, value) + + # remove any arch/repo specific section for foreign in self.sections(): # we would like to use lambda filter here, but pylint is too dumb if not foreign.startswith(f"{section}:"): @@ -289,11 +324,11 @@ class Configuration(configparser.RawConfigParser): """ reload configuration if possible or raise exception otherwise """ - path, architecture = self.check_loaded() + path, repository_id = self.check_loaded() for section in self.sections(): # clear current content self.remove_section(section) self.load(path) - self.merge_sections(architecture) + self.merge_sections(repository_id) def set_option(self, section: str, option: str, value: str) -> None: """ diff --git a/src/ahriman/core/configuration/schema.py b/src/ahriman/core/configuration/schema.py index 98cb7549..5b10dee7 100644 --- a/src/ahriman/core/configuration/schema.py +++ b/src/ahriman/core/configuration/schema.py @@ -187,7 +187,6 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = { "schema": { "name": { "type": "string", - "required": True, }, "root": { "type": "string", diff --git a/src/ahriman/core/database/migrations/m001_package_source.py b/src/ahriman/core/database/migrations/m001_package_source.py index ece66537..193a5cc6 100644 --- a/src/ahriman/core/database/migrations/m001_package_source.py +++ b/src/ahriman/core/database/migrations/m001_package_source.py @@ -71,7 +71,18 @@ def migrate_package_remotes(connection: Connection, paths: RepositoryPaths) -> N paths(RepositoryPaths): repository paths instance """ from ahriman.core.alpm.remote import AUR - from ahriman.core.database.operations import PackageOperations + from ahriman.models.package import Package + + def get_packages() -> dict[str, Package]: + return { + row["package_base"]: Package( + base=row["package_base"], + version=row["version"], + remote=RemoteSource.from_json(row), + packages={}, + packager=row.get("packager") or None, + ) for row in connection.execute("""select * from package_bases""") + } def insert_remote(base: str, remote: RemoteSource) -> None: connection.execute( @@ -88,8 +99,7 @@ def migrate_package_remotes(connection: Connection, paths: RepositoryPaths) -> N } ) - packages = PackageOperations._packages_get_select_package_bases(connection) - for package_base, package in packages.items(): + for package_base, package in get_packages().items(): local_cache = paths.cache_for(package_base) if local_cache.exists() and not package.is_vcs: continue # skip packages which are not VCS and with local cache diff --git a/src/ahriman/core/database/migrations/m005_make_opt_depends.py b/src/ahriman/core/database/migrations/m005_make_opt_depends.py index ed9f6084..be91665c 100644 --- a/src/ahriman/core/database/migrations/m005_make_opt_depends.py +++ b/src/ahriman/core/database/migrations/m005_make_opt_depends.py @@ -61,8 +61,8 @@ def migrate_package_depends(connection: Connection, configuration: Configuration if not configuration.repository_paths.repository.is_dir(): return - _, architecture = configuration.check_loaded() - pacman = Pacman(architecture, configuration, refresh_database=PacmanSynchronization.Disabled) + _, repository_id = configuration.check_loaded() + pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled) package_list = [] for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()): diff --git a/src/ahriman/core/database/migrations/m006_packages_architecture_required.py b/src/ahriman/core/database/migrations/m006_packages_architecture_required.py index a5438286..a2530239 100644 --- a/src/ahriman/core/database/migrations/m006_packages_architecture_required.py +++ b/src/ahriman/core/database/migrations/m006_packages_architecture_required.py @@ -45,9 +45,9 @@ steps = [ ) """, """ - insert into packages select * from packages_ where architecture is not null; + insert into packages select * from packages_ where architecture is not null """, """ - drop table packages_; + drop table packages_ """, ] diff --git a/src/ahriman/core/database/migrations/m007_check_depends.py b/src/ahriman/core/database/migrations/m007_check_depends.py index 9769bd01..870aa91e 100644 --- a/src/ahriman/core/database/migrations/m007_check_depends.py +++ b/src/ahriman/core/database/migrations/m007_check_depends.py @@ -58,8 +58,8 @@ def migrate_package_check_depends(connection: Connection, configuration: Configu if not configuration.repository_paths.repository.is_dir(): return - _, architecture = configuration.check_loaded() - pacman = Pacman(architecture, configuration, refresh_database=PacmanSynchronization.Disabled) + _, repository_id = configuration.check_loaded() + pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled) package_list = [] for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()): diff --git a/src/ahriman/core/database/migrations/m008_packagers.py b/src/ahriman/core/database/migrations/m008_packagers.py index df62c21f..1f5f6251 100644 --- a/src/ahriman/core/database/migrations/m008_packagers.py +++ b/src/ahriman/core/database/migrations/m008_packagers.py @@ -64,8 +64,8 @@ def migrate_package_base_packager(connection: Connection, configuration: Configu if not configuration.repository_paths.repository.is_dir(): return - _, architecture = configuration.check_loaded() - pacman = Pacman(architecture, configuration, refresh_database=PacmanSynchronization.Disabled) + _, repository_id = configuration.check_loaded() + pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled) package_list = [] for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()): diff --git a/src/ahriman/core/database/migrations/m011_repository_name.py b/src/ahriman/core/database/migrations/m011_repository_name.py new file mode 100644 index 00000000..8089fb4f --- /dev/null +++ b/src/ahriman/core/database/migrations/m011_repository_name.py @@ -0,0 +1,211 @@ +# +# Copyright (c) 2021-2023 ahriman team. +# +# This file is part of ahriman +# (see https://github.com/arcan1s/ahriman). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +from sqlite3 import Connection + +from ahriman.core.configuration import Configuration + + +__all__ = ["migrate_data", "steps"] + + +steps = [ + # set correct types for schema + """ + alter table users rename to users_ + """, + """ + create table users ( + username text not null unique, + access text not null, + password text, + packager_id text, + key_id text + ) + """, + """ + insert into users select * from users_ + """, + """ + drop table users_ + """, + # update base tables + # build_queue + """ + alter table build_queue add column repository text not null default '' + """, + """ + alter table build_queue rename to build_queue_ + """, + """ + create table build_queue ( + package_base text not null, + properties json not null, + repository text not null, + unique (package_base, repository) + ) + """, + """ + insert into build_queue select * from build_queue_ + """, + """ + drop table build_queue_ + """, + # package_bases + """ + alter table package_bases add column repository text not null default '' + """, + """ + alter table package_bases rename to package_bases_ + """, + """ + create table package_bases ( + package_base text not null, + version text not null, + branch text, + git_url text, + path text, + web_url text, + source text, + packager text, + repository text not null, + unique (package_base, repository) + ) + """, + """ + insert into package_bases select * from package_bases_ + """, + """ + drop table package_bases_ + """, + # package_statuses + """ + alter table package_statuses add column repository text not null default '' + """, + """ + alter table package_statuses rename to package_statuses_ + """, + """ + create table package_statuses ( + package_base text not null, + status text not null, + last_updated integer, + repository text not null, + unique (package_base, repository) + ) + """, + """ + insert into package_statuses select * from package_statuses_ + """, + """ + drop table package_statuses_ + """, + # packages + """ + alter table packages add column repository text not null default '' + """, + """ + alter table packages rename to packages_ + """, + """ + create table packages ( + package text not null, + package_base text not null, + architecture text not null, + archive_size integer, + build_date integer, + depends json, + description text, + filename text, + "groups" json, + installed_size integer, + licenses json, + provides json, + url text, + make_depends json, + opt_depends json, + check_depends json, + repository text not null, + unique (package, architecture, repository) + ) + """, + """ + insert into packages select * from packages_ + """, + """ + drop table packages_ + """, + # logs + """ + alter table logs add column repository text not null default '' + """, + """ + drop index logs_package_base_version + """, + """ + alter table logs rename to logs_ + """, + """ + create table logs ( + package_base text not null, + created real not null, + record text, + version text not null, + repository text not null + ) + """, + """ + insert into logs select * from logs_ + """, + """ + create index logs_package_base_version_repository + on logs (package_base, version, repository) + """, + """ + drop table logs_ + """, +] + + +def migrate_data(connection: Connection, configuration: Configuration) -> None: + """ + perform data migration + + Args: + connection(Connection): database connection + configuration(Configuration): configuration instance + """ + migrate_package_repository(connection, configuration) + + +def migrate_package_repository(connection: Connection, configuration: Configuration) -> None: + """ + update repository name from current settings + + Args: + connection(Connection): database connection + configuration(Configuration): configuration instance + """ + _, repository_id = configuration.check_loaded() + + connection.execute("""update build_queue set repository = :repository""", {"repository": repository_id.id}) + connection.execute("""update package_bases set repository = :repository""", {"repository": repository_id.id}) + connection.execute("""update package_statuses set repository = :repository""", {"repository": repository_id.id}) + connection.execute("""update packages set repository = :repository""", {"repository": repository_id.id}) + connection.execute("""update logs set repository = :repository""", {"repository": repository_id.id}) diff --git a/src/ahriman/core/database/operations/build_operations.py b/src/ahriman/core/database/operations/build_operations.py index 13e33e1d..bc0ae350 100644 --- a/src/ahriman/core/database/operations/build_operations.py +++ b/src/ahriman/core/database/operations/build_operations.py @@ -39,9 +39,12 @@ class BuildOperations(Operations): connection.execute( """ delete from build_queue - where :package_base is null or package_base = :package_base + where (:package_base is null or package_base = :package_base) and repository = :repository """, - {"package_base": package_base}) + { + "package_base": package_base, + "repository": self.repository_id.id, + }) return self.with_connection(run, commit=True) @@ -55,7 +58,10 @@ class BuildOperations(Operations): def run(connection: Connection) -> list[Package]: return [ Package.from_json(row["properties"]) - for row in connection.execute("""select * from build_queue""") + for row in connection.execute( + """select properties from build_queue where repository = :repository""", + {"repository": self.repository_id.id} + ) ] return self.with_connection(run) @@ -71,12 +77,16 @@ class BuildOperations(Operations): connection.execute( """ insert into build_queue - (package_base, properties) + (package_base, properties, repository) values - (:package_base, :properties) - on conflict (package_base) do update set + (:package_base, :properties, :repository) + on conflict (package_base, repository) do update set properties = :properties """, - {"package_base": package.base, "properties": package.view()}) + { + "package_base": package.base, + "properties": package.view(), + "repository": self.repository_id.id, + }) return self.with_connection(run, commit=True) diff --git a/src/ahriman/core/database/operations/logs_operations.py b/src/ahriman/core/database/operations/logs_operations.py index 1ecd2563..f4718b69 100644 --- a/src/ahriman/core/database/operations/logs_operations.py +++ b/src/ahriman/core/database/operations/logs_operations.py @@ -45,11 +45,13 @@ class LogsOperations(Operations): (row["created"], row["record"]) for row in connection.execute( """ - select created, record from logs where package_base = :package_base + select created, record from logs + where package_base = :package_base and repository = :repository order by created limit :limit offset :offset """, { "package_base": package_base, + "repository": self.repository_id.id, "limit": limit, "offset": offset, }) @@ -70,15 +72,16 @@ class LogsOperations(Operations): connection.execute( """ insert into logs - (package_base, version, created, record) + (package_base, version, created, record, repository) values - (:package_base, :version, :created, :record) + (:package_base, :version, :created, :record, :repository) """, { "package_base": log_record_id.package_base, "version": log_record_id.version, "created": created, "record": record, + "repository": self.repository_id.id, } ) @@ -97,9 +100,15 @@ class LogsOperations(Operations): connection.execute( """ delete from logs - where package_base = :package_base and (:version is null or version <> :version) + where package_base = :package_base + and repository = :repository + and (:version is null or version <> :version) """, - {"package_base": package_base, "version": version} + { + "package_base": package_base, + "version": version, + "repository": self.repository_id.id, + } ) return self.with_connection(run, commit=True) diff --git a/src/ahriman/core/database/operations/operations.py b/src/ahriman/core/database/operations/operations.py index 9a447adf..4fbe4805 100644 --- a/src/ahriman/core/database/operations/operations.py +++ b/src/ahriman/core/database/operations/operations.py @@ -24,6 +24,7 @@ from pathlib import Path from typing import Any, TypeVar from ahriman.core.log import LazyLogging +from ahriman.models.repository_id import RepositoryId T = TypeVar("T") @@ -35,16 +36,19 @@ class Operations(LazyLogging): Attributes: path(Path): path to the database file + repository_id(RepositoryId): repository unique identifier to perform implicit filtering """ - def __init__(self, path: Path) -> None: + def __init__(self, path: Path, repository_id: RepositoryId) -> None: """ default constructor Args: path(Path): path to the database file + repository_id(RepositoryId): repository unique identifier """ self.path = path + self.repository_id = repository_id @staticmethod def factory(cursor: sqlite3.Cursor, row: tuple[Any, ...]) -> dict[str, Any]: diff --git a/src/ahriman/core/database/operations/package_operations.py b/src/ahriman/core/database/operations/package_operations.py index acf28703..7d7795d3 100644 --- a/src/ahriman/core/database/operations/package_operations.py +++ b/src/ahriman/core/database/operations/package_operations.py @@ -32,8 +32,7 @@ class PackageOperations(Operations): package operations """ - @staticmethod - def _package_remove_package_base(connection: Connection, package_base: str) -> None: + def _package_remove_package_base(self, connection: Connection, package_base: str) -> None: """ remove package base information @@ -41,13 +40,15 @@ class PackageOperations(Operations): connection(Connection): database connection package_base(str): 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}) + connection.execute( + """delete from package_statuses where package_base = :package_base and repository = :repository""", + {"package_base": package_base, "repository": self.repository_id.id}) + connection.execute( + """delete from package_bases where package_base = :package_base and repository = :repository""", + {"package_base": package_base, "repository": self.repository_id.id}) - @staticmethod - def _package_remove_packages(connection: Connection, package_base: str, current_packages: Iterable[str]) -> None: + def _package_remove_packages(self, connection: Connection, package_base: str, + current_packages: Iterable[str]) -> None: """ remove packages belong to the package base @@ -59,13 +60,17 @@ class PackageOperations(Operations): packages = [ package for package in connection.execute( - """select package from packages where package_base = :package_base""", {"package_base": package_base}) + """ + select package, repository from packages + where package_base = :package_base and repository = :repository""", + {"package_base": package_base, "repository": self.repository_id.id}) if package["package"] not in current_packages ] - connection.executemany("""delete from packages where package = :package""", packages) + connection.executemany( + """delete from packages where package = :package and repository = :repository""", + packages) - @staticmethod - def _package_update_insert_base(connection: Connection, package: Package) -> None: + def _package_update_insert_base(self, connection: Connection, package: Package) -> None: """ insert base package into table @@ -76,10 +81,10 @@ class PackageOperations(Operations): connection.execute( """ insert into package_bases - (package_base, version, source, branch, git_url, path, web_url, packager) + (package_base, version, source, branch, git_url, path, web_url, packager, repository) values - (:package_base, :version, :source, :branch, :git_url, :path, :web_url, :packager) - on conflict (package_base) do update set + (:package_base, :version, :source, :branch, :git_url, :path, :web_url, :packager, :repository) + on conflict (package_base, repository) do update set version = :version, branch = :branch, git_url = :git_url, path = :path, web_url = :web_url, source = :source, packager = :packager """, @@ -92,11 +97,11 @@ class PackageOperations(Operations): "web_url": package.remote.web_url, "source": package.remote.source.value, "packager": package.packager, + "repository": self.repository_id.id, } ) - @staticmethod - def _package_update_insert_packages(connection: Connection, package: Package) -> None: + def _package_update_insert_packages(self, connection: Connection, package: Package) -> None: """ insert packages into table @@ -108,20 +113,27 @@ class PackageOperations(Operations): for name, description in package.packages.items(): if description.architecture is None: continue # architecture is required - package_list.append({"package": name, "package_base": package.base, **description.view()}) + package_list.append({ + "package": name, + "package_base": package.base, + "repository": self.repository_id.id, + **description.view(), + }) connection.executemany( """ insert into packages (package, package_base, architecture, archive_size, build_date, depends, description, filename, "groups", installed_size, licenses, provides, - url, make_depends, opt_depends, check_depends) + url, make_depends, opt_depends, check_depends, + repository) values (:package, :package_base, :architecture, :archive_size, :build_date, :depends, :description, :filename, :groups, :installed_size, :licenses, :provides, - :url, :make_depends, :opt_depends, :check_depends) - on conflict (package, architecture) do update set + :url, :make_depends, :opt_depends, :check_depends, + :repository) + on conflict (package, architecture, repository) 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, @@ -129,8 +141,7 @@ class PackageOperations(Operations): """, package_list) - @staticmethod - def _package_update_insert_status(connection: Connection, package_base: str, status: BuildStatus) -> None: + def _package_update_insert_status(self, connection: Connection, package_base: str, status: BuildStatus) -> None: """ insert base package status into table @@ -141,16 +152,21 @@ class PackageOperations(Operations): """ connection.execute( """ - insert into package_statuses (package_base, status, last_updated) + insert into package_statuses + (package_base, status, last_updated, repository) values - (:package_base, :status, :last_updated) - on conflict (package_base) do update set + (:package_base, :status, :last_updated, :repository) + on conflict (package_base, repository) do update set status = :status, last_updated = :last_updated """, - {"package_base": package_base, "status": status.status.value, "last_updated": status.timestamp}) + { + "package_base": package_base, + "status": status.status.value, + "last_updated": status.timestamp, + "repository": self.repository_id.id, + }) - @staticmethod - def _packages_get_select_package_bases(connection: Connection) -> dict[str, Package]: + def _packages_get_select_package_bases(self, connection: Connection) -> dict[str, Package]: """ select package bases from the table @@ -167,11 +183,13 @@ class PackageOperations(Operations): remote=RemoteSource.from_json(row), packages={}, packager=row["packager"] or None, - ) for row in connection.execute("""select * from package_bases""") + ) for row in connection.execute( + """select * from package_bases where repository = :repository""", + {"repository": self.repository_id.id} + ) } - @staticmethod - def _packages_get_select_packages(connection: Connection, packages: dict[str, Package]) -> dict[str, Package]: + def _packages_get_select_packages(self, connection: Connection, packages: dict[str, Package]) -> dict[str, Package]: """ select packages from the table @@ -182,14 +200,16 @@ class PackageOperations(Operations): Returns: dict[str, Package]: map of the package base to its descriptor including individual packages """ - for row in connection.execute("""select * from packages"""): + for row in connection.execute( + """select * from packages where repository = :repository""", + {"repository": self.repository_id.id} + ): 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]: + def _packages_get_select_statuses(self, connection: Connection) -> dict[str, BuildStatus]: """ select package build statuses from the table @@ -201,7 +221,10 @@ class PackageOperations(Operations): """ return { row["package_base"]: BuildStatus.from_json({"status": row["status"], "timestamp": row["last_updated"]}) - for row in connection.execute("""select * from package_statuses""") + for row in connection.execute( + """select * from package_statuses where repository = :repository""", + {"repository": self.repository_id.id} + ) } def package_remove(self, package_base: str) -> None: diff --git a/src/ahriman/core/database/operations/patch_operations.py b/src/ahriman/core/database/operations/patch_operations.py index 98cbbfc4..fd33fea8 100644 --- a/src/ahriman/core/database/operations/patch_operations.py +++ b/src/ahriman/core/database/operations/patch_operations.py @@ -40,7 +40,7 @@ class PatchOperations(Operations): Returns: list[PkgbuildPatch]: plain text patch for the package """ - return self.patches_list(package_base, []).get(package_base, []) + return self.patches_list(package_base, None).get(package_base, []) def patches_insert(self, package_base: str, patch: PkgbuildPatch) -> None: """ @@ -64,13 +64,13 @@ class PatchOperations(Operations): return self.with_connection(run, commit=True) - def patches_list(self, package_base: str | None, variables: list[str]) -> dict[str, list[PkgbuildPatch]]: + def patches_list(self, package_base: str | None, variables: list[str] | None) -> dict[str, list[PkgbuildPatch]]: """ extract all patches Args: package_base(str | None): optional filter by package base - variables(list[str]): extract patches only for specified PKGBUILD variables + variables(list[str] | None): extract patches only for specified PKGBUILD variables Returns: dict[str, list[PkgbuildPatch]]: map of package base to patch content @@ -86,29 +86,30 @@ class PatchOperations(Operations): # we could use itertools & operator but why? patches: dict[str, list[PkgbuildPatch]] = defaultdict(list) for package, patch in self.with_connection(run): - if variables and patch.key not in variables: + if variables is not None and patch.key not in variables: continue patches[package].append(patch) return dict(patches) - def patches_remove(self, package_base: str, variables: list[str]) -> None: + def patches_remove(self, package_base: str, variables: list[str] | None) -> None: """ remove patch set Args: package_base(str): package base to clear patches - variables(list[str]): remove patches only for specified PKGBUILD variables + variables(list[str] | None): remove patches only for specified PKGBUILD variables """ def run_many(connection: Connection) -> None: + patches = variables or [] # suppress mypy warning connection.executemany( """delete from patches where package_base = :package_base and variable = :variable""", - [{"package_base": package_base, "variable": variable} for variable in variables]) + [{"package_base": package_base, "variable": variable} for variable in patches]) def run(connection: Connection) -> None: connection.execute( """delete from patches where package_base = :package_base""", {"package_base": package_base}) - if variables: + if variables is not None: return self.with_connection(run_many, commit=True) return self.with_connection(run, commit=True) diff --git a/src/ahriman/core/database/sqlite.py b/src/ahriman/core/database/sqlite.py index 9a668983..500f63cc 100644 --- a/src/ahriman/core/database/sqlite.py +++ b/src/ahriman/core/database/sqlite.py @@ -56,8 +56,11 @@ class SQLite(AuthOperations, BuildOperations, LogsOperations, PackageOperations, Self: fully initialized instance of the database """ path = cls.database_path(configuration) - database = cls(path) + _, repository_id = configuration.check_loaded() + + database = cls(path, repository_id) database.init(configuration) + return database @staticmethod diff --git a/src/ahriman/core/exceptions.py b/src/ahriman/core/exceptions.py index 2be3fbc9..5687f83e 100644 --- a/src/ahriman/core/exceptions.py +++ b/src/ahriman/core/exceptions.py @@ -23,6 +23,8 @@ from collections.abc import Callable from pathlib import Path from typing import Any, Self +from ahriman.models.repository_id import RepositoryId + class BuildError(RuntimeError): """ @@ -165,7 +167,7 @@ class MissingArchitectureError(ValueError): Args: command(str): command name which throws exception """ - ValueError.__init__(self, f"Architecture required for subcommand {command}, but missing") + ValueError.__init__(self, f"Architecture/repository required for subcommand {command}, but missing") class MultipleArchitecturesError(ValueError): @@ -173,14 +175,18 @@ class MultipleArchitecturesError(ValueError): exception which will be raised if multiple architectures are not supported by the handler """ - def __init__(self, command: str) -> None: + def __init__(self, command: str, repositories: list[RepositoryId] | None = None) -> None: """ default constructor Args: command(str): command name which throws exception + repositories(list[RepositoryId] | None, optional): found repository list (Default value = None) """ - ValueError.__init__(self, f"Multiple architectures are not supported by subcommand {command}") + message = f"Multiple architectures/repositories are not supported by subcommand {command}" + if repositories is not None: + message += f", got {repositories}" + ValueError.__init__(self, message) class OptionError(ValueError): diff --git a/src/ahriman/core/gitremote/remote_pull.py b/src/ahriman/core/gitremote/remote_pull.py index f5d5718a..7536a59d 100644 --- a/src/ahriman/core/gitremote/remote_pull.py +++ b/src/ahriman/core/gitremote/remote_pull.py @@ -30,6 +30,7 @@ from ahriman.core.util import walk from ahriman.models.package import Package from ahriman.models.package_source import PackageSource from ahriman.models.remote_source import RemoteSource +from ahriman.models.repository_id import RepositoryId class RemotePull(LazyLogging): @@ -42,13 +43,13 @@ class RemotePull(LazyLogging): repository_paths(RepositoryPaths): repository paths instance """ - def __init__(self, configuration: Configuration, architecture: str, section: str) -> None: + def __init__(self, repository_id: RepositoryId, configuration: Configuration, section: str) -> None: """ default constructor Args: + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance - architecture(str): repository architecture section(str): settings section name """ self.remote_source = RemoteSource( @@ -58,7 +59,7 @@ class RemotePull(LazyLogging): branch=configuration.get(section, "pull_branch", fallback="master"), source=PackageSource.Local, ) - self.architecture = architecture + self.architecture = repository_id.architecture self.repository_paths = configuration.repository_paths def package_copy(self, pkgbuild_path: Path) -> None: diff --git a/src/ahriman/core/gitremote/remote_pull_trigger.py b/src/ahriman/core/gitremote/remote_pull_trigger.py index b850cc77..4fef34d5 100644 --- a/src/ahriman/core/gitremote/remote_pull_trigger.py +++ b/src/ahriman/core/gitremote/remote_pull_trigger.py @@ -20,6 +20,7 @@ from ahriman.core.configuration import Configuration from ahriman.core.gitremote.remote_pull import RemotePull from ahriman.core.triggers import Trigger +from ahriman.models.repository_id import RepositoryId class RemotePullTrigger(Trigger): @@ -56,15 +57,15 @@ class RemotePullTrigger(Trigger): } CONFIGURATION_SCHEMA_FALLBACK = "gitremote" - def __init__(self, architecture: str, configuration: Configuration) -> None: + def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None: """ default constructor Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance """ - Trigger.__init__(self, architecture, configuration) + Trigger.__init__(self, repository_id, configuration) self.targets = self.configuration_sections(configuration) @classmethod @@ -86,6 +87,6 @@ class RemotePullTrigger(Trigger): """ for target in self.targets: section, _ = self.configuration.gettype( - target, self.architecture, fallback=self.CONFIGURATION_SCHEMA_FALLBACK) - runner = RemotePull(self.configuration, self.architecture, section) + target, self.repository_id, fallback=self.CONFIGURATION_SCHEMA_FALLBACK) + runner = RemotePull(self.repository_id, self.configuration, section) runner.run() diff --git a/src/ahriman/core/gitremote/remote_push_trigger.py b/src/ahriman/core/gitremote/remote_push_trigger.py index a2a9d47d..a46713e6 100644 --- a/src/ahriman/core/gitremote/remote_push_trigger.py +++ b/src/ahriman/core/gitremote/remote_push_trigger.py @@ -24,6 +24,7 @@ from ahriman.core.gitremote.remote_push import RemotePush from ahriman.core.triggers import Trigger from ahriman.models.context_key import ContextKey from ahriman.models.package import Package +from ahriman.models.repository_id import RepositoryId from ahriman.models.result import Result @@ -67,15 +68,15 @@ class RemotePushTrigger(Trigger): } CONFIGURATION_SCHEMA_FALLBACK = "gitremote" - def __init__(self, architecture: str, configuration: Configuration) -> None: + def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None: """ default constructor Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance """ - Trigger.__init__(self, architecture, configuration) + Trigger.__init__(self, repository_id, configuration) self.targets = self.configuration_sections(configuration) @classmethod @@ -107,6 +108,6 @@ class RemotePushTrigger(Trigger): for target in self.targets: section, _ = self.configuration.gettype( - target, self.architecture, fallback=self.CONFIGURATION_SCHEMA_FALLBACK) + target, self.repository_id, fallback=self.CONFIGURATION_SCHEMA_FALLBACK) runner = RemotePush(database, self.configuration, section) runner.run(result) diff --git a/src/ahriman/core/http/sync_http_client.py b/src/ahriman/core/http/sync_http_client.py index 731e0b6f..6b670c31 100644 --- a/src/ahriman/core/http/sync_http_client.py +++ b/src/ahriman/core/http/sync_http_client.py @@ -41,14 +41,14 @@ class SyncHttpClient(LazyLogging): timeout(int): HTTP request timeout in seconds """ - def __init__(self, section: str | None = None, configuration: Configuration | None = None, *, + def __init__(self, configuration: Configuration | None = None, section: str | None = None, *, suppress_errors: bool = False) -> None: """ default constructor Args: - section(str, optional): settings section name (Default value = None) configuration(Configuration | None): configuration instance (Default value = None) + section(str, optional): settings section name (Default value = None) suppress_errors(bool, optional): suppress logging of request errors (Default value = False) """ if configuration is None: diff --git a/src/ahriman/core/log/__init__.py b/src/ahriman/core/log/__init__.py index 97b65d75..c0ba3e7d 100644 --- a/src/ahriman/core/log/__init__.py +++ b/src/ahriman/core/log/__init__.py @@ -18,4 +18,3 @@ # along with this program. If not, see . # from ahriman.core.log.lazy_logging import LazyLogging -from ahriman.core.log.log import Log diff --git a/src/ahriman/core/log/log.py b/src/ahriman/core/log/log_loader.py similarity index 95% rename from src/ahriman/core/log/log.py rename to src/ahriman/core/log/log_loader.py index 7125d973..57189d44 100644 --- a/src/ahriman/core/log/log.py +++ b/src/ahriman/core/log/log_loader.py @@ -27,7 +27,7 @@ from ahriman.core.log.http_log_handler import HttpLogHandler from ahriman.models.log_handler import LogHandler -class Log: +class LogLoader: """ simple static method class which setups application loggers @@ -63,7 +63,7 @@ class Log: del JournalHandler return LogHandler.Journald # journald import was found except ImportError: - if Log.DEFAULT_SYSLOG_DEVICE.exists(): + if LogLoader.DEFAULT_SYSLOG_DEVICE.exists(): return LogHandler.Syslog return LogHandler.Console @@ -94,8 +94,7 @@ class Log: fileConfig(log_configuration, disable_existing_loggers=True) logging.debug("using %s logger", default_handler) except Exception: - logging.basicConfig(filename=None, format=Log.DEFAULT_LOG_FORMAT, - level=Log.DEFAULT_LOG_LEVEL) + logging.basicConfig(filename=None, format=LogLoader.DEFAULT_LOG_FORMAT, level=LogLoader.DEFAULT_LOG_LEVEL) logging.exception("could not load logging from configuration, fallback to stderr") HttpLogHandler.load(configuration, report=report) diff --git a/src/ahriman/core/report/console.py b/src/ahriman/core/report/console.py index 8476a129..d8cd4b4e 100644 --- a/src/ahriman/core/report/console.py +++ b/src/ahriman/core/report/console.py @@ -21,6 +21,7 @@ from ahriman.core.configuration import Configuration from ahriman.core.formatters import BuildPrinter from ahriman.core.report.report import Report from ahriman.models.package import Package +from ahriman.models.repository_id import RepositoryId from ahriman.models.result import Result @@ -32,16 +33,16 @@ class Console(Report): use_utf(bool): print utf8 symbols instead of ASCII """ - def __init__(self, architecture: str, configuration: Configuration, section: str) -> None: + def __init__(self, repository_id: RepositoryId, configuration: Configuration, section: str) -> None: """ default constructor Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance section(str): settings section name """ - Report.__init__(self, architecture, configuration) + Report.__init__(self, repository_id, configuration) self.use_utf = configuration.getboolean(section, "use_utf", fallback=True) def generate(self, packages: list[Package], result: Result) -> None: diff --git a/src/ahriman/core/report/email.py b/src/ahriman/core/report/email.py index e51825a4..ad5360a9 100644 --- a/src/ahriman/core/report/email.py +++ b/src/ahriman/core/report/email.py @@ -27,6 +27,7 @@ from ahriman.core.report.jinja_template import JinjaTemplate from ahriman.core.report.report import Report from ahriman.core.util import pretty_datetime, utcnow from ahriman.models.package import Package +from ahriman.models.repository_id import RepositoryId from ahriman.models.result import Result from ahriman.models.smtp_ssl_settings import SmtpSSLSettings @@ -48,17 +49,17 @@ class Email(Report, JinjaTemplate): user(str | None): username to authenticate via SMTP """ - def __init__(self, architecture: str, configuration: Configuration, section: str) -> None: + def __init__(self, repository_id: RepositoryId, configuration: Configuration, section: str) -> None: """ default constructor Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance section(str): settings section name """ - Report.__init__(self, architecture, configuration) - JinjaTemplate.__init__(self, section, configuration) + Report.__init__(self, repository_id, configuration) + JinjaTemplate.__init__(self, repository_id, configuration, section) self.full_template_path = configuration.getpath(section, "full_template_path", fallback=None) self.template_path = configuration.getpath(section, "template_path") diff --git a/src/ahriman/core/report/html.py b/src/ahriman/core/report/html.py index ffc1ff08..11d89a87 100644 --- a/src/ahriman/core/report/html.py +++ b/src/ahriman/core/report/html.py @@ -21,6 +21,7 @@ from ahriman.core.configuration import Configuration from ahriman.core.report.jinja_template import JinjaTemplate from ahriman.core.report.report import Report from ahriman.models.package import Package +from ahriman.models.repository_id import RepositoryId from ahriman.models.result import Result @@ -33,17 +34,17 @@ class HTML(Report, JinjaTemplate): template_path(Path): path to template for full package list """ - def __init__(self, architecture: str, configuration: Configuration, section: str) -> None: + def __init__(self, repository_id: RepositoryId, configuration: Configuration, section: str) -> None: """ default constructor Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance section(str): settings section name """ - Report.__init__(self, architecture, configuration) - JinjaTemplate.__init__(self, section, configuration) + Report.__init__(self, repository_id, configuration) + JinjaTemplate.__init__(self, repository_id, configuration, section) self.report_path = configuration.getpath(section, "path") self.template_path = configuration.getpath(section, "template_path") diff --git a/src/ahriman/core/report/jinja_template.py b/src/ahriman/core/report/jinja_template.py index a525ecf6..4826d29c 100644 --- a/src/ahriman/core/report/jinja_template.py +++ b/src/ahriman/core/report/jinja_template.py @@ -25,6 +25,7 @@ from pathlib import Path from ahriman.core.configuration import Configuration from ahriman.core.sign.gpg import GPG from ahriman.core.util import pretty_datetime, pretty_size +from ahriman.models.repository_id import RepositoryId from ahriman.models.result import Result from ahriman.models.sign_settings import SignSettings @@ -63,19 +64,20 @@ class JinjaTemplate: sign_targets(set[SignSettings]): targets to sign enabled in configuration """ - def __init__(self, section: str, configuration: Configuration) -> None: + def __init__(self, repository_id: RepositoryId, configuration: Configuration, section: str) -> None: """ default constructor Args: - section(str): settings section name + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance + section(str): settings section name """ self.link_path = configuration.get(section, "link_path") # base template vars self.homepage = configuration.get(section, "homepage", fallback=None) - self.name = configuration.repository_name + self.name = repository_id.name self.sign_targets, self.default_pgp_key = GPG.sign_options(configuration) diff --git a/src/ahriman/core/report/remote_call.py b/src/ahriman/core/report/remote_call.py index 94d5feb3..55062686 100644 --- a/src/ahriman/core/report/remote_call.py +++ b/src/ahriman/core/report/remote_call.py @@ -23,6 +23,7 @@ from ahriman.core.configuration import Configuration from ahriman.core.report.report import Report from ahriman.core.status.web_client import WebClient from ahriman.models.package import Package +from ahriman.models.repository_id import RepositoryId from ahriman.models.result import Result from ahriman.models.waiter import Waiter @@ -39,16 +40,16 @@ class RemoteCall(Report): wait_timeout(int): timeout to wait external process """ - def __init__(self, architecture: str, configuration: Configuration, section: str) -> None: + def __init__(self, repository_id: RepositoryId, configuration: Configuration, section: str) -> None: """ default constructor Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance section(str): settings section name """ - Report.__init__(self, architecture, configuration) + Report.__init__(self, repository_id, configuration) self.client = WebClient(configuration) diff --git a/src/ahriman/core/report/report.py b/src/ahriman/core/report/report.py index 392837fb..c11898af 100644 --- a/src/ahriman/core/report/report.py +++ b/src/ahriman/core/report/report.py @@ -24,6 +24,7 @@ from ahriman.core.exceptions import ReportError from ahriman.core.log import LazyLogging from ahriman.models.package import Package from ahriman.models.report_settings import ReportSettings +from ahriman.models.repository_id import RepositoryId from ahriman.models.result import Result @@ -32,17 +33,15 @@ class Report(LazyLogging): base report generator Attributes: - architecture(str): repository architecture configuration(Configuration): configuration instance + repository_id(RepositoryId): repository unique identifier Examples: ``Report`` classes provide several method in order to operate with the report generation and additional class method ``load`` which can be used in order to determine right report instance:: - >>> from ahriman.core.configuration import Configuration - >>> >>> configuration = Configuration() - >>> report = Report.load("x86_64", configuration, "email") + >>> report = Report.load(RepositoryId("x86_64", "aur-clone"), configuration, "email") The ``generate`` method can be used in order to perform the report itself, whereas ``run`` method handles exception and raises ``ReportFailed`` instead:: @@ -55,49 +54,49 @@ class Report(LazyLogging): >>> report.run(Result(), []) """ - def __init__(self, architecture: str, configuration: Configuration) -> None: + def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None: """ default constructor Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance """ - self.architecture = architecture + self.repository_id = repository_id self.configuration = configuration @staticmethod - def load(architecture: str, configuration: Configuration, target: str) -> Report: + def load(repository_id: RepositoryId, configuration: Configuration, target: str) -> Report: """ load client from settings Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance target(str): target to generate report aka section name (e.g. html) Returns: Report: client according to current settings """ - section, provider_name = configuration.gettype(target, architecture) + section, provider_name = configuration.gettype(target, repository_id) match ReportSettings.from_option(provider_name): case ReportSettings.HTML: from ahriman.core.report.html import HTML - return HTML(architecture, configuration, section) + return HTML(repository_id, configuration, section) case ReportSettings.Email: from ahriman.core.report.email import Email - return Email(architecture, configuration, section) + return Email(repository_id, configuration, section) case ReportSettings.Console: from ahriman.core.report.console import Console - return Console(architecture, configuration, section) + return Console(repository_id, configuration, section) case ReportSettings.Telegram: from ahriman.core.report.telegram import Telegram - return Telegram(architecture, configuration, section) + return Telegram(repository_id, configuration, section) case ReportSettings.RemoteCall: from ahriman.core.report.remote_call import RemoteCall - return RemoteCall(architecture, configuration, section) + return RemoteCall(repository_id, configuration, section) case _: - return Report(architecture, configuration) # should never happen + return Report(repository_id, configuration) # should never happen def generate(self, packages: list[Package], result: Result) -> None: """ diff --git a/src/ahriman/core/report/report_trigger.py b/src/ahriman/core/report/report_trigger.py index a5d441eb..19ac542d 100644 --- a/src/ahriman/core/report/report_trigger.py +++ b/src/ahriman/core/report/report_trigger.py @@ -21,6 +21,7 @@ from ahriman.core.configuration import Configuration from ahriman.core.triggers import Trigger from ahriman.core.report.report import Report from ahriman.models.package import Package +from ahriman.models.repository_id import RepositoryId from ahriman.models.result import Result @@ -218,15 +219,15 @@ class ReportTrigger(Trigger): } } - def __init__(self, architecture: str, configuration: Configuration) -> None: + def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None: """ default constructor Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance """ - Trigger.__init__(self, architecture, configuration) + Trigger.__init__(self, repository_id, configuration) self.targets = self.configuration_sections(configuration) @classmethod @@ -251,5 +252,5 @@ class ReportTrigger(Trigger): packages(list[Package]): list of all available packages """ for target in self.targets: - runner = Report.load(self.architecture, self.configuration, target) + runner = Report.load(self.repository_id, self.configuration, target) runner.run(result, packages) diff --git a/src/ahriman/core/report/telegram.py b/src/ahriman/core/report/telegram.py index bab40311..46145150 100644 --- a/src/ahriman/core/report/telegram.py +++ b/src/ahriman/core/report/telegram.py @@ -22,6 +22,7 @@ from ahriman.core.http import SyncHttpClient from ahriman.core.report.jinja_template import JinjaTemplate from ahriman.core.report.report import Report from ahriman.models.package import Package +from ahriman.models.repository_id import RepositoryId from ahriman.models.result import Result @@ -41,18 +42,18 @@ class Telegram(Report, JinjaTemplate, SyncHttpClient): TELEGRAM_API_URL = "https://api.telegram.org" TELEGRAM_MAX_CONTENT_LENGTH = 4096 - def __init__(self, architecture: str, configuration: Configuration, section: str) -> None: + def __init__(self, repository_id: RepositoryId, configuration: Configuration, section: str) -> None: """ default constructor Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance section(str): settings section name """ - Report.__init__(self, architecture, configuration) - JinjaTemplate.__init__(self, section, configuration) - SyncHttpClient.__init__(self, section, configuration) + Report.__init__(self, repository_id, configuration) + JinjaTemplate.__init__(self, repository_id, configuration, section) + SyncHttpClient.__init__(self, configuration, section) self.api_key = configuration.get(section, "api_key") self.chat_id = configuration.get(section, "chat_id") diff --git a/src/ahriman/core/repository/repository.py b/src/ahriman/core/repository/repository.py index 90f519d4..0017289a 100644 --- a/src/ahriman/core/repository/repository.py +++ b/src/ahriman/core/repository/repository.py @@ -32,6 +32,7 @@ from ahriman.core.util import package_like from ahriman.models.context_key import ContextKey from ahriman.models.package import Package from ahriman.models.pacman_synchronization import PacmanSynchronization +from ahriman.models.repository_id import RepositoryId class Repository(Executor, UpdateHandler): @@ -47,7 +48,7 @@ class Repository(Executor, UpdateHandler): >>> >>> configuration = Configuration() >>> database = SQLite.load(configuration) - >>> repository = Repository.load("x86_64", configuration, database, report=True) + >>> repository = Repository.load(RepositoryId("x86_64", "x86_64"), configuration, database, report=True) >>> known_packages = repository.packages() >>> >>> build_result = repository.process_build(known_packages) @@ -58,13 +59,13 @@ class Repository(Executor, UpdateHandler): """ @classmethod - def load(cls, architecture: str, configuration: Configuration, database: SQLite, *, report: bool, + def load(cls, repository_id: RepositoryId, configuration: Configuration, database: SQLite, *, report: bool, refresh_pacman_database: PacmanSynchronization = PacmanSynchronization.Disabled) -> Self: """ load instance from argument list Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance database(SQLite): database instance report(bool): force enable or disable reporting @@ -74,7 +75,7 @@ class Repository(Executor, UpdateHandler): Returns: Self: fully loaded repository class instance """ - instance = cls(architecture, configuration, database, + instance = cls(repository_id, configuration, database, report=report, refresh_pacman_database=refresh_pacman_database) instance._set_context() return instance diff --git a/src/ahriman/core/repository/repository_properties.py b/src/ahriman/core/repository/repository_properties.py index a83a8412..26a78315 100644 --- a/src/ahriman/core/repository/repository_properties.py +++ b/src/ahriman/core/repository/repository_properties.py @@ -27,6 +27,7 @@ from ahriman.core.status.client import Client from ahriman.core.triggers import TriggerLoader from ahriman.models.packagers import Packagers from ahriman.models.pacman_synchronization import PacmanSynchronization +from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.user import User from ahriman.models.user_access import UserAccess @@ -37,47 +38,65 @@ class RepositoryProperties(LazyLogging): repository internal objects holder Attributes: - architecture(str): repository architecture configuration(Configuration): configuration instance database(SQLite): database instance ignore_list(list[str]): package bases which will be ignored during auto updates - name(str): repository name pacman(Pacman): alpm wrapper instance paths(RepositoryPaths): repository paths instance repo(Repo): repo commands wrapper instance reporter(Client): build status reporter instance + repository_id(RepositoryId): repository unique identifier sign(GPG): GPG wrapper instance triggers(TriggerLoader): triggers holder vcs_allowed_age(int): maximal age of the VCS packages before they will be checked """ - def __init__(self, architecture: str, configuration: Configuration, database: SQLite, *, report: bool, + def __init__(self, repository_id: RepositoryId, configuration: Configuration, database: SQLite, *, report: bool, refresh_pacman_database: PacmanSynchronization) -> None: """ default constructor Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance database(SQLite): database instance report(bool): force enable or disable reporting refresh_pacman_database(PacmanSynchronization): pacman database synchronization level """ - self.architecture = architecture + self.repository_id = repository_id self.configuration = configuration self.database = database - self.name = configuration.repository_name self.vcs_allowed_age = configuration.getint("build", "vcs_allowed_age", fallback=0) self.paths: RepositoryPaths = configuration.repository_paths # additional workaround for pycharm typing self.ignore_list = configuration.getlist("build", "ignore_packages", fallback=[]) - self.pacman = Pacman(architecture, configuration, refresh_database=refresh_pacman_database) + self.pacman = Pacman(repository_id, configuration, refresh_database=refresh_pacman_database) self.sign = GPG(configuration) self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args) self.reporter = Client.load(configuration, report=report) - self.triggers = TriggerLoader.load(architecture, configuration) + self.triggers = TriggerLoader.load(repository_id, configuration) + + @property + def architecture(self) -> str: + """ + repository architecture for backward compatibility + + Returns: + str: repository architecture + """ + return self.repository_id.architecture + + @property + def name(self) -> str: + """ + repository name for backward compatibility + + Returns: + str: repository name + """ + return self.repository_id.name def packager(self, packagers: Packagers, package_base: str) -> User: """ diff --git a/src/ahriman/core/spawn.py b/src/ahriman/core/spawn.py index ed550a25..d50801b8 100644 --- a/src/ahriman/core/spawn.py +++ b/src/ahriman/core/spawn.py @@ -28,6 +28,7 @@ from multiprocessing import Process, Queue from threading import Lock, Thread from ahriman.core.log import LazyLogging +from ahriman.models.repository_id import RepositoryId class Spawn(Thread, LazyLogging): @@ -37,22 +38,23 @@ class Spawn(Thread, LazyLogging): Attributes: active(dict[str, Process]): map of active child processes required to avoid zombies - architecture(str): repository architecture command_arguments(list[str]): base command line arguments queue(Queue[tuple[str, bool, int]]): multiprocessing queue to read updates from processes + repository_id(RepositoryId): repository unique identifier """ - def __init__(self, args_parser: argparse.ArgumentParser, architecture: str, command_arguments: list[str]) -> None: + def __init__(self, args_parser: argparse.ArgumentParser, repository_id: RepositoryId, + command_arguments: list[str]) -> None: """ default constructor Args: args_parser(argparse.ArgumentParser): command line parser for the application - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier command_arguments(list[str]): base command line arguments """ Thread.__init__(self, name="spawn") - self.architecture = architecture + self.repository_id = repository_id self.args_parser = args_parser self.command_arguments = command_arguments @@ -77,20 +79,20 @@ class Spawn(Thread, LazyLogging): return name if value else f"no-{name}" @staticmethod - def process(callback: Callable[[argparse.Namespace, str], bool], args: argparse.Namespace, architecture: str, - process_id: str, queue: Queue[tuple[str, bool, int]]) -> None: # pylint: disable=unsubscriptable-object + def process(callback: Callable[[argparse.Namespace, RepositoryId], bool], args: argparse.Namespace, + repository_id: RepositoryId, process_id: str, queue: Queue[tuple[str, bool, int]]) -> None: # pylint: disable=unsubscriptable-object """ helper to run external process Args: callback(Callable[[argparse.Namespace, str], bool]): application run function (i.e. Handler.run method) args(argparse.Namespace): command line arguments - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier process_id(str): process unique identifier queue(Queue[tuple[str, bool, int]]): output queue """ start_time = time.monotonic() - result = callback(args, architecture) + result = callback(args, repository_id) stop_time = time.monotonic() consumed_time = int(1000 * (stop_time - start_time)) @@ -128,7 +130,7 @@ class Spawn(Thread, LazyLogging): callback = parsed.handler.call process = Process(target=self.process, - args=(callback, parsed, self.architecture, process_id, self.queue), + args=(callback, parsed, self.repository_id, process_id, self.queue), daemon=True) process.start() diff --git a/src/ahriman/core/status/watcher.py b/src/ahriman/core/status/watcher.py index 3ea03c4c..05b35419 100644 --- a/src/ahriman/core/status/watcher.py +++ b/src/ahriman/core/status/watcher.py @@ -25,6 +25,7 @@ from ahriman.core.repository import Repository from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.log_record_id import LogRecordId from ahriman.models.package import Package +from ahriman.models.repository_id import RepositoryId class Watcher(LazyLogging): @@ -32,26 +33,26 @@ class Watcher(LazyLogging): package status watcher Attributes: - architecture(str): repository architecture database(SQLite): database instance known(dict[str, tuple[Package, BuildStatus]]): list of known packages. For the most cases ``packages`` should be used instead repository(Repository): repository object + repository_id(RepositoryId): repository unique identifier status(BuildStatus): daemon status """ - def __init__(self, architecture: str, configuration: Configuration, database: SQLite) -> None: + def __init__(self, repository_id: RepositoryId, configuration: Configuration, database: SQLite) -> None: """ default constructor Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance database(SQLite): database instance """ - self.architecture = architecture + self.repository_id = repository_id self.database = database - self.repository = Repository.load(architecture, configuration, database, report=False) + self.repository = Repository.load(repository_id, configuration, database, report=False) self.known: dict[str, tuple[Package, BuildStatus]] = {} self.status = BuildStatus() diff --git a/src/ahriman/core/status/web_client.py b/src/ahriman/core/status/web_client.py index 76e2a2b7..d63b79e0 100644 --- a/src/ahriman/core/status/web_client.py +++ b/src/ahriman/core/status/web_client.py @@ -51,7 +51,7 @@ class WebClient(Client, SyncHttpClient): configuration(Configuration): configuration instance """ suppress_errors = configuration.getboolean("settings", "suppress_http_log_errors", fallback=False) - SyncHttpClient.__init__(self, "web", configuration, suppress_errors=suppress_errors) + SyncHttpClient.__init__(self, configuration, "web", suppress_errors=suppress_errors) self.address, self.use_unix_socket = self.parse_address(configuration) diff --git a/src/ahriman/core/support/keyring_trigger.py b/src/ahriman/core/support/keyring_trigger.py index 583b3beb..6fbc0a06 100644 --- a/src/ahriman/core/support/keyring_trigger.py +++ b/src/ahriman/core/support/keyring_trigger.py @@ -25,6 +25,7 @@ from ahriman.core.support.package_creator import PackageCreator from ahriman.core.support.pkgbuild.keyring_generator import KeyringGenerator from ahriman.core.triggers import Trigger from ahriman.models.context_key import ContextKey +from ahriman.models.repository_id import RepositoryId class KeyringTrigger(Trigger): @@ -82,15 +83,15 @@ class KeyringTrigger(Trigger): }, } - def __init__(self, architecture: str, configuration: Configuration) -> None: + def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None: """ default constructor Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance """ - Trigger.__init__(self, architecture, configuration) + Trigger.__init__(self, repository_id, configuration) self.targets = self.configuration_sections(configuration) @classmethod @@ -115,6 +116,6 @@ class KeyringTrigger(Trigger): database = ctx.get(ContextKey("database", SQLite)) for target in self.targets: - generator = KeyringGenerator(database, sign, self.configuration, target) + generator = KeyringGenerator(database, sign, self.repository_id, self.configuration, target) runner = PackageCreator(self.configuration, generator) runner.run() diff --git a/src/ahriman/core/support/mirrorlist_trigger.py b/src/ahriman/core/support/mirrorlist_trigger.py index a9e182ca..37c3886a 100644 --- a/src/ahriman/core/support/mirrorlist_trigger.py +++ b/src/ahriman/core/support/mirrorlist_trigger.py @@ -21,6 +21,7 @@ from ahriman.core.configuration import Configuration from ahriman.core.support.package_creator import PackageCreator from ahriman.core.support.pkgbuild.mirrorlist_generator import MirrorlistGenerator from ahriman.core.triggers import Trigger +from ahriman.models.repository_id import RepositoryId class MirrorlistTrigger(Trigger): @@ -75,15 +76,15 @@ class MirrorlistTrigger(Trigger): }, } - def __init__(self, architecture: str, configuration: Configuration) -> None: + def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None: """ default constructor Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance """ - Trigger.__init__(self, architecture, configuration) + Trigger.__init__(self, repository_id, configuration) self.targets = self.configuration_sections(configuration) @classmethod @@ -104,6 +105,6 @@ class MirrorlistTrigger(Trigger): trigger action which will be called at the start of the application """ for target in self.targets: - generator = MirrorlistGenerator(self.configuration, target) + generator = MirrorlistGenerator(self.repository_id, self.configuration, target) runner = PackageCreator(self.configuration, generator) runner.run() diff --git a/src/ahriman/core/support/package_creator.py b/src/ahriman/core/support/package_creator.py index 4048d27b..44aee94d 100644 --- a/src/ahriman/core/support/package_creator.py +++ b/src/ahriman/core/support/package_creator.py @@ -66,6 +66,6 @@ class PackageCreator: # register package ctx = context.get() database: SQLite = ctx.get(ContextKey("database", SQLite)) - _, architecture = self.configuration.check_loaded() - package = Package.from_build(local_path, architecture, None) + _, repository_id = self.configuration.check_loaded() + package = Package.from_build(local_path, repository_id.architecture, None) database.package_update(package, BuildStatus()) diff --git a/src/ahriman/core/support/pkgbuild/keyring_generator.py b/src/ahriman/core/support/pkgbuild/keyring_generator.py index 3374eb2b..5e4d320b 100644 --- a/src/ahriman/core/support/pkgbuild/keyring_generator.py +++ b/src/ahriman/core/support/pkgbuild/keyring_generator.py @@ -25,6 +25,7 @@ from ahriman.core.database import SQLite from ahriman.core.exceptions import PkgbuildGeneratorError from ahriman.core.sign.gpg import GPG from ahriman.core.support.pkgbuild.pkgbuild_generator import PkgbuildGenerator +from ahriman.models.repository_id import RepositoryId class KeyringGenerator(PkgbuildGenerator): @@ -43,18 +44,20 @@ class KeyringGenerator(PkgbuildGenerator): trusted(list[str]): lif of trusted PGP keys """ - def __init__(self, database: SQLite, sign: GPG, configuration: Configuration, section: str) -> None: + def __init__(self, database: SQLite, sign: GPG, repository_id: RepositoryId, + configuration: Configuration, section: str) -> None: """ default constructor Args: database(SQLite): database instance sign(GPG): GPG wrapper instance + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance section(str): settings section name """ self.sign = sign - self.name = configuration.repository_name + self.name = repository_id.name # configuration fields packager_keys = [packager.key for packager in database.user_list(None, None) if packager.key is not None] diff --git a/src/ahriman/core/support/pkgbuild/mirrorlist_generator.py b/src/ahriman/core/support/pkgbuild/mirrorlist_generator.py index 36f3df08..0ca18dbe 100644 --- a/src/ahriman/core/support/pkgbuild/mirrorlist_generator.py +++ b/src/ahriman/core/support/pkgbuild/mirrorlist_generator.py @@ -23,6 +23,7 @@ from pathlib import Path from ahriman.core.configuration import Configuration from ahriman.core.support.pkgbuild.pkgbuild_generator import PkgbuildGenerator from ahriman.models.pkgbuild_patch import PkgbuildPatch +from ahriman.models.repository_id import RepositoryId class MirrorlistGenerator(PkgbuildGenerator): @@ -38,24 +39,24 @@ class MirrorlistGenerator(PkgbuildGenerator): servers(list[str]): list of mirror servers """ - def __init__(self, configuration: Configuration, section: str) -> None: + def __init__(self, repository_id: RepositoryId, configuration: Configuration, section: str) -> None: """ default constructor Args: + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance section(str): settings section name """ - name = configuration.repository_name - # configuration fields self.servers = configuration.getlist(section, "servers") - self.path = configuration.getpath(section, "path", fallback=Path("/etc") / "pacman.d" / f"{name}-mirrorlist") + self.path = configuration.getpath( + section, "path", fallback=Path("/etc") / "pacman.d" / f"{repository_id.name}-mirrorlist") self.path = self.path.relative_to("/") # in pkgbuild we are always operating with relative to / path # pkgbuild description fields - self.pkgbuild_pkgname = configuration.get(section, "package", fallback=f"{name}-mirrorlist") + self.pkgbuild_pkgname = configuration.get(section, "package", fallback=f"{repository_id.name}-mirrorlist") self.pkgbuild_pkgdesc = configuration.get( - section, "description", fallback=f"{name} mirror list for use by pacman") + section, "description", fallback=f"{repository_id.name} mirror list for use by pacman") self.pkgbuild_license = configuration.getlist(section, "license", fallback=["Unlicense"]) self.pkgbuild_url = configuration.get(section, "homepage", fallback="") diff --git a/src/ahriman/core/tree.py b/src/ahriman/core/tree.py index eb964f6c..976b877d 100644 --- a/src/ahriman/core/tree.py +++ b/src/ahriman/core/tree.py @@ -19,9 +19,8 @@ # from __future__ import annotations -import functools - from collections.abc import Iterable +from functools import partial from ahriman.core.exceptions import PartitionError from ahriman.core.util import minmax, partition @@ -102,10 +101,11 @@ class Tree: >>> from ahriman.core.configuration import Configuration >>> from ahriman.core.database import SQLite >>> from ahriman.core.repository import Repository + >>> from ahriman.models.repository_id import RepositoryId >>> >>> configuration = Configuration() >>> database = SQLite.load(configuration) - >>> repository = Repository.load("x86_64", configuration, database, report=True) + >>> repository = Repository.load(RepositoryId("x86_64", "aur-clone"), configuration, database, report=True) >>> packages = repository.packages() >>> >>> tree = Tree.resolve(packages) @@ -242,7 +242,7 @@ class Tree: unprocessed = self.leaves[:] while unprocessed: # additional workaround with partial in order to hide cell-var-from-loop pylint warning - predicate = functools.partial(Leaf.is_root, packages=unprocessed) + predicate = partial(Leaf.is_root, packages=unprocessed) new_level, unprocessed = partition(unprocessed, predicate) unsorted.append(new_level) @@ -252,7 +252,7 @@ class Tree: next_level = unsorted[next_num] # change lists inside the collection - predicate = functools.partial(Leaf.is_dependency, packages=next_level) + predicate = partial(Leaf.is_dependency, packages=next_level) unsorted[current_num], to_be_moved = partition(current_level, predicate) unsorted[next_num].extend(to_be_moved) @@ -279,12 +279,12 @@ class Tree: while True: # python doesn't allow to use walrus operator to unpack tuples # get packages which depend on packages in chunk - predicate = functools.partial(Leaf.is_root, packages=chunk) + predicate = partial(Leaf.is_root, packages=chunk) unprocessed, new_dependent = partition(unprocessed, predicate) chunk.extend(new_dependent) # get packages which are dependency of packages in chunk - predicate = functools.partial(Leaf.is_dependency, packages=chunk) + predicate = partial(Leaf.is_dependency, packages=chunk) new_dependencies, unprocessed = partition(unprocessed, predicate) chunk.extend(new_dependencies) diff --git a/src/ahriman/core/triggers/trigger.py b/src/ahriman/core/triggers/trigger.py index b443c513..d16df3c5 100644 --- a/src/ahriman/core/triggers/trigger.py +++ b/src/ahriman/core/triggers/trigger.py @@ -23,6 +23,7 @@ from ahriman.core.configuration import Configuration from ahriman.core.configuration.schema import ConfigurationSchema from ahriman.core.log import LazyLogging from ahriman.models.package import Package +from ahriman.models.repository_id import RepositoryId from ahriman.models.result import Result @@ -34,8 +35,8 @@ class Trigger(LazyLogging): CONFIGURATION_SCHEMA(ConfigurationSchema): (class attribute) configuration schema template CONFIGURATION_SCHEMA_FALLBACK(str | None): (class attribute) optional fallback option for defining configuration schema type used - architecture(str): repository architecture configuration(Configuration): configuration instance + repository_id(RepositoryId): repository unique identifier Examples: This class must be used in order to create own extension. Basically idea is the following:: @@ -51,26 +52,37 @@ class Trigger(LazyLogging): >>> configuration = Configuration() >>> configuration.set_option("build", "triggers", "my.awesome.package.CustomTrigger") >>> - >>> loader = TriggerLoader.load("x86_64", configuration) + >>> loader = TriggerLoader.load(RepositoryId("x86_64", "aur-clone"), configuration) >>> loader.on_result(Result(), []) """ CONFIGURATION_SCHEMA: ConfigurationSchema = {} CONFIGURATION_SCHEMA_FALLBACK: str | None = None - def __init__(self, architecture: str, configuration: Configuration) -> None: + def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None: """ default constructor Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance """ - self.architecture = architecture + self.repository_id = repository_id self.configuration = configuration + @property + def architecture(self) -> str: + """ + repository architecture for backward compatibility + + Returns: + str: repository architecture + """ + return self.repository_id.architecture + @classmethod - def configuration_schema(cls, architecture: str, configuration: Configuration | None) -> ConfigurationSchema: + def configuration_schema(cls, repository_id: RepositoryId, + configuration: Configuration | None) -> ConfigurationSchema: """ configuration schema based on supplied service configuration @@ -78,7 +90,7 @@ class Trigger(LazyLogging): Schema must be in cerberus format, for details and examples you can check built-in triggers. Args: - architecture(str): repository architecture + repository_id(str): repository unique identifier configuration(Configuration | None): configuration instance. If set to None, the default schema should be returned @@ -93,7 +105,7 @@ class Trigger(LazyLogging): if not configuration.has_section(target): continue section, schema_name = configuration.gettype( - target, architecture, fallback=cls.CONFIGURATION_SCHEMA_FALLBACK) + target, repository_id, fallback=cls.CONFIGURATION_SCHEMA_FALLBACK) if schema_name not in cls.CONFIGURATION_SCHEMA: continue result[section] = cls.CONFIGURATION_SCHEMA[schema_name] diff --git a/src/ahriman/core/triggers/trigger_loader.py b/src/ahriman/core/triggers/trigger_loader.py index 82d4405f..d92586f7 100644 --- a/src/ahriman/core/triggers/trigger_loader.py +++ b/src/ahriman/core/triggers/trigger_loader.py @@ -31,6 +31,7 @@ from ahriman.core.exceptions import ExtensionError from ahriman.core.log import LazyLogging from ahriman.core.triggers import Trigger from ahriman.models.package import Package +from ahriman.models.repository_id import RepositoryId from ahriman.models.result import Result @@ -49,7 +50,7 @@ class TriggerLoader(LazyLogging): Having such configuration you can create instance of the loader:: - >>> loader = TriggerLoader.load("x86_64", configuration) + >>> loader = TriggerLoader.load(RepositoryId("x86_64", "aur-clone"), configuration) >>> print(loader.triggers) After that you are free to run triggers:: @@ -65,12 +66,12 @@ class TriggerLoader(LazyLogging): self.triggers: list[Trigger] = [] @classmethod - def load(cls, architecture: str, configuration: Configuration) -> Self: + def load(cls, repository_id: RepositoryId, configuration: Configuration) -> Self: """ create instance from configuration Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance Returns: @@ -78,7 +79,7 @@ class TriggerLoader(LazyLogging): """ instance = cls() instance.triggers = [ - instance.load_trigger(trigger, architecture, configuration) + instance.load_trigger(trigger, repository_id, configuration) for trigger in instance.selected_triggers(configuration) ] @@ -166,13 +167,13 @@ class TriggerLoader(LazyLogging): except ModuleNotFoundError: raise ExtensionError(f"Module {package} not found") from None - def load_trigger(self, module_path: str, architecture: str, configuration: Configuration) -> Trigger: + def load_trigger(self, module_path: str, repository_id: RepositoryId, configuration: Configuration) -> Trigger: """ load trigger by module path Args: module_path(str): module import path to load - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance Returns: @@ -183,7 +184,7 @@ class TriggerLoader(LazyLogging): """ trigger_type = self.load_trigger_class(module_path) try: - trigger = trigger_type(architecture, configuration) + trigger = trigger_type(repository_id, configuration) except Exception: raise ExtensionError(f"Could not load instance of trigger from {trigger_type} loaded from {module_path}") diff --git a/src/ahriman/core/upload/github.py b/src/ahriman/core/upload/github.py index 5002b2a6..35be8fa1 100644 --- a/src/ahriman/core/upload/github.py +++ b/src/ahriman/core/upload/github.py @@ -25,32 +25,44 @@ from typing import Any from ahriman.core.configuration import Configuration from ahriman.core.upload.http_upload import HttpUpload +from ahriman.core.upload.upload import Upload from ahriman.core.util import walk from ahriman.models.package import Package +from ahriman.models.repository_id import RepositoryId -class Github(HttpUpload): +class GitHub(Upload, HttpUpload): """ upload files to GitHub releases Attributes: github_owner(str): GitHub repository owner + github_release_tag(str): GitHub release tag + github_release_tag_name(str): GitHub release tag name github_repository(str): GitHub repository name """ - def __init__(self, architecture: str, configuration: Configuration, section: str) -> None: + def __init__(self, repository_id: RepositoryId, configuration: Configuration, section: str) -> None: """ default constructor Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance section(str): settings section name """ - HttpUpload.__init__(self, architecture, configuration, section) + Upload.__init__(self, repository_id, configuration) + HttpUpload.__init__(self, configuration, section) + self.github_owner = configuration.get(section, "owner") self.github_repository = configuration.get(section, "repository") + if configuration.getboolean(section, "use_full_release_name", fallback=False): + self.github_release_tag = f"{repository_id.name}-{repository_id.architecture}" + self.github_release_tag_name = f"{repository_id.name} {repository_id.architecture}" + else: + self.github_release_tag_name = self.github_release_tag = repository_id.architecture + def asset_remove(self, release: dict[str, Any], name: str) -> None: """ remove asset from the release by name @@ -136,7 +148,10 @@ class Github(HttpUpload): dict[str, Any]: GitHub API release object for the new release """ url = f"https://api.github.com/repos/{self.github_owner}/{self.github_repository}/releases" - response = self.make_request("POST", url, json={"tag_name": self.architecture, "name": self.architecture}) + response = self.make_request("POST", url, json={ + "tag_name": self.github_release_tag, + "name": self.github_release_tag_name, + }) release: dict[str, Any] = response.json() return release @@ -147,7 +162,7 @@ class Github(HttpUpload): Returns: dict[str, Any] | None: GitHub API release object if release found and None otherwise """ - url = f"https://api.github.com/repos/{self.github_owner}/{self.github_repository}/releases/tags/{self.architecture}" + url = f"https://api.github.com/repos/{self.github_owner}/{self.github_repository}/releases/tags/{self.github_release_tag}" try: response = self.make_request("GET", url) release: dict[str, Any] = response.json() diff --git a/src/ahriman/core/upload/http_upload.py b/src/ahriman/core/upload/http_upload.py index 68cc5c52..9c7d4d8e 100644 --- a/src/ahriman/core/upload/http_upload.py +++ b/src/ahriman/core/upload/http_upload.py @@ -21,28 +21,14 @@ import hashlib from pathlib import Path -from ahriman.core.configuration import Configuration from ahriman.core.http import SyncHttpClient -from ahriman.core.upload.upload import Upload -class HttpUpload(Upload, SyncHttpClient): +class HttpUpload(SyncHttpClient): """ helper for the http based uploads """ - def __init__(self, architecture: str, configuration: Configuration, section: str) -> None: - """ - default constructor - - Args: - architecture(str): repository architecture - configuration(Configuration): configuration instance - section(str): configuration section name - """ - Upload.__init__(self, architecture, configuration) - SyncHttpClient.__init__(self, section, configuration) - @staticmethod def calculate_hash(path: Path) -> str: """ diff --git a/src/ahriman/core/upload/remote_service.py b/src/ahriman/core/upload/remote_service.py index 5ea2e03a..507e7383 100644 --- a/src/ahriman/core/upload/remote_service.py +++ b/src/ahriman/core/upload/remote_service.py @@ -27,10 +27,12 @@ from ahriman.core.http import MultipartType from ahriman.core.sign.gpg import GPG from ahriman.core.status.web_client import WebClient from ahriman.core.upload.http_upload import HttpUpload +from ahriman.core.upload.upload import Upload from ahriman.models.package import Package +from ahriman.models.repository_id import RepositoryId -class RemoteService(HttpUpload): +class RemoteService(Upload, HttpUpload): """ upload files to another server instance @@ -38,16 +40,17 @@ class RemoteService(HttpUpload): client(WebClient): web client instance """ - def __init__(self, architecture: str, configuration: Configuration, section: str) -> None: + def __init__(self, repository_id: RepositoryId, configuration: Configuration, section: str) -> None: """ default constructor Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance section(str): settings section name """ - HttpUpload.__init__(self, architecture, configuration, section) + Upload.__init__(self, repository_id, configuration) + HttpUpload.__init__(self, configuration, section) self.client = WebClient(configuration) @cached_property diff --git a/src/ahriman/core/upload/rsync.py b/src/ahriman/core/upload/rsync.py index fd5c66ea..8f5db29d 100644 --- a/src/ahriman/core/upload/rsync.py +++ b/src/ahriman/core/upload/rsync.py @@ -23,6 +23,7 @@ from ahriman.core.configuration import Configuration from ahriman.core.upload.upload import Upload from ahriman.core.util import check_output from ahriman.models.package import Package +from ahriman.models.repository_id import RepositoryId class Rsync(Upload): @@ -36,16 +37,16 @@ class Rsync(Upload): _check_output = check_output - def __init__(self, architecture: str, configuration: Configuration, section: str) -> None: + def __init__(self, repository_id: RepositoryId, configuration: Configuration, section: str) -> None: """ default constructor Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance section(str): settings section name """ - Upload.__init__(self, architecture, configuration) + Upload.__init__(self, repository_id, configuration) self.command = configuration.getlist(section, "command") self.remote = configuration.get(section, "remote") diff --git a/src/ahriman/core/upload/s3.py b/src/ahriman/core/upload/s3.py index dbf3bc4b..c7931344 100644 --- a/src/ahriman/core/upload/s3.py +++ b/src/ahriman/core/upload/s3.py @@ -28,6 +28,7 @@ from ahriman.core.configuration import Configuration from ahriman.core.upload.upload import Upload from ahriman.core.util import walk from ahriman.models.package import Package +from ahriman.models.repository_id import RepositoryId class S3(Upload): @@ -37,21 +38,29 @@ class S3(Upload): Attributes bucket(Any): boto3 S3 bucket object chunk_size(int): chunk size for calculating checksums + object_path(Path): relative path to which packages will be uploaded """ - def __init__(self, architecture: str, configuration: Configuration, section: str) -> None: + def __init__(self, repository_id: RepositoryId, configuration: Configuration, section: str) -> None: """ default constructor Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance section(str): settings section name """ - Upload.__init__(self, architecture, configuration) + Upload.__init__(self, repository_id, configuration) self.bucket = self.get_bucket(configuration, section) self.chunk_size = configuration.getint(section, "chunk_size", fallback=8 * 1024 * 1024) + if (object_path := configuration.get(section, "object_path", fallback=None)) is not None: + # we need to avoid path conversion here, hence the string + self.object_path = Path(object_path) + else: + paths = configuration.repository_paths + self.object_path = paths.repository.relative_to(paths.root / "repository") + @staticmethod def calculate_etag(path: Path, chunk_size: int) -> str: """ @@ -127,7 +136,7 @@ class S3(Upload): continue local_path = path / local_file - remote_path = Path(self.architecture) / local_file + remote_path = self.object_path / local_file.name (mime, _) = mimetypes.guess_type(local_path) extra_args = {"ContentType": mime} if mime is not None else None @@ -155,8 +164,8 @@ class S3(Upload): Returns: dict[Path, Any]: map of path object to the remote s3 object """ - objects = self.bucket.objects.filter(Prefix=self.architecture) - return {Path(item.key).relative_to(self.architecture): item for item in objects} + objects = self.bucket.objects.filter(Prefix=str(self.object_path)) + return {Path(item.key).relative_to(self.object_path): item for item in objects} def sync(self, path: Path, built_packages: list[Package]) -> None: """ diff --git a/src/ahriman/core/upload/upload.py b/src/ahriman/core/upload/upload.py index 83df31ae..f12c6534 100644 --- a/src/ahriman/core/upload/upload.py +++ b/src/ahriman/core/upload/upload.py @@ -25,6 +25,7 @@ from ahriman.core.configuration import Configuration from ahriman.core.exceptions import SynchronizationError from ahriman.core.log import LazyLogging from ahriman.models.package import Package +from ahriman.models.repository_id import RepositoryId from ahriman.models.upload_settings import UploadSettings @@ -33,18 +34,16 @@ class Upload(LazyLogging): base remote sync class Attributes: - architecture(str): repository architecture configuration(Configuration): configuration instance + repository_id(RepositoryId): repository unique identifier Examples: These classes provide the way to upload packages to remote sources as it is described in their implementations. Basic flow includes class instantiating by using the ``load`` method and then calling the ``run`` method which wraps any internal exceptions into the ``SyncFailed`` exception:: - >>> from ahriman.core.configuration import Configuration - >>> >>> configuration = Configuration() - >>> upload = Upload.load("x86_64", configuration, "s3") + >>> upload = Upload.load(RepositoryId("x86_64", "aur-clone"), configuration, "s3") >>> upload.run(configuration.repository_paths.repository, []) Or in case if direct access to exception is required, the ``sync`` method can be used:: @@ -55,46 +54,46 @@ class Upload(LazyLogging): >>> handle_exceptions(ex) """ - def __init__(self, architecture: str, configuration: Configuration) -> None: + def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None: """ default constructor Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance """ - self.architecture = architecture + self.repository_id = repository_id self.configuration = configuration @staticmethod - def load(architecture: str, configuration: Configuration, target: str) -> Upload: + def load(repository_id: RepositoryId, configuration: Configuration, target: str) -> Upload: """ load client from settings Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance target(str): target to run sync (e.g. s3) Returns: Upload: client according to current settings """ - section, provider_name = configuration.gettype(target, architecture) + section, provider_name = configuration.gettype(target, repository_id) match UploadSettings.from_option(provider_name): case UploadSettings.Rsync: from ahriman.core.upload.rsync import Rsync - return Rsync(architecture, configuration, section) + return Rsync(repository_id, configuration, section) case UploadSettings.S3: from ahriman.core.upload.s3 import S3 - return S3(architecture, configuration, section) + return S3(repository_id, configuration, section) case UploadSettings.GitHub: - from ahriman.core.upload.github import Github - return Github(architecture, configuration, section) + from ahriman.core.upload.github import GitHub + return GitHub(repository_id, configuration, section) case UploadSettings.RemoteService: from ahriman.core.upload.remote_service import RemoteService - return RemoteService(architecture, configuration, section) + return RemoteService(repository_id, configuration, section) case _: - return Upload(architecture, configuration) # should never happen + return Upload(repository_id, configuration) # should never happen def run(self, path: Path, built_packages: list[Package]) -> None: """ diff --git a/src/ahriman/core/upload/upload_trigger.py b/src/ahriman/core/upload/upload_trigger.py index 0ccdfe15..8106d357 100644 --- a/src/ahriman/core/upload/upload_trigger.py +++ b/src/ahriman/core/upload/upload_trigger.py @@ -21,6 +21,7 @@ from ahriman.core.configuration import Configuration from ahriman.core.triggers import Trigger from ahriman.core.upload.upload import Upload from ahriman.models.package import Package +from ahriman.models.repository_id import RepositoryId from ahriman.models.result import Result @@ -56,7 +57,6 @@ class UploadTrigger(Trigger): }, "password": { "type": "string", - "required": True, }, "repository": { "type": "string", @@ -67,6 +67,10 @@ class UploadTrigger(Trigger): "coerce": "integer", "min": 0, }, + "use_full_release_name": { + "type": "boolean", + "coerce": "boolean", + }, "username": { "type": "string", }, @@ -126,6 +130,9 @@ class UploadTrigger(Trigger): "coerce": "integer", "min": 0, }, + "object_path": { + "type": "string", + }, "region": { "type": "string", "required": True, @@ -138,15 +145,15 @@ class UploadTrigger(Trigger): }, } - def __init__(self, architecture: str, configuration: Configuration) -> None: + def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None: """ default constructor Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance """ - Trigger.__init__(self, architecture, configuration) + Trigger.__init__(self, repository_id, configuration) self.targets = self.configuration_sections(configuration) @classmethod @@ -171,5 +178,5 @@ class UploadTrigger(Trigger): packages(list[Package]): list of all available packages """ for target in self.targets: - runner = Upload.load(self.architecture, self.configuration, target) + runner = Upload.load(self.repository_id, self.configuration, target) runner.run(self.configuration.repository_paths.repository, result.success) diff --git a/src/ahriman/models/aur_package.py b/src/ahriman/models/aur_package.py index 8b9e620e..789d93ad 100644 --- a/src/ahriman/models/aur_package.py +++ b/src/ahriman/models/aur_package.py @@ -68,9 +68,10 @@ class AURPackage: >>> >>> from ahriman.core.alpm.pacman import Pacman >>> from ahriman.core.configuration import Configuration + >>> from ahriman.models.repository_id import RepositoryId >>> >>> configuration = Configuration() - >>> pacman = Pacman("x86_64", configuration) + >>> pacman = Pacman(RepositoryId("x86_64", "aur-clone"), configuration) >>> metadata = pacman.package_get("pacman") >>> package = AURPackage.from_pacman(next(metadata)) # load package from pyalpm wrapper """ diff --git a/src/ahriman/models/package_description.py b/src/ahriman/models/package_description.py index 7154e217..bbb08c31 100644 --- a/src/ahriman/models/package_description.py +++ b/src/ahriman/models/package_description.py @@ -56,9 +56,10 @@ class PackageDescription: >>> from pathlib import Path >>> from ahriman.core.alpm.pacman import Pacman >>> from ahriman.core.configuration import Configuration + >>> from ahriman.models.repository_id import RepositoryId >>> >>> configuration = Configuration() - >>> pacman = Pacman("x86_64", configuration) + >>> pacman = Pacman(RepositoryId("x86_64", "aur-clone"), configuration) >>> pyalpm_description = next(package for package in pacman.package_get("pacman")) >>> description = PackageDescription.from_package( >>> pyalpm_description, Path("/var/cache/pacman/pkg/pacman-6.0.1-4-x86_64.pkg.tar.zst")) diff --git a/src/ahriman/models/repository_id.py b/src/ahriman/models/repository_id.py new file mode 100644 index 00000000..58dc272b --- /dev/null +++ b/src/ahriman/models/repository_id.py @@ -0,0 +1,73 @@ +# +# Copyright (c) 2021-2023 ahriman team. +# +# This file is part of ahriman +# (see https://github.com/arcan1s/ahriman). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +from dataclasses import dataclass +from typing import Any + + +@dataclass(frozen=True) +class RepositoryId: + """ + unique identifier of the repository + + Attributes: + architecture(str): repository architecture + name(str): repository name + """ + + architecture: str + name: str + + @property + def is_empty(self) -> bool: + """ + check if all data is supplied for the loading + + Returns: + bool: True in case if architecture or name are not set and False otherwise + """ + return not self.architecture or not self.name + + @property + def id(self) -> str: + """ + get repository id to be used for databases + + Returns: + str: unique id for this repository + """ + return f"{self.architecture}-{self.name}" # basically the same as used for command line + + def __lt__(self, other: Any) -> bool: + """ + comparison operator for sorting + + Args: + other(Any): other object to compare + + Returns: + bool: True in case if this is less than other and False otherwise + + Raises: + TypeError: if other is different from RepositoryId type + """ + if not isinstance(other, RepositoryId): + raise ValueError(f"'<' not supported between instances of '{type(self)}' and '{type(other)}'") + + return (self.name, self.architecture) < (other.name, other.architecture) diff --git a/src/ahriman/models/repository_paths.py b/src/ahriman/models/repository_paths.py index b7fbd4f1..866712cd 100644 --- a/src/ahriman/models/repository_paths.py +++ b/src/ahriman/models/repository_paths.py @@ -20,25 +20,29 @@ import os import shutil -from dataclasses import dataclass +from collections.abc import Generator +from dataclasses import dataclass, field +from functools import cached_property from pathlib import Path from ahriman.core.exceptions import PathError +from ahriman.core.log import LazyLogging +from ahriman.models.repository_id import RepositoryId @dataclass(frozen=True) -class RepositoryPaths: +class RepositoryPaths(LazyLogging): """ repository paths holder. For the most operations with paths you want to use this object Attributes: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier root(Path): repository root (i.e. ahriman home) Examples: This class can be used in order to access the repository tree structure:: - >>> paths = RepositoryPaths(Path("/var/lib/ahriman"), "x86_64") + >>> paths = RepositoryPaths(Path("/var/lib/ahriman"), RepositoryId("x86_64", "aur-clone")) Additional methods can be used in order to ensure that tree is created:: @@ -51,7 +55,33 @@ class RepositoryPaths: """ root: Path - architecture: str + repository_id: RepositoryId + _force_current_tree: bool = field(default=False, kw_only=True) + + @property + def _repository_root(self) -> Path: + """ + repository root which can be used for invalid (not fully loaded instances) + + Returns: + Path: root path to repositories + """ + return self.root / "repository" + + @cached_property + def _suffix(self) -> Path: + """ + suffix of the paths as defined by repository structure + + Returns: + Path: relative path which contains only architecture segment in case if legacy tree is used and repository + name and architecture otherwise + """ + if not self._force_current_tree: + if (self._repository_root / self.repository_id.architecture).is_dir(): + self.logger.warning("using legacy per architecture tree") + return Path(self.repository_id.architecture) # legacy tree suffix + return Path(self.repository_id.name) / self.repository_id.architecture @property def cache(self) -> Path: @@ -72,7 +102,7 @@ class RepositoryPaths: Path: full patch to devtools chroot directory """ # for the chroot directory devtools will create own tree, and we don"t have to specify architecture here - return self.root / "chroot" + return self.root / "chroot" / self.repository_id.name @property def packages(self) -> Path: @@ -82,7 +112,7 @@ class RepositoryPaths: Returns: Path: full path to built packages directory """ - return self.root / "packages" / self.architecture + return self.root / "packages" / self._suffix @property def pacman(self) -> Path: @@ -92,7 +122,7 @@ class RepositoryPaths: Returns: Path: full path to pacman local database cache """ - return self.root / "pacman" / self.architecture + return self.root / "pacman" / self._suffix @property def repository(self) -> Path: @@ -102,7 +132,7 @@ class RepositoryPaths: Returns: Path: full path to the repository directory """ - return self.root / "repository" / self.architecture + return self._repository_root / self._suffix @property def root_owner(self) -> tuple[int, int]: @@ -114,23 +144,57 @@ class RepositoryPaths: """ return self.owner(self.root) + # TODO see https://github.com/python/mypy/issues/12534, remove type: ignore after release + # pylint: disable=protected-access @classmethod - def known_architectures(cls, root: Path) -> set[str]: + def known_architectures(cls, root: Path, name: str = "") -> set[str]: # type: ignore[return] """ - get known architectures + get known architecture names + + Args: + root(Path): repository root + name(str, optional): repository name (Default value = "") + + Returns: + set[str]: list of repository architectures for which there is created tree + """ + def walk(repository_dir: Path) -> Generator[str, None, None]: + for architecture in filter(lambda path: path.is_dir(), repository_dir.iterdir()): + yield architecture.name + + instance = cls(root, RepositoryId("", "")) + match (instance._repository_root / name): + case full_tree if full_tree.is_dir(): + return set(walk(full_tree)) # actually works for legacy too in case if name is set to empty string + case _ if instance._repository_root.is_dir(): + return set(walk(instance._repository_root)) # legacy only tree + case _: + return set() # no tree detected at all + + # pylint: disable=protected-access + @classmethod + def known_repositories(cls, root: Path) -> set[str]: + """ + get known repository names Args: root(Path): repository root Returns: - set[str]: list of architectures for which tree is created + set[str]: list of repository names for which there is created tree. Returns empty set in case if repository + is loaded in legacy mode """ - paths = cls(root, "") - return { - path.name - for path in paths.repository.iterdir() - if path.is_dir() - } + # simply walk through the root. In case if there are subdirectories, emit the name + def walk(paths: RepositoryPaths) -> Generator[str, None, None]: + for repository in filter(lambda path: path.is_dir(), paths._repository_root.iterdir()): + if any(path.is_dir() for path in repository.iterdir()): + yield repository.name + + instance = cls(root, RepositoryId("", "")) + if not instance._repository_root.is_dir(): + return set() # no tree created + + return set(walk(instance)) @staticmethod def owner(path: Path) -> tuple[int, int]: @@ -197,11 +261,13 @@ class RepositoryPaths: """ create ahriman working tree """ + if self.repository_id.is_empty: + return # do not even try to create tree in case if no repository id set for directory in ( self.cache, self.chroot, self.packages, - self.pacman / "sync", # we need sync directory in order to be able to copy databases + self.pacman, self.repository, ): directory.mkdir(mode=0o755, parents=True, exist_ok=True) diff --git a/src/ahriman/web/views/v1/status/status.py b/src/ahriman/web/views/v1/status/status.py index c30dcc41..8bd4feef 100644 --- a/src/ahriman/web/views/v1/status/status.py +++ b/src/ahriman/web/views/v1/status/status.py @@ -65,9 +65,9 @@ class StatusView(BaseView): counters = Counters.from_packages(self.service.packages) status = InternalStatus( status=self.service.status, - architecture=self.service.architecture, + architecture=self.service.repository_id.architecture, packages=counters, - repository=self.service.repository.name, + repository=self.service.repository_id.name, version=__version__, ) diff --git a/src/ahriman/web/web.py b/src/ahriman/web/web.py index 040343dd..25d937a6 100644 --- a/src/ahriman/web/web.py +++ b/src/ahriman/web/web.py @@ -31,6 +31,7 @@ from ahriman.core.exceptions import InitializeError from ahriman.core.log.filtered_access_logger import FilteredAccessLogger from ahriman.core.spawn import Spawn from ahriman.core.status.watcher import Watcher +from ahriman.models.repository_id import RepositoryId from ahriman.web.apispec import setup_apispec from ahriman.web.cors import setup_cors from ahriman.web.middlewares.exception_handler import exception_handler @@ -120,12 +121,12 @@ def run_server(application: Application) -> None: access_log=logging.getLogger("http"), access_log_class=FilteredAccessLogger) -def setup_service(architecture: str, configuration: Configuration, spawner: Spawn) -> Application: +def setup_service(repository_id: RepositoryId, configuration: Configuration, spawner: Spawn) -> Application: """ create web application Args: - architecture(str): repository architecture + repository_id(RepositoryId): repository unique identifier configuration(Configuration): configuration instance spawner(Spawn): spawner thread @@ -155,7 +156,7 @@ def setup_service(architecture: str, configuration: Configuration, spawner: Spaw database = application["database"] = SQLite.load(configuration) application.logger.info("setup watcher") - application["watcher"] = Watcher(architecture, configuration, database) + application["watcher"] = Watcher(repository_id, configuration, database) application.logger.info("setup process spawner") application["spawn"] = spawner diff --git a/tests/ahriman/application/application/conftest.py b/tests/ahriman/application/application/conftest.py index dbaef770..9b607430 100644 --- a/tests/ahriman/application/application/conftest.py +++ b/tests/ahriman/application/application/conftest.py @@ -27,7 +27,8 @@ def application_packages(configuration: Configuration, database: SQLite, reposit """ mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) mocker.patch("ahriman.core.database.SQLite.load", return_value=database) - return ApplicationPackages("x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + return ApplicationPackages(repository_id, configuration, report=False) @pytest.fixture @@ -47,7 +48,8 @@ def application_properties(configuration: Configuration, database: SQLite, repos """ mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) mocker.patch("ahriman.core.database.SQLite.load", return_value=database) - return ApplicationProperties("x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + return ApplicationProperties(repository_id, configuration, report=False) @pytest.fixture @@ -67,4 +69,5 @@ def application_repository(configuration: Configuration, database: SQLite, repos """ mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) mocker.patch("ahriman.core.database.SQLite.load", return_value=database) - return ApplicationRepository("x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + return ApplicationRepository(repository_id, configuration, report=False) diff --git a/tests/ahriman/application/application/test_application_properties.py b/tests/ahriman/application/application/test_application_properties.py index 803c8a16..ab64cdcd 100644 --- a/tests/ahriman/application/application/test_application_properties.py +++ b/tests/ahriman/application/application/test_application_properties.py @@ -6,3 +6,10 @@ def test_create_tree(application_properties: ApplicationProperties) -> None: must have repository attribute """ assert application_properties.repository + + +def test_architecture(application_properties: ApplicationProperties) -> None: + """ + must return repository architecture + """ + assert application_properties.architecture == application_properties.repository_id.architecture diff --git a/tests/ahriman/application/conftest.py b/tests/ahriman/application/conftest.py index 56a82310..fa98c1c1 100644 --- a/tests/ahriman/application/conftest.py +++ b/tests/ahriman/application/conftest.py @@ -28,7 +28,8 @@ def application(configuration: Configuration, repository: Repository, database: """ mocker.patch("ahriman.core.database.SQLite.load", return_value=database) mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) - return Application("x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + return Application(repository_id, configuration, report=False) @pytest.fixture @@ -39,7 +40,8 @@ def args() -> argparse.Namespace: Returns: argparse.Namespace: command line arguments test instance """ - return argparse.Namespace(architecture=None, lock=None, force=False, unsafe=False, report=False, wait_timeout=-1) + return argparse.Namespace(architecture=None, lock=None, force=False, unsafe=False, report=False, + repository=None, repository_id=None, wait_timeout=-1) @pytest.fixture @@ -54,7 +56,8 @@ def lock(args: argparse.Namespace, configuration: Configuration) -> Lock: Returns: Lock: file lock test instance """ - return Lock(args, "x86_64", configuration) + _, repository_id = configuration.check_loaded() + return Lock(args, repository_id, configuration) @pytest.fixture diff --git a/tests/ahriman/application/handlers/test_handler.py b/tests/ahriman/application/handlers/test_handler.py index 645cb738..7f513af5 100644 --- a/tests/ahriman/application/handlers/test_handler.py +++ b/tests/ahriman/application/handlers/test_handler.py @@ -8,48 +8,7 @@ from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration from ahriman.core.exceptions import ExitCode, MissingArchitectureError, MultipleArchitecturesError from ahriman.models.log_handler import LogHandler - - -def test_architectures_extract(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: - """ - must generate list of available architectures - """ - args.configuration = configuration.path - known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures") - - Handler.architectures_extract(args) - known_architectures_mock.assert_called_once_with(configuration.getpath("repository", "root")) - - -def test_architectures_extract_empty(args: argparse.Namespace, configuration: Configuration, - mocker: MockerFixture) -> None: - """ - must raise exception if no available architectures found - """ - args.command = "config" - args.configuration = configuration.path - mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures", return_value=set()) - - with pytest.raises(MissingArchitectureError): - Handler.architectures_extract(args) - - -def test_architectures_extract_exception(args: argparse.Namespace, mocker: MockerFixture) -> None: - """ - must raise exception on missing architectures - """ - args.command = "config" - mocker.patch.object(Handler, "ALLOW_AUTO_ARCHITECTURE_RUN", False) - with pytest.raises(MissingArchitectureError): - Handler.architectures_extract(args) - - -def test_architectures_extract_specified(args: argparse.Namespace) -> None: - """ - must return architecture list if it has been specified - """ - architectures = args.architecture = ["i686", "x86_64"] - assert Handler.architectures_extract(args) == sorted(set(architectures)) +from ahriman.models.repository_id import RepositoryId def test_call(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: @@ -62,20 +21,21 @@ def test_call(args: argparse.Namespace, configuration: Configuration, mocker: Mo args.report = False mocker.patch("ahriman.application.handlers.Handler.run") configuration_mock = mocker.patch("ahriman.core.configuration.Configuration.from_path", return_value=configuration) - log_handler_mock = mocker.patch("ahriman.core.log.Log.handler", return_value=args.log_handler) - log_load_mock = mocker.patch("ahriman.core.log.Log.load") + log_handler_mock = mocker.patch("ahriman.core.log.log_loader.LogLoader.handler", return_value=args.log_handler) + log_load_mock = mocker.patch("ahriman.core.log.log_loader.LogLoader.load") enter_mock = mocker.patch("ahriman.application.lock.Lock.__enter__") exit_mock = mocker.patch("ahriman.application.lock.Lock.__exit__") - assert Handler.call(args, "x86_64") - configuration_mock.assert_called_once_with(args.configuration, "x86_64") + _, repository_id = configuration.check_loaded() + assert Handler.call(args, repository_id) + configuration_mock.assert_called_once_with(args.configuration, repository_id) log_handler_mock.assert_called_once_with(args.log_handler) log_load_mock.assert_called_once_with(configuration, args.log_handler, quiet=args.quiet, report=args.report) enter_mock.assert_called_once_with() exit_mock.assert_called_once_with(None, None, None) -def test_call_exception(args: argparse.Namespace, mocker: MockerFixture) -> None: +def test_call_exception(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: """ must process exception """ @@ -84,11 +44,12 @@ def test_call_exception(args: argparse.Namespace, mocker: MockerFixture) -> None 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") + _, repository_id = configuration.check_loaded() + assert not Handler.call(args, repository_id) logging_mock.assert_called_once_with(pytest.helpers.anyvar(str, strict=True)) -def test_call_exit_code(args: argparse.Namespace, mocker: MockerFixture) -> None: +def test_call_exit_code(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: """ must process exitcode exception """ @@ -97,7 +58,8 @@ def test_call_exit_code(args: argparse.Namespace, mocker: MockerFixture) -> None 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") + _, repository_id = configuration.check_loaded() + assert not Handler.call(args, repository_id) logging_mock.assert_not_called() @@ -105,33 +67,39 @@ def test_execute(args: argparse.Namespace, mocker: MockerFixture) -> None: """ must run execution in multiple processes """ - args.architecture = ["i686", "x86_64"] + ids = [ + RepositoryId("i686", "aur-clone"), + RepositoryId("x86_64", "aur-clone"), + ] + mocker.patch("ahriman.application.handlers.Handler.repositories_extract", return_value=ids) starmap_mock = mocker.patch("multiprocessing.pool.Pool.starmap") Handler.execute(args) - starmap_mock.assert_called_once_with(Handler.call, [(args, architecture) for architecture in args.architecture]) + starmap_mock.assert_called_once_with(Handler.call, [(args, repository_id) for repository_id in ids]) def test_execute_multiple_not_supported(args: argparse.Namespace, mocker: MockerFixture) -> None: """ must raise an exception if multiple architectures are not supported by the handler """ - args.architecture = ["i686", "x86_64"] args.command = "web" + mocker.patch("ahriman.application.handlers.Handler.repositories_extract", return_value=[ + RepositoryId("i686", "aur-clone"), + RepositoryId("x86_64", "aur-clone"), + ]) mocker.patch.object(Handler, "ALLOW_MULTI_ARCHITECTURE_RUN", False) with pytest.raises(MultipleArchitecturesError): Handler.execute(args) -def test_execute_single(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: +def test_execute_single(args: argparse.Namespace, 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) + mocker.patch("ahriman.application.handlers.Handler.repositories_extract", return_value=[ + RepositoryId("x86_64", "aur-clone"), + ]) starmap_mock = mocker.patch("multiprocessing.pool.Pool.starmap") Handler.execute(args) @@ -142,8 +110,117 @@ def test_run(args: argparse.Namespace, configuration: Configuration) -> None: """ must raise NotImplemented for missing method """ + _, repository_id = configuration.check_loaded() with pytest.raises(NotImplementedError): - Handler.run(args, "x86_64", configuration, report=True) + Handler.run(args, repository_id, configuration, report=True) + + +def test_repositories_extract(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: + """ + must generate list of available repositories based on flags + """ + args.architecture = ["arch"] + args.configuration = configuration.path + args.repository = ["repo"] + known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures") + known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories") + + assert Handler.repositories_extract(args) == [RepositoryId("arch", "repo")] + known_architectures_mock.assert_not_called() + known_repositories_mock.assert_not_called() + + +def test_repositories_extract_repository(args: argparse.Namespace, configuration: Configuration, + mocker: MockerFixture) -> None: + """ + must generate list of available repositories based on flags and tree + """ + args.architecture = ["arch"] + args.configuration = configuration.path + known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures") + known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories", + return_value={"repo"}) + + assert Handler.repositories_extract(args) == [RepositoryId("arch", "repo")] + known_architectures_mock.assert_not_called() + known_repositories_mock.assert_called_once_with(configuration.repository_paths.root) + + +def test_repositories_extract_repository_legacy(args: argparse.Namespace, configuration: Configuration, + mocker: MockerFixture) -> None: + """ + must generate list of available repositories based on flags and tree + """ + args.architecture = ["arch"] + args.configuration = configuration.path + known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures") + known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories", + return_value=set()) + + assert Handler.repositories_extract(args) == [RepositoryId("arch", "aur-clone")] + known_architectures_mock.assert_not_called() + known_repositories_mock.assert_called_once_with(configuration.repository_paths.root) + + +def test_repositories_extract_architecture(args: argparse.Namespace, configuration: Configuration, + mocker: MockerFixture) -> None: + """ + must read repository name from config + """ + args.configuration = configuration.path + args.repository = ["repo"] + known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures", + return_value={"arch"}) + known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories") + + assert Handler.repositories_extract(args) == [RepositoryId("arch", "repo")] + known_architectures_mock.assert_called_once_with(configuration.repository_paths.root, "repo") + known_repositories_mock.assert_not_called() + + +def test_repositories_extract_empty(args: argparse.Namespace, configuration: Configuration, + mocker: MockerFixture) -> None: + """ + must raise exception if no available architectures found + """ + args.command = "config" + args.configuration = configuration.path + mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures", return_value=set()) + mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories", return_value=set()) + + with pytest.raises(MissingArchitectureError): + Handler.repositories_extract(args) + + +def test_repositories_extract_systemd(args: argparse.Namespace, configuration: Configuration, + mocker: MockerFixture) -> None: + """ + must extract repository list for systemd units + """ + args.configuration = configuration.path + args.repository_id = "i686/some/repo/name" + known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures") + known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories") + + assert Handler.repositories_extract(args) == [RepositoryId("i686", "some-repo-name")] + known_architectures_mock.assert_not_called() + known_repositories_mock.assert_not_called() + + +def test_repositories_extract_systemd_legacy(args: argparse.Namespace, configuration: Configuration, + mocker: MockerFixture) -> None: + """ + must extract repository list for systemd units in legacy format + """ + args.configuration = configuration.path + args.repository_id = "i686" + known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures") + known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories", + return_value=set()) + + assert Handler.repositories_extract(args) == [RepositoryId("i686", "aur-clone")] + known_architectures_mock.assert_not_called() + known_repositories_mock.assert_called_once_with(configuration.repository_paths.root) def test_check_if_empty() -> None: diff --git a/tests/ahriman/application/handlers/test_handler_add.py b/tests/ahriman/application/handlers/test_handler_add.py index b9939bbb..8cd5177e 100644 --- a/tests/ahriman/application/handlers/test_handler_add.py +++ b/tests/ahriman/application/handlers/test_handler_add.py @@ -44,7 +44,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository: dependencies_mock = mocker.patch("ahriman.application.application.Application.with_dependencies") on_start_mock = mocker.patch("ahriman.application.application.Application.on_start") - Add.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Add.run(args, repository_id, configuration, report=False) application_mock.assert_called_once_with(args.package, args.source, args.username) dependencies_mock.assert_not_called() on_start_mock.assert_called_once_with() @@ -68,7 +69,8 @@ def test_run_with_updates(args: argparse.Namespace, configuration: Configuration return_value=[package_ahriman]) print_mock = mocker.patch("ahriman.application.application.Application.print_updates") - Add.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Add.run(args, repository_id, configuration, report=False) updates_mock.assert_called_once_with(args.package, aur=False, local=False, manual=True, vcs=False) application_mock.assert_called_once_with([package_ahriman], Packagers(args.username, {package_ahriman.base: "packager"}), @@ -94,5 +96,6 @@ def test_run_empty_exception(args: argparse.Namespace, configuration: Configurat mocker.patch("ahriman.application.application.Application.print_updates") check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") - Add.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Add.run(args, repository_id, configuration, report=False) check_mock.assert_called_once_with(True, True) diff --git a/tests/ahriman/application/handlers/test_handler_backup.py b/tests/ahriman/application/handlers/test_handler_backup.py index 3679dcd7..a1349694 100644 --- a/tests/ahriman/application/handlers/test_handler_backup.py +++ b/tests/ahriman/application/handlers/test_handler_backup.py @@ -33,7 +33,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc add_mock = tarfile.__enter__.return_value = MagicMock() mocker.patch("tarfile.TarFile.__new__", return_value=tarfile) - Backup.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Backup.run(args, repository_id, configuration, report=False) add_mock.add.assert_called_once_with(Path("path")) @@ -55,8 +56,8 @@ def test_get_paths(configuration: Configuration, mocker: MockerFixture) -> None: assert all(path.exists() for path in paths if path.name not in (".gnupg", "cache")) -def test_disallow_auto_architecture_run() -> None: +def test_disallow_multi_architecture_run() -> None: """ must not allow multi architecture run """ - assert not Backup.ALLOW_AUTO_ARCHITECTURE_RUN + assert not Backup.ALLOW_MULTI_ARCHITECTURE_RUN diff --git a/tests/ahriman/application/handlers/test_handler_clean.py b/tests/ahriman/application/handlers/test_handler_clean.py index 35fa0252..196c4138 100644 --- a/tests/ahriman/application/handlers/test_handler_clean.py +++ b/tests/ahriman/application/handlers/test_handler_clean.py @@ -35,6 +35,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository: application_mock = mocker.patch("ahriman.application.application.Application.clean") on_start_mock = mocker.patch("ahriman.application.application.Application.on_start") - Clean.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Clean.run(args, repository_id, configuration, report=False) application_mock.assert_called_once_with(cache=False, chroot=False, manual=False, packages=False, pacman=False) on_start_mock.assert_called_once_with() diff --git a/tests/ahriman/application/handlers/test_handler_daemon.py b/tests/ahriman/application/handlers/test_handler_daemon.py index 698c3758..4018e3a1 100644 --- a/tests/ahriman/application/handlers/test_handler_daemon.py +++ b/tests/ahriman/application/handlers/test_handler_daemon.py @@ -33,7 +33,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc start_mock = mocker.patch("threading.Timer.start") join_mock = mocker.patch("threading.Timer.join") - Daemon.run(args, "x86_64", configuration, report=True) - run_mock.assert_called_once_with(args, "x86_64", configuration, report=True) + _, repository_id = configuration.check_loaded() + Daemon.run(args, repository_id, configuration, report=True) + run_mock.assert_called_once_with(args, repository_id, configuration, report=True) start_mock.assert_called_once_with() join_mock.assert_called_once_with() diff --git a/tests/ahriman/application/handlers/test_handler_dump.py b/tests/ahriman/application/handlers/test_handler_dump.py index 2f5ebc37..8816baa8 100644 --- a/tests/ahriman/application/handlers/test_handler_dump.py +++ b/tests/ahriman/application/handlers/test_handler_dump.py @@ -29,13 +29,14 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc application_mock = mocker.patch("ahriman.core.configuration.Configuration.dump", return_value=configuration.dump()) - Dump.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Dump.run(args, repository_id, configuration, report=False) application_mock.assert_called_once_with() print_mock.assert_called() -def test_disallow_auto_architecture_run() -> None: +def test_disallow_multi_architecture_run() -> None: """ must not allow multi architecture run """ - assert not Dump.ALLOW_AUTO_ARCHITECTURE_RUN + assert not Dump.ALLOW_MULTI_ARCHITECTURE_RUN diff --git a/tests/ahriman/application/handlers/test_handler_help.py b/tests/ahriman/application/handlers/test_handler_help.py index db986c0f..6e8f13cd 100644 --- a/tests/ahriman/application/handlers/test_handler_help.py +++ b/tests/ahriman/application/handlers/test_handler_help.py @@ -29,7 +29,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc args = _default_args(args) parse_mock = mocker.patch("argparse.ArgumentParser.parse_args") - Help.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Help.run(args, repository_id, configuration, report=False) parse_mock.assert_called_once_with(["--help"]) @@ -41,12 +42,13 @@ def test_run_command(args: argparse.Namespace, configuration: Configuration, moc args.command = "aur-search" parse_mock = mocker.patch("argparse.ArgumentParser.parse_args") - Help.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Help.run(args, repository_id, configuration, report=False) parse_mock.assert_called_once_with(["aur-search", "--help"]) -def test_disallow_auto_architecture_run() -> None: +def test_disallow_multi_architecture_run() -> None: """ must not allow multi architecture run """ - assert not Help.ALLOW_AUTO_ARCHITECTURE_RUN + assert not Help.ALLOW_MULTI_ARCHITECTURE_RUN diff --git a/tests/ahriman/application/handlers/test_handler_key_import.py b/tests/ahriman/application/handlers/test_handler_key_import.py index 33d2f0fb..dc8e9fd3 100644 --- a/tests/ahriman/application/handlers/test_handler_key_import.py +++ b/tests/ahriman/application/handlers/test_handler_key_import.py @@ -31,12 +31,13 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository: mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) application_mock = mocker.patch("ahriman.core.sign.gpg.GPG.key_import") - KeyImport.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + KeyImport.run(args, repository_id, configuration, report=False) application_mock.assert_called_once_with(args.key_server, args.key) -def test_disallow_auto_architecture_run() -> None: +def test_disallow_multi_architecture_run() -> None: """ must not allow multi architecture run """ - assert not KeyImport.ALLOW_AUTO_ARCHITECTURE_RUN + assert not KeyImport.ALLOW_MULTI_ARCHITECTURE_RUN diff --git a/tests/ahriman/application/handlers/test_handler_patch.py b/tests/ahriman/application/handlers/test_handler_patch.py index 0c504ff8..42e3ee50 100644 --- a/tests/ahriman/application/handlers/test_handler_patch.py +++ b/tests/ahriman/application/handlers/test_handler_patch.py @@ -44,8 +44,9 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository: return_value=(args.package, PkgbuildPatch(None, "patch"))) application_mock = mocker.patch("ahriman.application.handlers.Patch.patch_set_create") - Patch.run(args, "x86_64", configuration, report=False) - patch_mock.assert_called_once_with(args.package, "x86_64", args.track) + _, repository_id = configuration.check_loaded() + Patch.run(args, repository_id, configuration, report=False) + patch_mock.assert_called_once_with(args.package, repository_id.architecture, args.track) application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package, PkgbuildPatch(None, "patch")) @@ -63,7 +64,8 @@ def test_run_function(args: argparse.Namespace, configuration: Configuration, re patch_mock = mocker.patch("ahriman.application.handlers.Patch.patch_create_from_function", return_value=patch) application_mock = mocker.patch("ahriman.application.handlers.Patch.patch_set_create") - Patch.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Patch.run(args, repository_id, configuration, report=False) patch_mock.assert_called_once_with(args.variable, args.patch) application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package, patch) @@ -79,7 +81,8 @@ def test_run_list(args: argparse.Namespace, configuration: Configuration, reposi mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) application_mock = mocker.patch("ahriman.application.handlers.Patch.patch_set_list") - Patch.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Patch.run(args, repository_id, configuration, report=False) application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package, ["version"], False) @@ -94,11 +97,12 @@ def test_run_remove(args: argparse.Namespace, configuration: Configuration, repo mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) application_mock = mocker.patch("ahriman.application.handlers.Patch.patch_set_remove") - Patch.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Patch.run(args, repository_id, configuration, report=False) application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package, ["version"]) -def test_patch_create_from_diff(package_ahriman: Package, mocker: MockerFixture) -> None: +def test_patch_create_from_diff(package_ahriman: Package, configuration: Configuration, mocker: MockerFixture) -> None: """ must create patch from directory tree diff """ @@ -108,8 +112,9 @@ def test_patch_create_from_diff(package_ahriman: Package, mocker: MockerFixture) package_mock = mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman) sources_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_create", return_value=patch.value) - assert Patch.patch_create_from_diff(path, "x86_64", ["*.diff"]) == (package_ahriman.base, patch) - package_mock.assert_called_once_with(path, "x86_64", None) + _, repository_id = configuration.check_loaded() + assert Patch.patch_create_from_diff(path, repository_id.architecture, ["*.diff"]) == (package_ahriman.base, patch) + package_mock.assert_called_once_with(path, repository_id.architecture, None) sources_mock.assert_called_once_with(path, "*.diff") @@ -185,3 +190,10 @@ def test_patch_set_remove(application: Application, package_ahriman: Package, mo remove_mock = mocker.patch("ahriman.core.database.SQLite.patches_remove") Patch.patch_set_remove(application, package_ahriman.base, ["version"]) remove_mock.assert_called_once_with(package_ahriman.base, ["version"]) + + +def test_disallow_multi_architecture_run() -> None: + """ + must not allow multi architecture run + """ + assert not Patch.ALLOW_MULTI_ARCHITECTURE_RUN diff --git a/tests/ahriman/application/handlers/test_handler_rebuild.py b/tests/ahriman/application/handlers/test_handler_rebuild.py index 1fd3604a..9fff56a3 100644 --- a/tests/ahriman/application/handlers/test_handler_rebuild.py +++ b/tests/ahriman/application/handlers/test_handler_rebuild.py @@ -23,7 +23,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace: Returns: argparse.Namespace: generated arguments for these test cases """ - args.depends_on = [] + args.depends_on = None args.dry_run = False args.from_database = False args.exit_code = False @@ -49,7 +49,8 @@ def test_run(args: argparse.Namespace, package_ahriman: Package, configuration: check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") on_start_mock = mocker.patch("ahriman.application.application.Application.on_start") - Rebuild.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Rebuild.run(args, repository_id, configuration, report=False) extract_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.status, from_database=args.from_database) application_packages_mock.assert_called_once_with([package_ahriman], None) application_mock.assert_called_once_with([package_ahriman], args.username, bump_pkgrel=args.increment) @@ -70,7 +71,8 @@ def test_run_extract_packages(args: argparse.Namespace, configuration: Configura mocker.patch("ahriman.application.application.Application.print_updates") extract_mock = mocker.patch("ahriman.application.handlers.Rebuild.extract_packages", return_value=[]) - Rebuild.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Rebuild.run(args, repository_id, configuration, report=False) extract_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.status, from_database=args.from_database) @@ -87,7 +89,8 @@ def test_run_dry_run(args: argparse.Namespace, configuration: Configuration, rep check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") print_mock = mocker.patch("ahriman.application.application.Application.print_updates") - Rebuild.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Rebuild.run(args, repository_id, configuration, report=False) application_mock.assert_not_called() check_mock.assert_called_once_with(False, False) print_mock.assert_called_once_with([package_ahriman], log_fn=pytest.helpers.anyvar(int)) @@ -105,7 +108,8 @@ def test_run_filter(args: argparse.Namespace, configuration: Configuration, repo mocker.patch("ahriman.application.handlers.Rebuild.extract_packages", return_value=[]) application_packages_mock = mocker.patch("ahriman.core.repository.repository.Repository.packages_depend_on") - Rebuild.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Rebuild.run(args, repository_id, configuration, report=False) application_packages_mock.assert_called_once_with([], ["python-aur"]) @@ -120,7 +124,8 @@ def test_run_without_filter(args: argparse.Namespace, configuration: Configurati mocker.patch("ahriman.application.handlers.Rebuild.extract_packages", return_value=[]) application_packages_mock = mocker.patch("ahriman.core.repository.repository.Repository.packages_depend_on") - Rebuild.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Rebuild.run(args, repository_id, configuration, report=False) application_packages_mock.assert_called_once_with([], None) @@ -138,7 +143,8 @@ def test_run_update_empty_exception(args: argparse.Namespace, configuration: Con mocker.patch("ahriman.application.application.Application.print_updates") check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") - Rebuild.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Rebuild.run(args, repository_id, configuration, report=False) check_mock.assert_called_once_with(True, True) @@ -155,7 +161,8 @@ def test_run_build_empty_exception(args: argparse.Namespace, configuration: Conf mocker.patch("ahriman.application.application.Application.update", return_value=Result()) check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") - Rebuild.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Rebuild.run(args, repository_id, configuration, report=False) check_mock.assert_has_calls([MockCall(True, False), MockCall(True, True)]) diff --git a/tests/ahriman/application/handlers/test_handler_remove.py b/tests/ahriman/application/handlers/test_handler_remove.py index 77b6acb4..fba69bed 100644 --- a/tests/ahriman/application/handlers/test_handler_remove.py +++ b/tests/ahriman/application/handlers/test_handler_remove.py @@ -31,6 +31,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository: application_mock = mocker.patch("ahriman.application.application.Application.remove") on_start_mock = mocker.patch("ahriman.application.application.Application.on_start") - Remove.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Remove.run(args, repository_id, configuration, report=False) application_mock.assert_called_once_with([]) on_start_mock.assert_called_once_with() diff --git a/tests/ahriman/application/handlers/test_handler_remove_unknown.py b/tests/ahriman/application/handlers/test_handler_remove_unknown.py index 00adf667..d0f25819 100644 --- a/tests/ahriman/application/handlers/test_handler_remove_unknown.py +++ b/tests/ahriman/application/handlers/test_handler_remove_unknown.py @@ -34,7 +34,8 @@ def test_run(args: argparse.Namespace, package_ahriman: Package, configuration: remove_mock = mocker.patch("ahriman.application.application.Application.remove") on_start_mock = mocker.patch("ahriman.application.application.Application.on_start") - RemoveUnknown.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + RemoveUnknown.run(args, repository_id, configuration, report=False) application_mock.assert_called_once_with() remove_mock.assert_called_once_with([package_ahriman]) on_start_mock.assert_called_once_with() @@ -53,7 +54,8 @@ def test_run_dry_run(args: argparse.Namespace, configuration: Configuration, rep remove_mock = mocker.patch("ahriman.application.application.Application.remove") print_mock = mocker.patch("ahriman.core.formatters.Printer.print") - RemoveUnknown.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + RemoveUnknown.run(args, repository_id, configuration, report=False) application_mock.assert_called_once_with() remove_mock.assert_not_called() print_mock.assert_called_once_with(verbose=False) diff --git a/tests/ahriman/application/handlers/test_handler_restore.py b/tests/ahriman/application/handlers/test_handler_restore.py index 243fa0fa..3b2053cf 100644 --- a/tests/ahriman/application/handlers/test_handler_restore.py +++ b/tests/ahriman/application/handlers/test_handler_restore.py @@ -32,12 +32,13 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc extract_mock = tarfile.__enter__.return_value = MagicMock() mocker.patch("tarfile.TarFile.__new__", return_value=tarfile) - Restore.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Restore.run(args, repository_id, configuration, report=False) extract_mock.extractall.assert_called_once_with(path=args.output) -def test_disallow_auto_architecture_run() -> None: +def test_disallow_multi_architecture_run() -> None: """ must not allow multi architecture run """ - assert not Restore.ALLOW_AUTO_ARCHITECTURE_RUN + assert not Restore.ALLOW_MULTI_ARCHITECTURE_RUN diff --git a/tests/ahriman/application/handlers/test_handler_search.py b/tests/ahriman/application/handlers/test_handler_search.py index 8d7f6131..f2c08181 100644 --- a/tests/ahriman/application/handlers/test_handler_search.py +++ b/tests/ahriman/application/handlers/test_handler_search.py @@ -42,7 +42,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository: check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") print_mock = mocker.patch("ahriman.core.formatters.Printer.print") - Search.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Search.run(args, repository_id, configuration, report=False) aur_search_mock.assert_called_once_with("ahriman", pacman=pytest.helpers.anyvar(int)) official_search_mock.assert_called_once_with("ahriman", pacman=pytest.helpers.anyvar(int)) check_mock.assert_called_once_with(False, False) @@ -62,7 +63,8 @@ def test_run_empty_exception(args: argparse.Namespace, configuration: Configurat mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") - Search.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Search.run(args, repository_id, configuration, report=False) check_mock.assert_called_once_with(True, True) @@ -77,7 +79,8 @@ def test_run_sort(args: argparse.Namespace, configuration: Configuration, reposi mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) sort_mock = mocker.patch("ahriman.application.handlers.Search.sort") - Search.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Search.run(args, repository_id, configuration, report=False) sort_mock.assert_has_calls([ MockCall([], "name"), MockCall().__iter__(), MockCall([aur_package_ahriman], "name"), MockCall().__iter__() @@ -96,7 +99,8 @@ def test_run_sort_by(args: argparse.Namespace, configuration: Configuration, rep mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) sort_mock = mocker.patch("ahriman.application.handlers.Search.sort") - Search.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Search.run(args, repository_id, configuration, report=False) sort_mock.assert_has_calls([ MockCall([], "field"), MockCall().__iter__(), MockCall([aur_package_ahriman], "field"), MockCall().__iter__() @@ -124,11 +128,11 @@ def test_sort_exception(aur_package_ahriman: AURPackage) -> None: Search.sort([aur_package_ahriman], "random_field") -def test_disallow_auto_architecture_run() -> None: +def test_disallow_multi_architecture_run() -> None: """ must not allow multi architecture run """ - assert not Search.ALLOW_AUTO_ARCHITECTURE_RUN + assert not Search.ALLOW_MULTI_ARCHITECTURE_RUN def test_sort_fields(aur_package_ahriman: AURPackage) -> None: diff --git a/tests/ahriman/application/handlers/test_handler_service_updates.py b/tests/ahriman/application/handlers/test_handler_service_updates.py index f09f6caf..0f04128a 100644 --- a/tests/ahriman/application/handlers/test_handler_service_updates.py +++ b/tests/ahriman/application/handlers/test_handler_service_updates.py @@ -35,7 +35,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository: application_mock = mocker.patch("ahriman.core.formatters.Printer.print") check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") - ServiceUpdates.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + ServiceUpdates.run(args, repository_id, configuration, report=False) package_mock.assert_called_once_with(package_ahriman.base, repository.pacman, None) application_mock.assert_called_once_with(verbose=True, separator=" -> ") check_mock.assert_called_once_with(args.exit_code, True) @@ -53,6 +54,14 @@ def test_run_skip(args: argparse.Namespace, configuration: Configuration, reposi application_mock = mocker.patch("ahriman.core.formatters.Printer.print") check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") - ServiceUpdates.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + ServiceUpdates.run(args, repository_id, configuration, report=False) application_mock.assert_not_called() check_mock.assert_not_called() + + +def test_disallow_multi_architecture_run() -> None: + """ + must not allow multi architecture run + """ + assert not ServiceUpdates.ALLOW_MULTI_ARCHITECTURE_RUN diff --git a/tests/ahriman/application/handlers/test_handler_setup.py b/tests/ahriman/application/handlers/test_handler_setup.py index cec5bc03..6f09a548 100644 --- a/tests/ahriman/application/handlers/test_handler_setup.py +++ b/tests/ahriman/application/handlers/test_handler_setup.py @@ -8,7 +8,9 @@ from unittest.mock import call as MockCall from ahriman.application.handlers import Setup from ahriman.core.configuration import Configuration +from ahriman.core.exceptions import MissingArchitectureError from ahriman.core.repository import Repository +from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.sign_settings import SignSettings @@ -23,15 +25,15 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace: Returns: argparse.Namespace: generated arguments for these test cases """ + args.architecture = ["x86_64"] args.build_as_user = "ahriman" - args.build_command = "ahriman" args.from_configuration = Path("/usr/share/devtools/pacman.conf.d/extra.conf") args.generate_salt = True args.makeflags_jobs = True args.mirror = "mirror" args.multilib = True args.packager = "John Doe " - args.repository = "aur-clone" + args.repository = ["aur-clone"] args.server = None args.sign_key = "key" args.sign_target = [SignSettings.Packages] @@ -54,17 +56,36 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository: executable_mock = mocker.patch("ahriman.application.handlers.Setup.executable_create") init_mock = mocker.patch("ahriman.core.alpm.repo.Repo.init") - Setup.run(args, "x86_64", configuration, report=False) - ahriman_configuration_mock.assert_called_once_with(args, "x86_64", args.repository, configuration) + _, repository_id = configuration.check_loaded() + Setup.run(args, repository_id, configuration, report=False) + ahriman_configuration_mock.assert_called_once_with(args, repository_id, configuration) devtools_configuration_mock.assert_called_once_with( - args.build_command, "x86_64", args.from_configuration, args.mirror, args.multilib, args.repository, - f"file://{repository_paths.repository}") + repository_id, args.from_configuration, args.mirror, args.multilib, f"file://{repository_paths.repository}") makepkg_configuration_mock.assert_called_once_with(args.packager, args.makeflags_jobs, repository_paths) - sudo_configuration_mock.assert_called_once_with(repository_paths, args.build_command, "x86_64") - executable_mock.assert_called_once_with(repository_paths, args.build_command, "x86_64") + sudo_configuration_mock.assert_called_once_with(repository_paths, repository_id) + executable_mock.assert_called_once_with(repository_paths, repository_id) init_mock.assert_called_once_with() +def test_run_no_architecture_or_repository(configuration: Configuration) -> None: + """ + must raise MissingArchitectureError if either architecture or repository are not supplied + """ + _, repository_id = configuration.check_loaded() + + args = argparse.Namespace(architecture=None, command="service-setup", repository=None) + with pytest.raises(MissingArchitectureError): + Setup.run(args, repository_id, configuration, report=False) + + args = argparse.Namespace(architecture=[repository_id.architecture], command="service-setup", repository=None) + with pytest.raises(MissingArchitectureError): + Setup.run(args, repository_id, configuration, report=False) + + args = argparse.Namespace(architecture=None, command="service-setup", repository=[repository_id.name]) + with pytest.raises(MissingArchitectureError): + Setup.run(args, repository_id, configuration, report=False) + + def test_run_with_server(args: argparse.Namespace, configuration: Configuration, repository: Repository, mocker: MockerFixture) -> None: """ @@ -80,21 +101,20 @@ def test_run_with_server(args: argparse.Namespace, configuration: Configuration, mocker.patch("ahriman.core.alpm.repo.Repo.init") devtools_configuration_mock = mocker.patch("ahriman.application.handlers.Setup.configuration_create_devtools") - Setup.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Setup.run(args, repository_id, configuration, report=False) devtools_configuration_mock.assert_called_once_with( - args.build_command, "x86_64", args.from_configuration, args.mirror, args.multilib, args.repository, - "server") + repository_id, args.from_configuration, args.mirror, args.multilib, "server") -def test_build_command(args: argparse.Namespace) -> None: +def test_build_command(repository_id: RepositoryId) -> None: """ must generate correct build command name """ - args = _default_args(args) path = Path("local") - build_command = Setup.build_command(path, args.build_command, "x86_64") - assert build_command.name == f"{args.build_command}-x86_64-build" + build_command = Setup.build_command(path, repository_id) + assert build_command.name == f"{repository_id.name}-{repository_id.architecture}-build" assert build_command.parent == path @@ -107,19 +127,26 @@ def test_configuration_create_ahriman(args: argparse.Namespace, configuration: C mocker.patch("pathlib.Path.open") set_option_mock = mocker.patch("ahriman.core.configuration.Configuration.set_option") write_mock = mocker.patch("ahriman.core.configuration.Configuration.write") - command = Setup.build_command(repository_paths.root, args.build_command, "x86_64") + _, repository_id = configuration.check_loaded() + command = Setup.build_command(repository_paths.root, repository_id) - Setup.configuration_create_ahriman(args, "x86_64", args.repository, configuration) + Setup.configuration_create_ahriman(args, repository_id, configuration) set_option_mock.assert_has_calls([ - MockCall(Configuration.section_name("build", "x86_64"), "build_command", str(command)), - MockCall("repository", "name", args.repository), - MockCall(Configuration.section_name("build", "x86_64"), "makechrootpkg_flags", f"-U {args.build_as_user}"), - MockCall(Configuration.section_name("alpm", "x86_64"), "mirror", args.mirror), - MockCall(Configuration.section_name("sign", "x86_64"), "target", + MockCall(Configuration.section_name("build", repository_id.name, repository_id.architecture), "build_command", + str(command)), + MockCall("repository", "name", repository_id.name), + MockCall(Configuration.section_name("build", repository_id.name, repository_id.architecture), + "makechrootpkg_flags", f"-U {args.build_as_user}"), + MockCall(Configuration.section_name( + "alpm", repository_id.name, repository_id.architecture), "mirror", args.mirror), + MockCall(Configuration.section_name("sign", repository_id.name, repository_id.architecture), "target", " ".join([target.name.lower() for target in args.sign_target])), - MockCall(Configuration.section_name("sign", "x86_64"), "key", args.sign_key), - MockCall(Configuration.section_name("web", "x86_64"), "port", str(args.web_port)), - MockCall(Configuration.section_name("web", "x86_64"), "unix_socket", str(args.web_unix_socket)), + MockCall(Configuration.section_name("sign", repository_id.name, repository_id.architecture), "key", + args.sign_key), + MockCall(Configuration.section_name("web", repository_id.name, repository_id.architecture), "port", + str(args.web_port)), + MockCall(Configuration.section_name("web", repository_id.name, repository_id.architecture), "unix_socket", + str(args.web_unix_socket)), MockCall("auth", "salt", pytest.helpers.anyvar(str, strict=True)), ]) write_mock.assert_called_once_with(pytest.helpers.anyvar(int)) @@ -136,13 +163,16 @@ def test_configuration_create_ahriman_no_multilib(args: argparse.Namespace, conf mocker.patch("ahriman.core.configuration.Configuration.write") set_option_mock = mocker.patch("ahriman.core.configuration.Configuration.set_option") - Setup.configuration_create_ahriman(args, "x86_64", args.repository, configuration) + _, repository_id = configuration.check_loaded() + Setup.configuration_create_ahriman(args, repository_id, configuration) set_option_mock.assert_has_calls([ - MockCall(Configuration.section_name("alpm", "x86_64"), "mirror", args.mirror), + MockCall(Configuration.section_name("alpm", repository_id.name, repository_id.architecture), "mirror", + args.mirror), ]) # non-strict check called intentionally -def test_configuration_create_devtools(args: argparse.Namespace, mocker: MockerFixture) -> None: +def test_configuration_create_devtools(args: argparse.Namespace, configuration: Configuration, + mocker: MockerFixture) -> None: """ must create configuration for the devtools """ @@ -152,13 +182,14 @@ def test_configuration_create_devtools(args: argparse.Namespace, mocker: MockerF add_section_mock = mocker.patch("ahriman.core.configuration.Configuration.add_section") write_mock = mocker.patch("ahriman.core.configuration.Configuration.write") - Setup.configuration_create_devtools(args.build_command, "x86_64", args.from_configuration, - None, args.multilib, args.repository, "server") - add_section_mock.assert_has_calls([MockCall("multilib"), MockCall(args.repository)]) + _, repository_id = configuration.check_loaded() + Setup.configuration_create_devtools(repository_id, args.from_configuration, None, args.multilib, "server") + add_section_mock.assert_has_calls([MockCall("multilib"), MockCall(repository_id.name)]) write_mock.assert_called_once_with(pytest.helpers.anyvar(int)) -def test_configuration_create_devtools_mirror(args: argparse.Namespace, mocker: MockerFixture) -> None: +def test_configuration_create_devtools_mirror(args: argparse.Namespace, configuration: Configuration, + mocker: MockerFixture) -> None: """ must create configuration for the devtools with mirror set explicitly """ @@ -176,14 +207,15 @@ def test_configuration_create_devtools_mirror(args: argparse.Namespace, mocker: remove_option_mock = mocker.patch("ahriman.core.configuration.Configuration.remove_option") set_option_mock = mocker.patch("ahriman.core.configuration.Configuration.set_option") - Setup.configuration_create_devtools(args.build_command, "x86_64", args.from_configuration, - args.mirror, False, args.repository, "server") + _, repository_id = configuration.check_loaded() + Setup.configuration_create_devtools(repository_id, args.from_configuration, args.mirror, args.multilib, "server") get_mock.assert_has_calls([MockCall("core", "Include", fallback=None), MockCall("extra", "Include", fallback=None)]) remove_option_mock.assert_called_once_with("core", "Include") set_option_mock.assert_has_calls([MockCall("core", "Server", args.mirror)]) # non-strict check called intentionally -def test_configuration_create_devtools_no_multilib(args: argparse.Namespace, mocker: MockerFixture) -> None: +def test_configuration_create_devtools_no_multilib(args: argparse.Namespace, configuration: Configuration, + mocker: MockerFixture) -> None: """ must create configuration for the devtools without multilib """ @@ -192,8 +224,8 @@ def test_configuration_create_devtools_no_multilib(args: argparse.Namespace, moc mocker.patch("ahriman.core.configuration.Configuration.set") write_mock = mocker.patch("ahriman.core.configuration.Configuration.write") - Setup.configuration_create_devtools(args.build_command, "x86_64", args.from_configuration, - None, False, args.repository, "server") + _, repository_id = configuration.check_loaded() + Setup.configuration_create_devtools(repository_id, args.from_configuration, args.mirror, False, "server") write_mock.assert_called_once_with(pytest.helpers.anyvar(int)) @@ -211,37 +243,38 @@ def test_configuration_create_makepkg(args: argparse.Namespace, repository_paths Path("home") / ".makepkg.conf", pytest.helpers.anyvar(str, True), encoding="utf8") -def test_configuration_create_sudo(args: argparse.Namespace, repository_paths: RepositoryPaths, +def test_configuration_create_sudo(configuration: Configuration, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: """ must create sudo configuration """ - args = _default_args(args) chmod_text_mock = mocker.patch("pathlib.Path.chmod") write_text_mock = mocker.patch("pathlib.Path.write_text") - Setup.configuration_create_sudo(repository_paths, args.build_command, "x86_64") + _, repository_id = configuration.check_loaded() + Setup.configuration_create_sudo(repository_paths, repository_id) chmod_text_mock.assert_called_once_with(0o400) write_text_mock.assert_called_once_with(pytest.helpers.anyvar(str, True), encoding="utf8") -def test_executable_create(args: argparse.Namespace, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: +def test_executable_create(configuration: Configuration, repository_paths: RepositoryPaths, + mocker: MockerFixture) -> None: """ must create executable """ - args = _default_args(args) chown_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.chown") symlink_mock = mocker.patch("pathlib.Path.symlink_to") unlink_mock = mocker.patch("pathlib.Path.unlink") - Setup.executable_create(repository_paths, args.build_command, "x86_64") - chown_mock.assert_called_once_with(Setup.build_command(repository_paths.root, args.build_command, "x86_64")) + _, repository_id = configuration.check_loaded() + Setup.executable_create(repository_paths, repository_id) + chown_mock.assert_called_once_with(Setup.build_command(repository_paths.root, repository_id)) symlink_mock.assert_called_once_with(Setup.ARCHBUILD_COMMAND_PATH) unlink_mock.assert_called_once_with(missing_ok=True) -def test_disallow_auto_architecture_run() -> None: +def test_disallow_multi_architecture_run() -> None: """ must not allow multi architecture run """ - assert not Setup.ALLOW_AUTO_ARCHITECTURE_RUN + assert not Setup.ALLOW_MULTI_ARCHITECTURE_RUN diff --git a/tests/ahriman/application/handlers/test_handler_shell.py b/tests/ahriman/application/handlers/test_handler_shell.py index 5a6e32c6..d5104afb 100644 --- a/tests/ahriman/application/handlers/test_handler_shell.py +++ b/tests/ahriman/application/handlers/test_handler_shell.py @@ -32,7 +32,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository: mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) application_mock = mocker.patch("code.interact") - Shell.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Shell.run(args, repository_id, configuration, report=False) application_mock.assert_called_once_with(local=pytest.helpers.anyvar(int)) @@ -46,7 +47,8 @@ def test_run_eval(args: argparse.Namespace, configuration: Configuration, reposi mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) application_mock = mocker.patch("code.InteractiveConsole.runcode") - Shell.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Shell.run(args, repository_id, configuration, report=False) application_mock.assert_called_once_with(args.code) @@ -62,7 +64,15 @@ def test_run_verbose(args: argparse.Namespace, configuration: Configuration, rep print_mock = mocker.patch("ahriman.core.formatters.Printer.print") application_mock = mocker.patch("code.interact") - Shell.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Shell.run(args, repository_id, configuration, report=False) application_mock.assert_called_once_with(local=pytest.helpers.anyvar(int)) read_mock.assert_called_once_with(encoding="utf8") print_mock.assert_called_once_with(verbose=False) + + +def test_disallow_multi_architecture_run() -> None: + """ + must not allow multi architecture run + """ + assert not Shell.ALLOW_MULTI_ARCHITECTURE_RUN diff --git a/tests/ahriman/application/handlers/test_handler_sign.py b/tests/ahriman/application/handlers/test_handler_sign.py index c40d3492..9e1fa579 100644 --- a/tests/ahriman/application/handlers/test_handler_sign.py +++ b/tests/ahriman/application/handlers/test_handler_sign.py @@ -30,5 +30,6 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository: mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) application_mock = mocker.patch("ahriman.application.application.Application.sign") - Sign.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Sign.run(args, repository_id, configuration, report=False) application_mock.assert_called_once_with([]) diff --git a/tests/ahriman/application/handlers/test_handler_status.py b/tests/ahriman/application/handlers/test_handler_status.py index 4c2bf40e..3988aa0b 100644 --- a/tests/ahriman/application/handlers/test_handler_status.py +++ b/tests/ahriman/application/handlers/test_handler_status.py @@ -43,7 +43,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository: check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") print_mock = mocker.patch("ahriman.core.formatters.Printer.print") - Status.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Status.run(args, repository_id, configuration, report=False) application_mock.assert_called_once_with() packages_mock.assert_called_once_with(None) check_mock.assert_called_once_with(False, False) @@ -62,7 +63,8 @@ def test_run_empty_exception(args: argparse.Namespace, configuration: Configurat mocker.patch("ahriman.core.status.client.Client.package_get", return_value=[]) check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") - Status.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Status.run(args, repository_id, configuration, report=False) check_mock.assert_called_once_with(True, True) @@ -78,7 +80,8 @@ def test_run_verbose(args: argparse.Namespace, configuration: Configuration, rep return_value=[(package_ahriman, BuildStatus(BuildStatusEnum.Success))]) print_mock = mocker.patch("ahriman.core.formatters.Printer.print") - Status.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Status.run(args, repository_id, configuration, report=False) print_mock.assert_has_calls([MockCall(verbose=True) for _ in range(2)]) @@ -93,7 +96,8 @@ def test_run_with_package_filter(args: argparse.Namespace, configuration: Config packages_mock = mocker.patch("ahriman.core.status.client.Client.package_get", return_value=[(package_ahriman, BuildStatus(BuildStatusEnum.Success))]) - Status.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Status.run(args, repository_id, configuration, report=False) packages_mock.assert_called_once_with(package_ahriman.base) @@ -110,7 +114,8 @@ def test_run_by_status(args: argparse.Namespace, configuration: Configuration, r mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) print_mock = mocker.patch("ahriman.core.formatters.Printer.print") - Status.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Status.run(args, repository_id, configuration, report=False) print_mock.assert_has_calls([MockCall(verbose=False) for _ in range(2)]) @@ -123,12 +128,13 @@ def test_imply_with_report(args: argparse.Namespace, configuration: Configuratio mocker.patch("ahriman.core.database.SQLite.load", return_value=database) load_mock = mocker.patch("ahriman.core.repository.Repository.load") - Status.run(args, "x86_64", configuration, report=False) - load_mock.assert_called_once_with("x86_64", configuration, database, report=True, refresh_pacman_database=0) + _, repository_id = configuration.check_loaded() + Status.run(args, repository_id, configuration, report=False) + load_mock.assert_called_once_with(repository_id, configuration, database, report=True, refresh_pacman_database=0) -def test_disallow_auto_architecture_run() -> None: +def test_disallow_multi_architecture_run() -> None: """ must not allow multi architecture run """ - assert not Status.ALLOW_AUTO_ARCHITECTURE_RUN + assert not Status.ALLOW_MULTI_ARCHITECTURE_RUN diff --git a/tests/ahriman/application/handlers/test_handler_status_update.py b/tests/ahriman/application/handlers/test_handler_status_update.py index 5e307115..427f1ca6 100644 --- a/tests/ahriman/application/handlers/test_handler_status_update.py +++ b/tests/ahriman/application/handlers/test_handler_status_update.py @@ -36,7 +36,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository: mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) update_self_mock = mocker.patch("ahriman.core.status.client.Client.status_update") - StatusUpdate.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + StatusUpdate.run(args, repository_id, configuration, report=False) update_self_mock.assert_called_once_with(args.status) @@ -50,7 +51,8 @@ def test_run_packages(args: argparse.Namespace, configuration: Configuration, re mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) update_mock = mocker.patch("ahriman.core.status.client.Client.package_update") - StatusUpdate.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + StatusUpdate.run(args, repository_id, configuration, report=False) update_mock.assert_called_once_with(package_ahriman.base, args.status) @@ -65,7 +67,8 @@ def test_run_remove(args: argparse.Namespace, configuration: Configuration, repo mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) update_mock = mocker.patch("ahriman.core.status.client.Client.package_remove") - StatusUpdate.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + StatusUpdate.run(args, repository_id, configuration, report=False) update_mock.assert_called_once_with(package_ahriman.base) @@ -78,12 +81,13 @@ def test_imply_with_report(args: argparse.Namespace, configuration: Configuratio mocker.patch("ahriman.core.database.SQLite.load", return_value=database) load_mock = mocker.patch("ahriman.core.repository.Repository.load") - StatusUpdate.run(args, "x86_64", configuration, report=False) - load_mock.assert_called_once_with("x86_64", configuration, database, report=True, refresh_pacman_database=0) + _, repository_id = configuration.check_loaded() + StatusUpdate.run(args, repository_id, configuration, report=False) + load_mock.assert_called_once_with(repository_id, configuration, database, report=True, refresh_pacman_database=0) -def test_disallow_auto_architecture_run() -> None: +def test_disallow_multi_architecture_run() -> None: """ must not allow multi architecture run """ - assert not StatusUpdate.ALLOW_AUTO_ARCHITECTURE_RUN + assert not StatusUpdate.ALLOW_MULTI_ARCHITECTURE_RUN diff --git a/tests/ahriman/application/handlers/test_handler_structure.py b/tests/ahriman/application/handlers/test_handler_structure.py index 49c66a16..bde85b0c 100644 --- a/tests/ahriman/application/handlers/test_handler_structure.py +++ b/tests/ahriman/application/handlers/test_handler_structure.py @@ -35,7 +35,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository: application_mock = mocker.patch("ahriman.core.tree.Tree.resolve", return_value=[[package_ahriman]]) print_mock = mocker.patch("ahriman.core.formatters.Printer.print") - Structure.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Structure.run(args, repository_id, configuration, report=False) packages_mock.assert_called_once_with([package_ahriman], count=args.partitions) application_mock.assert_called_once_with([package_ahriman]) print_mock.assert_has_calls([ @@ -45,8 +46,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository: ]) -def test_disallow_auto_architecture_run() -> None: +def test_disallow_multi_architecture_run() -> None: """ must not allow multi architecture run """ - assert not Structure.ALLOW_AUTO_ARCHITECTURE_RUN + assert not Structure.ALLOW_MULTI_ARCHITECTURE_RUN diff --git a/tests/ahriman/application/handlers/test_handler_tree_migrate.py b/tests/ahriman/application/handlers/test_handler_tree_migrate.py new file mode 100644 index 00000000..60b731d2 --- /dev/null +++ b/tests/ahriman/application/handlers/test_handler_tree_migrate.py @@ -0,0 +1,42 @@ +import argparse + +from pathlib import Path +from pytest_mock import MockerFixture +from unittest.mock import call as MockCall + +from ahriman.application.handlers import TreeMigrate +from ahriman.core.configuration import Configuration +from ahriman.models.repository_id import RepositoryId +from ahriman.models.repository_paths import RepositoryPaths + + +def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: + """ + must run command + """ + tree_create_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") + application_mock = mocker.patch("ahriman.application.handlers.TreeMigrate.tree_move") + _, repository_id = configuration.check_loaded() + old_paths = configuration.repository_paths + new_paths = RepositoryPaths(old_paths.root, old_paths.repository_id, _force_current_tree=True) + + TreeMigrate.run(args, repository_id, configuration, report=False) + tree_create_mock.assert_called_once_with() + application_mock.assert_called_once_with(old_paths, new_paths) + + +def test_move_tree(mocker: MockerFixture) -> None: + """ + must move tree + """ + rename_mock = mocker.patch("pathlib.Path.rename", autospec=True) + root = Path("local") + from_paths = RepositoryPaths(root, RepositoryId("arch", "")) + to_paths = RepositoryPaths(root, RepositoryId("arch", "repo")) + + TreeMigrate.tree_move(from_paths, to_paths) + rename_mock.assert_has_calls([ + MockCall(from_paths.packages, to_paths.packages), + MockCall(from_paths.pacman, to_paths.pacman), + MockCall(from_paths.repository, to_paths.repository), + ]) diff --git a/tests/ahriman/application/handlers/test_handler_triggers.py b/tests/ahriman/application/handlers/test_handler_triggers.py index d433d1e5..fcc4220d 100644 --- a/tests/ahriman/application/handlers/test_handler_triggers.py +++ b/tests/ahriman/application/handlers/test_handler_triggers.py @@ -33,7 +33,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository: application_mock = mocker.patch("ahriman.application.application.Application.on_result") on_start_mock = mocker.patch("ahriman.application.application.Application.on_start") - Triggers.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Triggers.run(args, repository_id, configuration, report=False) application_mock.assert_called_once_with(Result()) on_start_mock.assert_called_once_with() @@ -50,6 +51,7 @@ def test_run_trigger(args: argparse.Namespace, configuration: Configuration, rep report_mock = mocker.patch("ahriman.core.report.ReportTrigger.on_result") upload_mock = mocker.patch("ahriman.core.upload.UploadTrigger.on_result") - Triggers.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Triggers.run(args, repository_id, configuration, report=False) report_mock.assert_called_once_with(Result(), [package_ahriman]) upload_mock.assert_not_called() diff --git a/tests/ahriman/application/handlers/test_handler_unsafe_commands.py b/tests/ahriman/application/handlers/test_handler_unsafe_commands.py index 238133ba..01fcc5d9 100644 --- a/tests/ahriman/application/handlers/test_handler_unsafe_commands.py +++ b/tests/ahriman/application/handlers/test_handler_unsafe_commands.py @@ -32,7 +32,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc return_value=["command"]) print_mock = mocker.patch("ahriman.core.formatters.Printer.print") - UnsafeCommands.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + UnsafeCommands.run(args, repository_id, configuration, report=False) commands_mock.assert_called_once_with(pytest.helpers.anyvar(int)) print_mock.assert_called_once_with(verbose=True) @@ -47,7 +48,8 @@ def test_run_check(args: argparse.Namespace, configuration: Configuration, mocke return_value=["command"]) check_mock = mocker.patch("ahriman.application.handlers.UnsafeCommands.check_unsafe") - UnsafeCommands.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + UnsafeCommands.run(args, repository_id, configuration, report=False) commands_mock.assert_called_once_with(pytest.helpers.anyvar(int)) check_mock.assert_called_once_with(["clean"], ["command"], pytest.helpers.anyvar(int)) @@ -81,8 +83,8 @@ def test_get_unsafe_commands() -> None: assert subparser.choices[command].get_default("unsafe") -def test_disallow_auto_architecture_run() -> None: +def test_disallow_multi_architecture_run() -> None: """ must not allow multi architecture run """ - assert not UnsafeCommands.ALLOW_AUTO_ARCHITECTURE_RUN + assert not UnsafeCommands.ALLOW_MULTI_ARCHITECTURE_RUN diff --git a/tests/ahriman/application/handlers/test_handler_update.py b/tests/ahriman/application/handlers/test_handler_update.py index e41761d5..40f61fdf 100644 --- a/tests/ahriman/application/handlers/test_handler_update.py +++ b/tests/ahriman/application/handlers/test_handler_update.py @@ -54,7 +54,8 @@ def test_run(args: argparse.Namespace, package_ahriman: Package, configuration: on_start_mock = mocker.patch("ahriman.application.application.Application.on_start") print_mock = mocker.patch("ahriman.application.application.Application.print_updates") - Update.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Update.run(args, repository_id, configuration, report=False) application_mock.assert_called_once_with([package_ahriman], Packagers(args.username, {package_ahriman.base: "packager"}), bump_pkgrel=args.increment) @@ -77,7 +78,8 @@ def test_run_empty_exception(args: argparse.Namespace, configuration: Configurat mocker.patch("ahriman.application.application.Application.updates", return_value=[]) check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") - Update.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Update.run(args, repository_id, configuration, report=False) check_mock.assert_called_once_with(True, True) @@ -95,7 +97,8 @@ def test_run_update_empty_exception(args: argparse.Namespace, package_ahriman: P mocker.patch("ahriman.application.application.Application.print_updates") check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") - Update.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Update.run(args, repository_id, configuration, report=False) check_mock.assert_has_calls([MockCall(True, False), MockCall(True, True)]) @@ -111,7 +114,8 @@ def test_run_dry_run(args: argparse.Namespace, configuration: Configuration, rep check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") updates_mock = mocker.patch("ahriman.application.application.Application.updates") - Update.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Update.run(args, repository_id, configuration, report=False) updates_mock.assert_called_once_with(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs) application_mock.assert_not_called() check_mock.assert_called_once_with(False, pytest.helpers.anyvar(int)) diff --git a/tests/ahriman/application/handlers/test_handler_users.py b/tests/ahriman/application/handlers/test_handler_users.py index 2c050c0c..82ac16dc 100644 --- a/tests/ahriman/application/handlers/test_handler_users.py +++ b/tests/ahriman/application/handlers/test_handler_users.py @@ -47,7 +47,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, database: S create_user_mock = mocker.patch("ahriman.application.handlers.Users.user_create", return_value=user) update_mock = mocker.patch("ahriman.core.database.SQLite.user_update") - Users.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Users.run(args, repository_id, configuration, report=False) create_user_mock.assert_called_once_with(args) update_mock.assert_called_once_with(user) @@ -64,7 +65,8 @@ def test_run_empty_salt(args: argparse.Namespace, configuration: Configuration, create_user_mock = mocker.patch("ahriman.application.handlers.Users.user_create", return_value=user) update_mock = mocker.patch("ahriman.core.database.SQLite.user_update") - Users.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Users.run(args, repository_id, configuration, report=False) create_user_mock.assert_called_once_with(args) update_mock.assert_called_once_with(user) @@ -83,7 +85,8 @@ def test_run_empty_salt_without_password(args: argparse.Namespace, configuration create_user_mock = mocker.patch("ahriman.application.handlers.Users.user_create", return_value=user) update_mock = mocker.patch("ahriman.core.database.SQLite.user_update") - Users.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Users.run(args, repository_id, configuration, report=False) create_user_mock.assert_called_once_with(args) update_mock.assert_called_once_with(user) @@ -99,7 +102,8 @@ def test_run_list(args: argparse.Namespace, configuration: Configuration, databa check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") list_mock = mocker.patch("ahriman.core.database.SQLite.user_list", return_value=[user]) - Users.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Users.run(args, repository_id, configuration, report=False) list_mock.assert_called_once_with("user", args.role) check_mock.assert_called_once_with(False, False) @@ -116,7 +120,8 @@ def test_run_empty_exception(args: argparse.Namespace, configuration: Configurat mocker.patch("ahriman.core.database.SQLite.user_list", return_value=[]) check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") - Users.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Users.run(args, repository_id, configuration, report=False) check_mock.assert_called_once_with(True, True) @@ -130,7 +135,8 @@ def test_run_remove(args: argparse.Namespace, configuration: Configuration, data mocker.patch("ahriman.core.database.SQLite.load", return_value=database) remove_mock = mocker.patch("ahriman.core.database.SQLite.user_remove") - Users.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Users.run(args, repository_id, configuration, report=False) remove_mock.assert_called_once_with(args.username) @@ -169,8 +175,8 @@ def test_user_create_getpass_exception(args: argparse.Namespace, mocker: MockerF Users.user_create(args) -def test_disallow_auto_architecture_run() -> None: +def test_disallow_multi_architecture_run() -> None: """ must not allow multi architecture run """ - assert not Users.ALLOW_AUTO_ARCHITECTURE_RUN + assert not Users.ALLOW_MULTI_ARCHITECTURE_RUN diff --git a/tests/ahriman/application/handlers/test_handler_validate.py b/tests/ahriman/application/handlers/test_handler_validate.py index 05a95de6..ad89cea6 100644 --- a/tests/ahriman/application/handlers/test_handler_validate.py +++ b/tests/ahriman/application/handlers/test_handler_validate.py @@ -33,8 +33,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc print_mock = mocker.patch("ahriman.core.formatters.Printer.print") application_mock = mocker.patch("ahriman.core.configuration.validator.Validator.validate", return_value=False) - Validate.run(args, "x86_64", configuration, report=False) - + _, repository_id = configuration.check_loaded() + Validate.run(args, repository_id, configuration, report=False) application_mock.assert_called_once_with(configuration.dump()) print_mock.assert_called_once_with(verbose=True) @@ -47,7 +47,8 @@ def test_run_skip(args: argparse.Namespace, configuration: Configuration, mocker mocker.patch("ahriman.core.configuration.validator.Validator.validate", return_value=True) print_mock = mocker.patch("ahriman.core.formatters.Printer.print") - Validate.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Validate.run(args, repository_id, configuration, report=False) print_mock.assert_not_called() @@ -55,7 +56,8 @@ def test_schema(configuration: Configuration) -> None: """ must generate full schema correctly """ - schema = Validate.schema("x86_64", configuration) + _, repository_id = configuration.check_loaded() + schema = Validate.schema(repository_id, configuration) # defaults assert schema.pop("console") @@ -86,7 +88,9 @@ def test_schema_invalid_trigger(configuration: Configuration) -> None: """ configuration.set_option("build", "triggers", "some.invalid.trigger.path.Trigger") configuration.remove_option("build", "triggers_known") - assert Validate.schema("x86_64", configuration) == CONFIGURATION_SCHEMA + _, repository_id = configuration.check_loaded() + + assert Validate.schema(repository_id, configuration) == CONFIGURATION_SCHEMA def test_schema_erase_required() -> None: @@ -111,8 +115,8 @@ def test_schema_merge() -> None: assert key in merged["gitremote"]["schema"] -def test_disallow_auto_architecture_run() -> None: +def test_disallow_multi_architecture_run() -> None: """ must not allow multi architecture run """ - assert not Validate.ALLOW_AUTO_ARCHITECTURE_RUN + assert not Validate.ALLOW_MULTI_ARCHITECTURE_RUN diff --git a/tests/ahriman/application/handlers/test_handler_versions.py b/tests/ahriman/application/handlers/test_handler_versions.py index 47cf725e..25d37dbe 100644 --- a/tests/ahriman/application/handlers/test_handler_versions.py +++ b/tests/ahriman/application/handlers/test_handler_versions.py @@ -14,7 +14,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc application_mock = mocker.patch("ahriman.application.handlers.Versions.package_dependencies") print_mock = mocker.patch("ahriman.core.formatters.Printer.print") - Versions.run(args, "x86_64", configuration, report=False) + _, repository_id = configuration.check_loaded() + Versions.run(args, repository_id, configuration, report=False) application_mock.assert_called_once_with("ahriman") print_mock.assert_has_calls([MockCall(verbose=False, separator=" "), MockCall(verbose=False, separator=" ")]) @@ -36,3 +37,10 @@ def test_package_dependencies_missing() -> None: assert packages assert packages.get("pyalpm") is not None assert packages.get("Sphinx") is None + + +def test_disallow_multi_architecture_run() -> None: + """ + must not allow multi architecture run + """ + assert not Versions.ALLOW_MULTI_ARCHITECTURE_RUN diff --git a/tests/ahriman/application/handlers/test_handler_web.py b/tests/ahriman/application/handlers/test_handler_web.py index 59ace2d1..3090d375 100644 --- a/tests/ahriman/application/handlers/test_handler_web.py +++ b/tests/ahriman/application/handlers/test_handler_web.py @@ -41,8 +41,9 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository: stop_mock = mocker.patch("ahriman.core.spawn.Spawn.stop") join_mock = mocker.patch("ahriman.core.spawn.Spawn.join") - Web.run(args, "x86_64", configuration, report=False) - setup_mock.assert_called_once_with("x86_64", configuration, pytest.helpers.anyvar(int)) + _, repository_id = configuration.check_loaded() + Web.run(args, repository_id, configuration, report=False) + setup_mock.assert_called_once_with(repository_id, configuration, pytest.helpers.anyvar(int)) run_mock.assert_called_once_with(pytest.helpers.anyvar(int)) start_mock.assert_called_once_with() stop_mock.assert_called_once_with() @@ -53,33 +54,35 @@ def test_extract_arguments(args: argparse.Namespace, configuration: Configuratio """ must extract correct args """ + _, repository_id = configuration.check_loaded() expected = [ - "--architecture", "x86_64", + "--architecture", repository_id.architecture, + "--repository", repository_id.name, "--configuration", str(configuration.path), ] probe = _default_args(args) - assert list(Web.extract_arguments(probe, "x86_64", configuration)) == expected + assert list(Web.extract_arguments(probe, repository_id, configuration)) == expected probe.force = True expected.extend(["--force"]) - assert list(Web.extract_arguments(probe, "x86_64", configuration)) == expected + assert list(Web.extract_arguments(probe, repository_id, configuration)) == expected probe.log_handler = LogHandler.Console expected.extend(["--log-handler", probe.log_handler.value]) - assert list(Web.extract_arguments(probe, "x86_64", configuration)) == expected + assert list(Web.extract_arguments(probe, repository_id, configuration)) == expected probe.quiet = True expected.extend(["--quiet"]) - assert list(Web.extract_arguments(probe, "x86_64", configuration)) == expected + assert list(Web.extract_arguments(probe, repository_id, configuration)) == expected probe.unsafe = True expected.extend(["--unsafe"]) - assert list(Web.extract_arguments(probe, "x86_64", configuration)) == expected + assert list(Web.extract_arguments(probe, repository_id, configuration)) == expected configuration.set_option("web", "wait_timeout", "60") expected.extend(["--wait-timeout", "60"]) - assert list(Web.extract_arguments(probe, "x86_64", configuration)) == expected + assert list(Web.extract_arguments(probe, repository_id, configuration)) == expected def test_extract_arguments_full(parser: argparse.ArgumentParser, configuration: Configuration): @@ -101,8 +104,10 @@ def test_extract_arguments_full(parser: argparse.ArgumentParser, configuration: value = action.type(value) setattr(args, action.dest, value) - assert list(Web.extract_arguments(args, "x86_64", configuration)) == [ - "--architecture", "x86_64", + _, repository_id = configuration.check_loaded() + assert list(Web.extract_arguments(args, repository_id, configuration)) == [ + "--architecture", repository_id.architecture, + "--repository", repository_id.name, "--configuration", str(configuration.path), "--force", "--log-handler", "console", @@ -111,13 +116,6 @@ def test_extract_arguments_full(parser: argparse.ArgumentParser, configuration: ] -def test_disallow_auto_architecture_run() -> None: - """ - must not allow auto architecture run - """ - assert not Web.ALLOW_AUTO_ARCHITECTURE_RUN - - def test_disallow_multi_architecture_run() -> None: """ must not allow multi architecture run diff --git a/tests/ahriman/application/test_ahriman.py b/tests/ahriman/application/test_ahriman.py index d3aa0868..ac966d56 100644 --- a/tests/ahriman/application/test_ahriman.py +++ b/tests/ahriman/application/test_ahriman.py @@ -6,6 +6,7 @@ from pytest_mock import MockerFixture from ahriman.application import ahriman from ahriman.application.handlers import Handler +from ahriman.core.configuration import Configuration from ahriman.models.action import Action from ahriman.models.build_status import BuildStatusEnum from ahriman.models.log_handler import LogHandler @@ -17,16 +18,16 @@ def test_parser(parser: argparse.ArgumentParser) -> None: """ must parse valid command line """ - parser.parse_args(["-a", "x86_64", "service-config"]) + parser.parse_args(["-a", "x86_64", "-r", "repo", "service-config"]) def test_parser_option_configuration(parser: argparse.ArgumentParser) -> None: """ must convert configuration option to Path instance """ - args = parser.parse_args(["-a", "x86_64", "service-config"]) + args = parser.parse_args(["-a", "x86_64", "-r", "repo", "service-config"]) assert isinstance(args.configuration, Path) - args = parser.parse_args(["-a", "x86_64", "-c", "ahriman.ini", "service-config"]) + args = parser.parse_args(["-a", "x86_64", "-c", "ahriman.ini", "-r", "repo", "service-config"]) assert isinstance(args.configuration, Path) @@ -58,7 +59,15 @@ def test_parser_option_wait_timeout(parser: argparse.ArgumentParser) -> None: assert isinstance(args.wait_timeout, int) -def test_multiple_architectures(parser: argparse.ArgumentParser) -> None: +def test_parser_option_architecture_empty(parser: argparse.ArgumentParser) -> None: + """ + must parse empty architecture list as None + """ + args = parser.parse_args(["service-config"]) + assert args.architecture is None + + +def test_parser_option_architecture_multiple(parser: argparse.ArgumentParser) -> None: """ must accept multiple architectures """ @@ -66,19 +75,36 @@ def test_multiple_architectures(parser: argparse.ArgumentParser) -> None: assert args.architecture == ["x86_64", "i686"] +def test_parser_option_repository_empty(parser: argparse.ArgumentParser) -> None: + """ + must parse empty repository list as None + """ + args = parser.parse_args(["service-config"]) + assert args.repository is None + + +def test_parser_option_repository_multiple(parser: argparse.ArgumentParser) -> None: + """ + must accept multiple architectures + """ + args = parser.parse_args(["-r", "repo1", "-r", "repo2", "service-config"]) + assert args.repository == ["repo1", "repo2"] + + def test_subparsers_aur_search(parser: argparse.ArgumentParser) -> None: """ - aur-search command must imply architecture list, lock, report, quiet and unsafe + aur-search command must imply architecture list, lock, quiet, report, repository and unsafe """ args = parser.parse_args(["aur-search", "ahriman"]) assert args.architecture == [""] assert args.lock is None - assert not args.report assert args.quiet + assert not args.report + assert args.repository == [""] assert args.unsafe -def test_subparsers_aur_search_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_aur_search_option_architecture(parser: argparse.ArgumentParser) -> None: """ aur-search command must correctly parse architecture list """ @@ -86,20 +112,29 @@ def test_subparsers_aur_search_architecture(parser: argparse.ArgumentParser) -> assert args.architecture == [""] +def test_subparsers_aur_search_option_repository(parser: argparse.ArgumentParser) -> None: + """ + aur-search command must correctly parse repository list + """ + args = parser.parse_args(["-r", "repo", "aur-search", "ahriman"]) + assert args.repository == [""] + + def test_subparsers_help(parser: argparse.ArgumentParser) -> None: """ - help command must imply architecture list, lock, report, quiet, unsafe and parser + help command must imply architecture list, lock, quiet, report, repository, unsafe and parser """ args = parser.parse_args(["help"]) assert args.architecture == [""] assert args.lock is None - assert not args.report assert args.quiet + assert not args.report + assert args.repository == [""] assert args.unsafe assert args.parser is not None and args.parser() -def test_subparsers_help_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_help_option_architecture(parser: argparse.ArgumentParser) -> None: """ help command must correctly parse architecture list """ @@ -107,20 +142,29 @@ def test_subparsers_help_architecture(parser: argparse.ArgumentParser) -> None: assert args.architecture == [""] +def test_subparsers_help_option_repository(parser: argparse.ArgumentParser) -> None: + """ + help command must correctly parse repository list + """ + args = parser.parse_args(["-r", "repo", "help"]) + assert args.repository == [""] + + def test_subparsers_help_commands_unsafe(parser: argparse.ArgumentParser) -> None: """ - help-commands-unsafe command must imply architecture list, lock, report, quiet, unsafe and parser + help-commands-unsafe command must imply architecture list, lock, quiet, report, repository, unsafe and parser """ args = parser.parse_args(["help-commands-unsafe"]) assert args.architecture == [""] assert args.lock is None - assert not args.report assert args.quiet + assert not args.report + assert args.repository == [""] assert args.unsafe assert args.parser is not None and args.parser() -def test_subparsers_help_commands_unsafe_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_help_commands_unsafe_option_architecture(parser: argparse.ArgumentParser) -> None: """ help-commands-unsafe command must correctly parse architecture list """ @@ -128,19 +172,28 @@ def test_subparsers_help_commands_unsafe_architecture(parser: argparse.ArgumentP assert args.architecture == [""] +def test_subparsers_help_commands_unsafe_option_repository(parser: argparse.ArgumentParser) -> None: + """ + help-commands-unsafe command must correctly parse repository list + """ + args = parser.parse_args(["-r", "repo", "help-commands-unsafe"]) + assert args.repository == [""] + + def test_subparsers_help_updates(parser: argparse.ArgumentParser) -> None: """ - help-updates command must imply architecture list, lock, report, quiet and unsafe + help-updates command must imply architecture list, lock, quiet, report, repository, and unsafe """ args = parser.parse_args(["help-updates"]) assert args.architecture == [""] assert args.lock is None - assert not args.report assert args.quiet + assert not args.report + assert args.repository == [""] assert args.unsafe -def test_subparsers_help_updates_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_help_updates_option_architecture(parser: argparse.ArgumentParser) -> None: """ help-updates command must correctly parse architecture list """ @@ -148,19 +201,28 @@ def test_subparsers_help_updates_architecture(parser: argparse.ArgumentParser) - assert args.architecture == [""] +def test_subparsers_help_updates_option_repository(parser: argparse.ArgumentParser) -> None: + """ + help-updates command must correctly parse repository list + """ + args = parser.parse_args(["-r", "repo", "help-updates"]) + assert args.repository == [""] + + def test_subparsers_help_version(parser: argparse.ArgumentParser) -> None: """ - help-version command must imply architecture, lock, report, quiet and unsafe + help-version command must imply architecture, lock, quiet, report, repository and unsafe """ args = parser.parse_args(["help-version"]) assert args.architecture == [""] assert args.lock is None - assert not args.report assert args.quiet + assert not args.report + assert args.repository == [""] assert args.unsafe -def test_subparsers_help_version_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_help_version_option_architecture(parser: argparse.ArgumentParser) -> None: """ help-version command must correctly parse architecture list """ @@ -168,7 +230,15 @@ def test_subparsers_help_version_architecture(parser: argparse.ArgumentParser) - assert args.architecture == [""] -def test_subparsers_package_add_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_help_version_option_repository(parser: argparse.ArgumentParser) -> None: + """ + help-version command must correctly parse repository list + """ + args = parser.parse_args(["-r", "repo", "help-version"]) + assert args.repository == [""] + + +def test_subparsers_package_add_option_architecture(parser: argparse.ArgumentParser) -> None: """ package-add command must correctly parse architecture list """ @@ -178,6 +248,16 @@ def test_subparsers_package_add_architecture(parser: argparse.ArgumentParser) -> assert args.architecture == ["x86_64"] +def test_subparsers_package_add_option_repository(parser: argparse.ArgumentParser) -> None: + """ + package-add command must correctly parse repository list + """ + args = parser.parse_args(["package-add", "ahriman"]) + assert args.repository is None + args = parser.parse_args(["-r", "repo", "package-add", "ahriman"]) + assert args.repository == ["repo"] + + def test_subparsers_package_add_option_refresh(parser: argparse.ArgumentParser) -> None: """ package-add command must count refresh options @@ -190,7 +270,7 @@ def test_subparsers_package_add_option_refresh(parser: argparse.ArgumentParser) assert args.refresh == 2 -def test_subparsers_package_remove_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_package_remove_option_architecture(parser: argparse.ArgumentParser) -> None: """ package-remove command must correctly parse architecture list """ @@ -200,41 +280,54 @@ def test_subparsers_package_remove_architecture(parser: argparse.ArgumentParser) assert args.architecture == ["x86_64"] +def test_subparsers_package_remove_option_repository(parser: argparse.ArgumentParser) -> None: + """ + package-remove command must correctly parse repository list + """ + args = parser.parse_args(["package-remove", "ahriman"]) + assert args.repository is None + args = parser.parse_args(["-r", "repo", "package-remove", "ahriman"]) + assert args.repository == ["repo"] + + def test_subparsers_package_status(parser: argparse.ArgumentParser) -> None: """ - package-status command must imply lock, report, quiet and unsafe + package-status command must imply lock, quiet, report and unsafe """ - args = parser.parse_args(["-a", "x86_64", "package-status"]) + args = parser.parse_args(["-a", "x86_64", "-r", "repo", "package-status"]) assert args.architecture == ["x86_64"] assert args.lock is None - assert not args.report assert args.quiet + assert not args.report + assert args.repository == ["repo"] assert args.unsafe def test_subparsers_package_status_remove(parser: argparse.ArgumentParser) -> None: """ - package-status-remove command must imply action, lock, report, quiet and unsafe + package-status-remove command must imply action, lock, quiet, report and unsafe """ - args = parser.parse_args(["-a", "x86_64", "package-status-remove", "ahriman"]) + args = parser.parse_args(["-a", "x86_64", "-r", "repo", "package-status-remove", "ahriman"]) assert args.architecture == ["x86_64"] assert args.action == Action.Remove assert args.lock is None - assert not args.report assert args.quiet + assert not args.report + assert args.repository == ["repo"] assert args.unsafe def test_subparsers_package_status_update(parser: argparse.ArgumentParser) -> None: """ - package-status-update command must imply action, lock, report, quiet and unsafe + package-status-update command must imply action, lock, quiet, report and unsafe """ - args = parser.parse_args(["-a", "x86_64", "package-status-update"]) + args = parser.parse_args(["-a", "x86_64", "-r", "repo", "package-status-update"]) assert args.architecture == ["x86_64"] assert args.action == Action.Update assert args.lock is None - assert not args.report assert args.quiet + assert not args.report + assert args.repository == ["repo"] assert args.unsafe @@ -242,24 +335,25 @@ def test_subparsers_package_status_update_option_status(parser: argparse.Argumen """ package-status-update command must convert status option to buildstatusenum instance """ - args = parser.parse_args(["-a", "x86_64", "package-status-update"]) + args = parser.parse_args(["-a", "x86_64", "-r", "repo", "package-status-update"]) assert isinstance(args.status, BuildStatusEnum) - args = parser.parse_args(["-a", "x86_64", "package-status-update", "--status", "failed"]) + args = parser.parse_args(["-a", "x86_64", "-r", "repo", "package-status-update", "--status", "failed"]) assert isinstance(args.status, BuildStatusEnum) def test_subparsers_patch_add(parser: argparse.ArgumentParser) -> None: """ - patch-add command must imply action, architecture list, lock and report + patch-add command must imply action, architecture list, lock, report and repository """ args = parser.parse_args(["patch-add", "ahriman", "version"]) assert args.action == Action.Update assert args.architecture == [""] assert args.lock is None assert not args.report + assert args.repository == [""] -def test_subparsers_patch_add_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_patch_add_option_architecture(parser: argparse.ArgumentParser) -> None: """ patch-add command must correctly parse architecture list """ @@ -267,19 +361,28 @@ def test_subparsers_patch_add_architecture(parser: argparse.ArgumentParser) -> N assert args.architecture == [""] +def test_subparsers_patch_add_option_repository(parser: argparse.ArgumentParser) -> None: + """ + patch-add command must correctly parse repository list + """ + args = parser.parse_args(["-r", "repo", "patch-add", "ahriman", "version"]) + assert args.repository == [""] + + def test_subparsers_patch_list(parser: argparse.ArgumentParser) -> None: """ - patch-list command must imply action, architecture list, lock, report and unsafe + patch-list command must imply action, architecture list, lock, report, repository and unsafe """ args = parser.parse_args(["patch-list", "ahriman"]) assert args.action == Action.List assert args.architecture == [""] assert args.lock is None assert not args.report + assert args.repository == [""] assert args.unsafe -def test_subparsers_patch_list_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_patch_list_option_architecture(parser: argparse.ArgumentParser) -> None: """ patch-list command must correctly parse architecture list """ @@ -287,18 +390,43 @@ def test_subparsers_patch_list_architecture(parser: argparse.ArgumentParser) -> assert args.architecture == [""] +def test_subparsers_patch_list_option_repository(parser: argparse.ArgumentParser) -> None: + """ + patch-list command must correctly parse repository list + """ + args = parser.parse_args(["-r", "repo", "patch-list", "ahriman"]) + assert args.repository == [""] + + +def test_subparsers_patch_list_option_variable_empty(parser: argparse.ArgumentParser) -> None: + """ + patch-list command must accept empty variable list as None + """ + args = parser.parse_args(["patch-list", "ahriman"]) + assert args.variable is None + + +def test_subparsers_patch_list_option_variable_multiple(parser: argparse.ArgumentParser) -> None: + """ + patch-list command must accept multiple variables + """ + args = parser.parse_args(["patch-list", "-v", "var1", "-v", "var2", "ahriman"]) + assert args.variable == ["var1", "var2"] + + def test_subparsers_patch_remove(parser: argparse.ArgumentParser) -> None: """ - patch-remove command must imply action, architecture list, lock and report + patch-remove command must imply action, architecture list, lock, report and repository """ args = parser.parse_args(["patch-remove", "ahriman"]) assert args.action == Action.Remove assert args.architecture == [""] assert args.lock is None assert not args.report + assert args.repository == [""] -def test_subparsers_patch_remove_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_patch_remove_option_architecture(parser: argparse.ArgumentParser) -> None: """ patch-remove command must correctly parse architecture list """ @@ -306,19 +434,44 @@ def test_subparsers_patch_remove_architecture(parser: argparse.ArgumentParser) - assert args.architecture == [""] +def test_subparsers_patch_remove_option_repository(parser: argparse.ArgumentParser) -> None: + """ + patch-remove command must correctly parse repository list + """ + args = parser.parse_args(["-r", "repo", "patch-remove", "ahriman"]) + assert args.repository == [""] + + +def test_subparsers_patch_remove_option_variable_empty(parser: argparse.ArgumentParser) -> None: + """ + patch-remove command must accept empty variable list as None + """ + args = parser.parse_args(["patch-remove", "ahriman"]) + assert args.variable is None + + +def test_subparsers_patch_remove_option_variable_multiple(parser: argparse.ArgumentParser) -> None: + """ + patch-remove command must accept multiple variables + """ + args = parser.parse_args(["patch-remove", "-v", "var1", "-v", "var2", "ahriman"]) + assert args.variable == ["var1", "var2"] + + def test_subparsers_patch_set_add(parser: argparse.ArgumentParser) -> None: """ - patch-set-add command must imply action, architecture list, lock, report and variable + patch-set-add command must imply action, architecture list, lock, report, repository and variable """ args = parser.parse_args(["patch-set-add", "ahriman"]) assert args.action == Action.Update assert args.architecture == [""] assert args.lock is None assert not args.report + assert args.repository == [""] assert args.variable is None -def test_subparsers_patch_set_add_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_patch_set_add_option_architecture(parser: argparse.ArgumentParser) -> None: """ patch-set-add command must correctly parse architecture list """ @@ -334,6 +487,14 @@ def test_subparsers_patch_set_add_option_package(parser: argparse.ArgumentParser assert isinstance(args.package, Path) +def test_subparsers_patch_set_add_option_repository(parser: argparse.ArgumentParser) -> None: + """ + patch-set-add command must correctly parse repository list + """ + args = parser.parse_args(["-r", "repo", "patch-set-add", "ahriman"]) + assert args.repository == [""] + + def test_subparsers_patch_set_add_option_track(parser: argparse.ArgumentParser) -> None: """ patch-set-add command must correctly parse track files patterns @@ -344,16 +505,17 @@ def test_subparsers_patch_set_add_option_track(parser: argparse.ArgumentParser) def test_subparsers_repo_backup(parser: argparse.ArgumentParser) -> None: """ - repo-backup command must imply architecture list, lock, report and unsafe + repo-backup command must imply architecture list, lock, report, repository and unsafe """ args = parser.parse_args(["repo-backup", "output.zip"]) assert args.architecture == [""] assert args.lock is None assert not args.report + assert args.repository == [""] assert args.unsafe -def test_subparsers_repo_backup_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_repo_backup_option_architecture(parser: argparse.ArgumentParser) -> None: """ repo-backup command must correctly parse architecture list """ @@ -361,6 +523,14 @@ def test_subparsers_repo_backup_architecture(parser: argparse.ArgumentParser) -> assert args.architecture == [""] +def test_subparsers_repo_backup_option_repository(parser: argparse.ArgumentParser) -> None: + """ + repo-backup command must correctly parse repository list + """ + args = parser.parse_args(["-r", "repo", "repo-backup", "output.zip"]) + assert args.repository == [""] + + def test_subparsers_repo_check(parser: argparse.ArgumentParser) -> None: """ repo-check command must imply dependencies, dry-run, aur, manual and username @@ -373,7 +543,7 @@ def test_subparsers_repo_check(parser: argparse.ArgumentParser) -> None: assert args.username is None -def test_subparsers_repo_check_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_repo_check_option_architecture(parser: argparse.ArgumentParser) -> None: """ repo-check command must correctly parse architecture list """ @@ -383,6 +553,16 @@ def test_subparsers_repo_check_architecture(parser: argparse.ArgumentParser) -> assert args.architecture == ["x86_64"] +def test_subparsers_repo_check_option_repository(parser: argparse.ArgumentParser) -> None: + """ + repo-check command must correctly parse architecture list + """ + args = parser.parse_args(["repo-check"]) + assert args.repository is None + args = parser.parse_args(["-r", "repo", "repo-check"]) + assert args.repository == ["repo"] + + def test_subparsers_repo_check_option_refresh(parser: argparse.ArgumentParser) -> None: """ repo-check command must count refresh options @@ -403,7 +583,7 @@ def test_subparsers_repo_create_keyring(parser: argparse.ArgumentParser) -> None assert args.trigger == ["ahriman.core.support.KeyringTrigger"] -def test_subparsers_repo_create_keyring_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_repo_create_keyring_option_architecture(parser: argparse.ArgumentParser) -> None: """ repo-create-keyring command must correctly parse architecture list """ @@ -413,6 +593,16 @@ def test_subparsers_repo_create_keyring_architecture(parser: argparse.ArgumentPa assert args.architecture == ["x86_64"] +def test_subparsers_repo_create_keyring_option_repository(parser: argparse.ArgumentParser) -> None: + """ + repo-create-keyring command must correctly parse repository list + """ + args = parser.parse_args(["repo-create-keyring"]) + assert args.repository is None + args = parser.parse_args(["-r", "repo", "repo-create-keyring"]) + assert args.repository == ["repo"] + + def test_subparsers_repo_create_mirrorlist(parser: argparse.ArgumentParser) -> None: """ repo-create-mirrorlist command must imply trigger @@ -421,7 +611,7 @@ def test_subparsers_repo_create_mirrorlist(parser: argparse.ArgumentParser) -> N assert args.trigger == ["ahriman.core.support.MirrorlistTrigger"] -def test_subparsers_repo_create_mirrorlist_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_repo_create_mirrorlist_option_architecture(parser: argparse.ArgumentParser) -> None: """ repo-create-mirrorlist command must correctly parse architecture list """ @@ -431,6 +621,16 @@ def test_subparsers_repo_create_mirrorlist_architecture(parser: argparse.Argumen assert args.architecture == ["x86_64"] +def test_subparsers_repo_create_mirrorlist_option_repository(parser: argparse.ArgumentParser) -> None: + """ + repo-create-mirrorlist command must correctly parse repository list + """ + args = parser.parse_args(["repo-create-mirrorlist"]) + assert args.repository is None + args = parser.parse_args(["-r", "repo", "repo-create-mirrorlist"]) + assert args.repository == ["repo"] + + def test_subparsers_repo_daemon(parser: argparse.ArgumentParser) -> None: """ repo-daemon command must imply dry run, exit code and package @@ -463,7 +663,7 @@ def test_subparsers_repo_daemon_option_interval(parser: argparse.ArgumentParser) assert isinstance(args.interval, int) -def test_subparsers_repo_rebuild_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_repo_rebuild_option_architecture(parser: argparse.ArgumentParser) -> None: """ repo-rebuild command must correctly parse architecture list """ @@ -473,15 +673,41 @@ def test_subparsers_repo_rebuild_architecture(parser: argparse.ArgumentParser) - assert args.architecture == ["x86_64"] +def test_subparsers_repo_rebuild_option_repository(parser: argparse.ArgumentParser) -> None: + """ + repo-rebuild command must correctly parse repository list + """ + args = parser.parse_args(["repo-rebuild"]) + assert args.repository is None + args = parser.parse_args(["-r", "repo", "repo-rebuild"]) + assert args.repository == ["repo"] + + +def test_subparsers_repo_rebuild_option_depends_on_empty(parser: argparse.ArgumentParser) -> None: + """ + repo-rebuild command must accept empty depends-on list as None + """ + args = parser.parse_args(["repo-rebuild"]) + assert args.depends_on is None + + +def test_subparsers_repo_rebuild_option_depends_on_multiple(parser: argparse.ArgumentParser) -> None: + """ + repo-rebuild command must accept multiple depends-on + """ + args = parser.parse_args(["repo-rebuild", "--depends-on", "package1", "--depends-on", "package2"]) + assert args.depends_on == ["package1", "package2"] + + def test_subparsers_repo_rebuild_option_status(parser: argparse.ArgumentParser) -> None: """ repo-rebuild command must convert status option to BuildStatusEnum instance """ - args = parser.parse_args(["-a", "x86_64", "repo-rebuild", "--status", "failed"]) + args = parser.parse_args(["-a", "x86_64", "-r", "repo", "repo-rebuild", "--status", "failed"]) assert isinstance(args.status, BuildStatusEnum) -def test_subparsers_repo_remove_unknown_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_repo_remove_unknown_option_architecture(parser: argparse.ArgumentParser) -> None: """ repo-remove-unknown command must correctly parse architecture list """ @@ -491,6 +717,16 @@ def test_subparsers_repo_remove_unknown_architecture(parser: argparse.ArgumentPa assert args.architecture == ["x86_64"] +def test_subparsers_repo_remove_unknown_option_repository(parser: argparse.ArgumentParser) -> None: + """ + repo-remove-unknown command must correctly parse repository list + """ + args = parser.parse_args(["repo-remove-unknown"]) + assert args.repository is None + args = parser.parse_args(["-r", "repo", "repo-remove-unknown"]) + assert args.repository == ["repo"] + + def test_subparsers_repo_report(parser: argparse.ArgumentParser) -> None: """ repo-report command must imply trigger @@ -499,7 +735,7 @@ def test_subparsers_repo_report(parser: argparse.ArgumentParser) -> None: assert args.trigger == ["ahriman.core.report.ReportTrigger"] -def test_subparsers_repo_report_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_repo_report_option_architecture(parser: argparse.ArgumentParser) -> None: """ repo-report command must correctly parse architecture list """ @@ -509,18 +745,29 @@ def test_subparsers_repo_report_architecture(parser: argparse.ArgumentParser) -> assert args.architecture == ["x86_64"] +def test_subparsers_repo_report_option_repository(parser: argparse.ArgumentParser) -> None: + """ + repo-report command must correctly parse repository list + """ + args = parser.parse_args(["repo-report"]) + assert args.repository is None + args = parser.parse_args(["-r", "repo", "repo-report"]) + assert args.repository == ["repo"] + + def test_subparsers_repo_restore(parser: argparse.ArgumentParser) -> None: """ - repo-restore command must imply architecture list, lock, report and unsafe + repo-restore command must imply architecture list, lock, report, repository and unsafe """ args = parser.parse_args(["repo-restore", "output.zip"]) assert args.architecture == [""] assert args.lock is None assert not args.report + assert args.repository == [""] assert args.unsafe -def test_subparsers_repo_restore_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_repo_restore_option_architecture(parser: argparse.ArgumentParser) -> None: """ repo-restore command must correctly parse architecture list """ @@ -528,7 +775,15 @@ def test_subparsers_repo_restore_architecture(parser: argparse.ArgumentParser) - assert args.architecture == [""] -def test_subparsers_repo_sign_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_repo_restore_option_repository(parser: argparse.ArgumentParser) -> None: + """ + repo-restore command must correctly parse repository list + """ + args = parser.parse_args(["-r", "repo", "repo-restore", "output.zip"]) + assert args.repository == [""] + + +def test_subparsers_repo_sign_option_architecture(parser: argparse.ArgumentParser) -> None: """ repo-sign command must correctly parse architecture list """ @@ -538,17 +793,28 @@ def test_subparsers_repo_sign_architecture(parser: argparse.ArgumentParser) -> N assert args.architecture == ["x86_64"] +def test_subparsers_repo_sign_option_repository(parser: argparse.ArgumentParser) -> None: + """ + repo-sign command must correctly parse repository list + """ + args = parser.parse_args(["repo-sign"]) + assert args.repository is None + args = parser.parse_args(["-r", "repo", "repo-sign"]) + assert args.repository == ["repo"] + + def test_subparsers_repo_status_update(parser: argparse.ArgumentParser) -> None: """ - re[p-status-update command must imply action, lock, report, package, quiet and unsafe + re[p-status-update command must imply action, lock, quiet, report, package and unsafe """ - args = parser.parse_args(["-a", "x86_64", "package-status-update"]) + args = parser.parse_args(["-a", "x86_64", "-r", "repo", "package-status-update"]) assert args.architecture == ["x86_64"] assert args.action == Action.Update assert args.lock is None - assert not args.report - assert not args.package assert args.quiet + assert not args.report + assert args.repository == ["repo"] + assert not args.package assert args.unsafe @@ -556,9 +822,9 @@ def test_subparsers_repo_status_update_option_status(parser: argparse.ArgumentPa """ repo-status-update command must convert status option to BuildStatusEnum instance """ - args = parser.parse_args(["-a", "x86_64", "repo-status-update"]) + args = parser.parse_args(["-a", "x86_64", "-r", "repo", "repo-status-update"]) assert isinstance(args.status, BuildStatusEnum) - args = parser.parse_args(["-a", "x86_64", "repo-status-update", "--status", "failed"]) + args = parser.parse_args(["-a", "x86_64", "-r", "repo", "repo-status-update", "--status", "failed"]) assert isinstance(args.status, BuildStatusEnum) @@ -570,7 +836,7 @@ def test_subparsers_repo_sync(parser: argparse.ArgumentParser) -> None: assert args.trigger == ["ahriman.core.upload.UploadTrigger"] -def test_subparsers_repo_sync_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_repo_sync_option_architecture(parser: argparse.ArgumentParser) -> None: """ repo-sync command must correctly parse architecture list """ @@ -580,18 +846,28 @@ def test_subparsers_repo_sync_architecture(parser: argparse.ArgumentParser) -> N assert args.architecture == ["x86_64"] +def test_subparsers_repo_sync_option_repository(parser: argparse.ArgumentParser) -> None: + """ + repo-sync command must correctly parse repository list + """ + args = parser.parse_args(["repo-sync"]) + assert args.repository is None + args = parser.parse_args(["-r", "repo", "repo-sync"]) + assert args.repository == ["repo"] + + def test_subparsers_repo_tree(parser: argparse.ArgumentParser) -> None: """ - repo-tree command must imply lock, report, quiet and unsafe + repo-tree command must imply lock, quiet, report and unsafe """ args = parser.parse_args(["repo-tree"]) assert args.lock is None - assert not args.report assert args.quiet + assert not args.report assert args.unsafe -def test_subparsers_repo_tree_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_repo_tree_option_architecture(parser: argparse.ArgumentParser) -> None: """ repo-tree command must correctly parse architecture list """ @@ -601,6 +877,16 @@ def test_subparsers_repo_tree_architecture(parser: argparse.ArgumentParser) -> N assert args.architecture == ["x86_64"] +def test_subparsers_repo_tree_option_repository(parser: argparse.ArgumentParser) -> None: + """ + repo-tree command must correctly parse repository list + """ + args = parser.parse_args(["repo-tree"]) + assert args.repository is None + args = parser.parse_args(["-r", "repo", "repo-tree"]) + assert args.repository == ["repo"] + + def test_subparsers_repo_tree_option_partitions(parser: argparse.ArgumentParser) -> None: """ must convert partitions option to int instance @@ -611,7 +897,7 @@ def test_subparsers_repo_tree_option_partitions(parser: argparse.ArgumentParser) assert isinstance(args.partitions, int) -def test_subparsers_repo_triggers_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_repo_triggers_option_architecture(parser: argparse.ArgumentParser) -> None: """ repo-triggers command must correctly parse architecture list """ @@ -621,7 +907,17 @@ def test_subparsers_repo_triggers_architecture(parser: argparse.ArgumentParser) assert args.architecture == ["x86_64"] -def test_subparsers_repo_update_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_repo_triggers_option_repository(parser: argparse.ArgumentParser) -> None: + """ + repo-triggers command must correctly parse repository list + """ + args = parser.parse_args(["repo-triggers"]) + assert args.repository is None + args = parser.parse_args(["-r", "repo", "repo-triggers"]) + assert args.repository == ["repo"] + + +def test_subparsers_repo_update_option_architecture(parser: argparse.ArgumentParser) -> None: """ repo-update command must correctly parse architecture list """ @@ -631,6 +927,16 @@ def test_subparsers_repo_update_architecture(parser: argparse.ArgumentParser) -> assert args.architecture == ["x86_64"] +def test_subparsers_repo_update_option_repository(parser: argparse.ArgumentParser) -> None: + """ + repo-update command must correctly parse repository list + """ + args = parser.parse_args(["repo-update"]) + assert args.repository is None + args = parser.parse_args(["-r", "repo", "repo-update"]) + assert args.repository == ["repo"] + + def test_subparsers_repo_update_option_refresh(parser: argparse.ArgumentParser) -> None: """ repo-update command must count refresh options @@ -652,7 +958,7 @@ def test_subparsers_service_clean(parser: argparse.ArgumentParser) -> None: assert args.unsafe -def test_subparsers_service_clean_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_service_clean_option_architecture(parser: argparse.ArgumentParser) -> None: """ service-clean command must correctly parse architecture list """ @@ -662,41 +968,54 @@ def test_subparsers_service_clean_architecture(parser: argparse.ArgumentParser) assert args.architecture == ["x86_64"] +def test_subparsers_service_clean_option_repository(parser: argparse.ArgumentParser) -> None: + """ + service-clean command must correctly parse repository list + """ + args = parser.parse_args(["service-clean"]) + assert args.repository is None + args = parser.parse_args(["-r", "repo", "service-clean"]) + assert args.repository == ["repo"] + + def test_subparsers_service_config(parser: argparse.ArgumentParser) -> None: """ - service-config command must imply lock, report, quiet and unsafe + service-config command must imply lock, quiet, report and unsafe """ - args = parser.parse_args(["-a", "x86_64", "service-config"]) + args = parser.parse_args(["-a", "x86_64", "-r", "repo", "service-config"]) assert args.architecture == ["x86_64"] assert args.lock is None - assert not args.report assert args.quiet + assert not args.report + assert args.repository == ["repo"] assert args.unsafe def test_subparsers_service_config_validate(parser: argparse.ArgumentParser) -> None: """ - service-config-validate command must imply lock, report, quiet and unsafe + service-config-validate command must imply lock, quiet, report and unsafe """ - args = parser.parse_args(["-a", "x86_64", "service-config-validate"]) + args = parser.parse_args(["-a", "x86_64", "-r", "repo", "service-config-validate"]) assert args.architecture == ["x86_64"] assert args.lock is None - assert not args.report assert args.quiet + assert not args.report + assert args.repository == ["repo"] assert args.unsafe def test_subparsers_service_key_import(parser: argparse.ArgumentParser) -> None: """ - service-key-import command must imply architecture list, lock and report + service-key-import command must imply architecture list, lock, report and repository """ args = parser.parse_args(["service-key-import", "key"]) assert args.architecture == [""] assert args.lock is None assert not args.report + assert args.repository == [""] -def test_subparsers_service_key_import_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_service_key_import_option_architecture(parser: argparse.ArgumentParser) -> None: """ service-key-import command must correctly parse architecture list """ @@ -704,16 +1023,24 @@ def test_subparsers_service_key_import_architecture(parser: argparse.ArgumentPar assert args.architecture == [""] +def test_subparsers_service_key_import_option_repository(parser: argparse.ArgumentParser) -> None: + """ + service-key-import command must correctly parse repository list + """ + args = parser.parse_args(["-r", "repo", "service-key-import", "key"]) + assert args.repository == [""] + + def test_subparsers_service_setup(parser: argparse.ArgumentParser) -> None: """ - service-setup command must imply lock, report, quiet and unsafe + service-setup command must imply lock, quiet, report and unsafe """ - args = parser.parse_args(["-a", "x86_64", "service-setup", "--packager", "John Doe ", - "--repository", "aur-clone"]) + args = parser.parse_args(["-a", "x86_64", "-r", "repo", "service-setup", "--packager", "John Doe "]) assert args.architecture == ["x86_64"] assert args.lock is None - assert not args.report assert args.quiet + assert not args.report + assert args.repository == ["repo"] assert args.unsafe @@ -721,11 +1048,10 @@ def test_subparsers_service_setup_option_from_configuration(parser: argparse.Arg """ service-setup command must convert from-configuration option to path instance """ - args = parser.parse_args(["-a", "x86_64", "service-setup", "--packager", "John Doe ", - "--repository", "aur-clone"]) + args = parser.parse_args(["-a", "x86_64", "-r", "repo", "service-setup", "--packager", "John Doe "]) assert isinstance(args.from_configuration, Path) - args = parser.parse_args(["-a", "x86_64", "service-setup", "--packager", "John Doe ", - "--repository", "aur-clone", "--from-configuration", "path"]) + args = parser.parse_args(["-a", "x86_64", "-r", "repo", "service-setup", "--packager", "John Doe ", + "--from-configuration", "path"]) assert isinstance(args.from_configuration, Path) @@ -733,12 +1059,29 @@ def test_subparsers_service_setup_option_sign_target(parser: argparse.ArgumentPa """ service-setup command must convert sign-target option to SignSettings instance """ - args = parser.parse_args(["-a", "x86_64", "service-setup", "--packager", "John Doe ", - "--repository", "aur-clone", "--sign-target", "packages"]) + args = parser.parse_args(["-a", "x86_64", "-r", "repo", "service-setup", "--packager", "John Doe ", + "--sign-target", "packages"]) assert args.sign_target assert all(isinstance(target, SignSettings) for target in args.sign_target) +def test_subparsers_service_setup_option_sign_target_empty(parser: argparse.ArgumentParser) -> None: + """ + service-setup command must accept empty sign-target list as None + """ + args = parser.parse_args(["-a", "x86_64", "-r", "repo", "service-setup", "--packager", "John Doe "]) + assert args.sign_target is None + + +def test_subparsers_service_setup_option_sign_target_multiple(parser: argparse.ArgumentParser) -> None: + """ + service-setup command must accept multiple sign-target + """ + args = parser.parse_args(["-a", "x86_64", "-r", "repo", "service-setup", "--packager", "John Doe ", + "--sign-target", "packages", "--sign-target", "repository"]) + assert args.sign_target == [SignSettings.Packages, SignSettings.Repository] + + def test_subparsers_service_shell(parser: argparse.ArgumentParser) -> None: """ service-shell command must imply lock and report @@ -748,19 +1091,30 @@ def test_subparsers_service_shell(parser: argparse.ArgumentParser) -> None: assert not args.report +def test_subparsers_service_tree_migrate(parser: argparse.ArgumentParser) -> None: + """ + service-tree-migrate command must imply lock, quiet and report + """ + args = parser.parse_args(["service-tree-migrate"]) + assert args.lock is None + assert args.quiet + assert not args.report + + def test_subparsers_user_add(parser: argparse.ArgumentParser) -> None: """ - user-add command must imply action, architecture, lock, report and quiet + user-add command must imply action, architecture, lock, quiet, report and repository """ args = parser.parse_args(["user-add", "username"]) assert args.action == Action.Update assert args.architecture == [""] assert args.lock is None - assert not args.report assert args.quiet + assert not args.report + assert args.repository == [""] -def test_subparsers_user_add_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_user_add_option_architecture(parser: argparse.ArgumentParser) -> None: """ user-add command must correctly parse architecture list """ @@ -768,6 +1122,14 @@ def test_subparsers_user_add_architecture(parser: argparse.ArgumentParser) -> No assert args.architecture == [""] +def test_subparsers_user_add_option_repository(parser: argparse.ArgumentParser) -> None: + """ + user-add command must correctly parse repository list + """ + args = parser.parse_args(["-r", "repo", "user-add", "username"]) + assert args.repository == [""] + + def test_subparsers_user_add_option_role(parser: argparse.ArgumentParser) -> None: """ user-add command must convert role option to UserAccess instance @@ -780,18 +1142,19 @@ def test_subparsers_user_add_option_role(parser: argparse.ArgumentParser) -> Non def test_subparsers_user_list(parser: argparse.ArgumentParser) -> None: """ - user-list command must imply action, architecture, lock, report, quiet and unsafe + user-list command must imply action, architecture, lock, quiet, report, repository and unsafe """ args = parser.parse_args(["user-list"]) assert args.action == Action.List assert args.architecture == [""] assert args.lock is None - assert not args.report assert args.quiet + assert not args.report + assert args.repository == [""] assert args.unsafe -def test_subparsers_user_list_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_user_list_option_architecture(parser: argparse.ArgumentParser) -> None: """ user-list command must correctly parse architecture list """ @@ -799,6 +1162,14 @@ def test_subparsers_user_list_architecture(parser: argparse.ArgumentParser) -> N assert args.architecture == [""] +def test_subparsers_user_list_option_repository(parser: argparse.ArgumentParser) -> None: + """ + user-list command must correctly parse repository list + """ + args = parser.parse_args(["-r", "repo", "user-list"]) + assert args.repository == [""] + + def test_subparsers_user_list_option_role(parser: argparse.ArgumentParser) -> None: """ user-list command must convert role option to UserAccess instance @@ -809,17 +1180,18 @@ def test_subparsers_user_list_option_role(parser: argparse.ArgumentParser) -> No def test_subparsers_user_remove(parser: argparse.ArgumentParser) -> None: """ - user-remove command must imply action, architecture, lock, report and quiet + user-remove command must imply action, architecture, lock, quiet, report and repository """ args = parser.parse_args(["user-remove", "username"]) assert args.action == Action.Remove assert args.architecture == [""] assert args.lock is None - assert not args.report assert args.quiet + assert not args.report + assert args.repository == [""] -def test_subparsers_user_remove_architecture(parser: argparse.ArgumentParser) -> None: +def test_subparsers_user_remove_option_architecture(parser: argparse.ArgumentParser) -> None: """ user-remove command must correctly parse architecture list """ @@ -827,21 +1199,34 @@ def test_subparsers_user_remove_architecture(parser: argparse.ArgumentParser) -> assert args.architecture == [""] +def test_subparsers_user_remove_option_repository(parser: argparse.ArgumentParser) -> None: + """ + user-remove command must correctly parse repository list + """ + args = parser.parse_args(["-r", "repo", "user-remove", "username"]) + assert args.repository == [""] + + def test_subparsers_web(parser: argparse.ArgumentParser) -> None: """ web command must imply report and parser """ - args = parser.parse_args(["-a", "x86_64", "web"]) + args = parser.parse_args(["-a", "x86_64", "-r", "repo", "web"]) assert args.architecture == ["x86_64"] assert not args.report + assert args.repository == ["repo"] assert args.parser is not None and args.parser() -def test_run(args: argparse.Namespace, mocker: MockerFixture) -> None: +def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: """ application must be run """ - args.architecture = "x86_64" + path, repository_id = configuration.check_loaded() + + args.architecture = repository_id.architecture + args.repository = repository_id.name + args.configuration = path args.command = "" args.handler = Handler diff --git a/tests/ahriman/application/test_lock.py b/tests/ahriman/application/test_lock.py index 088f6b5c..23851685 100644 --- a/tests/ahriman/application/test_lock.py +++ b/tests/ahriman/application/test_lock.py @@ -18,14 +18,16 @@ def test_path(args: argparse.Namespace, configuration: Configuration) -> None: """ must create path variable correctly """ - assert Lock(args, "x86_64", configuration).path is None + _, repository_id = configuration.check_loaded() + + assert Lock(args, repository_id, configuration).path is None args.lock = Path("/run/ahriman.lock") - assert Lock(args, "x86_64", configuration).path == Path("/run/ahriman_x86_64.lock") + assert Lock(args, repository_id, configuration).path == Path("/run/ahriman_aur-clone_x86_64.lock") with pytest.raises(ValueError): args.lock = Path("/") - Lock(args, "x86_64", configuration).path # special case + Lock(args, repository_id, configuration).path # special case def test_check_version(lock: Lock, mocker: MockerFixture) -> None: diff --git a/tests/ahriman/conftest.py b/tests/ahriman/conftest.py index 12b96d86..7a91d465 100644 --- a/tests/ahriman/conftest.py +++ b/tests/ahriman/conftest.py @@ -19,7 +19,9 @@ from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.package import Package from ahriman.models.package_description import PackageDescription from ahriman.models.package_source import PackageSource +from ahriman.models.pacman_synchronization import PacmanSynchronization from ahriman.models.remote_source import RemoteSource +from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.result import Result from ahriman.models.user import User @@ -243,18 +245,19 @@ def auth(configuration: Configuration) -> Auth: @pytest.fixture -def configuration(resource_path_root: Path) -> Configuration: +def configuration(repository_id: RepositoryId, resource_path_root: Path) -> Configuration: """ configuration fixture Args: + repository_id(RepositoryId): repository identifier fixture resource_path_root(Path): resource path root directory Returns: Configuration: configuration test instance """ path = resource_path_root / "core" / "ahriman.ini" - return Configuration.from_path(path=path, architecture="x86_64") + return Configuration.from_path(path, repository_id) @pytest.fixture @@ -434,7 +437,8 @@ def pacman(configuration: Configuration) -> Pacman: Returns: Pacman: pacman wrapper test instance """ - return Pacman("x86_64", configuration, refresh_database=0) + _, repository_id = configuration.check_loaded() + return Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled) @pytest.fixture @@ -481,7 +485,19 @@ def repository(configuration: Configuration, database: SQLite, mocker: MockerFix Repository: repository test instance """ mocker.patch("ahriman.core.repository.Repository._set_context") - return Repository.load("x86_64", configuration, database, report=False) + _, repository_id = configuration.check_loaded() + return Repository.load(repository_id, configuration, database, report=False) + + +@pytest.fixture +def repository_id() -> RepositoryId: + """ + fixture for repository identifier + + Returns: + RepositoryId: repository identifier test instance + """ + return RepositoryId("x86_64", "aur-clone") @pytest.fixture @@ -525,7 +541,12 @@ def spawner(configuration: Configuration) -> Spawn: Returns: Spawn: spawner fixture """ - return Spawn(MagicMock(), "x86_64", ["--architecture", "x86_64", "--configuration", str(configuration.path)]) + _, repository_id = configuration.check_loaded() + return Spawn(MagicMock(), repository_id, [ + "--architecture", "x86_64", + "--repository", repository_id.name, + "--configuration", str(configuration.path), + ]) @pytest.fixture @@ -554,4 +575,5 @@ def watcher(configuration: Configuration, database: SQLite, repository: Reposito Watcher: package status watcher test instance """ mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) - return Watcher("x86_64", configuration, database) + _, repository_id = configuration.check_loaded() + return Watcher(repository_id, configuration, database) diff --git a/tests/ahriman/core/alpm/test_pacman.py b/tests/ahriman/core/alpm/test_pacman.py index 63644055..4bcdb828 100644 --- a/tests/ahriman/core/alpm/test_pacman.py +++ b/tests/ahriman/core/alpm/test_pacman.py @@ -19,12 +19,13 @@ def test_init_with_local_cache(configuration: Configuration, mocker: MockerFixtu mocker.patch("ahriman.core.alpm.pacman.Pacman.database_copy") sync_mock = mocker.patch("ahriman.core.alpm.pacman.Pacman.database_sync") configuration.set_option("alpm", "use_ahriman_cache", "yes") + _, repository_id = configuration.check_loaded() # pyalpm.Handle is trying to reach the directory we've asked, thus we need to patch it a bit with TemporaryDirectory(ignore_cleanup_errors=True) as pacman_root: mocker.patch.object(RepositoryPaths, "pacman", Path(pacman_root)) # during the creation pyalpm.Handle will create also version file which we would like to remove later - pacman = Pacman("x86_64", configuration, refresh_database=PacmanSynchronization.Enabled) + pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Enabled) assert pacman.handle sync_mock.assert_called_once_with(pytest.helpers.anyvar(int), force=False) @@ -36,12 +37,13 @@ def test_init_with_local_cache_forced(configuration: Configuration, mocker: Mock mocker.patch("ahriman.core.alpm.pacman.Pacman.database_copy") sync_mock = mocker.patch("ahriman.core.alpm.pacman.Pacman.database_sync") configuration.set_option("alpm", "use_ahriman_cache", "yes") + _, repository_id = configuration.check_loaded() # pyalpm.Handle is trying to reach the directory we've asked, thus we need to patch it a bit with TemporaryDirectory(ignore_cleanup_errors=True) as pacman_root: mocker.patch.object(RepositoryPaths, "pacman", Path(pacman_root)) # during the creation pyalpm.Handle will create also version file which we would like to remove later - pacman = Pacman("x86_64", configuration, refresh_database=PacmanSynchronization.Force) + pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Force) assert pacman.handle sync_mock.assert_called_once_with(pytest.helpers.anyvar(int), force=True) @@ -56,10 +58,12 @@ def test_database_copy(pacman: Pacman, repository_paths: RepositoryPaths, mocker mocker.patch("pathlib.Path.is_dir", return_value=True) # root database exists, local database does not mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=lambda p: p.is_relative_to(path)) + mkdir_mock = mocker.patch("pathlib.Path.mkdir") copy_mock = mocker.patch("shutil.copy") chown_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.chown") pacman.database_copy(pacman.handle, database, path, repository_paths, use_ahriman_cache=True) + mkdir_mock.assert_called_once_with(mode=0o755, exist_ok=True) copy_mock.assert_called_once_with(path / "sync" / "core.db", dst_path) chown_mock.assert_called_once_with(dst_path) @@ -128,8 +132,8 @@ def test_database_init(pacman: Pacman, configuration: Configuration) -> None: must init database with settings """ mirror = configuration.get("alpm", "mirror") - database = pacman.database_init(pacman.handle, "test", mirror, "x86_64") - assert len(database.servers) == 1 + database = pacman.database_init(pacman.handle, "testing", mirror, "x86_64") + assert database.servers == ["https://geo.mirror.pkgbuild.com/testing/os/x86_64"] def test_database_sync(pacman: Pacman) -> None: diff --git a/tests/ahriman/core/build_tools/test_sources.py b/tests/ahriman/core/build_tools/test_sources.py index 5499788a..509ee2d5 100644 --- a/tests/ahriman/core/build_tools/test_sources.py +++ b/tests/ahriman/core/build_tools/test_sources.py @@ -177,7 +177,7 @@ def test_load(package_ahriman: Package, repository_paths: RepositoryPaths, mocke Sources.load(path, package_ahriman, [patch], repository_paths) fetch_mock.assert_called_once_with(path, package_ahriman.remote) patch_mock.assert_called_once_with(path, patch) - architectures_mock.assert_called_once_with(path, repository_paths.architecture) + architectures_mock.assert_called_once_with(path, repository_paths.repository_id.architecture) def test_load_no_patch(package_ahriman: Package, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: diff --git a/tests/ahriman/core/configuration/test_configuration.py b/tests/ahriman/core/configuration/test_configuration.py index 9aa1d249..db635f46 100644 --- a/tests/ahriman/core/configuration/test_configuration.py +++ b/tests/ahriman/core/configuration/test_configuration.py @@ -7,9 +7,17 @@ from unittest.mock import call as MockCall from ahriman.core.configuration import Configuration from ahriman.core.exceptions import InitializeError +from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_paths import RepositoryPaths +def test_architecture(configuration: Configuration) -> None: + """ + must return valid repository architecture + """ + assert configuration.architecture == "x86_64" + + def test_repository_name(configuration: Configuration) -> None: """ must return valid repository name @@ -24,7 +32,7 @@ def test_repository_paths(configuration: Configuration, repository_paths: Reposi assert configuration.repository_paths == repository_paths -def test_from_path(mocker: MockerFixture) -> None: +def test_from_path(repository_id: RepositoryId, mocker: MockerFixture) -> None: """ must load configuration """ @@ -33,13 +41,13 @@ def test_from_path(mocker: MockerFixture) -> None: load_includes_mock = mocker.patch("ahriman.core.configuration.Configuration.load_includes") path = Path("path") - configuration = Configuration.from_path(path, "x86_64") + configuration = Configuration.from_path(path, repository_id) assert configuration.path == path read_mock.assert_called_once_with(path) load_includes_mock.assert_called_once_with() -def test_from_path_file_missing(mocker: MockerFixture) -> None: +def test_from_path_file_missing(repository_id: RepositoryId, mocker: MockerFixture) -> None: """ must load configuration based on package files """ @@ -47,17 +55,40 @@ def test_from_path_file_missing(mocker: MockerFixture) -> None: mocker.patch("ahriman.core.configuration.Configuration.load_includes") read_mock = mocker.patch("ahriman.core.configuration.Configuration.read") - configuration = Configuration.from_path(Path("path"), "x86_64") + configuration = Configuration.from_path(Path("path"), repository_id) read_mock.assert_called_once_with(configuration.SYSTEM_CONFIGURATION_PATH) +def test_override_sections(repository_id: RepositoryId) -> None: + """ + must correctly generate override section names + """ + assert Configuration.override_sections("build", repository_id) == [ + "build:x86_64", + "build:aur-clone", + "build:aur-clone:x86_64", + ] + + +def test_section_name(configuration: Configuration) -> None: + """ + must return architecture specific group + """ + assert configuration.section_name("build") == "build" + assert configuration.section_name("build", None) == "build" + assert configuration.section_name("build", "x86_64") == "build:x86_64" + assert configuration.section_name("build", "aur-clone", "x86_64") == "build:aur-clone:x86_64" + assert configuration.section_name("build", "aur-clone", None) == "build:aur-clone" + assert configuration.section_name("build", None, "x86_64") == "build:x86_64" + + def test_check_loaded(configuration: Configuration) -> None: """ must return valid path and architecture """ - path, architecture = configuration.check_loaded() + path, repository_id = configuration.check_loaded() assert path == configuration.path - assert architecture == configuration.architecture + assert repository_id == configuration.repository_id def test_check_loaded_path(configuration: Configuration) -> None: @@ -73,7 +104,7 @@ def test_check_loaded_architecture(configuration: Configuration) -> None: """ must raise exception if architecture is none """ - configuration.architecture = None + configuration.repository_id = None with pytest.raises(InitializeError): configuration.check_loaded() @@ -89,9 +120,9 @@ def test_dump_architecture_specific(configuration: Configuration) -> None: """ dump must contain architecture specific settings """ - section = configuration.section_name("build", "x86_64") + section = configuration.section_name("build", configuration.architecture) configuration.set_option(section, "archbuild_flags", "hello flag") - configuration.merge_sections("x86_64") + configuration.merge_sections(configuration.repository_id) dump = configuration.dump() assert dump @@ -100,13 +131,6 @@ def test_dump_architecture_specific(configuration: Configuration) -> None: assert dump["build"]["archbuild_flags"] == "hello flag" -def test_section_name(configuration: Configuration) -> None: - """ - must return architecture specific group - """ - assert configuration.section_name("build", "x86_64") == "build:x86_64" - - def test_getlist(configuration: Configuration) -> None: """ must return list of string correctly @@ -209,7 +233,7 @@ def test_gettype(configuration: Configuration) -> None: """ must extract type from variable """ - section, provider = configuration.gettype("customs3", "x86_64") + section, provider = configuration.gettype("customs3", configuration.repository_id) assert section == "customs3" assert provider == "s3" @@ -218,7 +242,7 @@ def test_gettype_with_fallback(configuration: Configuration) -> None: """ must return same provider name as in fallback """ - section, provider = configuration.gettype("rsync", "x86_64", fallback="abracadabra") + section, provider = configuration.gettype("rsync", configuration.repository_id, fallback="abracadabra") assert section == "rsync" assert provider == "abracadabra" @@ -227,7 +251,7 @@ def test_gettype_from_section(configuration: Configuration) -> None: """ must extract type from section name """ - section, provider = configuration.gettype("rsync", "x86_64") + section, provider = configuration.gettype("rsync", configuration.repository_id) assert section == "rsync" assert provider == "rsync" @@ -236,7 +260,7 @@ def test_gettype_from_section_with_architecture(configuration: Configuration) -> """ must extract type from section name with architecture """ - section, provider = configuration.gettype("github", "x86_64") + section, provider = configuration.gettype("github", configuration.repository_id) assert section == "github:x86_64" assert provider == "github" @@ -248,7 +272,7 @@ def test_gettype_from_section_no_section(configuration: Configuration) -> None: # technically rsync:x86_64 is valid section # but in current configuration it must be considered as missing section with pytest.raises(configparser.NoSectionError): - configuration.gettype("rsync:x86_64", "x86_64") + configuration.gettype("rsync:x86_64", configuration.repository_id) def test_load_includes_missing(configuration: Configuration) -> None: @@ -279,14 +303,44 @@ def test_merge_sections_missing(configuration: Configuration) -> None: """ must merge create section if not exists """ - section = configuration.section_name("build", "x86_64") + section = configuration.section_name("build", configuration.architecture) configuration.remove_section("build") configuration.set_option(section, "key", "value") - configuration.merge_sections("x86_64") + configuration.merge_sections(configuration.repository_id) assert configuration.get("build", "key") == "value" +def test_merge_sections_priority(configuration: Configuration) -> None: + """ + must merge sections in valid order + """ + empty = "build" + arch = configuration.section_name(empty, configuration.architecture) + repo = configuration.section_name(empty, configuration.repository_name) + repo_arch = configuration.section_name(empty, configuration.repository_name, configuration.architecture) + + configuration.set_option(empty, "key1", "key1_value1") + configuration.set_option(arch, "key1", "key1_value2") + configuration.set_option(repo, "key1", "key1_value3") + configuration.set_option(repo_arch, "key1", "key1_value4") + + configuration.set_option(empty, "key2", "key2_value1") + configuration.set_option(arch, "key2", "key2_value2") + configuration.set_option(repo, "key2", "key2_value3") + + configuration.set_option(empty, "key3", "key3_value1") + configuration.set_option(arch, "key3", "key3_value2") + + configuration.set_option(empty, "key4", "key4_value1") + + configuration.merge_sections(configuration.repository_id) + assert configuration.get("build", "key1") == "key1_value4" + assert configuration.get("build", "key2") == "key2_value3" + assert configuration.get("build", "key3") == "key3_value2" + assert configuration.get("build", "key4") == "key4_value1" + + def test_reload(configuration: Configuration, mocker: MockerFixture) -> None: """ must reload configuration @@ -296,7 +350,7 @@ def test_reload(configuration: Configuration, mocker: MockerFixture) -> None: configuration.reload() load_mock.assert_called_once_with(configuration.path) - merge_mock.assert_called_once_with(configuration.architecture) + merge_mock.assert_called_once_with(configuration.repository_id) def test_reload_clear(configuration: Configuration, mocker: MockerFixture) -> None: @@ -314,7 +368,7 @@ def test_reload_no_architecture(configuration: Configuration) -> None: """ must raise exception on reload if no architecture set """ - configuration.architecture = None + configuration.repository_id = None with pytest.raises(InitializeError): configuration.reload() diff --git a/tests/ahriman/core/database/migrations/test_m001_package_source.py b/tests/ahriman/core/database/migrations/test_m001_package_source.py index 61cca58d..67d79e85 100644 --- a/tests/ahriman/core/database/migrations/test_m001_package_source.py +++ b/tests/ahriman/core/database/migrations/test_m001_package_source.py @@ -2,10 +2,12 @@ import pytest from sqlite3 import Connection from pytest_mock import MockerFixture +from unittest.mock import call as MockCall from ahriman.core.configuration import Configuration from ahriman.core.database.migrations.m001_package_source import migrate_data, migrate_package_remotes, steps from ahriman.models.package import Package +from ahriman.models.package_source import PackageSource from ahriman.models.repository_paths import RepositoryPaths @@ -30,13 +32,18 @@ def test_migrate_package_remotes(package_ahriman: Package, connection: Connectio """ must put package remotes to database """ - mocker.patch( - "ahriman.core.database.operations.PackageOperations._packages_get_select_package_bases", - return_value={package_ahriman.base: package_ahriman}) + connection.execute.return_value = [{ + "package_base": package_ahriman.base, + "version": package_ahriman.version, + "source": PackageSource.AUR, + }] mocker.patch("pathlib.Path.exists", return_value=False) migrate_package_remotes(connection, repository_paths) - connection.execute.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(int)) + connection.execute.assert_has_calls([ + MockCall(pytest.helpers.anyvar(str, strict=True)), + MockCall(pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(int)), + ]) def test_migrate_package_remotes_has_local(package_ahriman: Package, connection: Connection, @@ -44,13 +51,15 @@ def test_migrate_package_remotes_has_local(package_ahriman: Package, connection: """ must skip processing for packages which have local cache """ - mocker.patch( - "ahriman.core.database.operations.PackageOperations._packages_get_select_package_bases", - return_value={package_ahriman.base: package_ahriman}) + connection.execute.return_value = [{ + "package_base": package_ahriman.base, + "version": package_ahriman.version, + "source": PackageSource.AUR, + }] mocker.patch("pathlib.Path.exists", return_value=True) migrate_package_remotes(connection, repository_paths) - connection.execute.assert_not_called() + connection.execute.assert_called_once_with(pytest.helpers.anyvar(str, strict=True)) def test_migrate_package_remotes_vcs(package_ahriman: Package, connection: Connection, @@ -58,11 +67,16 @@ def test_migrate_package_remotes_vcs(package_ahriman: Package, connection: Conne """ must process VCS packages with local cache """ - mocker.patch( - "ahriman.core.database.operations.PackageOperations._packages_get_select_package_bases", - return_value={package_ahriman.base: package_ahriman}) + connection.execute.return_value = [{ + "package_base": package_ahriman.base, + "version": package_ahriman.version, + "source": PackageSource.AUR, + }] mocker.patch("pathlib.Path.exists", return_value=True) mocker.patch.object(Package, "is_vcs", True) migrate_package_remotes(connection, repository_paths) - connection.execute.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(int)) + connection.execute.assert_has_calls([ + MockCall(pytest.helpers.anyvar(str, strict=True)), + MockCall(pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(int)), + ]) diff --git a/tests/ahriman/core/database/migrations/test_m011_repository_name.py b/tests/ahriman/core/database/migrations/test_m011_repository_name.py new file mode 100644 index 00000000..319ccc51 --- /dev/null +++ b/tests/ahriman/core/database/migrations/test_m011_repository_name.py @@ -0,0 +1,39 @@ +import pytest + +from pytest_mock import MockerFixture +from sqlite3 import Connection +from unittest.mock import call as MockCall + +from ahriman.core.configuration import Configuration +from ahriman.core.database.migrations.m011_repository_name import migrate_data, migrate_package_repository, steps + + +def test_migration_check_depends() -> None: + """ + migration must not be empty + """ + assert steps + + +def test_migrate_data(connection: Connection, configuration: Configuration, mocker: MockerFixture) -> None: + """ + must perform data migration + """ + repository_mock = mocker.patch("ahriman.core.database.migrations.m011_repository_name.migrate_package_repository") + migrate_data(connection, configuration) + repository_mock.assert_called_once_with(connection, configuration) + + +def test_migrate_package_repository(connection: Connection, configuration: Configuration) -> None: + """ + must correctly set repository and architecture + """ + migrate_package_repository(connection, configuration) + + connection.execute.assert_has_calls([ + MockCall(pytest.helpers.anyvar(str, strict=True), {"repository": configuration.repository_id.id}), + MockCall(pytest.helpers.anyvar(str, strict=True), {"repository": configuration.repository_id.id}), + MockCall(pytest.helpers.anyvar(str, strict=True), {"repository": configuration.repository_id.id}), + MockCall(pytest.helpers.anyvar(str, strict=True), {"repository": configuration.repository_id.id}), + MockCall(pytest.helpers.anyvar(str, strict=True), {"repository": configuration.repository_id.id}), + ]) diff --git a/tests/ahriman/core/database/operations/test_build_operations.py b/tests/ahriman/core/database/operations/test_build_operations.py index 3e43e93d..e155d379 100644 --- a/tests/ahriman/core/database/operations/test_build_operations.py +++ b/tests/ahriman/core/database/operations/test_build_operations.py @@ -1,5 +1,6 @@ from ahriman.core.database import SQLite from ahriman.models.package import Package +from ahriman.models.repository_id import RepositoryId def test_build_queue_insert_clear(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None: @@ -13,6 +14,19 @@ def test_build_queue_insert_clear(database: SQLite, package_ahriman: Package, pa assert not database.build_queue_get() +def test_build_queue_insert_clear_multi(database: SQLite, package_ahriman: Package) -> None: + """ + must clear all packages from queue for specific repository + """ + database.build_queue_insert(package_ahriman) + database.repository_id = RepositoryId("i686", database.repository_id.name) + database.build_queue_insert(package_ahriman) + + database.build_queue_clear(None) + database.repository_id = RepositoryId("x86_64", database.repository_id.name) + assert database.build_queue_get() == [package_ahriman] + + def test_build_queue_insert_clear_specific(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None: """ @@ -43,3 +57,30 @@ def test_build_queue_insert(database: SQLite, package_ahriman: Package) -> None: package_ahriman.version = "42" database.build_queue_insert(package_ahriman) assert database.build_queue_get() == [package_ahriman] + + +def test_build_queue_insert_multi(database: SQLite, package_ahriman: Package) -> None: + """ + must update build queue in the database for multiple architectures and repositories + """ + package_ahriman.version = "1" + database.build_queue_insert(package_ahriman) + assert database.build_queue_get() == [package_ahriman] + + package_ahriman.version = "2" + database.repository_id = RepositoryId("i686", database.repository_id.name) + database.build_queue_insert(package_ahriman) + assert database.build_queue_get() == [package_ahriman] + + package_ahriman.version = "1" + database.repository_id = RepositoryId("x86_64", database.repository_id.name) + assert database.build_queue_get() == [package_ahriman] + + package_ahriman.version = "3" + database.repository_id = RepositoryId(database.repository_id.architecture, "repo") + database.build_queue_insert(package_ahriman) + assert database.build_queue_get() == [package_ahriman] + + package_ahriman.version = "1" + database.repository_id = RepositoryId(database.repository_id.architecture, "aur-clone") + assert database.build_queue_get() == [package_ahriman] diff --git a/tests/ahriman/core/database/operations/test_logs_operations.py b/tests/ahriman/core/database/operations/test_logs_operations.py index c4802be7..984af42b 100644 --- a/tests/ahriman/core/database/operations/test_logs_operations.py +++ b/tests/ahriman/core/database/operations/test_logs_operations.py @@ -1,12 +1,13 @@ from ahriman.core.database import SQLite from ahriman.models.log_record_id import LogRecordId from ahriman.models.package import Package +from ahriman.models.repository_id import RepositoryId -def test_logs_insert_remove_process(database: SQLite, package_ahriman: Package, +def test_logs_insert_remove_version(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None: """ - must clear process specific package logs + must clear version specific package logs """ database.logs_insert(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1") database.logs_insert(LogRecordId(package_ahriman.base, "2"), 43.0, "message 2") @@ -17,6 +18,20 @@ def test_logs_insert_remove_process(database: SQLite, package_ahriman: Package, assert database.logs_get(package_python_schedule.base) == [(42.0, "message 3")] +def test_logs_insert_remove_multi(database: SQLite, package_ahriman: Package) -> None: + """ + must clear logs for specified repository + """ + database.logs_insert(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1") + database.repository_id = RepositoryId("i686", database.repository_id.name) + database.logs_insert(LogRecordId(package_ahriman.base, "1"), 43.0, "message 2") + + database.logs_remove(package_ahriman.base, None) + assert not database.logs_get(package_ahriman.base) + database.repository_id = RepositoryId("x86_64", database.repository_id.name) + assert database.logs_get(package_ahriman.base) == [(42.0, "message 1")] + + def test_logs_insert_remove_full(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None: """ must clear full package logs @@ -46,3 +61,16 @@ def test_logs_insert_get_pagination(database: SQLite, package_ahriman: Package) database.logs_insert(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1") database.logs_insert(LogRecordId(package_ahriman.base, "1"), 43.0, "message 2") assert database.logs_get(package_ahriman.base, 1, 1) == [(43.0, "message 2")] + + +def test_logs_insert_get_multi(database: SQLite, package_ahriman: Package) -> None: + """ + must insert and get package logs for multiple repositories + """ + database.logs_insert(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1") + database.repository_id = RepositoryId("i686", database.repository_id.name) + database.logs_insert(LogRecordId(package_ahriman.base, "1"), 43.0, "message 2") + + assert database.logs_get(package_ahriman.base) == [(43.0, "message 2")] + database.repository_id = RepositoryId("x86_64", database.repository_id.name) + assert database.logs_get(package_ahriman.base) == [(42.0, "message 1")] diff --git a/tests/ahriman/core/database/operations/test_package_operations.py b/tests/ahriman/core/database/operations/test_package_operations.py index 7ad2cee7..b2ac37fb 100644 --- a/tests/ahriman/core/database/operations/test_package_operations.py +++ b/tests/ahriman/core/database/operations/test_package_operations.py @@ -16,9 +16,13 @@ def test_package_remove_package_base(database: SQLite, connection: Connection) - must remove package base """ database._package_remove_package_base(connection, "package") + args = { + "package_base": "package", + "repository": database.repository_id.id, + } connection.execute.assert_has_calls([ - MockCall(pytest.helpers.anyvar(str, strict=True), {"package_base": "package"}), - MockCall(pytest.helpers.anyvar(str, strict=True), {"package_base": "package"}), + MockCall(pytest.helpers.anyvar(str, strict=True), args), + MockCall(pytest.helpers.anyvar(str, strict=True), args), ]) @@ -28,7 +32,10 @@ def test_package_remove_packages(database: SQLite, connection: Connection, packa """ 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}) + pytest.helpers.anyvar(str, strict=True), { + "package_base": package_ahriman.base, + "repository": database.repository_id.id, + }) connection.executemany.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), []) diff --git a/tests/ahriman/core/database/operations/test_patch_operations.py b/tests/ahriman/core/database/operations/test_patch_operations.py index 4c13e862..c7f2669b 100644 --- a/tests/ahriman/core/database/operations/test_patch_operations.py +++ b/tests/ahriman/core/database/operations/test_patch_operations.py @@ -22,7 +22,7 @@ def test_patches_list(database: SQLite, package_ahriman: Package, package_python database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1")) database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch3")) database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2")) - assert database.patches_list(None, []) == { + assert database.patches_list(None, None) == { package_ahriman.base: [PkgbuildPatch(None, "patch1"), PkgbuildPatch("key", "patch3")], package_python_schedule.base: [PkgbuildPatch(None, "patch2")], } @@ -35,8 +35,8 @@ def test_patches_list_filter(database: SQLite, package_ahriman: Package, package database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1")) database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2")) - assert database.patches_list(package_ahriman.base, []) == {package_ahriman.base: [PkgbuildPatch(None, "patch1")]} - assert database.patches_list(package_python_schedule.base, []) == { + assert database.patches_list(package_ahriman.base, None) == {package_ahriman.base: [PkgbuildPatch(None, "patch1")]} + assert database.patches_list(package_python_schedule.base, None) == { package_python_schedule.base: [PkgbuildPatch(None, "patch2")], } @@ -50,7 +50,7 @@ def test_patches_list_filter_by_variable(database: SQLite, package_ahriman: Pack database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch2")) database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch3")) - assert database.patches_list(None, []) == { + assert database.patches_list(None, None) == { package_ahriman.base: [PkgbuildPatch(None, "patch1"), PkgbuildPatch("key", "patch2")], package_python_schedule.base: [PkgbuildPatch(None, "patch3")], } @@ -65,7 +65,7 @@ def test_patches_insert_remove(database: SQLite, package_ahriman: Package, packa """ database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1")) database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2")) - database.patches_remove(package_ahriman.base, []) + database.patches_remove(package_ahriman.base, None) assert database.patches_get(package_ahriman.base) == [] assert database.patches_get(package_python_schedule.base) == [PkgbuildPatch(None, "patch2")] diff --git a/tests/ahriman/core/gitremote/test_remote_pull.py b/tests/ahriman/core/gitremote/test_remote_pull.py index 8382c70f..219336ab 100644 --- a/tests/ahriman/core/gitremote/test_remote_pull.py +++ b/tests/ahriman/core/gitremote/test_remote_pull.py @@ -16,7 +16,8 @@ def test_repo_clone(configuration: Configuration, mocker: MockerFixture) -> None """ fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch") copy_mock = mocker.patch("ahriman.core.gitremote.remote_pull.RemotePull.repo_copy") - runner = RemotePull(configuration, "x86_64", "gitremote") + _, repository_id = configuration.check_loaded() + runner = RemotePull(repository_id, configuration, "gitremote") runner.repo_clone() fetch_mock.assert_called_once_with(pytest.helpers.anyvar(int), runner.remote_source) @@ -32,7 +33,8 @@ def test_package_copy(configuration: Configuration, package_ahriman: Package, mo ignore_patterns_mock = mocker.patch("shutil.ignore_patterns", return_value=patterns) copytree_mock = mocker.patch("shutil.copytree") init_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.init") - runner = RemotePull(configuration, "x86_64", "gitremote") + _, repository_id = configuration.check_loaded() + runner = RemotePull(repository_id, configuration, "gitremote") local = Path("local") runner.package_copy(local / "PKGBUILD") @@ -57,7 +59,8 @@ def test_repo_copy(configuration: Configuration, mocker: MockerFixture) -> None: local / "package3" / ".SRCINFO", ]) copy_mock = mocker.patch("ahriman.core.gitremote.remote_pull.RemotePull.package_copy") - runner = RemotePull(configuration, "x86_64", "gitremote") + _, repository_id = configuration.check_loaded() + runner = RemotePull(repository_id, configuration, "gitremote") runner.repo_copy(local) copy_mock.assert_has_calls([ @@ -71,7 +74,8 @@ def test_run(configuration: Configuration, mocker: MockerFixture) -> None: must clone repo on run """ clone_mock = mocker.patch("ahriman.core.gitremote.remote_pull.RemotePull.repo_clone") - runner = RemotePull(configuration, "x86_64", "gitremote") + _, repository_id = configuration.check_loaded() + runner = RemotePull(repository_id, configuration, "gitremote") runner.run() clone_mock.assert_called_once_with() @@ -82,7 +86,8 @@ def test_run_failed(configuration: Configuration, mocker: MockerFixture) -> None must reraise exception on error occurred """ mocker.patch("ahriman.core.gitremote.remote_pull.RemotePull.repo_clone", side_effect=Exception()) - runner = RemotePull(configuration, "x86_64", "gitremote") + _, repository_id = configuration.check_loaded() + runner = RemotePull(repository_id, configuration, "gitremote") with pytest.raises(GitRemoteError): runner.run() diff --git a/tests/ahriman/core/gitremote/test_remote_pull_trigger.py b/tests/ahriman/core/gitremote/test_remote_pull_trigger.py index f3c39f02..7e743429 100644 --- a/tests/ahriman/core/gitremote/test_remote_pull_trigger.py +++ b/tests/ahriman/core/gitremote/test_remote_pull_trigger.py @@ -20,7 +20,8 @@ def test_on_start(configuration: Configuration, mocker: MockerFixture) -> None: must clone repo on start """ run_mock = mocker.patch("ahriman.core.gitremote.remote_pull.RemotePull.run") - trigger = RemotePullTrigger("x86_64", configuration) + _, repository_id = configuration.check_loaded() + trigger = RemotePullTrigger(repository_id, configuration) trigger.on_start() run_mock.assert_called_once_with() diff --git a/tests/ahriman/core/gitremote/test_remote_push_trigger.py b/tests/ahriman/core/gitremote/test_remote_push_trigger.py index 15acef38..bfe35173 100644 --- a/tests/ahriman/core/gitremote/test_remote_push_trigger.py +++ b/tests/ahriman/core/gitremote/test_remote_push_trigger.py @@ -26,7 +26,8 @@ def test_on_result(configuration: Configuration, result: Result, package_ahriman """ database_mock = mocker.patch("ahriman.core._Context.get", return_value=database) run_mock = mocker.patch("ahriman.core.gitremote.remote_push.RemotePush.run") - trigger = RemotePushTrigger("x86_64", configuration) + _, repository_id = configuration.check_loaded() + trigger = RemotePushTrigger(repository_id, configuration) trigger.on_result(result, [package_ahriman]) database_mock.assert_called_once_with(ContextKey("database", SQLite)) diff --git a/tests/ahriman/core/http/test_sync_http_client.py b/tests/ahriman/core/http/test_sync_http_client.py index 9154102a..b5565905 100644 --- a/tests/ahriman/core/http/test_sync_http_client.py +++ b/tests/ahriman/core/http/test_sync_http_client.py @@ -22,7 +22,7 @@ def test_init_auth(configuration: Configuration) -> None: configuration.set_option("web", "username", "username") configuration.set_option("web", "password", "password") - assert SyncHttpClient("web", configuration).auth == ("username", "password") + assert SyncHttpClient(configuration, "web").auth == ("username", "password") assert SyncHttpClient(configuration=configuration).auth is None diff --git a/tests/ahriman/core/log/test_log.py b/tests/ahriman/core/log/test_log_loader.py similarity index 72% rename from tests/ahriman/core/log/test_log.py rename to tests/ahriman/core/log/test_log_loader.py index b6652481..4e12eb26 100644 --- a/tests/ahriman/core/log/test_log.py +++ b/tests/ahriman/core/log/test_log_loader.py @@ -7,7 +7,7 @@ from pytest_mock import MockerFixture from systemd.journal import JournalHandler from ahriman.core.configuration import Configuration -from ahriman.core.log import Log +from ahriman.core.log.log_loader import LogLoader from ahriman.models.log_handler import LogHandler @@ -15,14 +15,14 @@ def test_handler() -> None: """ must extract journald handler if available """ - assert Log.handler(None) == LogHandler.Journald + assert LogLoader.handler(None) == LogHandler.Journald def test_handler_selected() -> None: """ must return selected log handler """ - assert Log.handler(LogHandler.Console) == LogHandler.Console + assert LogLoader.handler(LogHandler.Console) == LogHandler.Console def test_handler_syslog(mocker: MockerFixture) -> None: @@ -31,7 +31,7 @@ def test_handler_syslog(mocker: MockerFixture) -> None: """ mocker.patch("pathlib.Path.exists", return_value=True) mocker.patch.dict(sys.modules, {"systemd.journal": None}) - assert Log.handler(None) == LogHandler.Syslog + assert LogLoader.handler(None) == LogHandler.Syslog def test_handler_console(mocker: MockerFixture) -> None: @@ -40,17 +40,17 @@ def test_handler_console(mocker: MockerFixture) -> None: """ mocker.patch("pathlib.Path.exists", return_value=False) mocker.patch.dict(sys.modules, {"systemd.journal": None}) - assert Log.handler(None) == LogHandler.Console + assert LogLoader.handler(None) == LogHandler.Console def test_load(configuration: Configuration, mocker: MockerFixture) -> None: """ must load logging """ - logging_mock = mocker.patch("ahriman.core.log.log.fileConfig", side_effect=fileConfig) + logging_mock = mocker.patch("ahriman.core.log.log_loader.fileConfig", side_effect=fileConfig) http_log_mock = mocker.patch("ahriman.core.log.http_log_handler.HttpLogHandler.load") - Log.load(configuration, LogHandler.Journald, quiet=False, report=False) + LogLoader.load(configuration, LogHandler.Journald, quiet=False, report=False) logging_mock.assert_called_once_with(pytest.helpers.anyvar(int), disable_existing_loggers=True) http_log_mock.assert_called_once_with(configuration, report=False) assert all(isinstance(handler, JournalHandler) for handler in logging.getLogger().handlers) @@ -60,8 +60,8 @@ def test_load_fallback(configuration: Configuration, mocker: MockerFixture) -> N """ must fall back to stderr without errors """ - mocker.patch("ahriman.core.log.log.fileConfig", side_effect=PermissionError()) - Log.load(configuration, LogHandler.Journald, quiet=False, report=False) + mocker.patch("ahriman.core.log.log_loader.fileConfig", side_effect=PermissionError()) + LogLoader.load(configuration, LogHandler.Journald, quiet=False, report=False) def test_load_quiet(configuration: Configuration, mocker: MockerFixture) -> None: @@ -69,5 +69,5 @@ def test_load_quiet(configuration: Configuration, mocker: MockerFixture) -> None must disable logging in case if quiet flag set """ disable_mock = mocker.patch("logging.disable") - Log.load(configuration, LogHandler.Journald, quiet=True, report=False) + LogLoader.load(configuration, LogHandler.Journald, quiet=True, report=False) disable_mock.assert_called_once_with(logging.WARNING) diff --git a/tests/ahriman/core/report/conftest.py b/tests/ahriman/core/report/conftest.py index a2927ba0..8dcc79dd 100644 --- a/tests/ahriman/core/report/conftest.py +++ b/tests/ahriman/core/report/conftest.py @@ -1,7 +1,24 @@ import pytest from ahriman.core.configuration import Configuration +from ahriman.core.report.email import Email from ahriman.core.report.remote_call import RemoteCall +from ahriman.core.report.telegram import Telegram + + +@pytest.fixture +def email(configuration: Configuration) -> Email: + """ + fixture for email trigger + + Args: + configuration(Configuration): configuration fixture + + Returns: + RemoteCall: email trigger test instance + """ + _, repository_id = configuration.check_loaded() + return Email(repository_id, configuration, "email") @pytest.fixture @@ -17,4 +34,20 @@ def remote_call(configuration: Configuration) -> RemoteCall: """ configuration.set_option("web", "host", "localhost") configuration.set_option("web", "port", "8080") - return RemoteCall("x86_64", configuration, "remote-call") + _, repository_id = configuration.check_loaded() + return RemoteCall(repository_id, configuration, "remote-call") + + +@pytest.fixture +def telegram(configuration: Configuration) -> Telegram: + """ + fixture for telegram trigger + + Args: + configuration(Configuration): configuration fixture + + Returns: + RemoteCall: telegram trigger test instance + """ + _, repository_id = configuration.check_loaded() + return Telegram(repository_id, configuration, "telegram") diff --git a/tests/ahriman/core/report/test_console.py b/tests/ahriman/core/report/test_console.py index 55429e74..24edb6bb 100644 --- a/tests/ahriman/core/report/test_console.py +++ b/tests/ahriman/core/report/test_console.py @@ -14,7 +14,8 @@ def test_generate(configuration: Configuration, result: Result, package_python_s """ print_mock = mocker.patch("ahriman.core.formatters.Printer.print") result.add_failed(package_python_schedule) - report = Console("x86_64", configuration, "console") + _, repository_id = configuration.check_loaded() + report = Console(repository_id, configuration, "console") report.generate([], result) print_mock.assert_has_calls([MockCall(verbose=True), MockCall(verbose=True)]) diff --git a/tests/ahriman/core/report/test_email.py b/tests/ahriman/core/report/test_email.py index f215702d..b835df7f 100644 --- a/tests/ahriman/core/report/test_email.py +++ b/tests/ahriman/core/report/test_email.py @@ -8,17 +8,16 @@ from ahriman.models.package import Package from ahriman.models.result import Result -def test_send(configuration: Configuration, mocker: MockerFixture) -> None: +def test_send(email: Email, mocker: MockerFixture) -> None: """ must send an email with attachment """ smtp_mock = mocker.patch("smtplib.SMTP") - report = Email("x86_64", configuration, "email") - report._send("a text", {"attachment.html": "an attachment"}) + email._send("a text", {"attachment.html": "an attachment"}) smtp_mock.return_value.starttls.assert_not_called() smtp_mock.return_value.login.assert_not_called() - smtp_mock.return_value.sendmail.assert_called_once_with(report.sender, report.receivers, pytest.helpers.anyvar(int)) + smtp_mock.return_value.sendmail.assert_called_once_with(email.sender, email.receivers, pytest.helpers.anyvar(int)) smtp_mock.return_value.quit.assert_called_once_with() @@ -29,10 +28,11 @@ def test_send_auth(configuration: Configuration, mocker: MockerFixture) -> None: configuration.set_option("email", "user", "username") configuration.set_option("email", "password", "password") smtp_mock = mocker.patch("smtplib.SMTP") + _, repository_id = configuration.check_loaded() - report = Email("x86_64", configuration, "email") - report._send("a text", {"attachment.html": "an attachment"}) - smtp_mock.return_value.login.assert_called_once_with(report.user, report.password) + email = Email(repository_id, configuration, "email") + email._send("a text", {"attachment.html": "an attachment"}) + smtp_mock.return_value.login.assert_called_once_with(email.user, email.password) def test_send_auth_no_password(configuration: Configuration, mocker: MockerFixture) -> None: @@ -41,9 +41,10 @@ def test_send_auth_no_password(configuration: Configuration, mocker: MockerFixtu """ configuration.set_option("email", "user", "username") smtp_mock = mocker.patch("smtplib.SMTP") + _, repository_id = configuration.check_loaded() - report = Email("x86_64", configuration, "email") - report._send("a text", {"attachment.html": "an attachment"}) + email = Email(repository_id, configuration, "email") + email._send("a text", {"attachment.html": "an attachment"}) smtp_mock.return_value.login.assert_not_called() @@ -53,9 +54,10 @@ def test_send_auth_no_user(configuration: Configuration, mocker: MockerFixture) """ configuration.set_option("email", "password", "password") smtp_mock = mocker.patch("smtplib.SMTP") + _, repository_id = configuration.check_loaded() - report = Email("x86_64", configuration, "email") - report._send("a text", {"attachment.html": "an attachment"}) + email = Email(repository_id, configuration, "email") + email._send("a text", {"attachment.html": "an attachment"}) smtp_mock.return_value.login.assert_not_called() @@ -65,12 +67,13 @@ def test_send_ssl_tls(configuration: Configuration, mocker: MockerFixture) -> No """ configuration.set_option("email", "ssl", "ssl") smtp_mock = mocker.patch("smtplib.SMTP_SSL") + _, repository_id = configuration.check_loaded() - report = Email("x86_64", configuration, "email") - report._send("a text", {"attachment.html": "an attachment"}) + email = Email(repository_id, configuration, "email") + email._send("a text", {"attachment.html": "an attachment"}) smtp_mock.return_value.starttls.assert_not_called() smtp_mock.return_value.login.assert_not_called() - smtp_mock.return_value.sendmail.assert_called_once_with(report.sender, report.receivers, pytest.helpers.anyvar(int)) + smtp_mock.return_value.sendmail.assert_called_once_with(email.sender, email.receivers, pytest.helpers.anyvar(int)) smtp_mock.return_value.quit.assert_called_once_with() @@ -80,48 +83,40 @@ def test_send_starttls(configuration: Configuration, mocker: MockerFixture) -> N """ configuration.set_option("email", "ssl", "starttls") smtp_mock = mocker.patch("smtplib.SMTP") + _, repository_id = configuration.check_loaded() - report = Email("x86_64", configuration, "email") - report._send("a text", {"attachment.html": "an attachment"}) + email = Email(repository_id, configuration, "email") + email._send("a text", {"attachment.html": "an attachment"}) smtp_mock.return_value.starttls.assert_called_once_with() -def test_generate(configuration: Configuration, package_ahriman: Package, mocker: MockerFixture) -> None: +def test_generate(email: Email, package_ahriman: Package, mocker: MockerFixture) -> None: """ must generate report """ send_mock = mocker.patch("ahriman.core.report.email.Email._send") - - report = Email("x86_64", configuration, "email") - report.generate([package_ahriman], Result()) + email.generate([package_ahriman], Result()) send_mock.assert_called_once_with(pytest.helpers.anyvar(int), {}) -def test_generate_with_built(configuration: Configuration, package_ahriman: Package, result: Result, - mocker: MockerFixture) -> None: +def test_generate_with_built(email: Email, package_ahriman: Package, result: Result, mocker: MockerFixture) -> None: """ must generate report with built packages """ send_mock = mocker.patch("ahriman.core.report.email.Email._send") - - report = Email("x86_64", configuration, "email") - report.generate([package_ahriman], result) + email.generate([package_ahriman], result) send_mock.assert_called_once_with(pytest.helpers.anyvar(int), {}) -def test_generate_with_built_and_full_path( - configuration: Configuration, - package_ahriman: Package, - result: Result, - mocker: MockerFixture) -> None: +def test_generate_with_built_and_full_path(email: Email, package_ahriman: Package, result: Result, + mocker: MockerFixture) -> None: """ must generate report with built packages and full packages lists """ send_mock = mocker.patch("ahriman.core.report.email.Email._send") - report = Email("x86_64", configuration, "email") - report.full_template_path = report.template_path - report.generate([package_ahriman], result) + email.full_template_path = email.template_path + email.generate([package_ahriman], result) send_mock.assert_called_once_with(pytest.helpers.anyvar(int), pytest.helpers.anyvar(int)) @@ -131,9 +126,10 @@ def test_generate_no_empty(configuration: Configuration, package_ahriman: Packag """ configuration.set_option("email", "no_empty_report", "yes") send_mock = mocker.patch("ahriman.core.report.email.Email._send") + _, repository_id = configuration.check_loaded() - report = Email("x86_64", configuration, "email") - report.generate([package_ahriman], Result()) + email = Email(repository_id, configuration, "email") + email.generate([package_ahriman], Result()) send_mock.assert_not_called() @@ -144,7 +140,8 @@ def test_generate_no_empty_with_built(configuration: Configuration, package_ahri """ configuration.set_option("email", "no_empty_report", "yes") send_mock = mocker.patch("ahriman.core.report.email.Email._send") + _, repository_id = configuration.check_loaded() - report = Email("x86_64", configuration, "email") - report.generate([package_ahriman], result) + email = Email(repository_id, configuration, "email") + email.generate([package_ahriman], result) send_mock.assert_called_once_with(pytest.helpers.anyvar(int), {}) diff --git a/tests/ahriman/core/report/test_html.py b/tests/ahriman/core/report/test_html.py index fa790b6c..71690118 100644 --- a/tests/ahriman/core/report/test_html.py +++ b/tests/ahriman/core/report/test_html.py @@ -13,7 +13,8 @@ def test_generate(configuration: Configuration, package_ahriman: Package, mocker must generate report """ write_mock = mocker.patch("pathlib.Path.write_text") + _, repository_id = configuration.check_loaded() - report = HTML("x86_64", configuration, "html") + report = HTML(repository_id, configuration, "html") report.generate([package_ahriman], Result()) write_mock.assert_called_once_with(pytest.helpers.anyvar(int), encoding="utf8") diff --git a/tests/ahriman/core/report/test_jinja_template.py b/tests/ahriman/core/report/test_jinja_template.py index 93deccbf..dd735d77 100644 --- a/tests/ahriman/core/report/test_jinja_template.py +++ b/tests/ahriman/core/report/test_jinja_template.py @@ -9,5 +9,6 @@ def test_generate(configuration: Configuration, package_ahriman: Package) -> Non must generate html report """ path = configuration.getpath("html", "template_path") - report = JinjaTemplate("html", configuration) + _, repository_id = configuration.check_loaded() + report = JinjaTemplate(repository_id, configuration, "html") assert report.make_html(Result(success=[package_ahriman]), path) diff --git a/tests/ahriman/core/report/test_report.py b/tests/ahriman/core/report/test_report.py index c47f97a3..7c92317d 100644 --- a/tests/ahriman/core/report/test_report.py +++ b/tests/ahriman/core/report/test_report.py @@ -14,8 +14,10 @@ def test_report_failure(configuration: Configuration, mocker: MockerFixture) -> must raise ReportFailed on errors """ mocker.patch("ahriman.core.report.html.HTML.generate", side_effect=Exception()) + _, repository_id = configuration.check_loaded() + with pytest.raises(ReportError): - Report.load("x86_64", configuration, "html").run(Result(), []) + Report.load(repository_id, configuration, "html").run(Result(), []) def test_report_dummy(configuration: Configuration, result: Result, mocker: MockerFixture) -> None: @@ -24,8 +26,9 @@ def test_report_dummy(configuration: Configuration, result: Result, mocker: Mock """ mocker.patch("ahriman.models.report_settings.ReportSettings.from_option", return_value=ReportSettings.Disabled) report_mock = mocker.patch("ahriman.core.report.report.Report.generate") + _, repository_id = configuration.check_loaded() - Report.load("x86_64", configuration, "disabled").run(result, []) + Report.load(repository_id, configuration, "disabled").run(result, []) report_mock.assert_called_once_with([], result) @@ -34,7 +37,9 @@ def test_report_console(configuration: Configuration, result: Result, mocker: Mo must generate console report """ report_mock = mocker.patch("ahriman.core.report.console.Console.generate") - Report.load("x86_64", configuration, "console").run(result, []) + _, repository_id = configuration.check_loaded() + + Report.load(repository_id, configuration, "console").run(result, []) report_mock.assert_called_once_with([], result) @@ -43,7 +48,9 @@ def test_report_email(configuration: Configuration, result: Result, mocker: Mock must generate email report """ report_mock = mocker.patch("ahriman.core.report.email.Email.generate") - Report.load("x86_64", configuration, "email").run(result, []) + _, repository_id = configuration.check_loaded() + + Report.load(repository_id, configuration, "email").run(result, []) report_mock.assert_called_once_with([], result) @@ -52,7 +59,9 @@ def test_report_html(configuration: Configuration, result: Result, mocker: Mocke must generate html report """ report_mock = mocker.patch("ahriman.core.report.html.HTML.generate") - Report.load("x86_64", configuration, "html").run(result, []) + _, repository_id = configuration.check_loaded() + + Report.load(repository_id, configuration, "html").run(result, []) report_mock.assert_called_once_with([], result) @@ -63,8 +72,9 @@ def test_report_remote_call(configuration: Configuration, result: Result, mocker configuration.set_option("web", "host", "localhost") configuration.set_option("web", "port", "8080") report_mock = mocker.patch("ahriman.core.report.remote_call.RemoteCall.generate") + _, repository_id = configuration.check_loaded() - Report.load("x86_64", configuration, "remote-call").run(result, []) + Report.load(repository_id, configuration, "remote-call").run(result, []) report_mock.assert_called_once_with([], result) @@ -73,5 +83,7 @@ def test_report_telegram(configuration: Configuration, result: Result, mocker: M must generate telegram report """ report_mock = mocker.patch("ahriman.core.report.telegram.Telegram.generate") - Report.load("x86_64", configuration, "telegram").run(result, []) + _, repository_id = configuration.check_loaded() + + Report.load(repository_id, configuration, "telegram").run(result, []) report_mock.assert_called_once_with([], result) diff --git a/tests/ahriman/core/report/test_report_trigger.py b/tests/ahriman/core/report/test_report_trigger.py index 4e1377ba..9e79edc9 100644 --- a/tests/ahriman/core/report/test_report_trigger.py +++ b/tests/ahriman/core/report/test_report_trigger.py @@ -22,7 +22,8 @@ def test_on_result(configuration: Configuration, mocker: MockerFixture) -> None: """ configuration.set_option("report", "target", "email") run_mock = mocker.patch("ahriman.core.report.report.Report.run") + _, repository_id = configuration.check_loaded() - trigger = ReportTrigger("x86_64", configuration) + trigger = ReportTrigger(repository_id, configuration) trigger.on_result(Result(), []) run_mock.assert_called_once_with(Result(), []) diff --git a/tests/ahriman/core/report/test_telegram.py b/tests/ahriman/core/report/test_telegram.py index dc86641b..047824fe 100644 --- a/tests/ahriman/core/report/test_telegram.py +++ b/tests/ahriman/core/report/test_telegram.py @@ -10,67 +10,58 @@ from ahriman.models.package import Package from ahriman.models.result import Result -def test_send(configuration: Configuration, mocker: MockerFixture) -> None: +def test_send(telegram: Telegram, mocker: MockerFixture) -> None: """ must send a message """ request_mock = mocker.patch("ahriman.core.report.telegram.Telegram.make_request") - report = Telegram("x86_64", configuration, "telegram") - report._send("a text") + telegram._send("a text") request_mock.assert_called_once_with( "POST", pytest.helpers.anyvar(str, strict=True), data={"chat_id": pytest.helpers.anyvar(str, strict=True), "text": "a text", "parse_mode": "HTML"}) -def test_send_failed(configuration: Configuration, mocker: MockerFixture) -> None: +def test_send_failed(telegram: Telegram, mocker: MockerFixture) -> None: """ must reraise generic exception """ mocker.patch("requests.Session.request", side_effect=Exception()) - report = Telegram("x86_64", configuration, "telegram") - with pytest.raises(Exception): - report._send("a text") + telegram._send("a text") -def test_send_failed_http_error(configuration: Configuration, mocker: MockerFixture) -> None: +def test_send_failed_http_error(telegram: Telegram, mocker: MockerFixture) -> None: """ must reraise http exception """ mocker.patch("requests.Session.request", side_effect=requests.HTTPError()) - report = Telegram("x86_64", configuration, "telegram") - with pytest.raises(requests.HTTPError): - report._send("a text") + telegram._send("a text") -def test_generate(configuration: Configuration, package_ahriman: Package, result: Result, +def test_generate(telegram: Telegram, package_ahriman: Package, result: Result, mocker: MockerFixture) -> None: """ must generate report """ send_mock = mocker.patch("ahriman.core.report.telegram.Telegram._send") - - report = Telegram("x86_64", configuration, "telegram") - report.generate([package_ahriman], result) + telegram.generate([package_ahriman], result) send_mock.assert_called_once_with(pytest.helpers.anyvar(int)) -def test_generate_big_text_without_spaces(configuration: Configuration, package_ahriman: Package, result: Result, +def test_generate_big_text_without_spaces(telegram: Telegram, package_ahriman: Package, result: Result, mocker: MockerFixture) -> None: """ must raise ValueError in case if there are no new lines in text """ mocker.patch("ahriman.core.report.jinja_template.JinjaTemplate.make_html", return_value="ab" * 4096) - report = Telegram("x86_64", configuration, "telegram") - with pytest.raises(ValueError): - report.generate([package_ahriman], result) + telegram.generate([package_ahriman], result) -def test_generate_big_text(configuration: Configuration, package_ahriman: Package, result: Result, +def test_generate_big_text(telegram: Telegram, package_ahriman: Package, result: Result, mocker: MockerFixture) -> None: """ must generate report with big text @@ -78,14 +69,13 @@ def test_generate_big_text(configuration: Configuration, package_ahriman: Packag mocker.patch("ahriman.core.report.jinja_template.JinjaTemplate.make_html", return_value="a\n" * 4096) send_mock = mocker.patch("ahriman.core.report.telegram.Telegram._send") - report = Telegram("x86_64", configuration, "telegram") - report.generate([package_ahriman], result) + telegram.generate([package_ahriman], result) send_mock.assert_has_calls([ MockCall(pytest.helpers.anyvar(str, strict=True)), MockCall(pytest.helpers.anyvar(str, strict=True)) ]) -def test_generate_very_big_text(configuration: Configuration, package_ahriman: Package, result: Result, +def test_generate_very_big_text(telegram: Telegram, package_ahriman: Package, result: Result, mocker: MockerFixture) -> None: """ must generate report with very big text @@ -93,8 +83,7 @@ def test_generate_very_big_text(configuration: Configuration, package_ahriman: P mocker.patch("ahriman.core.report.jinja_template.JinjaTemplate.make_html", return_value="ab\n" * 4096) send_mock = mocker.patch("ahriman.core.report.telegram.Telegram._send") - report = Telegram("x86_64", configuration, "telegram") - report.generate([package_ahriman], result) + telegram.generate([package_ahriman], result) send_mock.assert_has_calls([ MockCall(pytest.helpers.anyvar(str, strict=True)), MockCall(pytest.helpers.anyvar(str, strict=True)), @@ -102,12 +91,10 @@ def test_generate_very_big_text(configuration: Configuration, package_ahriman: P ]) -def test_generate_no_empty(configuration: Configuration, package_ahriman: Package, mocker: MockerFixture) -> None: +def test_generate_no_empty(telegram: Telegram, package_ahriman: Package, mocker: MockerFixture) -> None: """ must generate report """ send_mock = mocker.patch("ahriman.core.report.telegram.Telegram._send") - - report = Telegram("x86_64", configuration, "telegram") - report.generate([package_ahriman], Result()) + telegram.generate([package_ahriman], Result()) send_mock.assert_not_called() diff --git a/tests/ahriman/core/repository/conftest.py b/tests/ahriman/core/repository/conftest.py index fa602a10..439cf473 100644 --- a/tests/ahriman/core/repository/conftest.py +++ b/tests/ahriman/core/repository/conftest.py @@ -22,7 +22,8 @@ def cleaner(configuration: Configuration, database: SQLite) -> Cleaner: Returns: Cleaner: cleaner test instance """ - return Cleaner("x86_64", configuration, database, report=False, + _, repository_id = configuration.check_loaded() + return Cleaner(repository_id, configuration, database, report=False, refresh_pacman_database=PacmanSynchronization.Disabled) @@ -43,7 +44,8 @@ def executor(configuration: Configuration, database: SQLite, mocker: MockerFixtu mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_chroot") mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_packages") mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_queue") - return Executor("x86_64", configuration, database, report=False, + _, repository_id = configuration.check_loaded() + return Executor(repository_id, configuration, database, report=False, refresh_pacman_database=PacmanSynchronization.Disabled) @@ -64,5 +66,6 @@ def update_handler(configuration: Configuration, database: SQLite, mocker: Mocke mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_chroot") mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_packages") mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_queue") - return UpdateHandler("x86_64", configuration, database, report=False, + _, repository_id = configuration.check_loaded() + return UpdateHandler(repository_id, configuration, database, report=False, refresh_pacman_database=PacmanSynchronization.Disabled) diff --git a/tests/ahriman/core/repository/test_repository.py b/tests/ahriman/core/repository/test_repository.py index 413d2aca..0422e780 100644 --- a/tests/ahriman/core/repository/test_repository.py +++ b/tests/ahriman/core/repository/test_repository.py @@ -20,7 +20,9 @@ def test_load(configuration: Configuration, database: SQLite, mocker: MockerFixt must correctly load instance """ context_mock = mocker.patch("ahriman.core.repository.Repository._set_context") - Repository.load("x86_64", configuration, database, report=False) + _, repository_id = configuration.check_loaded() + + Repository.load(repository_id, configuration, database, report=False) context_mock.assert_called_once_with() @@ -29,8 +31,9 @@ def test_set_context(configuration: Configuration, database: SQLite, mocker: Moc must set context variables """ set_mock = mocker.patch("ahriman.core._Context.set") + _, repository_id = configuration.check_loaded() - instance = Repository.load("x86_64", configuration, database, report=False) + instance = Repository.load(repository_id, configuration, database, report=False) set_mock.assert_has_calls([ MockCall(ContextKey("database", SQLite), instance.database), MockCall(ContextKey("configuration", Configuration), instance.configuration), diff --git a/tests/ahriman/core/repository/test_repository_properties.py b/tests/ahriman/core/repository/test_repository_properties.py index 694e8467..2f6e6bf3 100644 --- a/tests/ahriman/core/repository/test_repository_properties.py +++ b/tests/ahriman/core/repository/test_repository_properties.py @@ -6,6 +6,20 @@ from ahriman.models.user import User from ahriman.models.user_access import UserAccess +def test_architecture(repository: RepositoryProperties) -> None: + """ + must provide repository architecture for backward compatibility + """ + assert repository.architecture == repository.repository_id.architecture + + +def test_name(repository: RepositoryProperties) -> None: + """ + must provide repository name for backward compatibility + """ + assert repository.name == repository.repository_id.name + + def test_packager(repository: RepositoryProperties, mocker: MockerFixture) -> None: """ must extract packager diff --git a/tests/ahriman/core/status/test_watcher.py b/tests/ahriman/core/status/test_watcher.py index 2fb618c8..0444268b 100644 --- a/tests/ahriman/core/status/test_watcher.py +++ b/tests/ahriman/core/status/test_watcher.py @@ -17,9 +17,10 @@ def test_force_no_report(configuration: Configuration, database: SQLite, mocker: """ configuration.set_option("web", "port", "8080") load_mock = mocker.patch("ahriman.core.repository.Repository.load") + _, repository_id = configuration.check_loaded() - Watcher("x86_64", configuration, database) - load_mock.assert_called_once_with("x86_64", configuration, database, report=False) + Watcher(repository_id, configuration, database) + load_mock.assert_called_once_with(repository_id, configuration, database, report=False) def test_load(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None: diff --git a/tests/ahriman/core/support/conftest.py b/tests/ahriman/core/support/conftest.py index 6e85fca9..905b9053 100644 --- a/tests/ahriman/core/support/conftest.py +++ b/tests/ahriman/core/support/conftest.py @@ -16,7 +16,8 @@ def mirrorlist_generator(configuration: Configuration) -> MirrorlistGenerator: Returns: MirrorlistGenerator: mirrorlist pkgbuild generator test instance """ - return MirrorlistGenerator(configuration, "mirrorlist") + _, repository_id = configuration.check_loaded() + return MirrorlistGenerator(repository_id, configuration, "mirrorlist") @pytest.fixture diff --git a/tests/ahriman/core/support/pkgbuild/conftest.py b/tests/ahriman/core/support/pkgbuild/conftest.py index 2851d5a2..e9f1c177 100644 --- a/tests/ahriman/core/support/pkgbuild/conftest.py +++ b/tests/ahriman/core/support/pkgbuild/conftest.py @@ -20,7 +20,8 @@ def keyring_generator(database: SQLite, gpg: GPG, configuration: Configuration) Returns: KeyringGenerator: keyring generator test instance """ - return KeyringGenerator(database, gpg, configuration, "keyring") + _, repository_id = configuration.check_loaded() + return KeyringGenerator(database, gpg, repository_id, configuration, "keyring") @pytest.fixture diff --git a/tests/ahriman/core/support/pkgbuild/test_keyring_generator.py b/tests/ahriman/core/support/pkgbuild/test_keyring_generator.py index 44ba059c..fdb0adf0 100644 --- a/tests/ahriman/core/support/pkgbuild/test_keyring_generator.py +++ b/tests/ahriman/core/support/pkgbuild/test_keyring_generator.py @@ -18,74 +18,87 @@ def test_init_packagers(database: SQLite, gpg: GPG, configuration: Configuration must extract packagers keys """ mocker.patch("ahriman.core.database.SQLite.user_list", return_value=[user]) + _, repository_id = configuration.check_loaded() - assert KeyringGenerator(database, gpg, configuration, "keyring").packagers == ["key"] + assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").packagers == ["key"] configuration.set_option("keyring", "packagers", "key1") - assert KeyringGenerator(database, gpg, configuration, "keyring").packagers == ["key1"] + assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").packagers == ["key1"] def test_init_revoked(database: SQLite, gpg: GPG, configuration: Configuration) -> None: """ must extract revoked keys """ - assert KeyringGenerator(database, gpg, configuration, "keyring").revoked == [] + _, repository_id = configuration.check_loaded() + + assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").revoked == [] configuration.set_option("keyring", "revoked", "key1") - assert KeyringGenerator(database, gpg, configuration, "keyring").revoked == ["key1"] + assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").revoked == ["key1"] def test_init_trusted(database: SQLite, gpg: GPG, configuration: Configuration) -> None: """ must extract trusted keys """ - assert KeyringGenerator(database, gpg, configuration, "keyring").trusted == [] + _, repository_id = configuration.check_loaded() + + assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").trusted == [] gpg.default_key = "key" - assert KeyringGenerator(database, gpg, configuration, "keyring").trusted == ["key"] + assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").trusted == ["key"] configuration.set_option("keyring", "trusted", "key1") - assert KeyringGenerator(database, gpg, configuration, "keyring").trusted == ["key1"] + assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").trusted == ["key1"] def test_license(database: SQLite, gpg: GPG, configuration: Configuration) -> None: """ must generate correct licenses list """ - assert KeyringGenerator(database, gpg, configuration, "keyring").license == ["Unlicense"] + _, repository_id = configuration.check_loaded() + + assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").license == ["Unlicense"] configuration.set_option("keyring", "license", "GPL MPL") - assert KeyringGenerator(database, gpg, configuration, "keyring").license == ["GPL", "MPL"] + assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").license == ["GPL", "MPL"] def test_pkgdesc(database: SQLite, gpg: GPG, configuration: Configuration) -> None: """ must generate correct pkgdesc property """ - assert KeyringGenerator(database, gpg, configuration, "keyring").pkgdesc == "aur-clone PGP keyring" + _, repository_id = configuration.check_loaded() + + assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").pkgdesc == "aur-clone PGP keyring" configuration.set_option("keyring", "description", "description") - assert KeyringGenerator(database, gpg, configuration, "keyring").pkgdesc == "description" + assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").pkgdesc == "description" def test_pkgname(database: SQLite, gpg: GPG, configuration: Configuration) -> None: """ must generate correct pkgname property """ - assert KeyringGenerator(database, gpg, configuration, "keyring").pkgname == "aur-clone-keyring" + _, repository_id = configuration.check_loaded() + + assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").pkgname == "aur-clone-keyring" configuration.set_option("keyring", "package", "keyring") - assert KeyringGenerator(database, gpg, configuration, "keyring").pkgname == "keyring" + assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").pkgname == "keyring" def test_url(database: SQLite, gpg: GPG, configuration: Configuration) -> None: """ must generate correct url property """ - assert KeyringGenerator(database, gpg, configuration, "keyring").url == "" + _, repository_id = configuration.check_loaded() + + assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").url == "" configuration.set_option("keyring", "homepage", "homepage") - assert KeyringGenerator(database, gpg, configuration, "keyring").url == "homepage" + assert KeyringGenerator(database, gpg, repository_id, configuration, "keyring").url == "homepage" def test_generate_gpg(keyring_generator: KeyringGenerator, mocker: MockerFixture) -> None: diff --git a/tests/ahriman/core/support/pkgbuild/test_mirrorlist_generator.py b/tests/ahriman/core/support/pkgbuild/test_mirrorlist_generator.py index 6e5c3553..e1691e6f 100644 --- a/tests/ahriman/core/support/pkgbuild/test_mirrorlist_generator.py +++ b/tests/ahriman/core/support/pkgbuild/test_mirrorlist_generator.py @@ -9,50 +9,62 @@ def test_init_path(configuration: Configuration) -> None: """ must set relative path to mirrorlist """ - assert MirrorlistGenerator(configuration, "mirrorlist").path == Path("etc") / "pacman.d" / "aur-clone-mirrorlist" + _, repository_id = configuration.check_loaded() + + assert MirrorlistGenerator(repository_id, configuration, "mirrorlist").path == \ + Path("etc") / "pacman.d" / "aur-clone-mirrorlist" configuration.set_option("mirrorlist", "path", "/etc") - assert MirrorlistGenerator(configuration, "mirrorlist").path == Path("etc") + assert MirrorlistGenerator(repository_id, configuration, "mirrorlist").path == Path("etc") def test_license(configuration: Configuration) -> None: """ must generate correct licenses list """ - assert MirrorlistGenerator(configuration, "mirrorlist").license == ["Unlicense"] + _, repository_id = configuration.check_loaded() + + assert MirrorlistGenerator(repository_id, configuration, "mirrorlist").license == ["Unlicense"] configuration.set_option("mirrorlist", "license", "GPL MPL") - assert MirrorlistGenerator(configuration, "mirrorlist").license == ["GPL", "MPL"] + assert MirrorlistGenerator(repository_id, configuration, "mirrorlist").license == ["GPL", "MPL"] def test_pkgdesc(configuration: Configuration) -> None: """ must generate correct pkgdesc property """ - assert MirrorlistGenerator(configuration, "mirrorlist").pkgdesc == "aur-clone mirror list for use by pacman" + _, repository_id = configuration.check_loaded() + + assert MirrorlistGenerator(repository_id, configuration, "mirrorlist").pkgdesc == \ + "aur-clone mirror list for use by pacman" configuration.set_option("mirrorlist", "description", "description") - assert MirrorlistGenerator(configuration, "mirrorlist").pkgdesc == "description" + assert MirrorlistGenerator(repository_id, configuration, "mirrorlist").pkgdesc == "description" def test_pkgname(configuration: Configuration) -> None: """ must generate correct pkgname property """ - assert MirrorlistGenerator(configuration, "mirrorlist").pkgname == "aur-clone-mirrorlist" + _, repository_id = configuration.check_loaded() + + assert MirrorlistGenerator(repository_id, configuration, "mirrorlist").pkgname == "aur-clone-mirrorlist" configuration.set_option("mirrorlist", "package", "mirrorlist") - assert MirrorlistGenerator(configuration, "mirrorlist").pkgname == "mirrorlist" + assert MirrorlistGenerator(repository_id, configuration, "mirrorlist").pkgname == "mirrorlist" def test_url(configuration: Configuration) -> None: """ must generate correct url property """ - assert MirrorlistGenerator(configuration, "mirrorlist").url == "" + _, repository_id = configuration.check_loaded() + + assert MirrorlistGenerator(repository_id, configuration, "mirrorlist").url == "" configuration.set_option("mirrorlist", "homepage", "homepage") - assert MirrorlistGenerator(configuration, "mirrorlist").url == "homepage" + assert MirrorlistGenerator(repository_id, configuration, "mirrorlist").url == "homepage" def test_generate_mirrorlist(mirrorlist_generator: MirrorlistGenerator, mocker: MockerFixture) -> None: diff --git a/tests/ahriman/core/support/test_keyring_trigger.py b/tests/ahriman/core/support/test_keyring_trigger.py index cf171ef1..bcc2fbfa 100644 --- a/tests/ahriman/core/support/test_keyring_trigger.py +++ b/tests/ahriman/core/support/test_keyring_trigger.py @@ -25,8 +25,9 @@ def test_on_start(configuration: Configuration, mocker: MockerFixture) -> None: """ context_mock = mocker.patch("ahriman.core._Context.get") run_mock = mocker.patch("ahriman.core.support.package_creator.PackageCreator.run") + _, repository_id = configuration.check_loaded() - trigger = KeyringTrigger("x86_64", configuration) + trigger = KeyringTrigger(repository_id, configuration) trigger.on_start() context_mock.assert_has_calls([MockCall(ContextKey("sign", GPG)), MockCall(ContextKey("database", SQLite))]) run_mock.assert_called_once_with() diff --git a/tests/ahriman/core/support/test_mirrorlist_trigger.py b/tests/ahriman/core/support/test_mirrorlist_trigger.py index e5e79013..7f34faaa 100644 --- a/tests/ahriman/core/support/test_mirrorlist_trigger.py +++ b/tests/ahriman/core/support/test_mirrorlist_trigger.py @@ -20,7 +20,8 @@ def test_on_start(configuration: Configuration, mocker: MockerFixture) -> None: must run report for specified targets """ run_mock = mocker.patch("ahriman.core.support.package_creator.PackageCreator.run") + _, repository_id = configuration.check_loaded() - trigger = MirrorlistTrigger("x86_64", configuration) + trigger = MirrorlistTrigger(repository_id, configuration) trigger.on_start() run_mock.assert_called_once_with() diff --git a/tests/ahriman/core/support/test_package_creator.py b/tests/ahriman/core/support/test_package_creator.py index 6a472e94..61ae71d5 100644 --- a/tests/ahriman/core/support/test_package_creator.py +++ b/tests/ahriman/core/support/test_package_creator.py @@ -7,6 +7,8 @@ from ahriman.core.support.package_creator import PackageCreator from ahriman.models.context_key import ContextKey from ahriman.models.package import Package from ahriman.models.package_description import PackageDescription +from ahriman.models.package_source import PackageSource +from ahriman.models.remote_source import RemoteSource def test_run(package_creator: PackageCreator, database: SQLite, mocker: MockerFixture) -> None: @@ -16,7 +18,7 @@ def test_run(package_creator: PackageCreator, database: SQLite, mocker: MockerFi package = Package( base=package_creator.generator.pkgname, version=package_creator.generator.pkgver, - remote=None, + remote=RemoteSource(source=PackageSource.Local), packages={package_creator.generator.pkgname: PackageDescription()}, ) local_path = package_creator.configuration.repository_paths.cache_for(package_creator.generator.pkgname) diff --git a/tests/ahriman/core/test_spawn.py b/tests/ahriman/core/test_spawn.py index 1608966d..dd137a09 100644 --- a/tests/ahriman/core/test_spawn.py +++ b/tests/ahriman/core/test_spawn.py @@ -20,9 +20,9 @@ def test_process(spawner: Spawn) -> None: callback = MagicMock() callback.return_value = True - spawner.process(callback, args, spawner.architecture, "id", spawner.queue) + spawner.process(callback, args, spawner.repository_id, "id", spawner.queue) - callback.assert_called_once_with(args, spawner.architecture) + callback.assert_called_once_with(args, spawner.repository_id) (uuid, status, time) = spawner.queue.get() assert uuid == "id" assert status @@ -37,7 +37,7 @@ def test_process_error(spawner: Spawn) -> None: callback = MagicMock() callback.return_value = False - spawner.process(callback, MagicMock(), spawner.architecture, "id", spawner.queue) + spawner.process(callback, MagicMock(), spawner.repository_id, "id", spawner.queue) (uuid, status, time) = spawner.queue.get() assert uuid == "id" diff --git a/tests/ahriman/core/triggers/conftest.py b/tests/ahriman/core/triggers/conftest.py index 386136f0..dcfb3483 100644 --- a/tests/ahriman/core/triggers/conftest.py +++ b/tests/ahriman/core/triggers/conftest.py @@ -15,7 +15,8 @@ def trigger(configuration: Configuration) -> Trigger: Returns: Trigger: trigger test instance """ - return Trigger("x86_64", configuration) + _, repository_id = configuration.check_loaded() + return Trigger(repository_id, configuration) @pytest.fixture @@ -28,4 +29,5 @@ def trigger_loader(configuration: Configuration) -> TriggerLoader: Returns: TriggerLoader: trigger loader test instance """ - return TriggerLoader.load("x86_64", configuration) + _, repository_id = configuration.check_loaded() + return TriggerLoader.load(repository_id, configuration) diff --git a/tests/ahriman/core/triggers/test_trigger.py b/tests/ahriman/core/triggers/test_trigger.py index 2a5e832f..57a0ef05 100644 --- a/tests/ahriman/core/triggers/test_trigger.py +++ b/tests/ahriman/core/triggers/test_trigger.py @@ -6,15 +6,23 @@ from ahriman.core.triggers import Trigger from ahriman.models.result import Result +def test_architecture(trigger: Trigger) -> None: + """ + must return repository architecture for backward compatibility + """ + assert trigger.architecture == trigger.repository_id.architecture + + def test_configuration_schema(configuration: Configuration) -> None: """ must return used configuration schema """ section = "console" configuration.set_option("report", "target", section) + _, repository_id = configuration.check_loaded() expected = {section: ReportTrigger.CONFIGURATION_SCHEMA[section]} - assert ReportTrigger.configuration_schema("x86_64", configuration) == expected + assert ReportTrigger.configuration_schema(repository_id, configuration) == expected def test_configuration_schema_no_section(configuration: Configuration) -> None: @@ -23,7 +31,9 @@ def test_configuration_schema_no_section(configuration: Configuration) -> None: """ section = "abracadabra" configuration.set_option("report", "target", section) - assert ReportTrigger.configuration_schema("x86_64", configuration) == {} + _, repository_id = configuration.check_loaded() + + assert ReportTrigger.configuration_schema(repository_id, configuration) == {} def test_configuration_schema_no_schema(configuration: Configuration) -> None: @@ -33,15 +43,17 @@ def test_configuration_schema_no_schema(configuration: Configuration) -> None: section = "abracadabra" configuration.set_option("report", "target", section) configuration.set_option(section, "key", "value") + _, repository_id = configuration.check_loaded() - assert ReportTrigger.configuration_schema("x86_64", configuration) == {} + assert ReportTrigger.configuration_schema(repository_id, configuration) == {} -def test_configuration_schema_empty() -> None: +def test_configuration_schema_empty(configuration: Configuration) -> None: """ must return default schema if no configuration set """ - assert ReportTrigger.configuration_schema("x86_64", None) == ReportTrigger.CONFIGURATION_SCHEMA + _, repository_id = configuration.check_loaded() + assert ReportTrigger.configuration_schema(repository_id, None) == ReportTrigger.CONFIGURATION_SCHEMA def test_configuration_schema_variables(configuration: Configuration) -> None: diff --git a/tests/ahriman/core/triggers/test_trigger_loader.py b/tests/ahriman/core/triggers/test_trigger_loader.py index 187a8451..0ea672fd 100644 --- a/tests/ahriman/core/triggers/test_trigger_loader.py +++ b/tests/ahriman/core/triggers/test_trigger_loader.py @@ -48,8 +48,10 @@ def test_load_trigger_package_error_on_creation(trigger_loader: TriggerLoader, c must raise InvalidException on trigger initialization if any exception is thrown """ mocker.patch("ahriman.core.triggers.trigger.Trigger.__init__", side_effect=Exception()) + _, repository_id = configuration.check_loaded() + with pytest.raises(ExtensionError): - trigger_loader.load_trigger("ahriman.core.report.ReportTrigger", "x86_64", configuration) + trigger_loader.load_trigger("ahriman.core.report.ReportTrigger", repository_id, configuration) def test_load_trigger_class_package(trigger_loader: TriggerLoader) -> None: @@ -155,8 +157,9 @@ def test_on_stop_with_on_start(configuration: Configuration, mocker: MockerFixtu mocker.patch("ahriman.core.upload.UploadTrigger.on_start") mocker.patch("ahriman.core.report.ReportTrigger.on_start") on_stop_mock = mocker.patch("ahriman.core.triggers.trigger_loader.TriggerLoader.on_stop") + _, repository_id = configuration.check_loaded() - trigger_loader = TriggerLoader.load("x86_64", configuration) + trigger_loader = TriggerLoader.load(repository_id, configuration) trigger_loader.on_start() del trigger_loader on_stop_mock.assert_called_once_with() @@ -167,8 +170,9 @@ def test_on_stop_without_on_start(configuration: Configuration, mocker: MockerFi must call not on_stop on exit if on_start wasn't called """ on_stop_mock = mocker.patch("ahriman.core.triggers.trigger_loader.TriggerLoader.on_stop") + _, repository_id = configuration.check_loaded() - trigger_loader = TriggerLoader.load("x86_64", configuration) + trigger_loader = TriggerLoader.load(repository_id, configuration) del trigger_loader on_stop_mock.assert_not_called() diff --git a/tests/ahriman/core/upload/conftest.py b/tests/ahriman/core/upload/conftest.py index f3439b07..4b051825 100644 --- a/tests/ahriman/core/upload/conftest.py +++ b/tests/ahriman/core/upload/conftest.py @@ -4,33 +4,34 @@ from typing import Any from unittest.mock import MagicMock from ahriman.core.configuration import Configuration -from ahriman.core.upload.github import Github +from ahriman.core.upload.github import GitHub from ahriman.core.upload.remote_service import RemoteService from ahriman.core.upload.rsync import Rsync from ahriman.core.upload.s3 import S3 @pytest.fixture -def github(configuration: Configuration) -> Github: +def github(configuration: Configuration) -> GitHub: """ - fixture for github synchronization + fixture for GitHub synchronization Args: configuration(Configuration): configuration fixture Returns: - Github: github test instance + GitHub: GitHub test instance """ - return Github("x86_64", configuration, "github:x86_64") + _, repository_id = configuration.check_loaded() + return GitHub(repository_id, configuration, "github:x86_64") @pytest.fixture def github_release() -> dict[str, Any]: """ - fixture for the github release object + fixture for the GitHub release object Returns: - dict[str, Any]: github test release object + dict[str, Any]: GitHub test release object """ return { "url": "release_url", @@ -59,7 +60,8 @@ def remote_service(configuration: Configuration) -> RemoteService: """ configuration.set_option("web", "host", "localhost") configuration.set_option("web", "port", "8080") - return RemoteService("x86_64", configuration, "remote-service") + _, repository_id = configuration.check_loaded() + return RemoteService(repository_id, configuration, "remote-service") @pytest.fixture @@ -73,7 +75,8 @@ def rsync(configuration: Configuration) -> Rsync: Returns: Rsync: rsync test instance """ - return Rsync("x86_64", configuration, "rsync") + _, repository_id = configuration.check_loaded() + return Rsync(repository_id, configuration, "rsync") @pytest.fixture @@ -87,23 +90,25 @@ def s3(configuration: Configuration) -> S3: Returns: S3: S3 test instance """ - return S3("x86_64", configuration, "customs3") + _, repository_id = configuration.check_loaded() + return S3(repository_id, configuration, "customs3") @pytest.fixture -def s3_remote_objects() -> list[MagicMock]: +def s3_remote_objects(configuration: Configuration) -> list[MagicMock]: """ fixture for boto3 like S3 objects Returns: list[MagicMock]: boto3 like S3 objects test instance """ + _, repository_id = configuration.check_loaded() delete_mock = MagicMock() result = [] for item in ["a", "b", "c"]: s3_object = MagicMock() - s3_object.key = f"x86_64/{item}" + s3_object.key = f"{repository_id.name}/{repository_id.architecture}/{item}" s3_object.e_tag = f"\"{item}\"" s3_object.delete = delete_mock diff --git a/tests/ahriman/core/upload/test_github.py b/tests/ahriman/core/upload/test_github.py index 477f8751..e050b973 100644 --- a/tests/ahriman/core/upload/test_github.py +++ b/tests/ahriman/core/upload/test_github.py @@ -6,34 +6,50 @@ from pytest_mock import MockerFixture from typing import Any from unittest.mock import call as MockCall -from ahriman.core.upload.github import Github +from ahriman.core.configuration import Configuration +from ahriman.core.upload.github import GitHub -def test_asset_remove(github: Github, github_release: dict[str, Any], mocker: MockerFixture) -> None: +def test_github_release_tag(configuration: Configuration) -> None: + """ + must correctly define GitHub release tag + """ + _, repository_id = configuration.check_loaded() + + instance = GitHub(repository_id, configuration, "github:x86_64") + assert instance.github_release_tag == instance.github_release_tag_name == repository_id.architecture + + configuration.set_option("github:x86_64", "use_full_release_name", "yes") + instance = GitHub(repository_id, configuration, "github:x86_64") + assert instance.github_release_tag == f"{repository_id.name}-{repository_id.architecture}" + assert instance.github_release_tag_name == f"{repository_id.name} {repository_id.architecture}" + + +def test_asset_remove(github: GitHub, github_release: dict[str, Any], mocker: MockerFixture) -> None: """ must remove asset from the release """ - request_mock = mocker.patch("ahriman.core.upload.github.Github.make_request") + request_mock = mocker.patch("ahriman.core.upload.github.GitHub.make_request") github.asset_remove(github_release, "asset_name") request_mock.assert_called_once_with("DELETE", "asset_url") -def test_asset_remove_unknown(github: Github, github_release: dict[str, Any], mocker: MockerFixture) -> None: +def test_asset_remove_unknown(github: GitHub, github_release: dict[str, Any], mocker: MockerFixture) -> None: """ must not fail if no asset found """ - request_mock = mocker.patch("ahriman.core.upload.github.Github.make_request") + request_mock = mocker.patch("ahriman.core.upload.github.GitHub.make_request") github.asset_remove(github_release, "unknown_asset_name") request_mock.assert_not_called() -def test_asset_upload(github: Github, github_release: dict[str, Any], mocker: MockerFixture) -> None: +def test_asset_upload(github: GitHub, github_release: dict[str, Any], mocker: MockerFixture) -> None: """ must upload asset to the repository """ mocker.patch("pathlib.Path.open") - request_mock = mocker.patch("ahriman.core.upload.github.Github.make_request") - remove_mock = mocker.patch("ahriman.core.upload.github.Github.asset_remove") + request_mock = mocker.patch("ahriman.core.upload.github.GitHub.make_request") + remove_mock = mocker.patch("ahriman.core.upload.github.GitHub.asset_remove") github.asset_upload(github_release, Path("/root/new.tar.xz")) request_mock.assert_called_once_with("POST", "upload_url", params=[("name", "new.tar.xz")], @@ -42,13 +58,13 @@ def test_asset_upload(github: Github, github_release: dict[str, Any], mocker: Mo remove_mock.assert_not_called() -def test_asset_upload_with_removal(github: Github, github_release: dict[str, Any], mocker: MockerFixture) -> None: +def test_asset_upload_with_removal(github: GitHub, github_release: dict[str, Any], mocker: MockerFixture) -> None: """ must remove existing file before upload """ mocker.patch("pathlib.Path.open") - mocker.patch("ahriman.core.upload.github.Github.make_request") - remove_mock = mocker.patch("ahriman.core.upload.github.Github.asset_remove") + mocker.patch("ahriman.core.upload.github.GitHub.make_request") + remove_mock = mocker.patch("ahriman.core.upload.github.GitHub.asset_remove") github.asset_upload(github_release, Path("asset_name")) github.asset_upload(github_release, Path("/root/asset_name")) @@ -58,14 +74,14 @@ def test_asset_upload_with_removal(github: Github, github_release: dict[str, Any ]) -def test_asset_upload_empty_mimetype(github: Github, github_release: dict[str, Any], mocker: MockerFixture) -> None: +def test_asset_upload_empty_mimetype(github: GitHub, github_release: dict[str, Any], mocker: MockerFixture) -> None: """ must upload asset to the repository with empty mime type if the library cannot guess it """ mocker.patch("pathlib.Path.open") - mocker.patch("ahriman.core.upload.github.Github.asset_remove") + mocker.patch("ahriman.core.upload.github.GitHub.asset_remove") mocker.patch("mimetypes.guess_type", return_value=(None, None)) - request_mock = mocker.patch("ahriman.core.upload.github.Github.make_request") + request_mock = mocker.patch("ahriman.core.upload.github.GitHub.make_request") github.asset_upload(github_release, Path("/root/new.tar.xz")) request_mock.assert_called_once_with("POST", "upload_url", params=[("name", "new.tar.xz")], @@ -73,7 +89,7 @@ def test_asset_upload_empty_mimetype(github: Github, github_release: dict[str, A headers={"Content-Type": "application/octet-stream"}) -def test_get_local_files(github: Github, resource_path_root: Path, mocker: MockerFixture) -> None: +def test_get_local_files(github: GitHub, resource_path_root: Path, mocker: MockerFixture) -> None: """ must get all local files recursively """ @@ -82,29 +98,29 @@ def test_get_local_files(github: Github, resource_path_root: Path, mocker: Mocke walk_mock.assert_called() -def test_files_remove(github: Github, github_release: dict[str, Any], mocker: MockerFixture) -> None: +def test_files_remove(github: GitHub, github_release: dict[str, Any], mocker: MockerFixture) -> None: """ must remove files from the remote """ - remove_mock = mocker.patch("ahriman.core.upload.github.Github.asset_remove") + remove_mock = mocker.patch("ahriman.core.upload.github.GitHub.asset_remove") github.files_remove(github_release, {Path("a"): "a"}, {"a": "a", "b": "b"}) remove_mock.assert_called_once_with(github_release, "b") -def test_files_remove_empty(github: Github, github_release: dict[str, Any], mocker: MockerFixture) -> None: +def test_files_remove_empty(github: GitHub, github_release: dict[str, Any], mocker: MockerFixture) -> None: """ must remove nothing if nothing changed """ - remove_mock = mocker.patch("ahriman.core.upload.github.Github.asset_remove") + remove_mock = mocker.patch("ahriman.core.upload.github.GitHub.asset_remove") github.files_remove(github_release, {Path("a"): "a"}, {"a": "a"}) remove_mock.assert_not_called() -def test_files_upload(github: Github, github_release: dict[str, Any], mocker: MockerFixture) -> None: +def test_files_upload(github: GitHub, github_release: dict[str, Any], mocker: MockerFixture) -> None: """ must upload files to the remote """ - upload_mock = mocker.patch("ahriman.core.upload.github.Github.asset_upload") + upload_mock = mocker.patch("ahriman.core.upload.github.GitHub.asset_upload") github.files_upload(github_release, {Path("a"): "a", Path("b"): "c", Path("c"): "c"}, {"a": "a", "b": "b"}) upload_mock.assert_has_calls([ MockCall(github_release, Path("b")), @@ -112,83 +128,84 @@ def test_files_upload(github: Github, github_release: dict[str, Any], mocker: Mo ]) -def test_files_upload_empty(github: Github, github_release: dict[str, Any], mocker: MockerFixture) -> None: +def test_files_upload_empty(github: GitHub, github_release: dict[str, Any], mocker: MockerFixture) -> None: """ must upload nothing if nothing changed """ - upload_mock = mocker.patch("ahriman.core.upload.github.Github.asset_upload") + upload_mock = mocker.patch("ahriman.core.upload.github.GitHub.asset_upload") github.files_upload(github_release, {Path("a"): "a"}, {"a": "a"}) upload_mock.assert_not_called() -def test_release_create(github: Github, mocker: MockerFixture) -> None: +def test_release_create(github: GitHub, mocker: MockerFixture) -> None: """ must create release """ - request_mock = mocker.patch("ahriman.core.upload.github.Github.make_request") + request_mock = mocker.patch("ahriman.core.upload.github.GitHub.make_request") github.release_create() - request_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), - json={"tag_name": github.architecture, "name": github.architecture}) + request_mock.assert_called_once_with( + "POST", pytest.helpers.anyvar(str, True), + json={"tag_name": github.github_release_tag, "name": github.github_release_tag_name}) -def test_release_get(github: Github, mocker: MockerFixture) -> None: +def test_release_get(github: GitHub, mocker: MockerFixture) -> None: """ must get release """ - request_mock = mocker.patch("ahriman.core.upload.github.Github.make_request") + request_mock = mocker.patch("ahriman.core.upload.github.GitHub.make_request") github.release_get() request_mock.assert_called_once_with("GET", pytest.helpers.anyvar(str, True)) -def test_release_get_empty(github: Github, mocker: MockerFixture) -> None: +def test_release_get_empty(github: GitHub, mocker: MockerFixture) -> None: """ must return nothing in case of 404 status code """ response = requests.Response() response.status_code = 404 - mocker.patch("ahriman.core.upload.github.Github.make_request", + mocker.patch("ahriman.core.upload.github.GitHub.make_request", side_effect=requests.HTTPError(response=response)) assert github.release_get() is None -def test_release_get_exception(github: Github, mocker: MockerFixture) -> None: +def test_release_get_exception(github: GitHub, mocker: MockerFixture) -> None: """ must re-raise non HTTPError exception """ - mocker.patch("ahriman.core.upload.github.Github.make_request", side_effect=Exception()) + mocker.patch("ahriman.core.upload.github.GitHub.make_request", side_effect=Exception()) with pytest.raises(Exception): github.release_get() -def test_release_get_exception_http_error(github: Github, mocker: MockerFixture) -> None: +def test_release_get_exception_http_error(github: GitHub, mocker: MockerFixture) -> None: """ must re-raise HTTPError exception with code differs from 404 """ exception = requests.HTTPError(response=requests.Response()) - mocker.patch("ahriman.core.upload.github.Github.make_request", side_effect=exception) + mocker.patch("ahriman.core.upload.github.GitHub.make_request", side_effect=exception) with pytest.raises(requests.HTTPError): github.release_get() -def test_release_update(github: Github, github_release: dict[str, Any], mocker: MockerFixture) -> None: +def test_release_update(github: GitHub, github_release: dict[str, Any], mocker: MockerFixture) -> None: """ must update release """ - request_mock = mocker.patch("ahriman.core.upload.github.Github.make_request") + request_mock = mocker.patch("ahriman.core.upload.github.GitHub.make_request") github.release_update(github_release, "body") request_mock.assert_called_once_with("POST", "release_url", json={"body": "body"}) -def test_release_sync(github: Github, mocker: MockerFixture) -> None: +def test_release_sync(github: GitHub, mocker: MockerFixture) -> None: """ must run sync command """ - release_get_mock = mocker.patch("ahriman.core.upload.github.Github.release_get", return_value={}) - get_hashes_mock = mocker.patch("ahriman.core.upload.github.Github.get_hashes", return_value={}) - get_local_files_mock = mocker.patch("ahriman.core.upload.github.Github.get_local_files", return_value={}) - files_upload_mock = mocker.patch("ahriman.core.upload.github.Github.files_upload") - files_remove_mock = mocker.patch("ahriman.core.upload.github.Github.files_remove") - release_update_mock = mocker.patch("ahriman.core.upload.github.Github.release_update") + release_get_mock = mocker.patch("ahriman.core.upload.github.GitHub.release_get", return_value={}) + get_hashes_mock = mocker.patch("ahriman.core.upload.github.GitHub.get_hashes", return_value={}) + get_local_files_mock = mocker.patch("ahriman.core.upload.github.GitHub.get_local_files", return_value={}) + files_upload_mock = mocker.patch("ahriman.core.upload.github.GitHub.files_upload") + files_remove_mock = mocker.patch("ahriman.core.upload.github.GitHub.files_remove") + release_update_mock = mocker.patch("ahriman.core.upload.github.GitHub.release_update") github.sync(Path("local"), []) release_get_mock.assert_called_once_with() @@ -199,17 +216,17 @@ def test_release_sync(github: Github, mocker: MockerFixture) -> None: release_update_mock.assert_called_once_with({}, pytest.helpers.anyvar(int)) -def test_release_sync_create_release(github: Github, mocker: MockerFixture) -> None: +def test_release_sync_create_release(github: GitHub, mocker: MockerFixture) -> None: """ must create release in case if it does not exist """ - mocker.patch("ahriman.core.upload.github.Github.release_get", return_value=None) - mocker.patch("ahriman.core.upload.github.Github.get_hashes") - mocker.patch("ahriman.core.upload.github.Github.get_local_files") - mocker.patch("ahriman.core.upload.github.Github.files_upload") - mocker.patch("ahriman.core.upload.github.Github.files_remove") - mocker.patch("ahriman.core.upload.github.Github.release_update") - release_create_mock = mocker.patch("ahriman.core.upload.github.Github.release_create") + mocker.patch("ahriman.core.upload.github.GitHub.release_get", return_value=None) + mocker.patch("ahriman.core.upload.github.GitHub.get_hashes") + mocker.patch("ahriman.core.upload.github.GitHub.get_local_files") + mocker.patch("ahriman.core.upload.github.GitHub.files_upload") + mocker.patch("ahriman.core.upload.github.GitHub.files_remove") + mocker.patch("ahriman.core.upload.github.GitHub.release_update") + release_create_mock = mocker.patch("ahriman.core.upload.github.GitHub.release_create") github.sync(Path("local"), []) release_create_mock.assert_called_once_with() diff --git a/tests/ahriman/core/upload/test_s3.py b/tests/ahriman/core/upload/test_s3.py index 979a1fce..6bd6d4af 100644 --- a/tests/ahriman/core/upload/test_s3.py +++ b/tests/ahriman/core/upload/test_s3.py @@ -3,12 +3,32 @@ from pytest_mock import MockerFixture from typing import Any from unittest.mock import MagicMock, call as MockCall +from ahriman.core.configuration import Configuration from ahriman.core.upload.s3 import S3 +from ahriman.models.repository_paths import RepositoryPaths _chunk_size = 8 * 1024 * 1024 +def test_object_path(configuration: Configuration, mocker: MockerFixture) -> None: + """ + must correctly read object path + """ + _, repository_id = configuration.check_loaded() + + # new-style tree + assert S3(repository_id, configuration, "customs3").object_path == Path("aur-clone/x86_64") + + # legacy tree + mocker.patch.object(RepositoryPaths, "_suffix", Path("x86_64")) + assert S3(repository_id, configuration, "customs3").object_path == Path("x86_64") + + # user defined prefix + configuration.set_option("customs3", "object_path", "local") + assert S3(repository_id, configuration, "customs3").object_path == Path("local") + + def test_calculate_etag_big(resource_path_root: Path) -> None: """ must calculate checksum for path which is more than one chunk @@ -38,12 +58,12 @@ def test_files_remove(s3_remote_objects: list[Any]) -> None: must remove remote objects """ local_files = { - Path(item.key): item.e_tag for item in s3_remote_objects if item.key != "x86_64/a" + Path(item.key): item.e_tag for item in s3_remote_objects if item.key != "aur-clone/x86_64/a" } remote_objects = {Path(item.key): item for item in s3_remote_objects} S3.files_remove(local_files, remote_objects) - remote_objects[Path("x86_64/a")].delete.assert_called_once_with() + remote_objects[Path("aur-clone/x86_64/a")].delete.assert_called_once_with() def test_files_upload(s3: S3, s3_remote_objects: list[Any], mocker: MockerFixture) -> None: @@ -55,9 +75,10 @@ def test_files_upload(s3: S3, s3_remote_objects: list[Any], mocker: MockerFixtur root = Path("path") local_files = { - Path(item.key.replace("a", "d")): item.e_tag.replace("b", "d").replace("\"", "") + Path(item.key[:-1] + item.key[-1].replace("a", "d")): item.e_tag.replace("b", "d").replace("\"", "") for item in s3_remote_objects } + print(local_files) remote_objects = {Path(item.key): item for item in s3_remote_objects} mocker.patch("mimetypes.guess_type", side_effect=mimetype) @@ -67,12 +88,12 @@ def test_files_upload(s3: S3, s3_remote_objects: list[Any], mocker: MockerFixtur upload_mock.upload_file.assert_has_calls( [ MockCall( - Filename=str(root / s3.architecture / "b"), - Key=f"{s3.architecture}/{s3.architecture}/b", + Filename=str(root / s3.object_path / "b"), + Key=f"{s3.object_path}/b", ExtraArgs={"ContentType": "text/html"}), MockCall( - Filename=str(root / s3.architecture / "d"), - Key=f"{s3.architecture}/{s3.architecture}/d", + Filename=str(root / s3.object_path / "d"), + Key=f"{s3.object_path}/d", ExtraArgs=None), ], any_order=True) @@ -91,7 +112,7 @@ def test_get_remote_objects(s3: S3, s3_remote_objects: list[Any]) -> None: """ must generate list of remote objects by calling boto3 function """ - expected = {Path(item.key).relative_to(s3.architecture): item for item in s3_remote_objects} + expected = {Path(item.key).relative_to(s3.object_path): item for item in s3_remote_objects} s3.bucket = MagicMock() s3.bucket.objects.filter.return_value = s3_remote_objects diff --git a/tests/ahriman/core/upload/test_upload.py b/tests/ahriman/core/upload/test_upload.py index 3f996a52..b01666c8 100644 --- a/tests/ahriman/core/upload/test_upload.py +++ b/tests/ahriman/core/upload/test_upload.py @@ -14,8 +14,10 @@ def test_upload_failure(configuration: Configuration, mocker: MockerFixture) -> must raise SyncFailed on errors """ mocker.patch("ahriman.core.upload.rsync.Rsync.sync", side_effect=Exception()) + _, repository_id = configuration.check_loaded() + with pytest.raises(SynchronizationError): - Upload.load("x86_64", configuration, "rsync").run(Path("path"), []) + Upload.load(repository_id, configuration, "rsync").run(Path("path"), []) def test_report_dummy(configuration: Configuration, mocker: MockerFixture) -> None: @@ -24,7 +26,9 @@ def test_report_dummy(configuration: Configuration, mocker: MockerFixture) -> No """ mocker.patch("ahriman.models.upload_settings.UploadSettings.from_option", return_value=UploadSettings.Disabled) upload_mock = mocker.patch("ahriman.core.upload.upload.Upload.sync") - Upload.load("x86_64", configuration, "disabled").run(Path("path"), []) + _, repository_id = configuration.check_loaded() + + Upload.load(repository_id, configuration, "disabled").run(Path("path"), []) upload_mock.assert_called_once_with(Path("path"), []) @@ -33,7 +37,9 @@ def test_upload_rsync(configuration: Configuration, mocker: MockerFixture) -> No must upload via rsync """ upload_mock = mocker.patch("ahriman.core.upload.rsync.Rsync.sync") - Upload.load("x86_64", configuration, "rsync").run(Path("path"), []) + _, repository_id = configuration.check_loaded() + + Upload.load(repository_id, configuration, "rsync").run(Path("path"), []) upload_mock.assert_called_once_with(Path("path"), []) @@ -42,7 +48,9 @@ def test_upload_s3(configuration: Configuration, mocker: MockerFixture) -> None: must upload via s3 """ upload_mock = mocker.patch("ahriman.core.upload.s3.S3.sync") - Upload.load("x86_64", configuration, "customs3").run(Path("path"), []) + _, repository_id = configuration.check_loaded() + + Upload.load(repository_id, configuration, "customs3").run(Path("path"), []) upload_mock.assert_called_once_with(Path("path"), []) @@ -50,8 +58,10 @@ def test_upload_github(configuration: Configuration, mocker: MockerFixture) -> N """ must upload via github """ - upload_mock = mocker.patch("ahriman.core.upload.github.Github.sync") - Upload.load("x86_64", configuration, "github").run(Path("path"), []) + upload_mock = mocker.patch("ahriman.core.upload.github.GitHub.sync") + _, repository_id = configuration.check_loaded() + + Upload.load(repository_id, configuration, "github").run(Path("path"), []) upload_mock.assert_called_once_with(Path("path"), []) @@ -62,6 +72,7 @@ def test_upload_ahriman(configuration: Configuration, mocker: MockerFixture) -> upload_mock = mocker.patch("ahriman.core.upload.remote_service.RemoteService.sync") configuration.set_option("web", "host", "localhost") configuration.set_option("web", "port", "8080") + _, repository_id = configuration.check_loaded() - Upload.load("x86_64", configuration, "remote-service").run(Path("path"), []) + Upload.load(repository_id, configuration, "remote-service").run(Path("path"), []) upload_mock.assert_called_once_with(Path("path"), []) diff --git a/tests/ahriman/core/upload/test_upload_trigger.py b/tests/ahriman/core/upload/test_upload_trigger.py index 06c45400..f49bc02c 100644 --- a/tests/ahriman/core/upload/test_upload_trigger.py +++ b/tests/ahriman/core/upload/test_upload_trigger.py @@ -22,7 +22,8 @@ def test_on_result(configuration: Configuration, mocker: MockerFixture) -> None: """ configuration.set_option("upload", "target", "rsync") run_mock = mocker.patch("ahriman.core.upload.upload.Upload.run") + _, repository_id = configuration.check_loaded() - trigger = UploadTrigger("x86_64", configuration) + trigger = UploadTrigger(repository_id, configuration) trigger.on_result(Result(), []) run_mock.assert_called_once_with(configuration.repository_paths.repository, []) diff --git a/tests/ahriman/models/test_repository_id.py b/tests/ahriman/models/test_repository_id.py new file mode 100644 index 00000000..ebe0c9dc --- /dev/null +++ b/tests/ahriman/models/test_repository_id.py @@ -0,0 +1,40 @@ +import pytest + +from ahriman.models.repository_id import RepositoryId + + +def test_is_empty() -> None: + """ + must check if repository id is empty or not + """ + assert RepositoryId("", "").is_empty + assert RepositoryId("arch", "").is_empty + assert RepositoryId("", "repo").is_empty + assert not RepositoryId("arch", "repo").is_empty + + +def test_id() -> None: + """ + must correctly generate id + """ + assert RepositoryId("", "").id == "-" + assert RepositoryId("arch", "repo").id == "arch-repo" + + +def test_lt() -> None: + """ + must correctly compare instances + """ + assert RepositoryId("x86_64", "a") < RepositoryId("x86_64", "b") + assert RepositoryId("x86_64", "b") > RepositoryId("x86_64", "a") + + assert RepositoryId("i686", "a") < RepositoryId("x86_64", "a") + assert RepositoryId("x86_64", "a") > RepositoryId("i686", "a") + + +def test_lt_invalid() -> None: + """ + must raise ValueError if other is not valid repository id + """ + with pytest.raises(ValueError): + RepositoryId("x86_64", "a") < 42 diff --git a/tests/ahriman/models/test_repository_paths.py b/tests/ahriman/models/test_repository_paths.py index a4d849f2..3ab87d7c 100644 --- a/tests/ahriman/models/test_repository_paths.py +++ b/tests/ahriman/models/test_repository_paths.py @@ -7,6 +7,7 @@ from unittest.mock import MagicMock, call as MockCall from ahriman.core.exceptions import PathError from ahriman.models.package import Package +from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_paths import RepositoryPaths @@ -26,6 +27,30 @@ def _get_owner(root: Path, same: bool) -> Callable[[Path], tuple[int, int]]: return lambda path: root_owner if path == root else non_root_owner +def test_suffix(repository_id: RepositoryId, mocker: MockerFixture) -> None: + """ + must correctly define suffix + """ + is_dir_mock = mocker.patch("pathlib.Path.is_dir", autospec=True) + + is_dir_mock.return_value = True + instance = RepositoryPaths(Path("root"), repository_id) + assert instance._suffix == Path(repository_id.architecture) + + is_dir_mock.return_value = False + instance = RepositoryPaths(Path("root"), repository_id) + assert instance._suffix == Path(repository_id.name) / repository_id.architecture + + is_dir_mock.return_value = True + instance = RepositoryPaths(Path("root"), repository_id, _force_current_tree=True) + assert instance._suffix == Path(repository_id.name) / repository_id.architecture + + is_dir_mock.assert_has_calls([ + MockCall(Path("root") / "repository" / repository_id.architecture), + MockCall(Path("root") / "repository" / repository_id.architecture), + ]) + + def test_root_owner(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: """ must correctly define root directory owner @@ -36,11 +61,129 @@ def test_root_owner(repository_paths: RepositoryPaths, mocker: MockerFixture) -> def test_known_architectures(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: """ - must list available directory paths + must list available directory paths /repository/repo/arch """ + is_dir_mock = mocker.patch("pathlib.Path.is_dir", autospec=True, return_value=True) + iterdir_mock = mocker.patch("pathlib.Path.iterdir", autospec=True, return_value=[Path("i686"), Path("x86_64")]) + + assert repository_paths.known_architectures(repository_paths.root, repository_paths.repository_id.name) == { + "i686", + "x86_64", + } + iterdir_mock.assert_called_once_with(repository_paths._repository_root / repository_paths.repository_id.name) + is_dir_mock.assert_has_calls([ + MockCall(repository_paths._repository_root / repository_paths.repository_id.name), + MockCall(Path("i686")), + MockCall(Path("x86_64")), + ]) + + +def test_known_architectures_legacy(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: + """ + must list available directory paths /repository/arch + """ + def is_dir(path: Path) -> bool: + return path.name != repository_paths.repository_id.name + + is_dir_mock = mocker.patch("pathlib.Path.is_dir", autospec=True, side_effect=is_dir) + iterdir_mock = mocker.patch("pathlib.Path.iterdir", autospec=True, return_value=[Path("i686"), Path("x86_64")]) + + assert repository_paths.known_architectures(repository_paths.root, repository_paths.repository_id.name) == { + "i686", + "x86_64", + } + iterdir_mock.assert_called_once_with(repository_paths._repository_root) + is_dir_mock.assert_has_calls([ + MockCall(repository_paths._repository_root / repository_paths.repository_id.name), + MockCall(repository_paths._repository_root), + MockCall(Path("i686")), + MockCall(Path("x86_64")), + ]) + + +def test_known_architectures_legacy_backward(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: + """ + must list available directory paths /repository/arch in backward compatibility mode + """ + def is_dir(path: Path) -> bool: + return path.name != repository_paths.repository_id.name + + is_dir_mock = mocker.patch("pathlib.Path.is_dir", autospec=True, side_effect=is_dir) + iterdir_mock = mocker.patch("pathlib.Path.iterdir", autospec=True, return_value=[Path("i686"), Path("x86_64")]) + + assert repository_paths.known_architectures(repository_paths.root) == {"i686", "x86_64"} + iterdir_mock.assert_called_once_with(repository_paths._repository_root) + is_dir_mock.assert_has_calls([ + MockCall(repository_paths._repository_root), + MockCall(Path("i686")), + MockCall(Path("x86_64")), + ]) + + +def test_known_architectures_empty(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: + """ + must return empty architectures if tree is not available + """ + mocker.patch("pathlib.Path.is_dir", return_value=False) iterdir_mock = mocker.patch("pathlib.Path.iterdir") - repository_paths.known_architectures(repository_paths.root) - iterdir_mock.assert_called_once_with() + + # new style + assert not repository_paths.known_architectures(repository_paths.root, repository_paths.repository_id.name) + # legacy mode + assert not repository_paths.known_architectures(repository_paths.root) + iterdir_mock.assert_not_called() + + +def test_known_repositories(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: + """ + must list available directory paths /repository/repo + """ + def iterdir(path: Path) -> list[Path]: + if path == repository_paths._repository_root: + return [Path("repo1"), Path("repo2")] + return [Path("x86_64")] + + is_dir_mock = mocker.patch("pathlib.Path.is_dir", autospec=True, return_value=True) + iterdir_mock = mocker.patch("pathlib.Path.iterdir", autospec=True, side_effect=iterdir) + + assert repository_paths.known_repositories(repository_paths.root) == {"repo1", "repo2"} + iterdir_mock.assert_has_calls([ + MockCall(repository_paths._repository_root), + MockCall(Path("repo1")), + MockCall(Path("repo2")), + ]) + is_dir_mock.assert_has_calls([ + MockCall(repository_paths._repository_root), + MockCall(Path("repo1")), + MockCall(Path("x86_64")), + MockCall(Path("repo2")), + MockCall(Path("x86_64")), + ]) + + +def test_known_repositories_legacy(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: + """ + must return empty repository list for legacy tree + """ + def is_dir(path: Path) -> bool: + return path == repository_paths._repository_root + + mocker.patch("pathlib.Path.is_dir", autospec=True, side_effect=is_dir) + iterdir_mock = mocker.patch("pathlib.Path.iterdir", autospec=True, return_value=[Path("i686"), Path("x86_64")]) + + assert not repository_paths.known_repositories(repository_paths.root) + iterdir_mock.assert_called_once_with(repository_paths._repository_root) + + +def test_known_repositories_empty(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: + """ + must return empty repositories if tree is not available + """ + mocker.patch("pathlib.Path.is_dir", return_value=False) + iterdir_mock = mocker.patch("pathlib.Path.iterdir") + + assert not repository_paths.known_repositories(repository_paths.root) + iterdir_mock.assert_not_called() def test_owner(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: @@ -139,19 +282,29 @@ def test_tree_create(repository_paths: RepositoryPaths, mocker: MockerFixture) - prop for prop in dir(repository_paths) if not prop.startswith("_") - and not prop.endswith("_for") - and prop not in ("architecture", - "chown", - "known_architectures", - "owner", + and not callable(getattr(repository_paths, prop)) + and prop not in ("logger_name", + "logger", + "repository_id", "root", - "root_owner", - "tree_clear", - "tree_create") + "root_owner") } mkdir_mock = mocker.patch("pathlib.Path.mkdir") chown_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.chown") + print(paths) + repository_paths.tree_create() mkdir_mock.assert_has_calls([MockCall(mode=0o755, parents=True, exist_ok=True) for _ in paths], any_order=True) chown_mock.assert_has_calls([MockCall(pytest.helpers.anyvar(int)) for _ in paths], any_order=True) + + +def test_tree_create_skip(mocker: MockerFixture) -> None: + """ + must skip tree creation if repository id is not set + """ + mkdir_mock = mocker.patch("pathlib.Path.mkdir") + repository_paths = RepositoryPaths(Path("local"), RepositoryId("", "")) + + repository_paths.tree_create() + mkdir_mock.assert_not_called() diff --git a/tests/ahriman/web/conftest.py b/tests/ahriman/web/conftest.py index 665bbdac..2480a451 100644 --- a/tests/ahriman/web/conftest.py +++ b/tests/ahriman/web/conftest.py @@ -112,7 +112,9 @@ def application(configuration: Configuration, spawner: Spawn, database: SQLite, mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) mocker.patch("aiohttp_apispec.setup_aiohttp_apispec") mocker.patch.object(ahriman.core.auth.helpers, "_has_aiohttp_security", False) - return setup_service("x86_64", configuration, spawner) + _, repository_id = configuration.check_loaded() + + return setup_service(repository_id, configuration, spawner) @pytest.fixture @@ -138,7 +140,8 @@ def application_with_auth(configuration: Configuration, user: User, spawner: Spa mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) mocker.patch("aiohttp_apispec.setup_aiohttp_apispec") mocker.patch.object(ahriman.core.auth.helpers, "_has_aiohttp_security", True) - application = setup_service("x86_64", configuration, spawner) + _, repository_id = configuration.check_loaded() + application = setup_service(repository_id, configuration, spawner) generated = user.hash_password(application["validator"].salt) mocker.patch("ahriman.core.database.SQLite.user_get", return_value=generated) @@ -169,7 +172,9 @@ def application_with_debug(configuration: Configuration, user: User, spawner: Sp mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) mocker.patch("aiohttp_apispec.setup_aiohttp_apispec") mocker.patch.object(ahriman.core.auth.helpers, "_has_aiohttp_security", False) - return setup_service("x86_64", configuration, spawner) + _, repository_id = configuration.check_loaded() + + return setup_service(repository_id, configuration, spawner) @pytest.fixture