mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-08-22 01:19:56 +00:00
implement atomic_move method, move files only with lock
This commit is contained in:
@ -165,6 +165,11 @@ Again, the most checks can be performed by `tox` command, though some additional
|
|||||||
|
|
||||||
# Blank line again and package imports
|
# Blank line again and package imports
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
|
# Multiline import example
|
||||||
|
from ahriman.core.database.operations import (
|
||||||
|
AuthOperations,
|
||||||
|
BuildOperations,
|
||||||
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
* One file should define only one class, exception is class satellites in case if file length remains less than 400 lines.
|
* One file should define only one class, exception is class satellites in case if file length remains less than 400 lines.
|
||||||
|
@ -25,8 +25,16 @@ from typing import Self
|
|||||||
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.database.migrations import Migrations
|
from ahriman.core.database.migrations import Migrations
|
||||||
from ahriman.core.database.operations import AuthOperations, BuildOperations, ChangesOperations, \
|
from ahriman.core.database.operations import (
|
||||||
DependenciesOperations, EventOperations, LogsOperations, PackageOperations, PatchOperations
|
AuthOperations,
|
||||||
|
BuildOperations,
|
||||||
|
ChangesOperations,
|
||||||
|
DependenciesOperations,
|
||||||
|
EventOperations,
|
||||||
|
LogsOperations,
|
||||||
|
PackageOperations,
|
||||||
|
PatchOperations,
|
||||||
|
)
|
||||||
from ahriman.models.repository_id import RepositoryId
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,8 +17,6 @@
|
|||||||
# 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/>.
|
||||||
#
|
#
|
||||||
import shutil # shutil.move is used here to ensure cross fs file movement
|
|
||||||
|
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
@ -27,7 +25,7 @@ from ahriman.core.build_tools.package_archive import PackageArchive
|
|||||||
from ahriman.core.build_tools.task import Task
|
from ahriman.core.build_tools.task import Task
|
||||||
from ahriman.core.repository.cleaner import Cleaner
|
from ahriman.core.repository.cleaner import Cleaner
|
||||||
from ahriman.core.repository.package_info import PackageInfo
|
from ahriman.core.repository.package_info import PackageInfo
|
||||||
from ahriman.core.utils import safe_filename
|
from ahriman.core.utils import atomic_move, safe_filename
|
||||||
from ahriman.models.changes import Changes
|
from ahriman.models.changes import Changes
|
||||||
from ahriman.models.event import EventType
|
from ahriman.models.event import EventType
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
@ -54,7 +52,7 @@ class Executor(PackageInfo, Cleaner):
|
|||||||
return # suppress type checking, it never can be none actually
|
return # suppress type checking, it never can be none actually
|
||||||
|
|
||||||
if (safe := safe_filename(description.filename)) != description.filename:
|
if (safe := safe_filename(description.filename)) != description.filename:
|
||||||
(self.paths.packages / description.filename).rename(self.paths.packages / safe)
|
atomic_move(self.paths.packages / description.filename, self.paths.packages / safe)
|
||||||
description.filename = safe
|
description.filename = safe
|
||||||
|
|
||||||
def _package_build(self, package: Package, path: Path, packager: str | None,
|
def _package_build(self, package: Package, path: Path, packager: str | None,
|
||||||
@ -81,7 +79,7 @@ class Executor(PackageInfo, Cleaner):
|
|||||||
package.with_packages(built, self.pacman)
|
package.with_packages(built, self.pacman)
|
||||||
for src in built:
|
for src in built:
|
||||||
dst = self.paths.packages / src.name
|
dst = self.paths.packages / src.name
|
||||||
shutil.move(src, dst)
|
atomic_move(src, dst)
|
||||||
|
|
||||||
return commit_sha
|
return commit_sha
|
||||||
|
|
||||||
@ -130,7 +128,7 @@ class Executor(PackageInfo, Cleaner):
|
|||||||
|
|
||||||
for src in files:
|
for src in files:
|
||||||
dst = self.paths.archive_for(package_base) / src.name
|
dst = self.paths.archive_for(package_base) / src.name
|
||||||
src.rename(dst) # move package to archive directory
|
atomic_move(src, dst) # move package to archive directory
|
||||||
if not (symlink := self.paths.repository / dst.name).exists():
|
if not (symlink := self.paths.repository / dst.name).exists():
|
||||||
symlink.symlink_to(dst.relative_to(symlink.parent, walk_up=True)) # create link to archive
|
symlink.symlink_to(dst.relative_to(symlink.parent, walk_up=True)) # create link to archive
|
||||||
|
|
||||||
|
@ -19,12 +19,14 @@
|
|||||||
#
|
#
|
||||||
# pylint: disable=too-many-lines
|
# pylint: disable=too-many-lines
|
||||||
import datetime
|
import datetime
|
||||||
|
import fcntl
|
||||||
import io
|
import io
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import selectors
|
import selectors
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from collections.abc import Callable, Generator, Iterable, Mapping
|
from collections.abc import Callable, Generator, Iterable, Mapping
|
||||||
@ -39,6 +41,7 @@ from ahriman.models.repository_paths import RepositoryPaths
|
|||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"atomic_move",
|
||||||
"check_output",
|
"check_output",
|
||||||
"check_user",
|
"check_user",
|
||||||
"dataclass_view",
|
"dataclass_view",
|
||||||
@ -65,6 +68,34 @@ __all__ = [
|
|||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
def atomic_move(src: Path, dst: Path) -> None:
|
||||||
|
"""
|
||||||
|
move file from ``source`` location to ``destination``. This method uses lock and :func:`shutil.move` to ensure that
|
||||||
|
file will be copied (if not rename) atomically. This method blocks execution until lock is available
|
||||||
|
|
||||||
|
Args:
|
||||||
|
src(Path): path to the source file
|
||||||
|
dst(Path): path to the destination
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
This method is a drop-in replacement for :func:`shutil.move` (except it doesn't allow to override copy method)
|
||||||
|
which first locking destination file. To use it simply call method with arguments::
|
||||||
|
|
||||||
|
>>> atomic_move(src, dst)
|
||||||
|
"""
|
||||||
|
lock_path = dst.with_name(f".{dst.name}")
|
||||||
|
try:
|
||||||
|
with lock_path.open("ab") as lock_file:
|
||||||
|
fd = lock_file.fileno()
|
||||||
|
try:
|
||||||
|
fcntl.flock(fd, fcntl.LOCK_EX) # lock file and wait lock is until available
|
||||||
|
shutil.move(src, dst)
|
||||||
|
finally:
|
||||||
|
fcntl.flock(fd, fcntl.LOCK_UN) # unlock file first
|
||||||
|
finally:
|
||||||
|
lock_path.unlink(missing_ok=True) # remove lock file at the end
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-locals
|
# pylint: disable=too-many-locals
|
||||||
def check_output(*args: str, exception: Exception | Callable[[int, list[str], str, str], Exception] | None = None,
|
def check_output(*args: str, exception: Exception | Callable[[int, list[str], str, str], Exception] | None = None,
|
||||||
cwd: Path | None = None, input_data: str | None = None,
|
cwd: Path | None = None, input_data: str | None = None,
|
||||||
|
@ -21,8 +21,18 @@ import aiohttp_jinja2
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aiohttp.typedefs import Middleware
|
from aiohttp.typedefs import Middleware
|
||||||
from aiohttp.web import HTTPClientError, HTTPException, HTTPMethodNotAllowed, HTTPNoContent, HTTPServerError, \
|
from aiohttp.web import (
|
||||||
HTTPUnauthorized, Request, StreamResponse, json_response, middleware
|
HTTPClientError,
|
||||||
|
HTTPException,
|
||||||
|
HTTPMethodNotAllowed,
|
||||||
|
HTTPNoContent,
|
||||||
|
HTTPServerError,
|
||||||
|
HTTPUnauthorized,
|
||||||
|
Request,
|
||||||
|
StreamResponse,
|
||||||
|
json_response,
|
||||||
|
middleware,
|
||||||
|
)
|
||||||
|
|
||||||
from ahriman.web.middlewares import HandlerType
|
from ahriman.web.middlewares import HandlerType
|
||||||
|
|
||||||
|
@ -25,8 +25,12 @@ from ahriman.models.build_status import BuildStatusEnum
|
|||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.apispec.decorators import apidocs
|
from ahriman.web.apispec.decorators import apidocs
|
||||||
from ahriman.web.schemas import PackageNameSchema, PackageStatusSchema, PackageStatusSimplifiedSchema, \
|
from ahriman.web.schemas import (
|
||||||
RepositoryIdSchema
|
PackageNameSchema,
|
||||||
|
PackageStatusSchema,
|
||||||
|
PackageStatusSimplifiedSchema,
|
||||||
|
RepositoryIdSchema,
|
||||||
|
)
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
from ahriman.web.views.status_view_guard import StatusViewGuard
|
from ahriman.web.views.status_view_guard import StatusViewGuard
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@ from tempfile import NamedTemporaryFile
|
|||||||
from typing import ClassVar
|
from typing import ClassVar
|
||||||
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.utils import atomic_move
|
||||||
from ahriman.models.repository_paths import RepositoryPaths
|
from ahriman.models.repository_paths import RepositoryPaths
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.apispec.decorators import apidocs
|
from ahriman.web.apispec.decorators import apidocs
|
||||||
@ -152,10 +153,8 @@ class UploadView(BaseView):
|
|||||||
|
|
||||||
files.append(await self.save_file(part, target, max_body_size=max_body_size))
|
files.append(await self.save_file(part, target, max_body_size=max_body_size))
|
||||||
|
|
||||||
# and now we can rename files, which is relatively fast operation
|
|
||||||
# it is probably good way to call lock here, however
|
|
||||||
for filename, current_location in files:
|
for filename, current_location in files:
|
||||||
target_location = current_location.parent / filename
|
target_location = current_location.parent / filename
|
||||||
current_location.rename(target_location)
|
atomic_move(current_location, target_location)
|
||||||
|
|
||||||
raise HTTPCreated
|
raise HTTPCreated
|
||||||
|
@ -20,10 +20,10 @@ def test_archive_rename(executor: Executor, package_ahriman: Package, mocker: Mo
|
|||||||
path = "gconf-3.2.6+11+g07808097-10-x86_64.pkg.tar.zst"
|
path = "gconf-3.2.6+11+g07808097-10-x86_64.pkg.tar.zst"
|
||||||
safe_path = "gconf-3.2.6-11-g07808097-10-x86_64.pkg.tar.zst"
|
safe_path = "gconf-3.2.6-11-g07808097-10-x86_64.pkg.tar.zst"
|
||||||
package_ahriman.packages[package_ahriman.base].filename = path
|
package_ahriman.packages[package_ahriman.base].filename = path
|
||||||
rename_mock = mocker.patch("pathlib.Path.rename")
|
rename_mock = mocker.patch("ahriman.core.repository.executor.atomic_move")
|
||||||
|
|
||||||
executor._archive_rename(package_ahriman.packages[package_ahriman.base], package_ahriman.base)
|
executor._archive_rename(package_ahriman.packages[package_ahriman.base], package_ahriman.base)
|
||||||
rename_mock.assert_called_once_with(executor.paths.packages / safe_path)
|
rename_mock.assert_called_once_with(executor.paths.packages / path, executor.paths.packages / safe_path)
|
||||||
assert package_ahriman.packages[package_ahriman.base].filename == safe_path
|
assert package_ahriman.packages[package_ahriman.base].filename == safe_path
|
||||||
|
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ def test_archive_rename_empty_filename(executor: Executor, package_ahriman: Pack
|
|||||||
must skip renaming if filename is not set
|
must skip renaming if filename is not set
|
||||||
"""
|
"""
|
||||||
package_ahriman.packages[package_ahriman.base].filename = None
|
package_ahriman.packages[package_ahriman.base].filename = None
|
||||||
rename_mock = mocker.patch("pathlib.Path.rename")
|
rename_mock = mocker.patch("ahriman.core.repository.executor.atomic_move")
|
||||||
|
|
||||||
executor._archive_rename(package_ahriman.packages[package_ahriman.base], package_ahriman.base)
|
executor._archive_rename(package_ahriman.packages[package_ahriman.base], package_ahriman.base)
|
||||||
rename_mock.assert_not_called()
|
rename_mock.assert_not_called()
|
||||||
@ -46,13 +46,13 @@ def test_package_build(executor: Executor, package_ahriman: Package, mocker: Moc
|
|||||||
status_client_mock = mocker.patch("ahriman.core.status.Client.set_building")
|
status_client_mock = mocker.patch("ahriman.core.status.Client.set_building")
|
||||||
init_mock = mocker.patch("ahriman.core.build_tools.task.Task.init", return_value="sha")
|
init_mock = mocker.patch("ahriman.core.build_tools.task.Task.init", return_value="sha")
|
||||||
with_packages_mock = mocker.patch("ahriman.models.package.Package.with_packages")
|
with_packages_mock = mocker.patch("ahriman.models.package.Package.with_packages")
|
||||||
move_mock = mocker.patch("shutil.move")
|
rename_mock = mocker.patch("ahriman.core.repository.executor.atomic_move")
|
||||||
|
|
||||||
assert executor._package_build(package_ahriman, Path("local"), "packager", None) == "sha"
|
assert executor._package_build(package_ahriman, Path("local"), "packager", None) == "sha"
|
||||||
status_client_mock.assert_called_once_with(package_ahriman.base)
|
status_client_mock.assert_called_once_with(package_ahriman.base)
|
||||||
init_mock.assert_called_once_with(pytest.helpers.anyvar(int), pytest.helpers.anyvar(int), None)
|
init_mock.assert_called_once_with(pytest.helpers.anyvar(int), pytest.helpers.anyvar(int), None)
|
||||||
with_packages_mock.assert_called_once_with([Path(package_ahriman.base)], executor.pacman)
|
with_packages_mock.assert_called_once_with([Path(package_ahriman.base)], executor.pacman)
|
||||||
move_mock.assert_called_once_with(Path(package_ahriman.base), executor.paths.packages / package_ahriman.base)
|
rename_mock.assert_called_once_with(Path(package_ahriman.base), executor.paths.packages / package_ahriman.base)
|
||||||
|
|
||||||
|
|
||||||
def test_package_remove(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
|
def test_package_remove(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
@ -94,7 +94,7 @@ def test_package_update(executor: Executor, package_ahriman: Package, user: User
|
|||||||
"""
|
"""
|
||||||
must update built package in repository
|
must update built package in repository
|
||||||
"""
|
"""
|
||||||
rename_mock = mocker.patch("pathlib.Path.rename")
|
rename_mock = mocker.patch("ahriman.core.repository.executor.atomic_move")
|
||||||
symlink_mock = mocker.patch("pathlib.Path.symlink_to")
|
symlink_mock = mocker.patch("pathlib.Path.symlink_to")
|
||||||
repo_add_mock = mocker.patch("ahriman.core.alpm.repo.Repo.add")
|
repo_add_mock = mocker.patch("ahriman.core.alpm.repo.Repo.add")
|
||||||
sign_package_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process_sign_package", side_effect=lambda fn, _: [fn])
|
sign_package_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process_sign_package", side_effect=lambda fn, _: [fn])
|
||||||
@ -102,7 +102,8 @@ def test_package_update(executor: Executor, package_ahriman: Package, user: User
|
|||||||
|
|
||||||
executor._package_update(filepath, package_ahriman.base, user.key)
|
executor._package_update(filepath, package_ahriman.base, user.key)
|
||||||
# must move files (once)
|
# must move files (once)
|
||||||
rename_mock.assert_called_once_with(executor.paths.archive_for(package_ahriman.base) / filepath)
|
rename_mock.assert_called_once_with(
|
||||||
|
executor.paths.packages / filepath, executor.paths.archive_for(package_ahriman.base) / filepath)
|
||||||
# must sign package
|
# must sign package
|
||||||
sign_package_mock.assert_called_once_with(executor.paths.packages / filepath, user.key)
|
sign_package_mock.assert_called_once_with(executor.paths.packages / filepath, user.key)
|
||||||
# symlink to the archive
|
# symlink to the archive
|
||||||
@ -122,7 +123,7 @@ def test_package_update_empty_filename(executor: Executor, package_ahriman: Pack
|
|||||||
"""
|
"""
|
||||||
must skip update for package which does not have path
|
must skip update for package which does not have path
|
||||||
"""
|
"""
|
||||||
rename_mock = mocker.patch("pathlib.Path.rename")
|
rename_mock = mocker.patch("ahriman.core.repository.executor.atomic_move")
|
||||||
executor._package_update(None, package_ahriman.base, None)
|
executor._package_update(None, package_ahriman.base, None)
|
||||||
rename_mock.assert_not_called()
|
rename_mock.assert_not_called()
|
||||||
|
|
||||||
@ -155,7 +156,7 @@ def test_process_build_bump_pkgrel(executor: Executor, package_ahriman: Package,
|
|||||||
"""
|
"""
|
||||||
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
|
mocker.patch("ahriman.core.repository.executor.Executor.packages", 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.build", return_value=[Path(package_ahriman.base)])
|
||||||
mocker.patch("shutil.move")
|
mocker.patch("ahriman.core.repository.executor.atomic_move")
|
||||||
init_mock = mocker.patch("ahriman.core.build_tools.task.Task.init")
|
init_mock = mocker.patch("ahriman.core.build_tools.task.Task.init")
|
||||||
|
|
||||||
executor.process_build([package_ahriman], Packagers("packager"), bump_pkgrel=True)
|
executor.process_build([package_ahriman], Packagers("packager"), bump_pkgrel=True)
|
||||||
@ -172,7 +173,7 @@ def test_process_build_failure(executor: Executor, package_ahriman: Package, moc
|
|||||||
mocker.patch("ahriman.core.repository.executor.Executor.packages_built")
|
mocker.patch("ahriman.core.repository.executor.Executor.packages_built")
|
||||||
mocker.patch("ahriman.core.build_tools.task.Task.build", return_value=[Path(package_ahriman.base)])
|
mocker.patch("ahriman.core.build_tools.task.Task.build", return_value=[Path(package_ahriman.base)])
|
||||||
mocker.patch("ahriman.core.build_tools.task.Task.init")
|
mocker.patch("ahriman.core.build_tools.task.Task.init")
|
||||||
mocker.patch("shutil.move", side_effect=Exception)
|
mocker.patch("ahriman.core.repository.executor.atomic_move", side_effect=Exception)
|
||||||
status_client_mock = mocker.patch("ahriman.core.status.Client.set_failed")
|
status_client_mock = mocker.patch("ahriman.core.status.Client.set_failed")
|
||||||
|
|
||||||
executor.process_build([package_ahriman])
|
executor.process_build([package_ahriman])
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
import fcntl
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import pytest
|
import pytest
|
||||||
@ -9,15 +10,82 @@ from typing import Any
|
|||||||
from unittest.mock import call as MockCall
|
from unittest.mock import call as MockCall
|
||||||
|
|
||||||
from ahriman.core.exceptions import BuildError, CalledProcessError, OptionError, UnsafeRunError
|
from ahriman.core.exceptions import BuildError, CalledProcessError, OptionError, UnsafeRunError
|
||||||
from ahriman.core.utils import check_output, check_user, dataclass_view, enum_values, extract_user, filter_json, \
|
from ahriman.core.utils import (
|
||||||
full_version, minmax, package_like, parse_version, partition, pretty_datetime, pretty_interval, pretty_size, \
|
atomic_move,
|
||||||
safe_filename, srcinfo_property, srcinfo_property_list, trim_package, utcnow, walk
|
check_output,
|
||||||
|
check_user,
|
||||||
|
dataclass_view,
|
||||||
|
enum_values,
|
||||||
|
extract_user,
|
||||||
|
filter_json,
|
||||||
|
full_version,
|
||||||
|
minmax,
|
||||||
|
package_like,
|
||||||
|
parse_version,
|
||||||
|
partition,
|
||||||
|
pretty_datetime,
|
||||||
|
pretty_interval,
|
||||||
|
pretty_size,
|
||||||
|
safe_filename,
|
||||||
|
srcinfo_property,
|
||||||
|
srcinfo_property_list,
|
||||||
|
trim_package,
|
||||||
|
utcnow,
|
||||||
|
walk,
|
||||||
|
)
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.package_source import PackageSource
|
from ahriman.models.package_source import PackageSource
|
||||||
from ahriman.models.repository_id import RepositoryId
|
from ahriman.models.repository_id import RepositoryId
|
||||||
from ahriman.models.repository_paths import RepositoryPaths
|
from ahriman.models.repository_paths import RepositoryPaths
|
||||||
|
|
||||||
|
|
||||||
|
def test_atomic_move(mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must move file with locking
|
||||||
|
"""
|
||||||
|
lock_mock = mocker.patch("fcntl.flock")
|
||||||
|
open_mock = mocker.patch("pathlib.Path.open", autospec=True)
|
||||||
|
move_mock = mocker.patch("shutil.move")
|
||||||
|
unlink_mock = mocker.patch("pathlib.Path.unlink")
|
||||||
|
|
||||||
|
atomic_move(Path("source"), Path("destination"))
|
||||||
|
open_mock.assert_called_once_with(Path(".destination"), "ab")
|
||||||
|
lock_mock.assert_has_calls([
|
||||||
|
MockCall(pytest.helpers.anyvar(int), fcntl.LOCK_EX),
|
||||||
|
MockCall(pytest.helpers.anyvar(int), fcntl.LOCK_UN),
|
||||||
|
])
|
||||||
|
move_mock.assert_called_once_with(Path("source"), Path("destination"))
|
||||||
|
unlink_mock.assert_called_once_with(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_atomic_move_remove_lock(mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must remove lock file in case of exception
|
||||||
|
"""
|
||||||
|
mocker.patch("pathlib.Path.open", side_effect=Exception)
|
||||||
|
unlink_mock = mocker.patch("pathlib.Path.unlink")
|
||||||
|
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
atomic_move(Path("source"), Path("destination"))
|
||||||
|
unlink_mock.assert_called_once_with(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_atomic_move_unlock(mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must unlock file in case of exception
|
||||||
|
"""
|
||||||
|
mocker.patch("pathlib.Path.open")
|
||||||
|
mocker.patch("shutil.move", side_effect=Exception)
|
||||||
|
lock_mock = mocker.patch("fcntl.flock")
|
||||||
|
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
atomic_move(Path("source"), Path("destination"))
|
||||||
|
lock_mock.assert_has_calls([
|
||||||
|
MockCall(pytest.helpers.anyvar(int), fcntl.LOCK_EX),
|
||||||
|
MockCall(pytest.helpers.anyvar(int), fcntl.LOCK_UN),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
def test_check_output(mocker: MockerFixture) -> None:
|
def test_check_output(mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must run command and log result
|
must run command and log result
|
||||||
|
@ -109,7 +109,7 @@ async def test_post(client: TestClient, repository_paths: RepositoryPaths, mocke
|
|||||||
local = Path("local")
|
local = Path("local")
|
||||||
save_mock = pytest.helpers.patch_view(client.app, "save_file",
|
save_mock = pytest.helpers.patch_view(client.app, "save_file",
|
||||||
AsyncMock(return_value=("filename", local / ".filename")))
|
AsyncMock(return_value=("filename", local / ".filename")))
|
||||||
rename_mock = mocker.patch("pathlib.Path.rename")
|
rename_mock = mocker.patch("ahriman.web.views.v1.service.upload.atomic_move")
|
||||||
# no content validation here because it has invalid schema
|
# no content validation here because it has invalid schema
|
||||||
|
|
||||||
data = FormData()
|
data = FormData()
|
||||||
@ -118,7 +118,7 @@ async def test_post(client: TestClient, repository_paths: RepositoryPaths, mocke
|
|||||||
response = await client.post("/api/v1/service/upload", data=data)
|
response = await client.post("/api/v1/service/upload", data=data)
|
||||||
assert response.ok
|
assert response.ok
|
||||||
save_mock.assert_called_once_with(pytest.helpers.anyvar(int), repository_paths.packages, max_body_size=None)
|
save_mock.assert_called_once_with(pytest.helpers.anyvar(int), repository_paths.packages, max_body_size=None)
|
||||||
rename_mock.assert_called_once_with(local / "filename")
|
rename_mock.assert_called_once_with(local / ".filename", local / "filename")
|
||||||
|
|
||||||
|
|
||||||
async def test_post_with_sig(client: TestClient, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
|
async def test_post_with_sig(client: TestClient, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
|
||||||
@ -131,7 +131,7 @@ async def test_post_with_sig(client: TestClient, repository_paths: RepositoryPat
|
|||||||
("filename", local / ".filename"),
|
("filename", local / ".filename"),
|
||||||
("filename.sig", local / ".filename.sig"),
|
("filename.sig", local / ".filename.sig"),
|
||||||
]))
|
]))
|
||||||
rename_mock = mocker.patch("pathlib.Path.rename")
|
rename_mock = mocker.patch("ahriman.web.views.v1.service.upload.atomic_move")
|
||||||
# no content validation here because it has invalid schema
|
# no content validation here because it has invalid schema
|
||||||
|
|
||||||
data = FormData()
|
data = FormData()
|
||||||
@ -145,8 +145,8 @@ async def test_post_with_sig(client: TestClient, repository_paths: RepositoryPat
|
|||||||
MockCall(pytest.helpers.anyvar(int), repository_paths.packages, max_body_size=None),
|
MockCall(pytest.helpers.anyvar(int), repository_paths.packages, max_body_size=None),
|
||||||
])
|
])
|
||||||
rename_mock.assert_has_calls([
|
rename_mock.assert_has_calls([
|
||||||
MockCall(local / "filename"),
|
MockCall(local / ".filename", local / "filename"),
|
||||||
MockCall(local / "filename.sig"),
|
MockCall(local / ".filename.sig", local / "filename.sig"),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user