Compare commits

..

14 Commits

Author SHA1 Message Date
d98211e5e5 add possibility to run full update
In case if packages are not set from web, the spawner will run full
repository update
2022-10-31 02:41:24 +02:00
b97c8928e1 add daemon subcommand
This command emulates default systemd timer and can be useful in docker
container in order to run 24/7
2022-10-31 01:38:01 +02:00
649df81aa5 implement single-function patches (#69) 2022-10-30 03:11:03 +03:00
ad7cdb7d95 drop ahriman.core.triggers.Trigger.run method
In order to force new triggers to use on_result method, the old method
has been removed. However, default on_result method still checks if the
old method exists and tries to run it
2022-10-19 20:07:31 +03:00
4bb598d2eb fix rtd docs building
Commit 6633766cc3 introduced kw_only
dataclasess which require python 3.10+
2022-10-18 02:24:18 +03:00
f47be6cab0 disallow no values in configuration
This option could lead to missing warnings about missing or invalid
configuration values because code usually expects that values are exists
and not empty unless it is explicitly specified.

However, pacman configuration still requires this option in order to be
able to deal with boolean values
2022-10-18 02:13:01 +03:00
342b3cb652 Add gitremote triggers (#68)
* add gitremote pull trigger

* add push gitremote trigger

* docs update
2022-10-18 01:46:27 +03:00
fc0d8387df extend triggers to on_start and on_stop methods
This commit also replaces old run method to new on_result
2022-09-26 01:22:54 +03:00
e0b0c3caeb add minimal install step to workflow 2022-09-17 14:52:08 +03:00
61969dd682 make sqlite import consistent 2022-09-17 14:32:21 +03:00
e441d93a56 Release 2.2.2 2022-09-17 04:05:06 +03:00
664b6369bb skip architecture list patching in case if any architecture is set 2022-09-17 04:04:28 +03:00
4f6bd29ff4 Release 2.2.1 2022-09-14 04:49:08 +03:00
f6d9ea480a docs update 2022-09-14 04:48:11 +03:00
99 changed files with 1706 additions and 363 deletions

View File

@ -7,6 +7,22 @@ on:
branches: [ master ]
jobs:
run-setup-minimal:
runs-on: ubuntu-latest
container:
image: archlinux:latest
volumes:
- ${{ github.workspace }}:/build
options: --privileged -w /build
steps:
- uses: actions/checkout@v2
- name: setup the minimal service in arch linux container
run: .github/workflows/setup.sh minimal
run-setup:
runs-on: ubuntu-latest

View File

@ -3,6 +3,8 @@
set -ex
[[ $1 = "minimal" ]] && MINIMAL_INSTALL=1
# install dependencies
echo -e '[arcanisrepo]\nServer = http://repo.arcanis.me/$arch\nSigLevel = Never' | tee -a /etc/pacman.conf
# refresh the image
@ -12,12 +14,14 @@ pacman --noconfirm -Sy base-devel devtools git pyalpm python-aur python-passlib
# make dependencies
pacman --noconfirm -Sy python-build python-installer python-wheel
# optional dependencies
# VCS support
pacman --noconfirm -Sy breezy darcs mercurial subversion
# web server
pacman --noconfirm -Sy python-aioauth-client python-aiohttp python-aiohttp-debugtoolbar python-aiohttp-jinja2 python-aiohttp-security python-aiohttp-session python-cryptography python-jinja
# additional features
pacman --noconfirm -Sy gnupg python-boto3 rsync
if [[ -z $MINIMAL_INSTALL ]]; then
# VCS support
pacman --noconfirm -Sy breezy darcs mercurial subversion
# web server
pacman --noconfirm -Sy python-aioauth-client python-aiohttp python-aiohttp-debugtoolbar python-aiohttp-jinja2 python-aiohttp-security python-aiohttp-session python-cryptography python-jinja
# additional features
pacman --noconfirm -Sy gnupg python-boto3 rsync
fi
# create fresh tarball
make VERSION=1.0.0 archlinux # well, it does not really matter which version we will put here
@ -33,14 +37,17 @@ systemd-machine-id-setup
# special thing for the container, because /dev/log interface is not available there
sed -i "s/handlers = syslog_handler/handlers = console_handler/g" /etc/ahriman.ini.d/logging.ini
# initial setup command as root
ahriman -a x86_64 repo-setup --packager "ahriman bot <ahriman@example.com>" --repository "github" --web-port 8080
[[ -z $MINIMAL_INSTALL ]] && WEB_ARGS=("--web-port" "8080")
ahriman -a x86_64 repo-setup --packager "ahriman bot <ahriman@example.com>" --repository "github" "${WEB_ARGS[@]}"
# enable services
systemctl enable ahriman-web@x86_64
systemctl enable ahriman@x86_64.timer
# run web service (detached)
sudo -u ahriman -- ahriman -a x86_64 web &
WEBPID=$!
sleep 15s # wait for the web service activation
if [[ -z $MINIMAL_INSTALL ]]; then
# run web service (detached)
sudo -u ahriman -- ahriman -a x86_64 web &
WEB_PID=$!
sleep 15s # wait for the web service activation
fi
# add the first package
# the build itself does not really work in the container
sudo -u ahriman -- ahriman package-add --now yay
@ -49,4 +56,4 @@ sudo -u ahriman -- ahriman package-add --now yay
# run package check
sudo -u ahriman -- ahriman repo-update
# stop web service lol
kill $WEBPID
[[ -z $WEB_PID ]] || kill $WEB_PID

View File

@ -82,7 +82,8 @@ disable=raw-checker-failed,
fixme,
too-many-arguments,
duplicate-code,
cyclic-import
cyclic-import,
confusing-with-statement,
# Enable the message, report, category or checker with the given id(s). You can

View File

@ -5,7 +5,7 @@ formats: all
build:
os: ubuntu-20.04
tools:
python: "3.9"
python: "3.10"
sphinx:
builder: html

View File

@ -56,4 +56,4 @@ version:
ifndef VERSION
$(error VERSION is required, but not set)
endif
sed -i '/__version__ = .*/s/[^"][^)]*/__version__ = "$(VERSION)"/' src/ahriman/version.py
sed -i 's/^__version__ = .*/__version__ = "$(VERSION)"/' src/ahriman/version.py

View File

@ -1,14 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 5.0.0 (0)
<!-- Generated by graphviz version 5.0.1 (0)
-->
<!-- Title: G Pages: 1 -->
<svg width="13233pt" height="4186pt"
viewBox="0.00 0.00 13232.57 4185.79" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 4181.79)">
<title>G</title><style>.edge>path:hover{stroke-width:8}</style>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-4181.79 13228.57,-4181.79 13228.57,4 -4,4"/>
<polygon fill="white" stroke="none" points="-4,4 -4,-4181.79 13228.57,-4181.79 13228.57,4 -4,4"/>
<!-- ahriman_application_ahriman -->
<g id="node1" class="node">
<title>ahriman_application_ahriman</title><style>.edge>path:hover{stroke-width:8}</style>

Before

Width:  |  Height:  |  Size: 537 KiB

After

Width:  |  Height:  |  Size: 537 KiB

View File

@ -3,7 +3,7 @@
ahriman
.SH SYNOPSIS
.B ahriman
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-report] [-q] [--unsafe] [-V] {aur-search,search,help,help-commands-unsafe,key-import,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,repo-backup,repo-check,check,repo-clean,clean,repo-config,config,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-sync,sync,repo-triggers,repo-update,update,shell,user-add,user-list,user-remove,version,web} ...
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-report] [-q] [--unsafe] [-V] {aur-search,search,help,help-commands-unsafe,key-import,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,patch-set-add,repo-backup,repo-check,check,repo-clean,clean,repo-config,config,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-sync,sync,repo-triggers,repo-update,update,shell,user-add,user-list,user-remove,version,web} ...
.SH DESCRIPTION
ArcH linux ReposItory MANager
@ -71,7 +71,7 @@ remove package status
update package status
.TP
\fBahriman\fR \fI\,patch-add\/\fR
add patch set
add patch for PKGBUILD function
.TP
\fBahriman\fR \fI\,patch-list\/\fR
list patch sets
@ -79,6 +79,9 @@ list patch sets
\fBahriman\fR \fI\,patch-remove\/\fR
remove patch set
.TP
\fBahriman\fR \fI\,patch-set-add\/\fR
add patch set
.TP
\fBahriman\fR \fI\,repo-backup\/\fR
backup repository data
.TP
@ -284,21 +287,25 @@ set status for specified packages. If no packages supplied, service status will
new status
.SH COMMAND \fI\,'ahriman patch-add'\/\fR
usage: ahriman patch-add [-h] [-t TRACK] package
usage: ahriman patch-add [-h] [-p PATCH] package variable
create or update source patches
create or update patched PKGBUILD function or variable
.TP
\fBpackage\fR
path to directory with changed files for patch addition/update
package base
.TP
\fBvariable\fR
PKGBUILD variable or function name. If variable is a function, it must end with ()
.SH OPTIONS \fI\,'ahriman patch-add'\/\fR
.TP
\fB\-t\fR \fI\,TRACK\/\fR, \fB\-\-track\fR \fI\,TRACK\/\fR
files which has to be tracked
\fB\-p\fR \fI\,PATCH\/\fR, \fB\-\-patch\fR \fI\,PATCH\/\fR
path to file which contains function or variable value. If not set, the value will be read from stdin
.SH COMMAND \fI\,'ahriman patch-list'\/\fR
usage: ahriman patch-list [-h] [-e] [package]
usage: ahriman patch-list [-h] [-e] [-v VARIABLE] [package]
list available patches for the package
@ -311,8 +318,12 @@ package base
\fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty
.TP
\fB\-v\fR \fI\,VARIABLE\/\fR, \fB\-\-variable\fR \fI\,VARIABLE\/\fR
if set, show only patches for specified PKGBUILD variables
.SH COMMAND \fI\,'ahriman patch-remove'\/\fR
usage: ahriman patch-remove [-h] package
usage: ahriman patch-remove [-h] [-v VARIABLE] package
remove patches for the package
@ -320,6 +331,26 @@ remove patches for the package
\fBpackage\fR
package base
.SH OPTIONS \fI\,'ahriman patch-remove'\/\fR
.TP
\fB\-v\fR \fI\,VARIABLE\/\fR, \fB\-\-variable\fR \fI\,VARIABLE\/\fR
should be used for single\-function patches in case if you wold like to remove only specified PKGBUILD variables. In case
if not set, it will remove all patches related to the package
.SH COMMAND \fI\,'ahriman patch-set-add'\/\fR
usage: ahriman patch-set-add [-h] [-t TRACK] package
create or update source patches
.TP
\fBpackage\fR
path to directory with changed files for patch addition/update
.SH OPTIONS \fI\,'ahriman patch-set-add'\/\fR
.TP
\fB\-t\fR \fI\,TRACK\/\fR, \fB\-\-track\fR \fI\,TRACK\/\fR
files which has to be tracked
.SH COMMAND \fI\,'ahriman repo-backup'\/\fR
usage: ahriman repo-backup [-h] path
@ -391,7 +422,7 @@ just perform check for packages without rebuild process itself
.TP
\fB\-\-from\-database\fR
read packages from database instead of filesystem. This feature in particular is required in case if you would like to
restore repository from another repository instance. Note however that in order to restore packages you need to have
restore repository from another repository instance. Note, however, that in order to restore packages you need to have
original ahriman instance run with web service and have run repo\-update at least once.
.TP

View File

@ -28,6 +28,14 @@ ahriman.core.database.migrations.m002\_user\_access module
:no-undoc-members:
:show-inheritance:
ahriman.core.database.migrations.m003\_patch\_variables module
--------------------------------------------------------------
.. automodule:: ahriman.core.database.migrations.m003_patch_variables
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -36,6 +36,14 @@ ahriman.core.formatters.package\_printer module
:no-undoc-members:
:show-inheritance:
ahriman.core.formatters.patch\_printer module
---------------------------------------------
.. automodule:: ahriman.core.formatters.patch_printer
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.formatters.printer module
--------------------------------------

View File

@ -0,0 +1,29 @@
ahriman.core.gitremote package
==============================
Submodules
----------
ahriman.core.gitremote.remote\_pull\_trigger module
---------------------------------------------------
.. automodule:: ahriman.core.gitremote.remote_pull_trigger
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.gitremote.remote\_push\_trigger module
---------------------------------------------------
.. automodule:: ahriman.core.gitremote.remote_push_trigger
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: ahriman.core.gitremote
:members:
:no-undoc-members:
:show-inheritance:

View File

@ -12,6 +12,7 @@ Subpackages
ahriman.core.build_tools
ahriman.core.database
ahriman.core.formatters
ahriman.core.gitremote
ahriman.core.report
ahriman.core.repository
ahriman.core.sign

View File

@ -92,6 +92,14 @@ ahriman.models.package\_source module
:no-undoc-members:
:show-inheritance:
ahriman.models.pkgbuild\_patch module
-------------------------------------
.. automodule:: ahriman.models.pkgbuild_patch
:members:
:no-undoc-members:
:show-inheritance:
ahriman.models.property module
------------------------------

View File

@ -35,12 +35,13 @@ This package contains everything which is required for any time of application r
* ``ahriman.core.build_tools`` is a package which provides wrapper for ``devtools`` commands.
* ``ahriman.core.database`` is everything including data and schema migrations for database.
* ``ahriman.core.formatters`` package provides ``Printer`` sub-classes for printing data (e.g. package properties) to stdout which are used by some handlers.
* ``ahriman.core.report`` is a package with reporting classes. Usually it must be called by ``ahriman.core.report.Report.load`` method.
* ``ahriman.core.gitremote`` is a package with remote PKGBUILD triggers. Should not be called directly.
* ``ahriman.core.report`` is a package with reporting triggers. Should not be called directly.
* ``ahriman.core.repository`` contains several traits and base repository (``ahriman.core.repository.Repository`` class) implementation.
* ``ahriman.core.sign`` package provides sign feature (only gpg calls are available).
* ``ahriman.core.status`` contains helpers and watcher class which are required for web application. Reporter must be initialized by using ``ahriman.core.status.client.Client.load`` method.
* ``ahriman.core.triggers`` package contains base trigger classes. Classes from this package must be imported in order to implement user extensions. In fact, ``ahriman.core.report`` and ``ahriman.core.upload`` use this package.
* ``ahriman.core.upload`` package provides sync feature, must be called by ``ahriman.core.upload.Upload.load`` method.
* ``ahriman.core.upload`` package provides sync feature, should not be called directly.
This package also provides some generic functions and classes which may be used by other packages:
@ -202,12 +203,14 @@ In order to configure users there are special commands.
Triggers
^^^^^^^^
Triggers are extensions which can be used in order to perform any actions after the update process. The package provides two default extensions - one is report generation and another one is remote upload feature.
Triggers are extensions which can be used in order to perform any actions on application start, after the update process and, finally, before the application exit. The package provides two default extensions - one is report generation and another one is remote upload feature.
The main idea is to load classes by their full path (e.g. ``ahriman.core.upload.UploadTrigger``) by using ``importlib``: get the last part of the import and treat it as class name, join remain part by ``.`` and interpret as module path, import module and extract attribute from it.
The loaded triggers will be called with ``ahriman.models.result.Result`` and ``List[Packages]`` arguments, which describes the process result and current repository packages respectively. Any exception raised will be suppressed and will generate an exception message in logs.
In addition triggers can implement ``on_start`` and ``on_stop`` actions which will be called on the application start and right before the application exit. The ``on_start`` action is usually being called from handlers directly in order to make sure that no trigger will be run when it is not required (e.g. on user management). As soon as ``on_start`` action is called, the additional flag will be set; ``ahriman.core.triggers.TriggerLoader`` class implements ``__del__`` method in which, if the flag is set, the ``on_stop`` actions will be called.
For more details how to deal with the triggers, refer to :doc:`documentation <triggers>` and modules descriptions.
Remote synchronization

