Thank you for your interest in contributing to btrfs-backup-ng!
# Clone the repository
git clone https://github.com/berrym/btrfs-backup-ng.git
cd btrfs-backup-ng
# Install in development mode with dev dependencies
pip install -e ".[dev]"We use several tools to maintain code quality:
# Run linter
ruff check src/ tests/
# Run formatter
ruff format src/ tests/
# Run type checker
mypy src/btrfs_backup_ng --ignore-missing-importsTests are organized into two tiers:
| Tier | Description | Requirements | CI |
|---|---|---|---|
| Tier 1 | Unit tests and mocked integration tests | Python only | Every push |
| Tier 2 | Real btrfs integration tests | btrfs-progs, root, Linux | Weekly/manual |
A test has to fail when the code is wrong. A test that passes against a broken implementation is worse than no test, because it makes the next person confident for no reason. A quick way to check your own: break the code deliberately and confirm the test notices.
Prefer testing behaviour over implementation -- what the code produces, rather than which functions it called on the way. Where something touches real filesystem or privilege behaviour, a real temporary directory usually pins it better than a mock, because a mock will agree with a wrong assumption without complaint.
Standard tests run without special privileges:
# Run all tier 1 tests
pytest
# With coverage
pytest --cov=src/btrfs_backup_ng --cov-report=term-missing
# Specific test file
pytest tests/test_config_loader.py -vTier 2 tests require:
- Linux with btrfs kernel support
btrfs-progspackage installed- Root privileges (for mount/loopback operations)
# Run tier 2 tests (requires root)
sudo pytest -m tier2 tests/integration/tier2/ -v
# Run ALL tests including tier 2
sudo pytest -m "" tests/ -vTier 2 tests use loopback-mounted btrfs filesystems:
- Creates sparse image files (256MB each)
- Formats them as btrfs
- Mounts via loopback devices
- Runs tests with real btrfs operations
- Cleans up automatically
This allows testing actual btrfs send/receive, snapshots, and subvolume operations without modifying your real filesystems.
For safer tier 2 testing, use a privileged container:
# Using Podman (Fedora/RHEL)
podman run --rm -it --privileged \
-v $(pwd):/workspace:Z \
fedora:latest \
bash -c "dnf install -y btrfs-progs python3 python3-pip && \
cd /workspace && \
pip install -e '.[test]' && \
pytest -m tier2 tests/integration/tier2/ -v"
# Using Docker
docker run --rm -it --privileged \
-v $(pwd):/workspace \
fedora:latest \
bash -c "dnf install -y btrfs-progs python3 python3-pip && \
cd /workspace && \
pip install -e '.[test]' && \
pytest -m tier2 tests/integration/tier2/ -v"test_btrfs_operations.py: Core btrfs subvolume, snapshot, send/receive operationstest_endpoints_real.py: LocalEndpoint class with real btrfs filesystems
Current coverage targets:
- Tier 1: ~63% of testable code
- Goal: 70%+ with additional mocking
Many modules require real btrfs operations and are excluded from tier 1 coverage metrics but covered by tier 2 tests.
src/btrfs_backup_ng/
├── __main__.py # Entry point
├── __util__.py # Common utilities
├── __logger__.py # Logging infrastructure
├── cli/ # CLI subcommands
│ ├── dispatcher.py # Command routing
│ ├── run.py # Main backup command
│ ├── restore.py # Restore command
│ └── ...
├── config/ # Configuration handling
│ ├── loader.py # TOML loading
│ └── schema.py # Validation
├── core/ # Core backup logic
│ ├── operations.py # Backup operations
│ ├── restore.py # Restore logic
│ └── ...
└── endpoint/ # Backup targets
├── local.py # Local filesystem
└── ssh.py # SSH remote
tests/
├── test_*.py # Tier 1 unit tests
└── integration/
├── test_*.py # Tier 1 integration tests (mocked)
└── tier2/ # Tier 2 real btrfs tests
├── conftest.py # Loopback btrfs fixtures
└── test_*.py # Real btrfs tests
Small, focused changes are easiest to review and land fastest.
- Tests: Add tests for new functionality
- Type hints: Include type annotations for new code
- Linting: Ensure
ruff checkpasses - Formatting: Run
ruff formatbefore committing - Documentation: Update README/docstrings as needed
Commit style:
- Conventional-commit prefixes:
fix:,feat:,docs:,refactor:,chore: - Explain why in the message. What changed is in the diff; why it changed is not, and that is what the next reader needs.
- No AI or tool attribution anywhere -- not in commits, code, comments or docs. The author line is the only attribution this project carries.
If you have found the cause of something but are unsure about the right fix, open a draft PR or title it WIP and say what you are unsure about. That is a useful contribution on its own, and it is usually the fastest way to agree on a direction before you spend more time on it. It will not be merged in that state, and it will not be closed for being unfinished.
The checklist above applies to work intended to land, not to a WIP that exists to start a conversation.
This is a backup tool. People point it at data they cannot afford to lose, so changes get read closely and may go through a few rounds. That reflects the blast radius of a mistake here, not a judgement of the contribution.
GitHub holds Actions workflows on pull requests from first-time contributors until a maintainer approves the run, so there may be a delay before you see CI results. That is normal and is not a problem with your PR.
| Workflow | Trigger | Purpose |
|---|---|---|
ci.yml |
Every push/PR | Tier 1 tests, linting, type checking |
integration-tier2.yml |
Weekly, manual, tier2 file changes | Real btrfs integration tests |
Please include:
- Python version (
python --version) - btrfs-progs version (
btrfs --version) - Linux kernel version (
uname -r) - Configuration file (sanitized)
- Full error output (add
--debugif you can reproduce it)
Two more things narrow a report down faster than anything else:
- Which target scheme: local,
ssh://,raw://orraw+ssh:// - Which privilege level: running as root, or as a normal user with
sudo
Behaviour genuinely differs across those combinations, so knowing which one you
hit usually points straight at the relevant code path. --ssh-sudo in
particular does not mean the same thing on every scheme; the Permissions section
of README.md covers that.
A question that turns out to be a documentation gap is a bug report, and gets treated as one.