Files
cura-te-ipsum/tests/test_cli.py

532 lines
22 KiB
Python
Raw Permalink Normal View History

"""Tests for CLI module (cli.py)."""
import sys
from unittest import mock
import pytest
from curateipsum import cli
class TestArgumentParsing:
"""Test command-line argument parsing."""
def test_requires_backups_dir(self):
"""Should fail when -b flag is missing."""
with pytest.raises(SystemExit):
with mock.patch('sys.argv', ['cura-te-ipsum', '/src1']):
with mock.patch('argparse.ArgumentParser.parse_args') as m:
m.side_effect = SystemExit(2)
cli.main()
def test_requires_at_least_one_source(self):
"""Should fail when no sources are provided."""
with pytest.raises(SystemExit):
with mock.patch('sys.argv', ['cura-te-ipsum', '-b', '/backups']):
with mock.patch('argparse.ArgumentParser.parse_args') as m:
m.side_effect = SystemExit(2)
cli.main()
def test_parses_single_source(self, tmp_path):
"""Should parse single source directory."""
backups_dir = tmp_path / "backups"
backups_dir.mkdir()
source = tmp_path / "src"
source.mkdir()
with mock.patch('sys.argv', ['cura-te-ipsum', '-b',
str(backups_dir), str(source)]):
with mock.patch('curateipsum.backup.set_backups_lock',
return_value=True):
with mock.patch('curateipsum.backup.cleanup_old_backups'):
with mock.patch('curateipsum.backup.initiate_backup'):
with mock.patch(
'curateipsum.backup.release_backups_lock'):
result = cli.main()
assert result == 0
def test_parses_multiple_sources(self, tmp_path):
"""Should parse multiple source directories."""
backups_dir = tmp_path / "backups"
backups_dir.mkdir()
src1 = tmp_path / "src1"
src1.mkdir()
src2 = tmp_path / "src2"
src2.mkdir()
with mock.patch('sys.argv', ['cura-te-ipsum', '-b',
str(backups_dir), str(src1), str(src2)]):
with mock.patch('curateipsum.backup.set_backups_lock',
return_value=True):
with mock.patch('curateipsum.backup.cleanup_old_backups'):
with mock.patch('curateipsum.backup.initiate_backup') \
as m_init:
with mock.patch(
'curateipsum.backup.release_backups_lock'):
result = cli.main()
assert result == 0
m_init.assert_called_once()
call_args = m_init.call_args
assert call_args.kwargs['sources'] == [str(src1), str(src2)]
def test_verbose_flag_sets_debug_logging(self, tmp_path):
"""Should enable DEBUG logging when --verbose is set."""
backups_dir = tmp_path / "backups"
backups_dir.mkdir()
source = tmp_path / "src"
source.mkdir()
with mock.patch('sys.argv', ['cura-te-ipsum', '-b',
str(backups_dir), str(source),
'--verbose']):
with mock.patch('logging.basicConfig') as m_logging:
with mock.patch('curateipsum.backup.set_backups_lock',
return_value=True):
with mock.patch('curateipsum.backup.cleanup_old_backups'):
with mock.patch('curateipsum.backup.initiate_backup'):
with mock.patch(
'curateipsum.backup.release_backups_lock'):
cli.main()
import logging
m_logging.assert_called_once()
assert m_logging.call_args.kwargs['level'] == logging.DEBUG
def test_dry_run_flag_passed_to_backup(self, tmp_path):
"""Should pass dry_run=True when --dry-run is set."""
backups_dir = tmp_path / "backups"
backups_dir.mkdir()
source = tmp_path / "src"
source.mkdir()
with mock.patch('sys.argv', ['cura-te-ipsum', '-b',
str(backups_dir), str(source),
'--dry-run']):
with mock.patch('curateipsum.backup.set_backups_lock',
return_value=True):
with mock.patch('curateipsum.backup.cleanup_old_backups') \
as m_cleanup:
with mock.patch('curateipsum.backup.initiate_backup') \
as m_init:
with mock.patch(
'curateipsum.backup.release_backups_lock'):
cli.main()
assert m_cleanup.call_args.kwargs['dry_run'] is True
assert m_init.call_args.kwargs['dry_run'] is True
def test_force_flag_passed_to_lock(self, tmp_path):
"""Should pass force=True to set_backups_lock when --force is set."""
backups_dir = tmp_path / "backups"
backups_dir.mkdir()
source = tmp_path / "src"
source.mkdir()
with mock.patch('sys.argv', ['cura-te-ipsum', '-b',
str(backups_dir), str(source),
'--force']):
with mock.patch('curateipsum.backup.set_backups_lock',
return_value=True) as m_lock:
with mock.patch('curateipsum.backup.cleanup_old_backups'):
with mock.patch('curateipsum.backup.initiate_backup'):
with mock.patch(
'curateipsum.backup.release_backups_lock'):
cli.main()
m_lock.assert_called_once_with(str(backups_dir), True)
def test_external_rsync_flag_passed_to_backup(self, tmp_path):
"""Should pass external_rsync=True when --external-rsync is set."""
backups_dir = tmp_path / "backups"
backups_dir.mkdir()
source = tmp_path / "src"
source.mkdir()
with mock.patch('sys.argv', ['cura-te-ipsum', '-b',
str(backups_dir), str(source),
'--external-rsync']):
with mock.patch('shutil.which', return_value='/usr/bin/rsync'):
with mock.patch('curateipsum.backup.set_backups_lock',
return_value=True):
with mock.patch('curateipsum.backup.cleanup_old_backups'):
with mock.patch('curateipsum.backup.initiate_backup') \
as m_init:
with mock.patch(
'curateipsum.backup.release_backups_lock'):
cli.main()
assert m_init.call_args.kwargs['external_rsync'] is True
def test_external_hardlink_flag_passed_to_backup(self, tmp_path):
"""Should pass external_hardlink=True when --external-hardlink is
set."""
backups_dir = tmp_path / "backups"
backups_dir.mkdir()
source = tmp_path / "src"
source.mkdir()
with mock.patch('sys.argv', ['cura-te-ipsum', '-b',
str(backups_dir), str(source),
'--external-hardlink']):
with mock.patch('shutil.which', return_value='/usr/bin/cp'):
with mock.patch('curateipsum.backup.set_backups_lock',
return_value=True):
with mock.patch('curateipsum.backup.cleanup_old_backups'):
with mock.patch('curateipsum.backup.initiate_backup') \
as m_init:
with mock.patch(
'curateipsum.backup.release_backups_lock'):
cli.main()
assert m_init.call_args.kwargs['external_hardlink'] is True
class TestPlatformValidation:
"""Test platform-specific checks."""
def test_allows_linux_platform(self, tmp_path):
"""Should allow execution on Linux."""
backups_dir = tmp_path / "backups"
backups_dir.mkdir()
source = tmp_path / "src"
source.mkdir()
with mock.patch('sys.platform', 'linux'):
with mock.patch('sys.argv', ['cura-te-ipsum', '-b',
str(backups_dir), str(source)]):
with mock.patch('curateipsum.backup.set_backups_lock',
return_value=True):
with mock.patch('curateipsum.backup.cleanup_old_backups'):
with mock.patch('curateipsum.backup.initiate_backup'):
with mock.patch(
'curateipsum.backup.release_backups_lock'):
result = cli.main()
assert result == 0
def test_allows_darwin_platform(self, tmp_path):
"""Should allow execution on macOS (darwin)."""
backups_dir = tmp_path / "backups"
backups_dir.mkdir()
source = tmp_path / "src"
source.mkdir()
with mock.patch('sys.platform', 'darwin'):
with mock.patch('sys.argv', ['cura-te-ipsum', '-b',
str(backups_dir), str(source)]):
with mock.patch('curateipsum.backup.set_backups_lock',
return_value=True):
with mock.patch('curateipsum.backup.cleanup_old_backups'):
with mock.patch('curateipsum.backup.initiate_backup'):
with mock.patch(
'curateipsum.backup.release_backups_lock'):
result = cli.main()
assert result == 0
def test_rejects_windows_platform(self, tmp_path):
"""Should reject Windows platform."""
backups_dir = tmp_path / "backups"
backups_dir.mkdir()
source = tmp_path / "src"
source.mkdir()
with mock.patch('sys.platform', 'win32'):
with mock.patch('sys.argv', ['cura-te-ipsum', '-b',
str(backups_dir), str(source)]):
result = cli.main()
assert result == 1
def test_rejects_unsupported_platform(self, tmp_path):
"""Should reject unsupported platforms."""
backups_dir = tmp_path / "backups"
backups_dir.mkdir()
source = tmp_path / "src"
source.mkdir()
with mock.patch('sys.platform', 'freebsd'):
with mock.patch('sys.argv', ['cura-te-ipsum', '-b',
str(backups_dir), str(source)]):
result = cli.main()
assert result == 1
class TestExternalToolValidation:
"""Test validation of external tool availability."""
def test_requires_rsync_when_external_rsync_enabled(self, tmp_path):
"""Should fail when rsync is missing and --external-rsync is set."""
backups_dir = tmp_path / "backups"
backups_dir.mkdir()
source = tmp_path / "src"
source.mkdir()
with mock.patch('sys.argv', ['cura-te-ipsum', '-b',
str(backups_dir), str(source),
'--external-rsync']):
with mock.patch('shutil.which', return_value=None):
result = cli.main()
assert result == 1
def test_requires_cp_on_linux_when_external_hardlink_enabled(
self, tmp_path):
"""Should fail when cp is missing on Linux and --external-hardlink is
set."""
backups_dir = tmp_path / "backups"
backups_dir.mkdir()
source = tmp_path / "src"
source.mkdir()
with mock.patch('sys.platform', 'linux'):
with mock.patch('sys.argv', ['cura-te-ipsum', '-b',
str(backups_dir), str(source),
'--external-hardlink']):
with mock.patch('shutil.which', return_value=None):
result = cli.main()
assert result == 1
def test_requires_gcp_on_darwin_when_external_hardlink_enabled(
self, tmp_path):
"""Should fail when gcp is missing on macOS and --external-hardlink is
set."""
backups_dir = tmp_path / "backups"
backups_dir.mkdir()
source = tmp_path / "src"
source.mkdir()
with mock.patch('sys.platform', 'darwin'):
with mock.patch('sys.argv', ['cura-te-ipsum', '-b',
str(backups_dir), str(source),
'--external-hardlink']):
with mock.patch('shutil.which', return_value=None):
result = cli.main()
assert result == 1
def test_checks_correct_tool_on_darwin(self, tmp_path):
"""Should check for gcp (not cp) on macOS."""
backups_dir = tmp_path / "backups"
backups_dir.mkdir()
source = tmp_path / "src"
source.mkdir()
with mock.patch('sys.platform', 'darwin'):
with mock.patch('sys.argv', ['cura-te-ipsum', '-b',
str(backups_dir), str(source),
'--external-hardlink']):
with mock.patch('shutil.which',
side_effect=lambda x: '/usr/bin/gcp'
if x == 'gcp' else None):
with mock.patch('curateipsum.backup.set_backups_lock',
return_value=True):
with mock.patch(
'curateipsum.backup.cleanup_old_backups'):
with mock.patch(
'curateipsum.backup.initiate_backup'):
with mock.patch(
'curateipsum.backup.release_backups_lock'):
result = cli.main()
assert result == 0
class TestDirectoryValidation:
"""Test validation of backup and source directories."""
def test_fails_when_backups_dir_missing(self, tmp_path):
"""Should fail when backup directory doesn't exist."""
backups_dir = tmp_path / "nonexistent"
source = tmp_path / "src"
source.mkdir()
with mock.patch('sys.argv', ['cura-te-ipsum', '-b',
str(backups_dir), str(source)]):
result = cli.main()
assert result == 1
def test_fails_when_source_dir_missing(self, tmp_path):
"""Should fail when source directory doesn't exist."""
backups_dir = tmp_path / "backups"
backups_dir.mkdir()
source = tmp_path / "nonexistent"
with mock.patch('sys.argv', ['cura-te-ipsum', '-b',
str(backups_dir), str(source)]):
result = cli.main()
assert result == 1
def test_fails_when_any_source_dir_missing(self, tmp_path):
"""Should fail when any source directory doesn't exist."""
backups_dir = tmp_path / "backups"
backups_dir.mkdir()
src1 = tmp_path / "src1"
src1.mkdir()
src2 = tmp_path / "nonexistent"
with mock.patch('sys.argv', ['cura-te-ipsum', '-b',
str(backups_dir), str(src1), str(src2)]):
result = cli.main()
assert result == 1
def test_converts_backups_dir_to_absolute_path(self, tmp_path):
"""Should convert relative backup directory path to absolute."""
backups_dir = tmp_path / "backups"
backups_dir.mkdir()
source = tmp_path / "src"
source.mkdir()
with mock.patch('sys.argv', ['cura-te-ipsum', '-b', 'backups',
str(source)]):
with mock.patch('os.path.abspath',
return_value=str(backups_dir)) as m_abspath:
with mock.patch('os.path.isdir', return_value=True):
with mock.patch('curateipsum.backup.set_backups_lock',
return_value=True) as m_lock:
with mock.patch(
'curateipsum.backup.cleanup_old_backups'):
with mock.patch(
'curateipsum.backup.initiate_backup'):
with mock.patch(
'curateipsum.backup.release_backups_lock'):
cli.main()
m_abspath.assert_called_with('backups')
m_lock.assert_called_once_with(str(backups_dir), False)
class TestLockHandling:
"""Test backup lock acquisition and release."""
def test_exits_when_lock_acquisition_fails(self, tmp_path):
"""Should exit with code 1 when lock cannot be acquired."""
backups_dir = tmp_path / "backups"
backups_dir.mkdir()
source = tmp_path / "src"
source.mkdir()
with mock.patch('sys.argv', ['cura-te-ipsum', '-b',
str(backups_dir), str(source)]):
with mock.patch('curateipsum.backup.set_backups_lock',
return_value=False):
result = cli.main()
assert result == 1
def test_releases_lock_after_successful_backup(self, tmp_path):
"""Should release lock after backup completes."""
backups_dir = tmp_path / "backups"
backups_dir.mkdir()
source = tmp_path / "src"
source.mkdir()
with mock.patch('sys.argv', ['cura-te-ipsum', '-b',
str(backups_dir), str(source)]):
with mock.patch('curateipsum.backup.set_backups_lock',
return_value=True):
with mock.patch('curateipsum.backup.cleanup_old_backups'):
with mock.patch('curateipsum.backup.initiate_backup'):
with mock.patch(
'curateipsum.backup.release_backups_lock') \
as m_release:
cli.main()
m_release.assert_called_once_with(str(backups_dir))
class TestBackupExecution:
"""Test backup execution flow."""
def test_calls_cleanup_before_initiate(self, tmp_path):
"""Should call cleanup_old_backups before initiate_backup."""
backups_dir = tmp_path / "backups"
backups_dir.mkdir()
source = tmp_path / "src"
source.mkdir()
call_order = []
def mock_cleanup(**kwargs):
call_order.append('cleanup')
def mock_initiate(**kwargs):
call_order.append('initiate')
with mock.patch('sys.argv', ['cura-te-ipsum', '-b',
str(backups_dir), str(source)]):
with mock.patch('curateipsum.backup.set_backups_lock',
return_value=True):
with mock.patch('curateipsum.backup.cleanup_old_backups',
side_effect=mock_cleanup):
with mock.patch('curateipsum.backup.initiate_backup',
side_effect=mock_initiate):
with mock.patch(
'curateipsum.backup.release_backups_lock'):
cli.main()
assert call_order == ['cleanup', 'initiate']
def test_passes_correct_arguments_to_initiate_backup(self, tmp_path):
"""Should pass all required arguments to initiate_backup."""
backups_dir = tmp_path / "backups"
backups_dir.mkdir()
src1 = tmp_path / "src1"
src1.mkdir()
src2 = tmp_path / "src2"
src2.mkdir()
with mock.patch('sys.argv', ['cura-te-ipsum', '-b',
str(backups_dir), str(src1), str(src2),
'--dry-run', '--external-rsync',
'--external-hardlink']):
with mock.patch('shutil.which', return_value='/usr/bin/tool'):
with mock.patch('curateipsum.backup.set_backups_lock',
return_value=True):
with mock.patch('curateipsum.backup.cleanup_old_backups'):
with mock.patch('curateipsum.backup.initiate_backup') \
as m_init:
with mock.patch(
'curateipsum.backup.release_backups_lock'):
cli.main()
m_init.assert_called_once_with(
sources=[str(src1), str(src2)],
backups_dir=str(backups_dir),
dry_run=True,
external_rsync=True,
external_hardlink=True,
)
def test_logs_completion_time(self, tmp_path, caplog):
"""Should log time spent after backup completes."""
backups_dir = tmp_path / "backups"
backups_dir.mkdir()
source = tmp_path / "src"
source.mkdir()
import logging
caplog.set_level(logging.INFO)
with mock.patch('sys.argv', ['cura-te-ipsum', '-b',
str(backups_dir), str(source)]):
with mock.patch('curateipsum.backup.set_backups_lock',
return_value=True):
with mock.patch('curateipsum.backup.cleanup_old_backups'):
with mock.patch('curateipsum.backup.initiate_backup'):
with mock.patch(
'curateipsum.backup.release_backups_lock'):
cli.main()
# check that completion message was logged
assert any("Finished" in record.message for record in caplog.records)
assert any("time spent" in record.message
for record in caplog.records)