View File

@ -75,6 +75,18 @@ Settings for signing packages or repository. Group name can refer to architectur
* ``key`` - default PGP key, string, required. This key will also be used for database signing if enabled.
* ``key_*`` settings - PGP key which will be used for specific packages, string, optional. For example, if there is ``key_yay`` option the specified key will be used for yay package and default key for others.
``gitremote`` group
-------------------
Remote git source synchronization settings. Unlike ``Upload`` triggers those triggers are used for PKGBUILD synchronization - e.g. fetch from remote repository PKGBUILDs before updating process or pulling updated PKGBUILDs to the remote repository.
Both urls support authorization; to do so you'd need to prefix the url with authorization part, e.g. ``https://key:token@github.com/arcan1s/ahriman.git``. It is highly recommended to use application tokens instead of your user authorization details.
* ``pull_url`` - url of the remote repository from which PKGBUILDs can be pulled before build process, string, required.
* ``pull_branch`` - branch of the remote repository from which PKGBUILDs can be pulled before build process, string, optional, default is ``master``.
* ``push_url`` - url of the remote repository to which PKGBUILDs should be pushed after build process, string, required.
* ``push_branch`` - branch of the remote repository to which PKGBUILDs should be pushed after build process, string, optional, default is ``master``.
``report`` group
----------------

View File

@ -112,14 +112,63 @@ TL;DR
Before using this command you will need to create local directory, put ``PKGBUILD`` there and generate ``.SRCINFO`` by using ``makepkg --printsrcinfo > .SRCINFO`` command. These packages will be stored locally and *will be ignored* during automatic update; in order to update the package you will need to run ``package-add`` command again.
Err, I have remote repository with PKGBUILDs and would like to get versions from there automatically
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
For that purpose you could use ``RemotePullTrigger`` trigger. To do so you will need:
#.
Append ``triggers`` option in ``build`` section with the following line:
.. code-block:: ini
[build]
triggers = ahriman.core.gitremote.RemotePullTrigger
#.
Configure trigger as following:
.. code-block:: ini
[gitremote]
pull_url = https://github.com/username/repository
During the next application run it will fetch repository from the specified url and will try to find packages there which can be used as local sources.
I would like to push PKGBUILDs to the remote repository
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
For that purpose you'd need to use another trigger called ``RemotePushTrigger``. Configure it as following:
#.
Append ``triggers`` option in ``build`` section with the trigger name:
.. code-block:: ini
[build]
triggers = ahriman.core.gitremote.RemotePushTrigger
#.
Configure trigger as following:
.. code-block:: ini
[gitremote]
push_url = https://github.com/username/repository
Unlike ``RemotePullTrigger`` trigger, the ``RemotePushTrigger`` more likely will require authorization. It is highly recommended to use application tokens for that instead of using your password (e.g. for Github you can generate tokens `here <https://github.com/settings/tokens>`_ with scope ``public_repo``). Authorization can be supplied by using authorization part of the url, e.g. ``https://key:token@github.com/username/repository``.
But I just wanted to change PKGBUILD from AUR a bit!
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Well it is supported also.
Well it is supported also. The recommended way is to patch specific function, e.g. by running ``sudo -u ahriman ahriman patch-add ahriman version``. This command will prompt for new value of the PKGBUILD variable ``version``. You can also write it to file and read from it ``sudo -u ahriman ahriman patch-add ahriman version version.patch``.
Alternatively you can create full-diff patches, which are calculated by using ``git diff`` from current PKGBUILD master branch:
#. Clone sources from AUR.
#. Make changes you would like to (e.g. edit ``PKGBUILD``, add external patches).
#. Run ``sudo -u ahriman ahriman patch-add /path/to/local/directory/with/PKGBUILD``.
#. Run ``sudo -u ahriman ahriman patch-set-add /path/to/local/directory/with/PKGBUILD``.
The last command will calculate diff from current tree to the ``HEAD`` and will store it locally. Patches will be applied on any package actions (e.g. it can be used for dependency management).

View File

