Files
pyflowx/tests/cli/test_folderback.py
T
zhou 3f966a230e
CI / Lint, Typecheck & Test (push) Successful in 2m5s
refactor: 简化 CLI 工具入口为 YAML 加载器
将 13 个工具入口文件重构为通过 px.run_yaml 调用 YAML 配置,
辅助函数移至 _ops 模块。新增 run_yaml 便捷函数支持 job 选择
和传递依赖收集,修复 _build_cmd 列表变量展开,新增 bump_project_version
高层函数封装版本号更新+git 提交流程。
2026-07-04 19:35:08 +08:00

179 lines
6.2 KiB
Python

"""Tests for cli.folderback module."""
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
import pyflowx as px
from pyflowx.cli import folderback
from pyflowx.cli._ops import files
# ---------------------------------------------------------------------- #
# remove_dump
# ---------------------------------------------------------------------- #
class TestRemoveDump:
"""Test remove_dump function."""
def test_remove_dump_no_files(self, tmp_path: Path) -> None:
"""Should handle no zip files."""
src = tmp_path / "source"
src.mkdir()
dst = tmp_path / "backup"
dst.mkdir()
files.remove_dump(src, dst, 5)
# Should not raise error
def test_remove_dump_within_limit(self, tmp_path: Path) -> None:
"""Should not remove files within limit."""
src = tmp_path / "source"
src.mkdir()
dst = tmp_path / "backup"
dst.mkdir()
# Create some zip files
for i in range(3):
zip_file = dst / f"source_20240101_12000{i}.zip"
zip_file.write_bytes(b"ZIP content")
files.remove_dump(src, dst, 5)
# All files should remain
assert len(list(dst.glob("*.zip"))) == 3
def test_remove_dump_exceeds_limit(self, tmp_path: Path) -> None:
"""Should remove oldest files when exceeds limit."""
src = tmp_path / "source"
src.mkdir()
dst = tmp_path / "backup"
dst.mkdir()
# Create more zip files than limit
for i in range(7):
zip_file = dst / f"source_20240101_12000{i}.zip"
zip_file.write_bytes(b"ZIP content")
files.remove_dump(src, dst, 5)
# Should have only 5 files
assert len(list(dst.glob("*.zip"))) == 5
# ---------------------------------------------------------------------- #
# zip_target
# ---------------------------------------------------------------------- #
class TestZipTarget:
"""Test zip_target function."""
def test_zip_target_creates_zip(self, tmp_path: Path) -> None:
"""Should create zip file."""
src = tmp_path / "source"
src.mkdir()
(src / "test.txt").write_text("test content")
dst = tmp_path / "backup"
dst.mkdir()
with patch("time.strftime", return_value="_20240101_120000"):
files.zip_target(src, dst, 5)
# Should create zip file
zip_files = list(dst.glob("*.zip"))
assert len(zip_files) == 1
def test_zip_target_with_subdirectories(self, tmp_path: Path) -> None:
"""Should zip files in subdirectories."""
src = tmp_path / "source"
src.mkdir()
subdir = src / "subdir"
subdir.mkdir()
(src / "test.txt").write_text("test content")
(subdir / "nested.txt").write_text("nested content")
dst = tmp_path / "backup"
dst.mkdir()
with patch("time.strftime", return_value="_20240101_120000"):
files.zip_target(src, dst, 5)
# Should create zip file
zip_files = list(dst.glob("*.zip"))
assert len(zip_files) == 1
# ---------------------------------------------------------------------- #
# backup_folder
# ---------------------------------------------------------------------- #
class TestBackupFolder:
"""Test backup_folder function."""
def test_backup_folder_with_source_and_backup(self, tmp_path: Path) -> None:
"""Should backup folder with source and backup paths."""
source_dir = tmp_path / "source"
source_dir.mkdir()
(source_dir / "test.txt").write_text("test content")
backup_dir = tmp_path / "backup"
with patch.object(files, "zip_target") as mock_zip:
files.backup_folder(str(source_dir), str(backup_dir), 5)
assert mock_zip.called
def test_backup_folder_with_max_backups(self, tmp_path: Path) -> None:
"""Should backup folder with max backups."""
source_dir = tmp_path / "source"
source_dir.mkdir()
(source_dir / "test.txt").write_text("test content")
backup_dir = tmp_path / "backup"
with patch.object(files, "zip_target") as mock_zip:
files.backup_folder(str(source_dir), str(backup_dir), 10)
assert mock_zip.called
def test_backup_folder_source_not_exists(self, tmp_path: Path) -> None:
"""Should handle non-existent source folder."""
source_dir = tmp_path / "nonexistent"
backup_dir = tmp_path / "backup"
backup_dir.mkdir()
files.backup_folder(str(source_dir), str(backup_dir), 5)
# Should print error message and return
def test_backup_folder_creates_dst(self, tmp_path: Path) -> None:
"""Should create destination directory."""
source_dir = tmp_path / "source"
source_dir.mkdir()
(source_dir / "test.txt").write_text("test content")
backup_dir = tmp_path / "backup"
with patch.object(files, "zip_target") as mock_zip:
files.backup_folder(str(source_dir), str(backup_dir), 5)
assert backup_dir.exists()
assert mock_zip.called
# ---------------------------------------------------------------------- #
# 函数注册
# ---------------------------------------------------------------------- #
class TestRegisteredFunctions:
"""Test that folderback functions are registered."""
def test_folderback_default_spec(self) -> None:
"""folderback_default should be a registered callable."""
# folderback_default 现在是通过 @px.register_fn 注册的普通函数, 不是 TaskSpec
assert callable(files.folderback_default)
# ---------------------------------------------------------------------- #
# main function
# ---------------------------------------------------------------------- #
class TestMain:
"""Test main function."""
def test_main_calls_run_yaml(self) -> None:
"""main() should parse args and call px.run_yaml()."""
with patch("sys.argv", ["folderback", "--src", ".", "--dst", "./backup"]), patch.object(
px, "run_yaml"
) as mock_run_yaml:
folderback.main()
assert mock_run_yaml.called
# 验证 jobs 参数为 "backup" (YAML 中的 job 名)
assert mock_run_yaml.call_args.kwargs["jobs"] == "backup"