Research-Stack/5-Applications/nodupe/tests/maintenance/test_snapshot.py
2026-05-05 21:15:26 -05:00

896 lines
35 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Comprehensive tests for the Snapshot module.
Tests cover:
- SnapshotFile and Snapshot dataclasses
- SnapshotManager initialization
- Hash computation with various algorithms
- Snapshot creation and restoration
- Idempotent backup behavior
- Edge cases and error conditions
"""
import hashlib
import json
import os
import shutil
import tempfile
from datetime import datetime
from pathlib import Path
import pytest
from nodupe.tools.maintenance.snapshot import (
HASH_ALGORITHMS,
Snapshot,
SnapshotFile,
SnapshotManager,
get_hasher,
)
class TestHashAlgorithms:
"""Tests for hash algorithm constants and get_hasher function."""
def test_hash_algorithms_contains_all_supported(self):
"""Test that HASH_ALGORITHMS contains all expected algorithms."""
expected_algorithms = {
"sha256", "sha384", "sha512",
"sha3_256", "sha3_384", "sha3_512",
"blake2b", "blake2s"
}
assert set(HASH_ALGORITHMS.keys()) == expected_algorithms
def test_get_hasher_returns_callable(self):
"""Test that get_hasher returns a callable for each algorithm."""
for algorithm in HASH_ALGORITHMS:
hasher_func = get_hasher(algorithm)
assert callable(hasher_func)
hasher = hasher_func()
assert hasattr(hasher, 'update')
assert hasattr(hasher, 'hexdigest')
def test_get_hasher_sha256(self):
"""Test get_hasher with sha256."""
hasher_func = get_hasher("sha256")
hasher = hasher_func()
hasher.update(b"test")
assert hasher.hexdigest() == hashlib.sha256(b"test").hexdigest()
def test_get_hasher_sha384(self):
"""Test get_hasher with sha384."""
hasher_func = get_hasher("sha384")
hasher = hasher_func()
hasher.update(b"test")
assert hasher.hexdigest() == hashlib.sha384(b"test").hexdigest()
def test_get_hasher_sha512(self):
"""Test get_hasher with sha512."""
hasher_func = get_hasher("sha512")
hasher = hasher_func()
hasher.update(b"test")
assert hasher.hexdigest() == hashlib.sha512(b"test").hexdigest()
def test_get_hasher_sha3_variants(self):
"""Test get_hasher with SHA-3 variants."""
for algorithm in ["sha3_256", "sha3_384", "sha3_512"]:
hasher_func = get_hasher(algorithm)
hasher = hasher_func()
hasher.update(b"test")
expected = getattr(hashlib, algorithm)(b"test").hexdigest()
assert hasher.hexdigest() == expected
def test_get_hasher_blake2_variants(self):
"""Test get_hasher with BLAKE2 variants."""
for algorithm in ["blake2b", "blake2s"]:
hasher_func = get_hasher(algorithm)
hasher = hasher_func()
hasher.update(b"test")
expected = getattr(hashlib, algorithm)(b"test").hexdigest()
assert hasher.hexdigest() == expected
def test_get_hasher_invalid_algorithm(self):
"""Test that get_hasher raises ValueError for invalid algorithm."""
with pytest.raises(ValueError) as exc_info:
get_hasher("invalid_algorithm")
assert "Unsupported hash algorithm" in str(exc_info.value)
assert "invalid_algorithm" in str(exc_info.value)
def test_get_hasher_error_message_lists_supported(self):
"""Test that error message includes list of supported algorithms."""
with pytest.raises(ValueError) as exc_info:
get_hasher("md5")
error_msg = str(exc_info.value)
assert "sha256" in error_msg
assert "blake2b" in error_msg
class TestSnapshotFile:
"""Tests for SnapshotFile dataclass."""
def test_snapshot_file_creation(self):
"""Test creating a SnapshotFile instance."""
sf = SnapshotFile(
path="/test/file.txt",
hash="abc123",
size=1024,
modified="2024-01-01T00:00:00"
)
assert sf.path == "/test/file.txt"
assert sf.hash == "abc123"
assert sf.size == 1024
assert sf.modified == "2024-01-01T00:00:00"
assert sf.backup_path is None
assert sf.hash_algorithm is None
def test_snapshot_file_with_optional_fields(self):
"""Test SnapshotFile with optional fields."""
sf = SnapshotFile(
path="/test/file.txt",
hash="abc123",
size=1024,
modified="2024-01-01T00:00:00",
backup_path="/backup/abc123",
hash_algorithm="sha256"
)
assert sf.backup_path == "/backup/abc123"
assert sf.hash_algorithm == "sha256"
class TestSnapshot:
"""Tests for Snapshot dataclass."""
def test_snapshot_creation(self):
"""Test creating a Snapshot instance."""
files = [
SnapshotFile(
path="/test/file1.txt",
hash="hash1",
size=100,
modified="2024-01-01T00:00:00"
)
]
snapshot = Snapshot(
snapshot_id="test123",
timestamp="2024-01-01T00:00:00",
files=files
)
assert snapshot.snapshot_id == "test123"
assert snapshot.timestamp == "2024-01-01T00:00:00"
assert len(snapshot.files) == 1
assert snapshot.hash_algorithm is None
def test_snapshot_with_hash_algorithm(self):
"""Test Snapshot with hash algorithm specified."""
snapshot = Snapshot(
snapshot_id="test123",
timestamp="2024-01-01T00:00:00",
files=[],
hash_algorithm="sha384"
)
assert snapshot.hash_algorithm == "sha384"
def test_snapshot_to_dict(self):
"""Test converting Snapshot to dictionary."""
files = [
SnapshotFile(
path="/test/file.txt",
hash="abc123",
size=1024,
modified="2024-01-01T00:00:00",
backup_path="/backup/abc123"
)
]
snapshot = Snapshot(
snapshot_id="test123",
timestamp="2024-01-01T00:00:00",
files=files,
hash_algorithm="sha256"
)
data = snapshot.to_dict()
assert data["snapshot_id"] == "test123"
assert data["timestamp"] == "2024-01-01T00:00:00"
assert data["hash_algorithm"] == "sha256"
assert len(data["files"]) == 1
assert data["files"][0]["path"] == "/test/file.txt"
assert data["files"][0]["backup_path"] == "/backup/abc123"
def test_snapshot_from_dict(self):
"""Test creating Snapshot from dictionary."""
data = {
"snapshot_id": "test456",
"timestamp": "2024-02-01T00:00:00",
"hash_algorithm": "sha512",
"files": [
{
"path": "/test/file.txt",
"hash": "def456",
"size": 2048,
"modified": "2024-02-01T00:00:00",
"backup_path": "/backup/def456",
"hash_algorithm": "sha512"
}
]
}
snapshot = Snapshot.from_dict(data)
assert snapshot.snapshot_id == "test456"
assert snapshot.timestamp == "2024-02-01T00:00:00"
assert snapshot.hash_algorithm == "sha512"
assert len(snapshot.files) == 1
assert snapshot.files[0].path == "/test/file.txt"
assert snapshot.files[0].hash == "def456"
def test_snapshot_from_dict_empty_files(self):
"""Test Snapshot.from_dict with empty files list."""
data = {
"snapshot_id": "empty123",
"timestamp": "2024-01-01T00:00:00",
"files": []
}
snapshot = Snapshot.from_dict(data)
assert snapshot.snapshot_id == "empty123"
assert len(snapshot.files) == 0
def test_snapshot_from_dict_missing_hash_algorithm(self):
"""Test Snapshot.from_dict with missing hash_algorithm."""
data = {
"snapshot_id": "test789",
"timestamp": "2024-01-01T00:00:00",
"files": []
}
snapshot = Snapshot.from_dict(data)
assert snapshot.hash_algorithm is None
def test_snapshot_roundtrip(self):
"""Test Snapshot to_dict and from_dict roundtrip."""
original = Snapshot(
snapshot_id="roundtrip123",
timestamp="2024-01-01T00:00:00",
files=[
SnapshotFile(
path="/test/file.txt",
hash="abc123",
size=1024,
modified="2024-01-01T00:00:00",
backup_path="/backup/abc123",
hash_algorithm="sha256"
)
],
hash_algorithm="sha256"
)
data = original.to_dict()
restored = Snapshot.from_dict(data)
assert restored.snapshot_id == original.snapshot_id
assert restored.timestamp == original.timestamp
assert restored.hash_algorithm == original.hash_algorithm
assert len(restored.files) == len(original.files)
assert restored.files[0].path == original.files[0].path
assert restored.files[0].hash == original.files[0].hash
class TestSnapshotManager:
"""Tests for SnapshotManager class."""
@pytest.fixture
def temp_backup_dir(self):
"""Create a temporary backup directory."""
temp_dir = tempfile.mkdtemp()
yield Path(temp_dir)
shutil.rmtree(temp_dir, ignore_errors=True)
@pytest.fixture
def snapshot_manager(self, temp_backup_dir):
"""Create a SnapshotManager instance with temp directory."""
return SnapshotManager(backup_dir=str(temp_backup_dir))
@pytest.fixture
def test_files(self, temp_backup_dir):
"""Create test files for snapshot testing."""
files_dir = temp_backup_dir / "test_files"
files_dir.mkdir()
test_files = {}
for i in range(3):
file_path = files_dir / f"file{i}.txt"
content = f"Content of file {i}" * (i + 1)
file_path.write_text(content)
test_files[f"file{i}"] = file_path
# Create a binary file
binary_file = files_dir / "binary.bin"
binary_file.write_bytes(os.urandom(256))
test_files["binary"] = binary_file
return test_files
# Initialization Tests
def test_snapshot_manager_initialization(self, temp_backup_dir):
"""Test SnapshotManager initialization creates directories."""
manager = SnapshotManager(backup_dir=str(temp_backup_dir))
assert manager.backup_dir == temp_backup_dir
assert manager.snapshot_dir.exists()
assert manager.content_dir.exists()
assert manager.snapshot_dir == temp_backup_dir / "snapshots"
assert manager.content_dir == temp_backup_dir / "content"
def test_snapshot_manager_default_hash_algorithm(self, temp_backup_dir):
"""Test SnapshotManager uses sha256 by default."""
manager = SnapshotManager(backup_dir=str(temp_backup_dir))
assert manager.hash_algorithm == "sha256"
def test_snapshot_manager_custom_hash_algorithm(self, temp_backup_dir):
"""Test SnapshotManager with custom hash algorithm."""
for algorithm in ["sha384", "sha512", "blake2b"]:
manager = SnapshotManager(
backup_dir=str(temp_backup_dir / algorithm),
hash_algorithm=algorithm
)
assert manager.hash_algorithm == algorithm
def test_snapshot_manager_creates_readme(self, temp_backup_dir):
"""Test that SnapshotManager creates README files."""
SnapshotManager(backup_dir=str(temp_backup_dir))
readme_path = temp_backup_dir / "README.md"
recovery_path = temp_backup_dir / "RECOVERY.txt"
assert readme_path.exists()
assert recovery_path.exists()
def test_snapshot_manager_readme_content(self, temp_backup_dir):
"""Test README content includes hash algorithm."""
SnapshotManager(
backup_dir=str(temp_backup_dir),
hash_algorithm="sha384"
)
readme_content = (temp_backup_dir / "README.md").read_text()
recovery_content = (temp_backup_dir / "RECOVERY.txt").read_text(encoding="latin-1")
assert "sha384" in readme_content
assert "sha384" in recovery_content
def test_snapshot_manager_does_not_overwrite_readme(self, temp_backup_dir):
"""Test that existing README files are not overwritten."""
readme_path = temp_backup_dir / "README.md"
original_content = "# Custom README\nThis is custom content."
readme_path.write_text(original_content)
SnapshotManager(backup_dir=str(temp_backup_dir))
assert readme_path.read_text() == original_content
# Hash Computation Tests
def test_compute_hash_sha256(self, snapshot_manager, test_files):
"""Test hash computation with sha256."""
file_path = test_files["file0"]
file_hash = snapshot_manager._compute_hash(file_path)
assert file_hash is not None
assert len(file_hash) == 64 # SHA-256 hex length
def test_compute_hash_consistency(self, snapshot_manager, test_files):
"""Test that hash computation is consistent."""
file_path = test_files["file0"]
hash1 = snapshot_manager._compute_hash(file_path)
hash2 = snapshot_manager._compute_hash(file_path)
assert hash1 == hash2
def test_compute_hash_different_files(self, snapshot_manager, test_files):
"""Test that different files have different hashes."""
hash1 = snapshot_manager._compute_hash(test_files["file0"])
hash2 = snapshot_manager._compute_hash(test_files["file1"])
assert hash1 != hash2
def test_compute_hash_nonexistent_file(self, snapshot_manager):
"""Test hash computation for nonexistent file returns None."""
result = snapshot_manager._compute_hash(Path("/nonexistent/file.txt"))
assert result is None
def test_compute_hash_directory(self, snapshot_manager, temp_backup_dir):
"""Test hash computation for directory returns None."""
result = snapshot_manager._compute_hash(temp_backup_dir)
assert result is None
def test_compute_hash_empty_file(self, snapshot_manager, temp_backup_dir):
"""Test hash computation for empty file."""
empty_file = temp_backup_dir / "empty.txt"
empty_file.write_text("")
result = snapshot_manager._compute_hash(empty_file)
assert result is not None
# SHA-256 of empty string
expected = hashlib.sha256(b"").hexdigest()
assert result == expected
def test_compute_hash_large_file(self, snapshot_manager, temp_backup_dir):
"""Test hash computation for large file (tests chunked reading)."""
large_file = temp_backup_dir / "large.bin"
# Create a 1MB file
with open(large_file, "wb") as f:
for _ in range(100):
f.write(os.urandom(10240))
result = snapshot_manager._compute_hash(large_file)
assert result is not None
assert len(result) == 64
# Backup Content Tests
def test_backup_file_content_creates_backup(self, snapshot_manager, test_files):
"""Test that _backup_file_content creates backup."""
file_path = test_files["file0"]
file_hash = snapshot_manager._compute_hash(file_path)
backup_path = snapshot_manager._backup_file_content(file_path, file_hash)
assert Path(backup_path).exists()
assert backup_path == str(snapshot_manager.content_dir / file_hash)
def test_backup_file_content_idempotent(self, snapshot_manager, test_files):
"""Test that _backup_file_content is idempotent."""
file_path = test_files["file0"]
file_hash = snapshot_manager._compute_hash(file_path)
# First backup
backup_path1 = snapshot_manager._backup_file_content(file_path, file_hash)
stat1 = Path(backup_path1).stat()
# Second backup (should not copy again)
backup_path2 = snapshot_manager._backup_file_content(file_path, file_hash)
stat2 = Path(backup_path2).stat()
assert backup_path1 == backup_path2
# File stats should be identical (same inode on Unix)
assert stat1.st_ino == stat2.st_ino
def test_backup_file_content_preserves_content(self, snapshot_manager, test_files):
"""Test that backup preserves file content."""
file_path = test_files["file0"]
original_content = file_path.read_text()
file_hash = snapshot_manager._compute_hash(file_path)
backup_path = snapshot_manager._backup_file_content(file_path, file_hash)
backup_content = Path(backup_path).read_text()
assert backup_content == original_content
# Snapshot Creation Tests
def test_create_snapshot_single_file(self, snapshot_manager, test_files):
"""Test creating snapshot of single file."""
file_path = test_files["file0"]
snapshot = snapshot_manager.create_snapshot([str(file_path)])
assert snapshot is not None
assert snapshot.snapshot_id is not None
assert len(snapshot.snapshot_id) == 16
assert len(snapshot.files) == 1
assert snapshot.files[0].path == str(file_path.absolute())
assert snapshot.files[0].hash is not None
assert snapshot.files[0].size == file_path.stat().st_size
assert snapshot.files[0].backup_path is not None
def test_create_snapshot_multiple_files(self, snapshot_manager, test_files):
"""Test creating snapshot of multiple files."""
paths = [str(test_files[f"file{i}"]) for i in range(3)]
snapshot = snapshot_manager.create_snapshot(paths)
assert snapshot is not None
assert len(snapshot.files) == 3
file_paths = {f.path for f in snapshot.files}
for path in paths:
assert Path(path).absolute() in [Path(p) for p in file_paths]
def test_create_snapshot_mixed_files(self, snapshot_manager, test_files):
"""Test creating snapshot with text and binary files."""
paths = [str(test_files["file0"]), str(test_files["binary"])]
snapshot = snapshot_manager.create_snapshot(paths)
assert len(snapshot.files) == 2
# Verify binary file hash is different from text file
hashes = {f.hash for f in snapshot.files}
assert len(hashes) == 2
def test_create_snapshot_nonexistent_file(self, snapshot_manager):
"""Test creating snapshot with nonexistent file."""
snapshot = snapshot_manager.create_snapshot(["/nonexistent/file.txt"])
assert snapshot is not None
assert len(snapshot.files) == 0
def test_create_snapshot_directory(self, snapshot_manager, temp_backup_dir):
"""Test creating snapshot of directory (should be ignored)."""
snapshot = snapshot_manager.create_snapshot([str(temp_backup_dir)])
assert len(snapshot.files) == 0
def test_create_snapshot_mixed_valid_invalid(self, snapshot_manager, test_files):
"""Test creating snapshot with mix of valid and invalid paths."""
paths = [
str(test_files["file0"]),
"/nonexistent/file.txt",
str(test_files["file1"])
]
snapshot = snapshot_manager.create_snapshot(paths)
assert len(snapshot.files) == 2
def test_create_snapshot_saves_json(self, snapshot_manager, test_files):
"""Test that snapshot is saved as JSON."""
file_path = test_files["file0"]
snapshot = snapshot_manager.create_snapshot([str(file_path)])
snapshot_path = snapshot_manager.snapshot_dir / f"{snapshot.snapshot_id}.json"
assert snapshot_path.exists()
with open(snapshot_path) as f:
data = json.load(f)
assert data["snapshot_id"] == snapshot.snapshot_id
assert len(data["files"]) == 1
def test_create_snapshot_idempotent_backup(self, snapshot_manager, test_files):
"""Test that snapshot creation uses idempotent backup."""
file_path = test_files["file0"]
# Create first snapshot
snapshot1 = snapshot_manager.create_snapshot([str(file_path)])
backup_path1 = snapshot1.files[0].backup_path
# Modify the file
file_path.write_text("Modified content")
# Create second snapshot
snapshot2 = snapshot_manager.create_snapshot([str(file_path)])
backup_path2 = snapshot2.files[0].backup_path
# Original backup should still exist
assert Path(backup_path1).exists()
# New backup should be created for modified content
assert backup_path1 != backup_path2
def test_create_snapshot_timestamp(self, snapshot_manager, test_files):
"""Test that snapshot has valid timestamp."""
file_path = test_files["file0"]
snapshot = snapshot_manager.create_snapshot([str(file_path)])
assert snapshot.timestamp is not None
# Verify timestamp format (ISO format)
datetime.fromisoformat(snapshot.timestamp)
def test_create_snapshot_file_modified_time(self, snapshot_manager, test_files):
"""Test that snapshot captures file modified time."""
file_path = test_files["file0"]
snapshot = snapshot_manager.create_snapshot([str(file_path)])
file_data = snapshot.files[0]
assert file_data.modified is not None
# Verify modified time format
datetime.fromisoformat(file_data.modified)
# Snapshot Restoration Tests
def test_restore_snapshot_success(self, snapshot_manager, test_files):
"""Test successful snapshot restoration."""
file_path = test_files["file0"]
original_content = file_path.read_text()
# Create snapshot
snapshot = snapshot_manager.create_snapshot([str(file_path)])
# Modify the file
modified_content = "Modified content"
file_path.write_text(modified_content)
assert file_path.read_text() == modified_content
# Restore snapshot
success = snapshot_manager.restore_snapshot(snapshot.snapshot_id)
assert success is True
assert file_path.read_text() == original_content
def test_restore_snapshot_nonexistent(self, snapshot_manager):
"""Test restoring nonexistent snapshot returns False."""
success = snapshot_manager.restore_snapshot("nonexistent_id")
assert success is False
def test_restore_snapshot_unchanged_file(self, snapshot_manager, test_files):
"""Test restoring snapshot when file is unchanged."""
file_path = test_files["file0"]
original_content = file_path.read_text()
# Create snapshot
snapshot = snapshot_manager.create_snapshot([str(file_path)])
# Restore without modifying
success = snapshot_manager.restore_snapshot(snapshot.snapshot_id)
assert success is True
assert file_path.read_text() == original_content
def test_restore_snapshot_deleted_file(self, snapshot_manager, test_files):
"""Test restoring snapshot when file was deleted."""
file_path = test_files["file0"]
original_content = file_path.read_text()
# Create snapshot
snapshot = snapshot_manager.create_snapshot([str(file_path)])
# Delete the file
file_path.unlink()
assert not file_path.exists()
# Restore snapshot - should recreate deleted files from backup
success = snapshot_manager.restore_snapshot(snapshot.snapshot_id)
assert success is True
# File should be restored from backup
assert file_path.exists()
assert file_path.read_text() == original_content
def test_restore_snapshot_multiple_files(self, snapshot_manager, test_files):
"""Test restoring snapshot with multiple files."""
file1 = test_files["file0"]
file2 = test_files["file1"]
original1 = file1.read_text()
original2 = file2.read_text()
# Create snapshot
snapshot = snapshot_manager.create_snapshot([str(file1), str(file2)])
# Modify both files
file1.write_text("Modified 1")
file2.write_text("Modified 2")
# Restore
success = snapshot_manager.restore_snapshot(snapshot.snapshot_id)
assert success is True
assert file1.read_text() == original1
assert file2.read_text() == original2
def test_restore_snapshot_partial_modification(self, snapshot_manager, test_files):
"""Test restoring snapshot when only some files changed."""
file1 = test_files["file0"]
file2 = test_files["file1"]
original2 = file2.read_text()
# Create snapshot
snapshot = snapshot_manager.create_snapshot([str(file1), str(file2)])
# Modify only file2
file2.write_text("Modified 2")
# Restore
success = snapshot_manager.restore_snapshot(snapshot.snapshot_id)
assert success is True
# file1 should be unchanged (wasn't modified)
# file2 should be restored
assert file2.read_text() == original2
def test_restore_snapshot_backup_path_missing(self, snapshot_manager, test_files):
"""Test restoring snapshot when backup path is missing."""
file_path = test_files["file0"]
file_path.read_text()
# Create snapshot
snapshot = snapshot_manager.create_snapshot([str(file_path)])
# Delete the backup content
backup_path = Path(snapshot.files[0].backup_path)
backup_path.unlink()
# Modify the file
file_path.write_text("Modified content")
# Restore should fail silently (backup missing)
success = snapshot_manager.restore_snapshot(snapshot.snapshot_id)
assert success is True # Returns True but doesn't restore
# File should remain modified
assert file_path.read_text() == "Modified content"
# List Snapshots Tests
def test_list_snapshots_empty(self, snapshot_manager):
"""Test listing snapshots when none exist."""
snapshots = snapshot_manager.list_snapshots()
assert snapshots == []
def test_list_snapshots_single(self, snapshot_manager, test_files):
"""Test listing single snapshot."""
file_path = test_files["file0"]
snapshot = snapshot_manager.create_snapshot([str(file_path)])
snapshots = snapshot_manager.list_snapshots()
assert len(snapshots) == 1
assert snapshots[0]["snapshot_id"] == snapshot.snapshot_id
assert snapshots[0]["file_count"] == 1
assert "timestamp" in snapshots[0]
def test_list_snapshots_multiple(self, snapshot_manager, test_files):
"""Test listing multiple snapshots."""
snapshots_created = []
for i in range(3):
file_path = test_files[f"file{i}"]
snapshot = snapshot_manager.create_snapshot([str(file_path)])
snapshots_created.append(snapshot)
snapshots = snapshot_manager.list_snapshots()
assert len(snapshots) == 3
# Verify all snapshot IDs are present
snapshot_ids = {s["snapshot_id"] for s in snapshots}
for snapshot in snapshots_created:
assert snapshot.snapshot_id in snapshot_ids
def test_list_snapshots_sorted_by_timestamp(self, snapshot_manager, test_files):
"""Test that snapshots are sorted by timestamp (newest first)."""
import time
file_path = test_files["file0"]
snapshot1 = snapshot_manager.create_snapshot([str(file_path)])
time.sleep(0.01) # Ensure different timestamps
snapshot2 = snapshot_manager.create_snapshot([str(file_path)])
snapshots = snapshot_manager.list_snapshots()
assert len(snapshots) == 2
# Newest first
assert snapshots[0]["snapshot_id"] == snapshot2.snapshot_id
assert snapshots[1]["snapshot_id"] == snapshot1.snapshot_id
def test_list_snapshots_includes_file_count(self, snapshot_manager, test_files):
"""Test that list_snapshots includes correct file count."""
paths = [str(test_files[f"file{i}"]) for i in range(3)]
snapshot_manager.create_snapshot(paths)
snapshots = snapshot_manager.list_snapshots()
assert snapshots[0]["file_count"] == 3
# Delete Snapshot Tests
def test_delete_snapshot_success(self, snapshot_manager, test_files):
"""Test successful snapshot deletion."""
file_path = test_files["file0"]
snapshot = snapshot_manager.create_snapshot([str(file_path)])
success = snapshot_manager.delete_snapshot(snapshot.snapshot_id)
assert success is True
# Verify snapshot file is deleted
snapshot_path = snapshot_manager.snapshot_dir / f"{snapshot.snapshot_id}.json"
assert not snapshot_path.exists()
def test_delete_snapshot_nonexistent(self, snapshot_manager):
"""Test deleting nonexistent snapshot returns False."""
success = snapshot_manager.delete_snapshot("nonexistent_id")
assert success is False
def test_delete_snapshot_content_preserved(self, snapshot_manager, test_files):
"""Test that deleting snapshot preserves content files."""
file_path = test_files["file0"]
snapshot = snapshot_manager.create_snapshot([str(file_path)])
backup_path = snapshot.files[0].backup_path
success = snapshot_manager.delete_snapshot(snapshot.snapshot_id)
assert success is True
# Content should still exist (content-addressable storage)
assert Path(backup_path).exists()
# Different Hash Algorithms Tests
def test_snapshot_manager_sha384(self, temp_backup_dir, test_files):
"""Test SnapshotManager with sha384 algorithm."""
manager = SnapshotManager(
backup_dir=str(temp_backup_dir),
hash_algorithm="sha384"
)
file_path = test_files["file0"]
snapshot = manager.create_snapshot([str(file_path)])
assert manager.hash_algorithm == "sha384"
# SHA-384 produces 96-character hex hash
assert len(snapshot.files[0].hash) == 96
def test_snapshot_manager_sha512(self, temp_backup_dir, test_files):
"""Test SnapshotManager with sha512 algorithm."""
manager = SnapshotManager(
backup_dir=str(temp_backup_dir),
hash_algorithm="sha512"
)
file_path = test_files["file0"]
snapshot = manager.create_snapshot([str(file_path)])
assert manager.hash_algorithm == "sha512"
# SHA-512 produces 128-character hex hash
assert len(snapshot.files[0].hash) == 128
def test_snapshot_manager_blake2b(self, temp_backup_dir, test_files):
"""Test SnapshotManager with blake2b algorithm."""
manager = SnapshotManager(
backup_dir=str(temp_backup_dir),
hash_algorithm="blake2b"
)
file_path = test_files["file0"]
snapshot = manager.create_snapshot([str(file_path)])
assert manager.hash_algorithm == "blake2b"
# BLAKE2b produces 128-character hex hash (default)
assert len(snapshot.files[0].hash) == 128
# Edge Cases
def test_create_snapshot_empty_paths_list(self, snapshot_manager):
"""Test creating snapshot with empty paths list."""
snapshot = snapshot_manager.create_snapshot([])
assert snapshot is not None
assert len(snapshot.files) == 0
def test_restore_snapshot_empty_files(self, snapshot_manager, temp_backup_dir):
"""Test restoring snapshot with no files."""
snapshot = Snapshot(
snapshot_id="empty",
timestamp="2024-01-01T00:00:00",
files=[]
)
# Save empty snapshot
snapshot_path = snapshot_manager.snapshot_dir / "empty.json"
with open(snapshot_path, "w") as f:
json.dump(snapshot.to_dict(), f)
success = snapshot_manager.restore_snapshot("empty")
assert success is True
def test_snapshot_manager_unicode_paths(self, snapshot_manager, temp_backup_dir):
"""Test snapshot with unicode file paths."""
unicode_file = temp_backup_dir / "test_файл_文件.txt"
unicode_file.write_text("Unicode content")
snapshot = snapshot_manager.create_snapshot([str(unicode_file)])
assert len(snapshot.files) == 1
assert "файл" in snapshot.files[0].path or "文件" in snapshot.files[0].path
def test_snapshot_manager_special_characters(self, snapshot_manager, temp_backup_dir):
"""Test snapshot with special characters in paths."""
special_file = temp_backup_dir / "test file-with spaces.txt"
special_file.write_text("Special content")
snapshot = snapshot_manager.create_snapshot([str(special_file)])
assert len(snapshot.files) == 1
def test_snapshot_manager_very_long_path(self, snapshot_manager, temp_backup_dir):
"""Test snapshot with very long file path."""
# Create nested directory structure
deep_dir = temp_backup_dir / "a" / "b" / "c" / "d" / "e"
deep_dir.mkdir(parents=True)
deep_file = deep_dir / "file.txt"
deep_file.write_text("Deep file content")
snapshot = snapshot_manager.create_snapshot([str(deep_file)])
assert len(snapshot.files) == 1
def test_snapshot_manager_symlink(self, snapshot_manager, test_files):
"""Test snapshot with symlink."""
symlink_path = test_files["file0"].parent / "symlink.txt"
try:
symlink_path.symlink_to(test_files["file0"])
snapshot = snapshot_manager.create_snapshot([str(symlink_path)])
# Symlinks should be followed
assert len(snapshot.files) == 1
except (OSError, NotImplementedError):
# Skip if symlinks not supported on this platform
pytest.skip("Symlinks not supported on this platform")
def test_snapshot_manager_permission_denied(self, snapshot_manager, temp_backup_dir):
"""Test snapshot creation when file is not readable."""
unreadable_file = temp_backup_dir / "unreadable.txt"
unreadable_file.write_text("Content")
try:
os.chmod(unreadable_file, 0o000)
snapshot = snapshot_manager.create_snapshot([str(unreadable_file)])
# Should skip unreadable files
assert len(snapshot.files) == 0
finally:
os.chmod(unreadable_file, 0o644)
def test_snapshot_manager_backup_dir_permissions(self, temp_backup_dir):
"""Test SnapshotManager creates directories with correct permissions."""
manager = SnapshotManager(backup_dir=str(temp_backup_dir))
assert manager.snapshot_dir.exists()
assert manager.content_dir.exists()
assert manager.snapshot_dir.is_dir()
assert manager.content_dir.is_dir()
def test_snapshot_restore_to_new_location(self, snapshot_manager, test_files, temp_backup_dir):
"""Test that restore handles moved files correctly."""
file_path = test_files["file0"]
original_content = file_path.read_text()
# Create snapshot
snapshot = snapshot_manager.create_snapshot([str(file_path)])
# Move file to new location
new_path = temp_backup_dir / "moved.txt"
file_path.rename(new_path)
# Restore from snapshot - should restore to original location
success = snapshot_manager.restore_snapshot(snapshot.snapshot_id)
assert success is True
# Original location should have file restored
assert file_path.exists()
assert file_path.read_text() == original_content
# New location should still have file (wasn't touched)
assert new_path.exists()