Compare commits

..

3 Commits

Author SHA1 Message Date
32cbafd12b Release 0.20.0 2021-04-01 02:38:59 +03:00
880c70bd58 constistent classmethod and staticmethod usage
General idea is to use classmethod for every constructor and
statismethod otherwise.
Also use self and cls whenever it's possible to call static and class
methods
2021-03-31 04:29:08 +03:00
d449eb3c2e change arch specific section naming from section_arch to section:arch
Some archs can have _ in their name. Also in future we can use sections
with similar names
2021-03-31 02:31:14 +03:00
25 changed files with 206 additions and 189 deletions

View File

@ -1,6 +1,6 @@
# ahriman configuration # ahriman configuration
Some groups can be specified for each architecture separately. E.g. if there are `build` and `build_x86_64` groups it will use the option from `build_x86_64` for the `x86_64` architecture and `build` for any other (architecture specific group has higher priority). In case if both groups are presented, architecture specific options will be merged into global ones overriding them. Some groups can be specified for each architecture separately. E.g. if there are `build` and `build:x86_64` groups it will use the option from `build:x86_64` for the `x86_64` architecture and `build` for any other (architecture specific group has higher priority). In case if both groups are presented, architecture specific options will be merged into global ones overriding them.
## `settings` group ## `settings` group
@ -18,9 +18,9 @@ libalpm and AUR related configuration.
* `repositories` - list of pacman repositories, space separated list of strings, required. * `repositories` - list of pacman repositories, space separated list of strings, required.
* `root` - root for alpm library, string, required. * `root` - root for alpm library, string, required.
## `build_*` groups ## `build:*` groups
Build related configuration. Group name must refer to architecture, e.g. it should be `build_x86_64` for x86_64 architecture. Build related configuration. Group name must refer to architecture, e.g. it should be `build:x86_64` for x86_64 architecture.
* `archbuild_flags` - additional flags passed to `archbuild` command, space separated list of strings, optional. * `archbuild_flags` - additional flags passed to `archbuild` command, space separated list of strings, optional.
* `build_command` - default build command, string, required. * `build_command` - default build command, string, required.
@ -35,9 +35,9 @@ Base repository settings.
* `name` - repository name, string, required. * `name` - repository name, string, required.
* `root` - root path for application, string, required. * `root` - root path for application, string, required.
## `sign_*` groups ## `sign:*` groups
Settings for signing packages or repository. Group name must refer to architecture, e.g. it should be `sign_x86_64` for x86_64 architecture. Settings for signing packages or repository. Group name must refer to architecture, e.g. it should be `sign:x86_64` for x86_64 architecture.
* `target` - configuration flag to enable signing, space separated list of strings, required. Allowed values are `package` (sign each package separately), `repository` (sign repository database file). * `target` - configuration flag to enable signing, space separated list of strings, required. Allowed values are `package` (sign each package separately), `repository` (sign repository database file).
* `key` - default PGP key, string, required. This key will also be used for database signing if enabled. * `key` - default PGP key, string, required. This key will also be used for database signing if enabled.
@ -49,9 +49,9 @@ Report generation settings.
* `target` - list of reports to be generated, space separated list of strings, optional. Allowed values are `html`. * `target` - list of reports to be generated, space separated list of strings, optional. Allowed values are `html`.
### `html_*` groups ### `html:*` groups
Group name must refer to architecture, e.g. it should be `html_x86_64` for x86_64 architecture. Group name must refer to architecture, e.g. it should be `html:x86_64` for x86_64 architecture.
* `path` - path to html report file, string, required. * `path` - path to html report file, string, required.
* `homepage` - link to homepage, string, optional. * `homepage` - link to homepage, string, optional.
@ -64,23 +64,23 @@ Remote synchronization settings.
* `target` - list of synchronizations to be used, space separated list of strings, optional. Allowed values are `rsync`, `s3`. * `target` - list of synchronizations to be used, space separated list of strings, optional. Allowed values are `rsync`, `s3`.
### `rsync_*` groups ### `rsync:*` groups
Group name must refer to architecture, e.g. it should be `rsync_x86_64` for x86_64 architecture. Requires `rsync` package to be installed. Do not forget to configure ssh for user `ahriman`. Group name must refer to architecture, e.g. it should be `rsync:x86_64` for x86_64 architecture. Requires `rsync` package to be installed. Do not forget to configure ssh for user `ahriman`.
* `command` - rsync command to run, space separated list of string, required. * `command` - rsync command to run, space separated list of string, required.
* `remote` - remote server to rsync (e.g. `1.2.3.4:5678:path/to/sync`), string, required. * `remote` - remote server to rsync (e.g. `1.2.3.4:5678:path/to/sync`), string, required.
### `s3_*` groups ### `s3:*` groups
Group name must refer to architecture, e.g. it should be `s3_x86_64` for x86_64 architecture. Requires `aws-cli` package to be installed. Do not forget to configure it for user `ahriman`. Group name must refer to architecture, e.g. it should be `s3:x86_64` for x86_64 architecture. Requires `aws-cli` package to be installed. Do not forget to configure it for user `ahriman`.
* `command` - s3 command to run, space separated list of string, required. * `command` - s3 command to run, space separated list of string, required.
* `bucket` - bucket name (e.g. `s3://bucket/path`), string, required. * `bucket` - bucket name (e.g. `s3://bucket/path`), string, required.
## `web_*` groups ## `web:*` groups
Web server settings. If any of `host`/`port` is not set, web integration will be disabled. Group name must refer to architecture, e.g. it should be `web_x86_64` for x86_64 architecture. Web server settings. If any of `host`/`port` is not set, web integration will be disabled. Group name must refer to architecture, e.g. it should be `web:x86_64` for x86_64 architecture.
* `host` - host to bind, string, optional. * `host` - host to bind, string, optional.
* `port` - port to bind, int, optional. * `port` - port to bind, int, optional.

