Files
android-assistant/tests/utils/test_file_deduplication.py
T

325 lines
13 KiB
Python

"""Tests for file deduplication utility."""
import pytest
import hashlib
import os
import tempfile
from unittest.mock import MagicMock, patch, mock_open
from src.utils.file_deduplication import FileDeduplicator
class TestFileDeduplicator:
"""Test FileDeduplicator class functionality."""
@pytest.fixture
def deduplicator(self):
"""Create a FileDeduplicator instance for testing."""
return FileDeduplicator()
@pytest.fixture
def deduplicator_with_callbacks(self):
"""Create a FileDeduplicator instance with callbacks for testing."""
status_callback = MagicMock()
progress_callback = MagicMock()
return (
FileDeduplicator(status_callback, progress_callback),
status_callback,
progress_callback,
)
def test_init_without_callbacks(self, deduplicator):
"""Test FileDeduplicator initialization without callbacks."""
assert deduplicator.status_callback is None
assert deduplicator.progress_callback is None
def test_init_with_callbacks(self):
"""Test FileDeduplicator initialization with callbacks."""
status_callback = MagicMock()
progress_callback = MagicMock()
deduplicator = FileDeduplicator(status_callback, progress_callback)
assert deduplicator.status_callback == status_callback
assert deduplicator.progress_callback == progress_callback
def test_update_status_with_callback(self, deduplicator_with_callbacks):
"""Test status update with callback."""
deduplicator, status_callback, _ = deduplicator_with_callbacks
deduplicator._update_status("Test message")
status_callback.assert_called_once_with("Test message")
def test_update_status_without_callback(self, deduplicator):
"""Test status update without callback."""
# Should not raise an exception
deduplicator._update_status("Test message")
def test_update_progress_with_callback(self, deduplicator_with_callbacks):
"""Test progress update with callback."""
deduplicator, _, progress_callback = deduplicator_with_callbacks
deduplicator._update_progress(75)
progress_callback.assert_called_once_with(75)
def test_update_progress_without_callback(self, deduplicator):
"""Test progress update without callback."""
# Should not raise an exception
deduplicator._update_progress(75)
@patch("builtins.open", new_callable=mock_open, read_data=b"test file content")
@patch("os.path.isfile", return_value=True)
def test_compute_local_file_hash_small_file(
self, mock_isfile, mock_file, deduplicator
):
"""Test file hash computation for small file."""
expected_hash = hashlib.sha256(b"test file content").hexdigest()
result = deduplicator.compute_local_file_hash("/fake/path/file.txt")
assert result == expected_hash
mock_file.assert_called_once_with("/fake/path/file.txt", "rb")
mock_isfile.assert_called_once_with("/fake/path/file.txt")
@patch("builtins.open", new_callable=mock_open)
@patch("os.path.isfile", return_value=True)
def test_compute_local_file_hash_large_file(
self, mock_isfile, mock_file, deduplicator_with_callbacks
):
"""Test file hash computation for large file with progress updates."""
deduplicator, status_callback, progress_callback = deduplicator_with_callbacks
# Mock reading chunks
mock_file.return_value.__enter__.return_value.read.side_effect = [
b"chunk1" * 1000, # First chunk
b"chunk2" * 1000, # Second chunk
b"", # EOF
]
result = deduplicator.compute_local_file_hash("/fake/path/largefile.txt")
# Should return a valid hash
assert isinstance(result, str)
assert len(result) == 64 # SHA256 hex digest length
@patch("os.path.isfile", return_value=False)
def test_compute_local_file_hash_nonexistent_file(self, mock_isfile, deduplicator):
"""Test file hash computation for nonexistent file."""
result = deduplicator.compute_local_file_hash("/fake/nonexistent/file.txt")
assert result is None
mock_isfile.assert_called_once_with("/fake/nonexistent/file.txt")
@patch("builtins.open", side_effect=PermissionError("Permission denied"))
@patch("os.path.isfile", return_value=True)
def test_compute_local_file_hash_permission_error(
self, mock_isfile, mock_file, deduplicator
):
"""Test file hash computation with permission error."""
result = deduplicator.compute_local_file_hash("/fake/protected/file.txt")
assert result is None
def test_check_files_identical_same_hash(self, deduplicator):
"""Test file comparison with identical hashes."""
with patch.object(deduplicator, "compute_local_file_hash") as mock_hash:
mock_hash.return_value = "abc123"
with patch.object(
deduplicator, "compute_remote_file_hash"
) as mock_remote_hash:
mock_remote_hash.return_value = "abc123"
result = deduplicator.check_files_identical("/file1.txt", "/file2.txt")
assert result is True
def test_check_files_identical_different_hash(self, deduplicator):
"""Test file comparison with different hashes."""
with patch.object(deduplicator, "compute_local_file_hash") as mock_hash:
mock_hash.return_value = "abc123"
with patch.object(
deduplicator, "compute_remote_file_hash"
) as mock_remote_hash:
mock_remote_hash.return_value = "def456"
result = deduplicator.check_files_identical("/file1.txt", "/file2.txt")
assert result is False
def test_check_files_identical_one_hash_none(self, deduplicator):
"""Test file comparison with one hash being None."""
with patch.object(deduplicator, "compute_local_file_hash") as mock_hash:
mock_hash.return_value = "abc123"
with patch.object(
deduplicator, "compute_remote_file_hash"
) as mock_remote_hash:
mock_remote_hash.return_value = None
result = deduplicator.check_files_identical("/file1.txt", "/file2.txt")
assert result is False
def test_check_files_identical_both_hashes_none(self, deduplicator):
"""Test file comparison with both hashes being None."""
with patch.object(deduplicator, "compute_local_file_hash") as mock_hash:
mock_hash.return_value = None
with patch.object(
deduplicator, "compute_remote_file_hash"
) as mock_remote_hash:
mock_remote_hash.return_value = None
result = deduplicator.check_files_identical("/file1.txt", "/file2.txt")
assert result is False
@patch("os.path.exists")
@patch("os.listdir")
@patch("os.path.isfile")
def test_build_local_file_hash_map(
self, mock_isfile, mock_listdir, mock_exists, deduplicator
):
"""Test getting local file hashes from directory."""
mock_exists.return_value = True
mock_listdir.return_value = ["file1.txt", "file2.jpg", "subdir"]
mock_isfile.side_effect = lambda x: not x.endswith("subdir")
file_paths = ["/fake/directory/file1.txt", "/fake/directory/file2.jpg"]
with patch.object(deduplicator, "compute_local_file_hash") as mock_hash:
mock_hash.side_effect = ["hash1", "hash2"]
result = deduplicator.build_local_file_hash_map(file_paths)
expected = {
"/fake/directory/file1.txt": "hash1",
"/fake/directory/file2.jpg": "hash2",
}
assert result == expected
@patch("os.path.exists", return_value=False)
def test_build_local_file_hash_map_nonexistent_files(
self, mock_exists, deduplicator
):
"""Test getting local file hashes from nonexistent files."""
result = deduplicator.build_local_file_hash_map(["/fake/nonexistent"])
assert result == {}
def test_find_duplicate_files_with_duplicates(self, deduplicator):
"""Test finding duplicates when duplicates exist."""
local_files = ["/local/file1.txt", "/local/file2.jpg", "/local/file3.txt"]
remote_files = [
"/remote/remote1.txt",
"/remote/remote2.jpg",
"/remote/remote3.txt",
]
with patch.object(deduplicator, "build_local_file_hash_map") as mock_local:
with patch.object(
deduplicator, "build_remote_file_hash_map"
) as mock_remote:
mock_local.return_value = {
"/local/file1.txt": "hash1",
"/local/file2.jpg": "hash2",
"/local/file3.txt": "hash3",
}
mock_remote.return_value = {
"/remote/remote1.txt": "hash1", # Duplicate of file1.txt
"/remote/remote2.jpg": "hash4", # Unique
"/remote/remote3.txt": "hash3", # Duplicate of file3.txt
}
files_to_transfer, duplicates = deduplicator.find_duplicate_files(
local_files, remote_files, is_remote_target=True
)
# Should find 2 duplicates: file1.txt and file3.txt
assert len(duplicates) == 2
assert "/local/file1.txt" in duplicates
assert "/local/file3.txt" in duplicates
# file2.jpg should be transferred since it's not a duplicate
assert len(files_to_transfer) == 1
assert "/local/file2.jpg" in files_to_transfer
def test_find_duplicate_files_no_duplicates(self, deduplicator):
"""Test finding duplicates when no duplicates exist."""
local_files = ["/local/file1.txt", "/local/file2.jpg"]
remote_files = ["/remote/remote1.txt", "/remote/remote2.jpg"]
with patch.object(deduplicator, "build_local_file_hash_map") as mock_local:
with patch.object(
deduplicator, "build_remote_file_hash_map"
) as mock_remote:
mock_local.return_value = {
"/local/file1.txt": "hash1",
"/local/file2.jpg": "hash2",
}
mock_remote.return_value = {
"/remote/remote1.txt": "hash3",
"/remote/remote2.jpg": "hash4",
}
files_to_transfer, duplicates = deduplicator.find_duplicate_files(
local_files, remote_files, is_remote_target=True
)
assert duplicates == []
assert files_to_transfer == local_files
def test_find_duplicate_files_empty_collections(self, deduplicator):
"""Test finding duplicates with empty collections."""
files_to_transfer, duplicates = deduplicator.find_duplicate_files([], [])
assert duplicates == []
assert files_to_transfer == []
class TestFileDeduplicatorIntegration:
"""Integration tests for FileDeduplicator with real files."""
def test_real_file_hash_computation(self):
"""Test hash computation with real temporary files."""
deduplicator = FileDeduplicator()
# Create temporary files with known content
with tempfile.NamedTemporaryFile(mode="w", delete=False) as tf1:
tf1.write("test content 1")
temp_file1 = tf1.name
with tempfile.NamedTemporaryFile(mode="w", delete=False) as tf2:
tf2.write("test content 1") # Same content
temp_file2 = tf2.name
with tempfile.NamedTemporaryFile(mode="w", delete=False) as tf3:
tf3.write("test content 2") # Different content
temp_file3 = tf3.name
try:
hash1 = deduplicator.compute_local_file_hash(temp_file1)
hash2 = deduplicator.compute_local_file_hash(temp_file2)
hash3 = deduplicator.compute_local_file_hash(temp_file3)
# Same content should have same hash
assert hash1 == hash2
# Different content should have different hash
assert hash1 != hash3
# Test file comparison - since check_files_identical expects local/remote,
# we'll just compare hashes directly for local files
assert hash1 == hash2 # Same files should have same hash
assert hash1 != hash3 # Different files should have different hash
finally:
# Clean up
os.unlink(temp_file1)
os.unlink(temp_file2)
os.unlink(temp_file3)
def test_real_directory_hash_computation(self):
"""Test directory hash computation with real temporary directory."""
deduplicator = FileDeduplicator()
# Create temporary directory with files
with tempfile.TemporaryDirectory() as temp_dir:
file1_path = os.path.join(temp_dir, "file1.txt")
file2_path = os.path.join(temp_dir, "file2.txt")
with open(file1_path, "w") as f1:
f1.write("content 1")
with open(file2_path, "w") as f2:
f2.write("content 2")
hashes = deduplicator.build_local_file_hash_map([file1_path, file2_path])
assert file1_path in hashes
assert file2_path in hashes
assert len(hashes) == 2
assert hashes[file1_path] != hashes[file2_path]
if __name__ == "__main__":
pytest.main([__file__])