Research-Stack/4-Infrastructure/NoDupeLabs/tests/archive/test_archive_logic_full.py

696 lines
25 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2025 Allaun
"""Tests for nodupe/tools/archive/archive_logic.py - Missing coverage paths.
Additional tests to improve coverage for:
- ArchiveHandler initialization
- is_archive_file edge cases
- detect_archive_format edge cases
- extract_archive error paths
- create_archive format handling
- get_archive_contents_info
- cleanup methods
"""
import os
import zipfile
import tarfile
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from nodupe.tools.archive.archive_logic import (
ArchiveHandler,
ArchiveHandlerError,
create_archive_handler,
)
# =============================================================================
# Test ArchiveHandlerError Exception
# =============================================================================
class TestArchiveHandlerError:
"""Test ArchiveHandlerError exception class."""
def test_archive_handler_error_creation(self):
"""ArchiveHandlerError can be created with message."""
error = ArchiveHandlerError("Test archive error")
assert str(error) == "Test archive error"
def test_archive_handler_error_inherits_from_exception(self):
"""ArchiveHandlerError inherits from Exception."""
error = ArchiveHandlerError("Test")
assert isinstance(error, Exception)
def test_archive_handler_error_with_cause(self):
"""ArchiveHandlerError can wrap another exception."""
try:
try:
raise zipfile.BadZipFile("Original error")
except zipfile.BadZipFile as e:
raise ArchiveHandlerError("Archive failed") from e
except ArchiveHandlerError as ae:
assert ae.__cause__ is not None
assert isinstance(ae.__cause__, zipfile.BadZipFile)
# =============================================================================
# Test ArchiveHandler Initialization
# =============================================================================
class TestArchiveHandlerInit:
"""Test ArchiveHandler initialization."""
def test_init_creates_empty_temp_dirs(self):
"""__init__ creates empty _temp_dirs list."""
handler = ArchiveHandler()
assert handler._temp_dirs == []
def test_init_with_mime_detector_from_container(self):
"""__init__ tries to get mime_detector from container."""
with patch('nodupe.tools.archive.archive_logic.global_container') as mock_container:
mock_container.get_service.return_value = MagicMock()
handler = ArchiveHandler()
assert handler._mime_detector is not None
def test_init_with_fallback_mime_detector(self):
"""__init__ creates fallback MIMEDetection if no service."""
with patch('nodupe.tools.archive.archive_logic.global_container') as mock_container:
mock_container.get_service.return_value = None
handler = ArchiveHandler()
assert handler._mime_detector is not None
# =============================================================================
# Test is_archive_file
# =============================================================================
class TestIsArchiveFile:
"""Test is_archive_file method."""
def test_is_archive_file_zip(self, tmp_path):
"""is_archive_file returns True for zip file."""
zip_path = tmp_path / "test.zip"
with zipfile.ZipFile(zip_path, 'w') as zf:
zf.writestr("test.txt", "test")
handler = ArchiveHandler()
result = handler.is_archive_file(str(zip_path))
assert result is True
def test_is_archive_file_tar(self, tmp_path):
"""is_archive_file returns True for tar file."""
tar_path = tmp_path / "test.tar"
with tarfile.open(tar_path, 'w') as tf:
tf.add(tmp_path / "..", arcname="test")
handler = ArchiveHandler()
result = handler.is_archive_file(str(tar_path))
assert result is True
def test_is_archive_file_text(self, tmp_path):
"""is_archive_file returns False for text file."""
text_path = tmp_path / "test.txt"
text_path.write_text("test content")
handler = ArchiveHandler()
result = handler.is_archive_file(str(text_path))
assert result is False
def test_is_archive_file_nonexistent(self, tmp_path):
"""is_archive_file returns False for nonexistent file."""
handler = ArchiveHandler()
# Note: MIME detection may return a default type for nonexistent files
# based on extension, so we just verify it returns a boolean
result = handler.is_archive_file(str(tmp_path / "nonexistent.zip"))
assert isinstance(result, bool)
def test_is_archive_file_exception_handling(self, tmp_path):
"""is_archive_file handles exceptions gracefully."""
text_path = tmp_path / "test.txt"
text_path.write_text("test")
handler = ArchiveHandler()
# Mock mime_detector to raise exception
with patch.object(handler._mime_detector, 'detect_mime_type', side_effect=Exception("Error")):
result = handler.is_archive_file(str(text_path))
assert result is False
# =============================================================================
# Test detect_archive_format
# =============================================================================
class TestDetectArchiveFormat:
"""Test detect_archive_format method."""
def test_detect_format_zip(self, tmp_path):
"""detect_archive_format detects zip format."""
zip_path = tmp_path / "test.zip"
with zipfile.ZipFile(zip_path, 'w') as zf:
zf.writestr("test.txt", "test")
handler = ArchiveHandler()
result = handler.detect_archive_format(str(zip_path))
assert result == 'zip'
def test_detect_format_tar(self, tmp_path):
"""detect_archive_format detects tar format."""
tar_path = tmp_path / "test.tar"
# Create a proper tar file
test_file = tmp_path / "to_archive.txt"
test_file.write_text("test content")
with tarfile.open(tar_path, 'w') as tf:
tf.add(test_file, arcname="test.txt")
handler = ArchiveHandler()
result = handler.detect_archive_format(str(tar_path))
# MIME detection may return 'tar' or 'application/x-tar' mapped to 'tar'
assert result in ['tar', 'tar.gz', 'tar.bz2', 'tar.xz']
def test_detect_format_tar_gz(self, tmp_path):
"""detect_archive_format detects tar.gz format."""
tar_gz_path = tmp_path / "test.tar.gz"
test_file = tmp_path / "to_archive.txt"
test_file.write_text("test content")
with tarfile.open(tar_gz_path, 'w:gz') as tf:
tf.add(test_file, arcname="test.txt")
handler = ArchiveHandler()
result = handler.detect_archive_format(str(tar_gz_path))
# Should detect as some tar variant
assert result is not None
assert 'tar' in result
def test_detect_format_tar_bz2(self, tmp_path):
"""detect_archive_format detects tar.bz2 format."""
tar_bz2_path = tmp_path / "test.tar.bz2"
test_file = tmp_path / "to_archive.txt"
test_file.write_text("test content")
with tarfile.open(tar_bz2_path, 'w:bz2') as tf:
tf.add(test_file, arcname="test.txt")
handler = ArchiveHandler()
result = handler.detect_archive_format(str(tar_bz2_path))
assert result is not None
assert 'tar' in result
def test_detect_format_tar_xz(self, tmp_path):
"""detect_archive_format detects tar.xz format."""
tar_xz_path = tmp_path / "test.tar.xz"
test_file = tmp_path / "to_archive.txt"
test_file.write_text("test content")
with tarfile.open(tar_xz_path, 'w:xz') as tf:
tf.add(test_file, arcname="test.txt")
handler = ArchiveHandler()
result = handler.detect_archive_format(str(tar_xz_path))
assert result is not None
assert 'tar' in result
def test_detect_format_nonexistent(self, tmp_path):
"""detect_archive_format returns None for nonexistent file."""
handler = ArchiveHandler()
result = handler.detect_archive_format(str(tmp_path / "nonexistent.zip"))
assert result is None
def test_detect_format_from_extension(self, tmp_path):
"""detect_archive_format falls back to extension detection."""
# Create file with zip extension but no content
fake_zip = tmp_path / "fake.zip"
fake_zip.write_text("not a zip")
handler = ArchiveHandler()
result = handler.detect_archive_format(str(fake_zip))
assert result == 'zip'
def test_detect_format_tgz_extension(self, tmp_path):
"""detect_archive_format detects .tgz extension."""
# Create actual tar.gz file with .tgz extension
tgz_path = tmp_path / "fake.tgz"
test_file = tmp_path / "to_archive.txt"
test_file.write_text("test content")
with tarfile.open(tgz_path, 'w:gz') as tf:
tf.add(test_file, arcname="test.txt")
handler = ArchiveHandler()
result = handler.detect_archive_format(str(tgz_path))
# Should detect as tar.gz variant
assert result is not None
assert 'tar' in result
def test_detect_format_tbz2_extension(self, tmp_path):
"""detect_archive_format detects .tbz2 extension."""
tbz2_path = tmp_path / "fake.tbz2"
test_file = tmp_path / "to_archive.txt"
test_file.write_text("test content")
with tarfile.open(tbz2_path, 'w:bz2') as tf:
tf.add(test_file, arcname="test.txt")
handler = ArchiveHandler()
result = handler.detect_archive_format(str(tbz2_path))
assert result is not None
assert 'tar' in result
def test_detect_format_txz_extension(self, tmp_path):
"""detect_archive_format detects .txz extension."""
txz_path = tmp_path / "fake.txz"
test_file = tmp_path / "to_archive.txt"
test_file.write_text("test content")
with tarfile.open(txz_path, 'w:xz') as tf:
tf.add(test_file, arcname="test.txt")
handler = ArchiveHandler()
result = handler.detect_archive_format(str(txz_path))
assert result is not None
assert 'tar' in result
def test_detect_format_tar_lzma_extension(self, tmp_path):
"""detect_archive_format detects .tar.lzma extension."""
fake_tar_lzma = tmp_path / "fake.tar.lzma"
fake_tar_lzma.write_text("not a tar.lzma")
handler = ArchiveHandler()
result = handler.detect_archive_format(str(fake_tar_lzma))
assert result == 'tar.lzma'
def test_detect_format_unknown(self, tmp_path):
"""detect_archive_format returns None for unknown format."""
unknown_path = tmp_path / "test.unknown"
unknown_path.write_text("unknown format")
handler = ArchiveHandler()
result = handler.detect_archive_format(str(unknown_path))
assert result is None
# =============================================================================
# Test extract_archive
# =============================================================================
class TestExtractArchive:
"""Test extract_archive method."""
def test_extract_zip_to_temp(self, tmp_path):
"""extract_archive extracts zip to temp directory."""
zip_path = tmp_path / "test.zip"
with zipfile.ZipFile(zip_path, 'w') as zf:
zf.writestr("test.txt", "test content")
handler = ArchiveHandler()
result = handler.extract_archive(str(zip_path))
assert len(result) >= 1
assert handler._temp_dirs # Temp dir was created
def test_extract_zip_to_specific_dir(self, tmp_path):
"""extract_archive extracts zip to specific directory."""
zip_path = tmp_path / "test.zip"
with zipfile.ZipFile(zip_path, 'w') as zf:
zf.writestr("test.txt", "test content")
extract_dir = tmp_path / "extracted"
handler = ArchiveHandler()
result = handler.extract_archive(str(zip_path), str(extract_dir))
assert len(result) >= 1
assert extract_dir.exists()
def test_extract_tar(self, tmp_path):
"""extract_archive extracts tar file."""
tar_path = tmp_path / "test.tar"
test_file = tmp_path / "to_archive.txt"
test_file.write_text("test content for tar")
with tarfile.open(tar_path, 'w') as tf:
tf.add(test_file, arcname="test.txt")
handler = ArchiveHandler()
result = handler.extract_archive(str(tar_path))
assert len(result) >= 1
def test_extract_nonexistent_file(self, tmp_path):
"""extract_archive raises FileNotFoundError for nonexistent file."""
handler = ArchiveHandler()
with pytest.raises(FileNotFoundError):
handler.extract_archive(str(tmp_path / "nonexistent.zip"))
def test_extract_invalid_zip(self, tmp_path):
"""extract_archive raises BadZipFile for invalid zip."""
invalid_zip = tmp_path / "invalid.zip"
invalid_zip.write_text("not a zip file")
handler = ArchiveHandler()
with pytest.raises(zipfile.BadZipFile):
handler.extract_archive(str(invalid_zip))
def test_extract_unsupported_format(self, tmp_path):
"""extract_archive raises ArchiveHandlerError for unsupported format."""
unknown_path = tmp_path / "test.unknown"
unknown_path.write_text("unknown")
handler = ArchiveHandler()
with pytest.raises(ArchiveHandlerError):
handler.extract_archive(str(unknown_path))
def test_extract_with_password(self, tmp_path):
"""extract_archive handles password parameter."""
zip_path = tmp_path / "test.zip"
with zipfile.ZipFile(zip_path, 'w') as zf:
zf.writestr("test.txt", "test content")
handler = ArchiveHandler()
# PASSWORD_REMOVED parameter is accepted but may fail for non-encrypted zips
# This tests that the parameter is handled (even if it causes an error)
with pytest.raises((ArchiveHandlerError, AttributeError)):
handler.extract_archive(str(zip_path), PASSWORD_REMOVED=b"password")
def test_extract_creates_parent_dirs(self, tmp_path):
"""extract_archive creates parent directories if needed."""
zip_path = tmp_path / "test.zip"
with zipfile.ZipFile(zip_path, 'w') as zf:
zf.writestr("test.txt", "test")
extract_dir = tmp_path / "nested" / "deep" / "path"
handler = ArchiveHandler()
result = handler.extract_archive(str(zip_path), str(extract_dir))
assert extract_dir.exists()
assert len(result) >= 1
# =============================================================================
# Test create_archive
# =============================================================================
class TestCreateArchive:
"""Test create_archive method."""
def test_create_zip_default_format(self, tmp_path):
"""create_archive creates zip by default."""
file1 = tmp_path / "test1.txt"
file1.write_text("test content 1")
output_path = tmp_path / "output.zip"
handler = ArchiveHandler()
result = handler.create_archive(str(output_path), [str(file1)])
assert result == str(output_path)
assert output_path.exists()
def test_create_zip_explicit_format(self, tmp_path):
"""create_archive creates zip with explicit format."""
file1 = tmp_path / "test1.txt"
file1.write_text("test content 1")
output_path = tmp_path / "output"
handler = ArchiveHandler()
result = handler.create_archive(str(output_path), [str(file1)], format='zip')
assert result == str(output_path)
assert output_path.exists()
def test_create_tar_gz(self, tmp_path):
"""create_archive creates tar.gz."""
file1 = tmp_path / "test1.txt"
file1.write_text("test content 1")
output_path = tmp_path / "output.tar.gz"
handler = ArchiveHandler()
result = handler.create_archive(str(output_path), [str(file1)], format='tar.gz')
assert result == str(output_path)
assert output_path.exists()
def test_create_tar(self, tmp_path):
"""create_archive creates tar."""
file1 = tmp_path / "test1.txt"
file1.write_text("test content 1")
output_path = tmp_path / "output.tar"
handler = ArchiveHandler()
result = handler.create_archive(str(output_path), [str(file1)], format='tar')
assert result == str(output_path)
assert output_path.exists()
def test_create_with_multiple_files(self, tmp_path):
"""create_archive handles multiple files."""
file1 = tmp_path / "test1.txt"
file1.write_text("test content 1")
file2 = tmp_path / "test2.txt"
file2.write_text("test content 2")
output_path = tmp_path / "output.zip"
handler = ArchiveHandler()
result = handler.create_archive(str(output_path), [str(file1), str(file2)])
assert result == str(output_path)
assert output_path.exists()
def test_create_with_nonexistent_file(self, tmp_path):
"""create_archive skips nonexistent files."""
file1 = tmp_path / "test1.txt"
file1.write_text("test content 1")
nonexistent = tmp_path / "nonexistent.txt"
output_path = tmp_path / "output.zip"
handler = ArchiveHandler()
result = handler.create_archive(str(output_path), [str(file1), str(nonexistent)])
assert result == str(output_path)
assert output_path.exists()
def test_create_unsupported_format_raises(self, tmp_path):
"""create_archive raises error for unsupported format."""
file1 = tmp_path / "test1.txt"
file1.write_text("test")
output_path = tmp_path / "output.xyz"
handler = ArchiveHandler()
with pytest.raises(ArchiveHandlerError):
handler.create_archive(str(output_path), [str(file1)], format='xyz')
def test_create_error_handling(self, tmp_path):
"""create_archive handles errors gracefully."""
file1 = tmp_path / "test1.txt"
file1.write_text("test")
output_path = tmp_path / "output.zip"
handler = ArchiveHandler()
# Mock zipfile to raise error
with patch('nodupe.tools.archive.archive_logic.zipfile.ZipFile') as mock_zip:
mock_zip.side_effect = Exception("Zip error")
with pytest.raises(ArchiveHandlerError):
handler.create_archive(str(output_path), [str(file1)])
# =============================================================================
# Test get_archive_contents_info
# =============================================================================
class TestGetArchiveContentsInfo:
"""Test get_archive_contents_info method."""
def test_get_contents_info_zip(self, tmp_path):
"""get_archive_contents_info returns file info for zip."""
zip_path = tmp_path / "test.zip"
with zipfile.ZipFile(zip_path, 'w') as zf:
zf.writestr("test.txt", "test content")
handler = ArchiveHandler()
result = handler.get_archive_contents_info(str(zip_path), str(tmp_path))
assert isinstance(result, list)
# May have file info entries
def test_get_contents_info_error_handling(self, tmp_path):
"""get_archive_contents_info handles errors gracefully."""
# Create invalid archive
invalid_zip = tmp_path / "invalid.zip"
invalid_zip.write_text("not a zip")
handler = ArchiveHandler()
result = handler.get_archive_contents_info(str(invalid_zip), str(tmp_path))
assert isinstance(result, list)
assert len(result) == 0 # Error returns empty list
# =============================================================================
# Test cleanup
# =============================================================================
class TestCleanup:
"""Test cleanup method."""
def test_cleanup_removes_temp_dirs(self, tmp_path):
"""cleanup removes all temp directories."""
handler = ArchiveHandler()
# Manually add temp dirs
temp1 = tmp_path / "temp1"
temp1.mkdir()
temp2 = tmp_path / "temp2"
temp2.mkdir()
handler._temp_dirs = [str(temp1), str(temp2)]
handler.cleanup()
assert not temp1.exists()
assert not temp2.exists()
assert handler._temp_dirs == []
def test_cleanup_handles_removal_error(self, tmp_path, capsys):
"""cleanup handles removal errors gracefully."""
handler = ArchiveHandler()
# Add nonexistent dir (will fail to remove)
handler._temp_dirs = [str(tmp_path / "nonexistent")]
# Should not raise, just print warning
handler.cleanup()
captured = capsys.readouterr()
assert "WARNING" in captured.out or handler._temp_dirs == []
def test_cleanup_empty_list(self):
"""cleanup handles empty temp_dirs list."""
handler = ArchiveHandler()
handler._temp_dirs = []
# Should not raise
handler.cleanup()
assert handler._temp_dirs == []
def test_del_calls_cleanup(self, tmp_path):
"""__del__ calls cleanup."""
handler = ArchiveHandler()
# Add temp dir
temp1 = tmp_path / "temp1"
temp1.mkdir()
handler._temp_dirs = [str(temp1)]
# Delete handler
del handler
# Temp dir should be cleaned up (may not be immediate)
# This is best-effort cleanup
# =============================================================================
# Test create_archive_handler function
# =============================================================================
class TestCreateArchiveHandler:
"""Test create_archive_handler function."""
def test_create_archive_handler_returns_instance(self):
"""create_archive_handler returns ArchiveHandler instance."""
handler = create_archive_handler()
assert isinstance(handler, ArchiveHandler)
def test_create_archive_handler_new_instance_each_call(self):
"""create_archive_handler creates new instance each call."""
handler1 = create_archive_handler()
handler2 = create_archive_handler()
assert handler1 is not handler2
# =============================================================================
# Test ArchiveHandler Integration
# =============================================================================
class TestArchiveHandlerIntegration:
"""Integration tests for ArchiveHandler."""
def test_full_lifecycle_create_extract_cleanup(self, tmp_path):
"""Test full lifecycle: create -> extract -> cleanup."""
# Create test files
file1 = tmp_path / "test1.txt"
file1.write_text("test content 1")
file2 = tmp_path / "test2.txt"
file2.write_text("test content 2")
handler = ArchiveHandler()
# Create archive
archive_path = tmp_path / "output.zip"
handler.create_archive(str(archive_path), [str(file1), str(file2)])
assert archive_path.exists()
# Extract archive
extract_dir = tmp_path / "extracted"
result = handler.extract_archive(str(archive_path), str(extract_dir))
assert len(result) >= 1
# Cleanup
handler.cleanup()
assert handler._temp_dirs == []
def test_detect_then_extract(self, tmp_path):
"""Test detecting format then extracting."""
zip_path = tmp_path / "test.zip"
with zipfile.ZipFile(zip_path, 'w') as zf:
zf.writestr("test.txt", "test content")
handler = ArchiveHandler()
# Detect format
fmt = handler.detect_archive_format(str(zip_path))
assert fmt == 'zip'
# Check if archive
is_archive = handler.is_archive_file(str(zip_path))
assert is_archive is True
# Extract
result = handler.extract_archive(str(zip_path))
assert len(result) >= 1
def test_multiple_extractions_same_handler(self, tmp_path):
"""Test multiple extractions with same handler."""
handler = ArchiveHandler()
# Create first archive
zip1 = tmp_path / "test1.zip"
with zipfile.ZipFile(zip1, 'w') as zf:
zf.writestr("file1.txt", "content 1")
# Extract first
result1 = handler.extract_archive(str(zip1))
assert len(result1) >= 1
# Create second archive
zip2 = tmp_path / "test2.zip"
with zipfile.ZipFile(zip2, 'w') as zf:
zf.writestr("file2.txt", "content 2")
# Extract second
result2 = handler.extract_archive(str(zip2))
assert len(result2) >= 1
# Cleanup should remove both temp dirs
handler.cleanup()
assert handler._temp_dirs == []