View File

@ -1,7 +1,7 @@
# Maintainer: Evgeniy Alekseev # Maintainer: Evgeniy Alekseev
pkgname='ahriman' pkgname='ahriman'
pkgver=0.19.0 pkgver=0.20.0
pkgrel=1 pkgrel=1
pkgdesc="ArcHlinux ReposItory MANager" pkgdesc="ArcHlinux ReposItory MANager"
arch=('any') arch=('any')
@ -23,7 +23,7 @@ optdepends=('aws-cli: sync to s3'
source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgname-$pkgver-src.tar.xz" source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgname-$pkgver-src.tar.xz"
'ahriman.sysusers' 'ahriman.sysusers'
'ahriman.tmpfiles') 'ahriman.tmpfiles')
sha512sums=('af644c52c990268f1190632ccd514f351283d5578b161aebd2819d02e9d6a041571d50fe54ca03568bdabecca2e0492222b1a88bffef6bc0eab4e7460193df61' sha512sums=('22d2d2ae5af4a5854eb08b3b97a5fe4faa85246a85ffa78fa350080cf40a42b77152a83e0184ba848a8d06a0cce8a02a6fd94e24982ebbcc80bb88901d229f25'
'13718afec2c6786a18f0b223ef8e58dccf0688bca4cdbe203f14071f5031ed20120eb0ce38b52c76cfd6e8b6581a9c9eaa2743eb11abbaca637451a84c33f075' '13718afec2c6786a18f0b223ef8e58dccf0688bca4cdbe203f14071f5031ed20120eb0ce38b52c76cfd6e8b6581a9c9eaa2743eb11abbaca637451a84c33f075'
'55b20f6da3d66e7bbf2add5d95a3b60632df121717d25a993e56e737d14f51fe063eb6f1b38bd81cc32e05db01c0c1d80aaa720c45cde87f238d8b46cdb8cbc4') '55b20f6da3d66e7bbf2add5d95a3b60632df121717d25a993e56e737d14f51fe063eb6f1b38bd81cc32e05db01c0c1d80aaa720c45cde87f238d8b46cdb8cbc4')
backup=('etc/ahriman.ini' backup=('etc/ahriman.ini'

View File

@ -80,7 +80,7 @@ class Lock:
:param exc_tb: exception traceback if any :param exc_tb: exception traceback if any
:return: always False (do not suppress any exception) :return: always False (do not suppress any exception)
""" """
self.remove() self.clear()
status = BuildStatusEnum.Success if exc_val is None else BuildStatusEnum.Failed status = BuildStatusEnum.Success if exc_val is None else BuildStatusEnum.Failed
self.reporter.update_self(status) self.reporter.update_self(status)
return False return False
@ -96,6 +96,14 @@ class Lock:
if current_uid != root_uid: if current_uid != root_uid:
raise UnsafeRun(current_uid, root_uid) raise UnsafeRun(current_uid, root_uid)
def clear(self) -> None:
"""
remove lock file
"""
if self.path is None:
return
self.path.unlink(missing_ok=True)
def create(self) -> None: def create(self) -> None:
""" """
create lock file create lock file
@ -106,11 +114,3 @@ class Lock:
self.path.touch(exist_ok=self.force) self.path.touch(exist_ok=self.force)
except FileExistsError: except FileExistsError:
raise DuplicateRun() raise DuplicateRun()
def remove(self) -> None:
"""
remove lock file
"""
if self.path is None:
return
self.path.unlink(missing_ok=True)

View File

@ -123,4 +123,4 @@ class Task:
if self.cache_path.is_dir(): if self.cache_path.is_dir():
# no need to clone whole repository, just copy from cache first # no need to clone whole repository, just copy from cache first
shutil.copytree(self.cache_path, git_path) shutil.copytree(self.cache_path, git_path)
return Task.fetch(git_path, self.package.git_url) return self.fetch(git_path, self.package.git_url)

View File

@ -84,7 +84,7 @@ class Configuration(configparser.RawConfigParser):
:param architecture: repository architecture :param architecture: repository architecture
:return: correct section name for repository specific section :return: correct section name for repository specific section
""" """
return f"{section}_{architecture}" return f"{section}:{architecture}"
def dump(self) -> Dict[str, Dict[str, str]]: def dump(self) -> Dict[str, Dict[str, str]]:
""" """
@ -183,6 +183,6 @@ class Configuration(configparser.RawConfigParser):
# remove any arch specific section # remove any arch specific section
for foreign in self.sections(): for foreign in self.sections():
# we would like to use lambda filter here, but pylint is too dumb # we would like to use lambda filter here, but pylint is too dumb
if not foreign.startswith(f"{section}_"): if not foreign.startswith(f"{section}:"):
continue continue
self.remove_section(foreign) self.remove_section(foreign)

View File

@ -17,9 +17,11 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from __future__ import annotations
import logging import logging
from typing import Iterable from typing import Iterable, Type
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import ReportFailed from ahriman.core.exceptions import ReportFailed
@ -45,30 +47,34 @@ class Report:
self.architecture = architecture self.architecture = architecture
self.configuration = configuration self.configuration = configuration
@staticmethod @classmethod
def run(architecture: str, configuration: Configuration, target: str, packages: Iterable[Package]) -> None: def load(cls: Type[Report], architecture: str, configuration: Configuration, target: str) -> Report:
""" """
run report generation load client from settings
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param target: target to generate report (e.g. html) :param target: target to generate report (e.g. html)
:param packages: list of packages to generate report :return: client according to current settings
""" """
provider = ReportSettings.from_option(target) provider = ReportSettings.from_option(target)
if provider == ReportSettings.HTML: if provider == ReportSettings.HTML:
from ahriman.core.report.html import HTML from ahriman.core.report.html import HTML
report: Report = HTML(architecture, configuration) return HTML(architecture, configuration)
else: return cls(architecture, configuration) # should never happen
report = Report(architecture, configuration)
try:
report.generate(packages)
except Exception:
report.logger.exception(f"report generation failed for target {provider.name}")
raise ReportFailed()
def generate(self, packages: Iterable[Package]) -> None: def generate(self, packages: Iterable[Package]) -> None:
""" """
generate report for the specified packages generate report for the specified packages
:param packages: list of packages to generate report :param packages: list of packages to generate report
""" """
def run(self, packages: Iterable[Package]) -> None:
"""
run report generation
:param packages: list of packages to generate report
"""
try:
self.generate(packages)
except Exception:
self.logger.exception("report generation failed")
raise ReportFailed()

View File

@ -108,7 +108,8 @@ class Executor(Cleaner):
if targets is None: if targets is None:
targets = self.configuration.getlist("report", "target") targets = self.configuration.getlist("report", "target")
for target in targets: for target in targets:
Report.run(self.architecture, self.configuration, target, self.packages()) runner = Report.load(self.architecture, self.configuration, target)
runner.run(self.packages())
def process_sync(self, targets: Optional[Iterable[str]]) -> None: def process_sync(self, targets: Optional[Iterable[str]]) -> None:
""" """
@ -118,7 +119,8 @@ class Executor(Cleaner):
if targets is None: if targets is None:
targets = self.configuration.getlist("upload", "target") targets = self.configuration.getlist("upload", "target")
for target in targets: for target in targets:
Upload.run(self.architecture, self.configuration, target, self.paths.repository) runner = Upload.load(self.architecture, self.configuration, target)
runner.run(self.paths.repository)
def process_update(self, packages: Iterable[Path]) -> Path: def process_update(self, packages: Iterable[Path]) -> Path:
""" """

View File

@ -33,6 +33,7 @@ class Properties:
:ivar architecture: repository architecture :ivar architecture: repository architecture
:ivar aur_url: base AUR url :ivar aur_url: base AUR url
:ivar configuration: configuration instance :ivar configuration: configuration instance
:ivar ignore_list: package bases which will be ignored during auto updates
:ivar logger: class logger :ivar logger: class logger
:ivar name: repository name :ivar name: repository name
:ivar pacman: alpm wrapper instance :ivar pacman: alpm wrapper instance
@ -53,6 +54,7 @@ class Properties:
self.paths = RepositoryPaths(configuration.getpath("repository", "root"), architecture) self.paths = RepositoryPaths(configuration.getpath("repository", "root"), architecture)
self.paths.create_tree() self.paths.create_tree()
self.ignore_list = configuration.getlist("build", "ignore_packages")
self.pacman = Pacman(configuration) self.pacman = Pacman(configuration)
self.sign = GPG(architecture, configuration) self.sign = GPG(architecture, configuration)
self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args) self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args)

