Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
---
version: 2
updates:
- package-ecosystem: "pip"
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
---
name: Python package

on: [push]
Expand All @@ -14,7 +15,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Install uv
run: pipx install 'uv==0.9.17'
run: pipx install 'uv==0.10.12'
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
---
name: Upload Python Package

on:
Expand All @@ -13,7 +14,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Install uv
run: pipx install 'uv==0.9.17'
run: pipx install 'uv==0.10.12'
- name: Publish package
run: |
uv build
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
---
on:
push:
branches:
Expand Down
2 changes: 1 addition & 1 deletion dockerfiles/python3.10
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ RUN apt update && \
apt autoclean

# Setup uv
ENV UV_VERSION=0.9.17
ENV UV_VERSION=0.10.12
RUN python3 -m pip install "uv==$UV_VERSION"

# Setup production dependencies
Expand Down
2 changes: 1 addition & 1 deletion dockerfiles/python3.11
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ RUN apt update && \
apt autoclean

# Setup uv
ENV UV_VERSION=0.9.17
ENV UV_VERSION=0.10.12
RUN python3 -m pip install "uv==$UV_VERSION"

# Setup production dependencies
Expand Down
2 changes: 1 addition & 1 deletion dockerfiles/python3.12
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ RUN apt update && \
apt autoclean

# Setup uv
ENV UV_VERSION=0.9.17
ENV UV_VERSION=0.10.12
RUN python3 -m pip install "uv==$UV_VERSION"

# Setup production dependencies
Expand Down
2 changes: 1 addition & 1 deletion dockerfiles/python3.13
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ RUN apt update && \
apt autoclean

# Setup uv
ENV UV_VERSION=0.9.17
ENV UV_VERSION=0.10.12
RUN python3 -m pip install "uv==$UV_VERSION"

# Setup production dependencies
Expand Down
2 changes: 1 addition & 1 deletion dockerfiles/python3.14
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ RUN apt update && \
apt autoclean

# Setup uv
ENV UV_VERSION=0.9.17
ENV UV_VERSION=0.10.12
RUN python3 -m pip install "uv==$UV_VERSION"

# Setup production dependencies
Expand Down
17 changes: 9 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ requires-python = ">=3.10,<4.0.0"
dependencies = [
"loguru>=0.7.3",
"pydantic>=2.12.5",
"shell-interface>=1.0.1",
"storage-device-managers>=1.0.0",
"typer>=0.20.0",
"shell-interface>=1.0.2",
"storage-device-managers>=1.0.1",
"typer>=0.24.1",
]

[project.urls]
Expand All @@ -31,13 +31,14 @@ butter-backup = "butter_backup.cli:cli"

[dependency-groups]
dev = [
"hypothesis>=6.148.7",
"mypy>=1.19.0",
"pdbpp>=0.11.7",
"hypothesis>=6.151.9",
"mypy>=1.19.1",
"pdbpp>=0.12.1",
"pytest>=9.0.2",
"pytest-cov>=7.0.0",
"pytest-cov>=7.1.0",
"pytest-mock>=3.15.1",
"pytest-xdist>=3.8.0",
"ruff>=0.14.9",
"ruff>=0.15.7",
]

[tool.mypy]
Expand Down
15 changes: 10 additions & 5 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import shutil
import typing as t
import uuid
from pathlib import Path
from tempfile import NamedTemporaryFile
Expand Down Expand Up @@ -49,7 +50,9 @@ def _encrypted_btrfs_device_persistent(


@pytest.fixture
def encrypted_btrfs_device(_encrypted_btrfs_device_persistent):
def encrypted_btrfs_device(
_encrypted_btrfs_device_persistent,
) -> t.Iterator[cp.BtrFSRsyncConfig]:
"""
Prepare device for ButterBackup and return its config

Expand Down Expand Up @@ -82,7 +85,9 @@ def _encrypted_restic_device_persistent(_big_file_persistent):


@pytest.fixture
def encrypted_restic_device(_encrypted_restic_device_persistent):
def encrypted_restic_device(
_encrypted_restic_device_persistent,
) -> t.Iterator[cp.ResticConfig]:
"""
Prepare device for Restic on BtrFS and return its config

Expand All @@ -106,13 +111,13 @@ def encrypted_restic_device(_encrypted_restic_device_persistent):


@pytest.fixture(params=["encrypted_btrfs_device", "encrypted_restic_device"])
def encrypted_device(request):
config = request.getfixturevalue(request.param)
def encrypted_device(request) -> cp.Configuration:
config: cp.Configuration = request.getfixturevalue(request.param)
return config


@pytest.fixture
def mounted_device(encrypted_device):
def mounted_device(encrypted_device) -> t.Iterator[tuple[cp.Configuration, Path]]:
config = encrypted_device
with sdm.decrypted_device(config.device(), config.DevicePassCmd) as decrypted:
with sdm.mounted_device(decrypted, config.Compression) as mounted_device:
Expand Down
44 changes: 44 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,3 +301,47 @@ def test_do_backup_refuses_backup_when_device_is_already_open(

assert result.exit_code == 0
assert expected_msg in result.stderr


def test_unmount_error_does_not_cause_content_deletion(
runner: CliRunner, encrypted_device: cp.Configuration, tmp_path: Path, mocker
) -> None:
# THIS IS A REGRESSION TEST!
#
# A previous version of the code had a serious bug, where a failed unmount operation
# would cause the content of the mount point (i.e. **the backups**!) to be deleted.
# This test ensures that this bug is fixed by provoking an unmount error and
# checking that:
#
# 1. Unmounting indeed failed (exit_code == 1).
# 2. The backup repository still exists after the failed unmount operation
# 3. The device can be closed successfully after the failed unmount operation
#
# This test "successfully" provoked the buggy behaviour before the bug was fixed.
mocker.patch(
"storage_device_managers.unmount_device",
side_effect=sdm.UnmountError("Mocked unmount error"),
)

config = complement_configuration(encrypted_device, tmp_path)
prepare_tmp_path(config, tmp_path)
config_file = tmp_path / "config.json"
config_file.write_text(f"[{config.model_dump_json()}]")

result = runner.invoke(app, ["backup", "--config", str(config_file)])
assert result.exit_code == 1
# Check that BackupRepositoryFolder still exists after the failed unmount operation.
# It is assumed that the device is still mounted, since the unmounting is mocked to
# fail.
mounts = sdm.get_mounted_devices()
mount_of_device = next(iter(mounts[str(config.map_name())]))
expected_backup_repository = mount_of_device / config.BackupRepositoryFolder
assert expected_backup_repository.exists()
assert expected_backup_repository.is_dir()
# Check that the device can be closed successfully after the failed unmount
# operation.
mocker.stopall()
result = runner.invoke(app, ["close", "--config", str(config_file)])
assert result.exit_code == 0
assert not mount_of_device.exists()
assert sdm.is_mounted(mount_of_device) is False
Loading
Loading