@ -1,12 +1,45 @@
Triggers
========
The package provides ability to write custom extensions which will be run on (the most) actions, e.g. after updates. By default ahriman provides two types of extensions - reporting and files uploading. Each extension must derive from the ``ahriman.core.triggers.Trigger`` class and implement ``run`` method
The package provides ability to write custom extensions which will be run on (the most) actions, e.g. after updates. By default ahriman provides three types of extensions - reporting, files uploading and PKGBUILD syncronization. Each extension must derive from the ``ahriman.core.triggers.Trigger`` class and should implement at least one of the abstract methods:
* ``on_result`` - trigger action which will be called after build process, the build result and the list of repository packages will be supplied as arguments.
* ``on_start`` - trigger action which will be called right before the start of the application process.
* ``on_stop`` - action which will be called right before the exit.
Note, it isn't required to implement all of those methods (or even one of them), however, it is highly recommended to avoid trigger actions in ``__init__`` method as it will be run on any application start (e.g. even if you are just searching in AUR).
Built-in triggers
-----------------
For the configuration details and settings explanation kindly refer to the :doc:`documentation <configuration>`.
``ahriman.core.gitremote.RemotePullTrigger``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This trigger will be called before any action (``on_start``) and pulls remote PKGBUILD repository locally; after that it copies found PKGBUILDs from the cloned repository to the local cache. It is useful in case if you have patched PGKBUILDs (or even missing in AUR) which you would like to use for package building and, technically, just simplifies the local package building.
In order to update those packages you would need to clone your repository separately, make changes in PKGBUILD (e.g. bump version and update checksums), commit them and push back. On the next ahriman's repository update, it will pull changes you commited and will perform package update.
``ahriman.core.gitremote.RemotePushTrigger``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This trigger will be called right after build process (``on_result``). It will pick PKGBUILDs for the updated packages, pull them (together with any other files) and commit and push changes to remote repository. No real use cases, but the most of user repositories do it.
``ahriman.core.report.ReportTrigger``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Trigger which can be used for reporting. It implements ``on_result`` method and thus being called on each build update and generates report (e.g. html, telegram etc) according to the current settings.
``ahriman.core.upload.UploadTrigger``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This trigger takes build result (``on_result``) and performs syncing of the local packages to the remote mirror (e.g. S3 or just by rsync).
Trigger example
---------------
Lets consider example of reporting trigger (e.g. `slack <https://slack.com/>`_, which provides easy HTTP API for integration triggers).
Lets consider example of reporting trigger (e.g. `slack <https://slack.com/>`_, which provides easy HTTP API for integration triggers).gre
In order to post message to slack we will need a specific trigger url (something like ``https://hooks.slack.com/services/company_id/trigger_id``), channel (e.g. ``#archrepo``) and username (``repo-bot``).
@ -49,12 +82,12 @@ Obviously you can implement the specified method in class, but for guide purpose
self.channel = configuration.get("slack", "channel")
self.username = configuration.get("slack", "username")
def run(self, result, packages):
def on_result(self, result, packages):
notify(result, self.slack_url, self.channel, self.username)
Setup the trigger
-----------------
^^^^^^^^^^^^^^^^^
First, put the trigger in any path it can be exported, e.g. by packing the resource into python package (which will lead to import path as ``package.slack_reporter.SlackReporter``) or just put file somewhere it can be accessed by application (e.g. ``/usr/local/lib/slack_reporter.py.SlackReporter``.
First, put the trigger in any path it can be exported, e.g. by packing the resource into python package (which will lead to import path as ``package.slack_reporter.SlackReporter``) or just put file somewhere it can be accessed by application (e.g. ``/usr/local/lib/slack_reporter.py.SlackReporter``).
After that run application as usual and receive notification in your slack channel.

View File

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

View File

@ -27,7 +27,7 @@
<button id="add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-form" hidden>
<i class="bi bi-plus"></i> add
</button>
<button id="update-btn" class="btn btn-secondary" onclick="updatePackages()" disabled hidden>
<button id="update-btn" class="btn btn-secondary" onclick="updatePackages()" hidden>
<i class="bi bi-play"></i> update
</button>
<button id="remove-btn" class="btn btn-danger" onclick="removePackages()" disabled hidden>

View File

@ -7,7 +7,6 @@
table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table",
() => {
removeButton.prop("disabled", !table.bootstrapTable("getSelections").length);
updateButton.prop("disabled", !table.bootstrapTable("getSelections").length);
});
const architectureBadge = $("#badge-architecture");
@ -16,8 +15,6 @@
const versionBadge = $("#badge-version");
function doPackageAction(uri, packages) {
if (packages.length === 0)
return;
$.ajax({
url: uri,
data: JSON.stringify({packages: packages}),

View File

@ -82,6 +82,7 @@ def _parser() -> argparse.ArgumentParser:
subparsers = parser.add_subparsers(title="command", help="command to run", dest="command", required=True)
_set_aur_search_parser(subparsers)
_set_daemon_parser(subparsers)
_set_help_parser(subparsers)
_set_help_commands_unsafe_parser(subparsers)
_set_key_import_parser(subparsers)
@ -93,6 +94,7 @@ def _parser() -> argparse.ArgumentParser:
_set_patch_add_parser(subparsers)
_set_patch_list_parser(subparsers)
_set_patch_remove_parser(subparsers)
_set_patch_set_add_parser(subparsers)
_set_repo_backup_parser(subparsers)
_set_repo_check_parser(subparsers)
_set_repo_clean_parser(subparsers)
@ -140,6 +142,28 @@ def _set_aur_search_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser
def _set_daemon_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for daemon subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("daemon", help="run application as daemon",
description="start process which periodically will run update process",
formatter_class=_formatter)
parser.add_argument("-i", "--interval", help="interval between runs in seconds", type=int, default=60 * 60 * 12)
parser.add_argument("--no-aur", help="do not check for AUR updates. Implies --no-vcs", action="store_true")
parser.add_argument("--no-local", help="do not check local packages for updates", action="store_true")
parser.add_argument("--no-manual", help="do not include manual updates", action="store_true")
parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true")
parser.set_defaults(handler=handlers.Daemon, dry_run=False, exit_code=False, package=[])
return parser
def _set_help_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for listing help subcommand
@ -320,7 +344,7 @@ def _set_package_status_update_parser(root: SubParserAction) -> argparse.Argumen
def _set_patch_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for new patch subcommand
add parser for new single-function patch subcommand
Args:
root(SubParserAction): subparsers for the commands
@ -328,16 +352,18 @@ def _set_patch_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("patch-add", help="add patch set", description="create or update source patches",
epilog="In order to add a patch set for the package you will need to clone "
"the AUR package manually, add required changes (e.g. external patches, "
"edit PKGBUILD) and run command, e.g. ``ahriman patch path/to/directory``. "
"By default it tracks *.patch and *.diff files, but this behavior can be changed "
"by using --track option",
parser = root.add_parser("patch-add", help="add patch for PKGBUILD function",
description="create or update patched PKGBUILD function or variable",
epilog="Unlike ``patch-set-add``, this function allows to patch only one PKGBUILD f"
"unction, e.g. typing ``ahriman patch-add ahriman version`` it will change the "
"``version`` inside PKGBUILD, typing ``ahriman patch-add ahriman build()`` "
"it will change ``build()`` function inside PKGBUILD",
formatter_class=_formatter)
parser.add_argument("package", help="path to directory with changed files for patch addition/update")
parser.add_argument("-t", "--track", help="files which has to be tracked", action="append",
default=["*.diff", "*.patch"])
parser.add_argument("package", help="package base")
parser.add_argument("variable", help="PKGBUILD variable or function name. If variable is a function, "
"it must end with ()")
parser.add_argument("patch", help="path to file which contains function or variable value. If not set, "
"the value will be read from stdin", type=Path, nargs="?")
parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture=[""], lock=None, no_report=True)
return parser
@ -356,6 +382,8 @@ def _set_patch_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
description="list available patches for the package", formatter_class=_formatter)
parser.add_argument("package", help="package base", nargs="?")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("-v", "--variable", help="if set, show only patches for specified PKGBUILD variables",
action="append")
parser.set_defaults(handler=handlers.Patch, action=Action.List, architecture=[""], lock=None, no_report=True)
return parser
@ -373,10 +401,39 @@ def _set_patch_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser = root.add_parser("patch-remove", help="remove patch set", description="remove patches for the package",
formatter_class=_formatter)
parser.add_argument("package", help="package base")
parser.add_argument("-v", "--variable", help="should be used for single-function patches in case if you wold like "
"to remove only specified PKGBUILD variables. In case if not set, "
"it will remove all patches related to the package",
action="append")
parser.set_defaults(handler=handlers.Patch, action=Action.Remove, architecture=[""], lock=None, no_report=True)
return parser
def _set_patch_set_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for new full-diff patch subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("patch-set-add", help="add patch set", description="create or update source patches",
epilog="In order to add a patch set for the package you will need to clone "
"the AUR package manually, add required changes (e.g. external patches, "
"edit PKGBUILD) and run command, e.g. ``ahriman patch-set-add path/to/directory``. "
"By default it tracks *.patch and *.diff files, but this behavior can be changed "
"by using --track option",
formatter_class=_formatter)
parser.add_argument("package", help="path to directory with changed files for patch addition/update", type=Path)
parser.add_argument("-t", "--track", help="files which has to be tracked", action="append",
default=["*.diff", "*.patch"])
parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture=[""], lock=None, no_report=True,
variable=None)
return parser
def _set_repo_backup_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository backup subcommand
@ -473,7 +530,7 @@ def _set_repo_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("--from-database",
help="read packages from database instead of filesystem. This feature in particular is "
"required in case if you would like to restore repository from another repository "
"instance. Note however that in order to restore packages you need to have original "
"instance. Note, however, that in order to restore packages you need to have original "
"ahriman instance run with web service and have run repo-update at least once.",
action="store_true")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")

View File

@ -49,15 +49,6 @@ class Application(ApplicationPackages, ApplicationRepository):
be used instead.
"""
def _finalize(self, result: Result) -> None:
"""
generate report and sync to remote server
Args:
result(Result): build result
"""
self.repository.process_triggers(result)
def _known_packages(self) -> Set[str]:
"""
load packages from repository and pacman repositories
@ -73,3 +64,26 @@ class Application(ApplicationPackages, ApplicationRepository):
known_packages.update(properties.provides)
known_packages.update(self.repository.pacman.all_packages())
return known_packages
def on_result(self, result: Result) -> None:
"""
generate report and sync to remote server
Args:
result(Result): build result
"""
packages = self.repository.packages()
self.repository.triggers.on_result(result, packages)
def on_start(self) -> None:
"""
run triggers on start of the application
"""
self.repository.triggers.on_start()
def on_stop(self) -> None:
"""
run triggers on stop of the application. Note, however, that in most cases this method should not be called
directly as it will be called after on_start action
"""
self.repository.triggers.on_stop()

View File

@ -37,18 +37,6 @@ class ApplicationPackages(ApplicationProperties):
package control class
"""
def _finalize(self, result: Result) -> None:
"""
generate report and sync to remote server
Args:
result(Result): build result
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def _known_packages(self) -> Set[str]:
"""
load packages from repository and pacman repositories
@ -61,6 +49,18 @@ class ApplicationPackages(ApplicationProperties):
"""
raise NotImplementedError
def on_result(self, result: Result) -> None:
"""
generate report and sync to remote server
Args:
result(Result): build result
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def _add_archive(self, source: str, *_: Any) -> None:
"""
add package from archive
@ -86,8 +86,7 @@ class ApplicationPackages(ApplicationProperties):
self.database.build_queue_insert(package)
self.database.remote_update(package)
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, \
(local_dir := Path(dir_name)): # pylint: disable=confusing-with-statement
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (local_dir := Path(dir_name)):
Sources.load(local_dir, package, self.database.patches_get(package.base), self.repository.paths)
self._process_dependencies(local_dir, known_packages, without_dependencies)
@ -187,4 +186,4 @@ class ApplicationPackages(ApplicationProperties):
names(Iterable[str]): list of packages (either base or name) to remove
"""
self.repository.process_remove(names)
self._finalize(Result())
self.on_result(Result())

View File

@ -35,7 +35,7 @@ class ApplicationRepository(ApplicationProperties):
repository control class
"""
def _finalize(self, result: Result) -> None:
def on_result(self, result: Result) -> None:
"""
generate report and sync to remote server
@ -89,7 +89,7 @@ class ApplicationRepository(ApplicationProperties):
self.update([])
# sign repository database if set
self.repository.sign.process_sign_repository(self.repository.repo.repo_path)
self._finalize(Result())
self.on_result(Result())
def unknown(self) -> List[str]:
"""
@ -139,7 +139,7 @@ class ApplicationRepository(ApplicationProperties):
if not paths:
return # don't need to process if no update supplied
update_result = self.repository.process_update(paths)
self._finalize(result.merge(update_result))
self.on_result(result.merge(update_result))
# process built packages
build_result = Result()

View File

@ -22,6 +22,7 @@ from ahriman.application.handlers.handler import Handler
from ahriman.application.handlers.add import Add
from ahriman.application.handlers.backup import Backup
from ahriman.application.handlers.clean import Clean
from ahriman.application.handlers.daemon import Daemon
from ahriman.application.handlers.dump import Dump
from ahriman.application.handlers.help import Help
from ahriman.application.handlers.key_import import KeyImport

View File

@ -45,6 +45,7 @@ class Add(Handler):
unsafe(bool): if set no user check will be performed before path creation
"""
application = Application(architecture, configuration, no_report, unsafe)
application.on_start()
application.add(args.package, args.source, args.without_dependencies)
if not args.now:
return

View File

@ -44,5 +44,6 @@ class Clean(Handler):
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
Application(architecture, configuration, no_report, unsafe).clean(
args.cache, args.chroot, args.manual, args.packages)
application = Application(architecture, configuration, no_report, unsafe)
application.on_start()
application.clean(args.cache, args.chroot, args.manual, args.packages)

View File

@ -0,0 +1,51 @@
#
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
import threading
from typing import Type
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
class Daemon(Handler):
"""
daemon packages handler
"""
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
from ahriman.application.handlers import Update
Update.run(args, architecture, configuration, no_report, unsafe)
timer = threading.Timer(args.interval, Daemon.run, (args, architecture, configuration, no_report, unsafe))
timer.start()
timer.join()

View File

@ -46,5 +46,5 @@ class KeyImport(Handler):
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
Application(architecture, configuration, no_report, unsafe).repository.sign.key_import(
args.key_server, args.key)
application = Application(architecture, configuration, no_report, unsafe)
application.repository.sign.key_import(args.key_server, args.key)

View File

@ -18,17 +18,19 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
import sys
from pathlib import Path
from typing import List, Optional, Type
from typing import List, Optional, Tuple, Type
from ahriman.application.application import Application
from ahriman.application.handlers import Handler
from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.core.formatters import StringPrinter
from ahriman.core.formatters import PatchPrinter
from ahriman.models.action import Action
from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
class Patch(Handler):
@ -50,52 +52,95 @@ class Patch(Handler):
unsafe(bool): if set no user check will be performed before path creation
"""
application = Application(architecture, configuration, no_report, unsafe)
application.on_start()
if args.action == Action.List:
Patch.patch_set_list(application, args.package, args.exit_code)
if args.action == Action.Update and args.variable is not None:
patch = Patch.patch_create_from_function(args.variable, args.patch)
Patch.patch_set_create(application, args.package, patch)
elif args.action == Action.Update and args.variable is None:
package_base, patch = Patch.patch_create_from_diff(args.package, args.track)
Patch.patch_set_create(application, package_base, patch)
elif args.action == Action.List:
Patch.patch_set_list(application, args.package, args.variable, args.exit_code)
elif args.action == Action.Remove:
Patch.patch_set_remove(application, args.package)
elif args.action == Action.Update:
Patch.patch_set_create(application, Path(args.package), args.track)
Patch.patch_set_remove(application, args.package, args.variable)
@staticmethod
def patch_set_create(application: Application, sources_dir: Path, track: List[str]) -> None:
def patch_create_from_diff(sources_dir: Path, track: List[str]) -> Tuple[str, PkgbuildPatch]:
"""
create PKGBUILD plain diff patches from sources directory
Args:
sources_dir(Path): path to directory with the package sources
track(List[str]): track files which match the glob before creating the patch
Returns:
Tuple[str, PkgbuildPatch]: package base and created PKGBUILD patch based on the diff from master HEAD
to current files
"""
package = Package.from_build(sources_dir)
patch = Sources.patch_create(sources_dir, *track)
return package.base, PkgbuildPatch(None, patch)
@staticmethod
def patch_create_from_function(variable: str, patch_path: Optional[Path]) -> PkgbuildPatch:
"""
create single-function patch set for the package base
Args:
variable(str): function or variable name inside PKGBUILD
patch_path(Path): optional path to patch content. If not set, it will be read from stdin
Returns:
PkgbuildPatch: created patch for the PKGBUILD function
"""
if patch_path is None:
print("Post new function or variable value below. Press Ctrl-D to finish:", file=sys.stderr)
patch = "".join(list(sys.stdin))
else:
patch = patch_path.read_text(encoding="utf8")
patch = patch.strip() # remove spaces around the patch
return PkgbuildPatch(variable, patch)
@staticmethod
def patch_set_create(application: Application, package_base: str, patch: PkgbuildPatch) -> None:
"""
create patch set for the package base
Args:
application(Application): application instance
sources_dir(Path): path to directory with the package sources
track(List[str]): track files which match the glob before creating the patch
package_base(str): package base
patch(PkgbuildPatch): patch descriptor
"""
package = Package.from_build(sources_dir)
patch = Sources.patch_create(sources_dir, *track)
application.database.patches_insert(package.base, patch)
application.database.patches_insert(package_base, patch)
@staticmethod
def patch_set_list(application: Application, package_base: Optional[str], exit_code: bool) -> None:
def patch_set_list(application: Application, package_base: Optional[str], variables: List[str],
exit_code: bool) -> None:
"""
list patches available for the package base
Args:
application(Application): application instance
package_base(Optional[str]): package base
variables(List[str]): extract patches only for specified PKGBUILD variables
exit_code(bool): exit with error on empty search result
:
"""
patches = application.database.patches_list(package_base)
patches = application.database.patches_list(package_base, variables)
Patch.check_if_empty(exit_code, not patches)
for base, patch in patches.items():
content = base if package_base is None else patch
StringPrinter(content).print(verbose=True)
PatchPrinter(base, patch).print(verbose=True, separator=" = ")
@staticmethod
def patch_set_remove(application: Application, package_base: str) -> None:
def patch_set_remove(application: Application, package_base: str, variables: List[str]) -> None:
"""
remove patch set for the package base
Args:
application(Application): application instance
package_base(str): package base
variables(List[str]): remove patches only for specified PKGBUILD variables
"""
application.database.patches_remove(package_base)
application.database.patches_remove(package_base, variables)

View File

@ -49,6 +49,8 @@ class Rebuild(Handler):
depends_on = set(args.depends_on) if args.depends_on else None
application = Application(architecture, configuration, no_report, unsafe)
application.on_start()
if args.from_database:
updates = Rebuild.extract_packages(application)
else:

View File

@ -44,4 +44,6 @@ class Remove(Handler):
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
Application(architecture, configuration, no_report, unsafe).remove(args.package)
application = Application(architecture, configuration, no_report, unsafe)
application.on_start()
application.remove(args.package)

View File

@ -46,6 +46,7 @@ class RemoveUnknown(Handler):
unsafe(bool): if set no user check will be performed before path creation
"""
application = Application(architecture, configuration, no_report, unsafe)
application.on_start()
unknown_packages = application.unknown()
if args.dry_run:

View File

@ -133,7 +133,9 @@ class Setup(Handler):
repository(str): repository name
paths(RepositoryPaths): repository paths instance
"""
configuration = Configuration()
# allow_no_value=True is required because pacman uses boolean configuration in which just keys present
# (e.g. NoProgressBar) which will lead to exception
configuration = Configuration(allow_no_value=True)
# preserve case
# stupid mypy thinks that it is impossible
configuration.optionxform = lambda key: key # type: ignore

View File

@ -49,4 +49,5 @@ class Triggers(Handler):
if args.trigger:
loader = application.repository.triggers
loader.triggers = [loader.load_trigger(trigger) for trigger in args.trigger]
application.repository.process_triggers(Result())
application.on_start()
application.on_result(Result())

View File

@ -45,6 +45,7 @@ class Update(Handler):
unsafe(bool): if set no user check will be performed before path creation
"""
application = Application(architecture, configuration, no_report, unsafe)
application.on_start()
packages = application.updates(args.package, args.no_aur, args.no_local, args.no_manual, args.no_vcs,
Update.log_fn(application, args.dry_run))
Update.check_if_empty(args.exit_code, not packages)

View File

@ -17,6 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import datetime
import shutil
from pathlib import Path
@ -44,22 +45,22 @@ class Sources(LazyLogging):
_check_output = check_output
@staticmethod
def extend_architectures(sources_dir: Path, architecture: str) -> None:
def extend_architectures(sources_dir: Path, architecture: str) -> List[PkgbuildPatch]:
"""
extend existing PKGBUILD with repository architecture
Args:
sources_dir(Path): local path to directory with source files
architecture(str): repository architecture
"""
pkgbuild_path = sources_dir / "PKGBUILD"
if not pkgbuild_path.is_file():
return
Returns:
List[PkgbuildPatch]: generated patch for PKGBUILD architectures if required
"""
architectures = Package.supported_architectures(sources_dir)
if "any" in architectures: # makepkg does not like when there is any other arch except for any
return []
architectures.add(architecture)
patch = PkgbuildPatch("arch", list(architectures))
patch.write(pkgbuild_path)
return [PkgbuildPatch("arch", list(architectures))]
@staticmethod
def fetch(sources_dir: Path, remote: Optional[RemoteSource]) -> None:
@ -131,14 +132,14 @@ class Sources(LazyLogging):
exception=None, cwd=sources_dir, logger=instance.logger)
@staticmethod
def load(sources_dir: Path, package: Package, patch: Optional[str], paths: RepositoryPaths) -> None:
def load(sources_dir: Path, package: Package, patches: List[PkgbuildPatch], paths: RepositoryPaths) -> None:
"""
fetch sources from remote and apply patches
Args:
sources_dir(Path): local path to fetch
package(Package): package definitions
patch(Optional[str]): optional patch to be applied
patches(List[PkgbuildPatch]): optional patch to be applied
paths(RepositoryPaths): repository paths instance
"""
instance = Sources()
@ -147,9 +148,9 @@ class Sources(LazyLogging):
shutil.copytree(cache_dir, sources_dir, dirs_exist_ok=True)
instance.fetch(sources_dir, package.remote)
if patch is not None:
patches.extend(instance.extend_architectures(sources_dir, paths.architecture))
for patch in patches:
instance.patch_apply(sources_dir, patch)
instance.extend_architectures(sources_dir, paths.architecture)
@staticmethod
def patch_create(sources_dir: Path, *pattern: str) -> str:
@ -168,6 +169,22 @@ class Sources(LazyLogging):
diff = instance.diff(sources_dir)
return f"{diff}\n" # otherwise, patch will be broken
@staticmethod
def push(sources_dir: Path, remote: RemoteSource, *pattern: str) -> None:
"""
commit selected changes and push files to the remote repository
Args:
sources_dir(Path): local path to git repository
remote(RemoteSource): remote target, branch and url
*pattern(str): glob patterns
"""
instance = Sources()
instance.add(sources_dir, *pattern)
instance.commit(sources_dir)
Sources._check_output("git", "push", remote.git_url, remote.branch,
exception=None, cwd=sources_dir, logger=instance.logger)
def add(self, sources_dir: Path, *pattern: str) -> None:
"""
track found files via git
@ -188,6 +205,20 @@ class Sources(LazyLogging):
*[str(fn.relative_to(sources_dir)) for fn in found_files],
exception=None, cwd=sources_dir, logger=self.logger)
def commit(self, sources_dir: Path, commit_message: Optional[str] = None) -> None:
"""
commit changes
Args:
sources_dir(Path): local path to git repository
commit_message(Optional[str]): optional commit message if any. If none set, message will be generated
according to the current timestamp
"""
if commit_message is None:
commit_message = f"Autogenerated commit at {datetime.datetime.utcnow()}"
Sources._check_output("git", "commit", "--all", "--message", commit_message,
exception=None, cwd=sources_dir, logger=self.logger)
def diff(self, sources_dir: Path) -> str:
"""
generate diff from the current version and write it to the output file
@ -215,15 +246,18 @@ class Sources(LazyLogging):
dst = sources_dir / src.relative_to(pkgbuild_dir)
shutil.move(src, dst)
def patch_apply(self, sources_dir: Path, patch: str) -> None:
def patch_apply(self, sources_dir: Path, patch: PkgbuildPatch) -> None:
"""
apply patches if any
Args:
sources_dir(Path): local path to directory with git sources
patch(str): patch to be applied
patch(PkgbuildPatch): patch to be applied
"""
# create patch
self.logger.info("apply patch from database")
Sources._check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace",
exception=None, cwd=sources_dir, input_data=patch, logger=self.logger)
self.logger.info("apply patch %s from database at %s", patch.key, sources_dir)
if patch.is_plain_diff:
Sources._check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace",
exception=None, cwd=sources_dir, input_data=patch.serialize(), logger=self.logger)
else:
patch.write(sources_dir / "PKGBUILD")