View File

@ -44,10 +44,8 @@ class UpdateHandler(Cleaner):
""" """
result: List[Package] = [] result: List[Package] = []
ignore_list = self.configuration.getlist("build", "ignore_packages")
for local in self.packages(): for local in self.packages():
if local.base in ignore_list: if local.base in self.ignore_list:
continue continue
if local.is_vcs and no_vcs: if local.is_vcs and no_vcs:
continue continue

View File

@ -19,7 +19,7 @@
# #
from __future__ import annotations from __future__ import annotations
from typing import List, Optional, Tuple from typing import List, Optional, Tuple, Type
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.build_status import BuildStatus, BuildStatusEnum
@ -31,6 +31,20 @@ class Client:
base build status reporter client base build status reporter client
""" """
@classmethod
def load(cls: Type[Client], configuration: Configuration) -> Client:
"""
load client from settings
:param configuration: configuration instance
:return: client according to current settings
"""
host = configuration.get("web", "host", fallback=None)
port = configuration.getint("web", "port", fallback=None)
if host is not None and port is not None:
from ahriman.core.status.web_client import WebClient
return WebClient(host, port)
return cls()
def add(self, package: Package, status: BuildStatusEnum) -> None: def add(self, package: Package, status: BuildStatusEnum) -> None:
""" """
add new package with status add new package with status
@ -109,18 +123,3 @@ class Client:
:param package: current package properties :param package: current package properties
""" """
return self.add(package, BuildStatusEnum.Unknown) return self.add(package, BuildStatusEnum.Unknown)
@staticmethod
def load(configuration: Configuration) -> Client:
"""
load client from settings
:param configuration: configuration instance
:return: client according to current settings
"""
host = configuration.get("web", "host", fallback=None)
port = configuration.getint("web", "port", fallback=None)
if host is None or port is None:
return Client()
from ahriman.core.status.web_client import WebClient
return WebClient(host, port)

