mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
1151 lines
40 KiB
Python
1151 lines
40 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# Copyright (c) 2025 Allaun
|
|
|
|
"""Tests for nodupe/tools/commands/apply.py - ApplyTool."""
|
|
|
|
import shutil
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from nodupe.tools.commands.apply import ApplyTool, register_tool
|
|
|
|
|
|
class TestApplyToolProperties:
|
|
"""Test ApplyTool properties."""
|
|
|
|
def test_name_property(self):
|
|
"""ApplyTool.name returns correct value."""
|
|
tool = ApplyTool()
|
|
assert tool.name == "apply"
|
|
|
|
def test_version_property(self):
|
|
"""ApplyTool.version returns correct value."""
|
|
tool = ApplyTool()
|
|
assert tool.version == "1.0.0"
|
|
|
|
def test_dependencies_property(self):
|
|
"""ApplyTool.dependencies returns empty list."""
|
|
tool = ApplyTool()
|
|
assert tool.dependencies == []
|
|
|
|
def test_description_attribute(self):
|
|
"""ApplyTool has correct description."""
|
|
tool = ApplyTool()
|
|
assert tool.description == "Apply actions to duplicate files"
|
|
|
|
|
|
class TestApplyToolInitialize:
|
|
"""Test ApplyTool.initialize() method."""
|
|
|
|
def test_initialize_no_error(self):
|
|
"""initialize() completes without error."""
|
|
tool = ApplyTool()
|
|
container = MagicMock()
|
|
# Should not raise
|
|
tool.initialize(container)
|
|
|
|
def test_initialize_does_not_require_container(self):
|
|
"""initialize() handles None container gracefully."""
|
|
tool = ApplyTool()
|
|
# Should not raise
|
|
tool.initialize(None)
|
|
|
|
|
|
class TestApplyToolShutdown:
|
|
"""Test ApplyTool.shutdown() method."""
|
|
|
|
def test_shutdown_no_error(self):
|
|
"""shutdown() completes without error."""
|
|
tool = ApplyTool()
|
|
# Should not raise
|
|
tool.shutdown()
|
|
|
|
|
|
class TestApplyToolGetCapabilities:
|
|
"""Test ApplyTool.get_capabilities() method."""
|
|
|
|
def test_get_capabilities_returns_dict(self):
|
|
"""get_capabilities() returns a dictionary."""
|
|
tool = ApplyTool()
|
|
capabilities = tool.get_capabilities()
|
|
|
|
assert isinstance(capabilities, dict)
|
|
assert 'commands' in capabilities
|
|
|
|
def test_get_capabilities_contains_apply_command(self):
|
|
"""get_capabilities() contains apply command."""
|
|
tool = ApplyTool()
|
|
capabilities = tool.get_capabilities()
|
|
|
|
assert 'apply' in capabilities['commands']
|
|
|
|
|
|
class TestApplyToolEventHandlers:
|
|
"""Test ApplyTool event handler methods."""
|
|
|
|
def test_on_apply_start(self, capsys):
|
|
"""_on_apply_start() prints start message."""
|
|
tool = ApplyTool()
|
|
tool._on_apply_start(action='delete')
|
|
|
|
captured = capsys.readouterr()
|
|
assert "[TOOL] Apply started: delete" in captured.out
|
|
|
|
def test_on_apply_start_default_action(self, capsys):
|
|
"""_on_apply_start() uses default action when not provided."""
|
|
tool = ApplyTool()
|
|
tool._on_apply_start()
|
|
|
|
captured = capsys.readouterr()
|
|
assert "[TOOL] Apply started: unknown" in captured.out
|
|
|
|
def test_on_apply_complete(self, capsys):
|
|
"""_on_apply_complete() prints completion message."""
|
|
tool = ApplyTool()
|
|
tool._on_apply_complete(files_processed=42)
|
|
|
|
captured = capsys.readouterr()
|
|
assert "[TOOL] Apply completed: 42 files processed" in captured.out
|
|
|
|
def test_on_apply_complete_default_count(self, capsys):
|
|
"""_on_apply_complete() uses default count when not provided."""
|
|
tool = ApplyTool()
|
|
tool._on_apply_complete()
|
|
|
|
captured = capsys.readouterr()
|
|
assert "[TOOL] Apply completed: 0 files processed" in captured.out
|
|
|
|
|
|
class TestApplyToolRegisterCommands:
|
|
"""Test ApplyTool.register_commands() method."""
|
|
|
|
def test_register_commands_creates_parser(self):
|
|
"""register_commands() creates apply subparser."""
|
|
tool = ApplyTool()
|
|
subparsers = MagicMock()
|
|
apply_parser = MagicMock()
|
|
subparsers.add_parser.return_value = apply_parser
|
|
|
|
tool.register_commands(subparsers)
|
|
|
|
subparsers.add_parser.assert_called_once_with('apply', help='Apply actions to duplicates')
|
|
|
|
def test_register_commands_adds_action_argument(self):
|
|
"""register_commands() adds action argument."""
|
|
tool = ApplyTool()
|
|
subparsers = MagicMock()
|
|
apply_parser = MagicMock()
|
|
subparsers.add_parser.return_value = apply_parser
|
|
|
|
tool.register_commands(subparsers)
|
|
|
|
# Verify add_argument was called for action
|
|
apply_parser.add_argument.assert_any_call(
|
|
'action',
|
|
choices=['delete', 'move', 'copy', 'list'],
|
|
help='Action to perform'
|
|
)
|
|
|
|
def test_register_commands_adds_destination_argument(self):
|
|
"""register_commands() adds destination argument."""
|
|
tool = ApplyTool()
|
|
subparsers = MagicMock()
|
|
apply_parser = MagicMock()
|
|
subparsers.add_parser.return_value = apply_parser
|
|
|
|
tool.register_commands(subparsers)
|
|
|
|
apply_parser.add_argument.assert_any_call(
|
|
'--destination', '-d',
|
|
help='Destination directory (required for move/copy)'
|
|
)
|
|
|
|
def test_register_commands_adds_dry_run_argument(self):
|
|
"""register_commands() adds dry-run argument."""
|
|
tool = ApplyTool()
|
|
subparsers = MagicMock()
|
|
apply_parser = MagicMock()
|
|
subparsers.add_parser.return_value = apply_parser
|
|
|
|
tool.register_commands(subparsers)
|
|
|
|
apply_parser.add_argument.assert_any_call(
|
|
'--dry-run', action='store_true', help='Dry run (no changes)'
|
|
)
|
|
|
|
def test_register_commands_adds_verbose_argument(self):
|
|
"""register_commands() adds verbose argument."""
|
|
tool = ApplyTool()
|
|
subparsers = MagicMock()
|
|
apply_parser = MagicMock()
|
|
subparsers.add_parser.return_value = apply_parser
|
|
|
|
tool.register_commands(subparsers)
|
|
|
|
apply_parser.add_argument.assert_any_call(
|
|
'--verbose', '-v', action='store_true', help='Verbose output'
|
|
)
|
|
|
|
def test_register_commands_sets_func(self):
|
|
"""register_commands() sets func to execute_apply."""
|
|
tool = ApplyTool()
|
|
subparsers = MagicMock()
|
|
apply_parser = MagicMock()
|
|
subparsers.add_parser.return_value = apply_parser
|
|
|
|
tool.register_commands(subparsers)
|
|
|
|
apply_parser.set_defaults.assert_called_once_with(func=tool.execute_apply)
|
|
|
|
|
|
class TestApplyToolExecuteApplyValidation:
|
|
"""Test ApplyTool.execute_apply() validation logic."""
|
|
|
|
def test_execute_apply_missing_input_file(self, capsys):
|
|
"""execute_apply() returns error when input file is None."""
|
|
tool = ApplyTool()
|
|
args = MagicMock()
|
|
args.input = None
|
|
args.action = 'list'
|
|
args.container = None
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 1
|
|
captured = capsys.readouterr()
|
|
assert "[ERROR] Input file is required" in captured.out
|
|
|
|
def test_execute_apply_input_file_not_exists(self, capsys):
|
|
"""execute_apply() returns error when input file doesn't exist."""
|
|
tool = ApplyTool()
|
|
args = MagicMock()
|
|
args.input = '/nonexistent/path/file.txt'
|
|
args.action = 'list'
|
|
args.container = None
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 1
|
|
captured = capsys.readouterr()
|
|
assert "[ERROR] Input file does not exist:" in captured.out
|
|
|
|
def test_execute_apply_invalid_action(self, capsys):
|
|
"""execute_apply() returns error for invalid action."""
|
|
tool = ApplyTool()
|
|
args = MagicMock()
|
|
args.action = 'invalid_action'
|
|
args.container = None
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 1
|
|
captured = capsys.readouterr()
|
|
assert "[ERROR] Invalid action: invalid_action" in captured.out
|
|
|
|
def test_execute_apply_missing_destination_for_move(self, capsys):
|
|
"""execute_apply() returns error when move action missing destination."""
|
|
tool = ApplyTool()
|
|
args = MagicMock()
|
|
args.action = 'move'
|
|
args.destination = None
|
|
args.container = None
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 1
|
|
captured = capsys.readouterr()
|
|
assert "[ERROR] --destination is required for move/copy actions" in captured.out
|
|
|
|
def test_execute_apply_missing_destination_for_copy(self, capsys):
|
|
"""execute_apply() returns error when copy action missing destination."""
|
|
tool = ApplyTool()
|
|
args = MagicMock()
|
|
args.action = 'copy'
|
|
args.destination = None
|
|
args.container = None
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 1
|
|
captured = capsys.readouterr()
|
|
assert "[ERROR] --destination is required for move/copy actions" in captured.out
|
|
|
|
def test_execute_apply_destination_not_exists(self, capsys):
|
|
"""execute_apply() returns error when destination directory doesn't exist."""
|
|
tool = ApplyTool()
|
|
args = MagicMock()
|
|
args.action = 'move'
|
|
args.destination = '/nonexistent/destination'
|
|
args.container = None
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 1
|
|
captured = capsys.readouterr()
|
|
assert "[ERROR] Target directory does not exist:" in captured.out
|
|
|
|
def test_execute_apply_missing_container(self, capsys):
|
|
"""execute_apply() returns error when container not available."""
|
|
tool = ApplyTool()
|
|
args = MagicMock()
|
|
args.action = 'list'
|
|
args.container = None
|
|
# Remove input attribute to skip input validation
|
|
delattr(args, 'input')
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 1
|
|
captured = capsys.readouterr()
|
|
assert "[ERROR] Dependency container not available" in captured.out
|
|
|
|
def test_execute_apply_missing_database_service(self, capsys):
|
|
"""execute_apply() handles missing database service."""
|
|
tool = ApplyTool()
|
|
args = MagicMock()
|
|
args.action = 'list'
|
|
args.container = MagicMock()
|
|
args.container.get_service.return_value = None
|
|
delattr(args, 'input')
|
|
|
|
# Mock DatabaseConnection.get_instance
|
|
with patch('nodupe.tools.commands.apply.DatabaseConnection') as mock_db_conn:
|
|
mock_instance = MagicMock()
|
|
mock_db_conn.get_instance.return_value = mock_instance
|
|
|
|
tool.execute_apply(args)
|
|
|
|
# Should fallback to default connection
|
|
mock_db_conn.get_instance.assert_called()
|
|
|
|
|
|
class TestApplyToolExecuteApplyList:
|
|
"""Test ApplyTool.execute_apply() list action."""
|
|
|
|
def test_execute_apply_list_no_duplicates(self, capsys):
|
|
"""execute_apply() list action with no duplicates."""
|
|
tool = ApplyTool()
|
|
args = MagicMock()
|
|
args.action = 'list'
|
|
args.container = MagicMock()
|
|
args.container.get_service.return_value = MagicMock()
|
|
delattr(args, 'input')
|
|
|
|
# Mock FileRepository
|
|
with patch('nodupe.tools.commands.apply.FileRepository') as mock_repo_class:
|
|
mock_repo = MagicMock()
|
|
mock_repo.get_duplicate_files.return_value = []
|
|
mock_repo_class.return_value = mock_repo
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 0
|
|
captured = capsys.readouterr()
|
|
assert "No items marked as duplicates" in captured.out
|
|
|
|
def test_execute_apply_list_with_duplicates(self, capsys):
|
|
"""execute_apply() list action with duplicates."""
|
|
tool = ApplyTool()
|
|
args = MagicMock()
|
|
args.action = 'list'
|
|
args.container = MagicMock()
|
|
args.container.get_service.return_value = MagicMock()
|
|
delattr(args, 'input')
|
|
|
|
# Mock duplicates
|
|
duplicates = [
|
|
{'id': 1, 'path': '/path/to/dup1.txt', 'duplicate_of': 100},
|
|
{'id': 2, 'path': '/path/to/dup2.txt', 'duplicate_of': 100},
|
|
]
|
|
original = {'id': 100, 'path': '/path/to/original.txt'}
|
|
|
|
with patch('nodupe.tools.commands.apply.FileRepository') as mock_repo_class:
|
|
mock_repo = MagicMock()
|
|
mock_repo.get_duplicate_files.return_value = duplicates
|
|
mock_repo.get_file.return_value = original
|
|
mock_repo_class.return_value = mock_repo
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 0
|
|
captured = capsys.readouterr()
|
|
assert "Identified Duplicates:" in captured.out
|
|
assert "/path/to/dup1.txt" in captured.out
|
|
assert "/path/to/dup2.txt" in captured.out
|
|
|
|
|
|
class TestApplyToolExecuteApplyDelete:
|
|
"""Test ApplyTool.execute_apply() delete action."""
|
|
|
|
@pytest.fixture
|
|
def temp_files(self):
|
|
"""Create temporary files for testing."""
|
|
temp_dir = Path(tempfile.mkdtemp())
|
|
files = []
|
|
for i in range(3):
|
|
f = temp_dir / f"file{i}.txt"
|
|
f.write_text(f"content {i}")
|
|
files.append(f)
|
|
yield temp_dir, files
|
|
shutil.rmtree(temp_dir)
|
|
|
|
def test_execute_apply_delete_dry_run(self, capsys, temp_files):
|
|
"""execute_apply() delete action in dry-run mode."""
|
|
tool = ApplyTool()
|
|
temp_dir, files = temp_files
|
|
|
|
args = MagicMock()
|
|
args.action = 'delete'
|
|
args.dry_run = True
|
|
args.verbose = False
|
|
args.container = MagicMock()
|
|
args.container.get_service.return_value = MagicMock()
|
|
delattr(args, 'input')
|
|
|
|
duplicates = [{'id': i, 'path': str(f)} for i, f in enumerate(files)]
|
|
|
|
with patch('nodupe.tools.commands.apply.FileRepository') as mock_repo_class:
|
|
mock_repo = MagicMock()
|
|
mock_repo.get_duplicate_files.return_value = duplicates
|
|
mock_repo_class.return_value = mock_repo
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 0
|
|
captured = capsys.readouterr()
|
|
assert "[DRY-RUN] Would delete:" in captured.out
|
|
assert "Dry run complete" in captured.out
|
|
|
|
def test_execute_apply_delete_actual(self, capsys, temp_files):
|
|
"""execute_apply() delete action actually deletes files."""
|
|
tool = ApplyTool()
|
|
temp_dir, files = temp_files
|
|
|
|
args = MagicMock()
|
|
args.action = 'delete'
|
|
args.dry_run = False
|
|
args.verbose = False
|
|
args.container = MagicMock()
|
|
args.container.get_service.return_value = MagicMock()
|
|
delattr(args, 'input')
|
|
|
|
duplicates = [{'id': i, 'path': str(f)} for i, f in enumerate(files)]
|
|
|
|
with patch('nodupe.tools.commands.apply.FileRepository') as mock_repo_class:
|
|
with patch('nodupe.tools.commands.apply.Filesystem') as mock_fs:
|
|
mock_repo = MagicMock()
|
|
mock_repo.get_duplicate_files.return_value = duplicates
|
|
mock_repo_class.return_value = mock_repo
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 0
|
|
captured = capsys.readouterr()
|
|
assert "[DELETED]" in captured.out
|
|
# Verify Filesystem.remove_file was called
|
|
assert mock_fs.remove_file.call_count == len(files)
|
|
|
|
|
|
class TestApplyToolExecuteApplyMove:
|
|
"""Test ApplyTool.execute_apply() move action."""
|
|
|
|
@pytest.fixture
|
|
def temp_dirs(self):
|
|
"""Create temporary directories for testing."""
|
|
src_dir = Path(tempfile.mkdtemp())
|
|
dst_dir = Path(tempfile.mkdtemp())
|
|
yield src_dir, dst_dir
|
|
shutil.rmtree(src_dir)
|
|
shutil.rmtree(dst_dir)
|
|
|
|
def test_execute_apply_move_dry_run(self, capsys, temp_dirs):
|
|
"""execute_apply() move action in dry-run mode."""
|
|
tool = ApplyTool()
|
|
src_dir, dst_dir = temp_dirs
|
|
|
|
# Create source file
|
|
src_file = src_dir / "test.txt"
|
|
src_file.write_text("test content")
|
|
|
|
args = MagicMock()
|
|
args.action = 'move'
|
|
args.dry_run = True
|
|
args.destination = str(dst_dir)
|
|
args.verbose = False
|
|
args.container = MagicMock()
|
|
args.container.get_service.return_value = MagicMock()
|
|
delattr(args, 'input')
|
|
|
|
duplicates = [{'id': 1, 'path': str(src_file)}]
|
|
|
|
with patch('nodupe.tools.commands.apply.FileRepository') as mock_repo_class:
|
|
mock_repo = MagicMock()
|
|
mock_repo.get_duplicate_files.return_value = duplicates
|
|
mock_repo_class.return_value = mock_repo
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 0
|
|
captured = capsys.readouterr()
|
|
assert "[DRY-RUN] Would move:" in captured.out
|
|
|
|
def test_execute_apply_move_actual(self, capsys, temp_dirs):
|
|
"""execute_apply() move action actually moves files."""
|
|
tool = ApplyTool()
|
|
src_dir, dst_dir = temp_dirs
|
|
|
|
# Create source file
|
|
src_file = src_dir / "test.txt"
|
|
src_file.write_text("test content")
|
|
|
|
args = MagicMock()
|
|
args.action = 'move'
|
|
args.dry_run = False
|
|
args.destination = str(dst_dir)
|
|
args.verbose = False
|
|
args.container = MagicMock()
|
|
args.container.get_service.return_value = MagicMock()
|
|
delattr(args, 'input')
|
|
|
|
duplicates = [{'id': 1, 'path': str(src_file)}]
|
|
|
|
with patch('nodupe.tools.commands.apply.FileRepository') as mock_repo_class:
|
|
with patch('nodupe.tools.commands.apply.Filesystem') as mock_fs:
|
|
mock_repo = MagicMock()
|
|
mock_repo.get_duplicate_files.return_value = duplicates
|
|
mock_repo_class.return_value = mock_repo
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 0
|
|
captured = capsys.readouterr()
|
|
assert "[MOVED]" in captured.out
|
|
mock_fs.move_file.assert_called()
|
|
mock_fs.ensure_directory.assert_called()
|
|
|
|
|
|
class TestApplyToolExecuteApplyCopy:
|
|
"""Test ApplyTool.execute_apply() copy action."""
|
|
|
|
@pytest.fixture
|
|
def temp_dirs(self):
|
|
"""Create temporary directories for testing."""
|
|
src_dir = Path(tempfile.mkdtemp())
|
|
dst_dir = Path(tempfile.mkdtemp())
|
|
yield src_dir, dst_dir
|
|
shutil.rmtree(src_dir)
|
|
shutil.rmtree(dst_dir)
|
|
|
|
def test_execute_apply_copy_dry_run(self, capsys, temp_dirs):
|
|
"""execute_apply() copy action in dry-run mode."""
|
|
tool = ApplyTool()
|
|
src_dir, dst_dir = temp_dirs
|
|
|
|
# Create source file
|
|
src_file = src_dir / "test.txt"
|
|
src_file.write_text("test content")
|
|
|
|
args = MagicMock()
|
|
args.action = 'copy'
|
|
args.dry_run = True
|
|
args.destination = str(dst_dir)
|
|
args.verbose = False
|
|
args.container = MagicMock()
|
|
args.container.get_service.return_value = MagicMock()
|
|
delattr(args, 'input')
|
|
|
|
duplicates = [{'id': 1, 'path': str(src_file)}]
|
|
|
|
with patch('nodupe.tools.commands.apply.FileRepository') as mock_repo_class:
|
|
mock_repo = MagicMock()
|
|
mock_repo.get_duplicate_files.return_value = duplicates
|
|
mock_repo_class.return_value = mock_repo
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 0
|
|
captured = capsys.readouterr()
|
|
assert "[DRY-RUN] Would copy:" in captured.out
|
|
|
|
def test_execute_apply_copy_actual(self, capsys, temp_dirs):
|
|
"""execute_apply() copy action actually copies files."""
|
|
tool = ApplyTool()
|
|
src_dir, dst_dir = temp_dirs
|
|
|
|
# Create source file
|
|
src_file = src_dir / "test.txt"
|
|
src_file.write_text("test content")
|
|
|
|
args = MagicMock()
|
|
args.action = 'copy'
|
|
args.dry_run = False
|
|
args.destination = str(dst_dir)
|
|
args.verbose = False
|
|
args.container = MagicMock()
|
|
args.container.get_service.return_value = MagicMock()
|
|
delattr(args, 'input')
|
|
|
|
duplicates = [{'id': 1, 'path': str(src_file)}]
|
|
|
|
with patch('nodupe.tools.commands.apply.FileRepository') as mock_repo_class:
|
|
with patch('nodupe.tools.commands.apply.Filesystem') as mock_fs:
|
|
mock_repo = MagicMock()
|
|
mock_repo.get_duplicate_files.return_value = duplicates
|
|
mock_repo_class.return_value = mock_repo
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 0
|
|
captured = capsys.readouterr()
|
|
assert "[COPIED]" in captured.out
|
|
mock_fs.copy_file.assert_called()
|
|
mock_fs.ensure_directory.assert_called()
|
|
|
|
|
|
class TestApplyToolExecuteApplyEdgeCases:
|
|
"""Test ApplyTool.execute_apply() edge cases."""
|
|
|
|
def test_execute_apply_file_not_found(self, capsys):
|
|
"""execute_apply() handles file not found during processing."""
|
|
tool = ApplyTool()
|
|
args = MagicMock()
|
|
args.action = 'delete'
|
|
args.dry_run = False
|
|
args.verbose = False
|
|
args.container = MagicMock()
|
|
args.container.get_service.return_value = MagicMock()
|
|
delattr(args, 'input')
|
|
|
|
duplicates = [{'id': 1, 'path': '/nonexistent/file.txt'}]
|
|
|
|
with patch('nodupe.tools.commands.apply.FileRepository') as mock_repo_class:
|
|
mock_repo = MagicMock()
|
|
mock_repo.get_duplicate_files.return_value = duplicates
|
|
mock_repo_class.return_value = mock_repo
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 0 # Should continue processing other files
|
|
captured = capsys.readouterr()
|
|
assert "[WARN] File not found:" in captured.out
|
|
|
|
def test_execute_apply_processing_error(self, capsys):
|
|
"""execute_apply() handles processing errors gracefully."""
|
|
tool = ApplyTool()
|
|
args = MagicMock()
|
|
args.action = 'delete'
|
|
args.dry_run = False
|
|
args.verbose = False
|
|
args.container = MagicMock()
|
|
args.container.get_service.return_value = MagicMock()
|
|
delattr(args, 'input')
|
|
|
|
# Create a temp file that exists so the error happens during processing
|
|
import tempfile
|
|
temp_dir = tempfile.mkdtemp()
|
|
temp_file = Path(temp_dir) / "test.txt"
|
|
temp_file.write_text("test")
|
|
|
|
try:
|
|
duplicates = [{'id': 1, 'path': str(temp_file)}]
|
|
|
|
with patch('nodupe.tools.commands.apply.FileRepository') as mock_repo_class:
|
|
with patch('nodupe.tools.commands.apply.Filesystem') as mock_fs:
|
|
mock_fs.remove_file.side_effect = Exception("Test error")
|
|
mock_repo = MagicMock()
|
|
mock_repo.get_duplicate_files.return_value = duplicates
|
|
mock_repo_class.return_value = mock_repo
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 0 # Should continue processing
|
|
captured = capsys.readouterr()
|
|
assert "[ERROR] Failed to process" in captured.out
|
|
finally:
|
|
import shutil
|
|
shutil.rmtree(temp_dir)
|
|
|
|
def test_execute_apply_general_exception(self, capsys):
|
|
"""execute_apply() handles general exceptions."""
|
|
tool = ApplyTool()
|
|
args = MagicMock()
|
|
args.action = 'list'
|
|
args.verbose = True
|
|
args.container = MagicMock()
|
|
args.container.get_service.side_effect = Exception("Test exception")
|
|
delattr(args, 'input')
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 1
|
|
captured = capsys.readouterr()
|
|
assert "[TOOL ERROR] Apply failed:" in captured.out
|
|
|
|
def test_execute_apply_verbose_exception_traceback(self, capsys):
|
|
"""execute_apply() prints traceback when verbose is True."""
|
|
tool = ApplyTool()
|
|
args = MagicMock()
|
|
args.action = 'list'
|
|
args.verbose = True
|
|
args.container = MagicMock()
|
|
args.container.get_service.side_effect = Exception("Test exception")
|
|
delattr(args, 'input')
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 1
|
|
captured = capsys.readouterr()
|
|
assert "Traceback" in captured.out or "Traceback" in captured.err or "[TOOL ERROR]" in captured.out
|
|
|
|
|
|
class TestApplyToolExecuteApplyMoveEdgeCases:
|
|
"""Test ApplyTool.execute_apply() move action edge cases."""
|
|
|
|
@pytest.fixture
|
|
def temp_dirs(self):
|
|
"""Create temporary directories for testing."""
|
|
src_dir = Path(tempfile.mkdtemp())
|
|
dst_dir = Path(tempfile.mkdtemp())
|
|
yield src_dir, dst_dir
|
|
shutil.rmtree(src_dir)
|
|
shutil.rmtree(dst_dir)
|
|
|
|
def test_execute_apply_move_collision_handling(self, capsys, temp_dirs):
|
|
"""execute_apply() move action handles destination collision."""
|
|
tool = ApplyTool()
|
|
src_dir, dst_dir = temp_dirs
|
|
|
|
# Create source file
|
|
src_file = src_dir / "test.txt"
|
|
src_file.write_text("test content")
|
|
|
|
# Create destination file with same name (collision)
|
|
dst_file = dst_dir / "test.txt"
|
|
dst_file.write_text("existing content")
|
|
|
|
args = MagicMock()
|
|
args.action = 'move'
|
|
args.dry_run = False
|
|
args.destination = str(dst_dir)
|
|
args.verbose = False
|
|
args.container = MagicMock()
|
|
args.container.get_service.return_value = MagicMock()
|
|
delattr(args, 'input')
|
|
|
|
duplicates = [{'id': 1, 'path': str(src_file)}]
|
|
|
|
with patch('nodupe.tools.commands.apply.FileRepository') as mock_repo_class:
|
|
with patch('nodupe.tools.commands.apply.Filesystem') as mock_fs:
|
|
mock_repo = MagicMock()
|
|
mock_repo.get_duplicate_files.return_value = duplicates
|
|
mock_repo_class.return_value = mock_repo
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 0
|
|
captured = capsys.readouterr()
|
|
assert "[MOVED]" in captured.out
|
|
mock_fs.move_file.assert_called()
|
|
|
|
def test_execute_apply_copy_collision_handling(self, capsys, temp_dirs):
|
|
"""execute_apply() copy action handles destination collision."""
|
|
tool = ApplyTool()
|
|
src_dir, dst_dir = temp_dirs
|
|
|
|
# Create source file
|
|
src_file = src_dir / "test.txt"
|
|
src_file.write_text("test content")
|
|
|
|
# Create destination file with same name (collision)
|
|
dst_file = dst_dir / "test.txt"
|
|
dst_file.write_text("existing content")
|
|
|
|
args = MagicMock()
|
|
args.action = 'copy'
|
|
args.dry_run = False
|
|
args.destination = str(dst_dir)
|
|
args.verbose = False
|
|
args.container = MagicMock()
|
|
args.container.get_service.return_value = MagicMock()
|
|
delattr(args, 'input')
|
|
|
|
duplicates = [{'id': 1, 'path': str(src_file)}]
|
|
|
|
with patch('nodupe.tools.commands.apply.FileRepository') as mock_repo_class:
|
|
with patch('nodupe.tools.commands.apply.Filesystem') as mock_fs:
|
|
mock_repo = MagicMock()
|
|
mock_repo.get_duplicate_files.return_value = duplicates
|
|
mock_repo_class.return_value = mock_repo
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 0
|
|
captured = capsys.readouterr()
|
|
assert "[COPIED]" in captured.out
|
|
mock_fs.copy_file.assert_called()
|
|
|
|
|
|
class TestApplyToolExecuteApplyDryRun:
|
|
"""Test ApplyTool.execute_apply() dry run messages."""
|
|
|
|
@pytest.fixture
|
|
def temp_dirs(self):
|
|
"""Create temporary directories for testing."""
|
|
src_dir = Path(tempfile.mkdtemp())
|
|
dst_dir = Path(tempfile.mkdtemp())
|
|
yield src_dir, dst_dir
|
|
shutil.rmtree(src_dir)
|
|
shutil.rmtree(dst_dir)
|
|
|
|
def test_execute_apply_dry_run_complete_message(self, capsys, temp_dirs):
|
|
"""execute_apply() prints dry run complete message."""
|
|
tool = ApplyTool()
|
|
src_dir, dst_dir = temp_dirs
|
|
|
|
# Create source file
|
|
src_file = src_dir / "test.txt"
|
|
src_file.write_text("test content")
|
|
|
|
args = MagicMock()
|
|
args.action = 'delete'
|
|
args.dry_run = True
|
|
args.verbose = False
|
|
args.container = MagicMock()
|
|
args.container.get_service.return_value = MagicMock()
|
|
delattr(args, 'input')
|
|
|
|
duplicates = [{'id': 1, 'path': str(src_file)}]
|
|
|
|
with patch('nodupe.tools.commands.apply.FileRepository') as mock_repo_class:
|
|
mock_repo = MagicMock()
|
|
mock_repo.get_duplicate_files.return_value = duplicates
|
|
mock_repo_class.return_value = mock_repo
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 0
|
|
captured = capsys.readouterr()
|
|
assert "Dry run complete" in captured.out
|
|
|
|
|
|
class TestApplyToolModuleLevelEdgeCases:
|
|
"""Test ApplyTool module-level edge cases."""
|
|
|
|
def test_api_methods_property(self):
|
|
"""api_methods property returns empty list."""
|
|
tool = ApplyTool()
|
|
assert tool.api_methods == {}
|
|
|
|
def test_describe_usage(self):
|
|
"""describe_usage() returns usage description."""
|
|
tool = ApplyTool()
|
|
usage = tool.describe_usage()
|
|
assert "Apply file management" in usage
|
|
|
|
def test_run_standalone(self):
|
|
"""run_standalone() calls execute_apply."""
|
|
tool = ApplyTool()
|
|
args = MagicMock()
|
|
args.action = 'list'
|
|
args.container = MagicMock()
|
|
args.container.get_service.return_value = MagicMock()
|
|
delattr(args, 'input')
|
|
|
|
with patch('nodupe.tools.commands.apply.FileRepository') as mock_repo_class:
|
|
mock_repo = MagicMock()
|
|
mock_repo.get_duplicate_files.return_value = []
|
|
mock_repo_class.return_value = mock_repo
|
|
|
|
result = tool.run_standalone(args)
|
|
assert result == 0
|
|
|
|
|
|
class TestApplyToolRegisterTool:
|
|
"""Test register_tool() function."""
|
|
|
|
def test_register_tool_returns_apply_tool(self):
|
|
"""register_tool() returns an ApplyTool instance."""
|
|
tool = register_tool()
|
|
assert isinstance(tool, ApplyTool)
|
|
|
|
|
|
class TestApplyToolModuleLevel:
|
|
"""Test module-level exports."""
|
|
|
|
def test_apply_tool_instance_exists(self):
|
|
"""apply_tool module-level instance exists."""
|
|
from nodupe.tools.commands import apply
|
|
assert apply.apply_tool is not None
|
|
assert isinstance(apply.apply_tool, ApplyTool)
|
|
|
|
def test_register_tool_function_exists(self):
|
|
"""register_tool function is exported."""
|
|
from nodupe.tools.commands import apply
|
|
assert callable(apply.register_tool)
|
|
|
|
def test_all_export(self):
|
|
"""__all__ contains expected exports."""
|
|
from nodupe.tools.commands import apply
|
|
assert 'apply_tool' in apply.__all__
|
|
assert 'register_tool' in apply.__all__
|
|
|
|
|
|
class TestApplyToolCoverageCompletion:
|
|
"""Additional tests to achieve 100% coverage."""
|
|
|
|
@pytest.fixture
|
|
def temp_dirs(self):
|
|
"""Create temporary directories for testing."""
|
|
src_dir = Path(tempfile.mkdtemp())
|
|
dst_dir = Path(tempfile.mkdtemp())
|
|
# Create a file in dst_dir to cause collision
|
|
collision_file = dst_dir / "test.txt"
|
|
collision_file.write_text("existing content")
|
|
yield src_dir, dst_dir
|
|
shutil.rmtree(src_dir)
|
|
shutil.rmtree(dst_dir)
|
|
|
|
def test_execute_apply_move_collision_with_existing_file(self, capsys, temp_dirs):
|
|
"""execute_apply() move action handles collision with existing file."""
|
|
tool = ApplyTool()
|
|
src_dir, dst_dir = temp_dirs
|
|
|
|
# Create source file
|
|
src_file = src_dir / "test.txt"
|
|
src_file.write_text("test content")
|
|
|
|
args = MagicMock()
|
|
args.action = 'move'
|
|
args.dry_run = False
|
|
args.destination = str(dst_dir)
|
|
args.verbose = False
|
|
args.container = MagicMock()
|
|
args.container.get_service.return_value = MagicMock()
|
|
delattr(args, 'input')
|
|
|
|
duplicates = [{'id': 1, 'path': str(src_file)}]
|
|
|
|
with patch('nodupe.tools.commands.apply.FileRepository') as mock_repo_class:
|
|
with patch('nodupe.tools.commands.apply.Filesystem') as mock_fs:
|
|
mock_repo = MagicMock()
|
|
mock_repo.get_duplicate_files.return_value = duplicates
|
|
mock_repo_class.return_value = mock_repo
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 0
|
|
captured = capsys.readouterr()
|
|
assert "[MOVED]" in captured.out
|
|
mock_fs.move_file.assert_called()
|
|
|
|
def test_execute_apply_copy_collision_with_existing_file(self, capsys, temp_dirs):
|
|
"""execute_apply() copy action handles collision with existing file."""
|
|
tool = ApplyTool()
|
|
src_dir, dst_dir = temp_dirs
|
|
|
|
# Create source file
|
|
src_file = src_dir / "test.txt"
|
|
src_file.write_text("test content")
|
|
|
|
args = MagicMock()
|
|
args.action = 'copy'
|
|
args.dry_run = False
|
|
args.destination = str(dst_dir)
|
|
args.verbose = False
|
|
args.container = MagicMock()
|
|
args.container.get_service.return_value = MagicMock()
|
|
delattr(args, 'input')
|
|
|
|
duplicates = [{'id': 1, 'path': str(src_file)}]
|
|
|
|
with patch('nodupe.tools.commands.apply.FileRepository') as mock_repo_class:
|
|
with patch('nodupe.tools.commands.apply.Filesystem') as mock_fs:
|
|
mock_repo = MagicMock()
|
|
mock_repo.get_duplicate_files.return_value = duplicates
|
|
mock_repo_class.return_value = mock_repo
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 0
|
|
captured = capsys.readouterr()
|
|
assert "[COPIED]" in captured.out
|
|
mock_fs.copy_file.assert_called()
|
|
|
|
def test_execute_apply_destination_validation_true_path(self, capsys):
|
|
"""execute_apply() validates destination path exists for move."""
|
|
tool = ApplyTool()
|
|
args = MagicMock()
|
|
args.action = 'move'
|
|
args.destination = '/tmp' # This should exist
|
|
args.container = MagicMock()
|
|
args.container.get_service.return_value = MagicMock()
|
|
delattr(args, 'input')
|
|
|
|
# Mock duplicates to be empty so we don't actually process
|
|
with patch('nodupe.tools.commands.apply.FileRepository') as mock_repo_class:
|
|
mock_repo = MagicMock()
|
|
mock_repo.get_duplicate_files.return_value = []
|
|
mock_repo_class.return_value = mock_repo
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
# Should pass validation and continue
|
|
assert result == 0
|
|
captured = capsys.readouterr()
|
|
assert "No items marked as duplicates" in captured.out
|
|
|
|
def test_execute_apply_verbose_exception_handling(self, capsys):
|
|
"""execute_apply() prints traceback when verbose and exception occurs."""
|
|
tool = ApplyTool()
|
|
args = MagicMock()
|
|
args.action = 'list'
|
|
args.verbose = True
|
|
args.container = MagicMock()
|
|
# Make get_service raise an exception
|
|
args.container.get_service.side_effect = RuntimeError("Test runtime error")
|
|
delattr(args, 'input')
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 1
|
|
captured = capsys.readouterr()
|
|
assert "[TOOL ERROR] Apply failed:" in captured.out
|
|
|
|
def test_execute_apply_destination_exists_validation(self, capsys):
|
|
"""execute_apply() validates destination path that exists."""
|
|
tool = ApplyTool()
|
|
args = MagicMock()
|
|
args.action = 'move'
|
|
args.destination = '/tmp' # This exists
|
|
args.container = MagicMock()
|
|
args.container.get_service.return_value = MagicMock()
|
|
delattr(args, 'input')
|
|
|
|
# Mock FileRepository to return empty duplicates
|
|
with patch('nodupe.tools.commands.apply.FileRepository') as mock_repo_class:
|
|
mock_repo = MagicMock()
|
|
mock_repo.get_duplicate_files.return_value = []
|
|
mock_repo_class.return_value = mock_repo
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
# Should pass validation (destination exists) and continue
|
|
assert result == 0
|
|
captured = capsys.readouterr()
|
|
# Should not have "Target directory does not exist" error
|
|
assert "Target directory does not exist" not in captured.out
|
|
|
|
def test_execute_apply_exception_without_verbose(self, capsys):
|
|
"""execute_apply() handles exception without verbose traceback."""
|
|
tool = ApplyTool()
|
|
args = MagicMock()
|
|
args.action = 'list'
|
|
args.verbose = False # No verbose
|
|
args.container = MagicMock()
|
|
args.container.get_service.side_effect = Exception("Test exception")
|
|
delattr(args, 'input')
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 1
|
|
captured = capsys.readouterr()
|
|
assert "[TOOL ERROR] Apply failed:" in captured.out
|
|
# Should NOT have traceback when verbose is False
|
|
|
|
def test_execute_apply_destination_not_set_branch(self, capsys):
|
|
"""execute_apply() covers branch when destination is not set."""
|
|
tool = ApplyTool()
|
|
args = MagicMock()
|
|
args.action = 'move'
|
|
args.destination = None # Destination not set - covers False branch
|
|
args.verbose = False
|
|
args.container = MagicMock()
|
|
args.container.get_service.return_value = MagicMock()
|
|
delattr(args, 'input')
|
|
|
|
# This should fail validation before reaching the destination check
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 1
|
|
captured = capsys.readouterr()
|
|
assert "--destination is required" in captured.out
|
|
|
|
def test_execute_apply_list_action_branch(self, capsys):
|
|
"""execute_apply() covers branch for list action (not move/copy)."""
|
|
tool = ApplyTool()
|
|
args = MagicMock()
|
|
args.action = 'list' # List action - covers False branch of move/copy
|
|
args.verbose = False
|
|
args.container = MagicMock()
|
|
args.container.get_service.return_value = MagicMock()
|
|
delattr(args, 'input')
|
|
|
|
duplicates = [
|
|
{'id': 1, 'path': '/path/to/dup1.txt', 'duplicate_of': 100},
|
|
]
|
|
original = {'id': 100, 'path': '/path/to/original.txt'}
|
|
|
|
with patch('nodupe.tools.commands.apply.FileRepository') as mock_repo_class:
|
|
mock_repo = MagicMock()
|
|
mock_repo.get_duplicate_files.return_value = duplicates
|
|
mock_repo.get_file.return_value = original
|
|
mock_repo_class.return_value = mock_repo
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 0
|
|
captured = capsys.readouterr()
|
|
assert "Identified Duplicates:" in captured.out
|
|
|
|
def test_execute_apply_delete_action_branch(self, capsys):
|
|
"""execute_apply() covers branch for delete action (not copy)."""
|
|
tool = ApplyTool()
|
|
temp_dir = Path(tempfile.mkdtemp())
|
|
test_file = temp_dir / "test.txt"
|
|
test_file.write_text("test content")
|
|
|
|
try:
|
|
args = MagicMock()
|
|
args.action = 'delete' # Delete action - covers False branch of copy
|
|
args.dry_run = True
|
|
args.verbose = False
|
|
args.container = MagicMock()
|
|
args.container.get_service.return_value = MagicMock()
|
|
delattr(args, 'input')
|
|
|
|
duplicates = [{'id': 1, 'path': str(test_file)}]
|
|
|
|
with patch('nodupe.tools.commands.apply.FileRepository') as mock_repo_class:
|
|
mock_repo = MagicMock()
|
|
mock_repo.get_duplicate_files.return_value = duplicates
|
|
mock_repo_class.return_value = mock_repo
|
|
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 0
|
|
captured = capsys.readouterr()
|
|
assert "[DRY-RUN] Would delete:" in captured.out
|
|
finally:
|
|
shutil.rmtree(temp_dir)
|
|
|
|
def test_execute_apply_move_action_no_destination_continue(self, capsys):
|
|
"""execute_apply() covers continue branch when destination missing for move."""
|
|
tool = ApplyTool()
|
|
args = MagicMock()
|
|
args.action = 'move'
|
|
args.destination = None # No destination - triggers continue
|
|
args.verbose = False
|
|
args.container = MagicMock()
|
|
args.container.get_service.return_value = MagicMock()
|
|
delattr(args, 'input')
|
|
|
|
# This should fail validation before reaching the continue statement
|
|
result = tool.execute_apply(args)
|
|
|
|
assert result == 1
|
|
captured = capsys.readouterr()
|
|
assert "--destination is required" in captured.out
|
|
assert "Traceback" not in captured.out
|