Files
tzst/tests/test_conflict_resolution.py
T
xixu-me cb5c198d16 Refactor tests and enhance coverage for conflict resolution and edge cases
- Removed outdated tests for missing lines in core.py.
- Added comprehensive tests for conflict resolution functionality, including various resolution strategies and edge cases.
- Improved error handling tests for archive creation and extraction processes.
- Enhanced unit tests for basic functionality and convenience functions with pytest markers.
- Introduced tests for unique filename generation and conflict handling scenarios.
- Added edge case tests for archive extraction and error conditions to improve overall test coverage.
2025-06-04 18:55:53 +08:00

464 lines
17 KiB
Python

# filepath: e:\GitHub\tzst\tests\test_conflict_resolution_clean.py
"""Comprehensive tests for conflict resolution functionality."""
from unittest.mock import Mock, patch
from tzst.cli import _interactive_conflict_callback
from tzst.core import (
ConflictResolution,
ConflictResolutionState,
TzstArchive,
_get_unique_filename,
_handle_file_conflict,
create_archive,
extract_archive,
)
class TestConflictResolution:
"""Test conflict resolution enum and basic functionality."""
def test_conflict_resolution_enum_values(self):
"""Test that all ConflictResolution enum values exist."""
assert ConflictResolution.REPLACE.value == "replace"
assert ConflictResolution.SKIP.value == "skip"
assert ConflictResolution.REPLACE_ALL.value == "replace_all"
assert ConflictResolution.SKIP_ALL.value == "skip_all"
assert ConflictResolution.AUTO_RENAME.value == "auto_rename"
assert ConflictResolution.AUTO_RENAME_ALL.value == "auto_rename_all"
assert ConflictResolution.EXIT.value == "exit"
assert ConflictResolution.ASK.value == "ask"
class TestUniqueFilename:
"""Test unique filename generation."""
def test_get_unique_filename_basic(self, temp_dir):
"""Test basic unique filename generation."""
# Create a file
original_file = temp_dir / "test.txt"
original_file.write_text("original")
# Get unique name
unique_path = _get_unique_filename(original_file)
expected_path = temp_dir / "test_1.txt"
assert unique_path == expected_path
assert not unique_path.exists()
def test_get_unique_filename_multiple_conflicts(self, temp_dir):
"""Test unique filename generation with multiple conflicts."""
# Create multiple files
original_file = temp_dir / "test.txt"
conflict1 = temp_dir / "test_1.txt"
conflict2 = temp_dir / "test_2.txt"
original_file.write_text("original")
conflict1.write_text("conflict1")
conflict2.write_text("conflict2")
# Get unique name
unique_path = _get_unique_filename(original_file)
expected_path = temp_dir / "test_3.txt"
assert unique_path == expected_path
assert not unique_path.exists()
def test_get_unique_filename_no_extension(self, temp_dir):
"""Test unique filename generation for files without extension."""
# Create a file without extension
original_file = temp_dir / "README"
original_file.write_text("readme content")
# Get unique name
unique_path = _get_unique_filename(original_file)
expected_path = temp_dir / "README_1"
assert unique_path == expected_path
assert not unique_path.exists()
def test_get_unique_filename_empty_stem(self, temp_dir):
"""Test unique filename generation for files with empty stem."""
# Create a file with empty stem (just extension)
original_file = temp_dir / ".gitignore"
original_file.write_text("git ignore")
# Get unique name
unique_path = _get_unique_filename(original_file)
expected_path = temp_dir / ".gitignore_1"
assert unique_path == expected_path
assert not unique_path.exists()
class TestHandleFileConflict:
"""Test file conflict handling function."""
def test_handle_file_conflict_replace(self, temp_dir):
"""Test REPLACE conflict resolution."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing")
resolution, final_path = _handle_file_conflict(
target_path, ConflictResolution.REPLACE, None
)
assert resolution == ConflictResolution.REPLACE
assert final_path == target_path
def test_handle_file_conflict_skip(self, temp_dir):
"""Test SKIP conflict resolution."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing")
resolution, final_path = _handle_file_conflict(
target_path, ConflictResolution.SKIP, None
)
assert resolution == ConflictResolution.SKIP
assert final_path is None
def test_handle_file_conflict_replace_all(self, temp_dir):
"""Test REPLACE_ALL conflict resolution."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing")
resolution, final_path = _handle_file_conflict(
target_path, ConflictResolution.REPLACE_ALL, None
)
assert resolution == ConflictResolution.REPLACE_ALL
assert final_path == target_path
def test_handle_file_conflict_skip_all(self, temp_dir):
"""Test SKIP_ALL conflict resolution."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing")
resolution, final_path = _handle_file_conflict(
target_path, ConflictResolution.SKIP_ALL, None
)
assert resolution == ConflictResolution.SKIP_ALL
assert final_path is None
def test_handle_file_conflict_auto_rename(self, temp_dir):
"""Test AUTO_RENAME conflict resolution."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing")
resolution, final_path = _handle_file_conflict(
target_path, ConflictResolution.AUTO_RENAME, None
)
assert resolution == ConflictResolution.AUTO_RENAME
assert final_path == temp_dir / "existing_1.txt"
assert not final_path.exists()
def test_handle_file_conflict_auto_rename_all(self, temp_dir):
"""Test AUTO_RENAME_ALL conflict resolution."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing")
resolution, final_path = _handle_file_conflict(
target_path, ConflictResolution.AUTO_RENAME_ALL, None
)
assert resolution == ConflictResolution.AUTO_RENAME_ALL
assert final_path == temp_dir / "existing_1.txt"
assert not final_path.exists()
def test_handle_file_conflict_exit(self, temp_dir):
"""Test EXIT conflict resolution."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing")
resolution, final_path = _handle_file_conflict(
target_path, ConflictResolution.EXIT, None
)
assert resolution == ConflictResolution.EXIT
assert final_path is None
def test_handle_file_conflict_ask_with_callback(self, temp_dir):
"""Test ASK conflict resolution with callback."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing")
mock_callback = Mock(return_value=ConflictResolution.REPLACE)
resolution, final_path = _handle_file_conflict(
target_path, ConflictResolution.ASK, mock_callback
)
assert resolution == ConflictResolution.REPLACE
assert final_path == target_path
mock_callback.assert_called_once_with(target_path)
def test_handle_file_conflict_ask_no_callback(self, temp_dir):
"""Test ASK conflict resolution without callback defaults to REPLACE."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing")
resolution, final_path = _handle_file_conflict(
target_path, ConflictResolution.ASK, None
)
assert resolution == ConflictResolution.REPLACE
assert final_path == target_path
def test_handle_file_conflict_string_resolution_valid(self, temp_dir):
"""Test string-based conflict resolution."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing")
resolution, final_path = _handle_file_conflict(target_path, "skip", None)
assert resolution == ConflictResolution.SKIP
assert final_path is None
def test_handle_file_conflict_string_resolution_invalid(self, temp_dir):
"""Test invalid string conflict resolution defaults to REPLACE."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing")
resolution, final_path = _handle_file_conflict(
target_path, "invalid_resolution", None
)
assert resolution == ConflictResolution.REPLACE
assert final_path == target_path
def test_handle_file_conflict_unknown_resolution(self, temp_dir):
"""Test unknown conflict resolution defaults to REPLACE."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing")
# Pass something that's not a valid enum value
resolution, final_path = _handle_file_conflict(
target_path, "completely_unknown", None
)
assert resolution == ConflictResolution.REPLACE
assert final_path == target_path
class TestConflictResolutionState:
"""Test conflict resolution state management."""
def test_initial_state(self):
"""Test initial state with different resolutions."""
state1 = ConflictResolutionState(ConflictResolution.REPLACE)
assert state1.current_resolution == ConflictResolution.REPLACE
assert state1.should_continue() is True
state2 = ConflictResolutionState(ConflictResolution.EXIT)
assert state2.current_resolution == ConflictResolution.EXIT
assert state2.should_continue() is False
def test_update_resolution_replace_all(self):
"""Test updating to REPLACE_ALL."""
state = ConflictResolutionState(ConflictResolution.ASK)
state.update_resolution(ConflictResolution.REPLACE_ALL)
assert state.current_resolution == ConflictResolution.REPLACE_ALL
assert state.should_continue() is True
def test_update_resolution_skip_all(self):
"""Test updating to SKIP_ALL."""
state = ConflictResolutionState(ConflictResolution.ASK)
state.update_resolution(ConflictResolution.SKIP_ALL)
assert state.current_resolution == ConflictResolution.SKIP_ALL
assert state.should_continue() is True
def test_update_resolution_auto_rename_all(self):
"""Test updating to AUTO_RENAME_ALL."""
state = ConflictResolutionState(ConflictResolution.ASK)
state.update_resolution(ConflictResolution.AUTO_RENAME_ALL)
assert state.current_resolution == ConflictResolution.AUTO_RENAME_ALL
assert state.should_continue() is True
def test_update_resolution_exit(self):
"""Test updating to EXIT."""
state = ConflictResolutionState(ConflictResolution.ASK)
state.update_resolution(ConflictResolution.EXIT)
assert state.current_resolution == ConflictResolution.EXIT
assert state.should_continue() is False
def test_update_resolution_normal(self):
"""Test updating to normal resolution doesn't change state."""
state = ConflictResolutionState(ConflictResolution.ASK)
state.update_resolution(ConflictResolution.REPLACE)
assert state.current_resolution == ConflictResolution.ASK
assert state.should_continue() is True
class TestExtractArchiveConflictResolution:
"""Test extract_archive with conflict resolution."""
def test_extract_with_replace_conflict_resolution(self, temp_dir):
"""Test extraction with REPLACE conflict resolution."""
# Create archive
archive_path = temp_dir / "test.tzst"
source_file = temp_dir / "source.txt"
source_file.write_text("archive content")
create_archive(archive_path, [source_file])
# Create extract directory with conflicting file
extract_dir = temp_dir / "extract"
extract_dir.mkdir()
conflict_file = extract_dir / "source.txt"
conflict_file.write_text("existing content")
# Extract with REPLACE resolution
extract_archive(
archive_path, extract_dir, conflict_resolution=ConflictResolution.REPLACE
)
# Verify file was replaced
assert conflict_file.read_text() == "archive content"
def test_extract_with_skip_conflict_resolution(self, temp_dir):
"""Test extraction with SKIP conflict resolution."""
# Create archive
archive_path = temp_dir / "test.tzst"
source_file = temp_dir / "source.txt"
source_file.write_text("archive content")
create_archive(archive_path, [source_file])
# Create extract directory with conflicting file
extract_dir = temp_dir / "extract"
extract_dir.mkdir()
conflict_file = extract_dir / "source.txt"
conflict_file.write_text("existing content")
# Extract with SKIP resolution
extract_archive(
archive_path, extract_dir, conflict_resolution=ConflictResolution.SKIP
)
# Verify file was not replaced
assert conflict_file.read_text() == "existing content"
def test_extract_with_interactive_callback(self, temp_dir):
"""Test extraction with interactive callback."""
# Create archive
archive_path = temp_dir / "test.tzst"
source_file = temp_dir / "source.txt"
source_file.write_text("archive content")
create_archive(archive_path, [source_file])
# Create extract directory with conflicting file
extract_dir = temp_dir / "extract"
extract_dir.mkdir()
conflict_file = extract_dir / "source.txt"
conflict_file.write_text("existing content")
mock_callback = Mock(return_value=ConflictResolution.REPLACE)
# Extract with interactive callback
extract_archive(
archive_path,
extract_dir,
conflict_resolution=ConflictResolution.ASK,
interactive_callback=mock_callback,
)
# Verify callback was called and file was replaced
mock_callback.assert_called_once()
assert conflict_file.read_text() == "archive content"
class TestTzstArchiveConflictResolution:
"""Test extract_archive function with conflict resolution (corrected)."""
def test_extract_archive_with_conflict_resolution(self, temp_dir):
"""Test extract_archive function with conflict resolution parameter."""
# Create archive
source_file = temp_dir / "source.txt"
source_file.write_text("archive content")
archive_path = temp_dir / "test.tzst"
with TzstArchive(archive_path, mode="w") as archive:
archive.add(str(source_file), arcname="source.txt")
# Create conflicting file in extract directory
extract_dir = temp_dir / "extract"
extract_dir.mkdir()
conflict_file = extract_dir / "source.txt"
conflict_file.write_text("existing content")
# Extract with REPLACE resolution using extract_archive function
extract_archive(
archive_path, extract_dir, conflict_resolution=ConflictResolution.REPLACE
)
# Should have replaced the file
assert conflict_file.read_text() == "archive content"
class TestInteractiveConflictCallback:
"""Test interactive conflict callback functionality."""
@patch("builtins.input")
def test_interactive_callback_replace(self, mock_input, temp_dir):
"""Test interactive callback with replace choice."""
mock_input.return_value = "r"
file_path = temp_dir / "test.txt"
result = _interactive_conflict_callback(file_path)
assert result == ConflictResolution.REPLACE
@patch("builtins.input")
def test_interactive_callback_skip(self, mock_input, temp_dir):
"""Test interactive callback with skip choice."""
mock_input.return_value = "n"
file_path = temp_dir / "test.txt"
result = _interactive_conflict_callback(file_path)
assert result == ConflictResolution.SKIP
@patch("builtins.input")
def test_interactive_callback_exit(self, mock_input, temp_dir):
"""Test interactive callback with exit choice."""
mock_input.return_value = "x"
file_path = temp_dir / "test.txt"
result = _interactive_conflict_callback(file_path)
assert result == ConflictResolution.EXIT
@patch("builtins.input")
def test_interactive_callback_invalid_then_valid(self, mock_input, temp_dir):
"""Test interactive callback with invalid then valid choice."""
mock_input.side_effect = ["invalid", "r"]
file_path = temp_dir / "test.txt"
result = _interactive_conflict_callback(file_path)
assert result == ConflictResolution.REPLACE
assert mock_input.call_count == 2
@patch("builtins.input")
def test_interactive_callback_keyboard_interrupt(self, mock_input, temp_dir):
"""Test interactive callback with KeyboardInterrupt."""
mock_input.side_effect = KeyboardInterrupt()
file_path = temp_dir / "test.txt"
result = _interactive_conflict_callback(file_path)
assert result == ConflictResolution.EXIT
@patch("builtins.input")
def test_interactive_callback_eof_error(self, mock_input, temp_dir):
"""Test interactive callback with EOFError."""
mock_input.side_effect = EOFError()
file_path = temp_dir / "test.txt"
result = _interactive_conflict_callback(file_path)
assert result == ConflictResolution.EXIT