mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
896 lines
35 KiB
Python
896 lines
35 KiB
Python
"""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()
|