View File

@ -17,9 +17,12 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from __future__ import annotations
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Type
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import SyncFailed from ahriman.core.exceptions import SyncFailed
@ -44,29 +47,33 @@ class Upload:
self.architecture = architecture self.architecture = architecture
self.config = configuration self.config = configuration
@staticmethod @classmethod
def run(architecture: str, configuration: Configuration, target: str, path: Path) -> None: def load(cls: Type[Upload], architecture: str, configuration: Configuration, target: str) -> Upload:
""" """
run remote sync load client from settings
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param target: target to run sync (e.g. s3) :param target: target to run sync (e.g. s3)
:param path: local path to sync :return: client according to current settings
""" """
provider = UploadSettings.from_option(target) provider = UploadSettings.from_option(target)
if provider == UploadSettings.Rsync: if provider == UploadSettings.Rsync:
from ahriman.core.upload.rsync import Rsync from ahriman.core.upload.rsync import Rsync
upload: Upload = Rsync(architecture, configuration) return Rsync(architecture, configuration)
elif provider == UploadSettings.S3: if provider == UploadSettings.S3:
from ahriman.core.upload.s3 import S3 from ahriman.core.upload.s3 import S3
upload = S3(architecture, configuration) return S3(architecture, configuration)
else: return cls(architecture, configuration) # should never happen
upload = Upload(architecture, configuration)
def run(self, path: Path) -> None:
"""
run remote sync
:param path: local path to sync
"""
try: try:
upload.sync(path) self.sync(path)
except Exception: except Exception:
upload.logger.exception(f"remote sync failed for {provider.name}") self.logger.exception("remote sync failed")
raise SyncFailed() raise SyncFailed()
def sync(self, path: Path) -> None: def sync(self, path: Path) -> None:

View File

