add project tooling and test suite for many-rsync
- Add pyproject.toml: hatchling build, many-rsync entrypoint, ruff/mypy/pytest config with 60% coverage floor - Add uv.lock for reproducible dev installs - Add .pre-commit-config.yaml: ruff (with --fix) + mypy hooks - Add test_main.py: unit tests for _build_rsync_cmd, _load_raw, and load_config covering happy paths and FATAL exit cases - Add explanation.md: architecture overview with flowchart - main.py: refactor into typed, testable functions (_load_raw, _build_rsync_cmd extracted); add RsyncParameters/Config TypedDicts; add rsync_parameters config support (rsync_path, exclude_from); harden validation (n clamped, log_level validated) - README.md: update install instructions and document all config fields including rsync_parameters
This commit is contained in:
105
many-rsync/test_main.py
Normal file
105
many-rsync/test_main.py
Normal file
@@ -0,0 +1,105 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from main import _build_rsync_cmd, _load_raw, load_config
|
||||
|
||||
# ── _build_rsync_cmd ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_build_rsync_cmd_no_params() -> None:
|
||||
cmd = _build_rsync_cmd({})
|
||||
assert cmd == ("rsync", "-avh", "--progress", "--delete", "--stats")
|
||||
|
||||
|
||||
def test_build_rsync_cmd_with_rsync_path() -> None:
|
||||
cmd = _build_rsync_cmd({"rsync_path": "/usr/local/bin/rsync"})
|
||||
assert "--rsync-path=/usr/local/bin/rsync" in cmd
|
||||
|
||||
|
||||
def test_build_rsync_cmd_with_exclude_from() -> None:
|
||||
cmd = _build_rsync_cmd({"exclude_from": ".gitignore"})
|
||||
assert "--exclude-from=.gitignore" in cmd
|
||||
|
||||
|
||||
def test_build_rsync_cmd_with_all_params() -> None:
|
||||
cmd = _build_rsync_cmd({"rsync_path": "/opt/rsync", "exclude_from": "exc.txt"})
|
||||
assert "--rsync-path=/opt/rsync" in cmd
|
||||
assert "--exclude-from=exc.txt" in cmd
|
||||
|
||||
|
||||
# ── _load_raw ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_load_raw_toml(tmp_path: Path) -> None:
|
||||
p = tmp_path / "cfg.toml"
|
||||
p.write_text('remote_folder = "host:/dst"\nlocal_folders = ["a"]\n')
|
||||
raw = _load_raw(p)
|
||||
assert raw["remote_folder"] == "host:/dst"
|
||||
|
||||
|
||||
def test_load_raw_json(tmp_path: Path) -> None:
|
||||
p = tmp_path / "cfg.json"
|
||||
p.write_text('{"remote_folder": "/dst", "local_folders": ["a"]}')
|
||||
raw = _load_raw(p)
|
||||
assert raw["remote_folder"] == "/dst"
|
||||
|
||||
|
||||
def test_load_raw_unsupported_format(tmp_path: Path) -> None:
|
||||
p = tmp_path / "cfg.yaml"
|
||||
p.write_text("key: val")
|
||||
with pytest.raises(SystemExit):
|
||||
_load_raw(p)
|
||||
|
||||
|
||||
# ── load_config ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_load_config_valid(tmp_path: Path) -> None:
|
||||
d = tmp_path / "src"
|
||||
d.mkdir()
|
||||
p = tmp_path / "cfg.toml"
|
||||
p.write_text(f'remote_folder = "host:/dst"\nlocal_folders = ["{d}"]\nn = 1\n')
|
||||
cfg = load_config(p)
|
||||
assert cfg["remote_folder"] == "host:/dst"
|
||||
assert cfg["local_folders"] == [d]
|
||||
assert cfg["n"] == 1
|
||||
assert cfg["log_level"] == "INFO"
|
||||
|
||||
|
||||
def test_load_config_empty_folders(tmp_path: Path) -> None:
|
||||
p = tmp_path / "cfg.toml"
|
||||
p.write_text('remote_folder = "host:/dst"\nlocal_folders = []\n')
|
||||
with pytest.raises(SystemExit):
|
||||
load_config(p)
|
||||
|
||||
|
||||
def test_load_config_missing_remote(tmp_path: Path) -> None:
|
||||
d = tmp_path / "src"
|
||||
d.mkdir()
|
||||
p = tmp_path / "cfg.toml"
|
||||
p.write_text(f'local_folders = ["{d}"]\n')
|
||||
with pytest.raises(SystemExit):
|
||||
load_config(p)
|
||||
|
||||
|
||||
def test_load_config_n_clamped(tmp_path: Path) -> None:
|
||||
d = tmp_path / "src"
|
||||
d.mkdir()
|
||||
p = tmp_path / "cfg.toml"
|
||||
p.write_text(f'remote_folder = "host:/dst"\nlocal_folders = ["{d}"]\nn = 99\n')
|
||||
cfg = load_config(p)
|
||||
assert cfg["n"] == 1 # clamped to len(folders)
|
||||
|
||||
|
||||
def test_load_config_invalid_log_level(tmp_path: Path) -> None:
|
||||
d = tmp_path / "src"
|
||||
d.mkdir()
|
||||
p = tmp_path / "cfg.toml"
|
||||
p.write_text(
|
||||
f'remote_folder = "host:/dst"\nlocal_folders = ["{d}"]\nlog_level = "TRACE"\n'
|
||||
)
|
||||
with pytest.raises(SystemExit):
|
||||
load_config(p)
|
||||
Reference in New Issue
Block a user