mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
661 lines
24 KiB
Python
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'])
|