@ -63,7 +63,7 @@ class BuildStatus:
""" """
build status holder build status holder
:ivar status: build status :ivar status: build status
:ivar _timestamp: build status update time :ivar timestamp: build status update time
""" """
def __init__(self, status: Union[BuildStatusEnum, str, None] = None, def __init__(self, status: Union[BuildStatusEnum, str, None] = None,

View File

@ -156,6 +156,27 @@ class Package:
aur_url=dump["aur_url"], aur_url=dump["aur_url"],
packages=packages) packages=packages)
@classmethod
def load(cls: Type[Package], path: Union[Path, str], pacman: Pacman, aur_url: str) -> Package:
"""
package constructor from available sources
:param path: one of path to sources directory, path to archive or package name/base
:param pacman: alpm wrapper instance (required to load from archive)
:param aur_url: AUR root url
:return: package properties
"""
try:
maybe_path = Path(path)
if maybe_path.is_dir():
return cls.from_build(maybe_path, aur_url)
if maybe_path.is_file():
return cls.from_archive(maybe_path, pacman, aur_url)
return cls.from_aur(str(path), aur_url)
except InvalidPackageInfo:
raise
except Exception as e:
raise InvalidPackageInfo(str(e))
@staticmethod @staticmethod
def dependencies(path: Path) -> Set[str]: def dependencies(path: Path) -> Set[str]:
""" """
@ -194,29 +215,6 @@ class Package:
prefix = f"{epoch}:" if epoch else "" prefix = f"{epoch}:" if epoch else ""
return f"{prefix}{pkgver}-{pkgrel}" return f"{prefix}{pkgver}-{pkgrel}"
@staticmethod
def load(path: Union[Path, str], pacman: Pacman, aur_url: str) -> Package:
"""
package constructor from available sources
:param path: one of path to sources directory, path to archive or package name/base
:param pacman: alpm wrapper instance (required to load from archive)
:param aur_url: AUR root url
:return: package properties
"""
try:
maybe_path = Path(path)
if maybe_path.is_dir():
package: Package = Package.from_build(maybe_path, aur_url)
elif maybe_path.is_file():
package = Package.from_archive(maybe_path, pacman, aur_url)
else:
package = Package.from_aur(str(path), aur_url)
return package
except InvalidPackageInfo:
raise
except Exception as e:
raise InvalidPackageInfo(str(e))
def actual_version(self, paths: RepositoryPaths) -> str: def actual_version(self, paths: RepositoryPaths) -> str:
""" """
additional method to handle VCS package versions additional method to handle VCS package versions

View File