View File

@ -70,11 +70,15 @@ class Configuration(configparser.RawConfigParser):
ARCHITECTURE_SPECIFIC_SECTIONS = ["build", "sign", "web"]
SYSTEM_CONFIGURATION_PATH = Path(sys.prefix) / "share" / "ahriman" / "settings" / "ahriman.ini"
def __init__(self) -> None:
def __init__(self, allow_no_value: bool = False) -> None:
"""
default constructor. In the most cases must not be called directly
Args:
allow_no_value(bool): copies ``configparser.RawConfigParser`` behaviour. In case if it is set to ``True``,
the keys without values will be allowed
"""
configparser.RawConfigParser.__init__(self, allow_no_value=True, converters={
configparser.RawConfigParser.__init__(self, allow_no_value=allow_no_value, converters={
"list": self.__convert_list,
"path": self.__convert_path,
})

View File

@ -0,0 +1,43 @@
#
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
__all__ = ["steps"]
steps = [
"""
alter table patches rename to patches_
""",
"""
create table patches (
package_base text not null,
variable text,
patch blob not null
)
""",
"""
create unique index patches_package_base_variable on patches (package_base, coalesce(variable, ''))
""",
"""
insert into patches (package_base, patch) select package_base, patch from patches_
""",
"""
drop table patches_
""",
]

View File

@ -20,7 +20,6 @@
import sqlite3
from pathlib import Path
from sqlite3 import Connection, Cursor
from typing import Any, Dict, Tuple, TypeVar, Callable
from ahriman.core.lazy_logging import LazyLogging
@ -46,7 +45,7 @@ class Operations(LazyLogging):
self.path = path
@staticmethod
def factory(cursor: Cursor, row: Tuple[Any, ...]) -> Dict[str, Any]:
def factory(cursor: sqlite3.Cursor, row: Tuple[Any, ...]) -> Dict[str, Any]:
"""
dictionary factory based on official documentation
@ -62,7 +61,7 @@ class Operations(LazyLogging):
result[column[0]] = row[index]
return result
def with_connection(self, query: Callable[[Connection], T], commit: bool = False) -> T:
def with_connection(self, query: Callable[[sqlite3.Connection], T], commit: bool = False) -> T:
"""
perform operation in connection

View File

@ -17,10 +17,13 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from collections import defaultdict
from sqlite3 import Connection
from typing import Dict, Optional
from typing import Dict, List, Optional, Tuple
from ahriman.core.database.operations import Operations
from ahriman.models.pkgbuild_patch import PkgbuildPatch
class PatchOperations(Operations):
@ -28,7 +31,7 @@ class PatchOperations(Operations):
operations for patches
"""
def patches_get(self, package_base: str) -> Optional[str]:
def patches_get(self, package_base: str) -> List[PkgbuildPatch]:
"""
retrieve patches for the package
@ -36,62 +39,77 @@ class PatchOperations(Operations):
package_base(str): package base to search for patches
Returns:
Optional[str]: plain text patch for the package
List[PkgbuildPatch]: plain text patch for the package
"""
return self.patches_list(package_base).get(package_base)
return self.patches_list(package_base, []).get(package_base, [])
def patches_insert(self, package_base: str, patch: str) -> None:
def patches_insert(self, package_base: str, patch: PkgbuildPatch) -> None:
"""
insert or update patch in database
Args:
package_base(str): package base to insert
patch(str): patch content
patch(PkgbuildPatch): patch content
"""
def run(connection: Connection) -> None:
connection.execute(
"""
insert into patches
(package_base, patch)
(package_base, variable, patch)
values
(:package_base, :patch)
on conflict (package_base) do update set
(:package_base, :variable, :patch)
on conflict (package_base, coalesce(variable, '')) do update set
patch = :patch
""",
{"package_base": package_base, "patch": patch})
{"package_base": package_base, "variable": patch.key, "patch": patch.value})
return self.with_connection(run, commit=True)
def patches_list(self, package_base: Optional[str]) -> Dict[str, str]:
def patches_list(self, package_base: Optional[str], variables: List[str]) -> Dict[str, List[PkgbuildPatch]]:
"""
extract all patches
Args:
package_base(Optional[str]): optional filter by package base
variables(List[str]): extract patches only for specified PKGBUILD variables
Returns:
Dict[str, str]: map of package base to patch content
Dict[str, List[PkgbuildPatch]]: map of package base to patch content
"""
def run(connection: Connection) -> Dict[str, str]:
return {
cursor["package_base"]: cursor["patch"]
def run(connection: Connection) -> List[Tuple[str, PkgbuildPatch]]:
return [
(cursor["package_base"], PkgbuildPatch(cursor["variable"], cursor["patch"]))
for cursor in connection.execute(
"""select * from patches where :package_base is null or package_base = :package_base""",
{"package_base": package_base})
}
]
return self.with_connection(run)
# we could use itertools & operator but why?
patches: Dict[str, List[PkgbuildPatch]] = defaultdict(list)
for package, patch in self.with_connection(run):
if variables and patch.key not in variables:
continue
patches[package].append(patch)
return dict(patches)
def patches_remove(self, package_base: str) -> None:
def patches_remove(self, package_base: str, variables: List[str]) -> None:
"""
remove patch set
Args:
package_base(str): package base to clear patches
variables(List[str]): remove patches only for specified PKGBUILD variables
"""
def run_many(connection: Connection) -> None:
connection.executemany(
"""delete from patches where package_base = :package_base and variable = :variable""",
[{"package_base": package_base, "variable": variable} for variable in variables])
def run(connection: Connection) -> None:
connection.execute(
"""delete from patches where package_base = :package_base""",
{"package_base": package_base})
if variables:
return self.with_connection(run_many, commit=True)
return self.with_connection(run, commit=True)

View File

@ -24,6 +24,7 @@ from ahriman.core.formatters.aur_printer import AurPrinter
from ahriman.core.formatters.build_printer import BuildPrinter
from ahriman.core.formatters.configuration_printer import ConfigurationPrinter
from ahriman.core.formatters.package_printer import PackagePrinter
from ahriman.core.formatters.patch_printer import PatchPrinter
from ahriman.core.formatters.status_printer import StatusPrinter
from ahriman.core.formatters.update_printer import UpdatePrinter
from ahriman.core.formatters.user_printer import UserPrinter

View File

@ -0,0 +1,56 @@
#
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from typing import List
from ahriman.core.formatters import StringPrinter
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.property import Property
class PatchPrinter(StringPrinter):
"""
print content of the PKGBUILD patch
Attributes:
patches(List[PkgbuildPatch]): PKGBUILD patch object
"""
def __init__(self, package_base: str, patches: List[PkgbuildPatch]) -> None:
"""
default constructor
Args:
package_base(str): package base
patches(List[PkgbuildPatch]): PKGBUILD patch object
"""
StringPrinter.__init__(self, package_base)
self.patches = patches
def properties(self) -> List[Property]:
"""
convert content into printable data
Returns:
List[Property]: list of content properties
"""
return [
Property(patch.key or "Full source diff", patch.value, is_required=True)
for patch in self.patches
]

View File

@ -0,0 +1,21 @@
#
# copyright (c) 2021-2022 ahriman team.
#
# this file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# this program is free software: you can redistribute it and/or modify
# it under the terms of the gnu general public license as published by
# the free software foundation, either version 3 of the license, or
# (at your option) any later version.
#
# this program is distributed in the hope that it will be useful,
# but without any warranty; without even the implied warranty of
# merchantability or fitness for a particular purpose. see the
# gnu general public license for more details.
#
# you should have received a copy of the gnu general public license
# along with this program. if not, see <http://www.gnu.org/licenses/>.
#
from ahriman.core.gitremote.remote_pull_trigger import RemotePullTrigger
from ahriman.core.gitremote.remote_push_trigger import RemotePushTrigger

View File

@ -0,0 +1,86 @@
#
# copyright (c) 2021-2022 ahriman team.
#
# this file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# this program is free software: you can redistribute it and/or modify
# it under the terms of the gnu general public license as published by
# the free software foundation, either version 3 of the license, or
# (at your option) any later version.
#
# this program is distributed in the hope that it will be useful,
# but without any warranty; without even the implied warranty of
# merchantability or fitness for a particular purpose. see the
# gnu general public license for more details.
#
# you should have received a copy of the gnu general public license
# along with this program. if not, see <http://www.gnu.org/licenses/>.
#
import shutil
from pathlib import Path
from tempfile import TemporaryDirectory
from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.core.triggers import Trigger
from ahriman.core.util import walk
from ahriman.models.package_source import PackageSource
from ahriman.models.remote_source import RemoteSource
class RemotePullTrigger(Trigger):
"""
trigger for fetching PKGBUILDs from remote repository
Attributes:
remote_source(RemoteSource): repository remote source (remote pull url and branch)
repository_paths(RepositoryPaths): repository paths instance
"""
def __init__(self, architecture: str, configuration: Configuration) -> None:
"""
default constructor
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
"""
Trigger.__init__(self, architecture, configuration)
self.remote_source = RemoteSource(
git_url=configuration.get("gitremote", "pull_url"),
web_url="",
path=".",
branch=configuration.get("gitremote", "pull_branch", fallback="master"),
source=PackageSource.Local,
)
self.repository_paths = configuration.repository_paths
def on_start(self) -> None:
"""
trigger action which will be called at the start of the application
"""
self.repo_clone()
def repo_clone(self) -> None:
"""
clone repository from remote source
"""
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (clone_dir := Path(dir_name)):
Sources.fetch(clone_dir, self.remote_source)
self.repo_copy(clone_dir)
def repo_copy(self, clone_dir: Path) -> None:
"""
copy directories from cloned remote source to local cache
Args:
clone_dir(Path): path to temporary cloned directory
"""
for pkgbuild_path in filter(lambda path: path.name == "PKGBUILD", walk(clone_dir)):
cloned_pkgbuild_dir = pkgbuild_path.parent
package_base = cloned_pkgbuild_dir.name
local_pkgbuild_dir = self.repository_paths.cache_for(package_base)
shutil.copytree(cloned_pkgbuild_dir, local_pkgbuild_dir, dirs_exist_ok=True)
Sources.init(local_pkgbuild_dir) # initialized git repository is required for local sources

View File

@ -0,0 +1,110 @@
#
# copyright (c) 2021-2022 ahriman team.
#
# this file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# this program is free software: you can redistribute it and/or modify
# it under the terms of the gnu general public license as published by
# the free software foundation, either version 3 of the license, or
# (at your option) any later version.
#
# this program is distributed in the hope that it will be useful,
# but without any warranty; without even the implied warranty of
# merchantability or fitness for a particular purpose. see the
# gnu general public license for more details.
#
# you should have received a copy of the gnu general public license
# along with this program. if not, see <http://www.gnu.org/licenses/>.
#
import shutil
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Generator, Iterable
from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.core.triggers import Trigger
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
from ahriman.models.remote_source import RemoteSource
from ahriman.models.result import Result
class RemotePushTrigger(Trigger):
"""
trigger for syncing PKGBUILDs to remote repository
Attributes:
remote_source(RemoteSource): repository remote source (remote pull url and branch)
"""
def __init__(self, architecture: str, configuration: Configuration) -> None:
"""
default constructor
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
"""
Trigger.__init__(self, architecture, configuration)
self.remote_source = RemoteSource(
git_url=configuration.get("gitremote", "push_url"),
web_url="",
path=".",
branch=configuration.get("gitremote", "push_branch", fallback="master"),
source=PackageSource.Local,
)
@staticmethod
def package_update(package: Package, target_dir: Path) -> str:
"""
clone specified package and update its content in cloned PKGBUILD repository
Args:
package(Package): built package to update pkgbuild repository
target_dir(Path): path to the cloned PKGBUILD repository
Returns:
str: relative path to be added as new file
"""
# instead of iterating by directory we can simplify the process
# firstly, we need to remove old data to make sure that removed files are not tracked anymore...
package_target_dir = target_dir / package.base
shutil.rmtree(package_target_dir, ignore_errors=True)
# ...secondly, we copy whole tree...
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (clone_dir := Path(dir_name)):
Sources.fetch(clone_dir, package.remote)
shutil.copytree(clone_dir, package_target_dir)
# ...and last, but not least, we remove the dot-git directory...
shutil.rmtree(package_target_dir / ".git", ignore_errors=True)
# ...and finally return path to the copied directory
return package.base
@staticmethod
def packages_update(result: Result, target_dir: Path) -> Generator[str, None, None]:
"""
update all packages from the build result
Args:
result(Result): build result
target_dir(Path): path to the cloned PKGBUILD repository
Yields:
str: path to updated files
"""
for package in result.success:
yield RemotePushTrigger.package_update(package, target_dir)
def on_result(self, result: Result, packages: Iterable[Package]) -> None:
"""
trigger action which will be called after build process with process result
Args:
result(Result): build result
packages(Iterable[Package]): list of all available packages
"""
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (clone_dir := Path(dir_name)):
Sources.fetch(clone_dir, self.remote_source)
Sources.push(clone_dir, self.remote_source, *RemotePushTrigger.packages_update(result, clone_dir))

View File

@ -17,5 +17,4 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from ahriman.core.report.report import Report
from ahriman.core.report.report_trigger import ReportTrigger

View File

@ -21,7 +21,7 @@ from typing import Iterable
from ahriman.core.configuration import Configuration
from ahriman.core.formatters import BuildPrinter
from ahriman.core.report import Report
from ahriman.core.report.report import Report
from ahriman.models.package import Package
from ahriman.models.result import Result

View File

@ -25,8 +25,8 @@ from email.mime.text import MIMEText
from typing import Dict, Iterable
from ahriman.core.configuration import Configuration
from ahriman.core.report import Report
from ahriman.core.report.jinja_template import JinjaTemplate
from ahriman.core.report.report import Report
from ahriman.core.util import pretty_datetime
from ahriman.models.package import Package
from ahriman.models.result import Result

View File

@ -20,8 +20,8 @@
from typing import Iterable
from ahriman.core.configuration import Configuration
from ahriman.core.report import Report
from ahriman.core.report.jinja_template import JinjaTemplate
from ahriman.core.report.report import Report
from ahriman.models.package import Package
from ahriman.models.result import Result

View File

@ -21,7 +21,7 @@ from typing import Iterable
from ahriman.core.configuration import Configuration
from ahriman.core.triggers import Trigger
from ahriman.core.report import Report
from ahriman.core.report.report import Report
from ahriman.models.package import Package
from ahriman.models.result import Result
@ -43,9 +43,9 @@ class ReportTrigger(Trigger):
configuration(Configuration): configuration instance
"""
Trigger.__init__(self, architecture, configuration)
self.targets = configuration.getlist("report", "target")
self.targets = configuration.getlist("report", "target", fallback=[])
def run(self, result: Result, packages: Iterable[Package]) -> None:
def on_result(self, result: Result, packages: Iterable[Package]) -> None:
"""
run trigger

View File

@ -23,8 +23,8 @@ import requests
from typing import Iterable
from ahriman.core.configuration import Configuration
from ahriman.core.report import Report
from ahriman.core.report.jinja_template import JinjaTemplate
from ahriman.core.report.report import Report
from ahriman.core.util import exception_response_text
from ahriman.models.package import Package
from ahriman.models.result import Result

View File

@ -84,8 +84,7 @@ class Executor(Cleaner):
result = Result()
for single in updates:
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, \
(build_dir := Path(dir_name)): # pylint: disable=confusing-with-statement
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (build_dir := Path(dir_name)):
try:
build_single(single, build_dir)
result.add_success(single)
@ -110,7 +109,7 @@ class Executor(Cleaner):
try:
self.paths.tree_clear(package_base) # remove all internal files
self.database.build_queue_clear(package_base)
self.database.patches_remove(package_base)
self.database.patches_remove(package_base, [])
self.reporter.remove(package_base) # we only update status page in case of base removal
except Exception:
self.logger.exception("could not remove base %s", package_base)
@ -144,15 +143,6 @@ class Executor(Cleaner):
return self.repo.repo_path
def process_triggers(self, result: Result) -> None:
"""
process triggers setup by settings
Args:
result(Result): build result
"""
self.triggers.process(result, self.packages())
def process_update(self, packages: Iterable[Path]) -> Result:
"""
sign packages, add them to repository and update repository database

View File

@ -97,7 +97,7 @@ class GPG(LazyLogging):
Tuple[Set[SignSettings], Optional[str]]: tuple of sign targets and default PGP key
"""
targets: Set[SignSettings] = set()
for option in configuration.getlist("sign", "target"):
for option in configuration.getlist("sign", "target", fallback=[]):
target = SignSettings.from_option(option)
if target == SignSettings.Disabled:
continue
@ -181,7 +181,7 @@ class GPG(LazyLogging):
sign repository if required by configuration
Note:
more likely you just want to pass ``repository_sign_args`` to repo wrapper
More likely you just want to pass ``repository_sign_args`` to repo wrapper
Args:
path(Path): path to repository database

View File

@ -86,10 +86,12 @@ class Spawn(Thread, LazyLogging):
packages(Iterable[str]): packages list to add
now(bool): build packages now
"""
if not packages:
return self.spawn_process("repo-update")
kwargs = {"source": PackageSource.AUR.value} # avoid abusing by building non-aur packages
if now:
kwargs["now"] = ""
self.spawn_process("add", *packages, **kwargs)
return self.spawn_process("package-add", *packages, **kwargs)
def packages_remove(self, packages: Iterable[str]) -> None:
"""
@ -98,7 +100,7 @@ class Spawn(Thread, LazyLogging):
Args:
packages(Iterable[str]): packages list to remove
"""
self.spawn_process("remove", *packages)
self.spawn_process("package-remove", *packages)
def spawn_process(self, command: str, *args: str, **kwargs: str) -> None:
"""

View File

@ -72,8 +72,7 @@ class Leaf:
Returns:
Leaf: loaded class
"""
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, \
(clone_dir := Path(dir_name)): # pylint: disable=confusing-with-statement
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (clone_dir := Path(dir_name)):
Sources.load(clone_dir, package, database.patches_get(package.base), paths)
dependencies = Package.dependencies(clone_dir)
return cls(package, dependencies)

View File

@ -37,7 +37,7 @@ class Trigger(LazyLogging):
This class must be used in order to create own extension. Basically idea is the following::
>>> class CustomTrigger(Trigger):
>>> def run(self, result: Result, packages: Iterable[Package]) -> None:
>>> def on_result(self, result: Result, packages: Iterable[Package]) -> None:
>>> perform_some_action()
Having this class you can pass it to ``configuration`` and it will be run on action::
@ -48,7 +48,7 @@ class Trigger(LazyLogging):
>>> configuration.set_option("build", "triggers", "my.awesome.package.CustomTrigger")
>>>
>>> loader = TriggerLoader("x86_64", configuration)
>>> loader.process(Result(), [])
>>> loader.on_result(Result(), [])
"""
def __init__(self, architecture: str, configuration: Configuration) -> None:
@ -62,15 +62,23 @@ class Trigger(LazyLogging):
self.architecture = architecture
self.configuration = configuration
def run(self, result: Result, packages: Iterable[Package]) -> None:
def on_result(self, result: Result, packages: Iterable[Package]) -> None:
"""
run trigger
trigger action which will be called after build process with process result
Args:
result(Result): build result
packages(Iterable[Package]): list of all available packages
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
if (run := getattr(self, "run", None)) is not None:
run(result, packages) # compatibility with old triggers
def on_start(self) -> None:
"""
trigger action which will be called at the start of the application
"""
def on_stop(self) -> None:
"""
trigger action which will be called before the stop of the application
"""

View File

@ -17,12 +17,13 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import contextlib
import importlib
import os
from pathlib import Path
from types import ModuleType
from typing import Iterable
from typing import Generator, Iterable
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import InvalidExtension
@ -54,7 +55,7 @@ class TriggerLoader(LazyLogging):
After that you are free to run triggers::
>>> loader.process(Result(), [])
>>> loader.on_result(Result(), [])
"""
def __init__(self, architecture: str, configuration: Configuration) -> None:
@ -72,6 +73,31 @@ class TriggerLoader(LazyLogging):
self.load_trigger(trigger)
for trigger in configuration.getlist("build", "triggers")
]
self._on_stop_requested = False
def __del__(self) -> None:
"""
custom destructor object which calls on_stop in case if it was requested
"""
if not self._on_stop_requested:
return
self.on_stop()
@contextlib.contextmanager
def __execute_trigger(self, trigger: Trigger) -> Generator[None, None, None]:
"""
decorator for calling triggers
Args:
trigger(Trigger): trigger instance to be called
"""
trigger_name = type(trigger).__name__
try:
self.logger.info("executing extension %s", trigger_name)
yield
except Exception:
self.logger.exception("got exception while run trigger %s", trigger_name)
def _load_module_from_file(self, module_path: str, implementation: str) -> ModuleType:
"""
@ -149,18 +175,34 @@ class TriggerLoader(LazyLogging):
return trigger
def process(self, result: Result, packages: Iterable[Package]) -> None:
def on_result(self, result: Result, packages: Iterable[Package]) -> None:
"""
run remote sync
run trigger with result of application run
Args:
result(Result): build result
packages(Iterable[Package]): list of all available packages
"""
self.logger.debug("executing triggers on result")
for trigger in self.triggers:
trigger_name = type(trigger).__name__
try:
self.logger.info("executing extension %s", trigger_name)
trigger.run(result, packages)
except Exception:
self.logger.exception("got exception while run trigger %s", trigger_name)
with self.__execute_trigger(trigger):
trigger.on_result(result, packages)
def on_start(self) -> None:
"""
run triggers on load
"""
self.logger.debug("executing triggers on start")
self._on_stop_requested = True
for trigger in self.triggers:
with self.__execute_trigger(trigger):
trigger.on_start()
def on_stop(self) -> None:
"""
run triggers before the application exit
"""
self.logger.debug("executing triggers on stop")
for trigger in self.triggers:
with self.__execute_trigger(trigger):
trigger.on_stop()

View File

@ -17,5 +17,4 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from ahriman.core.upload.upload import Upload
from ahriman.core.upload.upload_trigger import UploadTrigger

View File

@ -24,7 +24,7 @@ from pathlib import Path
from typing import Any, Dict
from ahriman.core.configuration import Configuration
from ahriman.core.upload import Upload
from ahriman.core.upload.upload import Upload
from ahriman.core.util import exception_response_text
@ -33,7 +33,7 @@ class HttpUpload(Upload):
helper for the http based uploads
Attributes:
auth(Tuple[str, str]): HTTP auth object
auth(Optional[Tuple[str, str]]): HTTP auth object if set
timeout(int): HTTP request timeout in seconds
"""
@ -47,9 +47,9 @@ class HttpUpload(Upload):
section(str): configuration section name
"""
Upload.__init__(self, architecture, configuration)
password = configuration.get(section, "password")
username = configuration.get(section, "username")
self.auth = (password, username)
password = configuration.get(section, "password", fallback=None)
username = configuration.get(section, "username", fallback=None)
self.auth = (password, username) if password and username else None
self.timeout = configuration.getint(section, "timeout", fallback=30)
@staticmethod

View File

@ -21,7 +21,7 @@ from pathlib import Path
from typing import Iterable
from ahriman.core.configuration import Configuration
from ahriman.core.upload import Upload
from ahriman.core.upload.upload import Upload
from ahriman.core.util import check_output
from ahriman.models.package import Package

View File

@ -25,7 +25,7 @@ from pathlib import Path
from typing import Any, Dict, Iterable
from ahriman.core.configuration import Configuration
from ahriman.core.upload import Upload
from ahriman.core.upload.upload import Upload
from ahriman.core.util import walk
from ahriman.models.package import Package

View File

@ -21,7 +21,7 @@ from typing import Iterable
from ahriman.core.configuration import Configuration
from ahriman.core.triggers import Trigger
from ahriman.core.upload import Upload
from ahriman.core.upload.upload import Upload
from ahriman.models.package import Package
from ahriman.models.result import Result
@ -43,9 +43,9 @@ class UploadTrigger(Trigger):
configuration(Configuration): configuration instance
"""
Trigger.__init__(self, architecture, configuration)
self.targets = configuration.getlist("upload", "target")
self.targets = configuration.getlist("upload", "target", fallback=[])
def run(self, result: Result, packages: Iterable[Package]) -> None:
def on_result(self, result: Result, packages: Iterable[Package]) -> None:
"""
run trigger

View File

@ -305,7 +305,7 @@ class Package(LazyLogging):
from ahriman.core.build_tools.sources import Sources
Sources.load(paths.cache_for(self.base), self, None, paths)
Sources.load(paths.cache_for(self.base), self, [], paths)
try:
# update pkgver first

View File

@ -21,7 +21,7 @@ import shlex
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Union
from typing import List, Optional, Union
@dataclass(frozen=True)
@ -30,16 +30,23 @@ class PkgbuildPatch:
wrapper for patching PKBGUILDs
Attributes:
key(str): name of the property in PKGBUILD, e.g. version, url etc
key(Optional[str]): name of the property in PKGBUILD, e.g. version, url etc. If not set, patch will be
considered as full PKGBUILD diffs
value(Union[str, List[str]]): value of the stored PKGBUILD property. It must be either string or list of string
values
unsafe(bool): if set, value will be not quoted, might break PKGBUILD
"""
key: str
key: Optional[str]
value: Union[str, List[str]]
unsafe: bool = field(default=False, kw_only=True)
def __post_init__(self) -> None:
"""
remove empty key
"""
object.__setattr__(self, "key", self.key or None)
@property
def is_function(self) -> bool:
"""
@ -48,7 +55,17 @@ class PkgbuildPatch:
Returns:
bool: True in case if key ends with parentheses and False otherwise
"""
return self.key.endswith("()")
return self.key is not None and self.key.endswith("()")
@property
def is_plain_diff(self) -> bool:
"""
check if patch is full diff one or just single-variable patch
Returns:
bool: True in case key set and False otherwise
"""
return self.key is None
def quote(self, value: str) -> str:
"""
@ -74,6 +91,8 @@ class PkgbuildPatch:
if isinstance(self.value, list): # list like
value = " ".join(map(self.quote, self.value))
return f"""{self.key}=({value})"""
if self.is_plain_diff: # no additional logic for plain diffs
return self.value
# we suppose that function values are only supported in string-like values
if self.is_function:
return f"{self.key} {self.value}" # no quoting enabled here

View File

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

View File

@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from aiohttp.web import HTTPBadRequest, HTTPFound
from aiohttp.web import HTTPFound
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
@ -47,11 +47,8 @@ class AddView(BaseView):
HTTPBadRequest: if bad data is supplied
HTTPFound: in case of success response
"""
try:
data = await self.extract_data(["packages"])
packages = data["packages"]
except Exception as e:
raise HTTPBadRequest(reason=str(e))
data = await self.extract_data(["packages"])
packages = data.get("packages", [])
self.spawner.packages_add(packages, now=True)

View File

@ -5,16 +5,6 @@ from ahriman.models.package import Package
from ahriman.models.result import Result
def test_finalize(application: Application, mocker: MockerFixture) -> None:
"""
must report and sync at the last
"""
triggers_mock = mocker.patch("ahriman.core.repository.Repository.process_triggers")
application._finalize(Result())
triggers_mock.assert_called_once_with(Result())
def test_known_packages(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must return not empty list of known packages
@ -23,3 +13,34 @@ def test_known_packages(application: Application, package_ahriman: Package, mock
packages = application._known_packages()
assert len(packages) > 1
assert package_ahriman.base in packages
def test_on_result(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must call on_result trigger function
"""
mocker.patch("ahriman.core.repository.Repository.packages", return_value=[package_ahriman])
triggers_mock = mocker.patch("ahriman.core.triggers.TriggerLoader.on_result")
application.on_result(Result())
triggers_mock.assert_called_once_with(Result(), [package_ahriman])
def test_on_start(application: Application, mocker: MockerFixture) -> None:
"""
must call on_start trigger function
"""
triggers_mock = mocker.patch("ahriman.core.triggers.TriggerLoader.on_start")
application.on_start()
triggers_mock.assert_called_once_with()
def test_on_stop(application: Application, mocker: MockerFixture) -> None:
"""
must call on_stop trigger function
"""
triggers_mock = mocker.patch("ahriman.core.triggers.TriggerLoader.on_stop")
application.on_stop()
triggers_mock.assert_called_once_with()

View File

@ -11,12 +11,12 @@ from ahriman.models.package_source import PackageSource
from ahriman.models.result import Result
def test_finalize(application_packages: ApplicationPackages) -> None:
def test_on_result(application_packages: ApplicationPackages) -> None:
"""
must raise NotImplemented for missing finalize method
"""
with pytest.raises(NotImplementedError):
application_packages._finalize([])
application_packages.on_result(Result())
def test_known_packages(application_packages: ApplicationPackages) -> None:
@ -242,8 +242,8 @@ def test_remove(application_packages: ApplicationPackages, mocker: MockerFixture
must remove package
"""
executor_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_remove")
finalize_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages._finalize")
on_result_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages.on_result")
application_packages.remove([])
executor_mock.assert_called_once_with([])
finalize_mock.assert_called_once_with(Result())
on_result_mock.assert_called_once_with(Result())

View File

@ -9,12 +9,12 @@ from ahriman.models.package import Package
from ahriman.models.result import Result
def test_finalize(application_repository: ApplicationRepository) -> None:
def test_on_result(application_repository: ApplicationRepository) -> None:
"""
must raise NotImplemented for missing finalize method
"""
with pytest.raises(NotImplementedError):
application_repository._finalize(Result())
application_repository.on_result(Result())
def test_clean_cache(application_repository: ApplicationRepository, mocker: MockerFixture) -> None:
@ -63,8 +63,8 @@ def test_sign(application_repository: ApplicationRepository, package_ahriman: Pa
copy_mock = mocker.patch("shutil.copy")
update_mock = mocker.patch("ahriman.application.application.application_repository.ApplicationRepository.update")
sign_repository_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process_sign_repository")
finalize_mock = mocker.patch(
"ahriman.application.application.application_repository.ApplicationRepository._finalize")
on_result_mock = mocker.patch(
"ahriman.application.application.application_repository.ApplicationRepository.on_result")
application_repository.sign([])
copy_mock.assert_has_calls([
@ -73,7 +73,7 @@ def test_sign(application_repository: ApplicationRepository, package_ahriman: Pa
])
update_mock.assert_called_once_with([])
sign_repository_mock.assert_called_once_with(application_repository.repository.repo.repo_path)
finalize_mock.assert_called_once_with(Result())
on_result_mock.assert_called_once_with(Result())
def test_sign_skip(application_repository: ApplicationRepository, package_ahriman: Package,
@ -84,7 +84,7 @@ def test_sign_skip(application_repository: ApplicationRepository, package_ahrima
package_ahriman.packages[package_ahriman.base].filename = None
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman])
mocker.patch("ahriman.application.application.application_repository.ApplicationRepository.update")
mocker.patch("ahriman.application.application.application_repository.ApplicationRepository._finalize")
mocker.patch("ahriman.application.application.application_repository.ApplicationRepository.on_result")
application_repository.sign([])
@ -99,8 +99,8 @@ def test_sign_specific(application_repository: ApplicationRepository, package_ah
copy_mock = mocker.patch("shutil.copy")
update_mock = mocker.patch("ahriman.application.application.application_repository.ApplicationRepository.update")
sign_repository_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process_sign_repository")
finalize_mock = mocker.patch(
"ahriman.application.application.application_repository.ApplicationRepository._finalize")
on_result_mock = mocker.patch(
"ahriman.application.application.application_repository.ApplicationRepository.on_result")
filename = package_ahriman.packages[package_ahriman.base].filepath
application_repository.sign([package_ahriman.base])
@ -109,7 +109,7 @@ def test_sign_specific(application_repository: ApplicationRepository, package_ah
application_repository.repository.paths.packages / filename.name)
update_mock.assert_called_once_with([])
sign_repository_mock.assert_called_once_with(application_repository.repository.repo.repo_path)
finalize_mock.assert_called_once_with(Result())
on_result_mock.assert_called_once_with(Result())
def test_unknown_no_aur(application_repository: ApplicationRepository, package_ahriman: Package,
@ -163,13 +163,13 @@ def test_update(application_repository: ApplicationRepository, package_ahriman:
mocker.patch("ahriman.core.repository.repository.Repository.packages_built", return_value=paths)
build_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_build", return_value=result)
update_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_update", return_value=result)
finalize_mock = mocker.patch(
"ahriman.application.application.application_repository.ApplicationRepository._finalize")
on_result_mock = mocker.patch(
"ahriman.application.application.application_repository.ApplicationRepository.on_result")
application_repository.update([package_ahriman])
build_mock.assert_called_once_with([package_ahriman])
update_mock.assert_has_calls([mock.call(paths), mock.call(paths)])
finalize_mock.assert_has_calls([mock.call(result), mock.call(result)])
on_result_mock.assert_has_calls([mock.call(result), mock.call(result)])
def test_update_empty(application_repository: ApplicationRepository, package_ahriman: Package,

View File

@ -35,9 +35,11 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
args = _default_args(args)
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
application_mock = mocker.patch("ahriman.application.application.Application.add")
on_start_mock = mocker.patch("ahriman.application.application.Application.on_start")
Add.run(args, "x86_64", configuration, True, False)
application_mock.assert_called_once_with(args.package, args.source, args.without_dependencies)
on_start_mock.assert_called_once_with()
def test_run_with_updates(args: argparse.Namespace, configuration: Configuration,

View File

@ -30,6 +30,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
args = _default_args(args)
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
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, True, False)
application_mock.assert_called_once_with(False, False, False, False)
on_start_mock.assert_called_once_with()

View File

@ -0,0 +1,40 @@
import argparse
from pytest_mock import MockerFixture
from ahriman.application.handlers import Daemon
from ahriman.core.configuration import Configuration
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
"""
default arguments for these test cases
Args:
args(argparse.Namespace): command line arguments fixture
Returns:
argparse.Namespace: generated arguments for these test cases
"""
args.interval = 60 * 60 * 12
args.no_aur = False
args.no_local = False
args.no_manual = False
args.no_vcs = False
return args
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
run_mock = mocker.patch("ahriman.application.handlers.Update.run")
start_mock = mocker.patch("threading.Timer.start")
join_mock = mocker.patch("threading.Timer.join")
Daemon.run(args, "x86_64", configuration, True, False)
Daemon._SHOULD_RUN = False
run_mock.assert_called_once_with(args, "x86_64", configuration, True, False)
start_mock.assert_called_once_with()
join_mock.assert_called_once_with()

View File

@ -1,5 +1,6 @@
import argparse
import pytest
import sys
from pathlib import Path
from pytest_mock import MockerFixture
@ -9,6 +10,7 @@ from ahriman.application.handlers import Patch
from ahriman.core.configuration import Configuration
from ahriman.models.action import Action
from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
@ -25,6 +27,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.exit_code = False
args.remove = False
args.track = ["*.diff", "*.patch"]
args.variable = None
return args
@ -35,10 +38,31 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
args = _default_args(args)
args.action = Action.Update
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
patch_mock = mocker.patch("ahriman.application.handlers.Patch.patch_create_from_diff",
return_value=(args.package, PkgbuildPatch(None, "patch")))
application_mock = mocker.patch("ahriman.application.handlers.Patch.patch_set_create")
Patch.run(args, "x86_64", configuration, True, False)
application_mock.assert_called_once_with(pytest.helpers.anyvar(int), Path(args.package), args.track)
patch_mock.assert_called_once_with(args.package, args.track)
application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package, PkgbuildPatch(None, "patch"))
def test_run_function(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command with patch function flag
"""
args = _default_args(args)
args.action = Action.Update
args.patch = "patch"
args.variable = "version"
patch = PkgbuildPatch(args.variable, args.patch)
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
patch_mock = mocker.patch("ahriman.application.handlers.Patch.patch_create_from_function", return_value=patch)
application_mock = mocker.patch("ahriman.application.handlers.Patch.patch_set_create")
Patch.run(args, "x86_64", configuration, True, False)
patch_mock.assert_called_once_with(args.variable, args.patch)
application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package, patch)
def test_run_list(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
@ -47,11 +71,12 @@ def test_run_list(args: argparse.Namespace, configuration: Configuration, mocker
"""
args = _default_args(args)
args.action = Action.List
args.variable = ["version"]
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
application_mock = mocker.patch("ahriman.application.handlers.Patch.patch_set_list")
Patch.run(args, "x86_64", configuration, True, False)
application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package, False)
application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package, ["version"], False)
def test_run_remove(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
@ -60,24 +85,71 @@ def test_run_remove(args: argparse.Namespace, configuration: Configuration, mock
"""
args = _default_args(args)
args.action = Action.Remove
args.variable = ["version"]
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
application_mock = mocker.patch("ahriman.application.handlers.Patch.patch_set_remove")
Patch.run(args, "x86_64", configuration, True, False)
application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package)
application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package, ["version"])
def test_patch_create_from_diff(package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must create patch from directory tree diff
"""
patch = PkgbuildPatch(None, "patch")
path = Path("local")
mocker.patch("pathlib.Path.mkdir")
package_mock = mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman)
sources_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_create", return_value=patch.value)
assert Patch.patch_create_from_diff(path, ["*.diff"]) == (package_ahriman.base, patch)
package_mock.assert_called_once_with(path)
sources_mock.assert_called_once_with(path, "*.diff")
def test_patch_create_from_function(mocker: MockerFixture) -> None:
"""
must create function patch from file
"""
path = Path("local")
patch = PkgbuildPatch("version", "patch")
read_mock = mocker.patch("pathlib.Path.read_text", return_value=patch.value)
assert Patch.patch_create_from_function(patch.key, path) == patch
read_mock.assert_called_once_with(encoding="utf8")
def test_patch_create_from_function_stdin(mocker: MockerFixture) -> None:
"""
must create function patch from stdin
"""
patch = PkgbuildPatch("version", "This is a patch")
mocker.patch.object(sys, "stdin", patch.value.splitlines())
assert Patch.patch_create_from_function(patch.key, None) == patch
def test_patch_create_from_function_strip(mocker: MockerFixture) -> None:
"""
must remove spaces at the beginning and at the end of the line
"""
patch = PkgbuildPatch("version", "This is a patch")
mocker.patch.object(sys, "stdin", ["\n"] + patch.value.splitlines() + ["\n"])
assert Patch.patch_create_from_function(patch.key, None) == patch
def test_patch_set_list(application: Application, mocker: MockerFixture) -> None:
"""
must list available patches for the command
"""
get_mock = mocker.patch("ahriman.core.database.SQLite.patches_list", return_value={"ahriman": "patch"})
get_mock = mocker.patch("ahriman.core.database.SQLite.patches_list",
return_value={"ahriman": PkgbuildPatch(None, "patch")})
print_mock = mocker.patch("ahriman.core.formatters.Printer.print")
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty")
Patch.patch_set_list(application, "ahriman", False)
get_mock.assert_called_once_with("ahriman")
print_mock.assert_called_once_with(verbose=True)
Patch.patch_set_list(application, "ahriman", ["version"], False)
get_mock.assert_called_once_with("ahriman", ["version"])
print_mock.assert_called_once_with(verbose=True, separator=" = ")
check_mock.assert_called_once_with(False, False)
@ -88,7 +160,7 @@ def test_patch_set_list_empty_exception(application: Application, mocker: Mocker
mocker.patch("ahriman.core.database.SQLite.patches_list", return_value={})
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty")
Patch.patch_set_list(application, "ahriman", True)
Patch.patch_set_list(application, "ahriman", [], True)
check_mock.assert_called_once_with(True, True)
@ -96,13 +168,9 @@ def test_patch_set_create(application: Application, package_ahriman: Package, mo
"""
must create patch set for the package
"""
mocker.patch("pathlib.Path.mkdir")
mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman)
mocker.patch("ahriman.core.build_tools.sources.Sources.patch_create", return_value="patch")
create_mock = mocker.patch("ahriman.core.database.SQLite.patches_insert")
Patch.patch_set_create(application, Path("path"), ["*.patch"])
create_mock.assert_called_once_with(package_ahriman.base, "patch")
Patch.patch_set_create(application, package_ahriman.base, PkgbuildPatch("version", package_ahriman.version))
create_mock.assert_called_once_with(package_ahriman.base, PkgbuildPatch("version", package_ahriman.version))
def test_patch_set_remove(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
@ -110,5 +178,5 @@ def test_patch_set_remove(application: Application, package_ahriman: Package, mo
must remove patch set for the package
"""
remove_mock = mocker.patch("ahriman.core.database.SQLite.patches_remove")
Patch.patch_set_remove(application, package_ahriman.base)
remove_mock.assert_called_once_with(package_ahriman.base)
Patch.patch_set_remove(application, package_ahriman.base, ["version"])
remove_mock.assert_called_once_with(package_ahriman.base, ["version"])

View File

@ -41,11 +41,13 @@ def test_run(args: argparse.Namespace, package_ahriman: Package,
return_value=[package_ahriman])
application_mock = mocker.patch("ahriman.application.application.Application.update", return_value=result)
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, True, False)
application_packages_mock.assert_called_once_with(None)
application_mock.assert_called_once_with([package_ahriman])
check_mock.assert_has_calls([mock.call(False, False), mock.call(False, False)])
on_start_mock.assert_called_once_with()
def test_run_extract_packages(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:

View File

@ -27,6 +27,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
args = _default_args(args)
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
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, True, False)
application_mock.assert_called_once_with([])
on_start_mock.assert_called_once_with()

View File

@ -32,10 +32,12 @@ def test_run(args: argparse.Namespace, package_ahriman: Package,
application_mock = mocker.patch("ahriman.application.application.Application.unknown",
return_value=[package_ahriman])
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, True, False)
application_mock.assert_called_once_with()
remove_mock.assert_called_once_with([package_ahriman])
on_start_mock.assert_called_once_with()
def test_run_dry_run(args: argparse.Namespace, configuration: Configuration, package_ahriman: Package,

View File

@ -28,10 +28,12 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
"""
args = _default_args(args)
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
application_mock = mocker.patch("ahriman.core.repository.Repository.process_triggers")
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, True, False)
application_mock.assert_called_once_with(Result())
on_start_mock.assert_called_once_with()
def test_run_trigger(args: argparse.Namespace, configuration: Configuration, package_ahriman: Package,
@ -43,8 +45,8 @@ def test_run_trigger(args: argparse.Namespace, configuration: Configuration, pac
args.trigger = ["ahriman.core.report.ReportTrigger"]
mocker.patch("ahriman.core.repository.Repository.packages", return_value=[package_ahriman])
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
report_mock = mocker.patch("ahriman.core.report.ReportTrigger.run")
upload_mock = mocker.patch("ahriman.core.upload.UploadTrigger.run")
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, True, False)
report_mock.assert_called_once_with(Result(), [package_ahriman])

View File

@ -43,12 +43,14 @@ def test_run(args: argparse.Namespace, package_ahriman: Package,
application_mock = mocker.patch("ahriman.application.application.Application.update", return_value=result)
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty")
updates_mock = mocker.patch("ahriman.application.application.Application.updates", return_value=[package_ahriman])
on_start_mock = mocker.patch("ahriman.application.application.Application.on_start")
Update.run(args, "x86_64", configuration, True, False)
application_mock.assert_called_once_with([package_ahriman])
updates_mock.assert_called_once_with(args.package, args.no_aur, args.no_local, args.no_manual, args.no_vcs,
pytest.helpers.anyvar(int))
check_mock.assert_has_calls([mock.call(False, False), mock.call(False, False)])
on_start_mock.assert_called_once_with()
def test_run_empty_exception(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:

View File

@ -65,6 +65,26 @@ def test_subparsers_aur_search_architecture(parser: argparse.ArgumentParser) ->
assert args.architecture == [""]
def test_subparsers_daemon(parser: argparse.ArgumentParser) -> None:
"""
daemon command must imply dry run, exit code and package
"""
args = parser.parse_args(["daemon"])
assert not args.dry_run
assert not args.exit_code
assert args.package == []
def test_subparsers_daemon_option_interval(parser: argparse.ArgumentParser) -> None:
"""
daemon command must convert interval option to int instance
"""
args = parser.parse_args(["daemon"])
assert isinstance(args.interval, int)
args = parser.parse_args(["daemon", "--interval", "10"])
assert isinstance(args.interval, int)
def test_subparsers_help(parser: argparse.ArgumentParser) -> None:
"""
help command must imply architecture list, lock, no-report, quiet, unsafe and parser
@ -197,7 +217,7 @@ def test_subparsers_patch_add(parser: argparse.ArgumentParser) -> None:
"""
patch-add command must imply action, architecture list, lock and no-report
"""
args = parser.parse_args(["patch-add", "ahriman"])
args = parser.parse_args(["patch-add", "ahriman", "version"])
assert args.action == Action.Update
assert args.architecture == [""]
assert args.lock is None
@ -208,18 +228,10 @@ def test_subparsers_patch_add_architecture(parser: argparse.ArgumentParser) -> N
"""
patch-add command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "patch-add", "ahriman"])
args = parser.parse_args(["-a", "x86_64", "patch-add", "ahriman", "version"])
assert args.architecture == [""]
def test_subparsers_patch_add_track(parser: argparse.ArgumentParser) -> None:
"""
patch-add command must correctly parse track files patterns
"""
args = parser.parse_args(["patch-add", "-t", "*.py", "ahriman"])
assert args.track == ["*.diff", "*.patch", "*.py"]
def test_subparsers_patch_list(parser: argparse.ArgumentParser) -> None:
"""
patch-list command must imply action, architecture list, lock and no-report
@ -258,6 +270,42 @@ def test_subparsers_patch_remove_architecture(parser: argparse.ArgumentParser) -
assert args.architecture == [""]
def test_subparsers_patch_set_add(parser: argparse.ArgumentParser) -> None:
"""
patch-set-add command must imply action, architecture list, lock, no-report and variable
"""
args = parser.parse_args(["patch-set-add", "ahriman"])
assert args.action == Action.Update
assert args.architecture == [""]
assert args.lock is None
assert args.no_report
assert args.variable is None
def test_subparsers_patch_set_add_architecture(parser: argparse.ArgumentParser) -> None:
"""
patch-set-add command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "patch-set-add", "ahriman"])
assert args.architecture == [""]
def test_subparsers_patch_set_add_option_package(parser: argparse.ArgumentParser) -> None:
"""
patch-set-add command must convert package option to path instance
"""
args = parser.parse_args(["patch-set-add", "ahriman"])
assert isinstance(args.package, Path)
def test_subparsers_patch_set_add_option_track(parser: argparse.ArgumentParser) -> None:
"""
patch-set-add command must correctly parse track files patterns
"""
args = parser.parse_args(["patch-set-add", "-t", "*.py", "ahriman"])
assert args.track == ["*.diff", "*.patch", "*.py"]
def test_subparsers_repo_backup(parser: argparse.ArgumentParser) -> None:
"""
repo-backup command must imply architecture list, lock, no-report and unsafe

View File

@ -6,6 +6,7 @@ from unittest import mock
from ahriman.core.build_tools.sources import Sources
from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.remote_source import RemoteSource
from ahriman.models.repository_paths import RepositoryPaths
@ -16,22 +17,18 @@ def test_extend_architectures(mocker: MockerFixture) -> None:
"""
mocker.patch("pathlib.Path.is_file", return_value=True)
archs_mock = mocker.patch("ahriman.models.package.Package.supported_architectures", return_value={"x86_64"})
write_mock = mocker.patch("ahriman.models.pkgbuild_patch.PkgbuildPatch.write")
Sources.extend_architectures(Path("local"), "i686")
assert Sources.extend_architectures(Path("local"), "i686") == [PkgbuildPatch("arch", list({"x86_64", "i686"}))]
archs_mock.assert_called_once_with(Path("local"))
write_mock.assert_called_once_with(Path("local") / "PKGBUILD")
def test_extend_architectures_skip(mocker: MockerFixture) -> None:
def test_extend_architectures_any(mocker: MockerFixture) -> None:
"""
must skip extending list of the architectures in case if no PKGBUILD file found
must skip architecture patching in case if there is any architecture
"""
mocker.patch("pathlib.Path.is_file", return_value=False)
write_mock = mocker.patch("ahriman.models.pkgbuild_patch.PkgbuildPatch.write")
Sources.extend_architectures(Path("local"), "i686")
write_mock.assert_not_called()
mocker.patch("pathlib.Path.is_file", return_value=True)
mocker.patch("ahriman.models.package.Package.supported_architectures", return_value={"any"})
assert Sources.extend_architectures(Path("local"), "i686") == []
def test_fetch_empty(remote_source: RemoteSource, mocker: MockerFixture) -> None:
@ -155,15 +152,16 @@ def test_load(package_ahriman: Package, repository_paths: RepositoryPaths, mocke
"""
must load packages sources correctly
"""
mocker.patch("pathlib.Path.is_dir", return_value=False)
patch = PkgbuildPatch(None, "patch")
path = Path("local")
fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
patch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_apply")
architectures_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.extend_architectures")
architectures_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.extend_architectures", return_value=[])
Sources.load(Path("local"), package_ahriman, "patch", repository_paths)
fetch_mock.assert_called_once_with(Path("local"), package_ahriman.remote)
patch_mock.assert_called_once_with(Path("local"), "patch")
architectures_mock.assert_called_once_with(Path("local"), repository_paths.architecture)
Sources.load(path, package_ahriman, [patch], repository_paths)
fetch_mock.assert_called_once_with(path, package_ahriman.remote)
patch_mock.assert_called_once_with(path, patch)
architectures_mock.assert_called_once_with(path, repository_paths.architecture)
def test_load_no_patch(package_ahriman: Package, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
@ -172,9 +170,10 @@ def test_load_no_patch(package_ahriman: Package, repository_paths: RepositoryPat
"""
mocker.patch("pathlib.Path.is_dir", return_value=False)
mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
mocker.patch("ahriman.core.build_tools.sources.Sources.extend_architectures", return_value=[])
patch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_apply")
Sources.load(Path("local"), package_ahriman, None, repository_paths)
Sources.load(Path("local"), package_ahriman, [], repository_paths)
patch_mock.assert_not_called()
@ -185,8 +184,9 @@ def test_load_with_cache(package_ahriman: Package, repository_paths: RepositoryP
mocker.patch("pathlib.Path.is_dir", return_value=True)
copytree_mock = mocker.patch("shutil.copytree")
mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
mocker.patch("ahriman.core.build_tools.sources.Sources.extend_architectures", return_value=[])
Sources.load(Path("local"), package_ahriman, None, repository_paths)
Sources.load(Path("local"), package_ahriman, [], repository_paths)
copytree_mock.assert_called_once() # we do not check full command here, sorry
@ -211,6 +211,23 @@ def test_patch_create_with_newline(mocker: MockerFixture) -> None:
assert Sources.patch_create(Path("local"), "glob").endswith("\n")
def test_push(package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must correctly push files to remote repository
"""
add_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.add")
commit_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.commit")
check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output")
local = Path("local")
Sources.push(Path("local"), package_ahriman.remote, "glob")
add_mock.assert_called_once_with(local, "glob")
commit_mock.assert_called_once_with(local)
check_output_mock.assert_called_once_with(
"git", "push", package_ahriman.remote.git_url, package_ahriman.remote.branch,
exception=None, cwd=local, logger=pytest.helpers.anyvar(int))
def test_add(sources: Sources, mocker: MockerFixture) -> None:
"""
must add files to git
@ -237,6 +254,35 @@ def test_add_skip(sources: Sources, mocker: MockerFixture) -> None:
check_output_mock.assert_not_called()
def test_commit(sources: Sources, mocker: MockerFixture) -> None:
"""
must commit changes
"""
check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output")
local = Path("local")
commit_message = "Commit message"
sources.commit(local, commit_message=commit_message)
check_output_mock.assert_called_once_with(
"git", "commit", "--all", "--message", commit_message,
exception=None, cwd=local, logger=pytest.helpers.anyvar(int)
)
def test_commit_autogenerated(sources: Sources, mocker: MockerFixture) -> None:
"""
must commit changes with autogenerated commit message
"""
check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output")
local = Path("local")
sources.commit(Path("local"))
check_output_mock.assert_called_once_with(
"git", "commit", "--all", "--message", pytest.helpers.anyvar(str, strict=True),
exception=None, cwd=local, logger=pytest.helpers.anyvar(int)
)
def test_diff(sources: Sources, mocker: MockerFixture) -> None:
"""
must calculate diff
@ -273,11 +319,24 @@ def test_patch_apply(sources: Sources, mocker: MockerFixture) -> None:
"""
must apply patches if any
"""
patch = PkgbuildPatch(None, "patch")
check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output")
local = Path("local")
sources.patch_apply(local, "patches")
sources.patch_apply(local, patch)
check_output_mock.assert_called_once_with(
"git", "apply", "--ignore-space-change", "--ignore-whitespace",
exception=None, cwd=local, input_data="patches", logger=pytest.helpers.anyvar(int)
exception=None, cwd=local, input_data=patch.value, logger=pytest.helpers.anyvar(int)
)
def test_patch_apply_function(sources: Sources, mocker: MockerFixture) -> None:
"""
must apply single-function patches
"""
patch = PkgbuildPatch("version", "42")
local = Path("local")
write_mock = mocker.patch("ahriman.models.pkgbuild_patch.PkgbuildPatch.write")
sources.patch_apply(local, patch)
write_mock.assert_called_once_with(local / "PKGBUILD")

View File

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

View File

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

View File

@ -1,55 +1,96 @@
from ahriman.core.database import SQLite
from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
def test_patches_get_insert(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must insert patch to database
"""
database.patches_insert(package_ahriman.base, "patch_1")
database.patches_insert(package_python_schedule.base, "patch_2")
assert database.patches_get(package_ahriman.base) == "patch_1"
assert not database.build_queue_get()
database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch_1"))
database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch_3"))
database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch_2"))
assert database.patches_get(package_ahriman.base) == [
PkgbuildPatch(None, "patch_1"), PkgbuildPatch("key", "patch_3")
]
def test_patches_list(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must list all patches
"""
database.patches_insert(package_ahriman.base, "patch1")
database.patches_insert(package_python_schedule.base, "patch2")
assert database.patches_list(None) == {package_ahriman.base: "patch1", package_python_schedule.base: "patch2"}
database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch3"))
database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2"))
assert database.patches_list(None, []) == {
package_ahriman.base: [PkgbuildPatch(None, "patch1"), PkgbuildPatch("key", "patch3")],
package_python_schedule.base: [PkgbuildPatch(None, "patch2")],
}
def test_patches_list_filter(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must list all patches filtered by package name (same as get)
"""
database.patches_insert(package_ahriman.base, "patch1")
database.patches_insert(package_python_schedule.base, "patch2")
database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2"))
assert database.patches_list(package_ahriman.base) == {package_ahriman.base: "patch1"}
assert database.patches_list(package_python_schedule.base) == {package_python_schedule.base: "patch2"}
assert database.patches_list(package_ahriman.base, []) == {package_ahriman.base: [PkgbuildPatch(None, "patch1")]}
assert database.patches_list(package_python_schedule.base, []) == {
package_python_schedule.base: [PkgbuildPatch(None, "patch2")],
}
def test_patches_list_filter_by_variable(database: SQLite, package_ahriman: Package,
package_python_schedule: Package) -> None:
"""
must list all patches filtered by package name (same as get)
"""
database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch2"))
database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch3"))
assert database.patches_list(None, []) == {
package_ahriman.base: [PkgbuildPatch(None, "patch1"), PkgbuildPatch("key", "patch2")],
package_python_schedule.base: [PkgbuildPatch(None, "patch3")],
}
assert database.patches_list(None, ["key"]) == {
package_ahriman.base: [PkgbuildPatch("key", "patch2")],
}
def test_patches_insert_remove(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must remove patch from database
"""
database.patches_insert(package_ahriman.base, "patch_1")
database.patches_insert(package_python_schedule.base, "patch_2")
database.patches_remove(package_ahriman.base)
database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2"))
database.patches_remove(package_ahriman.base, [])
assert database.patches_get(package_ahriman.base) is None
database.patches_insert(package_python_schedule.base, "patch_2")
assert database.patches_get(package_ahriman.base) == []
assert database.patches_get(package_python_schedule.base) == [PkgbuildPatch(None, "patch2")]
def test_patches_insert_remove_by_variable(database: SQLite, package_ahriman: Package,
package_python_schedule: Package) -> None:
"""
must remove patch from database by variable
"""
database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch3"))
database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2"))
database.patches_remove(package_ahriman.base, ["key"])
assert database.patches_get(package_ahriman.base) == [PkgbuildPatch(None, "patch1")]
assert database.patches_get(package_python_schedule.base) == [PkgbuildPatch(None, "patch2")]
def test_patches_insert_insert(database: SQLite, package_ahriman: Package) -> None:
"""
must update patch in database
"""
database.patches_insert(package_ahriman.base, "patch_1")
assert database.patches_get(package_ahriman.base) == "patch_1"
database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
assert database.patches_get(package_ahriman.base) == [PkgbuildPatch(None, "patch1")]
database.patches_insert(package_ahriman.base, "patch_2")
assert database.patches_get(package_ahriman.base) == "patch_2"
database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch2"))
assert database.patches_get(package_ahriman.base) == [PkgbuildPatch(None, "patch2")]

View File

@ -1,10 +1,11 @@
import pytest
from ahriman.core.formatters import AurPrinter, ConfigurationPrinter, PackagePrinter, StatusPrinter, StringPrinter, \
UpdatePrinter, UserPrinter, VersionPrinter
from ahriman.core.formatters import AurPrinter, ConfigurationPrinter, PackagePrinter, PatchPrinter, StatusPrinter, \
StringPrinter, UpdatePrinter, UserPrinter, VersionPrinter
from ahriman.models.aur_package import AURPackage
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.user import User
@ -47,6 +48,20 @@ def package_ahriman_printer(package_ahriman: Package) -> PackagePrinter:
return PackagePrinter(package_ahriman, BuildStatus())
@pytest.fixture
def patch_printer(package_ahriman: Package) -> PatchPrinter:
"""
fixture for patch printer
Args:
package_ahriman(Package): package fixture
Returns:
PatchPrinter: patch printer test instance
"""
return PatchPrinter(package_ahriman.base, [PkgbuildPatch("key", "value")])
@pytest.fixture
def status_printer() -> StatusPrinter:
"""

View File

@ -0,0 +1,22 @@
from ahriman.core.formatters import PatchPrinter
def test_properties(patch_printer: PatchPrinter) -> None:
"""
must return non empty properties list
"""
assert patch_printer.properties()
def test_properties_required(patch_printer: PatchPrinter) -> None:
"""
must return all properties as required
"""
assert all(prop.is_required for prop in patch_printer.properties())
def test_title(patch_printer: PatchPrinter) -> None:
"""
must return non empty title
"""
assert patch_printer.title() == "ahriman"

View File

@ -0,0 +1,58 @@
import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from unittest import mock
from ahriman.core.configuration import Configuration
from ahriman.core.gitremote import RemotePullTrigger
def test_on_start(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must clone repo on start
"""
clone_mock = mocker.patch("ahriman.core.gitremote.RemotePullTrigger.repo_clone")
trigger = RemotePullTrigger("x86_64", configuration)
trigger.on_start()
clone_mock.assert_called_once_with()
def test_repo_clone(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must clone repository locally and copy its content
"""
fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
copy_mock = mocker.patch("ahriman.core.gitremote.RemotePullTrigger.repo_copy")
trigger = RemotePullTrigger("x86_64", configuration)
trigger.repo_clone()
fetch_mock.assert_called_once_with(pytest.helpers.anyvar(int), trigger.remote_source)
copy_mock.assert_called_once_with(pytest.helpers.anyvar(int))
def test_repo_copy(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must copy repository tree from temporary directory to the local cache
"""
mocker.patch("ahriman.core.gitremote.remote_pull_trigger.walk", return_value=[
Path("local") / "package1" / "PKGBUILD",
Path("local") / "package1" / ".SRCINFO",
Path("local") / "package2" / ".SRCINFO",
Path("local") / "package3" / "PKGBUILD",
Path("local") / "package3" / ".SRCINFO",
])
copytree_mock = mocker.patch("shutil.copytree")
init_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.init")
trigger = RemotePullTrigger("x86_64", configuration)
trigger.repo_copy(Path("local"))
copytree_mock.assert_has_calls([
mock.call(Path("local") / "package1", configuration.repository_paths.cache_for("package1"), dirs_exist_ok=True),
mock.call(Path("local") / "package3", configuration.repository_paths.cache_for("package3"), dirs_exist_ok=True),
])
init_mock.assert_has_calls([
mock.call(configuration.repository_paths.cache_for("package1")),
mock.call(configuration.repository_paths.cache_for("package3")),
])

View File

@ -0,0 +1,56 @@
import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from unittest import mock
from ahriman.core.configuration import Configuration
from ahriman.core.gitremote import RemotePushTrigger
from ahriman.models.package import Package
from ahriman.models.result import Result
def test_package_update(package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must update single package
"""
rmtree_mock = mocker.patch("shutil.rmtree")
fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
copytree_mock = mocker.patch("shutil.copytree")
local = Path("local")
RemotePushTrigger.package_update(package_ahriman, local)
rmtree_mock.assert_has_calls([
mock.call(local / package_ahriman.base, ignore_errors=True),
mock.call(pytest.helpers.anyvar(int), onerror=pytest.helpers.anyvar(int)), # removal of the TemporaryDirectory
mock.call(local / package_ahriman.base / ".git", ignore_errors=True),
])
fetch_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman.remote)
copytree_mock.assert_called_once_with(pytest.helpers.anyvar(int), local / package_ahriman.base)
def test_packages_update(result: Result, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must generate packages update
"""
update_mock = mocker.patch("ahriman.core.gitremote.RemotePushTrigger.package_update",
return_value=[package_ahriman.base])
local = Path("local")
assert list(RemotePushTrigger.packages_update(result, local))
update_mock.assert_called_once_with(package_ahriman, local)
def test_on_result(configuration: Configuration, result: Result, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must push changes on result
"""
mocker.patch("ahriman.core.gitremote.RemotePushTrigger.packages_update", return_value=[package_ahriman.base])
fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
push_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.push")
trigger = RemotePushTrigger("x86_64", configuration)
trigger.on_result(result, [package_ahriman])
fetch_mock.assert_called_once_with(pytest.helpers.anyvar(int), trigger.remote_source)
push_mock.assert_called_once_with(pytest.helpers.anyvar(int), trigger.remote_source, package_ahriman.base)

View File

@ -4,7 +4,7 @@ from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import ReportFailed
from ahriman.core.report import Report
from ahriman.core.report.report import Report
from ahriman.models.report_settings import ReportSettings
from ahriman.models.result import Result
@ -23,7 +23,7 @@ def test_report_dummy(configuration: Configuration, result: Result, mocker: Mock
must construct dummy report class
"""
mocker.patch("ahriman.models.report_settings.ReportSettings.from_option", return_value=ReportSettings.Disabled)
report_mock = mocker.patch("ahriman.core.report.Report.generate")
report_mock = mocker.patch("ahriman.core.report.report.Report.generate")
Report.load("x86_64", configuration, "disabled").run(result, [])
report_mock.assert_called_once_with([], result)

View File

@ -5,13 +5,13 @@ from ahriman.core.report import ReportTrigger
from ahriman.models.result import Result
def test_run(configuration: Configuration, mocker: MockerFixture) -> None:
def test_on_result(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run report for specified targets
"""
configuration.set_option("report", "target", "email")
run_mock = mocker.patch("ahriman.core.report.Report.run")
run_mock = mocker.patch("ahriman.core.report.report.Report.run")
trigger = ReportTrigger("x86_64", configuration)
trigger.run(Result(), [])
trigger.on_result(Result(), [])
run_mock.assert_called_once_with(Result(), [])

View File

@ -4,10 +4,8 @@ from pathlib import Path
from pytest_mock import MockerFixture
from unittest import mock
from ahriman.core.report import Report
from ahriman.core.repository.executor import Executor
from ahriman.models.package import Package
from ahriman.models.result import Result
def test_load_archives(executor: Executor) -> None:
@ -74,7 +72,7 @@ def test_process_remove_base(executor: Executor, package_ahriman: Package, mocke
# must update status and remove package files
tree_clear_mock.assert_called_once_with(package_ahriman.base)
build_queue_mock.assert_called_once_with(package_ahriman.base)
patches_mock.assert_called_once_with(package_ahriman.base)
patches_mock.assert_called_once_with(package_ahriman.base, [])
status_client_mock.assert_called_once_with(package_ahriman.base)
@ -144,17 +142,6 @@ def test_process_remove_nothing(executor: Executor, package_ahriman: Package, pa
repo_remove_mock.assert_not_called()
def test_process_triggers(executor: Executor, package_ahriman: Package, result: Result, mocker: MockerFixture) -> None:
"""
must process report
"""
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
triggers_mock = mocker.patch("ahriman.core.triggers.TriggerLoader.process")
executor.process_triggers(result)
triggers_mock.assert_called_once_with(result, [package_ahriman])
def test_process_update(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must run update process

View File

@ -42,7 +42,7 @@ def test_packages_add(spawner: Spawn, mocker: MockerFixture) -> None:
"""
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn.spawn_process")
spawner.packages_add(["ahriman", "linux"], now=False)
spawn_mock.assert_called_once_with("add", "ahriman", "linux", source="aur")
spawn_mock.assert_called_once_with("package-add", "ahriman", "linux", source="aur")
def test_packages_add_with_build(spawner: Spawn, mocker: MockerFixture) -> None:
@ -51,7 +51,16 @@ def test_packages_add_with_build(spawner: Spawn, mocker: MockerFixture) -> None:
"""
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn.spawn_process")
spawner.packages_add(["ahriman", "linux"], now=True)
spawn_mock.assert_called_once_with("add", "ahriman", "linux", source="aur", now="")
spawn_mock.assert_called_once_with("package-add", "ahriman", "linux", source="aur", now="")
def test_packages_add_update(spawner: Spawn, mocker: MockerFixture) -> None:
"""
must call repo update
"""
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn.spawn_process")
spawner.packages_add([], now=False)
spawn_mock.assert_called_once_with("repo-update")
def test_packages_remove(spawner: Spawn, mocker: MockerFixture) -> None:
@ -60,7 +69,7 @@ def test_packages_remove(spawner: Spawn, mocker: MockerFixture) -> None:
"""
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn.spawn_process")
spawner.packages_remove(["ahriman", "linux"])
spawn_mock.assert_called_once_with("remove", "ahriman", "linux")
spawn_mock.assert_called_once_with("package-remove", "ahriman", "linux")
def test_spawn_process(spawner: Spawn, mocker: MockerFixture) -> None:

View File

@ -49,8 +49,7 @@ def test_leaf_load(package_ahriman: Package, repository_paths: RepositoryPaths,
leaf = Leaf.load(package_ahriman, repository_paths, database)
assert leaf.package == package_ahriman
assert leaf.dependencies == {"ahriman-dependency"}
load_mock.assert_called_once_with(
pytest.helpers.anyvar(int), package_ahriman, None, repository_paths)
load_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman, [], repository_paths)
dependencies_mock.assert_called_once_with(pytest.helpers.anyvar(int))

View File

@ -1,12 +1,36 @@
import pytest
from unittest.mock import MagicMock
from ahriman.core.triggers import Trigger
from ahriman.models.result import Result
def test_run(trigger: Trigger) -> None:
def test_on_result(trigger: Trigger) -> None:
"""
must raise NotImplemented for missing rum method
must pass execution nto run method
"""
with pytest.raises(NotImplementedError):
trigger.run(Result(), [])
trigger.on_result(Result(), [])
def test_on_result_run(trigger: Trigger) -> None:
"""
must fallback to run method if it exists
"""
run_mock = MagicMock()
setattr(trigger, "run", run_mock)
trigger.on_result(Result(), [])
run_mock.assert_called_once_with(Result(), [])
def test_on_start(trigger: Trigger) -> None:
"""
must do nothing for not implemented method on_start
"""
trigger.on_start()
def test_on_stop(trigger: Trigger) -> None:
"""
must do nothing for not implemented method on_stop
"""
trigger.on_stop()

View File

@ -3,6 +3,7 @@ import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import InvalidExtension
from ahriman.core.triggers import TriggerLoader
from ahriman.models.package import Package
@ -75,27 +76,75 @@ def test_load_trigger_path_not_found(trigger_loader: TriggerLoader) -> None:
trigger_loader.load_trigger("/some/random/path.py.SomeRandomModule")
def test_process(trigger_loader: TriggerLoader, package_ahriman: Package, mocker: MockerFixture) -> None:
def test_on_result(trigger_loader: TriggerLoader, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must run triggers
"""
upload_mock = mocker.patch("ahriman.core.upload.UploadTrigger.run")
report_mock = mocker.patch("ahriman.core.report.ReportTrigger.run")
upload_mock = mocker.patch("ahriman.core.upload.UploadTrigger.on_result")
report_mock = mocker.patch("ahriman.core.report.ReportTrigger.on_result")
trigger_loader.process(Result(), [package_ahriman])
trigger_loader.on_result(Result(), [package_ahriman])
report_mock.assert_called_once_with(Result(), [package_ahriman])
upload_mock.assert_called_once_with(Result(), [package_ahriman])
def test_process_exception(trigger_loader: TriggerLoader, package_ahriman: Package, mocker: MockerFixture) -> None:
def test_on_result_exception(trigger_loader: TriggerLoader, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must suppress exception during trigger run
"""
upload_mock = mocker.patch("ahriman.core.upload.UploadTrigger.run", side_effect=Exception())
report_mock = mocker.patch("ahriman.core.report.ReportTrigger.run")
upload_mock = mocker.patch("ahriman.core.upload.UploadTrigger.on_result", side_effect=Exception())
report_mock = mocker.patch("ahriman.core.report.ReportTrigger.on_result")
log_mock = mocker.patch("logging.Logger.exception")
trigger_loader.process(Result(), [package_ahriman])
trigger_loader.on_result(Result(), [package_ahriman])
report_mock.assert_called_once_with(Result(), [package_ahriman])
upload_mock.assert_called_once_with(Result(), [package_ahriman])
log_mock.assert_called_once()
def test_on_start(trigger_loader: TriggerLoader, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must run triggers on start
"""
upload_mock = mocker.patch("ahriman.core.upload.UploadTrigger.on_start")
report_mock = mocker.patch("ahriman.core.report.ReportTrigger.on_start")
trigger_loader.on_start()
assert trigger_loader._on_stop_requested
report_mock.assert_called_once_with()
upload_mock.assert_called_once_with()
def test_on_stop_with_on_start(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must call on_stop on exit if on_start was called
"""
on_stop_mock = mocker.patch("ahriman.core.triggers.trigger_loader.TriggerLoader.on_stop")
trigger_loader = TriggerLoader("x86_64", configuration)
trigger_loader.on_start()
del trigger_loader
on_stop_mock.assert_called_once_with()
def test_on_stop_without_on_start(configuration: Configuration, mocker: MockerFixture) -> None:
"""
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")
trigger_loader = TriggerLoader("x86_64", configuration)
del trigger_loader
on_stop_mock.assert_not_called()
def test_on_stop(trigger_loader: TriggerLoader, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must run triggers on stop
"""
upload_mock = mocker.patch("ahriman.core.upload.UploadTrigger.on_stop")
report_mock = mocker.patch("ahriman.core.report.ReportTrigger.on_stop")
trigger_loader.on_stop()
report_mock.assert_called_once_with()
upload_mock.assert_called_once_with()

View File

@ -5,7 +5,7 @@ from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import SyncFailed
from ahriman.core.upload import Upload
from ahriman.core.upload.upload import Upload
from ahriman.models.upload_settings import UploadSettings
@ -23,7 +23,7 @@ def test_report_dummy(configuration: Configuration, mocker: MockerFixture) -> No
must construct dummy upload class
"""
mocker.patch("ahriman.models.upload_settings.UploadSettings.from_option", return_value=UploadSettings.Disabled)
upload_mock = mocker.patch("ahriman.core.upload.Upload.sync")
upload_mock = mocker.patch("ahriman.core.upload.upload.Upload.sync")
Upload.load("x86_64", configuration, "disabled").run(Path("path"), [])
upload_mock.assert_called_once_with(Path("path"), [])

View File

@ -5,13 +5,13 @@ from ahriman.core.upload import UploadTrigger
from ahriman.models.result import Result
def test_run(configuration: Configuration, mocker: MockerFixture) -> None:
def test_on_result(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run report for specified targets
"""
configuration.set_option("upload", "target", "rsync")
run_mock = mocker.patch("ahriman.core.upload.Upload.run")
run_mock = mocker.patch("ahriman.core.upload.upload.Upload.run")
trigger = UploadTrigger("x86_64", configuration)
trigger.run(Result(), [])
trigger.on_result(Result(), [])
run_mock.assert_called_once_with(configuration.repository_paths.repository, [])

View File

@ -5,6 +5,15 @@ from unittest.mock import MagicMock, call
from ahriman.models.pkgbuild_patch import PkgbuildPatch
def test_post_init() -> None:
"""
must remove empty keys
"""
assert PkgbuildPatch("", "value").key is None
assert PkgbuildPatch(None, "value").key is None
assert PkgbuildPatch("key", "value").key == "key"
def test_is_function() -> None:
"""
must correctly define key as function
@ -13,6 +22,14 @@ def test_is_function() -> None:
assert PkgbuildPatch("key()", "value").is_function
def test_is_plain_diff() -> None:
"""
must correctly define key as function
"""
assert not PkgbuildPatch("key", "value").is_plain_diff
assert PkgbuildPatch(None, "value").is_plain_diff
def test_quote() -> None:
"""
must quote strings if unsafe flag is not set
@ -32,6 +49,13 @@ def test_serialize() -> None:
assert PkgbuildPatch("key", "4'2", unsafe=True).serialize() == "key=4'2"
def test_serialize_plain_diff() -> None:
"""
must correctly serialize function values
"""
assert PkgbuildPatch(None, "{ value }").serialize() == "{ value }"
def test_serialize_function() -> None:
"""
must correctly serialize function values

View File

@ -27,17 +27,6 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None:
add_mock.assert_called_once_with(["ahriman"], now=True)
async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None:
"""
must raise exception on missing packages payload
"""
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
response = await client.post("/api/v1/service/add")
assert response.status == 400
add_mock.assert_not_called()
async def test_post_update(client: TestClient, mocker: MockerFixture) -> None:
"""
must call post request correctly for alias

View File

@ -31,6 +31,10 @@ root = ../../../
[sign]
target =
[gitremote]
pull_url = https://github.com/arcan1s/repository.git
push_url = https://github.com/arcan1s/repository.git
[report]
target =