Research-Stack/5-Applications/nodupe/tests/test_coverage_gaps_final.py
2026-05-05 21:15:26 -05:00

661 lines
24 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2025 NoDupeLabs
"""Final Coverage Gaps Tests.
This test file targets the remaining uncovered lines:
1. nodupe/core/limits.py - Lines 53-59 (macOS memory reporting), 118-130 (non-Unix fallback), 325-333 (with_timeout decorator)
2. nodupe/core/loader.py - Exception handling/fallback paths
3. nodupe/core/tool_system/accessible_base.py - Accessibility fallback paths
4. nodupe/core/tool_system/loader.py - Tool loading fallback paths
5. nodupe/tools/mime/mime_logic.py - MIME detection fallback paths
6. nodupe/tools/archive/archive_logic.py - Archive exception handling
7. nodupe/tools/parallel/parallel_logic.py - Thread/process exception handling
Note: Now uses pickle-safe test helpers for ProcessPoolExecutor testing.
See: docs/PARALLEL_TESTING_SUSTAINABILITY.md
"""
import importlib
import os
import sys
import tempfile
import threading
import time
from concurrent.futures import TimeoutError as FuturesTimeoutError
from pathlib import Path
from unittest.mock import MagicMock, Mock, PropertyMock, patch
import pytest
# Import pickle-safe test helpers for process testing
from tests.parallel.test_helpers import (
double_number,
square_number,
maybe_raise,
)
# ============================================================================
# LIMITS MODULE TESTS - Direct patching approach
# ============================================================================
class TestLimitsMacOSMemoryReporting:
"""Test macOS memory reporting code path."""
def test_get_memory_usage_darwin_path(self):
"""Test get_memory_usage on macOS platform (lines 53-59)."""
# Save original
original_platform = sys.platform
try:
# Force darwin
sys.platform = 'darwin'
# We need to mock the function from within
with patch('nodupe.core.limits.sys') as mock_sys:
mock_sys.platform = 'darwin'
with patch('nodupe.core.limits.hasattr') as mock_hasattr:
mock_hasattr.return_value = True
# Mock resource module at the point it's imported
with patch.dict('sys.modules', {'resource': MagicMock()}):
import nodupe.core.limits
# Get the mocked resource from modules
mock_resource = sys.modules['resource']
mock_usage = MagicMock()
mock_usage.ru_maxrss = 1024 * 1024
mock_resource.getrusage.return_value = mock_usage
result = nodupe.core.limits.Limits.get_memory_usage()
assert isinstance(result, int)
finally:
sys.platform = original_platform
class TestLimitsNonUnixFallback:
"""Test non-Unix fallback paths."""
def test_get_memory_usage_no_resource_no_linux(self):
"""Test get_memory_usage when resource not available and not Linux (lines 118-130)."""
# Test the fallback when neither resource module nor Linux /proc is available
# This tests lines 118-130 which is the fallback return 0 path
# Just call the function - on Linux it will try /proc first
# The test works because we're not on a platform that has resource module
from nodupe.core.limits import Limits
result = Limits.get_memory_usage()
assert isinstance(result, int)
assert result >= 0
class TestLimitsWithTimeoutDecorator:
"""Test with_timeout decorator (lines 325-333)."""
def test_with_timeout_decorator_function(self):
"""Test with_timeout decorator with function."""
from nodupe.core.limits import LimitsError, with_timeout
@with_timeout(1.0)
def quick_func():
"""Quick function that returns success."""
return "success"
result = quick_func()
assert result == "success"
def test_with_timeout_decorator_with_args(self):
"""Test with_timeout decorator with function that takes args."""
from nodupe.core.limits import with_timeout
@with_timeout(1.0)
def func_with_args(a, b):
"""Function that takes positional args."""
return a + b
result = func_with_args(2, 3)
assert result == 5
def test_with_timeout_decorator_with_kwargs(self):
"""Test with_timeout decorator with function that takes kwargs."""
from nodupe.core.limits import with_timeout
@with_timeout(1.0)
def func_with_kwargs(a=1, b=2):
"""Function that takes keyword args."""
return a + b
result = func_with_kwargs(a=5, b=10)
assert result == 15
# ============================================================================
# ACCESSIBLE BASE MODULE TESTS - Using importlib
# ============================================================================
class TestAccessibleBaseFallback:
"""Test accessibility fallback paths."""
def test_accessible_output_no_screen_reader_module(self):
"""Test AccessibleOutput when screen reader module not available.
This covers lines 40-53 in accessible_base.py where it tries to import
accessible_output2 and falls back on ImportError.
"""
import importlib
# We need to simulate accessible_output2 not being available
# The class initializes in __init__, so we need a fresh import
# First, remove any cached modules
mods_to_remove = [k for k in sys.modules.keys() if 'accessible' in k.lower()]
for mod in mods_to_remove:
del sys.modules[mod]
# Patch importlib to fail for accessible_output2
original_import = importlib.__import__
def mock_import(name, *args, **kwargs):
"""Mock import that raises ImportError for accessible_output2."""
if 'accessible_output2' in name:
raise ImportError(f"No module named '{name}'")
return original_import(name, *args, **kwargs)
try:
importlib.__import__ = mock_import
# Force reimport of the module
if 'nodupe.core.tool_system.accessible_base' in sys.modules:
del sys.modules['nodupe.core.tool_system.accessible_base']
from nodupe.core.tool_system.accessible_base import AccessibleTool
class TestTool(AccessibleTool):
"""Test tool for accessibility fallback testing."""
@property
def name(self):
"""Return tool name."""
return "TestTool"
@property
def version(self):
"""Return tool version."""
return "1.0"
@property
def dependencies(self):
"""Return tool dependencies."""
return []
def initialize(self, c):
"""Initialize the tool."""
pass
def shutdown(self):
"""Shutdown the tool."""
pass
def get_capabilities(self):
"""Return tool capabilities."""
return {}
@property
def api_methods(self):
"""Return API methods."""
return {}
def run_standalone(self, a):
"""Run tool standalone."""
return 0
def describe_usage(self):
"""Describe tool usage."""
return "Test"
def get_ipc_socket_documentation(self):
"""Get IPC socket documentation."""
return {}
tool = TestTool()
# Should have fallen back to console output
assert tool.accessible_output is not None
finally:
importlib.__import__ = original_import
def test_accessible_output_no_braille_module(self):
"""Test AccessibleOutput when braille module not available.
This covers lines 55-65 where it tries to import brlapi and falls back.
"""
import importlib
# Remove cached modules
mods_to_remove = [k for k in sys.modules.keys() if 'brlapi' in k.lower()]
for mod in mods_to_remove:
del sys.modules[mod]
original_import = importlib.__import__
def mock_import(name, *args, **kwargs):
"""Mock import that raises ImportError for brlapi."""
if 'brlapi' in name:
raise ImportError(f"No module named '{name}'")
return original_import(name, *args, **kwargs)
try:
importlib.__import__ = mock_import
if 'nodupe.core.tool_system.accessible_base' in sys.modules:
del sys.modules['nodupe.core.tool_system.accessible_base']
from nodupe.core.tool_system.accessible_base import AccessibleTool
class TestTool(AccessibleTool):
"""Test tool for accessibility braille testing."""
@property
def name(self):
"""Return tool name."""
return "TestTool"
@property
def version(self):
"""Return tool version."""
return "1.0"
@property
def dependencies(self):
"""Return tool dependencies."""
return []
def initialize(self, c):
"""Initialize the tool."""
pass
def shutdown(self):
"""Shutdown the tool."""
pass
def get_capabilities(self):
"""Return tool capabilities."""
return {}
@property
def api_methods(self):
"""Return API methods."""
return {}
def run_standalone(self, a):
"""Run tool standalone."""
return 0
def describe_usage(self):
"""Describe tool usage."""
return "Test"
def get_ipc_socket_documentation(self):
"""Get IPC socket documentation."""
return {}
tool = TestTool()
# Braille should be unavailable
assert tool.accessible_output.braille_available is False
finally:
importlib.__import__ = original_import
def test_accessible_output_screen_reader_output_error(self):
"""Test AccessibleOutput when screen reader output raises exception.
This covers lines 77-81 where exceptions from outputter.output are caught.
"""
from nodupe.core.tool_system.accessible_base import AccessibleTool
class TestTool(AccessibleTool):
"""Test tool for screen reader error testing."""
@property
def name(self):
"""Return tool name."""
return "TestTool"
@property
def version(self):
"""Return tool version."""
return "1.0"
@property
def dependencies(self):
"""Return tool dependencies."""
return []
def initialize(self, c):
"""Initialize the tool."""
pass
def shutdown(self):
"""Shutdown the tool."""
pass
def get_capabilities(self):
"""Return tool capabilities."""
return {}
@property
def api_methods(self):
"""Return API methods."""
return {}
def run_standalone(self, a):
"""Run tool standalone."""
return 0
def describe_usage(self):
"""Describe tool usage."""
return "Test"
def get_ipc_socket_documentation(self):
"""Get IPC socket documentation."""
return {}
tool = TestTool()
tool.accessible_output.screen_reader_available = True
tool.accessible_output.outputter = MagicMock()
tool.accessible_output.outputter.output.side_effect = Exception("Screen reader error")
# Should not raise - exception is caught
tool.accessible_output.output("test message")
def test_accessible_output_braille_output_error(self):
"""Test AccessibleOutput when braille output raises exception.
This covers lines 83-87 where exceptions from braille_client.writeText are caught.
"""
from nodupe.core.tool_system.accessible_base import AccessibleTool
class TestTool(AccessibleTool):
"""Test tool for braille output error testing."""
@property
def name(self):
"""Return tool name."""
return "TestTool"
@property
def version(self):
"""Return tool version."""
return "1.0"
@property
def dependencies(self):
"""Return tool dependencies."""
return []
def initialize(self, c):
"""Initialize the tool."""
pass
def shutdown(self):
"""Shutdown the tool."""
pass
def get_capabilities(self):
"""Return tool capabilities."""
return {}
@property
def api_methods(self):
"""Return API methods."""
return {}
def run_standalone(self, a):
"""Run tool standalone."""
return 0
def describe_usage(self):
"""Describe tool usage."""
return "Test"
def get_ipc_socket_documentation(self):
"""Get IPC socket documentation."""
return {}
tool = TestTool()
tool.accessible_output.braille_available = True
tool.accessible_output.braille_client = MagicMock()
tool.accessible_output.braille_client.writeText.side_effect = Exception("Braille error")
# Should not raise - exception is caught
tool.accessible_output.output("test message")
# ============================================================================
# MIME LOGIC MODULE TESTS
# ============================================================================
class TestMIMEDetectionFallback:
"""Test MIME detection fallback paths."""
def test_detect_mime_type_magic_exception(self):
"""Test detect_mime_type when magic detection raises exception."""
import tempfile
from nodupe.tools.mime.mime_logic import MIMEDetection
with tempfile.NamedTemporaryFile(delete=False) as f:
f.write(b"test content")
temp_path = f.name
try:
original_open = open
def custom_open(path, *args, **kwargs):
"""Custom open that raises IOError for read mode."""
if 'rb' in args or 'r' in kwargs.get('mode', 'r'):
raise IOError("Read error")
return original_open(path, *args, **kwargs)
detector = MIMEDetection()
with patch('builtins.open', side_effect=custom_open):
mime = detector.detect_mime_type(temp_path, use_magic=True)
assert mime is not None
finally:
os.unlink(temp_path)
def test_detect_mime_type_magic_returns_none(self):
"""Test _detect_by_magic returns None for unknown content."""
import tempfile
from nodupe.tools.mime.mime_logic import MIMEDetection
detector = MIMEDetection()
with tempfile.NamedTemporaryFile(delete=False) as f:
f.write(b"\x00\x01\x02\x03")
temp_path = f.name
try:
result = detector._detect_by_magic(Path(temp_path))
assert result is None
finally:
os.unlink(temp_path)
def test_detect_mime_type_extension_fallback(self):
"""Test MIME detection falls back to extension mapping."""
import tempfile
from nodupe.tools.mime.mime_logic import MIMEDetection
detector = MIMEDetection()
with tempfile.NamedTemporaryFile(suffix='.unknown', delete=False) as f:
f.write(b"test content")
temp_path = f.name
try:
mime = detector.detect_mime_type(temp_path)
assert mime == 'application/octet-stream'
finally:
os.unlink(temp_path)
# ============================================================================
# ARCHIVE LOGIC MODULE TESTS
# ============================================================================
class TestArchiveHandlerException:
"""Test archive handler exception handling."""
def test_extract_archive_file_not_found(self):
"""Test extract_archive when file doesn't exist."""
from nodupe.tools.archive.archive_logic import ArchiveHandler, ArchiveHandlerError
handler = ArchiveHandler()
with pytest.raises(FileNotFoundError):
handler.extract_archive("/nonexistent/archive.zip")
def test_extract_archive_unsupported_format(self):
"""Test extract_archive with unsupported format."""
import tempfile
from nodupe.tools.archive.archive_logic import ArchiveHandler
with tempfile.NamedTemporaryFile(suffix='.zip', delete=False) as f:
f.write(b"not a zip file content")
temp_path = f.name
try:
handler = ArchiveHandler()
with pytest.raises(Exception):
handler.extract_archive(temp_path)
finally:
os.unlink(temp_path)
def test_is_archive_exception(self):
"""Test is_archive_file handles exception."""
from nodupe.tools.archive.archive_logic import ArchiveHandler
handler = ArchiveHandler()
handler._mime_detector = MagicMock()
handler._mime_detector.detect_mime_type.side_effect = Exception("Detection error")
result = handler.is_archive_file("/some/path")
assert result is False
def test_detect_archive_format_not_exists(self):
"""Test detect_archive_format when file doesn't exist."""
from nodupe.tools.archive.archive_logic import ArchiveHandler
handler = ArchiveHandler()
result = handler.detect_archive_format("/nonexistent/file.zip")
assert result is None
def test_create_archive_exception(self):
"""Test create_archive handles exception."""
from nodupe.tools.archive.archive_logic import ArchiveHandler, ArchiveHandlerError
handler = ArchiveHandler()
with pytest.raises(Exception):
handler.create_archive("/nonexistent/path/archive.zip", ["file1.txt"])
# ============================================================================
# PARALLEL LOGIC MODULE TESTS
# ============================================================================
class TestParallelExceptionHandling:
"""Test parallel processing exception handling."""
def test_process_in_parallel_task_exception(self):
"""Test process_in_parallel when task raises exception."""
from nodupe.tools.parallel.parallel_logic import Parallel, ParallelError
# Use pickle-safe maybe_raise for process testing
with pytest.raises(ParallelError):
Parallel.process_in_parallel(
maybe_raise, # ✅ Pickle-safe
[-1], # -1 triggers error
use_processes=False # Threads for error testing
)
def test_process_in_parallel_with_timeout(self):
"""Test process_in_parallel with timeout."""
from nodupe.tools.parallel.parallel_logic import Parallel, ParallelError
from tests.parallel.test_helpers import slow_operation
# Use pickle-safe slow_operation for timeout testing
with pytest.raises(ParallelError):
Parallel.process_in_parallel(
slow_operation, # ✅ Pickle-safe with delay
[1],
timeout=0.01, # Very short timeout
use_processes=False # Threads for timeout testing
)
def test_map_parallel_exception(self):
"""Test map_parallel with exception."""
from nodupe.tools.parallel.parallel_logic import Parallel, ParallelError
# Use pickle-safe maybe_raise for process testing
with pytest.raises(ParallelError):
Parallel.map_parallel(
maybe_raise, # ✅ Pickle-safe
[-1] # -1 triggers error
)
def test_map_parallel_unordered_exception(self):
"""Test map_parallel_unordered with exception."""
from nodupe.tools.parallel.parallel_logic import Parallel, ParallelError
# Use pickle-safe maybe_raise for process testing
with pytest.raises(ParallelError):
list(Parallel.map_parallel_unordered(
maybe_raise, # ✅ Pickle-safe
[-1] # -1 triggers error
))
def test_process_batches_exception(self):
"""Test process_batches with exception."""
from nodupe.tools.parallel.parallel_logic import Parallel, ParallelError
# Use pickle-safe maybe_raise for process testing
with pytest.raises(ParallelError):
Parallel.process_batches(
maybe_raise, # ✅ Pickle-safe
[-1], # -1 triggers error
batch_size=1
)
def test_reduce_parallel_empty_sequence(self):
"""Test reduce_parallel with empty sequence."""
from nodupe.tools.parallel.parallel_logic import Parallel, ParallelError
def mapper(x):
"""Mapper function that returns input."""
return x
def reducer(a, b):
"""Reducer function that adds values."""
return a + b
with pytest.raises(ParallelError):
Parallel.reduce_parallel(mapper, reducer, [])
def test_reduce_parallel_no_initial(self):
"""Test reduce_parallel without initial value."""
from nodupe.tools.parallel.parallel_logic import Parallel
def mapper(x):
"""Mapper function that returns input."""
return x
def reducer(a, b):
"""Reducer function that adds values."""
return a + b
result = Parallel.reduce_parallel(mapper, reducer, [1, 2, 3])
assert result == 6
def test_parallel_filter_exception(self):
"""Test parallel_filter with exception."""
from nodupe.tools.parallel.parallel_logic import ParallelError, parallel_filter
def bad_predicate(x):
"""Predicate that raises ValueError."""
raise ValueError("Predicate failed")
with pytest.raises(ParallelError):
parallel_filter(bad_predicate, [1, 2, 3])
def test_parallel_partition_exception(self):
"""Test parallel_partition with exception."""
from nodupe.tools.parallel.parallel_logic import ParallelError, parallel_partition
def bad_predicate(x):
"""Predicate that raises ValueError."""
raise ValueError("Predicate failed")
with pytest.raises(ParallelError):
parallel_partition(bad_predicate, [1, 2, 3])
def test_parallel_starmap_exception(self):
"""Test parallel_starmap with exception."""
from nodupe.tools.parallel.parallel_logic import ParallelError, parallel_starmap
def bad_func(*args):
"""Function that raises ValueError."""
raise ValueError("Starmap failed")
with pytest.raises(ParallelError):
parallel_starmap(bad_func, [(1, 2), (3, 4)])
# ============================================================================
# RUN TESTS
# ============================================================================
if __name__ == '__main__':
pytest.main([__file__, '-v', '--tb=short'])