@ -65,7 +65,7 @@ class PackageDescription:
:param path: path to package archive :param path: path to package archive
:return: package properties based on tarball :return: package properties based on tarball
""" """
return PackageDescription( return cls(
architecture=package.arch, architecture=package.arch,
archive_size=package.size, archive_size=package.size,
build_date=package.builddate, build_date=package.builddate,

View File

@ -20,6 +20,7 @@
from __future__ import annotations from __future__ import annotations
from enum import Enum, auto from enum import Enum, auto
from typing import Type
from ahriman.core.exceptions import InvalidOption from ahriman.core.exceptions import InvalidOption
@ -32,13 +33,13 @@ class ReportSettings(Enum):
HTML = auto() HTML = auto()
@staticmethod @classmethod
def from_option(value: str) -> ReportSettings: def from_option(cls: Type[ReportSettings], value: str) -> ReportSettings:
""" """
construct value from configuration construct value from configuration
:param value: configuration value :param value: configuration value
:return: parsed value :return: parsed value
""" """
if value.lower() in ("html",): if value.lower() in ("html",):
return ReportSettings.HTML return cls.HTML
raise InvalidOption(value) raise InvalidOption(value)

View File

@ -20,6 +20,7 @@
from __future__ import annotations from __future__ import annotations
from enum import Enum, auto from enum import Enum, auto
from typing import Type
from ahriman.core.exceptions import InvalidOption from ahriman.core.exceptions import InvalidOption
@ -34,15 +35,15 @@ class SignSettings(Enum):
SignPackages = auto() SignPackages = auto()
SignRepository = auto() SignRepository = auto()
@staticmethod @classmethod
def from_option(value: str) -> SignSettings: def from_option(cls: Type[SignSettings], value: str) -> SignSettings:
""" """
construct value from configuration construct value from configuration
:param value: configuration value :param value: configuration value
:return: parsed value :return: parsed value
""" """
if value.lower() in ("package", "packages", "sign-package"): if value.lower() in ("package", "packages", "sign-package"):
return SignSettings.SignPackages return cls.SignPackages
if value.lower() in ("repository", "sign-repository"): if value.lower() in ("repository", "sign-repository"):
return SignSettings.SignRepository return cls.SignRepository
raise InvalidOption(value) raise InvalidOption(value)

View File

@ -20,6 +20,7 @@
from __future__ import annotations from __future__ import annotations
from enum import Enum, auto from enum import Enum, auto
from typing import Type
from ahriman.core.exceptions import InvalidOption from ahriman.core.exceptions import InvalidOption
@ -34,15 +35,15 @@ class UploadSettings(Enum):
Rsync = auto() Rsync = auto()
S3 = auto() S3 = auto()
@staticmethod @classmethod
def from_option(value: str) -> UploadSettings: def from_option(cls: Type[UploadSettings], value: str) -> UploadSettings:
""" """
construct value from configuration construct value from configuration
:param value: configuration value :param value: configuration value
:return: parsed value :return: parsed value
""" """
if value.lower() in ("rsync",): if value.lower() in ("rsync",):
return UploadSettings.Rsync return cls.Rsync
if value.lower() in ("s3",): if value.lower() in ("s3",):
return UploadSettings.S3 return cls.S3
raise InvalidOption(value) raise InvalidOption(value)

View File

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

View File

@ -15,14 +15,14 @@ def test_enter(lock: Lock, mocker: MockerFixture) -> None:
must process with context manager must process with context manager
""" """
check_user_mock = mocker.patch("ahriman.application.lock.Lock.check_user") check_user_mock = mocker.patch("ahriman.application.lock.Lock.check_user")
remove_mock = mocker.patch("ahriman.application.lock.Lock.remove") clear_mock = mocker.patch("ahriman.application.lock.Lock.clear")
create_mock = mocker.patch("ahriman.application.lock.Lock.create") create_mock = mocker.patch("ahriman.application.lock.Lock.create")
update_status_mock = mocker.patch("ahriman.core.status.client.Client.update_self") update_status_mock = mocker.patch("ahriman.core.status.client.Client.update_self")
with lock: with lock:
pass pass
check_user_mock.assert_called_once() check_user_mock.assert_called_once()
remove_mock.assert_called_once() clear_mock.assert_called_once()
create_mock.assert_called_once() create_mock.assert_called_once()
update_status_mock.assert_has_calls([ update_status_mock.assert_has_calls([
mock.call(BuildStatusEnum.Building), mock.call(BuildStatusEnum.Building),
@ -35,7 +35,7 @@ def test_exit_with_exception(lock: Lock, mocker: MockerFixture) -> None:
must process with context manager in case if exception raised must process with context manager in case if exception raised
""" """
mocker.patch("ahriman.application.lock.Lock.check_user") mocker.patch("ahriman.application.lock.Lock.check_user")
mocker.patch("ahriman.application.lock.Lock.remove") mocker.patch("ahriman.application.lock.Lock.clear")
mocker.patch("ahriman.application.lock.Lock.create") mocker.patch("ahriman.application.lock.Lock.create")
update_status_mock = mocker.patch("ahriman.core.status.client.Client.update_self") update_status_mock = mocker.patch("ahriman.core.status.client.Client.update_self")
@ -79,6 +79,34 @@ def test_check_user_unsafe(lock: Lock) -> None:
lock.check_user() lock.check_user()
def test_clear(lock: Lock) -> None:
"""
must remove lock file
"""
lock.path = Path(tempfile.mktemp())
lock.path.touch()
lock.clear()
assert not lock.path.is_file()
def test_clear_missing(lock: Lock) -> None:
"""
must not fail on lock removal if file is missing
"""
lock.path = Path(tempfile.mktemp())
lock.clear()
def test_clear_skip(lock: Lock, mocker: MockerFixture) -> None:
"""
must skip removal if no file set
"""
unlink_mock = mocker.patch("pathlib.Path.unlink")
lock.clear()
unlink_mock.assert_not_called()
def test_create(lock: Lock) -> None: def test_create(lock: Lock) -> None:
""" """
must create lock must create lock
@ -121,31 +149,3 @@ def test_create_unsafe(lock: Lock) -> None:
lock.create() lock.create()
lock.path.unlink() lock.path.unlink()
def test_remove(lock: Lock) -> None:
"""
must remove lock file
"""
lock.path = Path(tempfile.mktemp())
lock.path.touch()
lock.remove()
assert not lock.path.is_file()
def test_remove_missing(lock: Lock) -> None:
"""
must not fail on lock removal if file is missing
"""
lock.path = Path(tempfile.mktemp())
lock.remove()
def test_remove_skip(lock: Lock, mocker: MockerFixture) -> None:
"""
must skip removal if no file set
"""
unlink_mock = mocker.patch("pathlib.Path.unlink")
lock.remove()
unlink_mock.assert_not_called()

View File

@ -15,7 +15,7 @@ def test_report_failure(configuration: Configuration, mocker: MockerFixture) ->
""" """
mocker.patch("ahriman.core.report.html.HTML.generate", side_effect=Exception()) mocker.patch("ahriman.core.report.html.HTML.generate", side_effect=Exception())
with pytest.raises(ReportFailed): with pytest.raises(ReportFailed):
Report.run("x86_64", configuration, ReportSettings.HTML.name, Path("path")) Report.load("x86_64", configuration, ReportSettings.HTML.name).run(Path("path"))
def test_report_html(configuration: Configuration, mocker: MockerFixture) -> None: def test_report_html(configuration: Configuration, mocker: MockerFixture) -> None:
@ -23,5 +23,5 @@ def test_report_html(configuration: Configuration, mocker: MockerFixture) -> Non
must generate html report must generate html report
""" """
report_mock = mocker.patch("ahriman.core.report.html.HTML.generate") report_mock = mocker.patch("ahriman.core.report.html.HTML.generate")
Report.run("x86_64", configuration, ReportSettings.HTML.name, Path("path")) Report.load("x86_64", configuration, ReportSettings.HTML.name).run(Path("path"))
report_mock.assert_called_once() report_mock.assert_called_once()

View File

@ -50,7 +50,7 @@ def test_updates_aur_ignore(update_handler: UpdateHandler, package_ahriman: Pack
""" """
must skip ignore packages must skip ignore packages
""" """
mocker.patch("ahriman.core.configuration.Configuration.getlist", return_value=[package_ahriman.base]) update_handler.ignore_list = [package_ahriman.base]
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman]) mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman])
package_load_mock = mocker.patch("ahriman.models.package.Package.load") package_load_mock = mocker.patch("ahriman.models.package.Package.load")

View File

@ -7,6 +7,22 @@ from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package import Package from ahriman.models.package import Package
def test_load_dummy_client(configuration: Configuration) -> None:
"""
must load dummy client if no settings set
"""
assert isinstance(Client.load(configuration), Client)
def test_load_full_client(configuration: Configuration) -> None:
"""
must load full client if no settings set
"""
configuration.set("web", "host", "localhost")
configuration.set("web", "port", "8080")
assert isinstance(Client.load(configuration), WebClient)
def test_add(client: Client, package_ahriman: Package) -> None: def test_add(client: Client, package_ahriman: Package) -> None:
""" """
must process package addition without errors must process package addition without errors
@ -98,19 +114,3 @@ def test_set_unknown(client: Client, package_ahriman: Package, mocker: MockerFix
client.set_unknown(package_ahriman) client.set_unknown(package_ahriman)
add_mock.assert_called_with(package_ahriman, BuildStatusEnum.Unknown) add_mock.assert_called_with(package_ahriman, BuildStatusEnum.Unknown)
def test_load_dummy_client(configuration: Configuration) -> None:
"""
must load dummy client if no settings set
"""
assert isinstance(Client.load(configuration), Client)
def test_load_full_client(configuration: Configuration) -> None:
"""
must load full client if no settings set
"""
configuration.set("web", "host", "localhost")
configuration.set("web", "port", "8080")
assert isinstance(Client.load(configuration), WebClient)

View File

@ -14,8 +14,8 @@ def test_from_path(mocker: MockerFixture) -> None:
load_logging_mock = mocker.patch("ahriman.core.configuration.Configuration.load_logging") load_logging_mock = mocker.patch("ahriman.core.configuration.Configuration.load_logging")
path = Path("path") path = Path("path")
config = Configuration.from_path(path, "x86_64", True) configuration = Configuration.from_path(path, "x86_64", True)
assert config.path == path assert configuration.path == path
read_mock.assert_called_with(path) read_mock.assert_called_with(path)
load_includes_mock.assert_called_once() load_includes_mock.assert_called_once()
load_logging_mock.assert_called_once() load_logging_mock.assert_called_once()
@ -25,7 +25,7 @@ def test_section_name(configuration: Configuration) -> None:
""" """
must return architecture specific group must return architecture specific group
""" """
assert configuration.section_name("build", "x86_64") == "build_x86_64" assert configuration.section_name("build", "x86_64") == "build:x86_64"
def test_absolute_path_for_absolute(configuration: Configuration) -> None: def test_absolute_path_for_absolute(configuration: Configuration) -> None:
@ -60,14 +60,15 @@ def test_dump_architecture_specific(configuration: Configuration) -> None:
""" """
dump must contain architecture specific settings dump must contain architecture specific settings
""" """
configuration.add_section("build_x86_64") section = configuration.section_name("build", "x86_64")
configuration.set("build_x86_64", "archbuild_flags", "hello flag") configuration.add_section(section)
configuration.set(section, "archbuild_flags", "hello flag")
configuration.merge_sections("x86_64") configuration.merge_sections("x86_64")
dump = configuration.dump() dump = configuration.dump()
assert dump assert dump
assert "build" in dump assert "build" in dump
assert "build_x86_64" not in dump assert section not in dump
assert dump["build"]["archbuild_flags"] == "hello flag" assert dump["build"]["archbuild_flags"] == "hello flag"
@ -125,9 +126,10 @@ def test_merge_sections_missing(configuration: Configuration) -> None:
""" """
must merge create section if not exists must merge create section if not exists
""" """
section = configuration.section_name("build", "x86_64")
configuration.remove_section("build") configuration.remove_section("build")
configuration.add_section("build_x86_64") configuration.add_section(section)
configuration.set("build_x86_64", "key", "value") configuration.set(section, "key", "value")
configuration.merge_sections("x86_64") configuration.merge_sections("x86_64")
assert configuration.get("build", "key") == "value" assert configuration.get("build", "key") == "value"

View File

@ -15,7 +15,7 @@ def test_upload_failure(configuration: Configuration, mocker: MockerFixture) ->
""" """
mocker.patch("ahriman.core.upload.rsync.Rsync.sync", side_effect=Exception()) mocker.patch("ahriman.core.upload.rsync.Rsync.sync", side_effect=Exception())
with pytest.raises(SyncFailed): with pytest.raises(SyncFailed):
Upload.run("x86_64", configuration, UploadSettings.Rsync.name, Path("path")) Upload.load("x86_64", configuration, UploadSettings.Rsync.name).run(Path("path"))
def test_upload_rsync(configuration: Configuration, mocker: MockerFixture) -> None: def test_upload_rsync(configuration: Configuration, mocker: MockerFixture) -> None:
@ -23,7 +23,7 @@ def test_upload_rsync(configuration: Configuration, mocker: MockerFixture) -> No
must upload via rsync must upload via rsync
""" """
upload_mock = mocker.patch("ahriman.core.upload.rsync.Rsync.sync") upload_mock = mocker.patch("ahriman.core.upload.rsync.Rsync.sync")
Upload.run("x86_64", configuration, UploadSettings.Rsync.name, Path("path")) Upload.load("x86_64", configuration, UploadSettings.Rsync.name).run(Path("path"))
upload_mock.assert_called_once() upload_mock.assert_called_once()
@ -32,5 +32,5 @@ def test_upload_s3(configuration: Configuration, mocker: MockerFixture) -> None:
must upload via s3 must upload via s3
""" """
upload_mock = mocker.patch("ahriman.core.upload.s3.S3.sync") upload_mock = mocker.patch("ahriman.core.upload.s3.S3.sync")
Upload.run("x86_64", configuration, UploadSettings.S3.name, Path("path")) Upload.load("x86_64", configuration, UploadSettings.S3.name).run(Path("path"))
upload_mock.assert_called_once() upload_mock.assert_called_once()

View File

@ -135,24 +135,6 @@ def test_from_json_view_3(package_tpacpi_bat_git: Package) -> None:
assert Package.from_json(package_tpacpi_bat_git.view()) == package_tpacpi_bat_git assert Package.from_json(package_tpacpi_bat_git.view()) == package_tpacpi_bat_git
def test_dependencies_with_version(mocker: MockerFixture, resource_path_root: Path) -> None:
"""
must load correct list of dependencies with version
"""
srcinfo = (resource_path_root / "models" / "package_yay_srcinfo").read_text()
mocker.patch("pathlib.Path.read_text", return_value=srcinfo)
assert Package.dependencies(Path("path")) == {"git", "go", "pacman"}
def test_full_version() -> None:
"""
must construct full version
"""
assert Package.full_version("1", "r2388.d30e3201", "1") == "1:r2388.d30e3201-1"
assert Package.full_version(None, "0.12.1", "1") == "0.12.1-1"
def test_load_from_archive(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None: def test_load_from_archive(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None:
""" """
must load package from package archive must load package from package archive
@ -198,6 +180,24 @@ def test_load_failure(package_ahriman: Package, pyalpm_handle: MagicMock, mocker
Package.load(Path("path"), pyalpm_handle, package_ahriman.aur_url) Package.load(Path("path"), pyalpm_handle, package_ahriman.aur_url)
def test_dependencies_with_version(mocker: MockerFixture, resource_path_root: Path) -> None:
"""
must load correct list of dependencies with version
"""
srcinfo = (resource_path_root / "models" / "package_yay_srcinfo").read_text()
mocker.patch("pathlib.Path.read_text", return_value=srcinfo)
assert Package.dependencies(Path("path")) == {"git", "go", "pacman"}
def test_full_version() -> None:
"""
must construct full version
"""
assert Package.full_version("1", "r2388.d30e3201", "1") == "1:r2388.d30e3201-1"
assert Package.full_version(None, "0.12.1", "1") == "0.12.1-1"
def test_actual_version(package_ahriman: Package, repository_paths: RepositoryPaths) -> None: def test_actual_version(package_ahriman: Package, repository_paths: RepositoryPaths) -> None:
""" """
must return same actual_version as version is must return same actual_version as version is