Compare commits

..

10 Commits

54 changed files with 809 additions and 116 deletions

29
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,29 @@
name: create release
on:
push:
tags:
- '*.*.*'
jobs:
make-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: extract version
id: version
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
- name: create archive
run: make archive
env:
VERSION: ${{ steps.version.outputs.VERSION }}
- name: Release
uses: softprops/action-gh-release@v1
with:
files: ahriman-*-src.tar.xz
fail_on_unmatched_files: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ steps.version.outputs.VERSION }}

View File

@ -1,6 +1,5 @@
# based on https://github.com/actions/starter-workflows/blob/main/ci/python-app.yml
name: ahriman
name: check commit
on:
push:
@ -9,7 +8,7 @@ on:
branches: [ master ]
jobs:
build:
run-tests:
runs-on: ubuntu-latest

View File

@ -1,7 +1,7 @@
# Maintainer: Evgeniy Alekseev
pkgname='ahriman'
pkgver=0.20.0
pkgver=0.21.1
pkgrel=1
pkgdesc="ArcHlinux ReposItory MANager"
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"
'ahriman.sysusers'
'ahriman.tmpfiles')
sha512sums=('22d2d2ae5af4a5854eb08b3b97a5fe4faa85246a85ffa78fa350080cf40a42b77152a83e0184ba848a8d06a0cce8a02a6fd94e24982ebbcc80bb88901d229f25'
sha512sums=('e401d6fa35007530f2e4279ff87a9f08dcbeb07e2069b49d818279f580a4cc83314693902e8b8628696e851adb366654e7e116bed2ececca3862777d01aba9fc'
'13718afec2c6786a18f0b223ef8e58dccf0688bca4cdbe203f14071f5031ed20120eb0ce38b52c76cfd6e8b6581a9c9eaa2743eb11abbaca637451a84c33f075'
'55b20f6da3d66e7bbf2add5d95a3b60632df121717d25a993e56e737d14f51fe063eb6f1b38bd81cc32e05db01c0c1d80aaa720c45cde87f238d8b46cdb8cbc4')
backup=('etc/ahriman.ini'

View File

@ -41,4 +41,5 @@ command = rsync --archive --verbose --compress --partial --delete
command = aws s3 sync --quiet --delete
[web]
host = 0.0.0.0
templates = /usr/share/ahriman

View File

@ -2,4 +2,4 @@
test = pytest
[tool:pytest]
addopts = --cov=ahriman --pspec
addopts = --cov=ahriman --cov-report term-missing:skip-covered --pspec

View File

@ -20,10 +20,13 @@
import argparse
import sys
from pathlib import Path
import ahriman.application.handlers as handlers
import ahriman.version as version
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.sign_settings import SignSettings
# pylint thinks it is bad idea, but get the fuck off
@ -40,9 +43,9 @@ def _parser() -> argparse.ArgumentParser:
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("-a", "--architecture", help="target architectures (can be used multiple times)",
action="append", required=True)
parser.add_argument("-c", "--configuration", help="configuration path", default="/etc/ahriman.ini")
parser.add_argument("-c", "--configuration", help="configuration path", type=Path, default=Path("/etc/ahriman.ini"))
parser.add_argument("--force", help="force run, remove file lock", action="store_true")
parser.add_argument("--lock", help="lock file", default="/tmp/ahriman.lock")
parser.add_argument("-l", "--lock", help="lock file", type=Path, default=Path("/tmp/ahriman.lock"))
parser.add_argument("--no-log", help="redirect all log messages to stderr", action="store_true")
parser.add_argument("--no-report", help="force disable reporting to web service", action="store_true")
parser.add_argument("--unsafe", help="allow to run ahriman as non-ahriman user", action="store_true")
@ -136,6 +139,7 @@ def _set_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
parser = root.add_parser("rebuild", help="rebuild repository", description="rebuild whole repository",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--depends-on", help="only rebuild packages that depend on specified package")
parser.set_defaults(handler=handlers.Rebuild)
return parser
@ -177,10 +181,14 @@ def _set_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--build-command", help="build command prefix", default="ahriman")
parser.add_argument("--from-configuration", help="path to default devtools pacman configuration",
default="/usr/share/devtools/pacman-extra.conf")
type=Path, default=Path("/usr/share/devtools/pacman-extra.conf"))
parser.add_argument("--no-multilib", help="do not add multilib repository", action="store_true")
parser.add_argument("--packager", help="packager name and email", required=True)
parser.add_argument("--repository", help="repository name", default="aur-clone")
parser.add_argument("--repository", help="repository name", required=True)
parser.add_argument("--sign-key", help="sign key id")
parser.add_argument("--sign-target", help="sign options", type=SignSettings.from_option,
choices=SignSettings, nargs="*")
parser.add_argument("--web-port", help="port of the web service", type=int)
parser.set_defaults(handler=handlers.Setup, lock=None, no_report=True, unsafe=True)
return parser
@ -224,8 +232,9 @@ def _set_status_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
"package",
help="set status for specified packages. If no packages supplied, service status will be updated",
nargs="*")
parser.add_argument("--status", help="new status", choices=[value.value for value in BuildStatusEnum],
default="success")
parser.add_argument("--status", help="new status", choices=BuildStatusEnum,
type=BuildStatusEnum, default=BuildStatusEnum.Success)
parser.add_argument("--remove", help="remove package status page", action="store_true")
parser.set_defaults(handler=handlers.StatusUpdate, lock=None, no_report=True, unsafe=True)
return parser
@ -272,11 +281,18 @@ def _set_web_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser
if __name__ == "__main__":
args_parser = _parser()
args = args_parser.parse_args()
def run() -> None:
"""
run application instance
"""
if __name__ == "__main__":
args_parser = _parser()
args = args_parser.parse_args()
handler: handlers.Handler = args.handler
status = handler.execute(args)
handler: handlers.Handler = args.handler
status = handler.execute(args)
sys.exit(status)
sys.exit(status)
run()

View File

@ -51,6 +51,13 @@ class Application:
self.architecture = architecture
self.repository = Repository(architecture, configuration)
def _finalize(self) -> None:
"""
generate report and sync to remote server
"""
self.report([])
self.sync([])
def _known_packages(self) -> Set[str]:
"""
load packages from repository and pacman repositories
@ -63,13 +70,6 @@ class Application:
known_packages.update(self.repository.pacman.all_packages())
return known_packages
def _finalize(self) -> None:
"""
generate report and sync to remote server
"""
self.report([])
self.sync([])
def get_updates(self, filter_packages: List[str], no_aur: bool, no_manual: bool, no_vcs: bool,
log_fn: Callable[[str], None]) -> List[Package]:
"""
@ -182,6 +182,7 @@ class Application:
continue
for archive in package.packages.values():
if archive.filepath is None:
self.logger.warning(f"filepath is empty for {package.base}")
continue # avoid mypy warning
src = self.repository.paths.repository / archive.filepath
dst = self.repository.paths.packages / archive.filepath

View File

@ -40,5 +40,9 @@ class Rebuild(Handler):
:param configuration: configuration instance
"""
application = Application(architecture, configuration)
packages = application.repository.packages()
packages = [
package
for package in application.repository.packages()
if args.depends_on is None or args.depends_on in package.depends
] # we have to use explicit list here for testing purpose
application.update(packages)

View File

@ -54,9 +54,9 @@ class Setup(Handler):
application = Application(architecture, configuration)
Setup.create_makepkg_configuration(args.packager, application.repository.paths)
Setup.create_executable(args.build_command, architecture)
Setup.create_devtools_configuration(args.build_command, architecture, Path(args.from_configuration),
Setup.create_devtools_configuration(args.build_command, architecture, args.from_configuration,
args.no_multilib, args.repository, application.repository.paths)
Setup.create_ahriman_configuration(args.build_command, architecture, args.repository, configuration.include)
Setup.create_ahriman_configuration(args, architecture, args.repository, configuration.include)
Setup.create_sudo_configuration(args.build_command, architecture)
@staticmethod
@ -70,23 +70,36 @@ class Setup(Handler):
return Setup.BIN_DIR_PATH / f"{prefix}-{architecture}-build"
@staticmethod
def create_ahriman_configuration(prefix: str, architecture: str, repository: str, include_path: Path) -> None:
def create_ahriman_configuration(args: argparse.Namespace, architecture: str, repository: str,
include_path: Path) -> None:
"""
create service specific configuration
:param prefix: command prefix in {prefix}-{architecture}-build
:param args: command line args
:param architecture: repository architecture
:param repository: repository name
:param include_path: path to directory with configuration includes
"""
configuration = configparser.ConfigParser()
configuration.add_section("build")
configuration.set("build", "build_command", str(Setup.build_command(prefix, architecture)))
section = Configuration.section_name("build", architecture)
configuration.add_section(section)
configuration.set(section, "build_command", str(Setup.build_command(args.build_command, architecture)))
configuration.add_section("repository")
configuration.set("repository", "name", repository)
target = include_path / "build-overrides.ini"
if args.sign_key is not None:
section = Configuration.section_name("sign", architecture)
configuration.add_section(section)
configuration.set(section, "target", " ".join([target.name.lower() for target in args.sign_target]))
configuration.set(section, "key", args.sign_key)
if args.web_port is not None:
section = Configuration.section_name("web", architecture)
configuration.add_section(section)
configuration.set(section, "port", str(args.web_port))
target = include_path / "setup-overrides.ini"
with target.open("w") as ahriman_configuration:
configuration.write(ahriman_configuration)

View File

@ -19,12 +19,11 @@
#
import argparse
from typing import Type
from typing import Callable, Type
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
from ahriman.models.build_status import BuildStatusEnum
class StatusUpdate(Handler):
@ -41,11 +40,11 @@ class StatusUpdate(Handler):
:param configuration: configuration instance
"""
client = Application(architecture, configuration).repository.reporter
status = BuildStatusEnum(args.status)
callback: Callable[[str], None] = lambda p: client.remove(p) if args.remove else client.update(p, args.status)
if args.package:
# update packages statuses
for package in args.package:
client.update(package, status)
callback(package)
else:
# update service status
client.update_self(status)
client.update_self(args.status)

View File

@ -19,7 +19,7 @@
#
import argparse
from typing import Type
from typing import Callable, Type
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler
@ -39,13 +39,22 @@ class Update(Handler):
:param architecture: repository architecture
:param configuration: configuration instance
"""
# typing workaround
def log_fn(line: str) -> None:
return print(line) if args.dry_run else application.logger.info(line)
application = Application(architecture, configuration)
packages = application.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs, log_fn)
packages = application.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs,
Update.log_fn(application, args.dry_run))
if args.dry_run:
return
application.update(packages)
@staticmethod
def log_fn(application: Application, dry_run: bool) -> Callable[[str], None]:
"""
package updates log function
:param application: application instance
:param dry_run: do not perform update itself
:return: in case if dry_run is set it will return print, logger otherwise
"""
def inner(line: str) -> None:
return print(line) if dry_run else application.logger.info(line)
return inner

View File

@ -43,6 +43,7 @@ class HTML(Report):
* architecture, string
* archive_size, pretty printed size, string
* build_date, pretty printed datetime, string
* depends, sorted list of strings
* description, string
* filename, string,
* groups, sorted list of strings
@ -95,6 +96,7 @@ class HTML(Report):
"architecture": properties.architecture or "",
"archive_size": pretty_size(properties.archive_size),
"build_date": pretty_datetime(properties.build_date),
"depends": properties.depends,
"description": properties.description or "",
"filename": properties.filename,
"groups": properties.groups,
@ -110,8 +112,8 @@ class HTML(Report):
html = template.render(
homepage=self.homepage,
link_path=self.link_path,
has_package_signed=SignSettings.SignPackages in self.sign_targets,
has_repo_signed=SignSettings.SignRepository in self.sign_targets,
has_package_signed=SignSettings.Packages in self.sign_targets,
has_repo_signed=SignSettings.Repository in self.sign_targets,
packages=sorted(content, key=comparator),
pgp_key=self.default_pgp_key,
repository=self.name)

View File

@ -53,4 +53,4 @@ class Repository(Executor, UpdateHandler):
get list of files in built packages directory
:return: list of filenames from the directory
"""
return list(self.paths.packages.iterdir())
return list(filter(package_like, self.paths.packages.iterdir()))

View File

@ -56,7 +56,7 @@ class GPG:
"""
:return: command line arguments for repo-add command to sign database
"""
if SignSettings.SignRepository not in self.targets:
if SignSettings.Repository not in self.targets:
return []
if self.default_key is None:
self.logger.error("no default key set, skip repository sign")
@ -107,7 +107,7 @@ class GPG:
:param base: package base required to check for key overrides
:return: list of generated files including original file
"""
if SignSettings.SignPackages not in self.targets:
if SignSettings.Packages not in self.targets:
return [path]
key = self.configuration.get("sign", f"key_{base}", fallback=self.default_key)
if key is None:
@ -122,7 +122,7 @@ class GPG:
:param path: path to repository database
:return: list of generated files including original file
"""
if SignSettings.SignRepository not in self.targets:
if SignSettings.Repository not in self.targets:
return [path]
if self.default_key is None:
self.logger.error("no default key set, skip repository sign")

View File

@ -45,6 +45,16 @@ class WebClient(Client):
self.host = host
self.port = port
@staticmethod
def _exception_response_text(exception: requests.exceptions.HTTPError) -> str:
"""
safe response exception text generation
:param exception: exception raised
:return: text of the response if it is not None and empty string otherwise
"""
result: str = exception.response.text if exception.response is not None else ""
return result
def _ahriman_url(self) -> str:
"""
url generator
@ -75,7 +85,7 @@ class WebClient(Client):
response = requests.post(self._package_url(package.base), json=payload)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
self.logger.exception(f"could not add {package.base}: {e.response.text}")
self.logger.exception(f"could not add {package.base}: {WebClient._exception_response_text(e)}")
except Exception:
self.logger.exception(f"could not add {package.base}")
@ -95,7 +105,7 @@ class WebClient(Client):
for package in status_json
]
except requests.exceptions.HTTPError as e:
self.logger.exception(f"could not get {base}: {e.response.text}")
self.logger.exception(f"could not get {base}: {WebClient._exception_response_text(e)}")
except Exception:
self.logger.exception(f"could not get {base}")
return []
@ -112,7 +122,7 @@ class WebClient(Client):
status_json = response.json()
return BuildStatus.from_json(status_json)
except requests.exceptions.HTTPError as e:
self.logger.exception(f"could not get service status: {e.response.text}")
self.logger.exception(f"could not get service status: {WebClient._exception_response_text(e)}")
except Exception:
self.logger.exception("could not get service status")
return BuildStatus()
@ -126,7 +136,7 @@ class WebClient(Client):
response = requests.delete(self._package_url(base))
response.raise_for_status()
except requests.exceptions.HTTPError as e:
self.logger.exception(f"could not delete {base}: {e.response.text}")
self.logger.exception(f"could not delete {base}: {WebClient._exception_response_text(e)}")
except Exception:
self.logger.exception(f"could not delete {base}")
@ -142,7 +152,7 @@ class WebClient(Client):
response = requests.post(self._package_url(base), json=payload)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
self.logger.exception(f"could not update {base}: {e.response.text}")
self.logger.exception(f"could not update {base}: {WebClient._exception_response_text(e)}")
except Exception:
self.logger.exception(f"could not update {base}")
@ -157,6 +167,6 @@ class WebClient(Client):
response = requests.post(self._ahriman_url(), json=payload)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
self.logger.exception(f"could not update service status: {e.response.text}")
self.logger.exception(f"could not update service status: {WebClient._exception_response_text(e)}")
except Exception:
self.logger.exception("could not update service status")

View File

@ -89,6 +89,6 @@ def pretty_size(size: Optional[float], level: int = 0) -> str:
if size is None:
return ""
if size < 1024 or level == 3:
if size < 1024 or level >= 3:
return f"{size:.1f} {str_level()}"
return pretty_size(size / 1024, level + 1)

View File

@ -52,6 +52,13 @@ class Package:
_check_output = check_output
@property
def depends(self) -> List[str]:
"""
:return: sum of dependencies per arch package
"""
return sorted(set(sum([package.depends for package in self.packages.values()], start=[])))
@property
def git_url(self) -> str:
"""
@ -147,7 +154,7 @@ class Package:
:return: package properties
"""
packages = {
key: PackageDescription(**value)
key: PackageDescription.from_json(value)
for key, value in dump.get("packages", {}).items()
}
return Package(

View File

@ -19,10 +19,10 @@
#
from __future__ import annotations
from dataclasses import dataclass, field
from dataclasses import dataclass, field, fields
from pathlib import Path
from pyalpm import Package # type: ignore
from typing import List, Optional, Type
from typing import Any, Dict, List, Optional, Type
@dataclass
@ -32,6 +32,7 @@ class PackageDescription:
:ivar architecture: package architecture
:ivar archive_size: package archive size
:ivar build_date: package build date
:ivar depends: package dependencies list
:ivar description: package description
:ivar filename: package archive name
:ivar groups: package groups
@ -43,6 +44,7 @@ class PackageDescription:
architecture: Optional[str] = None
archive_size: Optional[int] = None
build_date: Optional[int] = None
depends: List[str] = field(default_factory=list)
description: Optional[str] = None
filename: Optional[str] = None
groups: List[str] = field(default_factory=list)
@ -57,6 +59,18 @@ class PackageDescription:
"""
return Path(self.filename) if self.filename is not None else None
@classmethod
def from_json(cls: Type[PackageDescription], dump: Dict[str, Any]) -> PackageDescription:
"""
construct package properties from json dump
:param dump: json dump body
:return: package properties
"""
# filter to only known fields
known_fields = [pair.name for pair in fields(cls)]
dump = {key: value for key, value in dump.items() if key in known_fields}
return cls(**dump)
@classmethod
def from_package(cls: Type[PackageDescription], package: Package, path: Path) -> PackageDescription:
"""
@ -69,6 +83,7 @@ class PackageDescription:
architecture=package.arch,
archive_size=package.size,
build_date=package.builddate,
depends=package.depends,
description=package.desc,
filename=path.name,
groups=package.groups,

View File

@ -31,6 +31,7 @@ class ReportSettings(Enum):
:cvar HTML: html report generation
"""
Disabled = auto() # for testing purpose
HTML = auto()
@classmethod

View File

@ -28,12 +28,12 @@ from ahriman.core.exceptions import InvalidOption
class SignSettings(Enum):
"""
sign targets enumeration
:cvar SignPackages: sign each package
:cvar SignRepository: sign repository database file
:cvar Packages: sign each package
:cvar Repository: sign repository database file
"""
SignPackages = auto()
SignRepository = auto()
Packages = auto()
Repository = auto()
@classmethod
def from_option(cls: Type[SignSettings], value: str) -> SignSettings:
@ -43,7 +43,7 @@ class SignSettings(Enum):
:return: parsed value
"""
if value.lower() in ("package", "packages", "sign-package"):
return cls.SignPackages
return cls.Packages
if value.lower() in ("repository", "sign-repository"):
return cls.SignRepository
return cls.Repository
raise InvalidOption(value)

View File

@ -32,6 +32,7 @@ class UploadSettings(Enum):
:cvar S3: sync to Amazon S3
"""
Disabled = auto() # for testing purpose
Rsync = auto()
S3 = auto()

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__ = "0.20.0"
__version__ = "0.21.1"

View File

@ -36,6 +36,7 @@ class IndexView(BaseView):
architecture - repository architecture, string, required
packages - sorted list of packages properties, required
* base, string
* depends, sorted list of strings
* groups, sorted list of strings
* licenses, sorted list of strings
* packages, sorted list of strings
@ -61,6 +62,7 @@ class IndexView(BaseView):
packages = [
{
"base": package.base,
"depends": package.depends,
"groups": package.groups,
"licenses": package.licenses,
"packages": list(sorted(package.packages)),

View File

@ -1,15 +1,18 @@
import argparse
import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
def test_call(args: argparse.Namespace, mocker: MockerFixture) -> None:
"""
must call inside lock
"""
args.configuration = ""
args.configuration = Path("")
args.no_log = False
mocker.patch("ahriman.application.handlers.Handler.run")
mocker.patch("ahriman.core.configuration.Configuration.from_path")
@ -27,3 +30,22 @@ def test_call_exception(args: argparse.Namespace, mocker: MockerFixture) -> None
"""
mocker.patch("ahriman.application.lock.Lock.__enter__", side_effect=Exception())
assert not Handler._call(args, "x86_64")
def test_execute(args: argparse.Namespace, mocker: MockerFixture) -> None:
"""
must run execution in multiple processes
"""
args.architecture = ["i686", "x86_64"]
starmap_mock = mocker.patch("multiprocessing.pool.Pool.starmap")
Handler.execute(args)
starmap_mock.assert_called_once()
def test_packages(args: argparse.Namespace, configuration: Configuration) -> None:
"""
must raise NotImplemented for missing method
"""
with pytest.raises(NotImplementedError):
Handler.run(args, "x86_64", configuration)

View File

@ -4,12 +4,19 @@ from pytest_mock import MockerFixture
from ahriman.application.handlers import Rebuild
from ahriman.core.configuration import Configuration
from ahriman.models.package import Package
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.depends_on = None
return args
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
mocker.patch("pathlib.Path.mkdir")
application_packages_mock = mocker.patch("ahriman.core.repository.repository.Repository.packages")
application_mock = mocker.patch("ahriman.application.application.Application.update")
@ -17,3 +24,36 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
Rebuild.run(args, "x86_64", configuration)
application_packages_mock.assert_called_once()
application_mock.assert_called_once()
def test_run_filter(args: argparse.Namespace, configuration: Configuration,
package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must run command with depends filter
"""
args = _default_args(args)
args.depends_on = "python-aur"
mocker.patch("pathlib.Path.mkdir")
mocker.patch("ahriman.core.repository.repository.Repository.packages",
return_value=[package_ahriman, package_python_schedule])
application_mock = mocker.patch("ahriman.application.application.Application.update")
Rebuild.run(args, "x86_64", configuration)
application_mock.assert_called_with([package_ahriman])
def test_run_without_filter(args: argparse.Namespace, configuration: Configuration,
package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must run command for all packages if no filter supplied
"""
args = _default_args(args)
mocker.patch("pathlib.Path.mkdir")
mocker.patch("ahriman.core.repository.repository.Repository.packages",
return_value=[package_ahriman, package_python_schedule])
application_mock = mocker.patch("ahriman.application.application.Application.update")
Rebuild.run(args, "x86_64", configuration)
application_mock.assert_called_with([package_ahriman, package_python_schedule])

View File

@ -7,14 +7,18 @@ from unittest import mock
from ahriman.application.handlers import Setup
from ahriman.core.configuration import Configuration
from ahriman.models.repository_paths import RepositoryPaths
from ahriman.models.sign_settings import SignSettings
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.build_command = "ahriman"
args.from_configuration = "/usr/share/devtools/pacman-extra.conf"
args.from_configuration = Path("/usr/share/devtools/pacman-extra.conf")
args.no_multilib = False
args.packager = "John Doe <john@doe.com>"
args.repository = "aur-clone"
args.sign_key = "key"
args.sign_target = [SignSettings.Packages]
args.web_port = 8080
return args
@ -58,14 +62,20 @@ def test_create_ahriman_configuration(args: argparse.Namespace, configuration: C
write_mock = mocker.patch("configparser.RawConfigParser.write")
command = Setup.build_command(args.build_command, "x86_64")
Setup.create_ahriman_configuration(args.build_command, "x86_64", args.repository, configuration.include)
Setup.create_ahriman_configuration(args, "x86_64", args.repository, configuration.include)
add_section_mock.assert_has_calls([
mock.call("build"),
mock.call(Configuration.section_name("build", "x86_64")),
mock.call("repository"),
mock.call(Configuration.section_name("sign", "x86_64")),
mock.call(Configuration.section_name("web", "x86_64")),
])
set_mock.assert_has_calls([
mock.call("build", "build_command", str(command)),
mock.call(Configuration.section_name("build", "x86_64"), "build_command", str(command)),
mock.call("repository", "name", args.repository),
mock.call(Configuration.section_name("sign", "x86_64"), "target",
" ".join([target.name.lower() for target in args.sign_target])),
mock.call(Configuration.section_name("sign", "x86_64"), "key", args.sign_key),
mock.call(Configuration.section_name("web", "x86_64"), "port", str(args.web_port)),
])
write_mock.assert_called_once()
@ -81,7 +91,7 @@ def test_create_devtools_configuration(args: argparse.Namespace, repository_path
add_section_mock = mocker.patch("configparser.RawConfigParser.add_section")
write_mock = mocker.patch("configparser.RawConfigParser.write")
Setup.create_devtools_configuration(args.build_command, "x86_64", Path(args.from_configuration),
Setup.create_devtools_configuration(args.build_command, "x86_64", args.from_configuration,
args.no_multilib, args.repository, repository_paths)
add_section_mock.assert_has_calls([
mock.call("multilib"),
@ -101,7 +111,7 @@ def test_create_devtools_configuration_no_multilib(args: argparse.Namespace, rep
add_section_mock = mocker.patch("configparser.RawConfigParser.add_section")
write_mock = mocker.patch("configparser.RawConfigParser.write")
Setup.create_devtools_configuration(args.build_command, "x86_64", Path(args.from_configuration),
Setup.create_devtools_configuration(args.build_command, "x86_64", args.from_configuration,
True, args.repository, repository_paths)
add_section_mock.assert_called_once()
write_mock.assert_called_once()

View File

@ -4,6 +4,8 @@ from pytest_mock import MockerFixture
from ahriman.application.handlers import Status
from ahriman.core.configuration import Configuration
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
@ -12,15 +14,32 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
return args
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
def test_run(args: argparse.Namespace, configuration: Configuration, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.core.status.client.Client.get_self")
packages_mock = mocker.patch("ahriman.core.status.client.Client.get")
packages_mock = mocker.patch("ahriman.core.status.client.Client.get",
return_value=[(package_ahriman, BuildStatus())])
Status.run(args, "x86_64", configuration)
application_mock.assert_called_once()
packages_mock.assert_called_once()
def test_run_with_package_filter(args: argparse.Namespace, configuration: Configuration, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
args.package = [package_ahriman.base]
mocker.patch("pathlib.Path.mkdir")
packages_mock = mocker.patch("ahriman.core.status.client.Client.get",
return_value=[(package_ahriman, BuildStatus())])
Status.run(args, "x86_64", configuration)
packages_mock.assert_called_once()

View File

@ -9,8 +9,9 @@ from ahriman.models.package import Package
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.status = BuildStatusEnum.Success.value
args.status = BuildStatusEnum.Success
args.package = None
args.remove = False
return args
@ -38,3 +39,18 @@ def test_run_packages(args: argparse.Namespace, configuration: Configuration, pa
StatusUpdate.run(args, "x86_64", configuration)
update_mock.assert_called_once()
def test_run_remove(args: argparse.Namespace, configuration: Configuration, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must remove package from status page
"""
args = _default_args(args)
args.package = [package_ahriman.base]
args.remove = True
mocker.patch("pathlib.Path.mkdir")
update_mock = mocker.patch("ahriman.core.status.client.Client.remove")
StatusUpdate.run(args, "x86_64", configuration)
update_mock.assert_called_once()

View File

@ -2,6 +2,7 @@ import argparse
from pytest_mock import MockerFixture
from ahriman.application.application import Application
from ahriman.application.handlers import Update
from ahriman.core.configuration import Configuration
@ -40,3 +41,12 @@ def test_run_dry_run(args: argparse.Namespace, configuration: Configuration, moc
Update.run(args, "x86_64", configuration)
updates_mock.assert_called_once()
def test_log_fn(application: Application, mocker: MockerFixture) -> None:
"""
must print package updates
"""
logger_mock = mocker.patch("logging.Logger.info")
Update.log_fn(application, False)("hello")
logger_mock.assert_called_once()

View File

@ -1,5 +1,12 @@
import argparse
from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.application.handlers import Handler
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.sign_settings import SignSettings
def test_parser(parser: argparse.ArgumentParser) -> None:
"""
@ -8,6 +15,26 @@ def test_parser(parser: argparse.ArgumentParser) -> None:
parser.parse_args(["-a", "x86_64", "config"])
def test_parser_option_configuration(parser: argparse.ArgumentParser) -> None:
"""
must convert configuration option to Path instance
"""
args = parser.parse_args(["-a", "x86_64", "config"])
assert isinstance(args.configuration, Path)
args = parser.parse_args(["-a", "x86_64", "-c", "ahriman.ini", "config"])
assert isinstance(args.configuration, Path)
def test_parser_option_lock(parser: argparse.ArgumentParser) -> None:
"""
must convert lock option to Path instance
"""
args = parser.parse_args(["-a", "x86_64", "update"])
assert isinstance(args.lock, Path)
args = parser.parse_args(["-a", "x86_64", "-l", "ahriman.lock", "update"])
assert isinstance(args.lock, Path)
def test_multiple_architectures(parser: argparse.ArgumentParser) -> None:
"""
must accept multiple architectures
@ -48,12 +75,35 @@ def test_subparsers_setup(parser: argparse.ArgumentParser) -> None:
"""
setup command must imply lock, no_report and unsafe
"""
args = parser.parse_args(["-a", "x86_64", "setup", "--packager", "John Doe <john@doe.com>"])
args = parser.parse_args(["-a", "x86_64", "setup", "--packager", "John Doe <john@doe.com>",
"--repository", "aur-clone"])
assert args.lock is None
assert args.no_report
assert args.unsafe
def test_subparsers_setup_option_from_configuration(parser: argparse.ArgumentParser) -> None:
"""
setup command must convert from-configuration option to path instance
"""
args = parser.parse_args(["-a", "x86_64", "setup", "--packager", "John Doe <john@doe.com>",
"--repository", "aur-clone"])
assert isinstance(args.from_configuration, Path)
args = parser.parse_args(["-a", "x86_64", "setup", "--packager", "John Doe <john@doe.com>",
"--repository", "aur-clone", "--from-configuration", "path"])
assert isinstance(args.from_configuration, Path)
def test_subparsers_setup_option_sign_target(parser: argparse.ArgumentParser) -> None:
"""
setup command must convert sign-target option to signsettings instance
"""
args = parser.parse_args(["-a", "x86_64", "setup", "--packager", "John Doe <john@doe.com>",
"--repository", "aur-clone", "--sign-target", "packages"])
assert args.sign_target
assert all(isinstance(target, SignSettings) for target in args.sign_target)
def test_subparsers_status(parser: argparse.ArgumentParser) -> None:
"""
status command must imply lock, no_report and unsafe
@ -74,6 +124,16 @@ def test_subparsers_status_update(parser: argparse.ArgumentParser) -> None:
assert args.unsafe
def test_subparsers_status_update_option_status(parser: argparse.ArgumentParser) -> None:
"""
status-update command must convert status option to buildstatusenum instance
"""
args = parser.parse_args(["-a", "x86_64", "status-update"])
assert isinstance(args.status, BuildStatusEnum)
args = parser.parse_args(["-a", "x86_64", "status-update", "--status", "failed"])
assert isinstance(args.status, BuildStatusEnum)
def test_subparsers_web(parser: argparse.ArgumentParser) -> None:
"""
web command must imply lock and no_report
@ -81,3 +141,19 @@ def test_subparsers_web(parser: argparse.ArgumentParser) -> None:
args = parser.parse_args(["-a", "x86_64", "web"])
assert args.lock is None
assert args.no_report
def test_run(args: argparse.Namespace, mocker: MockerFixture) -> None:
"""
application must be run
"""
args.architecture = "x86_64"
args.handler = Handler
from ahriman.application import ahriman
mocker.patch.object(ahriman, "__name__", "__main__")
mocker.patch("argparse.ArgumentParser.parse_args", return_value=args)
exit_mock = mocker.patch("sys.exit")
ahriman.run()
exit_mock.assert_called_once()

View File

@ -20,11 +20,22 @@ def test_finalize(application: Application, mocker: MockerFixture) -> None:
sync_mock.assert_called_once()
def test_get_updates_all(application: Application, mocker: MockerFixture) -> None:
def test_known_packages(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must return not empty list of known packages
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman])
packages = application._known_packages()
assert len(packages) > 1
assert package_ahriman.base in packages
def test_get_updates_all(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must get updates for all
"""
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur",
return_value=[package_ahriman])
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
application.get_updates([], no_aur=False, no_manual=False, no_vcs=False, log_fn=print)
@ -233,6 +244,17 @@ def test_sign(application: Application, package_ahriman: Package, package_python
finalize_mock.assert_called_once()
def test_sign_skip(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must skip sign packages with empty filename
"""
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.update")
application.sign([])
def test_sign_specific(application: Application, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""

View File

@ -61,6 +61,7 @@ def package_description_ahriman() -> PackageDescription:
architecture="x86_64",
archive_size=4200,
build_date=42,
depends=["devtools", "git", "pyalpm", "python-aur", "python-srcinfo"],
description="ArcHlinux ReposItory MANager",
filename="ahriman-0.12.1-1-any.pkg.tar.zst",
groups=[],
@ -75,6 +76,7 @@ def package_description_python_schedule() -> PackageDescription:
architecture="x86_64",
archive_size=4201,
build_date=421,
depends=["python"],
description="Python job scheduling for humans.",
filename="python-schedule-1.0.0-2-any.pkg.tar.zst",
groups=[],
@ -89,6 +91,7 @@ def package_description_python2_schedule() -> PackageDescription:
architecture="x86_64",
archive_size=4202,
build_date=422,
depends=["python2"],
description="Python job scheduling for humans.",
filename="python2-schedule-1.0.0-2-any.pkg.tar.zst",
groups=[],

View File

@ -51,6 +51,15 @@ def test_fetch_new(mocker: MockerFixture) -> None:
])
def test_build(task_ahriman: Task, mocker: MockerFixture) -> None:
"""
must build package
"""
check_output_mock = mocker.patch("ahriman.core.build_tools.task.Task._check_output")
task_ahriman.build()
check_output_mock.assert_called()
def test_init_with_cache(task_ahriman: Task, mocker: MockerFixture) -> None:
"""
must copy tree instead of fetch

View File

@ -18,6 +18,16 @@ def test_report_failure(configuration: Configuration, mocker: MockerFixture) ->
Report.load("x86_64", configuration, ReportSettings.HTML.name).run(Path("path"))
def test_report_dummy(configuration: Configuration, mocker: MockerFixture) -> None:
"""
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.Report.generate")
Report.load("x86_64", configuration, ReportSettings.Disabled.name).run(Path("path"))
report_mock.assert_called_once()
def test_report_html(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must generate html report

View File

@ -1,3 +1,4 @@
import pytest
import shutil
from pathlib import Path
@ -20,6 +21,14 @@ def _mock_clear_check() -> None:
])
def test_packages_built(cleaner: Cleaner) -> None:
"""
must raise NotImplemented for missing method
"""
with pytest.raises(NotImplementedError):
cleaner.packages_built()
def test_clear_build(cleaner: Cleaner, mocker: MockerFixture) -> None:
"""
must remove directories with sources

View File

@ -1,23 +1,34 @@
import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from unittest import mock
from ahriman.core.report.report import Report
from ahriman.core.repository.executor import Executor
from ahriman.core.upload.upload import Upload
from ahriman.models.package import Package
def test_packages(executor: Executor) -> None:
"""
must raise NotImplemented for missing method
"""
with pytest.raises(NotImplementedError):
executor.packages()
def test_process_build(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must run build process
"""
mocker.patch("ahriman.core.repository.executor.Executor.packages_built", return_value=[package_ahriman])
mocker.patch("ahriman.core.build_tools.task.Task.build", return_value=[Path(package_ahriman.base)])
mocker.patch("ahriman.core.build_tools.task.Task.init")
move_mock = mocker.patch("shutil.move")
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_building")
built_packages_mock = mocker.patch("ahriman.core.repository.executor.Executor.packages_built")
# must return list of built packages
assert executor.process_build([package_ahriman]) == [package_ahriman]
executor.process_build([package_ahriman])
# must move files (once)
move_mock.assert_called_once()
# must update status
@ -25,6 +36,8 @@ def test_process_build(executor: Executor, package_ahriman: Package, mocker: Moc
# must clear directory
from ahriman.core.repository.cleaner import Cleaner
Cleaner.clear_build.assert_called_once()
# must return build packages after all
built_packages_mock.assert_called_once()
def test_process_build_failure(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
@ -68,7 +81,7 @@ def test_process_remove_base_multiple(executor: Executor, package_python_schedul
executor.process_remove([package_python_schedule.base])
# must remove via alpm wrapper
repo_remove_mock.assert_has_calls([
mock.call(package, Path(props.filename))
mock.call(package, props.filepath)
for package, props in package_python_schedule.packages.items()
], any_order=True)
# must update status
@ -91,6 +104,15 @@ def test_process_remove_base_single(executor: Executor, package_python_schedule:
status_client_mock.assert_not_called()
def test_process_remove_failed(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must suppress remove errors
"""
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
mocker.patch("ahriman.core.alpm.repo.Repo.remove", side_effect=Exception())
executor.process_remove([package_ahriman.base])
def test_process_remove_nothing(executor: Executor, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
@ -103,6 +125,18 @@ def test_process_remove_nothing(executor: Executor, package_ahriman: Package, pa
repo_remove_mock.assert_not_called()
def test_process_report(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must process report
"""
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
mocker.patch("ahriman.core.report.report.Report.load", return_value=Report("x86_64", executor.configuration))
report_mock = mocker.patch("ahriman.core.report.report.Report.run")
executor.process_report(["dummy"])
report_mock.assert_called_once()
def test_process_report_auto(executor: Executor, mocker: MockerFixture) -> None:
"""
must process report in auto mode if no targets supplied
@ -113,7 +147,18 @@ def test_process_report_auto(executor: Executor, mocker: MockerFixture) -> None:
configuration_getlist_mock.assert_called_once()
def test_process_sync_auto(executor: Executor, mocker: MockerFixture) -> None:
def test_process_upload(executor: Executor, mocker: MockerFixture) -> None:
"""
must process sync in auto mode if no targets supplied
"""
mocker.patch("ahriman.core.upload.upload.Upload.load", return_value=Upload("x86_64", executor.configuration))
upload_mock = mocker.patch("ahriman.core.upload.upload.Upload.run")
executor.process_sync(["dummy"])
upload_mock.assert_called_once()
def test_process_upload_auto(executor: Executor, mocker: MockerFixture) -> None:
"""
must process sync in auto mode if no targets supplied
"""
@ -134,7 +179,7 @@ def test_process_update(executor: Executor, package_ahriman: Package, mocker: Mo
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_success")
# must return complete
assert executor.process_update([Path(package.filename) for package in package_ahriman.packages.values()])
assert executor.process_update([package.filepath for package in package_ahriman.packages.values()])
# must move files (once)
move_mock.assert_called_once()
# must sign package
@ -158,14 +203,23 @@ def test_process_update_group(executor: Executor, package_python_schedule: Packa
repo_add_mock = mocker.patch("ahriman.core.alpm.repo.Repo.add")
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_success")
executor.process_update([Path(package.filename) for package in package_python_schedule.packages.values()])
executor.process_update([package.filepath for package in package_python_schedule.packages.values()])
repo_add_mock.assert_has_calls([
mock.call(executor.paths.repository / package.filename)
mock.call(executor.paths.repository / package.filepath)
for package in package_python_schedule.packages.values()
], any_order=True)
status_client_mock.assert_called_with(package_python_schedule)
def test_process_empty_filename(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must skip update for package which does not have path
"""
package_ahriman.packages[package_ahriman.base].filename = None
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
executor.process_update([package.filepath for package in package_ahriman.packages.values()])
def test_process_update_failed(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must process update for failed package
@ -174,7 +228,7 @@ def test_process_update_failed(executor: Executor, package_ahriman: Package, moc
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_failed")
executor.process_update([Path(package.filename) for package in package_ahriman.packages.values()])
executor.process_update([package.filepath for package in package_ahriman.packages.values()])
status_client_mock.assert_called_once()
@ -185,4 +239,4 @@ def test_process_update_failed_on_load(executor: Executor, package_ahriman: Pack
mocker.patch("shutil.move")
mocker.patch("ahriman.models.package.Package.load", side_effect=Exception())
assert executor.process_update([Path(package.filename) for package in package_ahriman.packages.values()])
assert executor.process_update([package.filepath for package in package_ahriman.packages.values()])

View File

@ -31,3 +31,28 @@ def test_packages(package_ahriman: Package, package_python_schedule: Package,
expected = set(package_ahriman.packages.keys())
expected.update(package_python_schedule.packages.keys())
assert set(archives) == expected
def test_packages_failed(repository: Repository, mocker: MockerFixture) -> None:
"""
must skip packages which cannot be loaded
"""
mocker.patch("pathlib.Path.iterdir", return_value=[Path("a.pkg.tar.xz")])
mocker.patch("ahriman.models.package.Package.load", side_effect=Exception())
assert not repository.packages()
def test_packages_not_package(repository: Repository, mocker: MockerFixture) -> None:
"""
must skip not packages from iteration
"""
mocker.patch("pathlib.Path.iterdir", return_value=[Path("a.tar.xz")])
assert not repository.packages()
def test_packages_built(repository: Repository, mocker: MockerFixture) -> None:
"""
must return build packages
"""
mocker.patch("pathlib.Path.iterdir", return_value=[Path("a.tar.xz"), Path("b.pkg.tar.xz")])
assert repository.packages_built() == [Path("b.pkg.tar.xz")]

View File

@ -1,9 +1,19 @@
import pytest
from pytest_mock import MockerFixture
from ahriman.core.repository.update_handler import UpdateHandler
from ahriman.models.package import Package
def test_packages(update_handler: UpdateHandler) -> None:
"""
must raise NotImplemented for missing method
"""
with pytest.raises(NotImplementedError):
update_handler.packages()
def test_updates_aur(update_handler: UpdateHandler, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""

View File

@ -9,7 +9,7 @@ def test_repository_sign_args_1(gpg_with_key: GPG) -> None:
"""
must generate correct sign args
"""
gpg_with_key.targets = {SignSettings.SignRepository}
gpg_with_key.targets = {SignSettings.Repository}
assert gpg_with_key.repository_sign_args
@ -17,7 +17,7 @@ def test_repository_sign_args_2(gpg_with_key: GPG) -> None:
"""
must generate correct sign args
"""
gpg_with_key.targets = {SignSettings.SignPackages, SignSettings.SignRepository}
gpg_with_key.targets = {SignSettings.Packages, SignSettings.Repository}
assert gpg_with_key.repository_sign_args
@ -33,7 +33,7 @@ def test_repository_sign_args_skip_2(gpg_with_key: GPG) -> None:
"""
must return empty args if it is not set
"""
gpg_with_key.targets = {SignSettings.SignPackages}
gpg_with_key.targets = {SignSettings.Packages}
assert not gpg_with_key.repository_sign_args
@ -41,7 +41,7 @@ def test_repository_sign_args_skip_3(gpg: GPG) -> None:
"""
must return empty args if it is not set
"""
gpg.targets = {SignSettings.SignRepository}
gpg.targets = {SignSettings.Repository}
assert not gpg.repository_sign_args
@ -49,10 +49,28 @@ def test_repository_sign_args_skip_4(gpg: GPG) -> None:
"""
must return empty args if it is not set
"""
gpg.targets = {SignSettings.SignPackages, SignSettings.SignRepository}
gpg.targets = {SignSettings.Packages, SignSettings.Repository}
assert not gpg.repository_sign_args
def test_sign_command(gpg_with_key: GPG) -> None:
"""
must generate sign command
"""
assert gpg_with_key.sign_command(Path("a"), gpg_with_key.default_key)
def test_process(gpg_with_key: GPG, mocker: MockerFixture) -> None:
"""
must call process method correctly
"""
result = [Path("a"), Path("a.sig")]
check_output_mock = mocker.patch("ahriman.core.sign.gpg.GPG._check_output")
assert gpg_with_key.process(Path("a"), gpg_with_key.default_key) == result
check_output_mock.assert_called()
def test_sign_package_1(gpg_with_key: GPG, mocker: MockerFixture) -> None:
"""
must sign package
@ -60,7 +78,7 @@ def test_sign_package_1(gpg_with_key: GPG, mocker: MockerFixture) -> None:
result = [Path("a"), Path("a.sig")]
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process", return_value=result)
gpg_with_key.targets = {SignSettings.SignPackages}
gpg_with_key.targets = {SignSettings.Packages}
assert gpg_with_key.sign_package(Path("a"), "a") == result
process_mock.assert_called_once()
@ -72,7 +90,7 @@ def test_sign_package_2(gpg_with_key: GPG, mocker: MockerFixture) -> None:
result = [Path("a"), Path("a.sig")]
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process", return_value=result)
gpg_with_key.targets = {SignSettings.SignPackages, SignSettings.SignRepository}
gpg_with_key.targets = {SignSettings.Packages, SignSettings.Repository}
assert gpg_with_key.sign_package(Path("a"), "a") == result
process_mock.assert_called_once()
@ -92,7 +110,7 @@ def test_sign_package_skip_2(gpg_with_key: GPG, mocker: MockerFixture) -> None:
must not sign package if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg_with_key.targets = {SignSettings.SignRepository}
gpg_with_key.targets = {SignSettings.Repository}
gpg_with_key.sign_package(Path("a"), "a")
process_mock.assert_not_called()
@ -102,7 +120,7 @@ def test_sign_package_skip_3(gpg: GPG, mocker: MockerFixture) -> None:
must not sign package if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg.targets = {SignSettings.SignPackages}
gpg.targets = {SignSettings.Packages}
gpg.sign_package(Path("a"), "a")
process_mock.assert_not_called()
@ -112,7 +130,7 @@ def test_sign_package_skip_4(gpg: GPG, mocker: MockerFixture) -> None:
must not sign package if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg.targets = {SignSettings.SignPackages, SignSettings.SignRepository}
gpg.targets = {SignSettings.Packages, SignSettings.Repository}
gpg.sign_package(Path("a"), "a")
process_mock.assert_not_called()
@ -124,7 +142,7 @@ def test_sign_repository_1(gpg_with_key: GPG, mocker: MockerFixture) -> None:
result = [Path("a"), Path("a.sig")]
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process", return_value=result)
gpg_with_key.targets = {SignSettings.SignRepository}
gpg_with_key.targets = {SignSettings.Repository}
assert gpg_with_key.sign_repository(Path("a")) == result
process_mock.assert_called_once()
@ -136,7 +154,7 @@ def test_sign_repository_2(gpg_with_key: GPG, mocker: MockerFixture) -> None:
result = [Path("a"), Path("a.sig")]
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process", return_value=result)
gpg_with_key.targets = {SignSettings.SignPackages, SignSettings.SignRepository}
gpg_with_key.targets = {SignSettings.Packages, SignSettings.Repository}
assert gpg_with_key.sign_repository(Path("a")) == result
process_mock.assert_called_once()
@ -156,7 +174,7 @@ def test_sign_repository_skip_2(gpg_with_key: GPG, mocker: MockerFixture) -> Non
must not sign repository if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg_with_key.targets = {SignSettings.SignPackages}
gpg_with_key.targets = {SignSettings.Packages}
gpg_with_key.sign_repository(Path("a"))
process_mock.assert_not_called()
@ -166,7 +184,7 @@ def test_sign_repository_skip_3(gpg: GPG, mocker: MockerFixture) -> None:
must not sign repository if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg.targets = {SignSettings.SignRepository}
gpg.targets = {SignSettings.Repository}
gpg.sign_repository(Path("a"))
process_mock.assert_not_called()
@ -176,6 +194,6 @@ def test_sign_repository_skip_4(gpg: GPG, mocker: MockerFixture) -> None:
must not sign repository if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg.targets = {SignSettings.SignPackages, SignSettings.SignRepository}
gpg.targets = {SignSettings.Packages, SignSettings.Repository}
gpg.sign_repository(Path("a"))
process_mock.assert_not_called()

View File

@ -51,6 +51,21 @@ def test_cache_load_no_file(watcher: Watcher, mocker: MockerFixture) -> None:
assert not watcher.known
def test_cache_load_package_load_error(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must not fail on json errors
"""
response = {"packages": [pytest.helpers.get_package_status_extended(package_ahriman)]}
mocker.patch("pathlib.Path.is_file", return_value=True)
mocker.patch("pathlib.Path.open")
mocker.patch("ahriman.models.package.Package.from_json", side_effect=Exception())
mocker.patch("json.load", return_value=response)
watcher._cache_load()
assert not watcher.known
def test_cache_load_unknown(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must not load unknown package

View File

@ -1,5 +1,6 @@
import json
import pytest
import requests
from pytest_mock import MockerFixture
from requests import Response
@ -44,6 +45,14 @@ def test_add_failed(web_client: WebClient, package_ahriman: Package, mocker: Moc
web_client.add(package_ahriman, BuildStatusEnum.Unknown)
def test_add_failed_http_error(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during addition
"""
mocker.patch("requests.post", side_effect=requests.exceptions.HTTPError())
web_client.add(package_ahriman, BuildStatusEnum.Unknown)
def test_get_all(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must return all packages status
@ -69,6 +78,14 @@ def test_get_failed(web_client: WebClient, mocker: MockerFixture) -> None:
assert web_client.get(None) == []
def test_get_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during status getting
"""
mocker.patch("requests.get", side_effect=requests.exceptions.HTTPError())
assert web_client.get(None) == []
def test_get_single(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must return single package status
@ -109,6 +126,14 @@ def test_get_self_failed(web_client: WebClient, mocker: MockerFixture) -> None:
assert web_client.get_self().status == BuildStatusEnum.Unknown
def test_get_self_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during service status getting
"""
mocker.patch("requests.get", side_effect=requests.exceptions.HTTPError())
assert web_client.get_self().status == BuildStatusEnum.Unknown
def test_remove(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must process package removal
@ -127,6 +152,14 @@ def test_remove_failed(web_client: WebClient, package_ahriman: Package, mocker:
web_client.remove(package_ahriman.base)
def test_remove_failed_http_error(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during removal
"""
mocker.patch("requests.delete", side_effect=requests.exceptions.HTTPError())
web_client.remove(package_ahriman.base)
def test_update(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must process package update
@ -145,6 +178,14 @@ def test_update_failed(web_client: WebClient, package_ahriman: Package, mocker:
web_client.update(package_ahriman.base, BuildStatusEnum.Unknown)
def test_update_failed_http_error(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during update
"""
mocker.patch("requests.post", side_effect=requests.exceptions.HTTPError())
web_client.update(package_ahriman.base, BuildStatusEnum.Unknown)
def test_update_self(web_client: WebClient, mocker: MockerFixture) -> None:
"""
must process service update
@ -161,3 +202,11 @@ def test_update_self_failed(web_client: WebClient, mocker: MockerFixture) -> Non
"""
mocker.patch("requests.post", side_effect=Exception())
web_client.update_self(BuildStatusEnum.Unknown)
def test_update_self_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during service update
"""
mocker.patch("requests.post", side_effect=requests.exceptions.HTTPError())
web_client.update_self(BuildStatusEnum.Unknown)

View File

@ -105,6 +105,14 @@ def test_load_includes_missing(configuration: Configuration) -> None:
configuration.load_includes()
def test_load_includes_no_option(configuration: Configuration) -> None:
"""
must not fail if no option set
"""
configuration.remove_option("settings", "include")
configuration.load_includes()
def test_load_logging_fallback(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must fallback to stderr without errors

View File

@ -4,6 +4,7 @@ import subprocess
from pytest_mock import MockerFixture
from ahriman.core.exceptions import InvalidOption
from ahriman.core.util import check_output, package_like, pretty_datetime, pretty_size
from ahriman.models.package import Package
@ -124,6 +125,14 @@ def test_pretty_size_pbytes() -> None:
assert abbrev == "GiB"
def test_pretty_size_pbytes_failure() -> None:
"""
must raise exception if level >= 4 supplied
"""
with pytest.raises(InvalidOption):
pretty_size(42 * 1024 * 1024 * 1024 * 1024, 4).split()
def test_pretty_size_empty() -> None:
"""
must generate empty string for None value

View File

@ -18,6 +18,16 @@ def test_upload_failure(configuration: Configuration, mocker: MockerFixture) ->
Upload.load("x86_64", configuration, UploadSettings.Rsync.name).run(Path("path"))
def test_report_dummy(configuration: Configuration, mocker: MockerFixture) -> None:
"""
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.Upload.sync")
Upload.load("x86_64", configuration, UploadSettings.Disabled.name).run(Path("path"))
upload_mock.assert_called_once()
def test_upload_rsync(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must upload via rsync

View File

@ -42,6 +42,7 @@ def pyalpm_package_description_ahriman(package_description_ahriman: PackageDescr
mock = MagicMock()
type(mock).arch = PropertyMock(return_value=package_description_ahriman.architecture)
type(mock).builddate = PropertyMock(return_value=package_description_ahriman.build_date)
type(mock).depends = PropertyMock(return_value=package_description_ahriman.depends)
type(mock).desc = PropertyMock(return_value=package_description_ahriman.description)
type(mock).groups = PropertyMock(return_value=package_description_ahriman.groups)
type(mock).isize = PropertyMock(return_value=package_description_ahriman.installed_size)

View File

@ -1,3 +1,5 @@
import datetime
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
@ -36,3 +38,59 @@ def test_build_status_from_json_view(build_status_failed: BuildStatus) -> None:
must construct same object from json
"""
assert BuildStatus.from_json(build_status_failed.view()) == build_status_failed
def test_build_status_pretty_print(build_status_failed: BuildStatus) -> None:
"""
must return string in pretty print function
"""
assert build_status_failed.pretty_print()
assert isinstance(build_status_failed.pretty_print(), str)
def test_build_status_eq(build_status_failed: BuildStatus) -> None:
"""
must be equal
"""
other = BuildStatus.from_json(build_status_failed.view())
assert other == build_status_failed
def test_build_status_eq_self(build_status_failed: BuildStatus) -> None:
"""
must be equal itself
"""
assert build_status_failed == build_status_failed
def test_build_status_ne_by_status(build_status_failed: BuildStatus) -> None:
"""
must be not equal by status
"""
other = BuildStatus.from_json(build_status_failed.view())
other.status = BuildStatusEnum.Success
assert build_status_failed != other
def test_build_status_ne_by_timestamp(build_status_failed: BuildStatus) -> None:
"""
must be not equal by timestamp
"""
other = BuildStatus.from_json(build_status_failed.view())
other.timestamp = datetime.datetime.utcnow().timestamp()
assert build_status_failed != other
def test_build_status_ne_other(build_status_failed: BuildStatus) -> None:
"""
must be not equal to random object
"""
assert build_status_failed != object()
def test_build_status_repr(build_status_failed: BuildStatus) -> None:
"""
must return string in __repr__ function
"""
assert build_status_failed.__repr__()
assert isinstance(build_status_failed.__repr__(), str)

View File

@ -9,6 +9,16 @@ from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths
def test_depends(package_python_schedule: Package) -> None:
"""
must return combined list of dependencies
"""
assert all(
set(package_python_schedule.depends).intersection(package.depends)
for package in package_python_schedule.packages.values()
)
def test_git_url(package_ahriman: Package) -> None:
"""
must generate valid git url
@ -114,6 +124,17 @@ def test_from_build(package_ahriman: Package, mocker: MockerFixture, resource_pa
assert package_ahriman == package
def test_from_build_failed(package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must raise exception if there are errors during srcinfo load
"""
mocker.patch("pathlib.Path.read_text", return_value="")
mocker.patch("ahriman.models.package.parse_srcinfo", return_value=({"packages": {}}, ["an error"]))
with pytest.raises(InvalidPackageInfo):
Package.from_build(Path("path"), package_ahriman.aur_url)
def test_from_json_view_1(package_ahriman: Package) -> None:
"""
must construct same object from json
@ -180,6 +201,17 @@ def test_load_failure(package_ahriman: Package, pyalpm_handle: MagicMock, mocker
Package.load(Path("path"), pyalpm_handle, package_ahriman.aur_url)
def test_dependencies_failed(mocker: MockerFixture) -> None:
"""
must raise exception if there are errors during srcinfo load
"""
mocker.patch("pathlib.Path.read_text", return_value="")
mocker.patch("ahriman.models.package.parse_srcinfo", return_value=({"packages": {}}, ["an error"]))
with pytest.raises(InvalidPackageInfo):
Package.dependencies(Path("path"))
def test_dependencies_with_version(mocker: MockerFixture, resource_path_root: Path) -> None:
"""
must load correct list of dependencies with version
@ -217,12 +249,25 @@ def test_actual_version_vcs(package_tpacpi_bat_git: Package, repository_paths: R
assert package_tpacpi_bat_git.actual_version(repository_paths) == "3.1.r13.g4959b52-1"
def test_actual_version_srcinfo_failed(package_tpacpi_bat_git: Package, repository_paths: RepositoryPaths,
mocker: MockerFixture) -> None:
"""
must return same version in case if exception occurred
"""
mocker.patch("ahriman.models.package.Package._check_output", side_effect=Exception())
mocker.patch("ahriman.core.build_tools.task.Task.fetch")
assert package_tpacpi_bat_git.actual_version(repository_paths) == package_tpacpi_bat_git.version
def test_actual_version_vcs_failed(package_tpacpi_bat_git: Package, repository_paths: RepositoryPaths,
mocker: MockerFixture) -> None:
"""
must return same version in case if exception occurred
"""
mocker.patch("ahriman.models.package.Package._check_output", side_effect=Exception())
mocker.patch("pathlib.Path.read_text", return_value="")
mocker.patch("ahriman.models.package.parse_srcinfo", return_value=({"packages": {}}, ["an error"]))
mocker.patch("ahriman.models.package.Package._check_output")
mocker.patch("ahriman.core.build_tools.task.Task.fetch")
assert package_tpacpi_bat_git.actual_version(repository_paths) == package_tpacpi_bat_git.version
@ -243,3 +288,11 @@ def test_is_outdated_true(package_ahriman: Package, repository_paths: Repository
other.version = other.version.replace("-1", "-2")
assert package_ahriman.is_outdated(other, repository_paths)
def test_build_status_pretty_print(package_ahriman: Package) -> None:
"""
must return string in pretty print function
"""
assert package_ahriman.pretty_print()
assert isinstance(package_ahriman.pretty_print(), str)

View File

@ -1,3 +1,4 @@
from dataclasses import asdict
from unittest.mock import MagicMock
from ahriman.models.package_description import PackageDescription
@ -19,6 +20,22 @@ def test_filepath_empty(package_description_ahriman: PackageDescription) -> None
assert package_description_ahriman.filepath is None
def test_from_json(package_description_ahriman: PackageDescription) -> None:
"""
must construct description from json object
"""
assert PackageDescription.from_json(asdict(package_description_ahriman)) == package_description_ahriman
def test_from_json_with_unknown_fields(package_description_ahriman: PackageDescription) -> None:
"""
must construct description from json object containing unknown fields
"""
dump = asdict(package_description_ahriman)
dump.update(unknown_field="value")
assert PackageDescription.from_json(dump) == package_description_ahriman
def test_from_package(package_description_ahriman: PackageDescription,
pyalpm_package_description_ahriman: MagicMock) -> None:
"""

View File

@ -16,11 +16,11 @@ def test_from_option_valid() -> None:
"""
must return value from valid options
"""
assert SignSettings.from_option("package") == SignSettings.SignPackages
assert SignSettings.from_option("PACKAGE") == SignSettings.SignPackages
assert SignSettings.from_option("packages") == SignSettings.SignPackages
assert SignSettings.from_option("sign-package") == SignSettings.SignPackages
assert SignSettings.from_option("package") == SignSettings.Packages
assert SignSettings.from_option("PACKAGE") == SignSettings.Packages
assert SignSettings.from_option("packages") == SignSettings.Packages
assert SignSettings.from_option("sign-package") == SignSettings.Packages
assert SignSettings.from_option("repository") == SignSettings.SignRepository
assert SignSettings.from_option("REPOSITORY") == SignSettings.SignRepository
assert SignSettings.from_option("sign-repository") == SignSettings.SignRepository
assert SignSettings.from_option("repository") == SignSettings.Repository
assert SignSettings.from_option("REPOSITORY") == SignSettings.Repository
assert SignSettings.from_option("sign-repository") == SignSettings.Repository

View File

@ -34,12 +34,10 @@ def test_run(application: web.Application, mocker: MockerFixture) -> None:
"""
must run application
"""
host = "localhost"
port = 8080
application["configuration"].set("web", "host", host)
application["configuration"].set("web", "port", str(port))
run_application_mock = mocker.patch("aiohttp.web.run_app")
run_server(application)
run_application_mock.assert_called_with(application, host=host, port=port,
run_application_mock.assert_called_with(application, host="0.0.0.0", port=port,
handle_signals=False, access_log=pytest.helpers.anyvar(int))

View File

@ -1,4 +1,5 @@
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
@ -35,3 +36,14 @@ async def test_post_exception(client: TestClient) -> None:
"""
post_response = await client.post("/api/v1/ahriman", json={})
assert post_response.status == 400
async def test_post_exception_inside(client: TestClient, mocker: MockerFixture) -> None:
"""
exception handler must handle 500 errors
"""
payload = {"status": BuildStatusEnum.Success.value}
mocker.patch("ahriman.core.status.watcher.Watcher.update_self", side_effect=Exception())
post_response = await client.post("/api/v1/ahriman", json=payload)
assert post_response.status == 500

View File

@ -43,4 +43,5 @@ bucket =
command = aws s3 sync --quiet --delete
[web]
host = 0.0.0.0
templates = ../web/templates